Files
beaver_project/app-instance/backend/beaver/foundation/config/loader.py
steven_li 5ba5c7e4c1 feat(app-instance): 集成Beaver后端并更新配置管理
集成新的Beaver后端服务到应用实例中,替换原有的nanobot实现。

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

此变更使应用实例能够使用统一的Beaver后端进行AI代理运行时管理。
2026-04-27 17:37:40 +08:00

128 lines
4.6 KiB
Python

"""Config loader for per-sandbox Beaver runtime settings."""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from .schema import AgentDefaultsConfig, BeaverConfig, EmbeddingConfig, ProviderConfig
def default_config_path(*, workspace: str | Path | None = None) -> Path:
"""Resolve the default config path for a single-user sandbox instance.
Priority:
1. `BEAVER_CONFIG_PATH`
2. `NANOBOT_CONFIG_PATH` for compatibility during migration
3. `BEAVER_HOME/config.json`
4. `NANOBOT_HOME/config.json` for migration compatibility
5. `<workspace>/.beaver/config.json`
6. `./.beaver/config.json`
"""
explicit = os.getenv("BEAVER_CONFIG_PATH") or os.getenv("NANOBOT_CONFIG_PATH")
if explicit:
return Path(explicit).expanduser()
beaver_home = os.getenv("BEAVER_HOME")
if beaver_home:
return Path(beaver_home).expanduser() / "config.json"
nanobot_home = os.getenv("NANOBOT_HOME")
if nanobot_home:
return Path(nanobot_home).expanduser() / "config.json"
root = Path(workspace).expanduser() if workspace is not None else Path.cwd()
return root / ".beaver" / "config.json"
def load_config(
*,
workspace: str | Path | None = None,
config_path: str | Path | None = None,
) -> BeaverConfig:
"""Load backend config; missing config is treated as an empty config."""
path = Path(config_path).expanduser() if config_path is not None else default_config_path(workspace=workspace)
if not path.exists():
return BeaverConfig(config_path=path)
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"Beaver config must be a JSON object: {path}")
return BeaverConfig(
agents_defaults=_parse_agent_defaults(data),
providers=_parse_providers(data.get("providers")),
embedding=_parse_embedding(data),
config_path=path,
)
def _parse_agent_defaults(data: dict[str, Any]) -> AgentDefaultsConfig:
agents = _as_dict(data.get("agents"))
defaults = _as_dict(agents.get("defaults"))
return AgentDefaultsConfig(
workspace=_string(defaults.get("workspace") or data.get("workspace")),
model=_string(defaults.get("model") or data.get("model")),
provider=_string(defaults.get("provider") or data.get("provider")),
embedding_model=_string(defaults.get("embeddingModel") or defaults.get("embedding_model") or data.get("embeddingModel")),
)
def _parse_providers(raw: Any) -> dict[str, ProviderConfig]:
providers: dict[str, ProviderConfig] = {}
for name, payload in _as_dict(raw).items():
if not isinstance(payload, dict):
continue
providers[str(name)] = ProviderConfig(
api_key=_string(payload.get("apiKey") or payload.get("api_key")),
api_base=_string(payload.get("apiBase") or payload.get("api_base") or payload.get("baseUrl") or payload.get("base_url")),
extra_headers=_string_dict(payload.get("extraHeaders") or payload.get("extra_headers") or payload.get("headers")),
request_timeout_seconds=_float(
payload.get("requestTimeoutSeconds")
or payload.get("request_timeout_seconds")
or payload.get("timeout")
),
)
return providers
def _parse_embedding(data: dict[str, Any]) -> EmbeddingConfig:
raw = _as_dict(data.get("embedding") or data.get("embeddings"))
return EmbeddingConfig(
provider=_string(raw.get("provider") or raw.get("provider_name")),
model=_string(raw.get("model") or data.get("embeddingModel") or data.get("embedding_model")),
api_key=_string(raw.get("apiKey") or raw.get("api_key")),
api_base=_string(raw.get("apiBase") or raw.get("api_base") or raw.get("baseUrl") or raw.get("base_url")),
extra_headers=_string_dict(raw.get("extraHeaders") or raw.get("extra_headers") or raw.get("headers")),
request_timeout_seconds=_float(
raw.get("requestTimeoutSeconds") or raw.get("request_timeout_seconds") or raw.get("timeout")
),
)
def _as_dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _string(value: Any) -> str | None:
if value is None:
return None
value = str(value).strip()
return value or None
def _string_dict(value: Any) -> dict[str, str]:
if not isinstance(value, dict):
return {}
return {str(key): str(item) for key, item in value.items() if item is not None}
def _float(value: Any) -> float | None:
if value in (None, ""):
return None
return float(value)