1use crate::error::AgentError;
34use serde::{Deserialize, Serialize};
35use serde_json::{json, Value};
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ToolCall {
40 pub id: Option<String>,
42 pub name: String,
44 pub arguments: String,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct ToolOutput {
51 pub id: Option<String>,
53 pub name: String,
55 pub success: bool,
57 pub output: String,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub struct ModelTurn {
64 pub text: Option<String>,
66 pub calls: Vec<ToolCall>,
68}
69
70fn args_string(v: Option<&Value>) -> String {
71 match v {
72 Some(Value::String(s)) => s.clone(),
74 Some(other) => other.to_string(),
76 None => "{}".into(),
77 }
78}
79
80fn push_text(text: &mut Option<String>, piece: &str) {
81 if piece.is_empty() {
82 return;
83 }
84 match text {
85 Some(t) => t.push_str(piece),
86 None => *text = Some(piece.to_string()),
87 }
88}
89
90pub fn parse_model_response(body: &str) -> Result<ModelTurn, AgentError> {
97 let v: Value = serde_json::from_str(body).map_err(|e| AgentError::ParseError(format!("response is not JSON: {e}")))?;
98 let mut turn = ModelTurn::default();
99
100 let message = v.pointer("/choices/0/message").or_else(|| v.get("message"));
102 if let Some(m) = message {
103 if let Some(t) = m.get("content").and_then(Value::as_str) {
104 push_text(&mut turn.text, t);
105 }
106 for c in m.get("tool_calls").and_then(Value::as_array).into_iter().flatten() {
107 let f = c.get("function").unwrap_or(c);
108 let name = f.get("name").and_then(Value::as_str).unwrap_or_default().to_string();
109 if name.is_empty() {
110 return Err(AgentError::ParseError("tool call without a name".into()));
111 }
112 turn.calls.push(ToolCall {
113 id: c.get("id").and_then(Value::as_str).map(str::to_string),
114 name,
115 arguments: args_string(f.get("arguments")),
116 });
117 }
118 return Ok(turn);
119 }
120
121 if let Some(blocks) = v.get("content").and_then(Value::as_array) {
123 for b in blocks {
124 match b.get("type").and_then(Value::as_str) {
125 Some("text") => push_text(&mut turn.text, b.get("text").and_then(Value::as_str).unwrap_or("")),
126 Some("tool_use") => turn.calls.push(ToolCall {
127 id: b.get("id").and_then(Value::as_str).map(str::to_string),
128 name: b.get("name").and_then(Value::as_str).unwrap_or_default().to_string(),
129 arguments: args_string(b.get("input")),
130 }),
131 _ => {}
132 }
133 }
134 return Ok(turn);
135 }
136
137 if let Some(items) = v.get("output").and_then(Value::as_array) {
139 for it in items {
140 match it.get("type").and_then(Value::as_str) {
141 Some("function_call") => turn.calls.push(ToolCall {
142 id: it.get("call_id").or_else(|| it.get("id")).and_then(Value::as_str).map(str::to_string),
143 name: it.get("name").and_then(Value::as_str).unwrap_or_default().to_string(),
144 arguments: args_string(it.get("arguments")),
145 }),
146 Some("message") => {
147 for c in it.get("content").and_then(Value::as_array).into_iter().flatten() {
148 push_text(&mut turn.text, c.get("text").and_then(Value::as_str).unwrap_or(""));
149 }
150 }
151 _ => {}
152 }
153 }
154 return Ok(turn);
155 }
156
157 Err(AgentError::ParseError("not an OpenAI, Anthropic or Ollama chat response".into()))
158}
159
160fn shown(o: &ToolOutput) -> String {
161 if o.success { o.output.clone() } else { format!("Error: {}", o.output) }
162}
163
164pub fn tool_results_openai(outputs: &[ToolOutput]) -> Vec<Value> {
168 outputs
169 .iter()
170 .map(|o| json!({"role": "tool", "tool_call_id": o.id.clone().unwrap_or_default(), "content": shown(o)}))
171 .collect()
172}
173
174pub fn tool_results_anthropic(outputs: &[ToolOutput]) -> Value {
176 let blocks: Vec<Value> = outputs
177 .iter()
178 .map(|o| {
179 let mut b = json!({"type": "tool_result", "tool_use_id": o.id.clone().unwrap_or_default(), "content": o.output});
180 if !o.success {
181 b["is_error"] = json!(true);
182 }
183 b
184 })
185 .collect();
186 json!({"role": "user", "content": blocks})
187}
188
189pub fn tool_results_responses(outputs: &[ToolOutput]) -> Vec<Value> {
192 outputs
193 .iter()
194 .map(|o| json!({"type": "function_call_output", "call_id": o.id.clone().unwrap_or_default(), "output": shown(o)}))
195 .collect()
196}
197
198pub(crate) fn api_schema(schema: &str) -> Result<(Value, bool), AgentError> {
203 let trimmed = schema.trim();
204 let v: Value = if trimmed.is_empty() || trimmed == "{}" {
205 json!({"type": "string"})
206 } else {
207 serde_json::from_str(trimmed).map_err(|e| AgentError::InvalidToolSignature(format!("input schema is not JSON: {e}")))?
208 };
209 if v.get("type").and_then(Value::as_str) == Some("object") {
210 Ok((v, false))
211 } else {
212 Ok((json!({"type": "object", "properties": {"input": v}, "required": ["input"]}), true))
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn openai_chat() {
222 let t = parse_model_response(
223 r#"{"choices":[{"message":{"role":"assistant","content":"Let me check.","tool_calls":[
224 {"id":"call_a","type":"function","function":{"name":"x","arguments":"{\"q\":1}"}},
225 {"id":"call_b","type":"function","function":{"name":"y","arguments":"{}"}}]}}]}"#,
226 )
227 .unwrap();
228 assert_eq!(t.text.as_deref(), Some("Let me check."));
229 assert_eq!(t.calls.len(), 2);
230 assert_eq!(t.calls[0], ToolCall { id: Some("call_a".into()), name: "x".into(), arguments: r#"{"q":1}"#.into() });
231 let fin = parse_model_response(r#"{"choices":[{"message":{"role":"assistant","content":"Paris."}}]}"#).unwrap();
232 assert!(fin.calls.is_empty());
233 assert_eq!(fin.text.as_deref(), Some("Paris."));
234 }
235
236 #[test]
237 fn anthropic_messages() {
238 let t = parse_model_response(
239 r#"{"id":"msg","type":"message","role":"assistant","stop_reason":"tool_use","content":[
240 {"type":"text","text":"Checking."},
241 {"type":"tool_use","id":"toolu_1","name":"get_weather","input":{"city":"Paris"}}]}"#,
242 )
243 .unwrap();
244 assert_eq!(t.calls[0].id.as_deref(), Some("toolu_1"));
245 assert_eq!(t.calls[0].arguments, r#"{"city":"Paris"}"#);
246 assert_eq!(t.text.as_deref(), Some("Checking."));
247 }
248
249 #[test]
250 fn openai_responses_and_ollama() {
251 let t = parse_model_response(
252 r#"{"output":[{"type":"function_call","id":"fc_1","call_id":"call_9","name":"x","arguments":"{\"a\":true}"}]}"#,
253 )
254 .unwrap();
255 assert_eq!(t.calls[0].id.as_deref(), Some("call_9"));
256 let o = parse_model_response(
257 r#"{"model":"llama3.2","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"x","arguments":{"a":1}}}]},"done":true}"#,
258 )
259 .unwrap();
260 assert_eq!(o.calls[0], ToolCall { id: None, name: "x".into(), arguments: r#"{"a":1}"#.into() });
261 assert_eq!(o.text, None);
262 }
263
264 #[test]
265 fn rejects_unknown_shapes() {
266 assert!(parse_model_response("not json").is_err());
267 assert!(parse_model_response(r#"{"hello":1}"#).is_err());
268 assert!(parse_model_response(r#"{"choices":[{"message":{"tool_calls":[{"function":{"arguments":"{}"}}]}}]}"#).is_err());
269 }
270
271 #[test]
272 fn result_formats() {
273 let outs = vec![
274 ToolOutput { id: Some("c1".into()), name: "x".into(), success: true, output: "ok".into() },
275 ToolOutput { id: Some("c2".into()), name: "y".into(), success: false, output: "boom".into() },
276 ];
277 assert_eq!(tool_results_openai(&outs)[1], json!({"role": "tool", "tool_call_id": "c2", "content": "Error: boom"}));
278 assert_eq!(tool_results_openai(&outs)[0]["content"], "ok");
279 let a = tool_results_anthropic(&outs);
280 assert_eq!(a["content"][1]["is_error"], json!(true));
281 assert!(a["content"][0].get("is_error").is_none());
282 assert_eq!(tool_results_responses(&outs)[0]["call_id"], "c1");
283 }
284
285 #[test]
286 fn schema_wrapping() {
287 let (s, wrapped) = api_schema(r#"{"type":"object","properties":{}}"#).unwrap();
288 assert!(!wrapped);
289 assert_eq!(s["type"], "object");
290 let (s, wrapped) = api_schema(r#"{"type":"string"}"#).unwrap();
291 assert!(wrapped);
292 assert_eq!(s["properties"]["input"]["type"], "string");
293 assert!(api_schema("{nope").is_err());
294 assert!(api_schema("").unwrap().1);
295 }
296}