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

@ -1,41 +1,39 @@
"""Gateway entrypoint for Beaver.
当前阶段先不扩 bus / channels adapter,只做最小消息桥接:
当前阶段只做最小 gateway 宿主与 channel adapter 桥接:
1. 启动时托管 `AgentService.start()`
2. 常驻消费 `MessageBus.inbound`
3. 调 `service.submit_direct(...)`
3. 调 `service.handle_inbound_message(...)`
4. 将结果写回 `MessageBus.outbound`
5. 退出时走 `AgentService.shutdown()`
5. 如果配置了 channel adapters则由 `ChannelManager` 分发 outbound
6. 退出时走 `AgentService.shutdown()`
"""
from __future__ import annotations
import asyncio
from collections.abc import Sequence
from contextlib import suppress
from pathlib import Path
from beaver.foundation.events import InboundMessage, MessageBus, OutboundMessage
from beaver.foundation.events import InboundMessage, MessageBus
from beaver.interfaces.channels import ChannelAdapter, ChannelManager
from beaver.services.agent_service import AgentService
async def _publish_bridge_error(
bus: MessageBus,
inbound: InboundMessage,
async def _cleanup_owned_service(
service: AgentService,
*,
detail: str,
finish_reason: str = "error",
timeout_seconds: float | None,
force: bool,
) -> None:
"""把 bridge 处理失败转换成结构化 outbound 错误消息。"""
"""Best-effort cleanup for service startup failures or cancellations."""
await bus.publish_outbound(
OutboundMessage(
message_id=inbound.message_id,
channel=inbound.channel,
session_id=inbound.session_id,
content=detail,
finish_reason=finish_reason,
metadata={"error": detail, "inbound_metadata": dict(inbound.metadata)},
)
)
with suppress(BaseException):
if service.is_running:
await service.shutdown(timeout_seconds=timeout_seconds, force=force)
else:
service.close()
async def _flush_pending_inbound(bus: MessageBus, *, reason: str) -> None:
@ -46,11 +44,17 @@ async def _flush_pending_inbound(bus: MessageBus, *, reason: str) -> None:
pending = bus.inbound.get_nowait()
except asyncio.QueueEmpty:
break
await _publish_bridge_error(bus, pending, detail=reason, finish_reason="stopped")
await bus.publish_outbound(
AgentService.build_outbound_error(
pending,
detail=reason,
finish_reason="stopped",
)
)
async def _await_bridge_shutdown(task: asyncio.Task[None], *, timeout_seconds: float = 1.0) -> None:
"""等待 bridge 退出;超时则取消,避免 shutdown 被桥接层反向卡死。"""
async def _await_task_shutdown(task: asyncio.Task[None], *, timeout_seconds: float = 1.0) -> None:
"""等待后台任务退出;超时则取消,避免 shutdown 被反向卡死。"""
try:
await asyncio.wait_for(task, timeout=timeout_seconds)
@ -85,53 +89,28 @@ async def _bridge_inbound_to_runtime(
continue
try:
result = await service.submit_direct(
inbound.content,
session_id=inbound.session_id,
source=f"gateway:{inbound.channel}",
user_id=inbound.user_id,
title=inbound.title,
execution_context=inbound.execution_context,
model=inbound.model,
provider_name=inbound.provider_name,
embedding_model=inbound.embedding_model,
)
outbound = await service.handle_inbound_message(inbound)
except asyncio.CancelledError:
await _publish_bridge_error(
bus,
inbound,
detail="Gateway stopped before completing the inbound message",
finish_reason="cancelled",
)
raise
except Exception as exc: # pragma: no cover - defensive bridge path
await _publish_bridge_error(
bus,
inbound,
detail=str(exc),
)
else:
await bus.publish_outbound(
OutboundMessage(
message_id=inbound.message_id,
channel=inbound.channel,
session_id=result.session_id,
run_id=result.run_id,
content=result.output_text,
finish_reason=result.finish_reason,
provider_name=result.provider_name,
model=result.model,
usage=dict(result.usage),
metadata={"inbound_metadata": dict(inbound.metadata)},
AgentService.build_outbound_error(
inbound,
detail="Gateway stopped before completing the inbound message",
finish_reason="cancelled",
)
)
raise
else:
await bus.publish_outbound(outbound)
async def run_gateway(
*,
workspace: str | Path | None = None,
config_path: str | Path | None = None,
service: AgentService | None = None,
bus: MessageBus | None = None,
channels: Sequence[ChannelAdapter] | None = None,
channel_manager: ChannelManager | None = None,
manage_service_lifecycle: bool | None = None,
stop_event: asyncio.Event | None = None,
shutdown_timeout_seconds: float | None = 5.0,
@ -142,19 +121,41 @@ async def run_gateway(
默认 ownership 语义:
- 未传 `service`gateway 自己创建并接管其 lifecycle
- 传入外部 `service`:默认只使用,不自动 start/shutdown
- `channel_manager` 和 `channels` 二选一,避免隐式修改外部 manager
"""
attached_service = service or AgentService(workspace=workspace)
attached_bus = bus or MessageBus()
attached_service = service or AgentService(workspace=workspace, config_path=config_path)
if channel_manager is not None and channels is not None:
raise ValueError("Pass either channel_manager or channels, not both")
if bus is not None:
attached_bus = bus
elif channel_manager is not None:
attached_bus = channel_manager.bus
else:
attached_bus = MessageBus()
attached_channel_manager = channel_manager
if attached_channel_manager is not None and attached_channel_manager.bus is not attached_bus:
raise ValueError("Injected channel_manager must share the gateway MessageBus")
if attached_channel_manager is None and channels is not None:
attached_channel_manager = ChannelManager(attached_bus)
if attached_channel_manager is not None and channels is not None:
for channel in channels:
attached_channel_manager.register(channel)
owns_service = manage_service_lifecycle if manage_service_lifecycle is not None else service is None
owned_stop_event = stop_event or asyncio.Event()
started = False
channels_started = False
if owns_service:
try:
await attached_service.start()
started = True
except Exception:
attached_service.close()
except BaseException:
await _cleanup_owned_service(
attached_service,
timeout_seconds=shutdown_timeout_seconds,
force=shutdown_force,
)
raise
if not attached_service.is_running:
@ -163,7 +164,25 @@ async def run_gateway(
"or allow the gateway to manage its lifecycle."
)
if attached_channel_manager is not None:
try:
await attached_channel_manager.start()
channels_started = True
except BaseException:
if owns_service and started:
await _cleanup_owned_service(
attached_service,
timeout_seconds=shutdown_timeout_seconds,
force=shutdown_force,
)
raise
bridge_task = asyncio.create_task(_bridge_inbound_to_runtime(attached_service, attached_bus, owned_stop_event))
dispatch_task: asyncio.Task[None] | None = None
dispatch_stop_event = asyncio.Event()
if attached_channel_manager is not None:
dispatch_task = asyncio.create_task(attached_channel_manager.dispatch_outbound(dispatch_stop_event))
try:
await owned_stop_event.wait()
finally:
@ -175,9 +194,14 @@ async def run_gateway(
force=shutdown_force,
)
finally:
await _await_bridge_shutdown(bridge_task)
await _await_task_shutdown(bridge_task)
else:
await _await_bridge_shutdown(bridge_task)
await _await_task_shutdown(bridge_task)
if dispatch_task is not None:
dispatch_stop_event.set()
await _await_task_shutdown(dispatch_task)
if attached_channel_manager is not None and channels_started:
await attached_channel_manager.stop()
def main() -> None: