File size: 3,122 Bytes
d06fa57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Copyright (c) 2026-present, Yumeow. Licensed under the MIT License.
from __future__ import annotations
import json
import re
import xml.etree.ElementTree as ET
from logging import getLogger
from xml.sax.saxutils import escape, quoteattr

from .base_parser import BaseParser
from ..core import ToolCall

_logger = getLogger(f'sr_harness.{__name__}')
_TOOL_CALL_PATTERN = re.compile(
    r"<tool_call\b[^>]*>.*?</tool_call\s*>",
    flags=re.DOTALL,
)


@BaseParser.register('xml')
class XMLParser(BaseParser):
    """Parser for XML-formatted tool calls."""

    def format_tools(self) -> str:
        """Return tool metadata and the required XML call syntax."""
        lines = ["## Available Tools:", "", "<tools>"]
        for tool in self.tools:
            metadata = tool.metadata
            parameters = json.dumps(
                metadata.parameters or {"type": "object", "properties": {}},
                ensure_ascii=False,
                separators=(",", ":"),
            )
            lines.extend([
                f"  <tool name={quoteattr(metadata.name)}>",
                f"    <description>{escape(metadata.description or '')}</description>",
                f"    <parameters>{escape(parameters)}</parameters>",
                "  </tool>",
            ])
        lines.extend([
            "</tools>",
            "",
            "## Output Format:",
            "",
            "Emit each tool call as a separate XML element. The arguments must be one JSON object:",
            "",
            '<tool_call name="tool_name">',
            '  <arguments>{"param1":"value1","param2":2}</arguments>',
            "</tool_call>",
            "",
            "You may emit multiple <tool_call> elements in one response.",
        ])
        return "\n".join(lines)

    def parse_response(self, response: str) -> list[ToolCall]:
        """Parse independent ``tool_call`` elements embedded in model text."""
        tool_calls = []
        for raw in _TOOL_CALL_PATTERN.findall(response):
            try:
                element = ET.fromstring(raw)
                name = (element.get("name") or element.findtext("name") or "").strip()
                if not name:
                    raise ValueError("tool_call has no tool name")
                arguments_element = element.find("arguments")
                if arguments_element is None:
                    arguments_element = element.find("params")
                arguments_text = (
                    "".join(arguments_element.itertext()).strip()
                    if arguments_element is not None
                    else ""
                )
                params = json.loads(arguments_text) if arguments_text else {}
                if not isinstance(params, dict):
                    raise ValueError("tool_call arguments must be a JSON object")
            except (ET.ParseError, json.JSONDecodeError, ValueError) as error:
                _logger.warning(f"Skip malformed XML tool call {raw!r}: {error}")
                continue
            tool_calls.append(ToolCall(name=name, params=params, raw=raw))
        return tool_calls