Files
beaver_project/app-instance/backend/beaver/tools/runtime/executor.py
steven_li 5ba5c7e4c1 feat(app-instance): 集成Beaver后端并更新配置管理
集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。

主要变更包括:
- 在Dockerfile和环境配置中添加Beaver相关路径和配置变量
- 更新工作目录结构从.nanobot到.beaver
- 实现Beaver引擎加载器,支持配置文件加载和工具组装
- 添加内置工具如ListDirectoryTool、ReadFileTool、SearchFilesTool
- 更新消息处理流程,支持通道适配器和网关模式
- 重构技能系统,支持显式工具提示和嵌入式检索
- 改进错误处理和生命周期管理

此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
2026-04-27 17:37:40 +08:00

118 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Beaver 工具执行器。
这层专门负责把 provider 返回的 tool call 转成真正的工具执行。
它不关心 provider 是 OpenAI、Anthropic 还是 Codex只关心
1. 工具叫什么
2. 参数是什么
3. registry 能不能找到它
4. 执行结果怎么标准化
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
from beaver.tools.base import ToolContext, ToolResult
from beaver.tools.registry.tool_registry import ToolRegistry
if TYPE_CHECKING:
from beaver.engine.providers.base import ToolCallRequest
class ToolExecutor:
"""统一执行单个 tool call。"""
def __init__(self, registry: ToolRegistry) -> None:
self.registry = registry
async def execute(
self,
tool_name: str,
arguments: dict[str, Any] | None,
*,
context: ToolContext | None = None,
) -> ToolResult:
"""按工具名执行一次调用。"""
tool = self.registry.get(tool_name)
if tool is None:
return ToolResult(
success=False,
content=f"Tool {tool_name} is not registered.",
tool_name=tool_name,
error="tool_not_found",
)
return await tool.invoke(arguments or {}, context or ToolContext())
async def execute_tool_call(
self,
tool_call: ToolCallRequest | dict[str, Any],
*,
context: ToolContext | None = None,
) -> ToolResult:
"""执行 provider 返回的一次结构化 tool call。
兼容两种输入:
- `ToolCallRequest`
- OpenAI 风格 dict
"""
try:
tool_name, arguments = self._normalize_tool_call(tool_call)
except Exception as exc:
return ToolResult(
success=False,
content=f"Tool call could not be parsed: {exc}",
tool_name=self._extract_tool_name(tool_call),
error="tool_call_parse_error",
)
parse_error = arguments.pop("__beaver_tool_argument_parse_error__", None)
if parse_error is not None:
return ToolResult(
success=False,
content=f"Tool call arguments for {tool_name} could not be parsed: {parse_error}",
tool_name=tool_name,
error="tool_call_argument_parse_error",
raw_output=arguments.get("__raw_arguments__"),
)
return await self.execute(tool_name, arguments, context=context)
@staticmethod
def _normalize_tool_call(tool_call: ToolCallRequest | dict[str, Any]) -> tuple[str, dict[str, Any]]:
if not isinstance(tool_call, dict):
name = getattr(tool_call, "name", None)
arguments = getattr(tool_call, "arguments", {})
else:
function = tool_call.get("function")
if isinstance(function, dict):
name = function.get("name")
arguments = function.get("arguments", {})
else:
name = tool_call.get("name")
arguments = tool_call.get("arguments", {})
if not name:
raise ValueError("Tool call is missing a tool name")
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError as exc:
raise ValueError(f"Tool call arguments for {name!r} are not valid JSON") from exc
if not isinstance(arguments, dict):
raise ValueError(f"Tool call arguments for {name!r} must be a dict")
return str(name), arguments
@staticmethod
def _extract_tool_name(tool_call: ToolCallRequest | dict[str, Any]) -> str:
if not isinstance(tool_call, dict):
return str(getattr(tool_call, "name", None) or "unknown")
function = tool_call.get("function")
if isinstance(function, dict) and function.get("name"):
return str(function["name"])
if tool_call.get("name"):
return str(tool_call["name"])
return "unknown"