diff --git a/README.md b/README.md index 6e6f6716..bd2087db 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ SILICONFLOW_API_KEY=sk-270ea********8bfa97.e3XOMd****Q1Sk OPENAI_API_KEY= # 如果需要配置 openai 则添加此行,并替换 API_KEY DEEPSEEK_API_KEY= # 如果配置 DeepSeek 添加此行,并替换 API_KEY ZHIPUAI_API_KEY= # 如果配置 智谱清言 添加此行,并替换 API_KEY +TAVILY_API_KEY= # 联网搜索需要配置 ``` 需要确保账户有一点点额度供调用,或使用这个链接注册[SiliconFlow 注册(含邀请码)](https://cloud.siliconflow.cn/i/Eo5yTHGJ)获得 14 元的赠送额度。 diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index 6430dcd1..5b373170 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -184,8 +184,6 @@ def chat_agent(agent_name: str, **kwargs }, ensure_ascii=False).encode('utf-8') + b"\n" - - def stream_messages(): # 代表服务端已经收到了请求 @@ -251,3 +249,47 @@ async def update_chat_models(model_provider: str, model_names: list[str], curren async def get_tools(current_user: User = Depends(get_admin_user)): """获取所有可用工具(需要登录)""" return {"tools": list(get_all_tools().keys())} + +@chat.post("/agent/{agent_name}/config") +async def save_agent_config( + agent_name: str, + config: dict = Body(...), + current_user: User = Depends(get_admin_user) +): + """保存智能体配置到YAML文件(需要管理员权限)""" + try: + # 获取Agent实例和配置类 + agent = agent_manager.get_agent(agent_name) + if not agent: + raise HTTPException(status_code=404, detail=f"智能体 {agent_name} 不存在") + + # 使用配置类的save_to_file方法保存配置 + config_cls = agent.config_schema + result = config_cls.save_to_file(config, agent_name) + + if result: + return {"success": True, "message": f"智能体 {agent_name} 配置已保存"} + else: + raise HTTPException(status_code=500, detail=f"保存智能体配置失败") + + except Exception as e: + logger.error(f"保存智能体配置出错: {e}, {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=f"保存智能体配置出错: {str(e)}") + +@chat.get("/agent/{agent_name}/config") +async def get_agent_config( + agent_name: str, + current_user: User = Depends(get_admin_user) +): + """从YAML文件加载智能体配置(需要管理员权限)""" + try: + # 检查智能体是否存在 + if not (agent := agent_manager.get_agent(agent_name)): + raise HTTPException(status_code=404, detail=f"智能体 {agent_name} 不存在") + + config = agent.config_schema.from_runnable_config(config={}, agent_name=agent_name) + return {"success": True, "config": config} + + except Exception as e: + logger.error(f"加载智能体配置出错: {e}, {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=f"加载智能体配置出错: {str(e)}") diff --git a/src/agents/chatbot/configuration.py b/src/agents/chatbot/configuration.py index 20eac6e0..a86874cd 100644 --- a/src/agents/chatbot/configuration.py +++ b/src/agents/chatbot/configuration.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, field from src.agents.registry import Configuration +from src.agents.tools_factory import get_all_tools @dataclass(kw_only=True) class ChatbotConfiguration(Configuration): @@ -39,7 +40,8 @@ class ChatbotConfiguration(Configuration): default_factory=list, metadata={ "name": "工具", - "configurable": False, + "configurable": True, + "options": list(get_all_tools().keys()), # 这里的选择是所有的工具 "description": "工具列表" }, ) diff --git a/src/agents/chatbot/graph.py b/src/agents/chatbot/graph.py index a80b6d38..c8fdaaae 100644 --- a/src/agents/chatbot/graph.py +++ b/src/agents/chatbot/graph.py @@ -42,8 +42,7 @@ class ChatbotAgent(BaseAgent): def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]: """调用 llm 模型""" - config_schema = config or {} - conf = self.config_schema.from_runnable_config(config_schema) + conf = self.config_schema.from_runnable_config(config, agent_name=self.name) system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}" model = load_chat_model(conf.model) @@ -57,7 +56,7 @@ class ChatbotAgent(BaseAgent): def get_graph(self, config_schema: RunnableConfig = None, **kwargs): """构建图""" - conf = self.config_schema.from_runnable_config(config_schema) + conf = self.config_schema.from_runnable_config(config_schema, agent_name=self.name) workflow = StateGraph(State, config_schema=self.config_schema) workflow.add_node("chatbot", self.llm_call) workflow.add_node("tools", ToolNode(tools=self._get_tools(conf.tools))) diff --git a/src/agents/registry.py b/src/agents/registry.py index 9ed7eca5..f3da6dc2 100644 --- a/src/agents/registry.py +++ b/src/agents/registry.py @@ -1,7 +1,8 @@ from __future__ import annotations import os - +import yaml +from pathlib import Path from typing import Type, Annotated, Optional, TypedDict from enum import Enum from abc import abstractmethod @@ -30,16 +31,95 @@ class State(TypedDict): class Configuration(dict): """ 定义一个基础 Configuration 供 各类 graph 继承 + + 配置优先级: + 1. 运行时配置(RunnableConfig):最高优先级,直接从函数参数传入 + 2. 文件配置(config.private.yaml):中等优先级,从文件加载 + 3. 类默认配置:最低优先级,类中定义的默认值 """ @classmethod def from_runnable_config( - cls, config: Optional[RunnableConfig] = None + cls, config: Optional[RunnableConfig] = None, agent_name: str = None ) -> Configuration: - """Create a Configuration instance from a RunnableConfig object.""" - configurable = (config.get("configurable") or {}) if config else {} + """Create a Configuration instance from a RunnableConfig object. + + Args: + config: RunnableConfig object with highest priority + agent_name: Name of the agent to load config file for + + Returns: + Configuration instance with merged config values + """ + # 获取类默认配置:创建一个实例获取所有默认值 + instance = cls() _fields = {f.name for f in fields(cls) if f.init} - return cls(**{k: v for k, v in configurable.items() if k in _fields}) + + # 尝试加载文件配置(中等优先级) + file_config = {} + if agent_name: + file_config = cls.from_file(agent_name) + + # 获取运行时配置(最高优先级) + configurable = (config.get("configurable") or {}) if config else {} + + # 合并三级配置,注意优先级 + merged_config = {} + for field in _fields: + # 1. 默认使用类默认值 + if hasattr(instance, field): + merged_config[field] = getattr(instance, field) + + # 2. 如果文件配置中有此字段,则覆盖 + if field in file_config: + merged_config[field] = file_config[field] + + # 3. 如果运行时配置中有此字段,则覆盖 + if field in configurable: + merged_config[field] = configurable[field] + + # 创建并返回配置实例 + # logger.debug(f"合并配置: {merged_config}") + return cls(**merged_config) + + @classmethod + def from_file(cls, agent_name: str) -> Configuration: + """从文件加载配置""" + config_file_path = Path(f"src/agents/{agent_name}/config.private.yaml") + file_config = {} + if os.path.exists(config_file_path): + try: + with open(config_file_path, 'r', encoding='utf-8') as f: + file_config = yaml.safe_load(f) or {} + # logger.info(f"从文件加载智能体 {agent_name} 配置: {file_config}") + except Exception as e: + logger.error(f"加载智能体配置文件出错: {e}") + + return file_config + + @classmethod + def save_to_file(cls, config: dict, agent_name: str) -> bool: + """Save configuration to a YAML file + + Args: + config: Configuration dictionary to save + agent_name: Name of the agent to save config for + + Returns: + True if saving was successful, False otherwise + """ + try: + config_file_path = Path(f"src/agents/{agent_name}/config.private.yaml") + # 确保目录存在 + os.makedirs(os.path.dirname(config_file_path), exist_ok=True) + with open(config_file_path, 'w', encoding='utf-8') as f: + yaml.dump(config, f, indent=2, allow_unicode=True) + + # logger.info(f"智能体 {agent_name} 配置已保存到 {config_file_path}") + return True + except Exception as e: + logger.error(f"保存智能体配置文件出错: {e}") + return False @classmethod def to_dict(cls): diff --git a/src/agents/tools_factory.py b/src/agents/tools_factory.py index 5f67b604..75e6de7d 100644 --- a/src/agents/tools_factory.py +++ b/src/agents/tools_factory.py @@ -120,7 +120,7 @@ class BaseToolOutput: @tool def calculator(a: float, b: float, operation: str) -> float: - """Calculate two numbers.""" + """Calculate two numbers. operation: add, subtract, multiply, divide""" if operation == "add": return a + b elif operation == "subtract": diff --git a/web/src/apis/admin_api.js b/web/src/apis/admin_api.js index 80b85dc8..e88df049 100644 --- a/web/src/apis/admin_api.js +++ b/web/src/apis/admin_api.js @@ -289,6 +289,26 @@ export const systemConfigApi = { return apiGet('/api/config', {}, true) }, + /** + * 获取智能体配置 + * @param {string} agentId - 智能体ID + * @returns {Promise} - 智能体配置 + */ + getAgentConfig: async (agentId) => { + checkAdminPermission() + return apiGet(`/api/chat/agent/${agentId}/config`, {}, true) + }, + + /** + * 保存智能体配置 + * @param {string} agentId - 智能体ID + * @param {Object} config - 配置内容 + * @returns {Promise} - 保存结果 + */ + saveAgentConfig: async (agentId, config) => { + checkAdminPermission() + return apiPost(`/api/chat/agent/${agentId}/config`, config, {}, true) + }, /** * 更新某个配置 @@ -322,4 +342,27 @@ export const logApi = { checkAdminPermission() return apiGet('/api/log', { params }, true) }, -} \ No newline at end of file +} + +// 通用admin +export const adminApi = { + /** + * 获取所有智能体 + * @param {Object} params - 查询参数 + * @returns {Promise} - 查询结果 + */ + adminGet: async (params, url) => { + checkAdminPermission() + return apiGet(url, { params }, true) + }, + + /** + * 更新某个配置 + * @param {Object} items - 配置项 + * @returns {Promise} - 更新结果 + */ + adminPost: async (data, url) => { + checkAdminPermission() + return apiPost(url, data, {}, true) + }, +} diff --git a/web/src/views/AgentView.vue b/web/src/views/AgentView.vue index 946ccd26..bce45f87 100644 --- a/web/src/views/AgentView.vue +++ b/web/src/views/AgentView.vue @@ -156,11 +156,18 @@ :placeholder="getPlaceholder(key, value)" /> + + + - - -

选择要启用的工具(注:retrieve 工具仅展现了与当前向量模型匹配的知识库,详情请查看 docker 日志。)

- -
-
- {{ tool }} - -
-
-
-
-
- 保存配置 - 重置 - 取消 +
+ 保存并发布配置 + 重置 +
+
+ 关闭 +
@@ -203,12 +197,8 @@ import { ref, onMounted, reactive, watch, computed, h } from 'vue'; import { useRouter } from 'vue-router'; import { - RobotOutlined, - MenuFoldOutlined, - MenuUnfoldOutlined, CloseOutlined, SettingOutlined, - KeyOutlined, LinkOutlined, StarOutlined, StarFilled @@ -226,7 +216,6 @@ const userStore = useUserStore(); // 状态 const agents = ref({}); const selectedAgentId = ref(null); -const availableTools = ref([]); // 存储所有可用的工具列表 const defaultAgentId = ref(null); // 存储默认智能体ID const state = reactive({ debug_mode: false, @@ -294,19 +283,8 @@ const fetchAgents = async () => { } }; -// 获取所有可用工具 -const fetchTools = async () => { - try { - const data = await chatApi.getTools(); - availableTools.value = data.tools; - console.log("Available tools:", availableTools.value); - } catch (error) { - console.error('获取工具列表错误:', error); - } -}; - // 根据选中的智能体加载配置 -const loadAgentConfig = () => { +const loadAgentConfig = async () => { // BUG: 目前消息重置有问题,需要重置消息 if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return; @@ -326,7 +304,7 @@ const loadAgentConfig = () => { agentConfig.value.model = schema.model; } - if (schema.tools) { + if (schema.tools && schema.tools.length > 0 && schema.tools[0] != 'undefined') { agentConfig.value.tools = schema.tools; } @@ -342,37 +320,82 @@ const loadAgentConfig = () => { } }); - // 加载存储的配置 - const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}'); + try { + // 从服务器加载配置 + const response = await systemConfigApi.getAgentConfig(selectedAgentId.value); + if (response.success && response.config) { + // 合并服务器配置 + Object.keys(response.config).forEach(key => { + if (key in agentConfig.value) { + agentConfig.value[key] = response.config[key]; + } + }); + console.log(`从服务器加载 ${selectedAgentId.value} 配置成功, ${JSON.stringify(agentConfig.value)}`); + } + } catch (error) { + console.error('从服务器加载配置出错:', error); + message.error('从服务器加载配置失败,将使用默认配置'); - // 合并已保存的配置 - if (savedConfig) { - Object.keys(savedConfig).forEach(key => { - if (key in agentConfig.value) { - agentConfig.value[key] = savedConfig[key]; - } - }); + // 如果服务器配置加载失败,尝试从本地存储加载 + // 这是为了兼容之前的配置方式 + const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}'); + + // 合并已保存的配置 + if (savedConfig) { + Object.keys(savedConfig).forEach(key => { + if (key in agentConfig.value) { + agentConfig.value[key] = savedConfig[key]; + } + }); + } } }; // 保存配置 -const saveConfig = () => { - // 保存配置到本地存储 - localStorage.setItem(`agent-config-${selectedAgentId.value}`, JSON.stringify(agentConfig.value)); +const saveConfig = async () => { + if (!selectedAgentId.value) { + message.error('没有选择智能体'); + return; + } - // 提示保存成功 - message.success('配置已保存'); - console.log("agentConfig.value", agentConfig.value); - closeConfigModal(); + try { + // 保存配置到服务器 + await systemConfigApi.saveAgentConfig(selectedAgentId.value, agentConfig.value); + + // 同时保存到本地存储(可选,为了兼容) + localStorage.setItem(`agent-config-${selectedAgentId.value}`, JSON.stringify(agentConfig.value)); + + // 提示保存成功 + message.success('配置已保存到服务器'); + console.log("保存配置:", agentConfig.value); + closeConfigModal(); + } catch (error) { + console.error('保存配置到服务器出错:', error); + message.error('保存配置到服务器失败'); + } }; // 重置配置 -const resetConfig = () => { - // 清除本地存储中的配置 - localStorage.removeItem(`agent-config-${selectedAgentId.value}`); - // 重新加载默认配置 - loadAgentConfig(); - message.info('配置已重置'); +const resetConfig = async () => { + if (!selectedAgentId.value) { + message.error('没有选择智能体'); + return; + } + + try { + // 保存空配置到服务器,相当于重置 + await systemConfigApi.saveAgentConfig(selectedAgentId.value, {}); + + // 清除本地存储中的配置 + localStorage.removeItem(`agent-config-${selectedAgentId.value}`); + + // 重新加载默认配置 + await loadAgentConfig(); + message.info('配置已重置'); + } catch (error) { + console.error('重置配置出错:', error); + message.error('重置配置失败'); + } }; // 监听侧边栏状态变化并保存到localStorage @@ -420,8 +443,6 @@ onMounted(async () => { await fetchDefaultAgent(); // 获取智能体列表 await fetchAgents(); - // 获取工具列表 - await fetchTools(); // 恢复上次选择的智能体 const lastSelectedAgent = localStorage.getItem('last-selected-agent'); @@ -461,31 +482,6 @@ const goToAgentPage = () => { } }; -// 检查工具是否激活 -const isToolActive = (tool) => { - if (!agentConfig.value.tools) { - agentConfig.value.tools = []; - } - return agentConfig.value.tools.includes(tool); -}; - -// 切换工具状态 -const toggleTool = (tool, checked) => { - if (!agentConfig.value.tools) { - agentConfig.value.tools = []; - } - - if (checked) { - // 添加工具到列表 - if (!agentConfig.value.tools.includes(tool)) { - agentConfig.value.tools.push(tool); - } - } else { - // 从列表中移除工具 - agentConfig.value.tools = agentConfig.value.tools.filter(item => item !== tool); - } -}; - // 调试模式 const toggleDebugMode = () => { state.debug_mode = !state.debug_mode; @@ -740,6 +736,12 @@ const closeConfigModal = () => { justify-content: space-between; margin-top: 20px; gap: 10px; + + .form-actions-left, + .form-actions-right { + display: flex; + gap: 10px; + } } } @@ -789,22 +791,6 @@ const closeConfigModal = () => { font-size: 14px; } } - -.tools-switches { - display: flex; - flex-direction: column; - gap: 12px; - - .tool-switch-item { - display: flex; - align-items: center; - justify-content: space-between; - - .tool-name { - margin-left: 10px; - } - } -}