Files
auto_agent/src/agent/core/config_manager.py
T
2026-06-17 12:58:18 +08:00

340 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Agent 配置管理:从 JSON 文件加载/保存 agent、tool、env、model 配置。
系统定位:
提供统一接口读写 ``agent/core/`` 下的 JSON 配置文件,
使前端手动配置的 JSON 可以替代 core 底下的相应 .py 硬编码。
配置文件:
agent_config.json - Agent 运行时配置(名称、模型引用、工具列表、system_prompt)
tool_config.json - 工具权限与参数配置
env_config.json - 环境变量与 LLM 模型列表
可扩展性:
可增加版本号、校验 schema、多环境配置切换。
"""
from __future__ import annotations
import json
import os
import uuid
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any
# ---------- 配置文件路径 ----------
_CORE_DIR = Path(__file__).resolve().parent
AGENT_CONFIG_PATH = _CORE_DIR / "agent_config.json"
TOOL_CONFIG_PATH = _CORE_DIR / "tool_config.json"
ENV_CONFIG_PATH = _CORE_DIR / "env_config.json"
# ---------- 模型配置 ----------
@dataclass
class ModelConfig:
id: str = "" # 唯一标识
name: str = "" # 显示名称
api_key: str = ""
base_url: str = ""
model: str = "" # API 模型名
timeout: float = 60.0
max_retries: int = 0
# ---------- LLM 配置结构(保留兼容) ----------
@dataclass
class LLMConfig:
model: str = ""
api_key: str = ""
base_url: str = ""
timeout: float = 60.0
max_retries: int = 0
@dataclass
class AgentConfig:
name: str = ""
system_prompt: str = ""
max_iterations: int = 200
llm: LLMConfig = field(default_factory=LLMConfig) # 保留兼容
llm_model_id: str = "" # 新:引用 models 列表中的模型 ID
tools: list[str] = field(default_factory=list)
enabled: bool = True
role: str = "sub" # "main" | "sub"
@dataclass
class ToolConfig:
name: str = ""
display_name: str = ""
description: str = ""
permission: str = "confirm" # "direct" | "confirm"
auto_execute_rules: list[dict] = field(default_factory=list) # [{"parameter":"action","operator":"equals","value":"open"}, ...]
enabled: bool = True
# ---------- 通用 JSON 读写 ----------
def _read_json(path: Path) -> dict | list | None:
"""读取 JSON 文件,不存在或格式错误返回 None。"""
if not path.is_file():
return None
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
def _write_json(path: Path, data: Any) -> bool:
"""写入 JSON 文件,成功返回 True。"""
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return True
except OSError:
return False
# ---------- Agent 配置 ----------
def _default_agent_config() -> list[dict]:
"""生成默认 agent 配置(单 agent: main)。"""
from agent.memory.system_prompt import MAIN_SYSTEM_PROMPT
from agent.tools import get_all_available_tool_names
models = load_models()
default_model_id = models[0]["id"] if models else ""
return [
{
"name": "main",
"system_prompt": MAIN_SYSTEM_PROMPT,
"max_iterations": 200,
"llm_model_id": default_model_id,
"tools": get_all_available_tool_names(),
"enabled": True,
"role": "main",
}
]
def load_agent_configs() -> list[AgentConfig]:
"""从 agent_config.json 加载 agent 配置列表,无文件时返回默认。"""
raw = _read_json(AGENT_CONFIG_PATH)
if not raw or not isinstance(raw, list):
raw = _default_agent_config()
_write_json(AGENT_CONFIG_PATH, raw)
configs = []
for item in raw:
llm_raw = item.get("llm", {})
configs.append(AgentConfig(
name=item.get("name", ""),
system_prompt=item.get("system_prompt", ""),
max_iterations=item.get("max_iterations", 200),
llm=LLMConfig(
model=llm_raw.get("model", ""),
api_key=llm_raw.get("api_key", ""),
base_url=llm_raw.get("base_url", ""),
timeout=llm_raw.get("timeout", 60.0),
max_retries=llm_raw.get("max_retries", 0),
),
llm_model_id=item.get("llm_model_id", ""),
tools=item.get("tools", []),
enabled=item.get("enabled", True),
role=item.get("role", "sub"),
))
return configs
def save_agent_configs(configs: list[dict]) -> bool:
"""将 agent 配置列表写入 agent_config.json。"""
return _write_json(AGENT_CONFIG_PATH, configs)
# ---------- Tool 配置 ----------
# 各工具的默认 auto_execute_rules —— 与 tool_policy.py 的硬编码规则对应
_TOOL_DEFAULT_AUTO_RULES: dict[str, list[dict]] = {
"browser_control": [
{"parameter": "action", "operator": "equals", "value": "open"},
],
"rag_tool": [
{"parameter": "action_type", "operator": "equals", "value": "query"},
],
}
def _get_tool_parameters() -> dict[str, list[dict]]:
"""获取每个工具的参数名和类型信息,供前端生成 auto_execute_rules 提示。
返回:
{tool_name: [{"name": "action", "type": "Literal[open,close]"}, ...], ...}
"""
from agent.tools import _ALL_AVAILABLE_TOOL_CLASSES
import typing
result: dict[str, list[dict]] = {}
for name, cls in _ALL_AVAILABLE_TOOL_CLASSES.items():
try:
inst = cls()
schema = getattr(inst, "args_schema", None)
if schema is None:
result[name] = []
continue
fields_info = getattr(schema, "model_fields", {})
params = []
for fname, finfo in fields_info.items():
annotation = finfo.annotation
type_str = str(annotation) if annotation is not None else "Any"
# 简化 Literal 显示
import re
m = re.search(r"Literal\[(.+?)\]", type_str)
if m:
type_str = "Literal[" + m.group(1) + "]"
params.append({"name": fname, "type": type_str})
result[name] = params
except Exception:
result[name] = []
return result
def _default_tool_config() -> list[dict]:
"""根据所有可用工具生成默认 tool 配置。"""
from agent.tools import get_all_available_tool_names, get_all_available_tool_descriptions
from agent.core.tool_policy import classify_tool_execution
all_names = get_all_available_tool_names()
all_descriptions = get_all_available_tool_descriptions()
result = []
for tname in all_names:
display_name, desc = all_descriptions.get(tname, (tname, ""))
perm = classify_tool_execution(tname, {})
default_rules = _TOOL_DEFAULT_AUTO_RULES.get(tname, [])
result.append({
"name": tname,
"display_name": display_name,
"description": desc,
"permission": perm,
"auto_execute_rules": default_rules,
"enabled": True,
})
return result
def load_tool_configs() -> list[ToolConfig]:
"""从 tool_config.json 加载工具配置,无文件时返回默认。"""
raw = _read_json(TOOL_CONFIG_PATH)
if not raw or not isinstance(raw, list):
raw = _default_tool_config()
_write_json(TOOL_CONFIG_PATH, raw)
configs = []
for item in raw:
configs.append(ToolConfig(
name=item.get("name", ""),
display_name=item.get("display_name", ""),
description=item.get("description", ""),
permission=item.get("permission", "confirm"),
auto_execute_rules=item.get("auto_execute_rules", []),
enabled=item.get("enabled", True),
))
return configs
def save_tool_configs(configs: list[dict]) -> bool:
"""将 tool 配置列表写入 tool_config.json。"""
return _write_json(TOOL_CONFIG_PATH, configs)
# ---------- 强制权限工具(前端锁死,不可修改) ----------
# 仅必须 confirm 的工具锁死;safe_tools 默认 direct 但用户可改为 confirm
_FORCED_CONFIRM_TOOLS = {"human_interaction"}
def get_tool_forced_permissions() -> dict[str, str]:
"""返回强制权限的工具映射 {tool_name: "confirm"},
前端应对这些工具禁用权限下拉框和 auto_execute_rules 编辑。"""
return {name: "confirm" for name in _FORCED_CONFIRM_TOOLS}
# ---------- 模型管理 ----------
def _default_models() -> list[dict]:
"""生成默认模型列表,首次启动时从 .env 继承已有 LLM 配置。"""
import os
api_key = os.environ.get("LLM_API_KEY", "")
base_url = os.environ.get("LLM_BASE_URL", "")
model = os.environ.get("LLM_MODEL", "")
timeout = os.environ.get("LLM_TIMEOUT", "60")
if api_key and base_url and model:
return [
{
"id": str(uuid.uuid4())[:8],
"name": "默认模型",
"api_key": api_key,
"base_url": base_url,
"model": model,
"timeout": float(timeout) if timeout else 60.0,
"max_retries": 0,
}
]
return []
def load_models() -> list[dict]:
"""从 env_config.json 的 models 字段加载模型列表。"""
raw = _read_json(ENV_CONFIG_PATH)
if isinstance(raw, dict) and "models" in raw and isinstance(raw["models"], list) and raw["models"]:
return raw["models"]
return _default_models()
def save_models(models: list[dict]) -> bool:
"""将模型列表写入 env_config.json(合并其他字段)。"""
current = load_env_config()
current["models"] = models
return _write_json(ENV_CONFIG_PATH, current)
# ---------- Env 配置 ----------
def _default_env_config() -> dict:
"""生成默认 env 配置(不含 models,models 由 load_models 独立处理)。"""
return {
"TTS_BASE_URL": "",
"ASR_BASE_URL": "",
"RAG_BASE_URL": "",
"LLM_CONTEXT_WINDOW": "131072",
"MINI_COMPRESS_RATE": "0.5",
"HARD_COMPRESS_RATE": "0.75",
"CURRENT_TURNS": "4",
"IMG_SIZE": "768",
"RAG_CHUNK_SIZE": "500",
"RAG_CHUNK_OVERLAP": "50",
"GROUNDING_WIDTH": "1000",
"GROUNDING_HEIGHT": "1000",
}
def load_env_config() -> dict:
"""从 env_config.json 加载环境变量配置,无文件时返回默认。"""
raw = _read_json(ENV_CONFIG_PATH)
if not raw or not isinstance(raw, dict):
raw = _default_env_config()
raw["models"] = _default_models()
_write_json(ENV_CONFIG_PATH, raw)
return raw
# 确保有 models 字段(向后兼容旧配置文件)
if "models" not in raw or not isinstance(raw.get("models"), list):
raw["models"] = _default_models()
return raw
def save_env_config(config: dict) -> bool:
"""将 env 配置写入 env_config.json(保留已有 models)。"""
# 保留已有的 models 列表(由 models API 单独管理)
existing = _read_json(ENV_CONFIG_PATH)
if isinstance(existing, dict) and "models" in existing:
config["models"] = existing["models"]
return _write_json(ENV_CONFIG_PATH, config)
# ---------- 获取所有已注册工具的基础信息 ----------
def get_registered_tool_names() -> list[str]:
"""返回所有可用工具名称(含数据库工具)。"""
from agent.tools import get_all_available_tool_names
return get_all_available_tool_names()