1use crate::error::{ParseError, ToolResult};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13pub enum CacheControl {
14 Breakpoint,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct TextBlock {
22 pub text: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub cache_control: Option<CacheControl>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct ThinkingBlock {
32 pub thinking: String,
33 pub redacted: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct ImageSource {
40 pub data: String,
42 pub media_type: String,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ToolCall {
49 pub id: String,
50 pub name: String,
51 pub arguments: serde_json::Value,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum ContentBlock {
59 Text(TextBlock),
60 Thinking(ThinkingBlock),
61 Image { source: ImageSource },
62 ToolCall(ToolCall),
63}
64
65impl ContentBlock {
66 pub fn text(s: impl Into<String>) -> Self {
68 ContentBlock::Text(TextBlock {
69 text: s.into(),
70 cache_control: None,
71 })
72 }
73
74 pub fn text_with_cache(s: String, cache: CacheControl) -> Self {
76 ContentBlock::Text(TextBlock {
77 text: s,
78 cache_control: Some(cache),
79 })
80 }
81
82 pub fn as_text(&self) -> Option<&str> {
83 match self {
84 ContentBlock::Text(block) => Some(&block.text),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "snake_case")]
93pub enum Message {
94 System {
95 content: Vec<ContentBlock>,
96 },
97 User {
98 content: Vec<ContentBlock>,
99 },
100 Assistant {
101 content: Vec<ContentBlock>,
102 },
103 ToolResult {
104 tool_call_id: String,
105 is_error: bool,
107 content: Vec<ContentBlock>,
108 },
109}
110
111impl Message {
112 pub fn system_text(s: &str) -> Self {
128 Message::System {
129 content: text_block(s.to_string()),
130 }
131 }
132
133 pub fn user_text(s: &str) -> Self {
141 Message::User {
142 content: text_block(s.to_string()),
143 }
144 }
145
146 pub fn assistant_text(s: &str) -> Self {
148 Message::Assistant {
149 content: text_block(s.to_string()),
150 }
151 }
152
153 pub fn user_text_image(text: &str, media_type: String, data: String) -> Self {
169 Message::User {
170 content: vec![
171 ContentBlock::text(text),
172 ContentBlock::Image {
173 source: ImageSource { data, media_type },
174 },
175 ],
176 }
177 }
178
179 pub fn user_image(media_type: String, data: String) -> Self {
181 Message::User {
182 content: vec![ContentBlock::Image {
183 source: ImageSource { data, media_type },
184 }],
185 }
186 }
187
188 pub fn system(content: Vec<ContentBlock>) -> Self {
194 Message::System { content }
195 }
196
197 pub fn user(content: Vec<ContentBlock>) -> Self {
199 Message::User { content }
200 }
201
202 pub fn assistant(content: Vec<ContentBlock>) -> Self {
204 Message::Assistant { content }
205 }
206
207 pub fn tool_result_ok(call_id: impl Into<String>, content: String) -> Self {
209 Message::ToolResult {
210 tool_call_id: call_id.into(),
211 is_error: false,
212 content: text_block(content),
213 }
214 }
215
216 pub fn tool_error(call_id: impl Into<String>, error: String) -> Self {
218 Message::ToolResult {
219 tool_call_id: call_id.into(),
220 is_error: true,
221 content: text_block(error),
222 }
223 }
224
225 pub fn content(&self) -> &Vec<ContentBlock> {
231 match self {
232 Message::System { content }
233 | Message::User { content }
234 | Message::Assistant { content }
235 | Message::ToolResult { content, .. } => content,
236 }
237 }
238
239 pub fn tool_call_id(&self) -> String {
241 match self {
242 Message::ToolResult { tool_call_id, .. } => tool_call_id.clone(),
243 _ => String::new(),
244 }
245 }
246
247 pub fn is_tool_error(&self) -> bool {
249 matches!(self, Message::ToolResult { is_error: true, .. })
250 }
251
252 pub fn tool_result(call: &ToolCall, result: &ToolResult) -> Self {
257 let (content_str, is_error) = match result {
258 Ok(v) => (
259 serde_json::to_string(v).unwrap_or_else(|_| v.to_string()),
260 false,
261 ),
262 Err(e) => (format!("tool error: {e}"), true),
263 };
264 Message::ToolResult {
265 tool_call_id: call.id.clone(),
266 is_error,
267 content: text_block(content_str),
268 }
269 }
270
271 pub fn validate(&self) -> Result<(), ParseError> {
279 match self {
280 Message::ToolResult {
281 tool_call_id,
282 is_error: _,
283 content,
284 } => {
285 if tool_call_id.is_empty() {
286 return Err(ParseError {
287 detail: "ToolResult.tool_call_id must not be empty".into(),
288 });
289 }
290 for block in content {
291 match block {
292 ContentBlock::ToolCall(_) => {
293 return Err(ParseError {
294 detail: "ToolResult must not contain ToolCall blocks".into(),
295 });
296 }
297 ContentBlock::Thinking(_) => {
298 return Err(ParseError {
299 detail: "ToolResult must not contain Thinking blocks".into(),
300 });
301 }
302 _ => {}
303 }
304 }
305 }
306 Message::Assistant { content } => {
307 for block in content {
308 if let ContentBlock::ToolCall(tc) = block
309 && tc.id.is_empty()
310 {
311 return Err(ParseError {
312 detail: "Assistant ToolCall.id must not be empty".into(),
313 });
314 }
315 }
316 }
317 Message::User { content } => {
318 for block in content {
319 if let ContentBlock::Thinking(_) = block {
320 return Err(ParseError {
321 detail: "User must not contain Thinking blocks".into(),
322 });
323 }
324 }
325 }
326 Message::System { .. } => {}
327 }
328 Ok(())
329 }
330
331 pub fn extract_tool_calls(&self) -> Vec<ToolCall> {
333 match self {
334 Message::Assistant { content } => content
335 .iter()
336 .filter_map(|b| {
337 if let ContentBlock::ToolCall(tc) = b {
338 Some(tc.clone())
339 } else {
340 None
341 }
342 })
343 .collect(),
344 _ => Vec::new(),
345 }
346 }
347}
348
349pub fn text_block(s: impl Into<String>) -> Vec<ContentBlock> {
351 vec![ContentBlock::text(s)]
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_content_block_text() {
360 let block = ContentBlock::text("hello");
361 assert_eq!(block.as_text(), Some("hello"));
362 }
363
364 #[test]
365 fn test_content_block_tool_call_no_as_text() {
366 let block = ContentBlock::ToolCall(ToolCall {
367 id: "1".into(),
368 name: "test".into(),
369 arguments: serde_json::json!({}),
370 });
371 assert_eq!(block.as_text(), None);
372 }
373
374 #[test]
375 fn test_message_content() {
376 let msg = Message::user_text("hello world");
377 assert_eq!(msg.content().len(), 1);
378 assert_eq!(msg.content()[0].as_text(), Some("hello world"));
379 }
380
381 #[test]
382 fn test_message_extract_tool_calls() {
383 let tc = ToolCall {
384 id: "1".into(),
385 name: "test".into(),
386 arguments: serde_json::json!({}),
387 };
388 let msg = Message::Assistant {
389 content: vec![ContentBlock::ToolCall(tc.clone())],
390 };
391 let calls = msg.extract_tool_calls();
392 assert_eq!(calls.len(), 1);
393 assert_eq!(calls[0].name, "test");
394 }
395
396 #[test]
399 fn test_validate_user_ok() {
400 let msg = Message::User {
401 content: text_block("hello".to_string()),
402 };
403 assert!(msg.validate().is_ok());
404 }
405
406 #[test]
407 fn test_validate_user_reject_thinking() {
408 let msg = Message::User {
409 content: vec![ContentBlock::Thinking(ThinkingBlock {
410 thinking: "hmm".into(),
411 redacted: None,
412 })],
413 };
414 assert!(matches!(msg.validate(), Err(ParseError { .. })));
415 }
416
417 #[test]
418 fn test_validate_assistant_ok() {
419 let msg = Message::Assistant {
420 content: text_block("hi".to_string()),
421 };
422 assert!(msg.validate().is_ok());
423 }
424
425 #[test]
426 fn test_validate_assistant_tool_call_empty_id() {
427 let msg = Message::Assistant {
428 content: vec![ContentBlock::ToolCall(ToolCall {
429 id: String::new(),
430 name: "test".into(),
431 arguments: serde_json::json!({}),
432 })],
433 };
434 assert!(matches!(msg.validate(), Err(ParseError { .. })));
435 }
436
437 #[test]
438 fn test_validate_tool_result_ok() {
439 let msg = Message::ToolResult {
440 tool_call_id: "call_1".to_string(),
441 is_error: false,
442 content: text_block("ok".to_string()),
443 };
444 assert!(msg.validate().is_ok());
445 }
446
447 #[test]
448 fn test_validate_tool_result_empty_id() {
449 let msg = Message::ToolResult {
450 tool_call_id: String::new(),
451 is_error: false,
452 content: text_block("ok".to_string()),
453 };
454 assert!(matches!(msg.validate(), Err(ParseError { .. })));
455 }
456
457 #[test]
458 fn test_validate_tool_result_reject_tool_call() {
459 let msg = Message::ToolResult {
460 tool_call_id: "call_1".to_string(),
461 is_error: false,
462 content: vec![ContentBlock::ToolCall(ToolCall {
463 id: "x".into(),
464 name: "y".into(),
465 arguments: serde_json::json!({}),
466 })],
467 };
468 assert!(matches!(msg.validate(), Err(ParseError { .. })));
469 }
470
471 #[test]
472 fn test_validate_tool_result_reject_thinking() {
473 let msg = Message::ToolResult {
474 tool_call_id: "call_1".to_string(),
475 is_error: false,
476 content: vec![ContentBlock::Thinking(ThinkingBlock {
477 thinking: "hmm".into(),
478 redacted: None,
479 })],
480 };
481 assert!(matches!(msg.validate(), Err(ParseError { .. })));
482 }
483
484 #[test]
485 fn test_validate_system_ok() {
486 let msg = Message::System {
487 content: text_block("you are helpful".to_string()),
488 };
489 assert!(msg.validate().is_ok());
490 }
491
492 #[test]
495 fn test_convenience_system_text() {
496 let msg = Message::system_text("you are helpful");
497 assert!(matches!(msg, Message::System { .. }));
498 assert_eq!(msg.content()[0].as_text(), Some("you are helpful"));
499 }
500
501 #[test]
502 fn test_convenience_user_text() {
503 let msg = Message::user_text("hello");
504 assert!(matches!(msg, Message::User { .. }));
505 assert_eq!(msg.content()[0].as_text(), Some("hello"));
506 }
507
508 #[test]
509 fn test_convenience_assistant_text() {
510 let msg = Message::assistant_text("the answer is 42");
511 assert!(matches!(msg, Message::Assistant { .. }));
512 assert_eq!(msg.content()[0].as_text(), Some("the answer is 42"));
513 }
514
515 #[test]
516 fn test_convenience_system_content() {
517 let msg = Message::system(vec![ContentBlock::text("prompt")]);
518 assert!(matches!(msg, Message::System { .. }));
519 assert_eq!(msg.content()[0].as_text(), Some("prompt"));
520 }
521
522 #[test]
523 fn test_convenience_user_content() {
524 let msg = Message::user(vec![ContentBlock::text("question")]);
525 assert!(matches!(msg, Message::User { .. }));
526 assert_eq!(msg.content()[0].as_text(), Some("question"));
527 }
528
529 #[test]
530 fn test_convenience_tool_result_ok() {
531 let msg = Message::tool_result_ok("call_1", "result data".to_string());
532 assert!(matches!(msg, Message::ToolResult { .. }));
533 assert!(!msg.is_tool_error());
534 assert_eq!(msg.tool_call_id(), "call_1");
535 }
536
537 #[test]
538 fn test_convenience_tool_error() {
539 let msg = Message::tool_error("call_2", "something failed".to_string());
540 assert!(matches!(msg, Message::ToolResult { .. }));
541 assert!(msg.is_tool_error());
542 assert_eq!(msg.tool_call_id(), "call_2");
543 }
544
545 #[test]
546 fn test_content_block_text_with_string() {
547 let s = String::from("dynamic");
548 let block = ContentBlock::text(s);
549 assert_eq!(block.as_text(), Some("dynamic"));
550 }
551
552 #[test]
553 fn test_text_block_with_str() {
554 let blocks = text_block("hello");
555 assert_eq!(blocks.len(), 1);
556 assert_eq!(blocks[0].as_text(), Some("hello"));
557 }
558
559 #[test]
560 fn test_text_block_with_string() {
561 let blocks = text_block(String::from("hello"));
562 assert_eq!(blocks.len(), 1);
563 assert_eq!(blocks[0].as_text(), Some("hello"));
564 }
565
566 #[test]
569 fn test_convenience_user_text_image() {
570 let msg = Message::user_text_image("what's this?", "image/png".into(), "base64data".into());
571 assert!(matches!(msg, Message::User { .. }));
572 assert_eq!(msg.content().len(), 2);
573 assert_eq!(msg.content()[0].as_text(), Some("what's this?"));
574 match &msg.content()[1] {
575 ContentBlock::Image { source } => {
576 assert_eq!(source.media_type, "image/png");
577 assert_eq!(source.data, "base64data");
578 }
579 _ => panic!("expected Image block"),
580 }
581 }
582
583 #[test]
584 fn test_convenience_user_image() {
585 let msg = Message::user_image("image/jpeg".into(), "jpgdata".into());
586 assert!(matches!(msg, Message::User { .. }));
587 assert_eq!(msg.content().len(), 1);
588 match &msg.content()[0] {
589 ContentBlock::Image { source } => {
590 assert_eq!(source.media_type, "image/jpeg");
591 assert_eq!(source.data, "jpgdata");
592 }
593 _ => panic!("expected Image block"),
594 }
595 }
596}