1use serde_json::{Map, Value};
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct ToolCall {
34 pub name: String,
35 pub arguments: Value,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ToolFormat {
41 Hermes,
43 Json,
45 Pythonic,
47}
48
49pub fn parse(text: &str, fmt: ToolFormat) -> Vec<ToolCall> {
52 match fmt {
53 ToolFormat::Hermes => parse_hermes(text),
54 ToolFormat::Json => parse_json(text.trim()),
55 ToolFormat::Pythonic => parse_pythonic(text),
56 }
57}
58
59pub fn detect_and_parse(text: &str) -> Vec<ToolCall> {
62 for fmt in [ToolFormat::Hermes, ToolFormat::Pythonic, ToolFormat::Json] {
63 let calls = parse(text, fmt);
64 if !calls.is_empty() {
65 return calls;
66 }
67 }
68 Vec::new()
69}
70
71fn tool_call_from_obj(obj: &Map<String, Value>) -> Option<ToolCall> {
75 let name = obj.get("name")?.as_str()?.to_string();
76 let args = obj
77 .get("arguments")
78 .or_else(|| obj.get("parameters"))
79 .cloned()
80 .unwrap_or_else(|| Value::Object(Map::new()));
81 let args = match args {
83 Value::String(s) => serde_json::from_str(&s).unwrap_or(Value::String(s)),
84 other => other,
85 };
86 Some(ToolCall {
87 name,
88 arguments: args,
89 })
90}
91
92fn parse_json(text: &str) -> Vec<ToolCall> {
93 match serde_json::from_str::<Value>(text) {
94 Ok(Value::Object(o)) => tool_call_from_obj(&o).into_iter().collect(),
95 Ok(Value::Array(a)) => a
96 .iter()
97 .filter_map(|v| v.as_object().and_then(tool_call_from_obj))
98 .collect(),
99 _ => Vec::new(),
100 }
101}
102
103fn parse_hermes(text: &str) -> Vec<ToolCall> {
104 const OPEN: &str = "<tool_call>";
105 const CLOSE: &str = "</tool_call>";
106 let mut out = Vec::new();
107 let mut rest = text;
108 while let Some(start) = rest.find(OPEN) {
109 let after = &rest[start + OPEN.len()..];
110 let Some(end) = after.find(CLOSE) else { break };
111 let inner = after[..end].trim();
112 out.extend(parse_json(inner));
113 rest = &after[end + CLOSE.len()..];
114 }
115 out
116}
117
118fn parse_pythonic(text: &str) -> Vec<ToolCall> {
119 let Some(open) = text.find('[') else {
121 return Vec::new();
122 };
123 let Some(close_rel) = text[open..].rfind(']') else {
124 return Vec::new();
125 };
126 let body = &text[open + 1..open + close_rel];
127 let mut out = Vec::new();
128 for call in split_top_level(body, ',') {
129 let call = call.trim();
130 let Some(paren) = call.find('(') else {
131 continue;
132 };
133 if !call.ends_with(')') {
134 continue;
135 }
136 let name = call[..paren].trim();
137 if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
138 continue;
139 }
140 let args_str = &call[paren + 1..call.len() - 1];
141 let mut args = Map::new();
142 for pair in split_top_level(args_str, ',') {
143 let pair = pair.trim();
144 if pair.is_empty() {
145 continue;
146 }
147 if let Some(eq) = pair.find('=') {
148 let key = pair[..eq].trim().to_string();
149 let val = pair[eq + 1..].trim();
150 args.insert(key, literal_to_value(val));
151 }
152 }
153 out.push(ToolCall {
154 name: name.to_string(),
155 arguments: Value::Object(args),
156 });
157 }
158 out
159}
160
161fn literal_to_value(s: &str) -> Value {
164 if let Ok(v) = serde_json::from_str::<Value>(s) {
165 return v;
166 }
167 if s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'') {
169 return Value::String(s[1..s.len() - 1].to_string());
170 }
171 match s {
172 "True" => Value::Bool(true),
173 "False" => Value::Bool(false),
174 "None" => Value::Null,
175 other => Value::String(other.to_string()),
176 }
177}
178
179fn split_top_level(s: &str, sep: char) -> Vec<String> {
182 let mut parts = Vec::new();
183 let mut depth = 0i32;
184 let mut quote: Option<char> = None;
185 let mut cur = String::new();
186 let mut chars = s.chars().peekable();
187 while let Some(c) = chars.next() {
188 match quote {
189 Some(q) => {
190 cur.push(c);
191 if c == '\\' {
192 if let Some(&n) = chars.peek() {
193 cur.push(n);
194 chars.next();
195 }
196 } else if c == q {
197 quote = None;
198 }
199 }
200 None => match c {
201 '"' | '\'' => {
202 quote = Some(c);
203 cur.push(c);
204 }
205 '(' | '[' | '{' => {
206 depth += 1;
207 cur.push(c);
208 }
209 ')' | ']' | '}' => {
210 depth -= 1;
211 cur.push(c);
212 }
213 _ if c == sep && depth == 0 => {
214 parts.push(std::mem::take(&mut cur));
215 }
216 _ => cur.push(c),
217 },
218 }
219 }
220 if !cur.trim().is_empty() || !parts.is_empty() {
221 parts.push(cur);
222 }
223 parts
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use serde_json::json;
230
231 #[test]
232 fn hermes_single_call() {
233 let text = r#"<tool_call>{"name":"get_weather","arguments":{"city":"Paris"}}</tool_call>"#;
234 let calls = parse(text, ToolFormat::Hermes);
235 assert_eq!(calls.len(), 1);
236 assert_eq!(calls[0].name, "get_weather");
237 assert_eq!(calls[0].arguments, json!({"city":"Paris"}));
238 }
239
240 #[test]
241 fn hermes_multiple_calls_with_surrounding_text() {
242 let text = "sure!\n<tool_call>{\"name\":\"a\",\"arguments\":{}}</tool_call>\n\
243 <tool_call>{\"name\":\"b\",\"parameters\":{\"x\":1}}</tool_call>";
244 let calls = parse(text, ToolFormat::Hermes);
245 assert_eq!(calls.len(), 2);
246 assert_eq!(calls[0].name, "a");
247 assert_eq!(calls[1].name, "b");
248 assert_eq!(calls[1].arguments, json!({"x":1}));
249 }
250
251 #[test]
252 fn json_double_encoded_arguments() {
253 let text = r#"{"name":"f","arguments":"{\"k\":2}"}"#;
254 let calls = parse(text, ToolFormat::Json);
255 assert_eq!(calls.len(), 1);
256 assert_eq!(calls[0].arguments, json!({"k":2}));
257 }
258
259 #[test]
260 fn pythonic_call() {
261 let text = r#"[get_weather(city="Paris", days=2, metric=true)]"#;
262 let calls = parse(text, ToolFormat::Pythonic);
263 assert_eq!(calls.len(), 1);
264 assert_eq!(calls[0].name, "get_weather");
265 assert_eq!(
266 calls[0].arguments,
267 json!({"city":"Paris","days":2,"metric":true})
268 );
269 }
270
271 #[test]
272 fn pythonic_with_tag_wrapper_and_commas_in_strings() {
273 let text = r#"<|tool_call_start|>[note(text="a, b, c")]<|tool_call_end|>"#;
274 let calls = parse(text, ToolFormat::Pythonic);
275 assert_eq!(calls.len(), 1);
276 assert_eq!(calls[0].arguments, json!({"text":"a, b, c"}));
277 }
278
279 #[test]
280 fn detect_picks_the_right_format() {
281 assert_eq!(detect_and_parse("just text"), vec![]);
282 assert_eq!(
283 detect_and_parse(r#"<tool_call>{"name":"x","arguments":{}}</tool_call>"#)[0].name,
284 "x"
285 );
286 assert_eq!(detect_and_parse(r#"[y(a=1)]"#)[0].name, "y");
287 }
288}