From 020d7445ec35c0ed2b7a5839438c937f238d88f6 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 6 Jan 2026 20:14:43 +0800 Subject: [PATCH] feat(web): agent support add tools --- web/src/views/ApplicationConfig/Agent.tsx | 65 +++++--- .../components/Knowledge.tsx | 53 +++---- .../ApplicationConfig/components/ToolList.tsx | 149 ++++++++++++++++++ .../components/ToolModal.tsx | 145 +++++++++++++++++ web/src/views/ApplicationConfig/types.ts | 36 ++++- 5 files changed, 389 insertions(+), 59 deletions(-) create mode 100644 web/src/views/ApplicationConfig/components/ToolList.tsx create mode 100644 web/src/views/ApplicationConfig/components/ToolModal.tsx diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index c6aa63e8..f3e327ec 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -18,7 +18,9 @@ import type { Variable, MemoryConfig, AiPromptModalRef, - Source + Source, + ToolModalRef, + ToolOption } from './types' import type { Model } from '@/views/ModelManagement/types' import { getModelList } from '@/api/models'; @@ -31,6 +33,8 @@ import { memoryConfigListUrl } from '@/api/memory' import CustomSelect from '@/components/CustomSelect' import aiPrompt from '@/assets/images/application/aiPrompt.png' import AiPromptModal from './components/AiPromptModal' +import ToolModal from './components/ToolModal' +import ToolList from './components/ToolList' const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => { return ( @@ -47,12 +51,12 @@ const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode} ) } -const SwitchWrapper: FC<{ title: string, desc: string, name: string }> = ({ title, desc, name }) => { +const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; needTransition?: boolean; }> = ({ title, desc, name, needTransition = true }) => { const { t } = useTranslation(); return (
- - + + {desc && } ((_props, ref) => { const [formData, setFormData] = useState<{ default_model_config_id?: string, model_parameters?: Config['model_parameters'], + tools: ToolOption[], } | null>(null) const values = Form.useWatch<{ memoryEnabled: boolean; memory_content?: string | number; - webSearch: boolean; } & Config>([], form) const [knowledgeConfig, setKnowledgeConfig] = useState({ knowledge_bases: [] }) @@ -149,17 +153,21 @@ const Agent = forwardRef((_props, ref) => { setLoading(true) getApplicationConfig(id as string).then(res => { const response = res as Config - setData(response) + setData({ + ...response, + tools: Array.isArray(response.tools) ? response.tools : [] + }) const { memory, tools } = response form.setFieldsValue({ ...response, memoryEnabled: memory?.enabled || false, memory_content: memory?.memory_content ? Number(memory?.memory_content) : undefined, - webSearch: tools?.web_search?.enabled || false, + tools: Array.isArray(tools) ? tools : [] }) setFormData({ default_model_config_id: response.default_model_config_id, model_parameters: response.model_parameters || {}, + tools: Array.isArray(tools) ? tools : [] }) if (response?.knowledge_retrieval?.knowledge_bases?.length) { getDefaultKnowledgeList(response) @@ -260,8 +268,9 @@ const Agent = forwardRef((_props, ref) => { // 保存Agent配置 const handleSave = (flag = true) => { if (!isSave || !data) return Promise.resolve() - const { memoryEnabled, memory_content, webSearch, ...rest } = values + const { memoryEnabled, memory_content, ...rest } = values const { knowledge_bases = [], ...knowledgeRest } = knowledgeConfig || {} + // 从原数据中获取memory的其他必要属性 const originalMemory = data.memory || ({} as MemoryConfig) @@ -285,15 +294,10 @@ const Agent = forwardRef((_props, ref) => { ...(item.config || {}) })) } as KnowledgeConfig : null, - tools: { - web_search: { - enabled: webSearch, - config: { - web_search: webSearch - } - } - } + tools: toolList } + + console.log('params', rest, params) return new Promise((resolve, reject) => { saveAgentConfig(data.app_id, params) @@ -342,6 +346,19 @@ const Agent = forwardRef((_props, ref) => { const updatePrompt = (value: string) => { form.setFieldValue('system_prompt', value) } + + const toolModalRef = useRef(null) + const [toolList, setToolList] = useState([]) + const handleAddTool = () => { + toolModalRef.current?.handleOpen() + } + const updateTools = (tool: ToolOption) => { + const tools = [...toolList, tool] + setToolList(tools) + form.setFieldValue('tools', tools) + } + + console.log('toolList', toolList) return ( <> {loading && } @@ -410,14 +427,12 @@ const Agent = forwardRef((_props, ref) => { data={data?.variables} onUpdate={setVariableList} /> + {/* 工具配置 */} - - - - {/* - */} - - + @@ -454,6 +469,10 @@ const Agent = forwardRef((_props, ref) => { defaultModel={defaultModel} refresh={updatePrompt} /> + ); }); diff --git a/web/src/views/ApplicationConfig/components/Knowledge.tsx b/web/src/views/ApplicationConfig/components/Knowledge.tsx index 0fac117d..bc1207e4 100644 --- a/web/src/views/ApplicationConfig/components/Knowledge.tsx +++ b/web/src/views/ApplicationConfig/components/Knowledge.tsx @@ -31,10 +31,6 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) setEditConfig({ ...(data || {}) }) const knowledge_bases = [...(data.knowledge_bases || [])] setKnowledgeList(knowledge_bases) - onUpdate(prev => ({ - ...prev, - knowledge_bases: knowledge_bases, - })) } }, [data]) @@ -47,10 +43,10 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) const handleDeleteKnowledge = (id: string) => { const list = knowledgeList.filter(item => item.id !== id) setKnowledgeList([...list]) - onUpdate(prev => ({ - ...prev, + onUpdate({ + ...editConfig, knowledge_bases: [...list], - })) + }) } const handleEditKnowledge = (item: KnowledgeBase) => { knowledgeConfigModalRef.current?.handleOpen(item) @@ -69,10 +65,10 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) list = [...values as KnowledgeBase[]] } setKnowledgeList([...list]) - onUpdate(prev => ({ - ...prev, + onUpdate({ + ...editConfig, knowledge_bases: [...list], - })) + }) } else if (type === 'knowledgeConfig') { const index = knowledgeList.findIndex(item => item.id === (values as KnowledgeBase).kb_id) const list = [...knowledgeList] @@ -81,18 +77,19 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) config: {...values as KnowledgeConfigForm} } setKnowledgeList([...list]) - onUpdate(prev => ({ - ...prev, + onUpdate({ + ...editConfig, knowledge_bases: [...list], - })) + }) } else if (type === 'rerankerConfig') { - setEditConfig(prev => ({ ...prev, ...(values as RerankerConfig) })) - onUpdate(prev => ({ - ...prev, - ...values, - reranker_id: values.rerank_model ? values.reranker_id : undefined, - reranker_top_k: values.rerank_model ? values.reranker_top_k : undefined, - })) + const rerankerValues = values as RerankerConfig + setEditConfig(prev => ({ ...prev, ...rerankerValues })) + onUpdate({ + ...editConfig, + ...rerankerValues, + reranker_id: rerankerValues.rerank_model ? rerankerValues.reranker_id : undefined, + reranker_top_k: rerankerValues.rerank_model ? rerankerValues.reranker_top_k : undefined, + }) } } return ( @@ -102,8 +99,8 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) } > -
-
{t('application.associatedKnowledgeBase')}
+
+
{t('application.associatedKnowledgeBase')}
@@ -115,21 +112,21 @@ const Knowledge: FC<{data: KnowledgeConfig; onUpdate: (config: KnowledgeConfig) dataSource={knowledgeList} renderItem={(item) => ( -
-
+
+
{item.name} - + {item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')} -
{t('application.contains', {include_count: item.doc_num})}
+
{t('application.contains', {include_count: item.doc_num})}
handleEditKnowledge(item)} >
handleDeleteKnowledge(item.id)} >
diff --git a/web/src/views/ApplicationConfig/components/ToolList.tsx b/web/src/views/ApplicationConfig/components/ToolList.tsx new file mode 100644 index 00000000..9834b186 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ToolList.tsx @@ -0,0 +1,149 @@ +import { type FC, useRef, useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { Space, Button, List, Switch } from 'antd' +import Card from './Card' +import type { + ToolModalRef, + ToolOption +} from '../types' +import Empty from '@/components/Empty' +import ToolModal from './ToolModal' +import { getToolMethods, getToolDetail } from '@/api/tools' + +const ToolList: FC<{ data: ToolOption[]; onUpdate: (config: ToolOption[]) => void}> = ({data, onUpdate}) => { + const { t } = useTranslation() + const toolModalRef = useRef(null) + const [toolList, setToolList] = useState([]) + useEffect(() => { + if (data) { + const processedData = data.map(async (item) => { + if (!item.label && item.tool_id) { + try { + const [toolDetail, methods] = await Promise.all([ + getToolDetail(item.tool_id), + getToolMethods(item.tool_id) + ]) + + switch ((toolDetail as any).tool_type) { + case 'mcp': + const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation) + return { + ...item, + label: mcpFilterItem?.description, + method_id: mcpFilterItem?.method_id, + value: mcpFilterItem?.name, + description: mcpFilterItem?.description, + parameters: mcpFilterItem?.parameters + } + break + case 'builtin': + if ((methods as any[]).length > 1) { + const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation) + return { + ...item, + label: builtinFilterItem?.description, + method_id: builtinFilterItem?.method_id, + value: builtinFilterItem?.name, + description: builtinFilterItem?.description, + parameters: builtinFilterItem?.parameters + } + } + return { + ...item, + label: (methods as any[])[0]?.description, + method_id: (methods as any[])[0]?.method_id, + value: (methods as any[])[0]?.name, + description: (methods as any[])[0]?.description, + parameters: (methods as any[])[0]?.parameters + } + break + default: + const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation) + return { + ...item, + label: customFilterItem?.name, + method_id: customFilterItem?.method_id, + value: customFilterItem?.name, + description: customFilterItem?.description, + parameters: customFilterItem?.parameters + } + } + } catch (error) { + return item + } + } + return item + }) + + Promise.all(processedData).then(setToolList) + } + }, [data]) + + console.log('toolList', toolList) + + const handleAddTool = () => { + toolModalRef.current?.handleOpen() + } + const updateTools = (tool: ToolOption) => { + const list = [...toolList, tool] + setToolList(list) + onUpdate(list) + } + const handleDeleteTool = (index: number) => { + const list = toolList.filter((_item, idx) => idx !== index) + setToolList([...list]) + onUpdate(list) + } + const handleChangeEnabled = (index: number) => { + const list = toolList.map((item, idx) => { + if (idx === index) { + return { + ...item, + enabled: !item.enabled + } + } + return item + }) + setToolList([...list]) + onUpdate(list) + } + return ( + +{t('application.addTool')} + } + > + + {toolList.length === 0 + ? + : + ( + +
+
+ {item.label} +
+ +
handleDeleteTool(index)} + >
+ handleChangeEnabled(index)} /> +
+
+
+ )} + /> + } + +
+ ) +} +export default ToolList \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ToolModal.tsx b/web/src/views/ApplicationConfig/components/ToolModal.tsx new file mode 100644 index 00000000..64fd6044 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ToolModal.tsx @@ -0,0 +1,145 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Cascader, type CascaderProps } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { ToolModalRef, ToolOption } from '../types' +import RbModal from '@/components/RbModal' +import { getToolMethods, getTools } from '@/api/tools' +import type { ToolType, ToolItem } from '@/views/ToolManagement/types' + +const FormItem = Form.Item; + +interface ToolModalProps { + refresh: (tool: ToolOption) => void; +} + +const ToolModal = forwardRef(({ + refresh, +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [optionList, setOptionList] = useState([ + { value: 'mcp', label: t('tool.mcp'), isLeaf: false }, + { value: 'builtin', label: t('tool.inner'), isLeaf: false }, + { value: 'custom', label: t('tool.custom'), isLeaf: false }, + ]) + const [selectdTools, setSelectedTools] = useState([]) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + setSelectedTools([]) + }; + + const handleOpen = () => { + setVisible(true); + form.resetFields(); + setSelectedTools([]) + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + form.validateFields().then(() => { + setLoading(false) + let operation: any = undefined + if (selectdTools[0].value === 'mcp' || (selectdTools[0].value === 'builtin' && selectdTools[1]?.children && selectdTools[1].children.length > 1)) { + operation = selectdTools[2].value + } else if (selectdTools[0].value === 'custom') { + operation = selectdTools[2].method_id + } + + const tool = { + ...selectdTools[2], + label: selectdTools[0].value === 'custom' ? selectdTools[2].label : selectdTools[2].description, + tool_id: selectdTools[1].value as string, + enabled: true + } + if (operation) { + tool.operation = operation + } + refresh(tool) + handleClose() + }) + } + const loadData = (selectedOptions: ToolOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + if (selectedOptions.length === 1) { + getTools({ tool_type: targetOption.value as ToolType }) + .then(res => { + const response = res as ToolItem[] + targetOption.children = response.map((vo: any) => { + return { + value: vo.id, + label: vo.name, + isLeaf: response.length === 0, + } + }) + setOptionList([...optionList]) + }) + } else { + getToolMethods(targetOption.value as string) + .then(res => { + const response = res as Array<{ method_id: string; name: string }> + targetOption.children = response.map((vo: any) => { + return { + value: vo.name, + label: vo.name, + description: vo.description, + isLeaf: true, + method_id: vo.method_id, + parameters: vo.parameters + } + }) + setOptionList([...optionList]) + }) + } + }; + + const handleChange: CascaderProps['onChange'] = (_value, selectedOptions) => { + console.log('selectedOptions', selectedOptions) + setSelectedTools(selectedOptions) + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+ + + +
+
+ ); +}); + +export default ToolModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 3a1c262c..cc6852b5 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -78,14 +78,7 @@ export interface Config extends MultiAgentConfig { knowledge_retrieval: KnowledgeConfig | null; memory?: MemoryConfig; variables: Variable[]; - tools: { - web_search: { - enabled: boolean; - config: { - web_search: boolean; - } - } - }; + tools: ToolOption[]; is_active: boolean; created_at: number; updated_at: number; @@ -211,4 +204,31 @@ export interface AiPromptForm { model_id?: string; message?: string; current_prompt?: string; +} +export interface ToolModalRef { + handleOpen: () => void; +} + +export interface ToolOption { + value?: string | number | null; + label?: React.ReactNode; + description?: string; + children?: ToolOption[]; + isLeaf?: boolean; + method_id?: string; + operation?: string; + parameters?: Parameter[]; + tool_id?: string; + enabled?: boolean; +} +export interface Parameter { + name: string; + type: string; + description: string; + required: boolean; + default: any; + enum: null | string[]; + minimum: number; + maximum: number; + pattern: null | string; } \ No newline at end of file