1use async_trait::async_trait;
2use openai_protocol::common::Tool;
3use regex::Regex;
4use serde_json::Value;
5
6use crate::{
7 errors::{ParserError, ParserResult},
8 parsers::helpers,
9 traits::ToolParser,
10 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
11};
12
13pub struct QwenXmlParser {
25 extractor: Regex,
27
28 buffer: String,
30
31 prev_tool_call_arr: Vec<Value>,
33
34 current_tool_id: i32,
36
37 current_tool_name_sent: bool,
39
40 streamed_args_for_tool: Vec<String>,
42
43 tool_call_start_token: &'static str,
45 tool_call_end_token: &'static str,
46
47 in_tool_call: bool,
49 current_function_name: String,
50 current_parameters: serde_json::Map<String, Value>,
51
52 xml_function_pattern: Regex,
54 xml_param_pattern: Regex,
55}
56
57fn safe_val(raw: &str) -> Value {
71 let trimmed = raw.trim();
72
73 if let Ok(v) = serde_json::from_str::<Value>(trimmed) {
75 return v;
76 }
77
78 match trimmed {
80 "True" => return Value::Bool(true),
81 "False" => return Value::Bool(false),
82 "None" => return Value::Null,
83 _ => {}
84 }
85
86 Value::String(trimmed.to_string())
88}
89
90fn coerce_value(raw: &str, declared_type: Option<&str>) -> Value {
96 let trimmed = raw.trim();
97 helpers::coerce_by_schema_type(trimmed, declared_type).unwrap_or_else(|| safe_val(raw))
98}
99
100impl QwenXmlParser {
101 #[expect(
103 clippy::expect_used,
104 reason = "regex patterns are compile-time string literals"
105 )]
106 pub fn new() -> Self {
107 let pattern = r"(?s)<tool_call>\s*(.*?)\s*</tool_call>";
109 let extractor = Regex::new(pattern).expect("Valid regex pattern");
110
111 let xml_function_pattern =
113 Regex::new(r"<function=([^>]+)>").expect("Valid XML function pattern");
114 let xml_param_pattern = Regex::new(r"(?s)<parameter=([^>]+)>(.*?)</parameter>")
115 .expect("Valid XML parameter pattern");
116
117 Self {
118 extractor,
119 buffer: String::new(),
120 prev_tool_call_arr: Vec::new(),
121 current_tool_id: -1,
122 current_tool_name_sent: false,
123 streamed_args_for_tool: Vec::new(),
124 tool_call_start_token: "<tool_call>",
125 tool_call_end_token: "</tool_call>",
126 in_tool_call: false,
127 current_function_name: String::new(),
128 current_parameters: serde_json::Map::new(),
129 xml_function_pattern,
130 xml_param_pattern,
131 }
132 }
133
134 fn parse_xml_format(&self, content: &str, tools: &[Tool]) -> ParserResult<Option<ToolCall>> {
136 let function_captures = self
137 .xml_function_pattern
138 .captures(content)
139 .ok_or_else(|| ParserError::ParsingFailed("No function name found".to_string()))?;
140
141 let function_name = function_captures
142 .get(1)
143 .ok_or_else(|| ParserError::ParsingFailed("Function name capture failed".to_string()))?
144 .as_str()
145 .trim()
146 .to_string();
147
148 if function_name.is_empty() {
149 return Ok(None);
150 }
151
152 let param_types = helpers::param_types_for_function(tools, &function_name);
153 let mut parameters = serde_json::Map::new();
154
155 for cap in self.xml_param_pattern.captures_iter(content) {
156 if let (Some(key_match), Some(value_match)) = (cap.get(1), cap.get(2)) {
157 let key = key_match.as_str().trim().to_string();
158 let value = value_match.as_str();
159 let json_value = coerce_value(value, param_types.get(&key).map(String::as_str));
160 parameters.insert(key, json_value);
161 }
162 }
163
164 let arguments = serde_json::to_string(¶meters)
165 .map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
166
167 Ok(Some(ToolCall {
168 function: FunctionCall {
169 name: function_name,
170 arguments,
171 },
172 }))
173 }
174
175 fn parse_and_stream_parameters(&mut self, tools: &[Tool]) -> Vec<ToolCallItem> {
178 let mut calls: Vec<ToolCallItem> = vec![];
179 let param_types = helpers::param_types_for_function(tools, &self.current_function_name);
180
181 let mut new_params = serde_json::Map::new();
183 for cap in self.xml_param_pattern.captures_iter(&self.buffer) {
184 if let (Some(key_match), Some(value_match)) = (cap.get(1), cap.get(2)) {
185 let key = key_match.as_str().trim().to_string();
186 let value = value_match.as_str();
187 let json_value = coerce_value(value, param_types.get(&key).map(String::as_str));
188 new_params.insert(key, json_value);
189 }
190 }
191
192 if new_params != self.current_parameters {
194 let current_args = &mut self.streamed_args_for_tool[self.current_tool_id as usize];
195
196 if self.current_parameters.is_empty() {
197 let mut items = Vec::new();
199 for (key, value) in &new_params {
200 let key_json =
201 serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
202 let value_json = serde_json::to_string(value).unwrap_or_default();
203 items.push(format!("{key_json}: {value_json}"));
204 }
205 let json_fragment = format!("{{{}", items.join(", "));
206
207 calls.push(ToolCallItem {
208 tool_index: self.current_tool_id as usize,
209 name: None,
210 parameters: json_fragment.clone(),
211 });
212 *current_args = json_fragment;
213 } else {
214 let new_keys: Vec<_> = new_params
216 .keys()
217 .filter(|k| !self.current_parameters.contains_key(*k))
218 .collect();
219
220 if !new_keys.is_empty() {
221 let mut continuation_parts = Vec::new();
222 for key in new_keys {
223 if let Some(value) = new_params.get(key) {
224 let key_json =
225 serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
226 let value_json = serde_json::to_string(value).unwrap_or_default();
227 continuation_parts.push(format!("{key_json}: {value_json}"));
228 }
229 }
230
231 let json_fragment = format!(", {}", continuation_parts.join(", "));
232
233 calls.push(ToolCallItem {
234 tool_index: self.current_tool_id as usize,
235 name: None,
236 parameters: json_fragment.clone(),
237 });
238 current_args.push_str(&json_fragment);
239 }
240 }
241
242 self.current_parameters.clone_from(&new_params);
244 if let Some(tool_obj) =
245 self.prev_tool_call_arr[self.current_tool_id as usize].as_object_mut()
246 {
247 tool_obj.insert("arguments".to_string(), Value::Object(new_params));
248 }
249 }
250
251 calls
252 }
253
254 fn parse_complete_inner(
256 &self,
257 text: &str,
258 tools: &[Tool],
259 ) -> ParserResult<(String, Vec<ToolCall>)> {
260 if !self.has_tool_markers(text) {
262 return Ok((text.to_string(), vec![]));
263 }
264
265 let idx = text
268 .find(self.tool_call_start_token)
269 .ok_or_else(|| ParserError::ParsingFailed("tool call marker not found".to_string()))?;
270 let normal_text = text[..idx].to_string();
271
272 let mut parsed = Vec::new();
274 for captures in self.extractor.captures_iter(text) {
275 if let Some(content_str) = captures.get(1) {
276 let content = content_str.as_str().trim();
277
278 match self.parse_xml_format(content, tools) {
279 Ok(Some(tool)) => parsed.push(tool),
280 Ok(None) => continue,
281 Err(e) => {
282 tracing::warn!("Failed to parse XML tool call: {:?}", e);
283 continue;
284 }
285 }
286 }
287 }
288
289 if parsed.is_empty() {
291 return Ok((text.to_string(), vec![]));
292 }
293
294 Ok((normal_text, parsed))
295 }
296
297 fn reset_streaming_state(&mut self) {
299 self.in_tool_call = false;
300 self.current_tool_name_sent = false;
301 self.current_function_name.clear();
302 self.current_parameters.clear();
303 }
304}
305
306impl Default for QwenXmlParser {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312#[async_trait]
313impl ToolParser for QwenXmlParser {
314 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
315 self.parse_complete_inner(text, &[])
316 }
317
318 async fn parse_complete_with_tools(
319 &self,
320 text: &str,
321 tools: &[Tool],
322 ) -> ParserResult<(String, Vec<ToolCall>)> {
323 self.parse_complete_inner(text, tools)
324 }
325
326 async fn parse_incremental(
327 &mut self,
328 chunk: &str,
329 tools: &[Tool],
330 ) -> ParserResult<StreamingParseResult> {
331 self.buffer.push_str(chunk);
332
333 let mut normal_text = String::new();
334 let mut calls: Vec<ToolCallItem> = vec![];
335
336 let tool_indices = helpers::get_tool_indices(tools);
338
339 loop {
340 if !self.in_tool_call && !self.buffer.contains(self.tool_call_start_token) {
342 if helpers::ends_with_partial_token(&self.buffer, self.tool_call_start_token)
344 .is_none()
345 {
346 normal_text.push_str(&self.buffer);
347 self.buffer.clear();
348 }
349 break;
350 }
351
352 if !self.in_tool_call {
354 if let Some(s) = self.buffer.find(self.tool_call_start_token) {
355 normal_text.push_str(&self.buffer[..s]);
356 self.buffer = self.buffer[s + self.tool_call_start_token.len()..].to_string();
357 self.in_tool_call = true;
358 self.current_tool_name_sent = false;
359 self.current_function_name.clear();
360 self.current_parameters.clear();
361 continue;
362 } else {
363 break;
364 }
365 }
366
367 if !self.current_tool_name_sent {
369 if let Some(captures) = self.xml_function_pattern.captures(&self.buffer) {
370 if let Some(name_match) = captures.get(1) {
371 let function_name = name_match.as_str().trim().to_string();
372
373 if tool_indices.contains_key(&function_name) {
375 self.current_function_name.clone_from(&function_name);
376 self.current_tool_name_sent = true;
377
378 if self.current_tool_id == -1 {
380 self.current_tool_id = 0;
381 }
382
383 helpers::ensure_capacity(
385 self.current_tool_id,
386 &mut self.prev_tool_call_arr,
387 &mut self.streamed_args_for_tool,
388 );
389
390 self.prev_tool_call_arr[self.current_tool_id as usize] = serde_json::json!({
392 "name": function_name,
393 "arguments": {}
394 });
395
396 calls.push(ToolCallItem {
398 tool_index: self.current_tool_id as usize,
399 name: Some(function_name),
400 parameters: String::new(),
401 });
402
403 self.buffer =
406 self.buffer[captures.get(0).map_or(0, |m| m.end())..].to_string();
407 continue;
408 } else {
409 tracing::warn!("Invalid function name: {}", function_name);
411 self.reset_streaming_state();
412 normal_text.push_str(&self.buffer);
413 self.buffer.clear();
414 break;
415 }
416 }
417 } else {
418 break;
420 }
421 }
422
423 if self.current_tool_name_sent {
425 let param_calls = self.parse_and_stream_parameters(tools);
426 calls.extend(param_calls);
427
428 if let Some(end_pos) = self.buffer.find(self.tool_call_end_token) {
430 let current_args = &self.streamed_args_for_tool[self.current_tool_id as usize];
432 if !current_args.is_empty() {
433 let open_braces = current_args.matches('{').count();
435 let close_braces = current_args.matches('}').count();
436 if open_braces > close_braces {
437 calls.push(ToolCallItem {
438 tool_index: self.current_tool_id as usize,
439 name: None,
440 parameters: "}".to_string(),
441 });
442 self.streamed_args_for_tool[self.current_tool_id as usize].push('}');
443 }
444 }
445
446 self.buffer =
448 self.buffer[end_pos + self.tool_call_end_token.len()..].to_string();
449 self.reset_streaming_state();
450 self.current_tool_id += 1;
451 continue;
452 } else {
453 break;
455 }
456 }
457
458 break;
459 }
460
461 Ok(StreamingParseResult { normal_text, calls })
462 }
463
464 fn has_tool_markers(&self, text: &str) -> bool {
465 text.contains(self.tool_call_start_token)
466 }
467
468 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
469 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
470 }
471
472 fn reset(&mut self) {
473 helpers::reset_parser_state(
474 &mut self.buffer,
475 &mut self.prev_tool_call_arr,
476 &mut self.current_tool_id,
477 &mut self.current_tool_name_sent,
478 &mut self.streamed_args_for_tool,
479 );
480 self.reset_streaming_state();
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use openai_protocol::common::Function;
487
488 use super::*;
489
490 #[test]
491 fn test_safe_val_json() {
492 assert_eq!(safe_val("42"), Value::Number(42.into()));
493 assert_eq!(safe_val("1.5"), serde_json::json!(1.5));
494 assert_eq!(safe_val("true"), Value::Bool(true));
495 assert_eq!(safe_val("false"), Value::Bool(false));
496 assert_eq!(safe_val("null"), Value::Null);
497 assert_eq!(
498 safe_val(r#"{"key": "value"}"#),
499 serde_json::json!({"key": "value"})
500 );
501 assert_eq!(safe_val(r"[1, 2, 3]"), serde_json::json!([1, 2, 3]));
502 }
503
504 #[test]
505 fn test_safe_val_python_literals() {
506 assert_eq!(safe_val("True"), Value::Bool(true));
507 assert_eq!(safe_val("False"), Value::Bool(false));
508 assert_eq!(safe_val("None"), Value::Null);
509 }
510
511 #[test]
512 fn test_safe_val_string_fallback() {
513 assert_eq!(
514 safe_val("hello world"),
515 Value::String("hello world".to_string())
516 );
517 assert_eq!(safe_val(" spaces "), Value::String("spaces".to_string()));
518 }
519
520 #[test]
523 fn test_safe_val_preserves_html_entities() {
524 assert_eq!(
525 safe_val("<div>"),
526 Value::String("<div>".to_string())
527 );
528 assert_eq!(
529 safe_val("Tom & Jerry"),
530 Value::String("Tom & Jerry".to_string())
531 );
532 assert_eq!(safe_val("it's"), Value::String("it's".to_string()));
534 assert_eq!(safe_val("<"), Value::String("<".to_string()));
535 }
536
537 fn tool_with_props(props: Value) -> Vec<Tool> {
538 vec![Tool {
539 tool_type: "function".to_string(),
540 function: Function {
541 name: "f".to_string(),
542 description: None,
543 parameters: serde_json::json!({"type": "object", "properties": props}),
544 strict: None,
545 },
546 }]
547 }
548
549 #[tokio::test]
551 async fn test_schema_aware_coercion_keeps_strings() {
552 let tools = tool_with_props(serde_json::json!({
553 "limit": {"type": "string"},
554 "flag": {"type": "string"},
555 "coords": {"type": "string"},
556 "cfg": {"type": "string"},
557 "count": {"type": "integer"},
558 }));
559 let text = "<tool_call>\n<function=f>\n\
560 <parameter=limit>4</parameter>\n\
561 <parameter=flag>true</parameter>\n\
562 <parameter=coords>[60,30]</parameter>\n\
563 <parameter=cfg>{\"a\": 1}</parameter>\n\
564 <parameter=count>5</parameter>\n\
565 </function>\n</tool_call>";
566 let (_, calls) = QwenXmlParser::new()
567 .parse_complete_with_tools(text, &tools)
568 .await
569 .unwrap();
570 assert_eq!(calls.len(), 1);
571 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
572 assert_eq!(args["limit"], Value::String("4".to_string()));
573 assert_eq!(args["flag"], Value::String("true".to_string()));
574 assert_eq!(args["coords"], Value::String("[60,30]".to_string()));
575 assert_eq!(args["cfg"], Value::String("{\"a\": 1}".to_string()));
576 assert_eq!(args["count"], Value::Number(5.into()));
577 }
578
579 #[tokio::test]
581 async fn test_streaming_schema_aware_coercion() {
582 let tools = tool_with_props(serde_json::json!({
583 "limit": {"type": "string"},
584 "count": {"type": "integer"},
585 }));
586 let text = "<tool_call>\n<function=f>\n\
587 <parameter=limit>4</parameter>\n\
588 <parameter=count>5</parameter>\n\
589 </function>\n</tool_call>";
590 let result = QwenXmlParser::new()
591 .parse_incremental(text, &tools)
592 .await
593 .unwrap();
594 let args: String = result.calls.iter().map(|c| c.parameters.as_str()).collect();
595 assert!(
596 args.contains(r#""limit": "4""#),
597 "string param must stay string: {args}"
598 );
599 assert!(
600 args.contains(r#""count": 5"#),
601 "int param must coerce: {args}"
602 );
603 }
604
605 #[tokio::test]
610 async fn test_arg_values_with_entities_roundtrip_unchanged() {
611 let literal = "<a>Tom & Jerry</a> <x> it's";
612
613 let text = format!(
616 "<tool_call>\n<function=f>\n\
617 <parameter=content>\n{literal}\n</parameter>\n\
618 </function>\n</tool_call>"
619 );
620
621 let (_, calls) = QwenXmlParser::new().parse_complete(&text).await.unwrap();
624 assert_eq!(calls.len(), 1);
625 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
626 assert_eq!(args["content"], Value::String(literal.to_string()));
627
628 let tools = tool_with_props(serde_json::json!({
630 "content": {"type": "string"},
631 }));
632 let (_, calls) = QwenXmlParser::new()
633 .parse_complete_with_tools(&text, &tools)
634 .await
635 .unwrap();
636 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
637 assert_eq!(args["content"], Value::String(literal.to_string()));
638 }
639}