1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ParsedToolCall {
15 pub name: String,
17 pub arguments: Value,
19 pub format: ToolCallFormat,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum ToolCallFormat {
26 JsonObject,
28 JsonArray,
30 Xml,
32 Markdown,
34}
35
36pub struct ParseResult {
38 pub text: String,
40 pub tool_calls: Vec<ParsedToolCall>,
42}
43
44pub fn parse(response: &str) -> ParseResult {
49 if let Some(result) = try_parse_json_object(response) {
51 return result;
52 }
53
54 if let Some(result) = try_parse_json_array(response) {
56 return result;
57 }
58
59 if let Some(result) = try_parse_xml(response) {
61 return result;
62 }
63
64 if let Some(result) = try_parse_markdown(response) {
66 return result;
67 }
68
69 ParseResult {
71 text: response.to_string(),
72 tool_calls: vec![],
73 }
74}
75
76fn try_parse_json_object(response: &str) -> Option<ParseResult> {
79 let trimmed = response.trim();
80
81 if !trimmed.starts_with('{') {
83 return None;
84 }
85
86 let parsed: Value = serde_json::from_str(trimmed).ok()?;
87
88 if let Some(tc) = parsed.get("tool_call") {
90 let name = tc.get("name")?.as_str()?.to_string();
91 let arguments = tc
92 .get("arguments")
93 .cloned()
94 .unwrap_or(Value::Object(Default::default()));
95 return Some(ParseResult {
96 text: String::new(),
97 tool_calls: vec![ParsedToolCall {
98 name,
99 arguments,
100 format: ToolCallFormat::JsonObject,
101 }],
102 });
103 }
104
105 if let Some(tcs) = parsed.get("tool_calls").and_then(|v| v.as_array()) {
107 let calls: Vec<ParsedToolCall> = tcs
108 .iter()
109 .filter_map(|tc| {
110 let name = tc.get("name")?.as_str()?.to_string();
111 let arguments = tc
112 .get("arguments")
113 .cloned()
114 .unwrap_or(Value::Object(Default::default()));
115 Some(ParsedToolCall {
116 name,
117 arguments,
118 format: ToolCallFormat::JsonObject,
119 })
120 })
121 .collect();
122
123 if !calls.is_empty() {
124 return Some(ParseResult {
125 text: String::new(),
126 tool_calls: calls,
127 });
128 }
129 }
130
131 if let (Some(name), Some(_)) = (
133 parsed.get("name").and_then(|v| v.as_str()),
134 parsed.get("arguments"),
135 ) {
136 return Some(ParseResult {
137 text: String::new(),
138 tool_calls: vec![ParsedToolCall {
139 name: name.to_string(),
140 arguments: parsed
141 .get("arguments")
142 .cloned()
143 .unwrap_or(Value::Object(Default::default())),
144 format: ToolCallFormat::JsonObject,
145 }],
146 });
147 }
148
149 None
150}
151
152fn try_parse_json_array(response: &str) -> Option<ParseResult> {
154 let trimmed = response.trim();
155
156 if !trimmed.starts_with('[') {
157 return None;
158 }
159
160 let parsed: Vec<Value> = serde_json::from_str(trimmed).ok()?;
161
162 let calls: Vec<ParsedToolCall> = parsed
163 .iter()
164 .filter_map(|tc| {
165 let name = tc.get("name")?.as_str()?.to_string();
166 let arguments = tc
167 .get("arguments")
168 .cloned()
169 .unwrap_or(Value::Object(Default::default()));
170 Some(ParsedToolCall {
171 name,
172 arguments,
173 format: ToolCallFormat::JsonArray,
174 })
175 })
176 .collect();
177
178 if calls.is_empty() {
179 return None;
180 }
181
182 Some(ParseResult {
183 text: String::new(),
184 tool_calls: calls,
185 })
186}
187
188fn try_parse_xml(response: &str) -> Option<ParseResult> {
191 let mut calls = Vec::new();
192 let mut text_parts = Vec::new();
193 let mut remaining = response;
194
195 while let Some(start) = remaining.find("<tool_call>") {
196 let before = &remaining[..start];
198 if !before.trim().is_empty() {
199 text_parts.push(before.trim().to_string());
200 }
201
202 let after_start = &remaining[start + "<tool_call>".len()..];
203 let end = after_start.find("</tool_call>")?;
204 let inner = &after_start[..end];
205
206 let name_start = inner.find("<name>")? + "<name>".len();
208 let name_end = inner.find("</name>")?;
209 let name = inner[name_start..name_end].trim().to_string();
210
211 let args = if let Some(args_start_pos) = inner.find("<arguments>") {
213 let args_content_start = args_start_pos + "<arguments>".len();
214 if let Some(args_end_pos) = inner.find("</arguments>") {
215 let args_str = inner[args_content_start..args_end_pos].trim();
216 serde_json::from_str(args_str).unwrap_or(Value::Object(Default::default()))
217 } else {
218 Value::Object(Default::default())
219 }
220 } else {
221 Value::Object(Default::default())
222 };
223
224 calls.push(ParsedToolCall {
225 name,
226 arguments: args,
227 format: ToolCallFormat::Xml,
228 });
229
230 remaining = &after_start[end + "</tool_call>".len()..];
231 }
232
233 if !remaining.trim().is_empty() {
235 text_parts.push(remaining.trim().to_string());
236 }
237
238 if calls.is_empty() {
239 return None;
240 }
241
242 Some(ParseResult {
243 text: text_parts.join("\n"),
244 tool_calls: calls,
245 })
246}
247
248fn try_parse_markdown(response: &str) -> Option<ParseResult> {
253 let mut calls = Vec::new();
254 let mut text_parts = Vec::new();
255 let mut remaining = response;
256
257 let fence_patterns = ["```tool_call\n", "```tool_call\r\n"];
258
259 loop {
260 let mut found = false;
261 for pattern in &fence_patterns {
262 if let Some(start) = remaining.find(pattern) {
263 let before = &remaining[..start];
265 if !before.trim().is_empty() {
266 text_parts.push(before.trim().to_string());
267 }
268
269 let content_start = start + pattern.len();
270 let after_content = &remaining[content_start..];
271
272 if let Some(end) = after_content.find("```") {
273 let block_content = after_content[..end].trim();
274
275 if let Ok(parsed) = serde_json::from_str::<Value>(block_content) {
277 if let Some(name) = parsed.get("name").and_then(|v| v.as_str()) {
278 let arguments = parsed
279 .get("arguments")
280 .cloned()
281 .unwrap_or(Value::Object(Default::default()));
282 calls.push(ParsedToolCall {
283 name: name.to_string(),
284 arguments,
285 format: ToolCallFormat::Markdown,
286 });
287 }
288 }
289
290 remaining = &after_content[end + "```".len()..];
291 found = true;
292 break;
293 }
294 }
295 }
296
297 if !found {
298 break;
299 }
300 }
301
302 if !remaining.trim().is_empty() {
304 text_parts.push(remaining.trim().to_string());
305 }
306
307 if calls.is_empty() {
308 return None;
309 }
310
311 Some(ParseResult {
312 text: text_parts.join("\n"),
313 tool_calls: calls,
314 })
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
324 fn test_parse_json_tool_call() {
325 let input = r#"{"tool_call": {"name": "search", "arguments": {"query": "rust"}}}"#;
326 let result = parse(input);
327 assert_eq!(result.tool_calls.len(), 1);
328 assert_eq!(result.tool_calls[0].name, "search");
329 assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonObject);
330 assert_eq!(result.tool_calls[0].arguments["query"], "rust");
331 }
332
333 #[test]
334 fn test_parse_json_tool_calls_array() {
335 let input = r#"{"tool_calls": [
336 {"name": "search", "arguments": {"query": "rust"}},
337 {"name": "read_file", "arguments": {"path": "/tmp/test"}}
338 ]}"#;
339 let result = parse(input);
340 assert_eq!(result.tool_calls.len(), 2);
341 assert_eq!(result.tool_calls[0].name, "search");
342 assert_eq!(result.tool_calls[1].name, "read_file");
343 }
344
345 #[test]
346 fn test_parse_bare_json_tool_call() {
347 let input = r#"{"name": "search", "arguments": {"query": "rust"}}"#;
348 let result = parse(input);
349 assert_eq!(result.tool_calls.len(), 1);
350 assert_eq!(result.tool_calls[0].name, "search");
351 }
352
353 #[test]
356 fn test_parse_json_array() {
357 let input = r#"[{"name": "search", "arguments": {"query": "rust"}}]"#;
358 let result = parse(input);
359 assert_eq!(result.tool_calls.len(), 1);
360 assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonArray);
361 }
362
363 #[test]
366 fn test_parse_xml() {
367 let input = r#"Let me search for that.
368<tool_call><name>search</name><arguments>{"query": "rust"}</arguments></tool_call>"#;
369 let result = parse(input);
370 assert_eq!(result.tool_calls.len(), 1);
371 assert_eq!(result.tool_calls[0].name, "search");
372 assert_eq!(result.tool_calls[0].format, ToolCallFormat::Xml);
373 assert!(result.text.contains("Let me search"));
374 }
375
376 #[test]
377 fn test_parse_multiple_xml() {
378 let input = r#"<tool_call><name>search</name><arguments>{"q": "a"}</arguments></tool_call>
379<tool_call><name>read</name><arguments>{"path": "b"}</arguments></tool_call>"#;
380 let result = parse(input);
381 assert_eq!(result.tool_calls.len(), 2);
382 }
383
384 #[test]
387 fn test_parse_markdown() {
388 let input = "Here's what I'll do:\n```tool_call\n{\"name\": \"search\", \"arguments\": {\"query\": \"rust\"}}\n```\n";
389 let result = parse(input);
390 assert_eq!(result.tool_calls.len(), 1);
391 assert_eq!(result.tool_calls[0].name, "search");
392 assert_eq!(result.tool_calls[0].format, ToolCallFormat::Markdown);
393 assert!(result.text.contains("Here's what I'll do"));
394 }
395
396 #[test]
399 fn test_parse_plain_text() {
400 let input = "This is just a normal response with no tool calls.";
401 let result = parse(input);
402 assert!(result.tool_calls.is_empty());
403 assert_eq!(result.text, input);
404 }
405
406 #[test]
407 fn test_parse_empty() {
408 let result = parse("");
409 assert!(result.tool_calls.is_empty());
410 }
411
412 #[test]
415 fn test_json_takes_priority_over_xml() {
416 let input = r#"{"tool_call": {"name": "search", "arguments": {}}}"#;
418 let result = parse(input);
419 assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonObject);
420 }
421}