2025-08-31 00:34:26 +08:00
|
|
|
|
"""Define the configurable parameters for the agent."""
|
|
|
|
|
|
|
|
|
|
|
|
import uuid
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from dataclasses import MISSING, dataclass, field, fields
|
2026-05-19 18:37:29 +08:00
|
|
|
|
from typing import Any, get_origin
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi import config as sys_config
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 09:40:19 +08:00
|
|
|
|
def _role_can_access(auth: str | None, role: str | None) -> bool:
|
|
|
|
|
|
if not auth:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if auth == "admin":
|
|
|
|
|
|
return role in {"admin", "superadmin"}
|
|
|
|
|
|
if auth == "superadmin":
|
|
|
|
|
|
return role == "superadmin"
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_config_by_role(
|
|
|
|
|
|
config_json: dict,
|
|
|
|
|
|
role: str | None,
|
|
|
|
|
|
context_schema: type["BaseContext"] | None = None,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""按 Context 字段 metadata.auth 过滤 config_json.context。"""
|
|
|
|
|
|
if not isinstance(config_json, dict):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
schema = context_schema or BaseContext
|
|
|
|
|
|
restricted_fields = {
|
|
|
|
|
|
f.name
|
|
|
|
|
|
for f in fields(schema)
|
|
|
|
|
|
if f.metadata.get("auth") and not _role_can_access(str(f.metadata.get("auth")), role)
|
|
|
|
|
|
}
|
|
|
|
|
|
if not restricted_fields:
|
|
|
|
|
|
return dict(config_json)
|
|
|
|
|
|
|
|
|
|
|
|
filtered = dict(config_json)
|
|
|
|
|
|
context = filtered.get("context")
|
|
|
|
|
|
if isinstance(context, dict):
|
|
|
|
|
|
filtered["context"] = {key: value for key, value in context.items() if key not in restricted_fields}
|
|
|
|
|
|
return filtered
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
@dataclass(kw_only=True)
|
|
|
|
|
|
class BaseContext:
|
|
|
|
|
|
"""
|
|
|
|
|
|
定义一个基础 Context 供 各类 graph 继承
|
|
|
|
|
|
|
|
|
|
|
|
配置优先级:
|
|
|
|
|
|
1. 运行时配置(RunnableConfig):最高优先级,直接从函数参数传入
|
2026-03-18 22:55:52 +08:00
|
|
|
|
2. 类默认配置:最低优先级,类中定义的默认值
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 22:44:05 +08:00
|
|
|
|
uid: str = field(
|
2025-08-31 00:34:26 +08:00
|
|
|
|
default_factory=lambda: str(uuid.uuid4()),
|
2026-05-17 22:44:05 +08:00
|
|
|
|
metadata={"name": "UID", "configurable": False, "description": "用来唯一标识一个用户"},
|
2025-08-31 00:34:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 13:06:25 +08:00
|
|
|
|
system_prompt: str = field(
|
2025-08-31 00:34:26 +08:00
|
|
|
|
default="You are a helpful assistant.",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
metadata={"name": "系统提示词", "description": "用来描述智能体的角色和行为", "kind": "prompt"},
|
2025-08-31 00:34:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 13:06:25 +08:00
|
|
|
|
model: str = 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 能力较强的模型,不建议使用小参数模型。",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "llm",
|
2025-12-30 19:01:45 +08:00
|
|
|
|
},
|
2025-10-25 20:01:23 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 18:37:29 +08:00
|
|
|
|
tools: list[str] | None = field(
|
|
|
|
|
|
default=None,
|
2026-01-15 16:04:19 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "工具",
|
2026-05-19 18:37:29 +08:00
|
|
|
|
"description": "内置的工具。默认选择当前用户可用的全部工具。",
|
|
|
|
|
|
"type": "list",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "tools",
|
2026-01-15 16:04:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 13:06:25 +08:00
|
|
|
|
knowledges: list[str] | None = field(
|
2026-05-12 13:40:44 +08:00
|
|
|
|
default=None,
|
2026-01-15 16:04:19 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "知识库",
|
2026-05-12 13:40:44 +08:00
|
|
|
|
"description": "知识库列表,可以在左侧知识库页面中创建知识库。默认选择当前用户可访问的全部知识库。",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"type": "list",
|
|
|
|
|
|
"kind": "knowledges",
|
2026-01-15 16:04:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 18:37:29 +08:00
|
|
|
|
mcps: list[str] | None = field(
|
|
|
|
|
|
default=None,
|
2026-01-15 16:04:19 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "MCP服务器",
|
2026-04-04 14:47:23 +08:00
|
|
|
|
"options": [],
|
2026-01-15 16:04:19 +08:00
|
|
|
|
"description": (
|
2026-05-19 18:37:29 +08:00
|
|
|
|
"MCP服务器列表,默认选择当前用户可用的全部 MCP 服务器。建议使用支持 SSE 的 MCP 服务器,"
|
2026-01-15 16:04:19 +08:00
|
|
|
|
"如果需要使用 uvx 或 npx 运行的服务器,也请在项目外部启动 MCP 服务器,并在项目中配置 MCP 服务器。"
|
|
|
|
|
|
),
|
2026-05-19 18:37:29 +08:00
|
|
|
|
"type": "list",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "mcps",
|
2026-01-15 16:04:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 18:37:29 +08:00
|
|
|
|
skills: list[str] | None = field(
|
|
|
|
|
|
default=None,
|
2026-02-21 02:48:20 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "Skills",
|
2026-02-26 18:31:55 +08:00
|
|
|
|
"options": [],
|
2026-05-19 18:37:29 +08:00
|
|
|
|
"description": "可选技能列表(由超级管理员维护),默认选择当前用户可用的全部 skills。"
|
|
|
|
|
|
"技能依赖的工具和 MCP 服务器也会被自动挂载。",
|
2026-02-21 02:48:20 +08:00
|
|
|
|
"type": "list",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "skills",
|
2026-02-21 02:48:20 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-17 13:06:25 +08:00
|
|
|
|
subagents_model: str = field(
|
2026-03-20 01:42:38 +08:00
|
|
|
|
default=sys_config.default_model,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "子智能体的默认模型",
|
2026-03-24 22:37:03 +08:00
|
|
|
|
"description": "为所有子智能体设置默认模型,可在各子智能体配置中单独覆盖。",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "llm",
|
2026-03-20 01:42:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-19 18:37:29 +08:00
|
|
|
|
subagents: list[str] | None = field(
|
|
|
|
|
|
default=None,
|
2026-03-20 01:42:38 +08:00
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "子智能体",
|
|
|
|
|
|
"options": [],
|
2026-05-19 18:37:29 +08:00
|
|
|
|
"description": (
|
|
|
|
|
|
"可选子智能体列表,默认选择当前用户可用的全部 SubAgent。"
|
|
|
|
|
|
"为空表示不启用任何 SubAgent,但依然会启用一个 general-purpose 的子智能体。"
|
|
|
|
|
|
),
|
2026-03-20 01:42:38 +08:00
|
|
|
|
"type": "list",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": "subagents",
|
2026-03-20 01:42:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
summary_threshold: int = field(
|
|
|
|
|
|
default=100,
|
|
|
|
|
|
metadata={
|
|
|
|
|
|
"name": "上下文摘要触发阈值 (KB)",
|
|
|
|
|
|
"description": "当上下文大小超过该值时,启用摘要功能以优化上下文使用。单位为 KB,默认值为 100KB。",
|
|
|
|
|
|
"type": "number",
|
2026-05-17 22:44:05 +08:00
|
|
|
|
"auth": "admin",
|
2026-03-20 01:42:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-31 00:34:26 +08:00
|
|
|
|
@classmethod
|
2026-05-18 09:40:19 +08:00
|
|
|
|
def get_configurable_items(cls, user_role: str | None = None):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"""实现一个可配置的参数列表,在 UI 上配置时使用"""
|
|
|
|
|
|
configurable_items = {}
|
|
|
|
|
|
for f in fields(cls):
|
|
|
|
|
|
if f.init and not f.metadata.get("hide", False):
|
2026-05-18 09:40:19 +08:00
|
|
|
|
if user_role is not None and not _role_can_access(f.metadata.get("auth"), user_role):
|
|
|
|
|
|
continue
|
2025-08-31 00:34:26 +08:00
|
|
|
|
if f.metadata.get("configurable", True):
|
2026-05-17 13:06:25 +08:00
|
|
|
|
type_name = cls._get_type_name(f.type)
|
2025-08-31 00:34:26 +08:00
|
|
|
|
|
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] = {
|
2026-03-20 02:12:54 +08:00
|
|
|
|
"type": f.metadata.get("type", type_name),
|
2025-08-31 00:34:26 +08:00
|
|
|
|
"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", ""),
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"kind": f.metadata.get("kind", ""),
|
2025-08-31 00:34:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return configurable_items
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _get_type_name(cls, field_type) -> str:
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"""获取类型名称"""
|
|
|
|
|
|
origin = get_origin(field_type)
|
|
|
|
|
|
if origin is not None:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
if hasattr(origin, "__name__"):
|
2025-08-31 00:34:26 +08:00
|
|
|
|
return origin.__name__
|
2026-05-17 13:06:25 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-03-18 22:55:52 +08:00
|
|
|
|
def update_from_dict(self, data: dict):
|
|
|
|
|
|
"""从字典更新配置字段"""
|
|
|
|
|
|
for key, value in data.items():
|
|
|
|
|
|
if hasattr(self, key):
|
|
|
|
|
|
setattr(self, key, value)
|
2026-05-19 18:37:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_DEFAULT_ALL_CONTEXT_FIELDS = frozenset({"tools", "knowledges", "mcps", "skills", "subagents"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_selected_resource_names(value: Any, available: list[str]) -> list[str]:
|
|
|
|
|
|
if not isinstance(value, list):
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
allowed = set(available)
|
|
|
|
|
|
normalized: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for item in value:
|
|
|
|
|
|
if not isinstance(item, str):
|
|
|
|
|
|
continue
|
|
|
|
|
|
name = item.strip()
|
|
|
|
|
|
if not name or name in seen or name not in allowed:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(name)
|
|
|
|
|
|
normalized.append(name)
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resource_fields_requiring_available_names(normalized: dict, resource_fields: set[str]) -> set[str]:
|
|
|
|
|
|
fields_to_load: set[str] = set()
|
|
|
|
|
|
for field_name in resource_fields:
|
|
|
|
|
|
current = normalized.get(field_name)
|
|
|
|
|
|
if current is None:
|
|
|
|
|
|
fields_to_load.add(field_name)
|
|
|
|
|
|
elif isinstance(current, list) and current:
|
|
|
|
|
|
fields_to_load.add(field_name)
|
|
|
|
|
|
else:
|
|
|
|
|
|
normalized[field_name] = []
|
|
|
|
|
|
return fields_to_load
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def normalize_agent_context_config(
|
|
|
|
|
|
context: dict | None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
db,
|
|
|
|
|
|
user,
|
|
|
|
|
|
context_schema: type[BaseContext] | None = None,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
schema = context_schema or BaseContext
|
|
|
|
|
|
raw_context = dict(context) if isinstance(context, dict) else {}
|
|
|
|
|
|
filtered = filter_config_by_role({"context": raw_context}, getattr(user, "role", None), schema)
|
|
|
|
|
|
normalized = dict(filtered.get("context") or {})
|
|
|
|
|
|
field_names = {item.name for item in fields(schema)}
|
|
|
|
|
|
resource_fields = _DEFAULT_ALL_CONTEXT_FIELDS & field_names
|
|
|
|
|
|
if not resource_fields:
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
fields_to_load = _resource_fields_requiring_available_names(normalized, resource_fields)
|
|
|
|
|
|
if not fields_to_load:
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
available: dict[str, list[str]] = {}
|
|
|
|
|
|
if "tools" in fields_to_load:
|
|
|
|
|
|
from yuxi.agents.toolkits import get_all_tool_instances
|
|
|
|
|
|
|
|
|
|
|
|
available["tools"] = [
|
|
|
|
|
|
tool.name for tool in get_all_tool_instances() if isinstance(getattr(tool, "name", None), str)
|
|
|
|
|
|
]
|
|
|
|
|
|
if "knowledges" in fields_to_load:
|
|
|
|
|
|
from yuxi.knowledge import knowledge_base
|
|
|
|
|
|
|
|
|
|
|
|
databases = (await knowledge_base.get_databases_by_user(user)).get("databases", [])
|
|
|
|
|
|
available["knowledges"] = [
|
|
|
|
|
|
str(db_item.get("db_id") or db_item.get("id"))
|
|
|
|
|
|
for db_item in databases
|
|
|
|
|
|
if isinstance(db_item, dict) and (db_item.get("db_id") or db_item.get("id"))
|
|
|
|
|
|
]
|
|
|
|
|
|
if "mcps" in fields_to_load:
|
|
|
|
|
|
from yuxi.services.mcp_service import get_enabled_mcp_server_names
|
|
|
|
|
|
|
|
|
|
|
|
available["mcps"] = await get_enabled_mcp_server_names(db=db)
|
|
|
|
|
|
if "skills" in fields_to_load:
|
|
|
|
|
|
from yuxi.services.skill_service import list_skill_slugs
|
|
|
|
|
|
|
|
|
|
|
|
available["skills"] = await list_skill_slugs(db)
|
|
|
|
|
|
if "subagents" in fields_to_load:
|
|
|
|
|
|
from yuxi.services.subagent_service import get_enabled_subagent_names
|
|
|
|
|
|
|
|
|
|
|
|
available["subagents"] = await get_enabled_subagent_names(db)
|
|
|
|
|
|
|
|
|
|
|
|
for field_name, available_names in available.items():
|
|
|
|
|
|
current = normalized.get(field_name)
|
|
|
|
|
|
if current is None:
|
|
|
|
|
|
normalized[field_name] = available_names
|
|
|
|
|
|
else:
|
|
|
|
|
|
normalized[field_name] = _normalize_selected_resource_names(current, available_names)
|
|
|
|
|
|
|
|
|
|
|
|
return normalized
|