File size: 7,951 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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# Copyright (c) 2026-present, Yumeow. Licensed under the MIT License.
from __future__ import annotations
import re
import json
import inspect
from ast import literal_eval
from logging import getLogger
from typing import List, Dict, Any
from .base_parser import BaseParser
from ..core import ToolCall

_logger = getLogger(f'sr_harness.{__name__}')


@BaseParser.register('text')
class TextParser(BaseParser):
    """Parser for tagged text tool calls."""

    def format_tools(self) -> str:
        """Format tool list into a description string for LLM.

        Returns:
            Formatted tool description string.
        """
        lines = ["## Available Tools:", ""]

        for tool in self.tools:
            name = tool.metadata.name
            desc = tool.metadata.description

            # 获取工具类的签名和 docstring
            try:
                if (tool_cls := type(tool)):
                    # 获取 execute 方法的签名
                    sig = inspect.signature(tool_cls.execute)
                    params = []
                    for param_name, param in sig.parameters.items():
                        if param_name in ('self', 'args', 'kwargs'):
                            continue
                        # 构建带类型注解的参数描述
                        param_parts = []
                        param_parts.append(param_name)
                        # 添加类型注解
                        if param.annotation is not inspect.Parameter.empty:
                            type_hint = self._format_type(param.annotation)
                            param_parts.append(f": {type_hint}")
                        # 添加默认值
                        if param.default is not inspect.Parameter.empty:
                            default_val = param.default
                            param_parts.append(f" = {default_val!r}")
                        params.append("".join(param_parts))
                    signature = f"{name}({', '.join(params)})"

                    # 获取 docstring
                    docstring = inspect.getdoc(tool_cls.execute) or "No detailed documentation available."
                else:
                    signature = f"{name}(...)"
                    docstring = "No detailed documentation available."
            except Exception:
                signature = f"{name}(...)"
                docstring = "No detailed documentation available."

            lines.append(f"### {name}")
            lines.append(f"- **Description**: {desc}")
            lines.append(f"- **Signature**: `{signature}`")
            lines.append(f"- **DocString**: {docstring}")
            lines.append("")

        lines.append("## Output Format:")
        lines.append("")
        lines.append("Use the following format for tool calls:")
        lines.append("")
        lines.append("Action: tool_name(param1=value1, param2=value2)")
        lines.append("")
        lines.append("You can make multiple tool calls in sequence, one Action per line.")

        return "\n".join(lines)

    def parse_response(self, response: str) -> List[ToolCall]:
        """Parse response.

        Args:
            response: Provider response object.

        Returns:
            List[ToolCall]: The operation result.
        """
        tool_calls = []
        for line in response.strip().splitlines():
            line = line.strip()
            if line.startswith('Action:'):
                action_line = line.removeprefix('Action:').strip()
                if (match := re.match(r'(\w+)\s*\((.*)\)', action_line)):
                    tool_name = match.group(1)
                    if (params_str := match.group(2).strip()):
                        params = self._parse_params(params_str)
                    else:
                        params = {}
                    tool_calls.append(ToolCall(name=tool_name, params=params, raw=line))
                else:
                    _logger.warning(f"Failed to parse action line: '{line}'")
        return tool_calls

    def _parse_params(self, params_str: str) -> Dict[str, Any]:
        """解析参数字符串为字典。

        Args:
            params_str: 参数字符串,如 'x_vars=["phi0", "phi1"], y_var="y"'

        Returns:
            参数字典
        """
        try:
            # 先尝试 JSON 解析(如果整个字符串是 JSON 对象)
            if params_str.startswith('{') and params_str.endswith('}'):
                return json.loads(params_str)
            else:
                # 将 key=value, key2=value2 格式转换为字典
                params_dict = {}
                parts = self._split_params(params_str)
                for part in parts:
                    if '=' in part:
                        key, value = part.split('=', 1)
                        key = key.strip().lower()  # 转换为小写
                        value = value.strip()
                        try:
                            params_dict[key] = literal_eval(value)
                        except (ValueError, SyntaxError):
                            params_dict[key] = value
                return params_dict
        except Exception as e:
            _logger.warning(f"Failed to parse params '{params_str}': {e}")
            return {'raw_params': params_str}

    def _format_type(self, annotation) -> str:
        """Format type annotation into a readable string.

        Args:
            annotation: The type annotation to format.

        Returns:
            Formatted type string.
        """
        import typing
        if annotation is inspect.Parameter.empty:
            return ""

        # 处理常见类型
        if annotation is type(None):
            return "None"
        if annotation is ...:
            return "..."

        # 处理 typing 模块的类型
        origin = getattr(annotation, '__origin__', None)
        if origin is not None:
            # 处理 Optional[T] = Union[T, None]
            if origin is typing.Union:
                args = getattr(annotation, '__args__', ())
                if type(None) in args:
                    non_none = [t for t in args if t is not type(None)]
                    if len(non_none) == 1:
                        return f"Optional[{self._format_type(non_none[0])}]"
            # 处理 List[T], Dict[K, V], Tuple[...]
            args = getattr(annotation, '__args__', ())
            args_str = ', '.join(self._format_type(arg) for arg in args)
            name = getattr(origin, '__name__', str(origin))
            return f"{name}[{args_str}]"

        # 处理普通类型
        if hasattr(annotation, '__name__'):
            return annotation.__name__

        return str(annotation)

    def _split_params(self, params_str: str) -> List[str]:
        """分割参数字符串,处理引号内的逗号。

        Args:
            params_str: 参数字符串,如 'x_vars=["phi0", "phi1"], y_var="y"'

        Returns:
            分割后的参数列表
        """
        parts = []
        current = []
        depth = 0
        in_string = False
        string_char = None

        for char in params_str:
            if char in '"\'':
                if not in_string:
                    in_string = True
                    string_char = char
                elif char == string_char:
                    in_string = False
                    string_char = None
                current.append(char)
            elif char in '[{(':
                depth += 1
                current.append(char)
            elif char in ']})':
                depth -= 1
                current.append(char)
            elif char == ',' and depth == 0 and not in_string:
                parts.append(''.join(current).strip())
                current = []
            else:
                current.append(char)

        if current:
            parts.append(''.join(current).strip())

        return parts