Files
beaver_project/app-instance/backend/beaver/tools/base.py
steven_li 30ab74ffb2 feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接
- 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、
  PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、
  TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等
- 实现工具注册和装配功能
- 添加技能选择上下文参数
- 支持思考模式控制参数thinking_enabled

feat(coordinator): 重构任务执行计划器参数命名

- 将learning_candidate_enabled重命名为allow_candidate_generation
- 更新TeamGraphScheduler中的参数传递
- 修改LocalAgentRunner中的相关参数处理
- 更新README文档中的相应描述

refactor(context): 标准化工具调用参数格式

- 添加_json导入用于参数序列化
- 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷
- 修复工具调用中参数非字符串类型的序列化问题

refactor(session): 优化消息历史记录过滤逻辑

- 修改get_messages_as_conversation为基于运行状态过滤消息
- 排除未完成、失败或错误结束的运行记录
- 改进对话历史的可见性控制机制

fix(store): 修复FTS索引重建逻辑

- 添加异常处理防止FTS索引创建失败
- 实现_rebuild_fts_index方法重新构建全文搜索索引
- 优化索引触发器和表的维护流程
2026-05-14 09:43:48 +08:00

209 lines
7.1 KiB
Python

"""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. 以 MCP-style descriptor 作为统一事实来源
2. 导出给 provider 的 function schema
3. 在 registry 中做列出、查找、调试与 embedding 召回
"""
name: str
description: str
input_schema: dict[str, Any]
toolset: str = "core"
always_available: bool = False
metadata: dict[str, Any] = field(default_factory=dict)
def to_mcp_descriptor(self) -> dict[str, Any]:
"""导出 MCP ListTools 风格的工具描述。
MCP 的基础字段是 `name`、`description`、`inputSchema`。
Beaver 内部额外的 toolset/always_available 不塞进这个对象,
避免未来对接真实 MCP server 时出现格式偏差。
"""
return {
"name": self.name,
"description": self.description,
"inputSchema": self.input_schema,
}
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,
},
}
def to_embedding_candidate(self) -> dict[str, str]:
"""导出给语义召回使用的轻量文本候选。"""
return {
"name": self.name,
"description": self.description,
"input_schema": json.dumps(self.input_schema, ensure_ascii=False, sort_keys=True),
}
@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": {}})),
toolset=str(getattr(backend, "toolset", "core")),
always_available=bool(getattr(backend, "always_available", False)),
)
@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
if "workspace" not in arguments and hasattr(self.backend, "workspace"):
arguments["workspace"] = context.workspace
if "metadata" not in arguments:
arguments["metadata"] = context.metadata
@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)}