修改了nanobot,往Hermes agent的风格走,进度1/3
This commit is contained in:
175
app-instance/backend/beaver/tools/base.py
Normal file
175
app-instance/backend/beaver/tools/base.py
Normal file
@ -0,0 +1,175 @@
|
||||
"""Beaver 工具系统的统一契约。
|
||||
|
||||
这一层的目标不是实现具体工具,而是把 runtime 真正依赖的最小接口定死。
|
||||
|
||||
我们需要统一回答 4 个问题:
|
||||
1. 一个工具长什么样
|
||||
2. tool schema 怎么导出给 provider
|
||||
3. 工具执行结果长什么样
|
||||
4. tool loop 执行时,可以把哪些运行时依赖传给工具
|
||||
|
||||
这层故意保持很薄:
|
||||
- 不绑定 MCP
|
||||
- 不绑定 memory/session
|
||||
- 不绑定具体 provider
|
||||
|
||||
这样内建工具、MCP 工具、未来插件工具都可以收敛到同一套契约上。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolSpec:
|
||||
"""单个工具对外暴露的描述信息。
|
||||
|
||||
这份信息主要服务两个场景:
|
||||
1. 导出给 provider 的 function schema
|
||||
2. 在 registry 中做列出、查找、调试
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
input_schema: dict[str, Any]
|
||||
|
||||
def to_provider_schema(self) -> dict[str, Any]:
|
||||
"""导出为 OpenAI-compatible function tool schema。"""
|
||||
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": self.input_schema,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolContext:
|
||||
"""一次工具执行时可用的运行时上下文。
|
||||
|
||||
这不是“所有系统对象的大杂烩”,而是当前工具执行阶段最常用的公共入口。
|
||||
后面主链接进来时,可以把 session manager / memory store / workspace 等从这里传入。
|
||||
"""
|
||||
|
||||
workspace: str | None = None
|
||||
session_id: str | None = None
|
||||
user_id: str | None = None
|
||||
services: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""优先从 services 中取依赖,方便工具侧少写样板代码。"""
|
||||
|
||||
return self.services.get(key, default)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ToolResult:
|
||||
"""标准化工具执行结果。
|
||||
|
||||
统一返回结构的意义是:
|
||||
1. tool loop 更容易记录日志和失败信息
|
||||
2. provider 回灌时可以稳定地拿到字符串内容
|
||||
3. 后面要做工具审计时,数据结构已经固定
|
||||
"""
|
||||
|
||||
success: bool
|
||||
content: str
|
||||
tool_name: str
|
||||
error: str | None = None
|
||||
raw_output: Any | None = None
|
||||
|
||||
|
||||
class BaseTool(ABC):
|
||||
"""所有工具实现都应遵守的抽象基类。"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def spec(self) -> ToolSpec:
|
||||
"""返回工具元数据。"""
|
||||
|
||||
@abstractmethod
|
||||
async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
|
||||
"""执行工具调用。"""
|
||||
|
||||
|
||||
class ObjectBackedTool(BaseTool):
|
||||
"""把现有“轻量对象工具”适配到统一 BaseTool 契约。
|
||||
|
||||
目前 `MemoryTool` / `SessionSearchTool` 已经存在,但它们还不是统一的 BaseTool。
|
||||
这个适配器的作用就是避免重写业务逻辑,只做接口收口。
|
||||
"""
|
||||
|
||||
def __init__(self, backend: Any) -> None:
|
||||
self.backend = backend
|
||||
self._spec = ToolSpec(
|
||||
name=str(getattr(backend, "name")),
|
||||
description=str(getattr(backend, "description", "")),
|
||||
input_schema=dict(getattr(backend, "parameters", {"type": "object", "properties": {}})),
|
||||
)
|
||||
|
||||
@property
|
||||
def spec(self) -> ToolSpec:
|
||||
return self._spec
|
||||
|
||||
async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
|
||||
try:
|
||||
call_arguments = dict(arguments)
|
||||
self._inject_runtime_context(call_arguments, context)
|
||||
content = await self.backend.execute(**call_arguments)
|
||||
result = self._normalize_output(content)
|
||||
return ToolResult(
|
||||
success=result["success"],
|
||||
content=result["content"],
|
||||
tool_name=self.spec.name,
|
||||
error=result.get("error"),
|
||||
raw_output=content,
|
||||
)
|
||||
except Exception as exc:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
content=f"Tool {self.spec.name} failed: {exc}",
|
||||
tool_name=self.spec.name,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
def _inject_runtime_context(self, arguments: dict[str, Any], context: ToolContext) -> None:
|
||||
"""把少量 runtime 上下文注入到后端工具参数中。
|
||||
|
||||
当前只做最小注入:
|
||||
- 只有当 backend 明确暴露对应字段时才注入
|
||||
- 避免把 ToolContext 整个对象直接塞给现有 builtin 工具
|
||||
"""
|
||||
|
||||
if "current_session_id" not in arguments and hasattr(self.backend, "current_session_id"):
|
||||
arguments["current_session_id"] = context.session_id
|
||||
|
||||
@staticmethod
|
||||
def _normalize_output(content: Any) -> dict[str, Any]:
|
||||
"""把后端工具返回值转成统一 success/content/error 语义。
|
||||
|
||||
对现有 builtin 工具最关键的是:
|
||||
- 若返回的是 JSON 字符串,且包含 `success` 字段,就尊重它
|
||||
- 否则默认视为普通成功文本
|
||||
"""
|
||||
|
||||
if isinstance(content, str):
|
||||
try:
|
||||
parsed = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "content": content}
|
||||
if isinstance(parsed, dict) and "success" in parsed:
|
||||
return {
|
||||
"success": bool(parsed.get("success")),
|
||||
"content": content,
|
||||
"error": parsed.get("error"),
|
||||
}
|
||||
return {"success": True, "content": content}
|
||||
return {"success": True, "content": str(content)}
|
||||
Reference in New Issue
Block a user