feat: Agent配置可以同步在用户侧了,用户侧会使用 config file 中的配置

This commit is contained in:
Wenjie Zhang 2025-05-13 19:49:54 +08:00
parent d0ded4d30f
commit b01abfcf71
8 changed files with 270 additions and 117 deletions

View File

@ -49,6 +49,7 @@ SILICONFLOW_API_KEY=sk-270ea********8bfa97.e3XOMd****Q1Sk
OPENAI_API_KEY=<API_KEY> # 如果需要配置 openai 则添加此行,并替换 API_KEY OPENAI_API_KEY=<API_KEY> # 如果需要配置 openai 则添加此行,并替换 API_KEY
DEEPSEEK_API_KEY=<API_KEY> # 如果配置 DeepSeek 添加此行,并替换 API_KEY DEEPSEEK_API_KEY=<API_KEY> # 如果配置 DeepSeek 添加此行,并替换 API_KEY
ZHIPUAI_API_KEY=<API_KEY> # 如果配置 智谱清言 添加此行,并替换 API_KEY ZHIPUAI_API_KEY=<API_KEY> # 如果配置 智谱清言 添加此行,并替换 API_KEY
TAVILY_API_KEY=<TAVILY_API_KEY> # 联网搜索需要配置
``` ```
需要确保账户有一点点额度供调用,或使用这个链接注册[SiliconFlow 注册(含邀请码)](https://cloud.siliconflow.cn/i/Eo5yTHGJ)获得 14 元的赠送额度。 需要确保账户有一点点额度供调用,或使用这个链接注册[SiliconFlow 注册(含邀请码)](https://cloud.siliconflow.cn/i/Eo5yTHGJ)获得 14 元的赠送额度。

View File

@ -184,8 +184,6 @@ def chat_agent(agent_name: str,
**kwargs **kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n" }, ensure_ascii=False).encode('utf-8') + b"\n"
def stream_messages(): 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)): async def get_tools(current_user: User = Depends(get_admin_user)):
"""获取所有可用工具(需要登录)""" """获取所有可用工具(需要登录)"""
return {"tools": list(get_all_tools().keys())} 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)}")

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from src.agents.registry import Configuration from src.agents.registry import Configuration
from src.agents.tools_factory import get_all_tools
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ChatbotConfiguration(Configuration): class ChatbotConfiguration(Configuration):
@ -39,7 +40,8 @@ class ChatbotConfiguration(Configuration):
default_factory=list, default_factory=list,
metadata={ metadata={
"name": "工具", "name": "工具",
"configurable": False, "configurable": True,
"options": list(get_all_tools().keys()), # 这里的选择是所有的工具
"description": "工具列表" "description": "工具列表"
}, },
) )

View File

@ -42,8 +42,7 @@ class ChatbotAgent(BaseAgent):
def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]: def llm_call(self, state: State, config: RunnableConfig = None) -> dict[str, Any]:
"""调用 llm 模型""" """调用 llm 模型"""
config_schema = config or {} conf = self.config_schema.from_runnable_config(config, agent_name=self.name)
conf = self.config_schema.from_runnable_config(config_schema)
system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}" system_prompt = f"{conf.system_prompt} Now is {get_cur_time_with_utc()}"
model = load_chat_model(conf.model) model = load_chat_model(conf.model)
@ -57,7 +56,7 @@ class ChatbotAgent(BaseAgent):
def get_graph(self, config_schema: RunnableConfig = None, **kwargs): 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 = StateGraph(State, config_schema=self.config_schema)
workflow.add_node("chatbot", self.llm_call) workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=self._get_tools(conf.tools))) workflow.add_node("tools", ToolNode(tools=self._get_tools(conf.tools)))

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
import yaml
from pathlib import Path
from typing import Type, Annotated, Optional, TypedDict from typing import Type, Annotated, Optional, TypedDict
from enum import Enum from enum import Enum
from abc import abstractmethod from abc import abstractmethod
@ -30,16 +31,95 @@ class State(TypedDict):
class Configuration(dict): class Configuration(dict):
""" """
定义一个基础 Configuration 各类 graph 继承 定义一个基础 Configuration 各类 graph 继承
配置优先级:
1. 运行时配置(RunnableConfig)最高优先级直接从函数参数传入
2. 文件配置(config.private.yaml)中等优先级从文件加载
3. 类默认配置最低优先级类中定义的默认值
""" """
@classmethod @classmethod
def from_runnable_config( def from_runnable_config(
cls, config: Optional[RunnableConfig] = None cls, config: Optional[RunnableConfig] = None, agent_name: str = None
) -> Configuration: ) -> Configuration:
"""Create a Configuration instance from a RunnableConfig object.""" """Create a Configuration instance from a RunnableConfig object.
configurable = (config.get("configurable") or {}) if config else {}
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} _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 @classmethod
def to_dict(cls): def to_dict(cls):

View File

@ -120,7 +120,7 @@ class BaseToolOutput:
@tool @tool
def calculator(a: float, b: float, operation: str) -> float: def calculator(a: float, b: float, operation: str) -> float:
"""Calculate two numbers.""" """Calculate two numbers. operation: add, subtract, multiply, divide"""
if operation == "add": if operation == "add":
return a + b return a + b
elif operation == "subtract": elif operation == "subtract":

View File

@ -289,6 +289,26 @@ export const systemConfigApi = {
return apiGet('/api/config', {}, true) 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)
},
/** /**
* 更新某个配置 * 更新某个配置
@ -323,3 +343,26 @@ export const logApi = {
return apiGet('/api/log', { params }, true) return apiGet('/api/log', { params }, true)
}, },
} }
// 通用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)
},
}

View File

@ -156,11 +156,18 @@
:placeholder="getPlaceholder(key, value)" :placeholder="getPlaceholder(key, value)"
/> />
<a-select <a-select
v-else-if="value?.options" v-else-if="value?.options && value?.type === 'str'"
v-model:value="agentConfig[key]" v-model:value="agentConfig[key]"
> >
<a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option> <a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option>
</a-select> </a-select>
<a-select
v-else-if="value?.options && value?.type === 'list'"
v-model:value="agentConfig[key]"
mode="multiple"
>
<a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option>
</a-select>
<a-input <a-input
v-else v-else
v-model:value="agentConfig[key]" v-model:value="agentConfig[key]"
@ -169,28 +176,15 @@
</a-form-item> </a-form-item>
</template> </template>
<!-- 添加工具选择部分 -->
<a-form-item label="可用工具" name="tools" class="config-item">
<p class="description">选择要启用的工具retrieve 工具仅展现了与当前向量模型匹配的知识库详情请查看 docker 日志</p>
<a-form-item-rest>
<div class="tools-switches">
<div v-for="tool in availableTools" :key="tool" class="tool-switch-item">
<span class="tool-name">{{ tool }}</span>
<a-switch
size="small"
:checked="isToolActive(tool)"
@change="(checked) => toggleTool(tool, checked)"
/>
</div>
</div>
</a-form-item-rest>
</a-form-item>
<!-- 弹窗底部按钮 --> <!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig"> <div class="form-actions" v-if="!state.isEmptyConfig">
<a-button type="primary" @click="saveConfig">保存配置</a-button> <div class="form-actions-left">
<a-button @click="resetConfig">重置</a-button> <a-button type="primary" @click="saveConfig">保存并发布配置</a-button>
<a-button @click="closeConfigModal">取消</a-button> <a-button @click="resetConfig">重置</a-button>
</div>
<div class="form-actions-right">
<a-button @click="closeConfigModal">关闭</a-button>
</div>
</div> </div>
</a-form> </a-form>
</div> </div>
@ -203,12 +197,8 @@
import { ref, onMounted, reactive, watch, computed, h } from 'vue'; import { ref, onMounted, reactive, watch, computed, h } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { import {
RobotOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
CloseOutlined, CloseOutlined,
SettingOutlined, SettingOutlined,
KeyOutlined,
LinkOutlined, LinkOutlined,
StarOutlined, StarOutlined,
StarFilled StarFilled
@ -226,7 +216,6 @@ const userStore = useUserStore();
// //
const agents = ref({}); const agents = ref({});
const selectedAgentId = ref(null); const selectedAgentId = ref(null);
const availableTools = ref([]); //
const defaultAgentId = ref(null); // ID const defaultAgentId = ref(null); // ID
const state = reactive({ const state = reactive({
debug_mode: false, 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: // BUG:
if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return; if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return;
@ -326,7 +304,7 @@ const loadAgentConfig = () => {
agentConfig.value.model = schema.model; agentConfig.value.model = schema.model;
} }
if (schema.tools) { if (schema.tools && schema.tools.length > 0 && schema.tools[0] != 'undefined') {
agentConfig.value.tools = schema.tools; agentConfig.value.tools = schema.tools;
} }
@ -342,37 +320,82 @@ const loadAgentConfig = () => {
} }
}); });
// try {
const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}'); //
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 => { const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}');
if (key in agentConfig.value) {
agentConfig.value[key] = savedConfig[key]; //
} if (savedConfig) {
}); Object.keys(savedConfig).forEach(key => {
if (key in agentConfig.value) {
agentConfig.value[key] = savedConfig[key];
}
});
}
} }
}; };
// //
const saveConfig = () => { const saveConfig = async () => {
// if (!selectedAgentId.value) {
localStorage.setItem(`agent-config-${selectedAgentId.value}`, JSON.stringify(agentConfig.value)); message.error('没有选择智能体');
return;
}
// try {
message.success('配置已保存'); //
console.log("agentConfig.value", agentConfig.value); await systemConfigApi.saveAgentConfig(selectedAgentId.value, agentConfig.value);
closeConfigModal();
//
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 = () => { const resetConfig = async () => {
// if (!selectedAgentId.value) {
localStorage.removeItem(`agent-config-${selectedAgentId.value}`); message.error('没有选择智能体');
// return;
loadAgentConfig(); }
message.info('配置已重置');
try {
//
await systemConfigApi.saveAgentConfig(selectedAgentId.value, {});
//
localStorage.removeItem(`agent-config-${selectedAgentId.value}`);
//
await loadAgentConfig();
message.info('配置已重置');
} catch (error) {
console.error('重置配置出错:', error);
message.error('重置配置失败');
}
}; };
// localStorage // localStorage
@ -420,8 +443,6 @@ onMounted(async () => {
await fetchDefaultAgent(); await fetchDefaultAgent();
// //
await fetchAgents(); await fetchAgents();
//
await fetchTools();
// //
const lastSelectedAgent = localStorage.getItem('last-selected-agent'); 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 = () => { const toggleDebugMode = () => {
state.debug_mode = !state.debug_mode; state.debug_mode = !state.debug_mode;
@ -740,6 +736,12 @@ const closeConfigModal = () => {
justify-content: space-between; justify-content: space-between;
margin-top: 20px; margin-top: 20px;
gap: 10px; gap: 10px;
.form-actions-left,
.form-actions-right {
display: flex;
gap: 10px;
}
} }
} }
@ -789,22 +791,6 @@ const closeConfigModal = () => {
font-size: 14px; 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;
}
}
}
</style> </style>