diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index a96d986c..6154f439 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1137,10 +1137,10 @@ export const en = { promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.', master: 'Supervisor Mode', - master_agent: 'Supervisor Mode', - master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', - handoffs: 'Collaboration Mode', - handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', + supervisor: 'Supervisor Mode', + supervisorDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', + collaboration: 'Collaboration Mode', + collaborationDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', masterConfig: 'Supervisor Configuration', orchestrationMode: 'Task Assignment Strategy', conditional: 'Intelligent Assignment', @@ -1150,6 +1150,8 @@ export const en = { merge: 'Complete Aggregation', vote: 'Key Information Extraction', priority: 'Structured Integration', + addTool: 'Add Tool', + tool: 'Tool', }, userMemory: { userMemory: 'User Memory', @@ -1207,6 +1209,7 @@ export const en = { IMPLICIT_MEMORY: 'Implicit Memory', EMOTIONAL_MEMORY: 'Emotional Memory', EPISODIC_MEMORY: 'Episodic Memory', + FORGETTING_MANAGEMENT: 'Forgetting Management', endUserProfile: 'Core Profile', editEndUserProfile: 'Edit', @@ -1839,6 +1842,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re status_code: 'Status Code', max_attempts: 'Max Retry Attempts', retry_interval: 'Retry Interval', + errorBranch: 'Error Branch', }, 'jinja-render': { template: 'Code', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 2f38cf8e..1fa15454 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -626,10 +626,10 @@ export const zh = { promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。', master: '主管模式', - master_agent: '主管模式', - master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', - handoffs: '协作模式', - handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', + supervisor: '主管模式', + supervisorDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', + collaboration: '协作模式', + collaborationDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', masterConfig: '主管配置', orchestrationMode: '任务分配策略', conditional: '智能分配', @@ -639,6 +639,8 @@ export const zh = { merge: '完整汇总', vote: '关键信息提取', priority: '结构化整合', + addTool: '添加工具', + tool: '工具', }, // 角色管理相关翻译 role: { @@ -1286,6 +1288,7 @@ export const zh = { IMPLICIT_MEMORY: '隐性记忆', EMOTIONAL_MEMORY: '情绪记忆', EPISODIC_MEMORY: '情景记忆', + FORGETTING_MANAGEMENT: '遗忘', endUserProfile: '核心档案', editEndUserProfile: '编辑', @@ -1939,6 +1942,7 @@ export const zh = { status_code: '状态码', max_attempts: '最大重试次数', retry_interval: '重试间隔', + errorBranch: '异常分支', }, 'jinja-render': { template: '代码', @@ -2252,5 +2256,12 @@ export const zh = { orderPayInfo: '支付信息', create_time: '创建时间', }, + forgetDetail: { + title: '遗忘管理系统帮助AI智能管理记忆生命周期,通过自动识别低价值记忆、设置遗忘策略和执行定期清理,优化记忆库存储空间,提升检索效率。', + overviewTitle: '核心指标概览', + totalMemory: '记忆总量', + MemoryHealth: '记忆健康度', + riskOfForgetting: '遗忘风险', + } }, } \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index f3e327ec..ce51c622 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -19,7 +19,6 @@ import type { MemoryConfig, AiPromptModalRef, Source, - ToolModalRef, ToolOption } from './types' import type { Model } from '@/views/ModelManagement/types' @@ -33,7 +32,6 @@ 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}) => { @@ -115,6 +113,7 @@ const Agent = forwardRef((_props, ref) => { const [variableList, setVariableList] = useState([]) const [isSave, setIsSave] = useState(false) const initialized = useRef(false) + const [toolList, setToolList] = useState([]) // 初始化完成标记 useEffect(() => { @@ -143,6 +142,11 @@ const Agent = forwardRef((_props, ref) => { if (isSave) return setIsSave(true) }, [values]) + useEffect(() => { + if (!initialized.current) return + if (isSave) return + setIsSave(true) + }, [toolList]) useEffect(() => { getModels() @@ -294,7 +298,11 @@ const Agent = forwardRef((_props, ref) => { ...(item.config || {}) })) } as KnowledgeConfig : null, - tools: toolList + tools: toolList.map(vo => ({ + tool_id: vo.tool_id, + operation: vo.operation, + enabled: vo.enabled + })) } console.log('params', rest, params) @@ -347,18 +355,6 @@ const Agent = forwardRef((_props, ref) => { 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 && } @@ -469,10 +465,6 @@ const Agent = forwardRef((_props, ref) => { defaultModel={defaultModel} refresh={updatePrompt} /> - ); }); diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index ec38c96a..ce639d62 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -42,7 +42,7 @@ const Cluster = forwardRef((_props, ref) => { const handleSave = (flag = true) => { if (!data) return Promise.resolve() - if (!values.default_model_config_id) { + if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') { message.warning(t('common.selectPlaceholder', { title: t('application.model') })) return Promise.resolve() } @@ -140,15 +140,14 @@ const Cluster = forwardRef((_props, ref) => { ({ + options={['supervisor', 'collaboration'].map((type) => ({ value: type, label: t(`application.${type}`), labelDesc: t(`application.${type}Desc`), - disabled: type === 'handoffs' }))} allowClear={false} /> @@ -192,7 +191,7 @@ const Cluster = forwardRef((_props, ref) => { ))} - + {values?.orchestration_mode !== 'collaboration' && ((_props, ref) => { (({ - ) : ( - - )} - - - ); -}; - export interface TableRow { - key: string; + key?: string; name?: string; value?: string; type?: string; @@ -56,100 +18,158 @@ interface EditableTableProps { title?: string; options?: Suggestion[]; typeOptions?: { value: string, label: string }[] + filterBooleanType?: boolean; } const EditableTable: React.FC = ({ parentName, title, options = [], - typeOptions = [] + typeOptions = [], + filterBooleanType = false }) => { const { t } = useTranslation(); - const form = Form.useFormInstance(); - const values = Form.useWatch(typeof parentName === 'string' ? [parentName] : parentName, form); const createNewRow = (): TableRow => ({ - key: Date.now().toString(), name: undefined, value: undefined, ...(typeOptions.length > 0 && { type: typeOptions[0].value }) }); - const handleAdd = useCallback(() => { - form.setFieldValue(parentName, [...(values ?? []), createNewRow()]); - }, [form, parentName, values, typeOptions]); - - const handleDelete = useCallback((index: number) => { - const currentValues = form.getFieldValue(parentName) || []; - form.setFieldValue(parentName, currentValues.filter((_: TableRow, i: number) => i !== index)); - }, [form, parentName]); - - 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: TableProps['columns'] = useMemo(() => { + const getColumns = (remove: (index: number) => void): TableProps['columns'] => { 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: t('workflow.config.name'), + dataIndex: 'name', + width: baseWidth, + render: (_: any, __: TableRow, index: number) => ( + + + + ) + }, + ...(hasType ? [{ + title: t('workflow.config.type'), + dataIndex: 'type', + width: '20%', + render: (_: any, __: TableRow, index: number) => ( + + {(form) => ( + + = ({ <> body string} > status_code number} > { - if (!value) return Promise.resolve(); - try { - JSON.parse(value); - return Promise.resolve(); - } catch { - return Promise.reject(new Error('Please enter valid JSON format')); - } - } - } - ]} + label={<>headers object} > diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx index b8f7caf0..f35a422e 100644 --- a/web/src/views/Workflow/components/Properties/MappingList/index.tsx +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -17,14 +17,14 @@ const MappingList: React.FC = ({ name, options }) => { {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }) => ( - + - + diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx index cd9dd17c..e5c72523 100644 --- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons'; import Editor from '../Editor' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' -interface TextareaProps { +interface MessageEditor { options: Suggestion[]; title?: string isArray?: boolean; - parentName?: string; + parentName?: string | string[]; label?: string; placeholder?: string; value?: string; + enableJinja2?: boolean; onChange?: (value?: string) => void; } const roleOptions = [ @@ -20,12 +21,13 @@ const roleOptions = [ { label: 'USER', value: 'USER' }, { label: 'ASSISTANT', value: 'ASSISTANT' }, ] -const MessageEditor: FC = ({ +const MessageEditor: FC = ({ title, isArray = true, parentName = 'messages', placeholder, options, + enableJinja2 = false, }) => { const { t } = useTranslation() const form = Form.useFormInstance(); @@ -33,10 +35,17 @@ const MessageEditor: FC = ({ // 检查是否已经使用了context变量,将已使用的context设置为disabled const processedOptions = useMemo(() => { - if (!isArray || !values?.[parentName]) return options; + if (!isArray) return options; + + // 获取表单中对应字段的值 + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + if (!fieldValue) return options; // 获取所有消息内容 - const allContents = values[parentName] + const allContents = fieldValue .map((msg: any) => msg?.content || '') .join(' '); @@ -50,7 +59,11 @@ const MessageEditor: FC = ({ }, [options, values, parentName, isArray]); const handleAdd = (add: FormListOperation['add']) => { - const list = values?.[parentName] || []; + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + const list = fieldValue || []; const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT'; add({ @@ -61,14 +74,14 @@ const MessageEditor: FC = ({ if (!isArray) { return ( - + {title ?? t('workflow.answerDesc')} - + ); @@ -79,7 +92,11 @@ const MessageEditor: FC = ({ {(fields, { add, remove }) => ( {fields.map(({ key, name, ...restField }) => { - const currentRole = (values?.[parentName]?.[name]?.role || 'USER').toUpperCase(); + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase(); return ( @@ -105,7 +122,7 @@ const MessageEditor: FC = ({ )} - + ); diff --git a/web/src/views/Workflow/components/Properties/VariableSelect.tsx b/web/src/views/Workflow/components/Properties/VariableSelect.tsx index 7b4a7dfb..60eb2d0d 100644 --- a/web/src/views/Workflow/components/Properties/VariableSelect.tsx +++ b/web/src/views/Workflow/components/Properties/VariableSelect.tsx @@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps { value?: string; onChange?: (value: string) => void; allowClear?: boolean; + filterBooleanType?: boolean; } const VariableSelect: FC = ({ @@ -18,6 +19,7 @@ const VariableSelect: FC = ({ allowClear = true, onChange, size, + filterBooleanType = false, ...resetPorps }) => { @@ -26,7 +28,7 @@ const VariableSelect: FC = ({ } const labelRender: LabelRender = (props) => { const { value } = props - const filterOption = options.find(vo => `{{${vo.value}}}` === value) + const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value) if (filterOption) { return ( @@ -54,7 +56,11 @@ const VariableSelect: FC = ({ } return null } - const groupedSuggestions = options.reduce((groups: Record, suggestion) => { + const filteredOptions = filterBooleanType + ? options.filter(option => option.dataType !== 'boolean') + : options; + + const groupedSuggestions = filteredOptions.reduce((groups: Record, suggestion) => { const { nodeData } = suggestion const nodeId = nodeData.id as string; if (!groups[nodeId]) { @@ -64,12 +70,10 @@ const VariableSelect: FC = ({ return groups; }, {}); - const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({ + const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({ label: suggestions[0].nodeData.name, options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` })) })); - - console.log('groupedOptions', groupedOptions) return ( ({ ...vo, label: t(vo.label) })) : config.options} + options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options} placeholder={t('common.pleaseSelect')} /> : config.type === 'inputNumber' @@ -698,9 +990,10 @@ const Properties: FC = ({ ? { + const baseVariableList = getFilteredVariableList(selectedNode?.data?.type); // Apply filtering if specified in config if (config.filterNodeTypes || config.filterVariableNames) { - return variableList.filter(variable => { + return baseVariableList.filter(variable => { const nodeTypeMatch = !config.filterNodeTypes || (Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type)); const variableNameMatch = !config.filterVariableNames || @@ -721,22 +1014,38 @@ const Properties: FC = ({ return nodeData?.cycle === selectedNode.id; }); - return variableList.filter(variable => + return baseVariableList.filter(variable => childNodes.some(node => node.id === variable.nodeData?.id) ); } - return variableList; + return baseVariableList; })() } /> : config.type === 'switch' - ? { form.setFieldValue('group_variables', []) } : undefined} /> + ? { form.setFieldValue('group_variables', []) } : undefined} /> : config.type === 'categoryList' ? : config.type === 'conditionList' ? { + // For loop nodes, add cycle_vars to condition options + if (selectedNode?.data?.type === 'loop') { + const cycleVars = values?.cycle_vars || []; + const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({ + key: `${selectedNode.id}_cycle_${cycleVar.name}`, + label: cycleVar.name, + type: 'variable', + dataType: cycleVar.type || 'String', + value: `${selectedNode.getData().id}.${cycleVar.name}`, + nodeData: selectedNode.getData(), + })); + return [...getFilteredVariableList(selectedNode?.data?.type), ...cycleVarSuggestions]; + } + return getFilteredVariableList(selectedNode?.data?.type); + })() + } selectedNode={selectedNode} graphRef={graphRef} addBtnText={t('workflow.config.addCase')} diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 98f6d865..692339da 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -300,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [ config: { cycle_vars: { type: 'cycleVarsList', + defaultValue: [] }, condition: { type: 'conditionList', @@ -395,12 +396,14 @@ export const nodeLibrary: NodeLibrary[] = [ }, retry: { type: 'switch', - defaultValue: false + defaultValue: { + enable: false + } }, error_handle: { type: 'define', defaultValue: { - method: 'default' + method: 'none' } } } @@ -420,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [ config: { mapping: { type: 'mappingList', - defaultValue: [] + defaultValue: [{name: 'arg1'}] }, template: { type: 'messageEditor', isArray: false, + enableJinja2: true, + defaultValue: "{{arg1}}" }, } } diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 04e6a63a..1f3c5846 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -193,6 +193,27 @@ export const useWorkflowGraph = ({ nodeConfig.height = newHeight; } + // 如果是http-request节点,检查error_handle.method配置 + if (type === 'http-request' && (config as any).error_handle?.method === 'branch') { + const portAttrs = { + circle: { + r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 } + }, + }; + + nodeConfig.ports = { + groups: { + right: { position: 'right', attrs: portAttrs }, + left: { position: 'left', attrs: portAttrs }, + }, + items: [ + { group: 'left' }, + { group: 'right', id: 'right' }, + { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}} + ] + }; + } + return nodeConfig }) @@ -284,6 +305,14 @@ export const useWorkflowGraph = ({ } } + // 如果是http-request节点且有label,根据label匹配对应的端口 + if (sourceCell.getData()?.type === 'http-request' && label) { + const matchingPort = sourcePorts.find((port: any) => port.id === label); + if (matchingPort) { + sourcePort = label; + } + } + const edgeConfig = { source: { cell: sourceCell.id, @@ -954,6 +983,23 @@ export const useWorkflowGraph = ({ }; } + // 如果是http-request节点的右侧端口连线,添加label + if (sourceCell?.getData()?.type === 'http-request') { + if (sourcePortId === 'ERROR') { + return { + source: sourceCell.getData().id, + target: targetCell?.getData().id, + label: 'ERROR', + }; + } else { + return { + source: sourceCell.getData().id, + target: targetCell?.getData().id, + label: 'SUCCESS', + }; + } + } + return { source: sourceCell?.getData().id, target: targetCell?.getData().id, diff --git a/web/src/views/Workflow/types.ts b/web/src/views/Workflow/types.ts index c3e883b1..b5d72b4a 100644 --- a/web/src/views/Workflow/types.ts +++ b/web/src/views/Workflow/types.ts @@ -26,6 +26,7 @@ export interface NodeConfig { group_variables?: Array<{ key: string, value: string[] }> cycle?: string; + cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }> [key: string]: unknown; }