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(
178 &mut self,
179 tools: &[Tool],
180 parameter_end: usize,
181 ) -> Vec<ToolCallItem> {
182 let mut calls: Vec<ToolCallItem> = vec![];
183 let param_types = helpers::param_types_for_function(tools, &self.current_function_name);
184
185 let mut new_params = serde_json::Map::new();
187 for cap in self
188 .xml_param_pattern
189 .captures_iter(&self.buffer[..parameter_end])
190 {
191 if let (Some(key_match), Some(value_match)) = (cap.get(1), cap.get(2)) {
192 let key = key_match.as_str().trim().to_string();
193 let value = value_match.as_str();
194 let json_value = coerce_value(value, param_types.get(&key).map(String::as_str));
195 new_params.insert(key, json_value);
196 }
197 }
198
199 if new_params != self.current_parameters {
201 let current_args = &mut self.streamed_args_for_tool[self.current_tool_id as usize];
202
203 if self.current_parameters.is_empty() {
204 let mut items = Vec::new();
206 for (key, value) in &new_params {
207 let key_json =
208 serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
209 let value_json = serde_json::to_string(value).unwrap_or_default();
210 items.push(format!("{key_json}: {value_json}"));
211 }
212 let json_fragment = format!("{{{}", items.join(", "));
213
214 calls.push(ToolCallItem {
215 tool_index: self.current_tool_id as usize,
216 name: None,
217 parameters: json_fragment.clone(),
218 });
219 *current_args = json_fragment;
220 } else {
221 let new_keys: Vec<_> = new_params
223 .keys()
224 .filter(|k| !self.current_parameters.contains_key(*k))
225 .collect();
226
227 if !new_keys.is_empty() {
228 let mut continuation_parts = Vec::new();
229 for key in new_keys {
230 if let Some(value) = new_params.get(key) {
231 let key_json =
232 serde_json::to_string(key).unwrap_or_else(|_| format!("\"{key}\""));
233 let value_json = serde_json::to_string(value).unwrap_or_default();
234 continuation_parts.push(format!("{key_json}: {value_json}"));
235 }
236 }
237
238 let json_fragment = format!(", {}", continuation_parts.join(", "));
239
240 calls.push(ToolCallItem {
241 tool_index: self.current_tool_id as usize,
242 name: None,
243 parameters: json_fragment.clone(),
244 });
245 current_args.push_str(&json_fragment);
246 }
247 }
248
249 self.current_parameters.clone_from(&new_params);
251 if let Some(tool_obj) =
252 self.prev_tool_call_arr[self.current_tool_id as usize].as_object_mut()
253 {
254 tool_obj.insert("arguments".to_string(), Value::Object(new_params));
255 }
256 }
257
258 calls
259 }
260
261 fn parse_complete_inner(
263 &self,
264 text: &str,
265 tools: &[Tool],
266 ) -> ParserResult<(String, Vec<ToolCall>)> {
267 if !self.has_tool_markers(text) {
269 return Ok((text.to_string(), vec![]));
270 }
271
272 let idx = text
275 .find(self.tool_call_start_token)
276 .ok_or_else(|| ParserError::ParsingFailed("tool call marker not found".to_string()))?;
277 let normal_text = text[..idx].to_string();
278
279 let mut parsed = Vec::new();
281 for captures in self.extractor.captures_iter(text) {
282 if let Some(content_str) = captures.get(1) {
283 let content = content_str.as_str().trim();
284
285 match self.parse_xml_format(content, tools) {
286 Ok(Some(tool)) => parsed.push(tool),
287 Ok(None) => continue,
288 Err(e) => {
289 tracing::warn!("Failed to parse XML tool call: {:?}", e);
290 continue;
291 }
292 }
293 }
294 }
295
296 if parsed.is_empty() {
298 return Ok((text.to_string(), vec![]));
299 }
300
301 Ok((normal_text, parsed))
302 }
303
304 fn reset_streaming_state(&mut self) {
306 self.in_tool_call = false;
307 self.current_tool_name_sent = false;
308 self.current_function_name.clear();
309 self.current_parameters.clear();
310 }
311}
312
313impl Default for QwenXmlParser {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319#[async_trait]
320impl ToolParser for QwenXmlParser {
321 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
322 self.parse_complete_inner(text, &[])
323 }
324
325 async fn parse_complete_with_tools(
326 &self,
327 text: &str,
328 tools: &[Tool],
329 ) -> ParserResult<(String, Vec<ToolCall>)> {
330 self.parse_complete_inner(text, tools)
331 }
332
333 async fn parse_incremental(
334 &mut self,
335 chunk: &str,
336 tools: &[Tool],
337 ) -> ParserResult<StreamingParseResult> {
338 self.buffer.push_str(chunk);
339
340 let mut normal_text = String::new();
341 let mut calls: Vec<ToolCallItem> = vec![];
342
343 let tool_indices = helpers::get_tool_indices(tools);
345
346 loop {
347 if !self.in_tool_call && !self.buffer.contains(self.tool_call_start_token) {
349 if helpers::ends_with_partial_token(&self.buffer, self.tool_call_start_token)
351 .is_none()
352 {
353 normal_text.push_str(&self.buffer);
354 self.buffer.clear();
355 }
356 break;
357 }
358
359 if !self.in_tool_call {
361 if let Some(s) = self.buffer.find(self.tool_call_start_token) {
362 normal_text.push_str(&self.buffer[..s]);
363 self.buffer = self.buffer[s + self.tool_call_start_token.len()..].to_string();
364 self.in_tool_call = true;
365 self.current_tool_name_sent = false;
366 self.current_function_name.clear();
367 self.current_parameters.clear();
368 continue;
369 } else {
370 break;
371 }
372 }
373
374 if !self.current_tool_name_sent {
376 if let Some(captures) = self.xml_function_pattern.captures(&self.buffer) {
377 if let Some(name_match) = captures.get(1) {
378 let function_name = name_match.as_str().trim().to_string();
379
380 if tool_indices.contains_key(&function_name) {
382 self.current_function_name.clone_from(&function_name);
383 self.current_tool_name_sent = true;
384
385 if self.current_tool_id == -1 {
387 self.current_tool_id = 0;
388 }
389
390 helpers::ensure_capacity(
392 self.current_tool_id,
393 &mut self.prev_tool_call_arr,
394 &mut self.streamed_args_for_tool,
395 );
396
397 self.prev_tool_call_arr[self.current_tool_id as usize] = serde_json::json!({
399 "name": function_name,
400 "arguments": {}
401 });
402
403 calls.push(ToolCallItem {
405 tool_index: self.current_tool_id as usize,
406 name: Some(function_name),
407 parameters: String::new(),
408 });
409
410 self.buffer =
413 self.buffer[captures.get(0).map_or(0, |m| m.end())..].to_string();
414 continue;
415 } else {
416 tracing::warn!("Invalid function name: {}", function_name);
418 self.reset_streaming_state();
419 normal_text.push_str(&self.buffer);
420 self.buffer.clear();
421 break;
422 }
423 }
424 } else {
425 break;
427 }
428 }
429
430 if self.current_tool_name_sent {
432 let end_pos = self.buffer.find(self.tool_call_end_token);
433 let parameter_end = end_pos.unwrap_or(self.buffer.len());
434 let param_calls = self.parse_and_stream_parameters(tools, parameter_end);
435 calls.extend(param_calls);
436
437 if let Some(end_pos) = end_pos {
439 let current_args =
441 &mut self.streamed_args_for_tool[self.current_tool_id as usize];
442 let closing = if current_args.is_empty() { "{}" } else { "}" };
443 calls.push(ToolCallItem {
444 tool_index: self.current_tool_id as usize,
445 name: None,
446 parameters: closing.to_string(),
447 });
448 current_args.push_str(closing);
449
450 self.buffer =
452 self.buffer[end_pos + self.tool_call_end_token.len()..].to_string();
453 self.reset_streaming_state();
454 self.current_tool_id += 1;
455 continue;
456 } else {
457 break;
459 }
460 }
461
462 break;
463 }
464
465 Ok(StreamingParseResult { normal_text, calls })
466 }
467
468 fn has_tool_markers(&self, text: &str) -> bool {
469 text.contains(self.tool_call_start_token)
470 }
471
472 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
473 let tool_index = self.prev_tool_call_arr.len().checked_sub(1)?;
474 let actual = self.streamed_args_for_tool.get(tool_index)?;
475 let expected = self.prev_tool_call_arr[tool_index].get("arguments")?;
476 if !actual.is_empty()
479 && serde_json::from_str::<Value>(&format!("{actual}}}"))
480 .ok()
481 .as_ref()
482 == Some(expected)
483 {
484 return Some(vec![ToolCallItem {
485 tool_index,
486 name: None,
487 parameters: "}".to_string(),
488 }]);
489 }
490 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
491 }
492
493 fn reset(&mut self) {
494 helpers::reset_parser_state(
495 &mut self.buffer,
496 &mut self.prev_tool_call_arr,
497 &mut self.current_tool_id,
498 &mut self.current_tool_name_sent,
499 &mut self.streamed_args_for_tool,
500 );
501 self.reset_streaming_state();
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use openai_protocol::common::Function;
508
509 use super::*;
510
511 #[test]
512 fn test_safe_val_json() {
513 assert_eq!(safe_val("42"), Value::Number(42.into()));
514 assert_eq!(safe_val("1.5"), serde_json::json!(1.5));
515 assert_eq!(safe_val("true"), Value::Bool(true));
516 assert_eq!(safe_val("false"), Value::Bool(false));
517 assert_eq!(safe_val("null"), Value::Null);
518 assert_eq!(
519 safe_val(r#"{"key": "value"}"#),
520 serde_json::json!({"key": "value"})
521 );
522 assert_eq!(safe_val(r"[1, 2, 3]"), serde_json::json!([1, 2, 3]));
523 }
524
525 #[test]
526 fn test_safe_val_python_literals() {
527 assert_eq!(safe_val("True"), Value::Bool(true));
528 assert_eq!(safe_val("False"), Value::Bool(false));
529 assert_eq!(safe_val("None"), Value::Null);
530 }
531
532 #[test]
533 fn test_safe_val_string_fallback() {
534 assert_eq!(
535 safe_val("hello world"),
536 Value::String("hello world".to_string())
537 );
538 assert_eq!(safe_val(" spaces "), Value::String("spaces".to_string()));
539 }
540
541 #[test]
544 fn test_safe_val_preserves_html_entities() {
545 assert_eq!(
546 safe_val("<div>"),
547 Value::String("<div>".to_string())
548 );
549 assert_eq!(
550 safe_val("Tom & Jerry"),
551 Value::String("Tom & Jerry".to_string())
552 );
553 assert_eq!(safe_val("it's"), Value::String("it's".to_string()));
555 assert_eq!(safe_val("<"), Value::String("<".to_string()));
556 }
557
558 fn tool_with_props(props: Value) -> Vec<Tool> {
559 vec![Tool {
560 tool_type: "function".to_string(),
561 function: Function {
562 name: "f".to_string(),
563 description: None,
564 parameters: serde_json::json!({"type": "object", "properties": props}),
565 strict: None,
566 },
567 }]
568 }
569
570 #[tokio::test]
572 async fn test_schema_aware_coercion_keeps_strings() {
573 let tools = tool_with_props(serde_json::json!({
574 "limit": {"type": "string"},
575 "flag": {"type": "string"},
576 "coords": {"type": "string"},
577 "cfg": {"type": "string"},
578 "count": {"type": "integer"},
579 }));
580 let text = "<tool_call>\n<function=f>\n\
581 <parameter=limit>4</parameter>\n\
582 <parameter=flag>true</parameter>\n\
583 <parameter=coords>[60,30]</parameter>\n\
584 <parameter=cfg>{\"a\": 1}</parameter>\n\
585 <parameter=count>5</parameter>\n\
586 </function>\n</tool_call>";
587 let (_, calls) = QwenXmlParser::new()
588 .parse_complete_with_tools(text, &tools)
589 .await
590 .unwrap();
591 assert_eq!(calls.len(), 1);
592 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
593 assert_eq!(args["limit"], Value::String("4".to_string()));
594 assert_eq!(args["flag"], Value::String("true".to_string()));
595 assert_eq!(args["coords"], Value::String("[60,30]".to_string()));
596 assert_eq!(args["cfg"], Value::String("{\"a\": 1}".to_string()));
597 assert_eq!(args["count"], Value::Number(5.into()));
598 }
599
600 #[tokio::test]
602 async fn test_streaming_schema_aware_coercion() {
603 let tools = tool_with_props(serde_json::json!({
604 "limit": {"type": "string"},
605 "count": {"type": "integer"},
606 }));
607 let text = "<tool_call>\n<function=f>\n\
608 <parameter=limit>4</parameter>\n\
609 <parameter=count>5</parameter>\n\
610 </function>\n</tool_call>";
611 let result = QwenXmlParser::new()
612 .parse_incremental(text, &tools)
613 .await
614 .unwrap();
615 let args: String = result.calls.iter().map(|c| c.parameters.as_str()).collect();
616 assert!(
617 args.contains(r#""limit": "4""#),
618 "string param must stay string: {args}"
619 );
620 assert!(
621 args.contains(r#""count": 5"#),
622 "int param must coerce: {args}"
623 );
624 }
625
626 #[tokio::test]
631 async fn test_arg_values_with_entities_roundtrip_unchanged() {
632 let literal = "<a>Tom & Jerry</a> <x> it's";
633
634 let text = format!(
637 "<tool_call>\n<function=f>\n\
638 <parameter=content>\n{literal}\n</parameter>\n\
639 </function>\n</tool_call>"
640 );
641
642 let (_, calls) = QwenXmlParser::new().parse_complete(&text).await.unwrap();
645 assert_eq!(calls.len(), 1);
646 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
647 assert_eq!(args["content"], Value::String(literal.to_string()));
648
649 let tools = tool_with_props(serde_json::json!({
651 "content": {"type": "string"},
652 }));
653 let (_, calls) = QwenXmlParser::new()
654 .parse_complete_with_tools(&text, &tools)
655 .await
656 .unwrap();
657 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
658 assert_eq!(args["content"], Value::String(literal.to_string()));
659 }
660}