1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::typed_id::{ImageId, MessageId, ModelId};
13
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "openapi", derive(ToSchema))]
35pub enum ExecutionPhase {
36 Commentary,
39 FinalAnswer,
41}
42
43impl ExecutionPhase {
44 pub fn from_has_tool_calls(has_tool_calls: bool) -> Self {
46 if has_tool_calls {
47 Self::Commentary
48 } else {
49 Self::FinalAnswer
50 }
51 }
52
53 pub fn from_provider_str(s: &str) -> Option<Self> {
56 match s {
57 "commentary" | "in_progress" => Some(Self::Commentary),
58 "final_answer" | "completed" => Some(Self::FinalAnswer),
59 _ => None,
60 }
61 }
62
63 pub fn as_provider_str(&self) -> &'static str {
65 match self {
66 Self::Commentary => "commentary",
67 Self::FinalAnswer => "final_answer",
68 }
69 }
70}
71
72impl std::fmt::Display for ExecutionPhase {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(self.as_provider_str())
75 }
76}
77
78impl Serialize for ExecutionPhase {
79 fn serialize<S: serde::Serializer>(
80 &self,
81 serializer: S,
82 ) -> std::result::Result<S::Ok, S::Error> {
83 serializer.serialize_str(self.as_provider_str())
84 }
85}
86
87impl<'de> Deserialize<'de> for ExecutionPhase {
88 fn deserialize<D: serde::Deserializer<'de>>(
89 deserializer: D,
90 ) -> std::result::Result<Self, D::Error> {
91 let s = String::deserialize(deserializer)?;
92 match s.as_str() {
93 "commentary" | "in_progress" => Ok(Self::Commentary),
94 "final_answer" | "completed" => Ok(Self::FinalAnswer),
95 other => Err(serde::de::Error::unknown_variant(
96 other,
97 &["commentary", "final_answer", "in_progress", "completed"],
98 )),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[cfg_attr(feature = "openapi", derive(ToSchema))]
106#[serde(rename_all = "snake_case")]
107pub enum MessageRole {
108 System,
110 User,
112 Agent,
114 ToolResult,
116}
117
118impl std::fmt::Display for MessageRole {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 match self {
121 MessageRole::System => write!(f, "system"),
122 MessageRole::User => write!(f, "user"),
123 MessageRole::Agent => write!(f, "agent"),
124 MessageRole::ToolResult => write!(f, "tool_result"),
125 }
126 }
127}
128
129impl From<&str> for MessageRole {
130 fn from(s: &str) -> Self {
131 match s.to_lowercase().as_str() {
132 "system" => MessageRole::System,
133 "user" => MessageRole::User,
134 "agent" | "assistant" => MessageRole::Agent,
136 "tool_result" => MessageRole::ToolResult,
137 _ => MessageRole::User,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153#[cfg_attr(feature = "openapi", derive(ToSchema))]
154pub struct ExternalActor {
155 pub actor_id: String,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub actor_name: Option<String>,
160 pub source: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub metadata: Option<std::collections::HashMap<String, String>>,
165}
166
167impl ExternalActor {
168 pub fn display_label(&self) -> &str {
170 self.actor_name.as_deref().unwrap_or(&self.actor_id)
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
180#[cfg_attr(feature = "openapi", derive(ToSchema))]
181pub struct ReasoningConfig {
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub effort: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
189#[cfg_attr(feature = "openapi", derive(ToSchema))]
190pub struct Controls {
191 #[serde(skip_serializing_if = "Option::is_none")]
194 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
195 pub model_id: Option<ModelId>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
200 pub locale: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub reasoning: Option<ReasoningConfig>,
205
206 #[serde(default, skip_serializing_if = "Option::is_none")]
212 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
213 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
214}
215
216impl Controls {
217 pub fn resolve_hints(
220 session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
221 message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
222 ) -> std::collections::HashMap<String, serde_json::Value> {
223 match (session_hints, message_hints) {
224 (None, None) => std::collections::HashMap::new(),
225 (Some(s), None) => s.clone(),
226 (None, Some(m)) => m.clone(),
227 (Some(s), Some(m)) => {
228 let mut merged = s.clone();
229 merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
230 merged
231 }
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238#[cfg_attr(feature = "openapi", derive(ToSchema))]
239pub struct Message {
240 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
242 pub id: MessageId,
243
244 pub role: MessageRole,
246
247 pub content: Vec<ContentPart>,
249
250 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub phase: Option<ExecutionPhase>,
258
259 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub thinking: Option<String>,
264
265 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub thinking_signature: Option<String>,
269
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub controls: Option<Controls>,
273
274 #[serde(default, skip_serializing_if = "Option::is_none")]
276 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
277 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
278
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub external_actor: Option<ExternalActor>,
282
283 pub created_at: DateTime<Utc>,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[cfg_attr(feature = "openapi", derive(ToSchema))]
294#[serde(rename_all = "snake_case")]
295pub enum ContentType {
296 Text,
297 Image,
298 ImageFile,
299 ToolCall,
300 ToolResult,
301}
302
303impl std::fmt::Display for ContentType {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 match self {
306 ContentType::Text => write!(f, "text"),
307 ContentType::Image => write!(f, "image"),
308 ContentType::ImageFile => write!(f, "image_file"),
309 ContentType::ToolCall => write!(f, "tool_call"),
310 ContentType::ToolResult => write!(f, "tool_result"),
311 }
312 }
313}
314
315impl From<&str> for ContentType {
316 fn from(s: &str) -> Self {
317 match s {
318 "image" => ContentType::Image,
319 "image_file" => ContentType::ImageFile,
320 "tool_call" => ContentType::ToolCall,
321 "tool_result" => ContentType::ToolResult,
322 _ => ContentType::Text,
323 }
324 }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
333#[cfg_attr(feature = "openapi", derive(ToSchema))]
334pub struct TextContentPart {
335 pub text: String,
336}
337
338impl TextContentPart {
339 pub fn new(text: impl Into<String>) -> Self {
340 Self { text: text.into() }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
346#[cfg_attr(feature = "openapi", derive(ToSchema))]
347pub struct ImageContentPart {
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub url: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub base64: Option<String>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub media_type: Option<String>,
354}
355
356impl ImageContentPart {
357 pub fn from_url(url: impl Into<String>) -> Self {
358 Self {
359 url: Some(url.into()),
360 base64: None,
361 media_type: None,
362 }
363 }
364
365 pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
366 Self {
367 url: None,
368 base64: Some(base64.into()),
369 media_type: Some(media_type.into()),
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
380#[cfg_attr(feature = "openapi", derive(ToSchema))]
381pub struct ImageFileContentPart {
382 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
384 pub image_id: ImageId,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub filename: Option<String>,
388}
389
390impl ImageFileContentPart {
391 pub fn new(image_id: ImageId) -> Self {
392 Self {
393 image_id,
394 filename: None,
395 }
396 }
397
398 pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
399 Self {
400 image_id,
401 filename: Some(filename.into()),
402 }
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
408#[cfg_attr(feature = "openapi", derive(ToSchema))]
409pub struct ToolCallContentPart {
410 pub id: String,
411 pub name: String,
412 pub arguments: serde_json::Value,
413}
414
415impl ToolCallContentPart {
416 pub fn new(
417 id: impl Into<String>,
418 name: impl Into<String>,
419 arguments: serde_json::Value,
420 ) -> Self {
421 Self {
422 id: id.into(),
423 name: name.into(),
424 arguments,
425 }
426 }
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
431#[cfg_attr(feature = "openapi", derive(ToSchema))]
432pub struct ToolResultContentPart {
433 pub tool_call_id: String,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub result: Option<serde_json::Value>,
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub error: Option<String>,
439}
440
441impl ToolResultContentPart {
442 pub fn new(
443 tool_call_id: impl Into<String>,
444 result: Option<serde_json::Value>,
445 error: Option<String>,
446 ) -> Self {
447 Self {
448 tool_call_id: tool_call_id.into(),
449 result,
450 error,
451 }
452 }
453
454 pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
455 Self {
456 tool_call_id: tool_call_id.into(),
457 result: Some(result),
458 error: None,
459 }
460 }
461
462 pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
463 Self {
464 tool_call_id: tool_call_id.into(),
465 result: None,
466 error: Some(error.into()),
467 }
468 }
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
480#[cfg_attr(feature = "openapi", derive(ToSchema))]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum ContentPart {
483 Text(TextContentPart),
485 Image(ImageContentPart),
487 ImageFile(ImageFileContentPart),
489 ToolCall(ToolCallContentPart),
491 ToolResult(ToolResultContentPart),
493}
494
495impl ContentPart {
496 pub fn text(text: impl Into<String>) -> Self {
498 ContentPart::Text(TextContentPart::new(text))
499 }
500
501 pub fn image_url(url: impl Into<String>) -> Self {
503 ContentPart::Image(ImageContentPart::from_url(url))
504 }
505
506 pub fn image_file(image_id: ImageId) -> Self {
508 ContentPart::ImageFile(ImageFileContentPart::new(image_id))
509 }
510
511 pub fn tool_call(
513 id: impl Into<String>,
514 name: impl Into<String>,
515 arguments: serde_json::Value,
516 ) -> Self {
517 ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
518 }
519
520 pub fn tool_result(
522 tool_call_id: impl Into<String>,
523 result: Option<serde_json::Value>,
524 error: Option<String>,
525 ) -> Self {
526 ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
527 }
528
529 pub fn as_text(&self) -> Option<&str> {
531 match self {
532 ContentPart::Text(t) => Some(&t.text),
533 _ => None,
534 }
535 }
536
537 pub fn is_image_file(&self) -> bool {
539 matches!(self, ContentPart::ImageFile(_))
540 }
541
542 pub fn content_type(&self) -> ContentType {
544 match self {
545 ContentPart::Text(_) => ContentType::Text,
546 ContentPart::Image(_) => ContentType::Image,
547 ContentPart::ImageFile(_) => ContentType::ImageFile,
548 ContentPart::ToolCall(_) => ContentType::ToolCall,
549 ContentPart::ToolResult(_) => ContentType::ToolResult,
550 }
551 }
552
553 pub fn to_openai_format(&self) -> Option<serde_json::Value> {
558 match self {
559 ContentPart::Text(t) => Some(serde_json::json!({
560 "type": "text",
561 "text": t.text
562 })),
563 ContentPart::Image(img) => {
564 if let Some(url) = &img.url {
565 Some(serde_json::json!({
566 "type": "image_url",
567 "image_url": { "url": url }
568 }))
569 } else if let Some(b64) = &img.base64 {
570 let media_type = img.media_type.as_deref().unwrap_or("image/png");
571 Some(serde_json::json!({
572 "type": "image_url",
573 "image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
574 }))
575 } else {
576 None
577 }
578 }
579 _ => None,
581 }
582 }
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
590#[cfg_attr(feature = "openapi", derive(ToSchema))]
591#[serde(tag = "type", rename_all = "snake_case")]
592pub enum InputContentPart {
593 Text(TextContentPart),
595 Image(ImageContentPart),
597 ImageFile(ImageFileContentPart),
599}
600
601impl From<InputContentPart> for ContentPart {
602 fn from(input: InputContentPart) -> Self {
603 match input {
604 InputContentPart::Text(t) => ContentPart::Text(t),
605 InputContentPart::Image(i) => ContentPart::Image(i),
606 InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
607 }
608 }
609}
610
611impl InputContentPart {
612 pub fn text(text: impl Into<String>) -> Self {
614 InputContentPart::Text(TextContentPart::new(text))
615 }
616
617 pub fn image_url(url: impl Into<String>) -> Self {
619 InputContentPart::Image(ImageContentPart::from_url(url))
620 }
621
622 pub fn image_file(image_id: ImageId) -> Self {
624 InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
625 }
626
627 pub fn as_text(&self) -> Option<&str> {
629 match self {
630 InputContentPart::Text(t) => Some(&t.text),
631 _ => None,
632 }
633 }
634
635 pub fn content_type(&self) -> ContentType {
637 match self {
638 InputContentPart::Text(_) => ContentType::Text,
639 InputContentPart::Image(_) => ContentType::Image,
640 InputContentPart::ImageFile(_) => ContentType::ImageFile,
641 }
642 }
643}
644
645impl Message {
646 pub fn user(content: impl Into<String>) -> Self {
648 Self {
649 id: MessageId::new(),
650 role: MessageRole::User,
651 content: vec![ContentPart::text(content)],
652 phase: None,
653 thinking: None,
654 thinking_signature: None,
655 controls: None,
656 metadata: None,
657 external_actor: None,
658 created_at: Utc::now(),
659 }
660 }
661
662 pub fn assistant(content: impl Into<String>) -> Self {
664 Self {
665 id: MessageId::new(),
666 role: MessageRole::Agent,
667 content: vec![ContentPart::text(content)],
668 phase: None,
669 thinking: None,
670 thinking_signature: None,
671 controls: None,
672 metadata: None,
673 external_actor: None,
674 created_at: Utc::now(),
675 }
676 }
677
678 pub fn assistant_with_tools(
684 content: impl Into<String>,
685 tool_calls: Vec<crate::tool_types::ToolCall>,
686 ) -> Self {
687 let text_content = content.into();
688 let mut parts = Vec::new();
689 if !text_content.is_empty() {
691 parts.push(ContentPart::text(text_content));
692 }
693 for tc in tool_calls {
694 parts.push(ContentPart::ToolCall(ToolCallContentPart {
695 id: tc.id,
696 name: tc.name,
697 arguments: tc.arguments,
698 }));
699 }
700 Self {
701 id: MessageId::new(),
702 role: MessageRole::Agent,
703 content: parts,
704 phase: None,
705 thinking: None,
706 thinking_signature: None,
707 controls: None,
708 metadata: None,
709 external_actor: None,
710 created_at: Utc::now(),
711 }
712 }
713
714 pub fn system(content: impl Into<String>) -> Self {
716 Self {
717 id: MessageId::new(),
718 role: MessageRole::System,
719 content: vec![ContentPart::text(content)],
720 phase: None,
721 thinking: None,
722 thinking_signature: None,
723 controls: None,
724 metadata: None,
725 external_actor: None,
726 created_at: Utc::now(),
727 }
728 }
729
730 pub fn tool_result(
732 tool_call_id: impl Into<String>,
733 result: Option<serde_json::Value>,
734 error: Option<String>,
735 ) -> Self {
736 let tool_call_id = tool_call_id.into();
737 Self {
738 id: MessageId::new(),
739 role: MessageRole::ToolResult,
740 content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
741 tool_call_id,
742 result,
743 error,
744 ))],
745 phase: None,
746 thinking: None,
747 thinking_signature: None,
748 controls: None,
749 metadata: None,
750 external_actor: None,
751 created_at: Utc::now(),
752 }
753 }
754
755 pub fn tool_result_with_images(
761 tool_call_id: impl Into<String>,
762 result: Option<serde_json::Value>,
763 images: Vec<crate::tools::ToolResultImage>,
764 ) -> Self {
765 let tool_call_id = tool_call_id.into();
766 let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
767 tool_call_id,
768 result,
769 None,
770 ))];
771 for img in images {
772 content.push(ContentPart::Image(ImageContentPart::from_base64(
773 img.base64,
774 img.media_type,
775 )));
776 }
777 Self {
778 id: MessageId::new(),
779 role: MessageRole::ToolResult,
780 content,
781 phase: None,
782 thinking: None,
783 thinking_signature: None,
784 controls: None,
785 metadata: None,
786 external_actor: None,
787 created_at: Utc::now(),
788 }
789 }
790
791 pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
793 self.phase = Some(phase);
794 self
795 }
796
797 pub fn tool_call_id(&self) -> Option<&str> {
801 self.content.iter().find_map(|p| match p {
802 ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
803 _ => None,
804 })
805 }
806
807 pub fn text(&self) -> Option<&str> {
809 self.content.iter().find_map(|p| p.as_text())
810 }
811
812 pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
814 self.content
815 .iter()
816 .filter_map(|p| match p {
817 ContentPart::ToolCall(tc) => Some(tc),
818 _ => None,
819 })
820 .collect()
821 }
822
823 pub fn has_tool_calls(&self) -> bool {
825 self.content
826 .iter()
827 .any(|p| matches!(p, ContentPart::ToolCall(_)))
828 }
829
830 pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
832 self.content.iter().find_map(|p| match p {
833 ContentPart::ToolResult(tr) => Some(tr),
834 _ => None,
835 })
836 }
837
838 pub fn content_to_llm_string(&self) -> String {
840 self.content
841 .iter()
842 .map(|part| match part {
843 ContentPart::Text(t) => t.text.clone(),
844 ContentPart::Image(_) => "[Image]".to_string(),
845 ContentPart::ImageFile(_) => "[Image File]".to_string(),
846 ContentPart::ToolCall(tc) => {
847 format!(
848 "Tool call: {} with arguments: {}",
849 tc.name,
850 serde_json::to_string(&tc.arguments).unwrap_or_default()
851 )
852 }
853 ContentPart::ToolResult(tr) => {
854 if let Some(err) = &tr.error {
855 format!("Tool error: {}", err)
856 } else if let Some(res) = &tr.result {
857 serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
858 } else {
859 "{}".to_string()
860 }
861 }
862 })
863 .collect::<Vec<_>>()
864 .join("\n")
865 }
866
867 pub fn to_openai_format(&self) -> serde_json::Value {
876 let role = match self.role {
877 MessageRole::System => "system",
878 MessageRole::User => "user",
879 MessageRole::Agent => "assistant",
880 MessageRole::ToolResult => "tool",
881 };
882
883 if self.role == MessageRole::ToolResult {
885 let tool_call_id = self.tool_call_id().unwrap_or("");
886 let content = self
887 .content
888 .iter()
889 .find_map(|p| match p {
890 ContentPart::ToolResult(tr) => {
891 if let Some(error) = &tr.error {
892 Some(format!("Error: {}", error))
893 } else if let Some(result) = &tr.result {
894 Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
895 } else {
896 Some("{}".to_string())
897 }
898 }
899 _ => None,
900 })
901 .unwrap_or_else(|| "{}".to_string());
902
903 return serde_json::json!({
904 "role": role,
905 "content": content,
906 "tool_call_id": tool_call_id
907 });
908 }
909
910 if self.role == MessageRole::Agent {
912 let tool_calls: Vec<serde_json::Value> = self
913 .content
914 .iter()
915 .filter_map(|p| match p {
916 ContentPart::ToolCall(tc) => Some(serde_json::json!({
917 "id": tc.id,
918 "type": "function",
919 "function": {
920 "name": tc.name,
921 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
922 }
923 })),
924 _ => None,
925 })
926 .collect();
927
928 let text_content: String = self
929 .content
930 .iter()
931 .filter_map(|p| match p {
932 ContentPart::Text(t) => Some(t.text.clone()),
933 _ => None,
934 })
935 .collect::<Vec<_>>()
936 .join("\n");
937
938 if tool_calls.is_empty() {
939 return serde_json::json!({
940 "role": role,
941 "content": text_content
942 });
943 } else {
944 let mut result = serde_json::json!({
945 "role": role,
946 "tool_calls": tool_calls
947 });
948 if !text_content.is_empty() {
949 result["content"] = serde_json::json!(text_content);
950 }
951 return result;
952 }
953 }
954
955 let content = self.content_to_openai_format();
957 serde_json::json!({
958 "role": role,
959 "content": content
960 })
961 }
962
963 fn content_to_openai_format(&self) -> serde_json::Value {
965 if self.content.len() == 1
967 && let ContentPart::Text(t) = &self.content[0]
968 {
969 return serde_json::json!(t.text);
970 }
971
972 let parts: Vec<serde_json::Value> = self
974 .content
975 .iter()
976 .filter_map(|part| part.to_openai_format())
977 .collect();
978
979 if parts.is_empty() {
980 return serde_json::json!("");
981 }
982
983 if parts.len() == 1
985 && let Some(text) = parts[0].get("text")
986 {
987 return text.clone();
988 }
989
990 serde_json::json!(parts)
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997 use crate::tool_types::ToolCall;
998
999 #[test]
1000 fn test_user_message() {
1001 let msg = Message::user("Hello");
1002 assert_eq!(msg.role, MessageRole::User);
1003 assert_eq!(msg.text(), Some("Hello"));
1004 }
1005
1006 #[test]
1007 fn test_assistant_message() {
1008 let msg = Message::assistant("Hi there!");
1009 assert_eq!(msg.role, MessageRole::Agent);
1010 assert_eq!(msg.text(), Some("Hi there!"));
1011 }
1012
1013 #[test]
1014 fn test_tool_result_message() {
1015 let msg = Message::tool_result(
1016 "call_123",
1017 Some(serde_json::json!({"result": "success"})),
1018 None,
1019 );
1020 assert_eq!(msg.role, MessageRole::ToolResult);
1021 assert_eq!(msg.tool_call_id(), Some("call_123"));
1022 }
1023
1024 #[test]
1025 fn test_assistant_with_tools_and_text() {
1026 let tool_call = ToolCall {
1027 id: "call_123".to_string(),
1028 name: "get_weather".to_string(),
1029 arguments: serde_json::json!({"location": "Tokyo"}),
1030 };
1031 let msg = Message::assistant_with_tools("Let me check the weather.", vec![tool_call]);
1032
1033 assert_eq!(msg.role, MessageRole::Agent);
1034 assert_eq!(msg.text(), Some("Let me check the weather."));
1035 assert_eq!(msg.tool_calls().len(), 1);
1036 assert_eq!(msg.tool_calls()[0].name, "get_weather");
1037 }
1038
1039 #[test]
1040 fn test_assistant_with_tools_empty_text() {
1041 let tool_call = ToolCall {
1044 id: "call_123".to_string(),
1045 name: "search".to_string(),
1046 arguments: serde_json::json!({"query": "rust"}),
1047 };
1048 let msg = Message::assistant_with_tools("", vec![tool_call]);
1049
1050 assert_eq!(msg.role, MessageRole::Agent);
1051 assert_eq!(msg.text(), None);
1053 assert_eq!(msg.tool_calls().len(), 1);
1055 assert_eq!(msg.tool_calls()[0].name, "search");
1056 assert_eq!(msg.content.len(), 1);
1058 assert!(matches!(msg.content[0], ContentPart::ToolCall(_)));
1059 }
1060
1061 #[test]
1062 fn test_assistant_with_tools_whitespace_text() {
1063 let tool_call = ToolCall {
1065 id: "call_456".to_string(),
1066 name: "fetch".to_string(),
1067 arguments: serde_json::json!({}),
1068 };
1069 let msg = Message::assistant_with_tools(" ", vec![tool_call]);
1070
1071 assert_eq!(msg.text(), Some(" "));
1073 assert_eq!(msg.content.len(), 2); }
1075
1076 #[test]
1077 fn test_assistant_with_multiple_tool_calls() {
1078 let tool_calls = vec![
1079 ToolCall {
1080 id: "call_1".to_string(),
1081 name: "search".to_string(),
1082 arguments: serde_json::json!({"q": "a"}),
1083 },
1084 ToolCall {
1085 id: "call_2".to_string(),
1086 name: "fetch".to_string(),
1087 arguments: serde_json::json!({"url": "http://example.com"}),
1088 },
1089 ];
1090 let msg = Message::assistant_with_tools("", tool_calls);
1091
1092 assert_eq!(msg.tool_calls().len(), 2);
1093 assert_eq!(msg.content.len(), 2);
1095 }
1096
1097 #[test]
1102 fn test_to_openai_format_user_message() {
1103 let msg = Message::user("Hello, world!");
1104 let converted = msg.to_openai_format();
1105
1106 assert_eq!(converted["role"], "user");
1107 assert_eq!(converted["content"], "Hello, world!");
1108 }
1109
1110 #[test]
1111 fn test_to_openai_format_system_message() {
1112 let msg = Message::system("You are a helpful assistant.");
1113 let converted = msg.to_openai_format();
1114
1115 assert_eq!(converted["role"], "system");
1116 assert_eq!(converted["content"], "You are a helpful assistant.");
1117 }
1118
1119 #[test]
1120 fn test_to_openai_format_assistant_role_mapping() {
1121 let msg = Message::assistant("Hi there!");
1123 let converted = msg.to_openai_format();
1124
1125 assert_eq!(converted["role"], "assistant");
1126 assert_eq!(converted["content"], "Hi there!");
1127 }
1128
1129 #[test]
1130 fn test_to_openai_format_assistant_with_tool_calls() {
1131 let tool_call = ToolCall {
1132 id: "call_123".to_string(),
1133 name: "get_weather".to_string(),
1134 arguments: serde_json::json!({"location": "Tokyo"}),
1135 };
1136 let msg = Message::assistant_with_tools("Let me check.", vec![tool_call]);
1137 let converted = msg.to_openai_format();
1138
1139 assert_eq!(converted["role"], "assistant");
1140 assert_eq!(converted["content"], "Let me check.");
1141
1142 let tool_calls = converted["tool_calls"].as_array().unwrap();
1143 assert_eq!(tool_calls.len(), 1);
1144 assert_eq!(tool_calls[0]["id"], "call_123");
1145 assert_eq!(tool_calls[0]["type"], "function");
1146 assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
1147 assert_eq!(
1148 tool_calls[0]["function"]["arguments"],
1149 r#"{"location":"Tokyo"}"#
1150 );
1151 }
1152
1153 #[test]
1154 fn test_to_openai_format_assistant_tool_calls_only() {
1155 let tool_call = ToolCall {
1157 id: "call_abc".to_string(),
1158 name: "search".to_string(),
1159 arguments: serde_json::json!({"query": "rust"}),
1160 };
1161 let msg = Message::assistant_with_tools("", vec![tool_call]);
1162 let converted = msg.to_openai_format();
1163
1164 assert_eq!(converted["role"], "assistant");
1165 assert!(converted.get("content").is_none());
1167 assert!(converted["tool_calls"].is_array());
1168 }
1169
1170 #[test]
1171 fn test_to_openai_format_tool_result_role_mapping() {
1172 let msg = Message::tool_result(
1174 "call_123",
1175 Some(serde_json::json!({"temperature": 72})),
1176 None,
1177 );
1178 let converted = msg.to_openai_format();
1179
1180 assert_eq!(converted["role"], "tool");
1181 assert_eq!(converted["tool_call_id"], "call_123");
1182 assert_eq!(converted["content"], r#"{"temperature":72}"#);
1183 }
1184
1185 #[test]
1186 fn test_to_openai_format_tool_result_error() {
1187 let msg = Message::tool_result("call_456", None, Some("API timeout".to_string()));
1188 let converted = msg.to_openai_format();
1189
1190 assert_eq!(converted["role"], "tool");
1191 assert_eq!(converted["tool_call_id"], "call_456");
1192 assert_eq!(converted["content"], "Error: API timeout");
1193 }
1194
1195 #[test]
1196 fn test_to_openai_format_full_conversation() {
1197 let tool_call = ToolCall {
1199 id: "call_abc".to_string(),
1200 name: "search".to_string(),
1201 arguments: serde_json::json!({"query": "rust"}),
1202 };
1203
1204 let messages = [
1205 Message::user("Search for rust"),
1206 Message::assistant_with_tools("", vec![tool_call]),
1207 Message::tool_result(
1208 "call_abc",
1209 Some(serde_json::json!({"results": ["rust-lang.org"]})),
1210 None,
1211 ),
1212 Message::assistant("Here are the search results."),
1213 ];
1214 let converted: Vec<_> = messages.iter().map(|m| m.to_openai_format()).collect();
1215
1216 assert_eq!(converted.len(), 4);
1217 assert_eq!(converted[0]["role"], "user");
1218 assert_eq!(converted[1]["role"], "assistant");
1219 assert!(converted[1]["tool_calls"].is_array());
1220 assert_eq!(converted[2]["role"], "tool");
1221 assert_eq!(converted[2]["tool_call_id"], "call_abc");
1222 assert_eq!(converted[3]["role"], "assistant");
1223 }
1224
1225 #[test]
1230 fn test_content_part_to_openai_format_text() {
1231 let part = ContentPart::text("Hello");
1232 let converted = part.to_openai_format().unwrap();
1233
1234 assert_eq!(converted["type"], "text");
1235 assert_eq!(converted["text"], "Hello");
1236 }
1237
1238 #[test]
1239 fn test_content_part_to_openai_format_image_url() {
1240 let part = ContentPart::image_url("https://example.com/img.png");
1241 let converted = part.to_openai_format().unwrap();
1242
1243 assert_eq!(converted["type"], "image_url");
1244 assert_eq!(converted["image_url"]["url"], "https://example.com/img.png");
1245 }
1246
1247 #[test]
1248 fn test_content_part_to_openai_format_image_base64() {
1249 let part = ContentPart::Image(ImageContentPart::from_base64("abc123", "image/jpeg"));
1250 let converted = part.to_openai_format().unwrap();
1251
1252 assert_eq!(converted["type"], "image_url");
1253 assert_eq!(
1254 converted["image_url"]["url"],
1255 "data:image/jpeg;base64,abc123"
1256 );
1257 }
1258
1259 #[test]
1260 fn test_content_part_to_openai_format_tool_call_returns_none() {
1261 let part = ContentPart::tool_call("call_1", "search", serde_json::json!({}));
1263 assert!(part.to_openai_format().is_none());
1264 }
1265
1266 #[test]
1267 fn test_content_part_to_openai_format_tool_result_returns_none() {
1268 let part = ContentPart::tool_result("call_1", Some(serde_json::json!({})), None);
1270 assert!(part.to_openai_format().is_none());
1271 }
1272
1273 #[test]
1274 fn test_execution_phase_from_has_tool_calls() {
1275 assert_eq!(
1276 ExecutionPhase::from_has_tool_calls(true),
1277 ExecutionPhase::Commentary
1278 );
1279 assert_eq!(
1280 ExecutionPhase::from_has_tool_calls(false),
1281 ExecutionPhase::FinalAnswer
1282 );
1283 }
1284
1285 #[test]
1286 fn test_execution_phase_from_provider_str() {
1287 assert_eq!(
1288 ExecutionPhase::from_provider_str("commentary"),
1289 Some(ExecutionPhase::Commentary)
1290 );
1291 assert_eq!(
1292 ExecutionPhase::from_provider_str("final_answer"),
1293 Some(ExecutionPhase::FinalAnswer)
1294 );
1295 assert_eq!(
1297 ExecutionPhase::from_provider_str("in_progress"),
1298 Some(ExecutionPhase::Commentary)
1299 );
1300 assert_eq!(
1301 ExecutionPhase::from_provider_str("completed"),
1302 Some(ExecutionPhase::FinalAnswer)
1303 );
1304 assert_eq!(ExecutionPhase::from_provider_str("unknown"), None);
1305 }
1306
1307 #[test]
1308 fn test_execution_phase_serde_roundtrip() {
1309 let commentary = ExecutionPhase::Commentary;
1310 let json = serde_json::to_string(&commentary).unwrap();
1311 assert_eq!(json, "\"commentary\"");
1312 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1313 assert_eq!(deserialized, ExecutionPhase::Commentary);
1314
1315 let final_answer = ExecutionPhase::FinalAnswer;
1316 let json = serde_json::to_string(&final_answer).unwrap();
1317 assert_eq!(json, "\"final_answer\"");
1318 let deserialized: ExecutionPhase = serde_json::from_str(&json).unwrap();
1319 assert_eq!(deserialized, ExecutionPhase::FinalAnswer);
1320 }
1321
1322 #[test]
1323 fn test_execution_phase_deserialize_legacy() {
1324 let legacy_in_progress: ExecutionPhase = serde_json::from_str("\"in_progress\"").unwrap();
1325 assert_eq!(legacy_in_progress, ExecutionPhase::Commentary);
1326
1327 let legacy_completed: ExecutionPhase = serde_json::from_str("\"completed\"").unwrap();
1328 assert_eq!(legacy_completed, ExecutionPhase::FinalAnswer);
1329 }
1330
1331 #[test]
1332 fn test_execution_phase_deserialize_unknown_fails() {
1333 let result = serde_json::from_str::<ExecutionPhase>("\"bogus\"");
1334 assert!(result.is_err());
1335 }
1336
1337 #[test]
1338 fn test_message_with_phase() {
1339 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::Commentary);
1340 assert_eq!(msg.phase, Some(ExecutionPhase::Commentary));
1341 }
1342
1343 #[test]
1344 fn test_message_phase_skipped_when_none() {
1345 let msg = Message::assistant("Hello");
1346 let json = serde_json::to_value(&msg).unwrap();
1347 assert!(json.get("phase").is_none());
1348 }
1349
1350 #[test]
1351 fn test_message_phase_included_when_set() {
1352 let msg = Message::assistant("Hello").with_phase(ExecutionPhase::FinalAnswer);
1353 let json = serde_json::to_value(&msg).unwrap();
1354 assert_eq!(json.get("phase").unwrap(), "final_answer");
1355 }
1356
1357 #[test]
1358 fn test_resolve_hints_both_none() {
1359 let result = Controls::resolve_hints(None, None);
1360 assert!(result.is_empty());
1361 }
1362
1363 #[test]
1364 fn test_resolve_hints_session_only() {
1365 let mut session = std::collections::HashMap::new();
1366 session.insert("key1".into(), serde_json::json!("val1"));
1367 session.insert("key2".into(), serde_json::json!(42));
1368
1369 let result = Controls::resolve_hints(Some(&session), None);
1370 assert_eq!(result.len(), 2);
1371 assert_eq!(result["key1"], serde_json::json!("val1"));
1372 assert_eq!(result["key2"], serde_json::json!(42));
1373 }
1374
1375 #[test]
1376 fn test_resolve_hints_message_only() {
1377 let mut message = std::collections::HashMap::new();
1378 message.insert("key1".into(), serde_json::json!(true));
1379
1380 let result = Controls::resolve_hints(None, Some(&message));
1381 assert_eq!(result.len(), 1);
1382 assert_eq!(result["key1"], serde_json::json!(true));
1383 }
1384
1385 #[test]
1386 fn test_resolve_hints_message_overrides_session() {
1387 let mut session = std::collections::HashMap::new();
1388 session.insert("shared".into(), serde_json::json!("session_val"));
1389 session.insert("session_only".into(), serde_json::json!(1));
1390
1391 let mut message = std::collections::HashMap::new();
1392 message.insert("shared".into(), serde_json::json!("message_val"));
1393 message.insert("message_only".into(), serde_json::json!(2));
1394
1395 let result = Controls::resolve_hints(Some(&session), Some(&message));
1396 assert_eq!(result.len(), 3);
1397 assert_eq!(result["shared"], serde_json::json!("message_val"));
1398 assert_eq!(result["session_only"], serde_json::json!(1));
1399 assert_eq!(result["message_only"], serde_json::json!(2));
1400 }
1401
1402 #[test]
1403 fn test_controls_hints_serde_roundtrip() {
1404 let mut hints = std::collections::HashMap::new();
1405 hints.insert("setup_connection".into(), serde_json::json!(true));
1406 hints.insert("theme".into(), serde_json::json!("dark"));
1407
1408 let controls = Controls {
1409 hints: Some(hints),
1410 ..Default::default()
1411 };
1412
1413 let json = serde_json::to_value(&controls).unwrap();
1414 let deserialized: Controls = serde_json::from_value(json).unwrap();
1415 let h = deserialized.hints.unwrap();
1416 assert_eq!(h["setup_connection"], serde_json::json!(true));
1417 assert_eq!(h["theme"], serde_json::json!("dark"));
1418 }
1419}