Files
beaver_project/app-instance/backend/tests/unit/test_tool_assembler.py
steven_li 8aeb97a5fc feat(app): 移除内置agents并添加CORS支持和技能上传优化
移除了agents/registry.json中的所有内置agents配置,将agents数组清空。
为web应用添加了CORS中间件支持,允许指定的前端地址跨域访问。
重构了技能上传功能,增加了LLM重写机制,自动规范化上传的技能格式。
新增了工具名称提取逻辑,从技能正文中自动识别Required Tools段落。
更新了技能学习候选者和草稿的载荷结构,添加评估报告统计信息。
修改了意图路由技能的说明,改进任务状态管理逻辑。
2026-06-12 13:25:20 +08:00

201 lines
6.1 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
import subprocess
import sys
from types import SimpleNamespace
from beaver.engine.context import SkillContext
from beaver.foundation.embedding import EmbeddingRetriever
from beaver.skills.catalog.loader import SkillsLoader
from beaver.tools import BaseTool, ToolAssembler, ToolContext, ToolExecutor, ToolRegistry, ToolResult, ToolSpec
class DummyTool(BaseTool):
def __init__(
self,
name: str,
*,
description: str | None = None,
toolset: str = "test",
always_available: bool = False,
) -> None:
self._spec = ToolSpec(
name=name,
description=description or name,
input_schema={"type": "object", "properties": {}},
toolset=toolset,
always_available=always_available,
)
@property
def spec(self) -> ToolSpec:
return self._spec
async def invoke(self, arguments: dict, context: ToolContext) -> ToolResult:
return ToolResult(success=True, content="ok", tool_name=self.spec.name)
class StaticRetriever:
async def retrieve(self, **kwargs):
candidates = kwargs["candidates"]
top_k = kwargs["top_k"]
preferred = ["search_files", "echo"]
ordered = sorted(
candidates,
key=lambda item: preferred.index(item["name"]) if item["name"] in preferred else len(preferred),
)
return ordered[:top_k]
def test_tool_spec_exports_mcp_and_provider_schema() -> None:
spec = ToolSpec(
name="read_file",
description="Read a file",
input_schema={"type": "object", "properties": {"path": {"type": "string"}}},
toolset="file",
)
assert spec.to_mcp_descriptor() == {
"name": "read_file",
"description": "Read a file",
"inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}},
}
assert spec.to_provider_schema()["function"]["parameters"] == spec.input_schema
def test_tool_assembler_merges_always_skill_hints_and_embedding(tmp_path: Path) -> None:
skill_dir = tmp_path / "skills" / "docker-debug"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"""---
name: docker-debug
description: Debug Docker issues.
tools:
- terminal
---
# Docker Debug
""",
encoding="utf-8",
)
registry = ToolRegistry()
registry.register(DummyTool("memory", toolset="memory", always_available=True))
registry.register(DummyTool("terminal", toolset="shell"))
registry.register(DummyTool("search_files", toolset="file"))
registry.register(DummyTool("echo", toolset="debug"))
assembler = ToolAssembler(retriever=StaticRetriever())
loader = SkillsLoader(tmp_path)
selected = asyncio.run(
assembler.assemble(
task_description="排查 Docker 容器日志",
registry=registry,
skills_loader=loader,
activated_skills=[SkillContext(name="docker-debug", content="")],
top_k=1,
)
)
assert [spec.name for spec in selected] == ["memory", "terminal", "search_files"]
def test_tool_assembler_uses_required_tools_section_when_frontmatter_omits_tools(tmp_path: Path) -> None:
skill_dir = tmp_path / "skills" / "docker-debug"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"""---
name: docker-debug
description: Debug Docker issues.
---
# Docker Debug
## Overview
Debug Docker issues.
## Required Tools
- `terminal`
- `search_files`
## Workflow
Inspect logs and search related files.
""",
encoding="utf-8",
)
registry = ToolRegistry()
registry.register(DummyTool("memory", toolset="memory", always_available=True))
registry.register(DummyTool("terminal", toolset="shell"))
registry.register(DummyTool("search_files", toolset="file"))
registry.register(DummyTool("echo", toolset="debug"))
assembler = ToolAssembler(retriever=StaticRetriever())
loader = SkillsLoader(tmp_path)
record = loader.get_skill_record("docker-debug")
assert record is not None
assert record.tool_hints == ["terminal", "search_files"]
selected = asyncio.run(
assembler.assemble(
task_description="排查 Docker 容器日志",
registry=registry,
skills_loader=loader,
activated_skills=[SkillContext(name="docker-debug", content="", tool_hints=record.tool_hints)],
top_k=1,
)
)
assert [spec.name for spec in selected] == ["memory", "terminal", "search_files", "echo"]
def test_embedding_fallback_can_return_all_or_top_k() -> None:
candidates = [{"name": f"tool_{index}", "description": "", "input_schema": "{}"} for index in range(3)]
retriever = EmbeddingRetriever(api_key_env="MISSING_EMBEDDING_KEY", api_base_env="MISSING_EMBEDDING_BASE")
all_candidates = asyncio.run(
retriever.retrieve(query="x", candidates=candidates, top_k=1, fallback_top_k=None)
)
top_candidate = asyncio.run(
retriever.retrieve(query="x", candidates=candidates, top_k=1, fallback_top_k=1)
)
assert [item["name"] for item in all_candidates] == ["tool_0", "tool_1", "tool_2"]
assert [item["name"] for item in top_candidate] == ["tool_0"]
def test_beaver_tools_import_does_not_load_provider_stack_with_socks_proxy() -> None:
code = (
"import beaver.tools\n"
"from beaver.skills.catalog.loader import SkillsLoader\n"
"print('ok')"
)
result = subprocess.run(
[sys.executable, "-c", code],
check=False,
capture_output=True,
text=True,
env={
"PYTHONPATH": str(Path(__file__).resolve().parents[2]),
"HTTP_PROXY": "socks://127.0.0.1:7897/",
"HTTPS_PROXY": "socks://127.0.0.1:7897/",
},
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "ok"
def test_tool_executor_parses_object_tool_call_string_arguments() -> None:
tool_call = SimpleNamespace(name="echo", arguments='{"text": "hello"}')
name, arguments = ToolExecutor._normalize_tool_call(tool_call)
assert name == "echo"
assert arguments == {"text": "hello"}