feat(engine): 添加技能查看工具并优化异步任务管理 - 添加SkillViewTool到引擎加载器中,增强技能管理功能 - 在AgentLoop中引入_active_direct_task来跟踪活跃任务 - 实现直接任务执行时的同步处理逻辑 - 更新工具实例化方式以支持依赖注入 feat(config): 增加智能体运行时参数配置支持 - 扩展AgentDefaultsConfig添加max_tokens和temperature字段 - 实现配置解析函数_first_config_value处理多个配置源 - 支持通过Web API动态更新智能体运行时参数 - 添加前端页面配置表单和验证逻辑 refactor(provider): 统一最大令牌数参数类型为可选整型 - 将所有LLM提供者的max_tokens参数改为int | None类型 - 为AnthropicProvider实现模型特定的最大令牌数默认值 - 调整参数传递逻辑,优先级:调用参数 > 配置文件 > 模型默认值 - 移除硬编码的默认值,改用条件判断 feat(process): 增强事件投影功能 - 添加工具调用开始/结束事件的映射逻辑 - 实现技能激活事件的识别和展示 - 添加辅助函数处理工具调用名称和参数提取 - 优化运行记录关联逻辑,提升事件匹配准确性 fix(web): 更新网络请求客户端信任环境设置 - 将WebFetchTool和WebSearchTool的trust_env参数设为True - 确保HTTP客户端能够正确使用系统代理配置 - 修复可能的网络连接问题 test: 添加配置加载和事件投影相关测试 - 新增智能体默认参数配置测试用例 - 实现API配置持久化和重载测试 - 添加技能卡片和工具事件的投影测试 ```
223 lines
7.3 KiB
Python
223 lines
7.3 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
|
|
max_tokens: int | None = None
|
|
temperature: float | None = None
|
|
max_context_messages: int | None = None
|
|
max_tool_iterations: int | 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 MCPServerConfig:
|
|
"""One configured MCP server.
|
|
|
|
Transport is inferred from fields:
|
|
- command => local stdio MCP server
|
|
- url => remote streamable HTTP MCP server
|
|
"""
|
|
|
|
command: str = ""
|
|
args: list[str] = field(default_factory=list)
|
|
env: dict[str, str] = field(default_factory=dict)
|
|
url: str = ""
|
|
headers: dict[str, str] = field(default_factory=dict)
|
|
auth_mode: str = "none"
|
|
auth_audience: str = ""
|
|
auth_scopes: list[str] = field(default_factory=list)
|
|
tool_timeout: int = 30
|
|
sensitive: bool = False
|
|
kind: str = "online"
|
|
category: str = "online"
|
|
managed: bool = False
|
|
display_name: str = ""
|
|
source: str = "config"
|
|
|
|
@property
|
|
def transport(self) -> str:
|
|
return "stdio" if _clean(self.command) else "http"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ToolsConfig:
|
|
"""Runtime tool configuration."""
|
|
|
|
restrict_to_workspace: bool = True
|
|
mcp_servers: dict[str, MCPServerConfig] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AuthzConfig:
|
|
"""External AuthZ service configuration."""
|
|
|
|
enabled: bool = False
|
|
base_url: str = ""
|
|
request_timeout_seconds: int = 10
|
|
outlook_mcp_url: str = ""
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class BackendIdentityConfig:
|
|
"""This backend's AuthZ client identity."""
|
|
|
|
backend_id: str = ""
|
|
client_id: str = ""
|
|
client_secret: str = ""
|
|
name: str = ""
|
|
public_base_url: str = ""
|
|
|
|
|
|
@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)
|
|
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
|
authz: AuthzConfig = field(default_factory=AuthzConfig)
|
|
backend_identity: BackendIdentityConfig = field(default_factory=BackendIdentityConfig)
|
|
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
|
|
requested_provider = _clean(provider_name)
|
|
enabled_providers = self._enabled_provider_names()
|
|
resolved_provider = (
|
|
requested_provider
|
|
if requested_provider and requested_provider in enabled_providers
|
|
else 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 and configured_provider != "custom":
|
|
return configured_provider
|
|
|
|
if model and "/" in model:
|
|
prefix = model.split("/", 1)[0]
|
|
if prefix in self._enabled_provider_names():
|
|
return prefix
|
|
|
|
enabled_providers = self._enabled_provider_names()
|
|
if len(enabled_providers) == 1:
|
|
return enabled_providers[0]
|
|
return None
|
|
|
|
def _enabled_provider_names(self) -> list[str]:
|
|
return [
|
|
name
|
|
for name, provider in self.providers.items()
|
|
if name != "custom"
|
|
and any(
|
|
[
|
|
_clean(provider.api_key),
|
|
_clean(provider.api_base),
|
|
provider.extra_headers,
|
|
]
|
|
)
|
|
]
|
|
|
|
|
|
def _clean(value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
value = str(value).strip()
|
|
return value or None
|