1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LlmMessage {
9 pub role: MessageRole,
11
12 pub content: MessageContent,
14
15 pub metadata: Option<HashMap<String, serde_json::Value>>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum MessageRole {
23 System,
25
26 User,
28
29 Assistant,
31
32 Tool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum MessageContent {
40 Text(String),
42
43 MultiModal(Vec<ContentBlock>),
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "snake_case")]
50pub enum ContentBlock {
51 Text { text: String },
53
54 Image {
56 data: String,
58 mime_type: String,
60 },
61
62 ToolUse {
64 id: String,
66 name: String,
68 input: serde_json::Value,
70 },
71
72 ToolResult {
74 tool_use_id: String,
76 is_error: Option<bool>,
78 content: String,
80 },
81}
82
83impl LlmMessage {
84 pub fn system<S: Into<String>>(content: S) -> Self {
86 Self {
87 role: MessageRole::System,
88 content: MessageContent::Text(content.into()),
89 metadata: None,
90 }
91 }
92
93 pub fn user<S: Into<String>>(content: S) -> Self {
95 Self {
96 role: MessageRole::User,
97 content: MessageContent::Text(content.into()),
98 metadata: None,
99 }
100 }
101
102 pub fn assistant<S: Into<String>>(content: S) -> Self {
104 Self {
105 role: MessageRole::Assistant,
106 content: MessageContent::Text(content.into()),
107 metadata: None,
108 }
109 }
110
111 pub fn tool<S: Into<String>>(content: S) -> Self {
113 Self {
114 role: MessageRole::Tool,
115 content: MessageContent::Text(content.into()),
116 metadata: None,
117 }
118 }
119
120 pub fn get_text(&self) -> Option<String> {
122 match &self.content {
123 MessageContent::Text(text) => Some(text.clone()),
124 MessageContent::MultiModal(blocks) => {
125 let mut text_parts = Vec::new();
126 for block in blocks {
127 if let ContentBlock::Text { text } = block {
128 text_parts.push(text.clone());
129 }
130 }
131 if text_parts.is_empty() {
132 None
133 } else {
134 Some(text_parts.join("\n"))
135 }
136 }
137 }
138 }
139
140 pub fn has_tool_use(&self) -> bool {
142 match &self.content {
143 MessageContent::Text(_) => false,
144 MessageContent::MultiModal(blocks) => blocks
145 .iter()
146 .any(|block| matches!(block, ContentBlock::ToolUse { .. })),
147 }
148 }
149
150 pub fn get_tool_uses(&self) -> Vec<&ContentBlock> {
152 match &self.content {
153 MessageContent::Text(_) => Vec::new(),
154 MessageContent::MultiModal(blocks) => blocks
155 .iter()
156 .filter(|block| matches!(block, ContentBlock::ToolUse { .. }))
157 .collect(),
158 }
159 }
160}
161
162impl From<String> for MessageContent {
163 fn from(text: String) -> Self {
164 MessageContent::Text(text)
165 }
166}
167
168impl From<&str> for MessageContent {
169 fn from(text: &str) -> Self {
170 MessageContent::Text(text.to_string())
171 }
172}