dejadb_core/format/tool_schema/
parse.rs1use std::sync::OnceLock;
18
19use serde_json::Value;
20
21use super::ProviderKind;
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize)]
25#[cfg_attr(feature = "http", derive(utoipa::ToSchema))]
26pub struct ParsedToolCall {
27 pub id: String,
30 pub name: String,
32 #[cfg_attr(feature = "http", schema(value_type = serde_json::Value))]
35 pub arguments: Value,
36}
37
38#[derive(Debug, Clone, PartialEq, serde::Serialize)]
41#[cfg_attr(feature = "http", derive(utoipa::ToSchema))]
42pub struct ParseError {
43 pub reason: String,
44 pub position: Option<usize>,
46}
47
48pub fn parse(provider: ProviderKind, raw_output: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
55 match provider {
56 ProviderKind::Hermes => parse_hermes(raw_output),
57 ProviderKind::Llama31 => parse_llama31(raw_output),
58 ProviderKind::AnthropicTools => parse_anthropic(raw_output),
59 ProviderKind::OpenAiTools | ProviderKind::OpenAiResponses => parse_openai(raw_output),
60 ProviderKind::MarkdownTools => parse_markdown(raw_output),
61 ProviderKind::GeminiTools | ProviderKind::McpTools | ProviderKind::SmlTools => (
62 vec![],
63 vec![ParseError {
64 reason: format!("parse is not supported for provider {}", provider.as_str()),
65 position: None,
66 }],
67 ),
68 }
69}
70
71static HERMES_RE: OnceLock<regex::Regex> = OnceLock::new();
72static LLAMA31_RE: OnceLock<regex::Regex> = OnceLock::new();
73static MARKDOWN_RE: OnceLock<regex::Regex> = OnceLock::new();
74
75fn bounded(pat: &'static str) -> regex::Regex {
76 regex::RegexBuilder::new(pat)
77 .size_limit(1 << 20)
78 .dot_matches_new_line(true)
79 .build()
80 .expect("compile-time-valid pattern")
81}
82
83fn parse_hermes(raw: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
84 let re = HERMES_RE.get_or_init(|| bounded(r"<tool_call>\s*(\{.*?\})\s*</tool_call>"));
85 let mut calls = Vec::new();
86 let mut errors = Vec::new();
87 for (idx, cap) in re.captures_iter(raw).enumerate() {
88 let m = cap.get(1).unwrap();
89 match serde_json::from_str::<Value>(m.as_str()) {
90 Ok(v) => {
91 if let Some(c) = normalize_call(idx, &v) {
92 calls.push(c);
93 } else {
94 errors.push(ParseError {
95 reason: "hermes tool_call missing name/arguments".into(),
96 position: Some(m.start()),
97 });
98 }
99 }
100 Err(e) => errors.push(ParseError {
101 reason: format!("hermes tool_call JSON: {e}"),
102 position: Some(m.start()),
103 }),
104 }
105 }
106 (calls, errors)
107}
108
109fn parse_llama31(raw: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
110 let re = LLAMA31_RE
112 .get_or_init(|| bounded(r"(?:<\|python_tag\|>)?\s*(\{.*?\})\s*<\|e(?:om|ot)_id\|>"));
113 let mut calls = Vec::new();
114 let mut errors = Vec::new();
115 for (idx, cap) in re.captures_iter(raw).enumerate() {
116 let m = cap.get(1).unwrap();
117 match serde_json::from_str::<Value>(m.as_str()) {
118 Ok(v) => {
119 if let Some(c) = normalize_call(idx, &v) {
120 calls.push(c);
121 } else {
122 errors.push(ParseError {
123 reason: "llama31 call missing name".into(),
124 position: Some(m.start()),
125 });
126 }
127 }
128 Err(e) => errors.push(ParseError {
129 reason: format!("llama31 call JSON: {e}"),
130 position: Some(m.start()),
131 }),
132 }
133 }
134 (calls, errors)
135}
136
137fn parse_anthropic(raw: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
138 let mut calls = Vec::new();
142 let mut errors = Vec::new();
143
144 if let Ok(Value::Array(blocks)) = serde_json::from_str::<Value>(raw) {
146 for (idx, block) in blocks.iter().enumerate() {
147 if block.get("type").and_then(|v| v.as_str()) == Some("tool_use") {
148 let id = block
149 .get("id")
150 .and_then(|v| v.as_str())
151 .map(str::to_string)
152 .unwrap_or_else(|| format!("call_{idx}"));
153 let name = match block.get("name").and_then(|v| v.as_str()) {
154 Some(n) => n.to_string(),
155 None => {
156 errors.push(ParseError {
157 reason: "anthropic tool_use missing name".into(),
158 position: None,
159 });
160 continue;
161 }
162 };
163 let arguments = block
164 .get("input")
165 .cloned()
166 .unwrap_or_else(|| serde_json::json!({}));
167 calls.push(ParsedToolCall {
168 id,
169 name,
170 arguments,
171 });
172 }
173 }
174 if !calls.is_empty() || !errors.is_empty() {
175 return (calls, errors);
176 }
177 }
178
179 static ANTHROPIC_INVOKE_RE: OnceLock<regex::Regex> = OnceLock::new();
181 let invoke_re = ANTHROPIC_INVOKE_RE
182 .get_or_init(|| bounded(r#"<invoke\s+name="([^"]+)"\s*>(.*?)</invoke>"#));
183 static PARAM_RE: OnceLock<regex::Regex> = OnceLock::new();
184 let param_re =
185 PARAM_RE.get_or_init(|| bounded(r#"<parameter\s+name="([^"]+)"\s*>(.*?)</parameter>"#));
186
187 for (idx, cap) in invoke_re.captures_iter(raw).enumerate() {
188 let name = cap.get(1).unwrap().as_str().to_string();
189 let inner = cap.get(2).unwrap().as_str();
190 let mut args = serde_json::Map::new();
191 for pcap in param_re.captures_iter(inner) {
192 let k = pcap.get(1).unwrap().as_str().to_string();
193 let v = pcap.get(2).unwrap().as_str();
194 let value = serde_json::from_str::<Value>(v.trim())
196 .unwrap_or_else(|_| Value::String(v.to_string()));
197 args.insert(k, value);
198 }
199 calls.push(ParsedToolCall {
200 id: format!("call_{idx}"),
201 name,
202 arguments: Value::Object(args),
203 });
204 }
205 (calls, errors)
206}
207
208fn parse_openai(raw: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
209 let mut calls = Vec::new();
212 let mut errors = Vec::new();
213 let parsed: Value = match serde_json::from_str(raw) {
214 Ok(v) => v,
215 Err(e) => {
216 return (
217 calls,
218 vec![ParseError {
219 reason: format!("openai JSON parse: {e}"),
220 position: None,
221 }],
222 )
223 }
224 };
225 let tool_calls = parsed
226 .get("tool_calls")
227 .or_else(|| {
228 parsed
229 .get("choices")
230 .and_then(|c| c.get(0))
231 .and_then(|c| c.get("message"))
232 .and_then(|m| m.get("tool_calls"))
233 })
234 .or(Some(&parsed)) .and_then(|v| v.as_array());
236 let Some(arr) = tool_calls else {
237 return (
238 calls,
239 vec![ParseError {
240 reason: "openai response has no tool_calls".into(),
241 position: None,
242 }],
243 );
244 };
245 for (idx, tc) in arr.iter().enumerate() {
246 let id = tc
249 .get("id")
250 .or_else(|| tc.get("call_id"))
251 .and_then(|v| v.as_str())
252 .map(str::to_string)
253 .unwrap_or_else(|| format!("call_{idx}"));
254 let (name, args_raw) = match tc.get("function") {
255 Some(f) => (
256 f.get("name").and_then(|v| v.as_str()).map(str::to_string),
257 f.get("arguments").cloned(),
258 ),
259 None => (
260 tc.get("name").and_then(|v| v.as_str()).map(str::to_string),
261 tc.get("arguments").cloned(),
262 ),
263 };
264 let Some(name) = name else {
265 errors.push(ParseError {
266 reason: "openai tool_call missing name".into(),
267 position: None,
268 });
269 continue;
270 };
271 let arguments = match args_raw {
273 Some(Value::String(s)) => serde_json::from_str::<Value>(&s).unwrap_or(Value::String(s)),
274 Some(other) => other,
275 None => Value::Object(Default::default()),
276 };
277 calls.push(ParsedToolCall {
278 id,
279 name,
280 arguments,
281 });
282 }
283 (calls, errors)
284}
285
286fn parse_markdown(raw: &str) -> (Vec<ParsedToolCall>, Vec<ParseError>) {
287 let re = MARKDOWN_RE.get_or_init(|| bounded(r"```(?:json)?\s*(\{.*?\})\s*```"));
289 let mut calls = Vec::new();
290 let mut errors = Vec::new();
291 for (idx, cap) in re.captures_iter(raw).enumerate() {
292 let m = cap.get(1).unwrap();
293 match serde_json::from_str::<Value>(m.as_str()) {
294 Ok(v) => {
295 if let Some(c) = normalize_call(idx, &v) {
296 calls.push(c);
297 } else {
298 errors.push(ParseError {
299 reason: "markdown fenced JSON has no name".into(),
300 position: Some(m.start()),
301 });
302 }
303 }
304 Err(e) => errors.push(ParseError {
305 reason: format!("markdown fenced JSON: {e}"),
306 position: Some(m.start()),
307 }),
308 }
309 }
310 (calls, errors)
311}
312
313fn normalize_call(idx: usize, v: &Value) -> Option<ParsedToolCall> {
315 let name = v.get("name").and_then(|x| x.as_str())?.to_string();
316 let id = v
317 .get("id")
318 .and_then(|x| x.as_str())
319 .map(str::to_string)
320 .unwrap_or_else(|| format!("call_{idx}"));
321 let arguments = v
322 .get("arguments")
323 .or_else(|| v.get("parameters"))
324 .or_else(|| v.get("input"))
325 .cloned()
326 .unwrap_or_else(|| Value::Object(Default::default()));
327 let arguments = match arguments {
328 Value::String(s) => serde_json::from_str::<Value>(&s).unwrap_or(Value::String(s)),
330 other => other,
331 };
332 Some(ParsedToolCall {
333 id,
334 name,
335 arguments,
336 })
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use serde_json::json;
343
344 #[test]
345 fn hermes_single_call() {
346 let raw = r##"<tool_call>{"name":"slack_post_message","arguments":{"channel":"#ops","text":"hi"}}</tool_call>"##;
347 let (calls, errs) = parse(ProviderKind::Hermes, raw);
348 assert!(errs.is_empty());
349 assert_eq!(calls.len(), 1);
350 assert_eq!(calls[0].name, "slack_post_message");
351 assert_eq!(calls[0].arguments, json!({"channel":"#ops","text":"hi"}));
352 }
353
354 #[test]
355 fn hermes_multiple_calls() {
356 let raw = r##"chat<tool_call>{"name":"a","arguments":{}}</tool_call><tool_call>{"name":"b","arguments":{}}</tool_call>end"##;
357 let (calls, _) = parse(ProviderKind::Hermes, raw);
358 assert_eq!(calls.len(), 2);
359 assert_eq!(calls[0].name, "a");
360 assert_eq!(calls[1].name, "b");
361 }
362
363 #[test]
364 fn hermes_malformed_emits_error_not_panic() {
365 let raw = r##"<tool_call>{"name":}</tool_call>"##;
366 let (calls, errs) = parse(ProviderKind::Hermes, raw);
367 assert!(calls.is_empty());
368 assert_eq!(errs.len(), 1);
369 }
370
371 #[test]
372 fn llama31_python_tag() {
373 let raw = r##"<|python_tag|>{"name":"calc","arguments":{"x":2}}<|eom_id|>"##;
374 let (calls, _) = parse(ProviderKind::Llama31, raw);
375 assert_eq!(calls.len(), 1);
376 assert_eq!(calls[0].name, "calc");
377 }
378
379 #[test]
380 fn openai_chat_completions_shape() {
381 let raw = r##"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"f","arguments":"{\"x\":1}"}}]}"##;
382 let (calls, _) = parse(ProviderKind::OpenAiTools, raw);
383 assert_eq!(calls.len(), 1);
384 assert_eq!(calls[0].id, "call_1");
385 assert_eq!(calls[0].arguments, json!({"x":1}));
386 }
387
388 #[test]
389 fn anthropic_native_tool_use_block() {
390 let raw = r##"[{"type":"tool_use","id":"toolu_1","name":"f","input":{"x":1}}]"##;
391 let (calls, _) = parse(ProviderKind::AnthropicTools, raw);
392 assert_eq!(calls.len(), 1);
393 assert_eq!(calls[0].id, "toolu_1");
394 assert_eq!(calls[0].name, "f");
395 }
396
397 #[test]
398 fn markdown_fenced_json() {
399 let raw = "Here:\n```json\n{\"name\":\"f\",\"arguments\":{\"x\":1}}\n```";
400 let (calls, _) = parse(ProviderKind::MarkdownTools, raw);
401 assert_eq!(calls.len(), 1);
402 assert_eq!(calls[0].arguments, json!({"x":1}));
403 }
404
405 #[test]
406 fn unsupported_provider_returns_error() {
407 let (calls, errs) = parse(ProviderKind::GeminiTools, "whatever");
408 assert!(calls.is_empty());
409 assert_eq!(errs.len(), 1);
410 }
411}