2025-08-31 00:34:26 +08:00
|
|
|
|
"""Define the configurable parameters for the agent."""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import uuid
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from dataclasses import MISSING, dataclass, field, fields
|
2025-08-31 00:34:26 +08:00
|
|
|
|
from pathlib import Path
|
2025-10-25 20:01:23 +08:00
|
|
|
|
from typing import Annotated, get_args, get_origin
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
|
|
|
|
|
import yaml
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
from src import config as sys_config
|
2026-01-15 16:04:19 +08:00
|
|
|
|
from src.services.mcp_service import get_mcp_server_names
|
2025-08-31 00:34:26 +08:00
|
|
|
|
from src.utils import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
|
|
|
|
class BaseContext:
|
|
|
|
|
|
"""
|
|
|
|
|
|
定义一个基础 Context 供 各类 graph 继承
|
|
|
|
|
|
|
|
|
|
|
|
配置优先级:
|
|
|
|
|
|
1. 运行时配置(RunnableConfig):最高优先级,直接从函数参数传入
|
|
|
|
|
|
2. 文件配置(config.private.yaml):中等优先级,从文件加载
|
|
|
|
|
|
3. 类默认配置:最低优先级,类中定义的默认值
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def update(self, data: dict):
|
|
|
|
|
|
"""更新配置字段"""
|
|
|
|
|
|
for key, value in data.items():
|
|
|
|
|
|
if hasattr(self, key):
|
|
|
|
|
|
setattr(self, key, value)
|
|
|
|
|
|
|
|
|
|
|
|
thread_id: str = field(
|
|
|
|
|
|
default_factory=lambda: str(uuid.uuid4()),
|
2025-11-12 01:21:38 +08:00
|
|
|
|
metadata={"name": "线程ID", "configurable": False, "description": "用来唯一标识一个对话线程"},
|
2025-08-31 00:34:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
user_id: str = field(
|
|
|
|
|
|
default_factory=lambda: str(uuid.uuid4()),
|
2025-11-12 01:21:38 +08:00
|
|
|
|
metadata={"name": "用户ID", "configurable": False, "description": "用来唯一标识一个用户"},
|
2025-08-31 00:34:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 05:57:13 +08:00
|
|
|
|
department_id: int | None = field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
metadata={"name": "部门ID", "configurable": False, "description": "用来唯一标识一个部门"},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 11:16:15 +08:00
|
|
|
|
system_prompt: Annotated[str, {"__template_metadata__": {"kind": "prompt"}}] = field(
|
2025-08-31 00:34:26 +08:00
|
|
|
|
default="You are a helpful assistant.",
|
2025-09-01 22:37:03 +08:00
|
|
|
|
metadata={"name": "系统提示词", "description": "用来描述智能体的角色和行为"},
|
2025-08-31 00:34:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-25 20:01:23 +08:00
|
|
|
|
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
|
2025-11-12 01:21:38 +08:00
|
|
|
|
default=sys_config.default_model,
|
2025-12-30 19:01:45 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "智能体模型",
|
|
|
|
|
|
"options": [],
|
2025-12-30 19:19:49 +08:00
|
|
|
|
"description": "智能体的驱动模型,建议选择 Agent 能力较强的模型,不建议使用小参数模型。",
|
2025-12-30 19:01:45 +08:00
|
|
|
|
},
|
2025-10-25 20:01:23 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-15 16:04:19 +08:00
|
|
|
|
tools: Annotated[list[dict], {"__template_metadata__": {"kind": "tools"}}] = field(
|
|
|
|
|
|
default_factory=list,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "工具",
|
|
|
|
|
|
"description": "内置的工具。",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 11:16:15 +08:00
|
|
|
|
knowledges: Annotated[list[str], {"__template_metadata__": {"kind": "knowledges"}}] = field(
|
2026-01-15 16:04:19 +08:00
|
|
|
|
default_factory=list,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "知识库",
|
|
|
|
|
|
"description": "知识库列表,可以在左侧知识库页面中创建知识库。",
|
|
|
|
|
|
"type": "list", # Explicitly mark as list type for frontend if needed
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 11:16:15 +08:00
|
|
|
|
mcps: Annotated[list[str], {"__template_metadata__": {"kind": "mcps"}}] = field(
|
2026-01-15 16:04:19 +08:00
|
|
|
|
default_factory=list,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "MCP服务器",
|
|
|
|
|
|
"options": lambda: get_mcp_server_names(),
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"MCP服务器列表,建议使用支持 SSE 的 MCP 服务器,"
|
|
|
|
|
|
"如果需要使用 uvx 或 npx 运行的服务器,也请在项目外部启动 MCP 服务器,并在项目中配置 MCP 服务器。"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-21 02:48:20 +08:00
|
|
|
|
skills: Annotated[list[str], {"__template_metadata__": {"kind": "skills"}}] = field(
|
|
|
|
|
|
default_factory=list,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "Skills",
|
2026-02-26 18:31:55 +08:00
|
|
|
|
"options": [],
|
2026-03-06 10:15:58 +08:00
|
|
|
|
"description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。技能依赖的工具和 MCP 服务器也会被自动挂载。",
|
2026-02-21 02:48:20 +08:00
|
|
|
|
"type": "list",
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
@classmethod
|
2025-10-25 20:01:23 +08:00
|
|
|
|
def from_file(cls, module_name: str, input_context: dict = None) -> "BaseContext":
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"""Load configuration from a YAML file. 用于持久化配置"""
|
|
|
|
|
|
|
|
|
|
|
|
# 从文件加载配置
|
|
|
|
|
|
context = cls()
|
|
|
|
|
|
config_file_path = Path(sys_config.save_dir) / "agents" / module_name / "config.yaml"
|
|
|
|
|
|
if module_name is not None and os.path.exists(config_file_path):
|
|
|
|
|
|
file_config = {}
|
|
|
|
|
|
try:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
with open(config_file_path, encoding="utf-8") as f:
|
2025-08-31 00:34:26 +08:00
|
|
|
|
file_config = yaml.safe_load(f) or {}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"加载智能体配置文件出错: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
context.update(file_config)
|
|
|
|
|
|
|
|
|
|
|
|
if input_context:
|
|
|
|
|
|
context.update(input_context)
|
|
|
|
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def save_to_file(cls, config: dict, module_name: str) -> bool:
|
|
|
|
|
|
"""Save configuration to a YAML file 用于持久化配置"""
|
|
|
|
|
|
|
|
|
|
|
|
configurable_items = cls.get_configurable_items()
|
|
|
|
|
|
configurable_config = {}
|
|
|
|
|
|
for k, v in config.items():
|
|
|
|
|
|
if k in configurable_items:
|
|
|
|
|
|
configurable_config[k] = v
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
config_file_path = Path(sys_config.save_dir) / "agents" / module_name / "config.yaml"
|
|
|
|
|
|
# 确保目录存在
|
|
|
|
|
|
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
with open(config_file_path, "w", encoding="utf-8") as f:
|
2025-08-31 00:34:26 +08:00
|
|
|
|
yaml.dump(configurable_config, f, indent=2, allow_unicode=True)
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"保存智能体配置文件出错: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_configurable_items(cls):
|
|
|
|
|
|
"""实现一个可配置的参数列表,在 UI 上配置时使用"""
|
|
|
|
|
|
configurable_items = {}
|
|
|
|
|
|
for f in fields(cls):
|
|
|
|
|
|
if f.init and not f.metadata.get("hide", False):
|
|
|
|
|
|
if f.metadata.get("configurable", True):
|
|
|
|
|
|
# 处理类型信息
|
|
|
|
|
|
field_type = f.type
|
|
|
|
|
|
type_name = cls._get_type_name(field_type)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 Annotated 的元数据
|
|
|
|
|
|
template_metadata = cls._extract_template_metadata(field_type)
|
|
|
|
|
|
|
2025-12-24 18:52:14 +08:00
|
|
|
|
options = f.metadata.get("options", [])
|
|
|
|
|
|
if callable(options):
|
|
|
|
|
|
options = options()
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
configurable_items[f.name] = {
|
|
|
|
|
|
"type": type_name,
|
|
|
|
|
|
"name": f.metadata.get("name", f.name),
|
2025-12-24 18:52:14 +08:00
|
|
|
|
"options": options,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"default": f.default
|
|
|
|
|
|
if f.default is not MISSING
|
|
|
|
|
|
else f.default_factory()
|
|
|
|
|
|
if f.default_factory is not MISSING
|
|
|
|
|
|
else None,
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"description": f.metadata.get("description", ""),
|
|
|
|
|
|
"template_metadata": template_metadata, # Annotated 的额外元数据
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return configurable_items
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _get_type_name(cls, field_type) -> str:
|
|
|
|
|
|
"""获取类型名称,处理 Annotated 类型"""
|
|
|
|
|
|
# 检查是否是 Annotated 类型
|
|
|
|
|
|
if get_origin(field_type) is not None:
|
|
|
|
|
|
# 处理泛型类型如 list[str], Annotated[str, {...}]
|
|
|
|
|
|
origin = get_origin(field_type)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
if hasattr(origin, "__name__"):
|
|
|
|
|
|
if origin.__name__ == "Annotated":
|
2025-08-31 00:34:26 +08:00
|
|
|
|
# Annotated 类型,获取真实类型
|
|
|
|
|
|
args = get_args(field_type)
|
|
|
|
|
|
if args:
|
|
|
|
|
|
return cls._get_type_name(args[0]) # 递归处理真实类型
|
|
|
|
|
|
return origin.__name__
|
|
|
|
|
|
else:
|
|
|
|
|
|
return str(origin)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
elif hasattr(field_type, "__name__"):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
return field_type.__name__
|
|
|
|
|
|
else:
|
|
|
|
|
|
return str(field_type)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _extract_template_metadata(cls, field_type) -> dict:
|
|
|
|
|
|
"""从 Annotated 类型中提取模板元数据"""
|
|
|
|
|
|
if get_origin(field_type) is not None:
|
|
|
|
|
|
origin = get_origin(field_type)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
if hasattr(origin, "__name__") and origin.__name__ == "Annotated":
|
2025-08-31 00:34:26 +08:00
|
|
|
|
args = get_args(field_type)
|
|
|
|
|
|
if len(args) > 1:
|
|
|
|
|
|
# 查找包含 __template_metadata__ 的字典
|
|
|
|
|
|
for metadata in args[1:]:
|
|
|
|
|
|
if isinstance(metadata, dict) and "__template_metadata__" in metadata:
|
|
|
|
|
|
return metadata["__template_metadata__"]
|
|
|
|
|
|
return {}
|