Skip to content

Commit b7789d7

Browse files
feat: make XML tool call parsing dynamic and configurable
- Add xml_tool_format configuration parameter (auto/true/false) - Replace static Qwen detection with dynamic _supports_xml_tool_format() method - Add fallback XML detection for any model outputting <tool_call> tags - Maintain backward compatibility with existing Qwen auto-detection - Support manual XML format enabling for any model Fixes #1077 Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent 81d084e commit b7789d7

File tree

1 file changed

+67
-0
lines changed
  • src/praisonai-agents/praisonaiagents/llm

1 file changed

+67
-0
lines changed

src/praisonai-agents/praisonaiagents/llm/llm.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def __init__(
281281
self.min_reflect = extra_settings.get('min_reflect', 1)
282282
self.reasoning_steps = extra_settings.get('reasoning_steps', False)
283283
self.metrics = extra_settings.get('metrics', False)
284+
# Auto-detect XML tool format for known models, or allow manual override
285+
self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto')
284286

285287
# Token tracking
286288
self.last_token_metrics: Optional[TokenMetrics] = None
@@ -359,6 +361,32 @@ def _is_ollama_provider(self) -> bool:
359361

360362
return False
361363

364+
def _is_qwen_provider(self) -> bool:
365+
"""Detect if this is a Qwen provider"""
366+
if not self.model:
367+
return False
368+
369+
# Direct qwen/ prefix or Qwen in model name
370+
model_lower = self.model.lower()
371+
if any(pattern in model_lower for pattern in ["qwen", "qwen2", "qwen2.5"]):
372+
return True
373+
374+
# OpenAI-compatible API serving Qwen models
375+
if "openai/" in self.model and any(pattern in model_lower for pattern in ["qwen", "qwen2", "qwen2.5"]):
376+
return True
377+
378+
return False
379+
380+
def _supports_xml_tool_format(self) -> bool:
381+
"""Check if the model should use XML tool format"""
382+
if self.xml_tool_format == 'auto':
383+
# Auto-detect based on known models that use XML format
384+
return self._is_qwen_provider()
385+
elif self.xml_tool_format is True or self.xml_tool_format == 'true':
386+
return True
387+
else:
388+
return False
389+
362390
def _generate_ollama_tool_summary(self, tool_results: List[Any], response_text: str) -> Optional[str]:
363391
"""
364392
Generate a summary from tool results for Ollama to prevent infinite loops.
@@ -658,6 +686,10 @@ def _supports_streaming_tools(self) -> bool:
658686
if any(self.model.startswith(prefix) for prefix in ["gemini-", "gemini/"]):
659687
return True
660688

689+
# Models with XML tool format support streaming with tools
690+
if self._supports_xml_tool_format():
691+
return True
692+
661693
# For other providers, default to False to be safe
662694
# This ensures we make a single non-streaming call rather than risk
663695
# missing tool calls or making duplicate calls
@@ -1427,6 +1459,41 @@ def get_response(
14271459
except (json.JSONDecodeError, KeyError) as e:
14281460
logging.debug(f"Could not parse Ollama tool call from response: {e}")
14291461

1462+
# Parse tool calls from XML format in response text
1463+
# Try for known XML models first, or fallback for any model that might output XML
1464+
if not tool_calls and response_text and formatted_tools:
1465+
# Check if this model is known to use XML format, or try as fallback
1466+
should_try_xml = (self._supports_xml_tool_format() or
1467+
# Fallback: try XML if response contains XML-like tool call tags
1468+
'<tool_call>' in response_text)
1469+
1470+
if should_try_xml:
1471+
# Look for <tool_call> XML tags
1472+
tool_call_pattern = r'<tool_call>\s*({.*?})\s*</tool_call>'
1473+
matches = re.findall(tool_call_pattern, response_text, re.DOTALL)
1474+
1475+
if matches:
1476+
tool_calls = []
1477+
for idx, match in enumerate(matches):
1478+
try:
1479+
# Parse the JSON inside the XML tag
1480+
tool_json = json.loads(match.strip())
1481+
if isinstance(tool_json, dict) and "name" in tool_json:
1482+
tool_calls.append({
1483+
"id": f"tool_{iteration_count}_{idx}",
1484+
"type": "function",
1485+
"function": {
1486+
"name": tool_json["name"],
1487+
"arguments": json.dumps(tool_json.get("arguments", {}))
1488+
}
1489+
})
1490+
except (json.JSONDecodeError, KeyError) as e:
1491+
logging.debug(f"Could not parse XML tool call: {e}")
1492+
continue
1493+
1494+
if tool_calls:
1495+
logging.debug(f"Parsed {len(tool_calls)} tool call(s) from XML format")
1496+
14301497
# For Ollama, if response is empty but we have tools, prompt for tool usage
14311498
if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
14321499
messages.append({

0 commit comments

Comments
 (0)