Skip to main content

vv_agent/types/dict/
messages.rs

1use super::common::*;
2use super::*;
3
4impl Message {
5    pub fn to_openai_message(&self, include_reasoning_content: bool) -> Value {
6        let mut payload = serde_json::Map::from_iter([
7            (
8                "role".to_string(),
9                Value::String(message_role_value(self.role).to_string()),
10            ),
11            ("content".to_string(), Value::String(self.content.clone())),
12        ]);
13        insert_non_empty_optional_string(&mut payload, "name", &self.name);
14        insert_non_empty_optional_string(&mut payload, "tool_call_id", &self.tool_call_id);
15        if self.role == MessageRole::Assistant && !self.tool_calls.is_empty() {
16            payload.insert(
17                "tool_calls".to_string(),
18                Value::Array(self.tool_calls.iter().map(tool_call_to_openai).collect()),
19            );
20            if self.content.is_empty() {
21                payload.insert("content".to_string(), Value::Null);
22            }
23        }
24        if include_reasoning_content && self.role == MessageRole::Assistant {
25            insert_non_empty_optional_string(
26                &mut payload,
27                "reasoning_content",
28                &self.reasoning_content,
29            );
30        }
31        if self.role == MessageRole::User {
32            if let Some(image_url) = self.image_url.as_deref().filter(|value| !value.is_empty()) {
33                let mut blocks = Vec::new();
34                if !self.content.is_empty() {
35                    blocks.push(serde_json::json!({
36                        "type": "text",
37                        "text": self.content,
38                    }));
39                }
40                blocks.push(serde_json::json!({
41                    "type": "image_url",
42                    "image_url": {"url": image_url},
43                }));
44                payload.insert("content".to_string(), Value::Array(blocks));
45            }
46        }
47        Value::Object(payload)
48    }
49
50    pub fn to_dict(&self) -> Value {
51        let mut payload = serde_json::Map::from_iter([
52            (
53                "role".to_string(),
54                Value::String(message_role_value(self.role).to_string()),
55            ),
56            ("content".to_string(), Value::String(self.content.clone())),
57        ]);
58        insert_optional_string(&mut payload, "name", &self.name);
59        insert_optional_string(&mut payload, "tool_call_id", &self.tool_call_id);
60        if !self.tool_calls.is_empty() {
61            payload.insert(
62                "tool_calls".to_string(),
63                Value::Array(self.tool_calls.iter().map(tool_call_to_openai).collect()),
64            );
65        }
66        insert_optional_string(&mut payload, "reasoning_content", &self.reasoning_content);
67        insert_optional_string(&mut payload, "image_url", &self.image_url);
68        if !self.metadata.is_empty() {
69            payload.insert("metadata".to_string(), metadata_to_value(&self.metadata));
70        }
71        if let Some(artifact_ref) = &self.artifact_ref {
72            payload.insert(
73                "artifact_ref".to_string(),
74                serde_json::to_value(artifact_ref).expect("ToolArtifactRef is serializable"),
75            );
76        }
77        Value::Object(payload)
78    }
79
80    pub fn from_dict(data: &Value) -> Result<Self, String> {
81        let object = expect_object(data, "Message")?;
82        const ALLOWED_FIELDS: &[&str] = &[
83            "role",
84            "content",
85            "name",
86            "tool_call_id",
87            "tool_calls",
88            "reasoning_content",
89            "image_url",
90            "metadata",
91            "artifact_ref",
92        ];
93        let unknown = object
94            .keys()
95            .filter(|field| !ALLOWED_FIELDS.contains(&field.as_str()))
96            .cloned()
97            .collect::<Vec<_>>();
98        if !unknown.is_empty() {
99            return Err(format!("Message contains unknown fields: {unknown:?}"));
100        }
101        let role = parse_message_role(read_required_string(object, "role")?)?;
102        let content = read_required_string(object, "content")?.to_string();
103        let tool_calls = read_array(object, "tool_calls")
104            .unwrap_or(&[])
105            .iter()
106            .map(ToolCall::from_dict)
107            .collect::<Result<Vec<_>, _>>()?;
108        for field in ["name", "tool_call_id", "reasoning_content", "image_url"] {
109            if object
110                .get(field)
111                .is_some_and(|value| !value.is_null() && !value.is_string())
112            {
113                return Err(format!("Message field {field:?} must be a string or null"));
114            }
115        }
116        let artifact_ref = object
117            .get("artifact_ref")
118            .map(|value| {
119                if !value.is_object() {
120                    return Err("Message field \"artifact_ref\" must be an object".to_string());
121                }
122                let artifact = serde_json::from_value::<ToolArtifactRef>(value.clone())
123                    .map_err(|error| error.to_string())?;
124                artifact.validate()?;
125                Ok(artifact)
126            })
127            .transpose()?;
128        Ok(Self {
129            role,
130            content,
131            name: read_optional_string(object, "name"),
132            tool_call_id: read_optional_string(object, "tool_call_id"),
133            tool_calls,
134            reasoning_content: read_optional_string(object, "reasoning_content"),
135            image_url: read_optional_string(object, "image_url"),
136            metadata: read_metadata(object, "metadata")?,
137            artifact_ref,
138        })
139    }
140}
141
142fn tool_call_to_openai(tool_call: &ToolCall) -> Value {
143    let mut payload = serde_json::json!({
144        "id": tool_call.id,
145        "type": "function",
146        "function": {
147            "name": tool_call.name,
148            "arguments": Value::Object(tool_call.arguments.clone().into_iter().collect()).to_string(),
149        },
150    });
151    if let Some(extra_content) = &tool_call.extra_content {
152        payload["extra_content"] = extra_content.clone();
153    }
154    payload
155}
156
157impl ToolCall {
158    pub fn to_dict(&self) -> Value {
159        let mut payload = serde_json::Map::from_iter([
160            ("id".to_string(), Value::String(self.id.clone())),
161            ("name".to_string(), Value::String(self.name.clone())),
162            (
163                "arguments".to_string(),
164                Value::Object(self.arguments.clone().into_iter().collect()),
165            ),
166        ]);
167        if let Some(extra_content) = &self.extra_content {
168            payload.insert("extra_content".to_string(), extra_content.clone());
169        }
170        Value::Object(payload)
171    }
172
173    pub fn from_dict(data: &Value) -> Result<Self, String> {
174        let object = expect_object(data, "ToolCall")?;
175        if let Some(function) = object.get("function").and_then(Value::as_object) {
176            let id = read_required_string(object, "id")?.to_string();
177            let name = read_required_string(function, "name")?.to_string();
178            let raw_arguments = function
179                .get("arguments")
180                .cloned()
181                .unwrap_or_else(|| Value::String("{}".to_string()));
182            let mut tool_call = ToolCall::from_raw_arguments(id, name, raw_arguments);
183            tool_call.extra_content = object
184                .get("extra_content")
185                .filter(|value| value.is_object())
186                .cloned();
187            return Ok(tool_call);
188        }
189        Ok(Self {
190            id: read_required_string(object, "id")?.to_string(),
191            name: read_required_string(object, "name")?.to_string(),
192            arguments: read_metadata(object, "arguments")?,
193            extra_content: object
194                .get("extra_content")
195                .filter(|value| value.is_object())
196                .cloned(),
197        })
198    }
199}