feat: 更新工具工厂和聊天机器人逻辑,增强前端多选组件

- 在 tools_factory.py 中修改工具名称生成逻辑,确保兼容 Deepseek 的要求。
- 在 graph.py 中优化聊天模型与工具的绑定逻辑,增加条件判断以提高灵活性。
- 在 AgentView.vue 中新增环境变量显示部分,增强用户界面,添加多选卡片组件以改善选项选择体验。
- 更新样式以支持多选卡片的响应式设计,提升整体用户体验。
This commit is contained in:
Wenjie Zhang 2025-06-24 00:13:11 +08:00
parent 286e06f25b
commit fd7cd8cf25
3 changed files with 184 additions and 23 deletions

View File

@ -51,9 +51,11 @@ class ChatbotAgent(BaseAgent):
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)
model_with_tools = model.bind_tools(self._get_tools(conf.tools))
res = model_with_tools.invoke( if tools := self._get_tools(conf.tools):
model = model.bind_tools(tools)
res = model.invoke(
[{"role": "system", "content": system_prompt}, *state["messages"]] [{"role": "system", "content": system_prompt}, *state["messages"]]
) )
return {"messages": [res]} return {"messages": [res]}

View File

@ -81,7 +81,7 @@ def get_all_tools():
# 获取所有知识库 # 获取所有知识库
for db_Id, retrieve_info in knowledge_base.get_retrievers().items(): for db_Id, retrieve_info in knowledge_base.get_retrievers().items():
name = f"retrieve_{retrieve_info['name']}" name = f"retrieve_{db_Id}" # Deepseek does not support non-alphanumeric characters in tool names
description = ( description = (
f"使用 {retrieve_info['name']} 知识库进行检索。\n" f"使用 {retrieve_info['name']} 知识库进行检索。\n"
f"下面是这个知识库的描述:\n{retrieve_info['description']}" f"下面是这个知识库的描述:\n{retrieve_info['description']}"

View File

@ -62,6 +62,18 @@
<p>{{ selectedAgent.description }}</p> <p>{{ selectedAgent.description }}</p>
<pre v-if="state.debug_mode">{{ selectedAgent }}</pre> <pre v-if="state.debug_mode">{{ selectedAgent }}</pre>
<!-- 添加requirements显示部分 -->
<div v-if="agents[selectedAgentId]?.requirements && agents[selectedAgentId]?.requirements.length > 0" class="info-section">
<h3>所需环境变量:</h3>
<div class="requirements-list">
<a-tag v-for="req in agents[selectedAgentId].requirements" :key="req">
{{ req }}
</a-tag>
</div>
</div>
<a-divider />
<div v-if="selectedAgentId && configSchema" class="config-modal-content"> <div v-if="selectedAgentId && configSchema" class="config-modal-content">
<!-- 配置表单 --> <!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical"> <a-form :model="agentConfig" layout="vertical">
@ -78,7 +90,7 @@
<!-- key匹配 --> <!-- key匹配 -->
<div v-if="key === 'model'" class="agent-model"> <div v-if="key === 'model'" class="agent-model">
<p><small>注意部分模型对于 Tool Calling 的支持不稳定建议采用{{ value.options }} </small></p> <!-- <p><small>注意部分模型对于 Tool Calling 的支持不稳定建议采用{{ value.options }} </small></p> -->
<ModelSelectorComponent <ModelSelectorComponent
@select-model="handleModelChange" @select-model="handleModelChange"
:model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''" :model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''"
@ -103,13 +115,35 @@
> >
<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'" <div v-else-if="value?.options && value?.type === 'list'" class="multi-select-cards">
v-model:value="agentConfig[key]" <div class="multi-select-label">
mode="multiple" <span>已选择 {{ getSelectedCount(key) }} </span>
> <a-button type="link" size="small" @click="clearSelection(key)" v-if="getSelectedCount(key) > 0" >
<a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option> 清空
</a-select> </a-button>
</div>
<div class="options-grid">
<div
v-for="option in value.options"
:key="option"
class="option-card"
:class="{
'selected': isOptionSelected(key, option),
'unselected': !isOptionSelected(key, option)
}"
@click="toggleOption(key, option)"
>
<div class="option-content">
<span class="option-text">{{ option }}</span>
<div class="option-indicator">
<CheckCircleOutlined v-if="isOptionSelected(key, option)" />
<PlusCircleOutlined v-else />
</div>
</div>
</div>
</div>
</div>
<a-input <a-input
v-else v-else
v-model:value="agentConfig[key]" v-model:value="agentConfig[key]"
@ -118,6 +152,8 @@
</a-form-item> </a-form-item>
</template> </template>
<a-divider />
<!-- 弹窗底部按钮 --> <!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig"> <div class="form-actions" v-if="!state.isEmptyConfig">
<div class="form-actions-left"> <div class="form-actions-left">
@ -128,15 +164,6 @@
</a-form> </a-form>
</div> </div>
<!-- 添加requirements显示部分 -->
<div v-if="agents[selectedAgentId]?.requirements && agents[selectedAgentId]?.requirements.length > 0" class="info-section">
<h3>所需环境变量:</h3>
<div class="requirements-list">
<a-tag v-for="req in agents[selectedAgentId].requirements" :key="req">
{{ req }}
</a-tag>
</div>
</div>
</div> </div>
</div> </div>
@ -163,7 +190,9 @@ import {
LinkOutlined, LinkOutlined,
StarOutlined, StarOutlined,
MenuOutlined, MenuOutlined,
StarFilled StarFilled,
CheckCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue'; import AgentChatComponent from '@/components/AgentChatComponent.vue';
@ -215,6 +244,42 @@ const setAsDefaultAgent = async () => {
} }
}; };
//
const ensureArray = (key) => {
if (!agentConfig.value[key] || !Array.isArray(agentConfig.value[key])) {
agentConfig.value[key] = [];
}
};
const isOptionSelected = (key, option) => {
ensureArray(key);
return agentConfig.value[key].includes(option);
};
const getSelectedCount = (key) => {
ensureArray(key);
return agentConfig.value[key].length;
};
const toggleOption = (key, option) => {
ensureArray(key);
const currentOptions = [...agentConfig.value[key]];
const index = currentOptions.indexOf(option);
if (index > -1) {
currentOptions.splice(index, 1);
} else {
currentOptions.push(option);
}
agentConfig.value[key] = currentOptions;
};
const clearSelection = (key) => {
agentConfig.value[key] = [];
};
// ID // ID
const fetchDefaultAgent = async () => { const fetchDefaultAgent = async () => {
try { try {
@ -278,6 +343,9 @@ const loadAgentConfig = async () => {
// //
if (typeof item.default === 'boolean') { if (typeof item.default === 'boolean') {
agentConfig.value[key] = item.default; agentConfig.value[key] = item.default;
} else if (item.type === 'list') {
// list
agentConfig.value[key] = Array.isArray(item.default) ? item.default : [];
} else { } else {
agentConfig.value[key] = item.default || ''; agentConfig.value[key] = item.default || '';
} }
@ -290,7 +358,13 @@ const loadAgentConfig = async () => {
// //
Object.keys(response.config).forEach(key => { Object.keys(response.config).forEach(key => {
if (key in agentConfig.value) { if (key in agentConfig.value) {
agentConfig.value[key] = response.config[key]; const item = items[key];
// list
if (item && item.type === 'list') {
agentConfig.value[key] = Array.isArray(response.config[key]) ? response.config[key] : [];
} else {
agentConfig.value[key] = response.config[key];
}
} }
}); });
console.log(`从服务器加载 ${selectedAgentId.value} 配置成功, ${JSON.stringify(agentConfig.value)}`); console.log(`从服务器加载 ${selectedAgentId.value} 配置成功, ${JSON.stringify(agentConfig.value)}`);
@ -568,7 +642,6 @@ const toggleSidebar = () => {
// requirements // requirements
.info-section { .info-section {
margin-top: 16px; margin-top: 16px;
border-top: 1px solid var(--main-light-3);
padding-top: 12px; padding-top: 12px;
h3 { h3 {
@ -693,6 +766,92 @@ const toggleSidebar = () => {
font-size: 14px; font-size: 14px;
} }
} }
//
.multi-select-cards {
.multi-select-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 12px;
color: var(--gray-600);
height: 24px;
}
.options-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px;
}
.option-card {
border: 1px solid var(--gray-300);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s ease;
background: white;
user-select: none;
&:hover {
border-color: var(--main-color);
}
&.selected {
border-color: var(--main-color);
background: var(--main-10);
.option-indicator {
color: var(--main-color);
}
.option-text {
color: var(--main-color);
font-weight: 500;
}
}
&.unselected {
.option-indicator {
color: var(--gray-400);
}
.option-text {
color: var(--gray-700);
}
}
.option-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.option-text {
flex: 1;
font-size: 14px;
line-height: 1.4;
word-break: break-word;
}
.option-indicator {
flex-shrink: 0;
font-size: 16px;
transition: color 0.2s ease;
}
}
}
//
@media (max-width: 768px) {
.multi-select-cards {
.options-grid {
grid-template-columns: 1fr;
}
}
}
</style> </style>