Fix tool calling: handle nested XML format from Kimi K2.5 model

- Add _parse_nested_xml_tool() to extract tool calls from nested XML
- Add _xml_to_dict() helper for recursive XML to dict conversion
- Update extract_tool_calls() to try both JSON and nested XML formats
- Improve system prompt with clearer tool format instructions and examples

This fixes the issue where models outputting raw XML tool syntax
(like Kimi K2.5) would have their tool calls end up in the response
text instead of being properly parsed.
parent d9608d7c
......@@ -181,16 +181,78 @@ class ToolCallParser:
def __init__(self, tokenizer=None):
self.tokenizer = tokenizer
def _parse_nested_xml_tool(self, xml_content: str) -> Optional[Dict]:
"""Parse nested XML tool format like <tool><name>...</name><arguments>...</arguments></tool>."""
try:
# Extract name
name_match = re.search(r'<name>\s*(.*?)\s*</name>', xml_content, re.DOTALL)
if not name_match:
return None
tool_name = name_match.group(1).strip()
# Extract arguments - handle both JSON and nested XML
args_match = re.search(r'<arguments>\s*(.*?)\s*</arguments>', xml_content, re.DOTALL)
if not args_match:
return None
args_content = args_match.group(1).strip()
# Try to parse as JSON first
try:
args_dict = json.loads(args_content)
return {"name": tool_name, "arguments": args_dict}
except json.JSONDecodeError:
# Try to parse as nested XML (e.g., <files><path>...</path></files>)
try:
args_dict = self._xml_to_dict(args_content)
return {"name": tool_name, "arguments": args_dict}
except:
# Return raw string as fallback
return {"name": tool_name, "arguments": args_content}
except Exception:
return None
def _xml_to_dict(self, xml_content: str) -> Dict:
"""Convert simple nested XML to dictionary."""
result = {}
# Find all top-level tags
pattern = r'<(\w+)>\s*(.*?)\s*</\1>'
matches = re.findall(pattern, xml_content, re.DOTALL)
for tag, content in matches:
# Check if content has nested tags
if re.search(r'<\w+>', content):
# Recursively parse nested content
try:
result[tag] = self._xml_to_dict(content)
except:
# If recursive parsing fails, check for array-like content
items = re.findall(r'<(\w+)>\s*(.*?)\s*</\1>', content, re.DOTALL)
if items and all(item[0] == items[0][0] for item in items):
# Array of items with same tag
result[tag] = []
for _, item_content in items:
if re.search(r'<\w+>', item_content):
result[tag].append(self._xml_to_dict(item_content))
else:
result[tag].append(item_content)
else:
result[tag] = content
else:
result[tag] = content
return result if result else xml_content
def extract_tool_calls(self, text: str, available_tools: List[Tool]) -> Optional[List[Dict]]:
"""Extract tool calls from model output."""
tool_calls = []
# Look for function calls in various formats
# Format 1: <tool> or <function> tags
# Format 1: <tool> or <function> tags with JSON content
tool_pattern = r'<(?:tool|function)>(.*?)</(?:tool|function)>'
tool_matches = re.findall(tool_pattern, text, re.DOTALL)
for match in tool_matches:
# Try JSON format first
try:
tool_data = json.loads(match.strip())
if 'name' in tool_data and 'arguments' in tool_data:
......@@ -202,8 +264,24 @@ class ToolCallParser:
"arguments": json.dumps(tool_data["arguments"])
}
})
continue
except json.JSONDecodeError:
pass
# Try nested XML format (Format 1b)
try:
tool_data = self._parse_nested_xml_tool(match)
if tool_data and 'name' in tool_data and 'arguments' in tool_data:
tool_calls.append({
"id": f"call_{uuid.uuid4().hex[:16]}",
"type": "function",
"function": {
"name": tool_data["name"],
"arguments": json.dumps(tool_data["arguments"]) if isinstance(tool_data["arguments"], dict) else str(tool_data["arguments"])
}
})
except Exception:
pass
# Format 2: JSON with function_call key
try:
......@@ -270,8 +348,16 @@ def format_tools_for_prompt(tools: List[Tool], messages: List[ChatMessage]) -> L
tool_descriptions.append(desc)
tools_text = "You have access to the following tools:\n\n" + "\n\n".join(tool_descriptions)
tools_text += "\n\nWhen you need to use a tool, format your response as:\n"
tools_text += '<tool>{"name": "tool_name", "arguments": {...}}</tool>'
tools_text += "\n\nIMPORTANT: When you need to use a tool, you MUST format your response EXACTLY as:\n"
tools_text += '<tool>{"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}</tool>'
tools_text += "\n\nRules:\n"
tools_text += "1. The content inside <tool> tags must be valid JSON\n"
tools_text += "2. Do NOT use nested XML tags like <name> or <arguments> - use JSON format only\n"
tools_text += "3. The 'name' field must match one of the available tool names exactly\n"
tools_text += "4. The 'arguments' field must be a JSON object with the required parameters\n"
tools_text += "\nExample:\n"
tools_text += 'User: Read the file example.txt\n'
tools_text += 'Assistant: <tool>{"name": "read_file", "arguments": {"files": [{"path": "example.txt"}]}}</tool>'
# Add or prepend to system message
new_messages = list(messages)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment