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

View File

@ -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)}")

View File

@ -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": "工具列表"
},
)

View File

@ -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)))

View File

@ -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):

View File

@ -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":

View File

@ -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)
},
}
}
// 通用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)"
/>
<a-select
v-else-if="value?.options"
v-else-if="value?.options && value?.type === 'str'"
v-model:value="agentConfig[key]"
>
<a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option>
</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
v-else
v-model:value="agentConfig[key]"
@ -169,28 +176,15 @@
</a-form-item>
</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">
<a-button type="primary" @click="saveConfig">保存配置</a-button>
<a-button @click="resetConfig">重置</a-button>
<a-button @click="closeConfigModal">取消</a-button>
<div class="form-actions-left">
<a-button type="primary" @click="saveConfig">保存并发布配置</a-button>
<a-button @click="resetConfig">重置</a-button>
</div>
<div class="form-actions-right">
<a-button @click="closeConfigModal">关闭</a-button>
</div>
</div>
</a-form>
</div>
@ -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;
}
}
}
</style>