Download src/agentcade/tools.py from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 6 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/tools.py
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/agentcade/tools.py
-
curl -L -o tools.py https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/tools.py
6 kB
| import asyncio | |
| import inspect | |
| import json | |
| import re | |
| import typing | |
| from collections.abc import Awaitable, Callable | |
| from dataclasses import dataclass | |
| from typing import Any, Literal | |
| from pydantic import BaseModel, ConfigDict, Field, create_model | |
| import observability | |
| class ToolInput(BaseModel): | |
| model_config = ConfigDict(extra="forbid", strict=True) | |
| class NoInput(ToolInput): | |
| pass | |
| class TextInput(ToolInput): | |
| text: str = Field(min_length=1, description="用户当前消息的完整原文") | |
| def original_text_input(text: str): | |
| return create_model( | |
| "CurrentMessageInput", __base__=TextInput, | |
| text=(Literal[text], Field(description="本轮用户消息的完整原文,保持字词和标点")), | |
| ) | |
| class Tool: | |
| name: str | |
| description: str | |
| inputs: type[BaseModel] | |
| handler: Callable[..., Awaitable[Any]] | |
| def definition(self): | |
| return { | |
| "name": self.name, "description": self.description, | |
| "parameters": self.inputs.model_json_schema(), | |
| } | |
| # 大模型接口对工具名的限制。 | |
| TOOL_NAME = re.compile(r"[A-Za-z0-9_-]{1,64}") | |
| # 工具方法里叫这个名字的参数由框架传入当前这一轮,不交给大模型填写。 | |
| TURN_PARAMETER = "turn" | |
| class ToolSpec: | |
| """用 @tool 声明的工具:发给大模型的定义和对应的方法。""" | |
| name: str | |
| description: str | |
| inputs: type[BaseModel] | |
| function: Callable[..., Any] | |
| takes_turn: bool | |
| def tool(description: str, *, name: str | None = None): | |
| """把游戏 Agent 的方法声明成工具。description 原样发给大模型,name 不写时用方法名。 | |
| 参数 turn 由框架传入当前这一轮;其余参数由大模型填写,必须写类型注解,有默认值的是可选参数, | |
| 取值范围和说明写成 Annotated[int, Field(ge=1, le=20, description="用户猜的数")]。 | |
| 方法可以是同步或 async 的,返回能转成 JSON 的字典。 | |
| """ | |
| if not isinstance(description, str): | |
| raise TypeError('@tool 要带上工具说明,写成 @tool("说明")') | |
| def decorate(function): | |
| function.__tool__ = tool_spec(function, description, name) | |
| return function | |
| return decorate | |
| def tool_spec(function, description: str, name: str | None) -> ToolSpec: | |
| """检查方法的签名,生成工具定义;写错的地方在定义类时就报错。""" | |
| where = function.__qualname__ | |
| name = function.__name__ if name is None else name | |
| if not TOOL_NAME.fullmatch(name): | |
| raise ValueError(f"{where} 的工具名 {name!r} 不合法:只能用字母、数字、下划线和短横线,最长 64 个字符") | |
| if not description.strip(): | |
| raise ValueError(f"{where} 的工具说明不能为空") | |
| hints = typing.get_type_hints(function, include_extras=True) | |
| fields, takes_turn = {}, False | |
| # 第一个参数是 self。 | |
| for parameter in list(inspect.signature(function).parameters.values())[1:]: | |
| if parameter.name == TURN_PARAMETER: | |
| takes_turn = True | |
| continue | |
| if parameter.kind not in (parameter.POSITIONAL_OR_KEYWORD, parameter.KEYWORD_ONLY): | |
| raise TypeError(f"{where} 的参数 {parameter.name} 要写成普通参数,不能用 *args、**kwargs 或仅限位置的参数") | |
| if parameter.name not in hints: | |
| raise TypeError(f"{where} 的参数 {parameter.name} 缺少类型注解") | |
| default = ... if parameter.default is parameter.empty else parameter.default | |
| fields[parameter.name] = (hints[parameter.name], default) | |
| try: | |
| inputs = create_model(f"{name}_input", __base__=ToolInput, **fields) | |
| inputs.model_json_schema() | |
| except Exception as error: | |
| raise TypeError(f"{where} 的参数类型生成不了工具定义:{error}") from error | |
| return ToolSpec(name, description, inputs, function, takes_turn) | |
| def collect_tools(cls: type) -> tuple[ToolSpec, ...]: | |
| """按定义顺序收集类上用 @tool 声明的方法;子类重写同名方法时以子类为准。""" | |
| found = {} | |
| for klass in reversed(cls.__mro__): | |
| for attribute, value in vars(klass).items(): | |
| found[attribute] = getattr(value, "__tool__", None) | |
| specs = tuple(spec for spec in found.values() if isinstance(spec, ToolSpec)) | |
| owners = {} | |
| for spec in specs: | |
| if spec.name in owners: | |
| raise ValueError(f"{cls.__name__} 的工具名 {spec.name} 重复:" | |
| f"{owners[spec.name]} 和 {spec.function.__qualname__}") | |
| owners[spec.name] = spec.function.__qualname__ | |
| return specs | |
| class ToolRegistry: | |
| def __init__(self, tools: list[Tool], *, terminal=()): | |
| self.tools = {tool.name: tool for tool in tools} | |
| if len(self.tools) != len(tools): | |
| raise ValueError("工具名称不能重复") | |
| self.calls: list[dict] = [] | |
| self.lock = asyncio.Lock() | |
| self.terminal = set(terminal) | |
| self.completed = asyncio.Event() | |
| def definitions(self): | |
| return [tool.definition() for tool in self.tools.values()] | |
| async def call(self, name: str, arguments: dict): | |
| async with self.lock: | |
| if self.completed.is_set(): | |
| raise ValueError("当前工具调用轮次已经结束") | |
| with observability.tool_call(name, arguments) as outcome: | |
| tool = self.tools[name] | |
| inputs = tool.inputs.model_validate(arguments) | |
| result = await tool.handler(**inputs.model_dump()) | |
| # 在工具执行位置验证返回值可以通过模型接口传输。 | |
| json.dumps(result, ensure_ascii=False, allow_nan=False) | |
| outcome["result"] = result | |
| self.calls.append({"name": name, "arguments": arguments, "result": result}) | |
| if name in self.terminal: | |
| self.completed.set() | |
| return result | |