1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
5pub enum MessageRole {
6 System,
7 User,
8 Assistant,
9 Tool,
10}
11
12impl MessageRole {
13 pub fn as_str(&self) -> &'static str {
14 match self {
15 MessageRole::System => "system",
16 MessageRole::User => "user",
17 MessageRole::Assistant => "assistant",
18 MessageRole::Tool => "tool",
19 }
20 }
21
22 pub fn as_api_role(&self) -> &'static str {
23 self.as_str()
24 }
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct Message {
29 pub role: MessageRole,
30 pub content: String,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub enum ChatMessage {
35 System {
36 content: String,
37 #[serde(default, skip_serializing)]
39 ephemeral: bool,
40 },
41 User {
42 content: String,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 images: Vec<ImageAttachment>,
45 #[serde(default, skip_serializing)]
47 ephemeral: bool,
48 },
49 Assistant {
50 content: Option<String>,
51 reasoning_content: Option<String>,
52 tool_calls: Option<Vec<ToolCallMessage>>,
53 },
54 Tool {
55 tool_call_id: String,
56 content: String,
57 },
58 Custom {
65 role: String,
66 data: serde_json::Value,
67 },
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize)]
71pub struct ToolCallMessage {
72 pub id: String,
73 pub name: String,
74 pub arguments: String,
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub enum ImageAttachment {
79 Url {
80 url: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 detail: Option<ImageDetail>,
83 },
84 Base64 {
85 data: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 media_type: Option<String>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 detail: Option<ImageDetail>,
90 },
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub enum ImageDetail {
95 Low,
96 High,
97 Auto,
98}
99
100impl ChatMessage {
101 pub fn system(content: impl Into<String>) -> Self {
102 Self::System {
103 content: content.into(),
104 ephemeral: false,
105 }
106 }
107
108 pub fn system_ephemeral(content: impl Into<String>) -> Self {
110 Self::System {
111 content: content.into(),
112 ephemeral: true,
113 }
114 }
115
116 pub fn user(content: impl Into<String>) -> Self {
117 Self::User {
118 content: content.into(),
119 images: Vec::new(),
120 ephemeral: false,
121 }
122 }
123
124 pub fn user_ephemeral(content: impl Into<String>) -> Self {
126 Self::User {
127 content: content.into(),
128 images: Vec::new(),
129 ephemeral: true,
130 }
131 }
132
133 pub fn user_with_images(content: impl Into<String>, images: Vec<ImageAttachment>) -> Self {
134 Self::User {
135 content: content.into(),
136 images,
137 ephemeral: false,
138 }
139 }
140
141 pub fn is_ephemeral(&self) -> bool {
143 match self {
144 Self::System { ephemeral, .. } => *ephemeral,
145 Self::User { ephemeral, .. } => *ephemeral,
146 _ => false,
147 }
148 }
149
150 pub fn assistant(content: impl Into<String>) -> Self {
151 Self::Assistant {
152 content: Some(content.into()),
153 reasoning_content: None,
154 tool_calls: None,
155 }
156 }
157
158 pub fn assistant_with_reasoning(
159 content: impl Into<String>,
160 reasoning: impl Into<String>,
161 ) -> Self {
162 Self::Assistant {
163 content: Some(content.into()),
164 reasoning_content: Some(reasoning.into()),
165 tool_calls: None,
166 }
167 }
168
169 pub fn assistant_tool_call(
170 tool_call_id: impl Into<String>,
171 tool_name: impl Into<String>,
172 arguments: impl Into<String>,
173 ) -> Self {
174 Self::Assistant {
175 content: None,
176 reasoning_content: None,
177 tool_calls: Some(vec![ToolCallMessage {
178 id: tool_call_id.into(),
179 name: tool_name.into(),
180 arguments: arguments.into(),
181 }]),
182 }
183 }
184
185 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
186 Self::Tool {
187 tool_call_id: tool_call_id.into(),
188 content: content.into(),
189 }
190 }
191}
192
193impl From<&ChatMessage> for Message {
194 fn from(cm: &ChatMessage) -> Self {
195 match cm {
196 ChatMessage::System { content, .. } => Message {
197 role: MessageRole::System,
198 content: content.clone(),
199 },
200 ChatMessage::User { content, .. } => Message {
201 role: MessageRole::User,
202 content: content.clone(),
203 },
204 ChatMessage::Assistant { content, .. } => Message {
205 role: MessageRole::Assistant,
206 content: content.clone().unwrap_or_default(),
207 },
208 ChatMessage::Tool { content, .. } => Message {
209 role: MessageRole::Tool,
210 content: content.clone(),
211 },
212 ChatMessage::Custom { role: _, data } => Message {
213 role: MessageRole::User,
214 content: data.to_string(),
215 },
216 }
217 }
218}
219
220pub type ConvertToLlmFn = Arc<dyn Fn(&[ChatMessage]) -> Vec<ChatMessage> + Send + Sync>;
226
227pub fn default_convert_to_llm(messages: &[ChatMessage]) -> Vec<ChatMessage> {
229 messages
230 .iter()
231 .filter(|m| !matches!(m, ChatMessage::Custom { .. }))
232 .cloned()
233 .collect()
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_default_convert_to_llm_filters_custom() {
242 let messages = vec![
243 ChatMessage::system("You are a helpful assistant."),
244 ChatMessage::user("Hello"),
245 ChatMessage::Custom {
246 role: "artifact".to_string(),
247 data: serde_json::json!({"id": "abc123"}),
248 },
249 ChatMessage::assistant("Hi there!"),
250 ChatMessage::Custom {
251 role: "notification".to_string(),
252 data: serde_json::json!({"level": "info"}),
253 },
254 ChatMessage::tool("call_1", "result"),
255 ];
256
257 let filtered = default_convert_to_llm(&messages);
258
259 assert_eq!(filtered.len(), 4);
261 assert!(matches!(filtered[0], ChatMessage::System { .. }));
262 assert!(matches!(filtered[1], ChatMessage::User { .. }));
263 assert!(matches!(filtered[2], ChatMessage::Assistant { .. }));
264 assert!(matches!(filtered[3], ChatMessage::Tool { .. }));
265 }
266
267 #[test]
268 fn test_default_convert_to_llm_no_custom() {
269 let messages = vec![
270 ChatMessage::system("sys"),
271 ChatMessage::user("usr"),
272 ChatMessage::assistant("asst"),
273 ];
274
275 let filtered = default_convert_to_llm(&messages);
276 assert_eq!(filtered.len(), 3);
277 }
278
279 #[test]
280 fn test_custom_convert_to_llm_preserves_selected() {
281 let messages = vec![
282 ChatMessage::system("sys"),
283 ChatMessage::Custom {
284 role: "artifact".to_string(),
285 data: serde_json::json!({"id": "x"}),
286 },
287 ChatMessage::user("usr"),
288 ];
289
290 let convert = |msgs: &[ChatMessage]| -> Vec<ChatMessage> {
292 msgs.iter()
293 .filter(|m| match m {
294 ChatMessage::Custom { role, .. } => role == "artifact",
295 _ => true,
296 })
297 .cloned()
298 .collect()
299 };
300
301 let filtered = convert(&messages);
302 assert_eq!(filtered.len(), 3);
303 assert!(matches!(filtered[0], ChatMessage::System { .. }));
304 assert!(matches!(filtered[1], ChatMessage::Custom { .. }));
305 assert!(matches!(filtered[2], ChatMessage::User { .. }));
306
307 let messages2 = vec![
309 ChatMessage::system("sys"),
310 ChatMessage::Custom {
311 role: "notification".to_string(),
312 data: serde_json::json!({"level": "info"}),
313 },
314 ];
315 let filtered2 = convert(&messages2);
316 assert_eq!(filtered2.len(), 1);
317 assert!(matches!(filtered2[0], ChatMessage::System { .. }));
318 }
319
320 #[test]
321 fn test_custom_message_is_ephemeral_false() {
322 let custom = ChatMessage::Custom {
323 role: "artifact".to_string(),
324 data: serde_json::json!({}),
325 };
326 assert!(!custom.is_ephemeral());
327 }
328
329 #[test]
330 fn test_custom_message_serialization_roundtrip() {
331 let custom = ChatMessage::Custom {
332 role: "artifact".to_string(),
333 data: serde_json::json!({"id": "test-123", "content": "hello"}),
334 };
335 let json_str = serde_json::to_string(&custom).unwrap();
336 let deserialized: ChatMessage = serde_json::from_str(&json_str).unwrap();
337 match deserialized {
338 ChatMessage::Custom { role, data } => {
339 assert_eq!(role, "artifact");
340 assert_eq!(data["id"], "test-123");
341 assert_eq!(data["content"], "hello");
342 }
343 _ => panic!("Expected Custom variant"),
344 }
345 }
346}