修改了nanobot,往Hermes agent的风格走,进度1/3

This commit is contained in:
2026-04-20 18:11:14 +08:00
parent cdfc222c9f
commit 36882a7d7b
261 changed files with 12659 additions and 604 deletions

View File

@ -0,0 +1,5 @@
"""Tool execution runtime helpers."""
from .executor import ToolExecutor
__all__ = ["ToolExecutor"]

View File

@ -0,0 +1,114 @@
"""Beaver 工具执行器。
这层专门负责把 provider 返回的 tool call 转成真正的工具执行。
它不关心 provider 是 OpenAI、Anthropic 还是 Codex只关心
1. 工具叫什么
2. 参数是什么
3. registry 能不能找到它
4. 执行结果怎么标准化
"""
from __future__ import annotations
import json
from typing import Any
from beaver.engine.providers.base import ToolCallRequest
from beaver.tools.base import ToolContext, ToolResult
from beaver.tools.registry.tool_registry import ToolRegistry
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 isinstance(tool_call, ToolCallRequest):
return tool_call.name, dict(tool_call.arguments)
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 isinstance(tool_call, ToolCallRequest):
return str(tool_call.name 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"