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()}"
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"]]
)
return {"messages": [res]}

View File

@ -81,7 +81,7 @@ def get_all_tools():
# 获取所有知识库
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 = (
f"使用 {retrieve_info['name']} 知识库进行检索。\n"
f"下面是这个知识库的描述:\n{retrieve_info['description']}"

View File

@ -62,6 +62,18 @@
<p>{{ selectedAgent.description }}</p>
<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">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
@ -78,7 +90,7 @@
<!-- key匹配 -->
<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
@select-model="handleModelChange"
: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>
<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>
<!-- 多选标签卡片 -->
<div v-else-if="value?.options && value?.type === 'list'" class="multi-select-cards">
<div class="multi-select-label">
<span>已选择 {{ getSelectedCount(key) }} </span>
<a-button type="link" size="small" @click="clearSelection(key)" v-if="getSelectedCount(key) > 0" >
清空
</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
v-else
v-model:value="agentConfig[key]"
@ -118,6 +152,8 @@
</a-form-item>
</template>
<a-divider />
<!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig">
<div class="form-actions-left">
@ -128,15 +164,6 @@
</a-form>
</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>
@ -163,7 +190,9 @@ import {
LinkOutlined,
StarOutlined,
MenuOutlined,
StarFilled
StarFilled,
CheckCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-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
const fetchDefaultAgent = async () => {
try {
@ -278,6 +343,9 @@ const loadAgentConfig = async () => {
//
if (typeof item.default === 'boolean') {
agentConfig.value[key] = item.default;
} else if (item.type === 'list') {
// list
agentConfig.value[key] = Array.isArray(item.default) ? item.default : [];
} else {
agentConfig.value[key] = item.default || '';
}
@ -290,7 +358,13 @@ const loadAgentConfig = async () => {
//
Object.keys(response.config).forEach(key => {
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)}`);
@ -568,7 +642,6 @@ const toggleSidebar = () => {
// requirements
.info-section {
margin-top: 16px;
border-top: 1px solid var(--main-light-3);
padding-top: 12px;
h3 {
@ -693,6 +766,92 @@ const toggleSidebar = () => {
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>