From 020d7445ec35c0ed2b7a5839438c937f238d88f6 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 6 Jan 2026 20:14:43 +0800 Subject: [PATCH 1/2] 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 From 7a1131d8afce0006b2542eb56e199ae2d08b3f10 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 6 Jan 2026 20:35:01 +0800 Subject: [PATCH 2/2] fix(web): workflow bug --- .../Properties/AssignmentList/index.tsx | 25 +- .../components/Properties/CaseList/index.tsx | 14 +- .../Properties/ConditionList/index.tsx | 12 +- .../Properties/HttpRequest/EditableTable.tsx | 252 ++++++++---------- .../Properties/HttpRequest/index.tsx | 33 +-- .../Properties/ParamsList/ParamEditModal.tsx | 1 + .../Workflow/components/Properties/index.tsx | 4 +- web/src/views/Workflow/constant.ts | 6 +- .../views/Workflow/hooks/useWorkflowGraph.ts | 26 +- web/src/views/Workflow/types.ts | 2 +- 10 files changed, 177 insertions(+), 198 deletions(-) diff --git a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx index eac3775f..2ac8397b 100644 --- a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx +++ b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx @@ -62,6 +62,10 @@ const AssignmentList: FC = ({ placeholder={t('common.pleaseSelect')} options={options} popupMatchSelectWidth={false} + onChange={() => { + form.setFieldValue([parentName, name, 'operation'], undefined); + form.setFieldValue([parentName, name, 'value'], undefined); + }} /> @@ -72,6 +76,7 @@ const AssignmentList: FC = ({ noStyle > ({ ...vo, diff --git a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx index 2f544281..6d955647 100644 --- a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx +++ b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx @@ -9,7 +9,7 @@ import Editor from '../../Editor' interface Case { logical_operator: 'and' | 'or'; - expressions: Array<{ left: string; comparison_operator: string; right: string; input_type: string; }> + expressions: Array<{ left: string; operator: string; right: string; input_type: string; }> } interface CaseListProps { @@ -45,6 +45,8 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = { boolean: [ { value: 'eq', label: 'workflow.config.if-else.boolean.eq' }, { value: 'ne', label: 'workflow.config.if-else.boolean.ne' }, + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, ] } @@ -61,7 +63,7 @@ const ConditionList: FC = ({ expressions: { [index]: { left: newValue, - comparison_operator: undefined, + operator: undefined, right: undefined, input_type: undefined } @@ -87,7 +89,7 @@ const ConditionList: FC = ({ {fields.map((field, index) => { const expressions = form.getFieldValue([parentName, 'expressions']) || []; const currentExpression = expressions[index] || {}; - const currentOperator = currentExpression.comparison_operator; + const currentOperator = currentExpression.operator; const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty'; const leftFieldValue = currentExpression.left; const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); @@ -122,7 +124,7 @@ const ConditionList: FC = ({ - + + ) : ( + + )} + + + ); +}; + export interface TableRow { key: string; - name: string; - value: string; + name?: string; + value?: string; type?: string; } interface EditableTableProps { + parentName: string | string[]; title?: string; - value?: Record | TableRow[]; - onChange?: (value: TableRow[]) => void; options?: Suggestion[]; - typeOptions?: {value: string, label: string}[] + typeOptions?: { value: string, label: string }[] } const EditableTable: React.FC = ({ + parentName, title, - value, - onChange, options = [], typeOptions = [] }) => { - const { t } = useTranslation() - const [rows, setRows] = useState([]); + const { t } = useTranslation(); + const form = Form.useFormInstance(); + const values = Form.useWatch(typeof parentName === 'string' ? [parentName] : parentName, form); - useEffect(() => { - if (Array.isArray(value)) { - setRows([...value]) - } else if (value && Object.keys(value).length > 0) { - setRows(Object.entries(value).map(([key, val], index) => ({ - key: index.toString(), - name: key || '', - value: val || '', - type: typeOptions.length > 0 ? typeOptions[0].value : undefined - }))) - } else { - setRows([]) - } - }, [value, typeOptions]) + const createNewRow = (): TableRow => ({ + key: Date.now().toString(), + name: undefined, + value: undefined, + ...(typeOptions.length > 0 && { type: typeOptions[0].value }) + }); - const handleChange = (key: string, field: 'name' | 'value' | 'type', val: string) => { - const newRows = rows.map(row => - row.key === key ? { ...row, [field]: val } : row - ); - setRows(newRows); - onChange?.(newRows); - }; + const handleAdd = useCallback(() => { + form.setFieldValue(parentName, [...(values ?? []), createNewRow()]); + }, [form, parentName, values, typeOptions]); - const handleAdd = () => { - const newRow: TableRow = { - key: Date.now().toString(), - name: '', - value: '', - ...(typeOptions.length > 0 && { type: typeOptions[0].value }) - }; - const newRows = [...rows, newRow]; - setRows(newRows); - onChange?.(newRows); - }; + const handleDelete = useCallback((index: number) => { + const currentValues = form.getFieldValue(parentName) || []; + form.setFieldValue(parentName, currentValues.filter((_: TableRow, i: number) => i !== index)); + }, [form, parentName]); - const handleDelete = (key: string) => { - const newRows = rows.filter(row => row.key !== key); - setRows(newRows); - onChange?.(newRows); - }; + const createColumn = (dataIndex: string, inputType: 'select' | 'variableSelect', width: string, columnOptions: any[]) => ({ + title: t(`workflow.config.${dataIndex}`), + dataIndex, + width, + onCell: (_: TableRow, index?: number) => ({ + name: typeof parentName === 'string' ? [parentName, index ?? 0, dataIndex] : [...parentName, index ?? 0, dataIndex], + inputType, + options: columnOptions + } as any) + }); - const columns = useMemo(() => { - const baseColumns = [ + const columns: TableProps['columns'] = useMemo(() => { + const hasType = typeOptions.length > 0; + const baseWidth = hasType ? '35%' : '45%'; + + return [ + createColumn('name', 'variableSelect', baseWidth, options), + ...(hasType ? [createColumn('type', 'select', '20%', typeOptions)] : []), + createColumn('value', 'variableSelect', baseWidth, options), { - title: typeOptions.length > 0 ? t('workflow.config.name') : '键', - dataIndex: 'name', - width: typeOptions.length > 0 ? '35%' : '45%', - render: (text: string, record: TableRow) => ( - handleChange(record.key, 'name', value || '')} - /> - ), + title: '', + dataIndex: 'actions', + width: '10%', + render: (_: any, __: TableRow, index: number) => ( +