feat(app-instance): 集成Beaver后端并更新配置管理

集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。

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

此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
This commit is contained in:
2026-04-27 17:37:40 +08:00
parent 36882a7d7b
commit 5ba5c7e4c1
47 changed files with 2821 additions and 462 deletions

View File

@ -2,17 +2,27 @@
from __future__ import annotations
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from beaver.engine.context import ContextBuilder
from beaver.engine.session import SessionManager
from beaver.foundation.config import BeaverConfig, load_config
from beaver.memory.curated.store import MemoryStore
from beaver.services.memory_service import MemoryService
from beaver.skills import SkillAssembler, SkillsLoader
from beaver.tools import ObjectBackedTool, ToolExecutor, ToolRegistry
from beaver.tools.builtins import EchoTool, MemoryTool, SessionSearchTool, SkillViewTool
from beaver.tools import ObjectBackedTool, ToolAssembler, ToolExecutor, ToolRegistry
from beaver.tools.builtins import (
EchoTool,
ListDirectoryTool,
MemoryTool,
ReadFileTool,
SearchFilesTool,
SessionSearchTool,
SkillViewTool,
)
@dataclass(slots=True)
@ -27,6 +37,7 @@ class EngineLoadResult:
"""
workspace: Path
config: BeaverConfig = field(default_factory=BeaverConfig)
tools: list[str] = field(default_factory=list)
skills: list[str] = field(default_factory=list)
memory_stores: list[str] = field(default_factory=list)
@ -35,6 +46,7 @@ class EngineLoadResult:
curated_memory_store: MemoryStore | None = None
memory_service: MemoryService | None = None
tool_registry: ToolRegistry | None = None
tool_assembler: ToolAssembler | None = None
tool_executor: ToolExecutor | None = None
context_builder: ContextBuilder | None = None
skills_loader: SkillsLoader | None = None
@ -89,19 +101,26 @@ class EngineLoader:
self,
*,
workspace: str | Path | None = None,
config_path: str | Path | None = None,
config: BeaverConfig | None = None,
session_manager: SessionManager | None = None,
curated_memory_store: MemoryStore | None = None,
memory_service: MemoryService | None = None,
tool_registry: ToolRegistry | None = None,
tool_assembler: ToolAssembler | None = None,
context_builder: ContextBuilder | None = None,
skills_loader: SkillsLoader | None = None,
skill_assembler: SkillAssembler | None = None,
) -> None:
self.workspace = Path(workspace or Path.cwd())
self.config = config or load_config(workspace=workspace, config_path=config_path)
configured_workspace = self.config.agents_defaults.workspace
env_workspace = os.getenv("BEAVER_WORKSPACE")
self.workspace = Path(workspace or configured_workspace or env_workspace or Path.cwd())
self._session_manager = session_manager
self._curated_memory_store = curated_memory_store
self._memory_service = memory_service
self._tool_registry = tool_registry
self._tool_assembler = tool_assembler
self._context_builder = context_builder
self._skills_loader = skills_loader
self._skill_assembler = skill_assembler
@ -127,15 +146,20 @@ class EngineLoader:
ObjectBackedTool(MemoryTool(store=memory_service.get_store())),
ObjectBackedTool(SkillViewTool(loader=skills_loader)),
ObjectBackedTool(SessionSearchTool(db=session_manager)),
ObjectBackedTool(ListDirectoryTool()),
ObjectBackedTool(ReadFileTool()),
ObjectBackedTool(SearchFilesTool()),
]
)
context_builder = self._context_builder or ContextBuilder()
tool_assembler = self._tool_assembler or ToolAssembler()
tool_executor = ToolExecutor(tool_registry)
skill_assembler = self._skill_assembler or SkillAssembler(skills_loader)
result = EngineLoadResult(
workspace=workspace,
config=self.config,
tools=[spec.name for spec in tool_registry.list_specs()],
skills=[record.name for record in skills_loader.list_skills(filter_unavailable=False)],
memory_stores=["curated"],
@ -144,6 +168,7 @@ class EngineLoader:
curated_memory_store=memory_service.get_store(),
memory_service=memory_service,
tool_registry=tool_registry,
tool_assembler=tool_assembler,
tool_executor=tool_executor,
context_builder=context_builder,
skills_loader=skills_loader,

View File

@ -272,12 +272,24 @@ class AgentLoop:
memory_service = self._require_loaded("memory_service")
context_builder = self._require_loaded("context_builder")
tool_registry = self._require_loaded("tool_registry")
tool_assembler = self._require_loaded("tool_assembler")
tool_executor = self._require_loaded("tool_executor")
skills_loader = self._require_loaded("skills_loader")
skill_assembler = self._require_loaded("skill_assembler")
config = loaded.config
configured_provider = config.resolve_provider_target(model=model, provider_name=provider_name)
resolved_session_id = session_id or uuid4().hex
resolved_run_id = uuid4().hex
resolved_model = model or self.profile.default_model
resolved_model = configured_provider.get("model") or self.profile.default_model
resolved_provider_name = configured_provider.get("provider_name") or provider_name
resolved_api_key = api_key or configured_provider.get("api_key")
resolved_api_base = api_base or configured_provider.get("api_base")
resolved_extra_headers = extra_headers or configured_provider.get("extra_headers")
resolved_request_timeout_seconds = configured_provider.get("request_timeout_seconds")
resolved_embedding_model = embedding_model or config.default_embedding_model
resolved_embedding_target = embedding_target or config.resolve_embedding_target()
resolved_max_tokens = max_tokens or self.profile.max_tokens
resolved_temperature = self.profile.temperature if temperature is None else temperature
resolved_max_tool_iterations = (
@ -316,20 +328,21 @@ class AgentLoop:
user_message_recorded = False
iterations = 0
final_usage: dict[str, Any] = {}
final_provider_name: str | None = provider_name
final_provider_name: str | None = resolved_provider_name
final_model: str | None = resolved_model
try:
bundle = provider_bundle or make_provider_bundle(
model=resolved_model,
provider_name=provider_name,
api_key=api_key,
api_base=api_base,
extra_headers=extra_headers,
provider_name=resolved_provider_name,
api_key=resolved_api_key,
api_base=resolved_api_base,
request_timeout_seconds=resolved_request_timeout_seconds,
extra_headers=resolved_extra_headers,
routing=routing,
fallback_target=fallback_target,
auxiliary_target=auxiliary_target,
embedding_target=embedding_target,
embedding_model=embedding_model or "text-embedding-v4",
embedding_target=resolved_embedding_target,
embedding_model=resolved_embedding_model,
)
skill_selector_provider = bundle.auxiliary_provider or bundle.main_provider
skill_selector_model = (
@ -364,6 +377,32 @@ class AgentLoop:
user_id=user_id,
)
selected_tool_specs = await tool_assembler.assemble(
task_description=task,
registry=tool_registry,
skills_loader=skills_loader,
activated_skills=assembled_skills.activated_skills,
embedding_runtime=bundle.embedding_runtime,
top_k=10,
)
tool_schemas = tool_registry.export_selected_provider_schemas(selected_tool_specs)
session_manager.append_message(
resolved_session_id,
run_id=resolved_run_id,
role="system",
event_type="tool_selection_snapshotted",
event_payload={
"tools": [spec.to_mcp_descriptor() for spec in selected_tool_specs],
"tool_names": [spec.name for spec in selected_tool_specs],
},
content=", ".join(spec.name for spec in selected_tool_specs) or None,
context_visible=False,
source=source,
title=title,
model=resolved_model,
user_id=user_id,
)
build_input = ContextBuildInput(
base_system_prompt=self.profile.system_prompt,
history=session_manager.get_history(resolved_session_id),
@ -412,7 +451,6 @@ class AgentLoop:
provider = bundle.main_provider
messages = list(context_result.messages)
tool_schemas = tool_registry.export_provider_schemas()
tool_context = ToolContext(
workspace=str(loaded.workspace),
session_id=resolved_session_id,

View File

@ -8,7 +8,7 @@ import os
from typing import Any
from .base import LLMProvider, LLMResponse, ToolCallRequest
from .registry import find_by_model, find_gateway
from .registry import find_by_model, find_by_name, find_gateway
from .runtime import ProviderRoutingConfig
try: # pragma: no cover - optional dependency
@ -58,7 +58,11 @@ class LiteLLMProvider(LLMProvider):
if not api_key:
return {}
spec = self._gateway or find_by_model(model)
spec = self._gateway
if spec is None and self.provider_name:
spec = find_by_name(self.provider_name)
if spec is None:
spec = find_by_model(model)
if spec is None or not spec.env_key:
return {}
overrides: dict[str, str] = {spec.env_key: api_key}
@ -97,6 +101,15 @@ class LiteLLMProvider(LLMProvider):
if prefix and not resolved.startswith(f"{prefix}/"):
resolved = f"{prefix}/{resolved}"
return resolved
if self.provider_name:
spec = find_by_name(self.provider_name)
if spec is not None and not spec.is_gateway and not spec.is_local:
resolved = model
if spec.litellm_prefix and not any(resolved.startswith(prefix) for prefix in spec.skip_prefixes):
resolved = f"{spec.litellm_prefix}/{resolved}"
elif spec.name == "openai" and "/" not in resolved:
resolved = f"openai/{resolved}"
return resolved
spec = find_by_model(model)
if spec and spec.litellm_prefix:
if not any(model.startswith(prefix) for prefix in spec.skip_prefixes):