集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。 主要变更包括: - 在Dockerfile和环境配置中添加Beaver相关路径和配置变量 - 更新工作目录结构从.nanobot到.beaver - 实现Beaver引擎加载器,支持配置文件加载和工具组装 - 添加内置工具如ListDirectoryTool、ReadFileTool、SearchFilesTool - 更新消息处理流程,支持通道适配器和网关模式 - 重构技能系统,支持显式工具提示和嵌入式检索 - 改进错误处理和生命周期管理 此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
"""Runtime configuration schema for Beaver sandbox instances."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ProviderConfig:
|
|
"""One configured LLM provider profile."""
|
|
|
|
api_key: str | None = None
|
|
api_base: str | None = None
|
|
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
request_timeout_seconds: float | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AgentDefaultsConfig:
|
|
"""Default agent settings for this sandbox instance."""
|
|
|
|
workspace: str | None = None
|
|
model: str | None = None
|
|
provider: str | None = None
|
|
embedding_model: str | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class EmbeddingConfig:
|
|
"""Optional dedicated embedding model settings."""
|
|
|
|
provider: str | None = None
|
|
model: str | None = None
|
|
api_key: str | None = None
|
|
api_base: str | None = None
|
|
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
request_timeout_seconds: float | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class BeaverConfig:
|
|
"""Config loaded once per backend sandbox instance."""
|
|
|
|
agents_defaults: AgentDefaultsConfig = field(default_factory=AgentDefaultsConfig)
|
|
providers: dict[str, ProviderConfig] = field(default_factory=dict)
|
|
embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
|
|
config_path: Path | None = None
|
|
|
|
@property
|
|
def default_model(self) -> str | None:
|
|
return _clean(self.agents_defaults.model)
|
|
|
|
@property
|
|
def default_embedding_model(self) -> str:
|
|
return _clean(self.embedding.model) or _clean(self.agents_defaults.embedding_model) or "text-embedding-v4"
|
|
|
|
def resolve_provider_target(
|
|
self,
|
|
*,
|
|
model: str | None = None,
|
|
provider_name: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Resolve model/provider credentials from instance config.
|
|
|
|
Request-level model/provider overrides are allowed, but credentials are still
|
|
read from backend config, not from Web/channel payloads.
|
|
"""
|
|
|
|
resolved_model = _clean(model) or self.default_model
|
|
resolved_provider = _clean(provider_name) or self._infer_provider(resolved_model)
|
|
provider_cfg = self.providers.get(resolved_provider or "") if resolved_provider else None
|
|
payload: dict[str, Any] = {
|
|
"model": resolved_model,
|
|
"provider_name": resolved_provider,
|
|
}
|
|
if provider_cfg is not None:
|
|
payload.update(
|
|
{
|
|
"api_key": provider_cfg.api_key,
|
|
"api_base": provider_cfg.api_base,
|
|
"extra_headers": dict(provider_cfg.extra_headers),
|
|
"request_timeout_seconds": provider_cfg.request_timeout_seconds,
|
|
}
|
|
)
|
|
return {key: value for key, value in payload.items() if value not in (None, "", {})}
|
|
|
|
def resolve_embedding_target(self) -> dict[str, Any] | None:
|
|
"""Return an explicit embedding target when configured."""
|
|
|
|
has_explicit_embedding = any(
|
|
[
|
|
_clean(self.embedding.provider),
|
|
_clean(self.embedding.api_key),
|
|
_clean(self.embedding.api_base),
|
|
self.embedding.extra_headers,
|
|
self.embedding.request_timeout_seconds is not None,
|
|
]
|
|
)
|
|
if not has_explicit_embedding:
|
|
return None
|
|
|
|
provider_cfg = self.providers.get(_clean(self.embedding.provider) or "")
|
|
payload: dict[str, Any] = {
|
|
"provider": _clean(self.embedding.provider),
|
|
"model": self.default_embedding_model,
|
|
"api_key": _clean(self.embedding.api_key) or (provider_cfg.api_key if provider_cfg else None),
|
|
"api_base": _clean(self.embedding.api_base) or (provider_cfg.api_base if provider_cfg else None),
|
|
"extra_headers": self.embedding.extra_headers or (dict(provider_cfg.extra_headers) if provider_cfg else {}),
|
|
"request_timeout_seconds": self.embedding.request_timeout_seconds
|
|
or (provider_cfg.request_timeout_seconds if provider_cfg else None),
|
|
}
|
|
return {key: value for key, value in payload.items() if value not in (None, "", {})}
|
|
|
|
def _infer_provider(self, model: str | None) -> str | None:
|
|
configured_provider = _clean(self.agents_defaults.provider)
|
|
if configured_provider:
|
|
return configured_provider
|
|
|
|
if model and "/" in model:
|
|
prefix = model.split("/", 1)[0]
|
|
if prefix in self.providers:
|
|
return prefix
|
|
|
|
if len(self.providers) == 1:
|
|
return next(iter(self.providers))
|
|
return None
|
|
|
|
|
|
def _clean(value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
value = str(value).strip()
|
|
return value or None
|
|
|