diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts index 142d4c41..b14905f8 100644 --- a/web/src/api/tools.ts +++ b/web/src/api/tools.ts @@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => { } export const parseSchema = (data: Record) => { return request.post(`/tools/parse_schema`, data) - +} +export const getToolDetail = (tool_id: string) => { + return request.get(`/tools/${tool_id}`) +} +export const getToolMethods = (tool_id: string) => { + return request.get(`/tools/${tool_id}/methods`) } \ No newline at end of file diff --git a/web/src/assets/images/workflow/assigner.png b/web/src/assets/images/workflow/assigner.png new file mode 100644 index 00000000..4370bfdd Binary files /dev/null and b/web/src/assets/images/workflow/assigner.png differ diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 80b69879..caa373f3 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1562,14 +1562,17 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re loop: 'Loop', 'cycle-start': '', break: 'Break Loop', + assigner: 'Variable Assignment', parallel: 'Parallel Execution', 'var-aggregator': 'Variable Aggregator', externalInteraction: 'External Interaction', "http-request": 'HTTP Request', - tools: 'Tools', + tool: 'Tools', code_execution: 'Code Execution', "jinja-render": 'Template Rendering', cognitiveUpgrading: 'Cognitive Upgrading (Innovation)', + 'memory-read': 'Memory Retrieval', + 'memory-write': 'Memory Storage', task_planning: 'Task Planning', reasoning_control: 'Reasoning Control', self_reflection: 'Self Reflection', @@ -1714,6 +1717,32 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re cycle_vars: 'Loop Variables', condition: 'Loop Termination Condition', }, + assigner: { + assignments: 'Variables', + cover: 'Overwrite', + assign: 'Set', + clear: 'Clear' + }, + iteration: { + input: 'Input Variable', + output: 'Output Variable', + parallel: 'Parallel Mode', + parallel_count: 'Max Parallelism', + flatten: 'Flatten Output', + }, + tool: { + tool_id: 'Tool', + }, + 'memory-read': { + message: 'Message', + config_id: 'Memory Configuration', + search_switch: 'Search Mode', + }, + 'memory-write': { + message: 'Message', + config_id: 'Memory Configuration', + search_switch: 'Search Mode', + }, name: 'Key', type: 'Type', value: 'Value', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index f081b42d..62271c32 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1663,14 +1663,17 @@ export const zh = { loop: '循环 (Loop)', 'cycle-start': '', break: '退出循环', + assigner: '变量赋值', parallel: '并行执行', 'var-aggregator': '变量聚合器', externalInteraction: '外部交互', "http-request": 'HTTP请求', - tools: '工具 (Tools)', + tool: '工具 (Tool)', code_execution: '代码执行', "jinja-render": '模板渲染', cognitiveUpgrading: '认知升级(创新)', + 'memory-read': '记忆提取', + 'memory-write': '记忆储存', task_planning: '任务规划', reasoning_control: '推理控制', self_reflection: '自我反思', @@ -1815,6 +1818,32 @@ export const zh = { cycle_vars: '循环变量', condition: '循环终止条件', }, + assigner: { + assignments: '变量', + cover: '覆盖', + assign: '设置', + clear: '清空' + }, + iteration: { + input: '输入变量', + output: '输出变量', + parallel: '并行模式', + parallel_count: '最大并行度', + flatten: '扁平化输出', + }, + tool: { + tool_id: '工具', + }, + 'memory-read': { + message: '消息', + config_id: '记忆配置', + search_switch: '检索模式', + }, + 'memory-write': { + message: '消息', + config_id: '记忆配置', + search_switch: '检索模式', + }, name: '键', type: '类型', value: '值', diff --git a/web/src/views/ToolManagement/types.ts b/web/src/views/ToolManagement/types.ts index 4fa07740..6fd4e439 100644 --- a/web/src/views/ToolManagement/types.ts +++ b/web/src/views/ToolManagement/types.ts @@ -1,4 +1,4 @@ -type ToolType = 'mcp' | 'builtin' | 'custom' +export type ToolType = 'mcp' | 'builtin' | 'custom' export interface Query { name?: string; tool_type: ToolType diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index d26a40a1..0be6fba1 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -145,16 +145,18 @@ const PortClickHandler: React.FC = ({ graph }) => { {nodeLibrary.map((category, categoryIndex) => { const sourceNodeData = sourceNode?.getData(); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); + const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration'); let filteredNodes; if (isChildOfLoop) { // Use same filtering as AddNode for child nodes of loop - filteredNodes = category.nodes.filter(nodeType => - nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start' - ); - + filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); + } else if (isChildOfIteration) { + // Filter out loop and iteration nodes for children of iteration nodes + filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type)); } else { // Original filtering for non-loop child nodes + filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' ); diff --git a/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx new file mode 100644 index 00000000..34c133c7 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/AssignmentList/index.tsx @@ -0,0 +1,107 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next'; +import { Form, Input, Button, Row, Col, Select } from 'antd' +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' +import VariableSelect from '../VariableSelect' + +interface AssignmentListProps { + value?: Array<{ variable_selector: string; operation: string[]; value: string;}>; + parentName: string; + options: Suggestion[]; +} + +const AssignmentList: FC = ({ + parentName, + options = [], +}) => { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + + return ( + + {(fields, { add, remove }) => ( + <> +
+ {t(`workflow.config.assigner.${parentName}`)} + add({ operation: 'cover'})} /> +
+ {fields.map(({ key, name, ...restField }) => { + return ( +
+ + + + + + + + + ({ value: key, label: t(`workflow.config.if-else.${key}`) diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index a4186d7b..367e8483 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -80,14 +80,14 @@ const CycleVarsList: FC = ({ return (
-
- 循环变量 - -
{(fields, { add, remove }) => ( <> +
+ 循环变量 + add({ name: '', type: 'string', input_type: 'constant', value: '' })} /> +
{fields.map(({ key, name, ...field }, index) => { const currentInputType = value?.[index]?.input_type; @@ -96,7 +96,7 @@ const CycleVarsList: FC = ({ - + @@ -153,15 +153,6 @@ const CycleVarsList: FC = ({
) })} - - )} diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx new file mode 100644 index 00000000..0cba5596 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -0,0 +1,216 @@ +import { type FC, useEffect, useState } from "react"; +import { useTranslation } from 'react-i18next' +import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps } from 'antd' +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' +import { getToolMethods, getToolDetail, getTools } from '@/api/tools' +import type { ToolType, ToolItem } from '@/views/ToolManagement/types' +import Editor from "../../Editor"; + +interface Option { + value?: string | number | null; + label?: React.ReactNode; + children?: Option[]; + isLeaf?: boolean; + method_id?: string; + parameters?: Parameter[]; +} +interface Parameter { + name: string; + type: string; + description: string; + required: boolean; + default: any; + enum: null | string[]; + minimum: number; + maximum: number; + pattern: null | string; +} + + +const ToolConfig: FC<{ options: Suggestion[]; }> = ({ + options, +}) => { + const { t } = useTranslation() + const form = Form.useFormInstance(); + const values = Form.useWatch([], form) || {} + 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 [parameters, setParameters] = useState([]) + + useEffect(() => { + if (values.tool_id) { + getToolDetail(values.tool_id) + .then(res => { + const detail = res as { tool_type: ToolType; } + + getTools({ tool_type: detail.tool_type }) + .then(toolsRes => { + const tools = toolsRes as ToolItem[] + + getToolMethods(values.tool_id) + .then(methodsRes => { + const response = methodsRes as Array<{ method_id: string; name: string; parameters: Parameter[] }> + + setOptionList(prevList => { + return prevList.map(item => { + if (item.value === detail.tool_type) { + return { + ...item, + children: tools.map((vo: ToolItem) => ({ + value: vo.id, + label: vo.name, + isLeaf: false, + children: vo.id === values.tool_id ? response.map(method => ({ + value: method.name, + label: method.name, + isLeaf: true, + method_id: method.method_id, + parameters: method.parameters + })) : undefined + })) + } + } + return item + }) + }) + + if (response.length > 1) { + const filterTarget = response.find(vo => vo.name === values.tool_parameters?.operation) + if (filterTarget) { + setParameters([...filterTarget.parameters]) + } else { + setParameters([]) + } + } else { + setParameters([...response[0].parameters]) + } + + form.setFieldValue('tools', [detail.tool_type, values.tool_id, values.tool_parameters?.operation ?? response[0].name]) + }) + }) + }) + } + }, [values.tool_id, values.tool_parameters?.operation]); + + useEffect(() => { + if (values.tools && values.tools.length === 3) { + const [toolType, toolId, operation] = values.tools + + // 从 optionList 中查找对应的参数 + const typeOption = optionList.find(opt => opt.value === toolType) + if (typeOption?.children) { + const toolOption = typeOption.children.find(opt => opt.value === toolId) + if (toolOption?.children) { + const methodOption = toolOption.children.find(opt => opt.value === operation) + if (methodOption?.parameters) { + setParameters([...methodOption.parameters]) + } + } + } + } + }, [values.tools]) + + const loadData = (selectedOptions: Option[]) => { + 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, + isLeaf: true, + method_id: vo.method_id, + parameters: vo.parameters + } + }) + setOptionList([...optionList]) + }) + } + }; + + const handleChange: CascaderProps