SRHarness / src /sr_harness /parser /text_parser.py
yuzihan@SIM1
Deploy SRHarness Docker Space
d06fa57
Raw History Blame Contribute Delete
7.95 kB
# 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