diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/agent-header-bar.tsx similarity index 100% rename from web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx rename to web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/agent-header-bar.tsx diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/index.ts b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/index.ts new file mode 100644 index 0000000000..18c10e6585 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/index.ts @@ -0,0 +1,2 @@ +export { default as AgentHeaderBar } from './agent-header-bar' +export { default as Placeholder } from './placeholder' diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/placeholder.tsx similarity index 97% rename from web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx rename to web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/placeholder.tsx index 02053861a3..9d17d4b26c 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/components/placeholder.tsx @@ -19,7 +19,7 @@ const Placeholder = ({ disableVariableInsertion = false, hasSelectedAgent = fals const textNode = new CustomTextNode(text) $insertNodes([textNode]) }) - editor.dispatchCommand(FOCUS_COMMAND, undefined as any) + editor.dispatchCommand(FOCUS_COMMAND, new FocusEvent('focus')) }, [editor]) return ( diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/index.ts b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/index.ts new file mode 100644 index 0000000000..818b242598 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/index.ts @@ -0,0 +1,7 @@ +export { + AGENT_CONTEXT_VAR_PATTERN, + buildAssemblePlaceholder, + getAgentNodeIdFromContextVar, + useMixedVariableExtractor, +} from './use-mixed-variable-extractor' +export type { DetectedAgent } from './use-mixed-variable-extractor' diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts new file mode 100644 index 0000000000..289c242f2f --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts @@ -0,0 +1,440 @@ +import type { ReactFlowState } from 'reactflow' +import type { ToolParameter } from '@/app/components/tools/types' +import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types' +import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' +import type { + CommonNodeType, + PromptItem, + PromptTemplateItem, + Node as WorkflowNode, +} from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import { Type } from '@/app/components/workflow/nodes/llm/types' +import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types' +import { generateNewNode, getNodeCustomTypeByNodeDataType, mergeNodeDefaultData } from '@/app/components/workflow/utils' +import { fetchMentionGraph } from '@/service/workflow' +import { FlowType } from '@/types/common' + +// Constants + +export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g +const AGENT_CONTEXT_VAR_PREFIX = '{{@' +const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' + +export const getAgentNodeIdFromContextVar = (placeholder: string): string => { + if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX)) + return '' + return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length) +} + +export const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string): string => { + if (!toolNodeId || !paramKey) + return '' + return `{{#${toolNodeId}_ext_${paramKey}.result#}}` +} + +const resolvePromptText = (item?: PromptItem): string => { + if (!item) + return '' + if (item.edition_type === EditionType.jinja2) + return item.jinja2_text || item.text || '' + return item.text || '' +} + +const getUserPromptText = (promptTemplate?: PromptTemplateItem[] | PromptItem): string => { + if (!promptTemplate) + return '' + if (Array.isArray(promptTemplate)) { + const userPrompt = promptTemplate.find( + item => !isPromptMessageContext(item) && item.role === PromptRole.user, + ) as PromptItem | undefined + return resolvePromptText(userPrompt) + } + return resolvePromptText(promptTemplate) +} + +const hasUserPromptTemplate = (promptTemplate: PromptTemplateItem[] | PromptItem): boolean => { + if (!Array.isArray(promptTemplate)) + return true + return promptTemplate.some(item => !isPromptMessageContext(item) && item.role === PromptRole.user) +} + +const applyPromptText = (item: PromptItem, text: string): PromptItem => { + if (item.edition_type === EditionType.jinja2) { + return { + ...item, + text, + jinja2_text: text, + } + } + return { + ...item, + text, + } +} + +const buildPromptTemplateWithText = ( + promptTemplate: PromptTemplateItem[] | PromptItem, + text: string, +): PromptTemplateItem[] | PromptItem => { + if (!Array.isArray(promptTemplate)) + return applyPromptText(promptTemplate as PromptItem, text) + + const userIndex = promptTemplate.findIndex( + item => !isPromptMessageContext(item) && item.role === PromptRole.user, + ) + if (userIndex >= 0) { + return promptTemplate.map((item, index) => { + if (index !== userIndex || isPromptMessageContext(item)) + return item + return applyPromptText(item as PromptItem, text) + }) as PromptTemplateItem[] + } + + const useJinja = promptTemplate.some( + item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2, + ) + const defaultUserPrompt: PromptItem = useJinja + ? { + role: PromptRole.user, + text, + jinja2_text: text, + edition_type: EditionType.jinja2, + } + : { role: PromptRole.user, text } + + return [...promptTemplate, defaultUserPrompt] as PromptTemplateItem[] +} + +type NodesMetaDataMap = Record + checkValid?: (data: CommonNodeType, t: (key: string, options?: Record) => string, moreData?: unknown) => { isValid: boolean, errorMessage?: string } +}> + +type ConfigsMap = { + flowId?: string + flowType?: FlowType +} + +type ExtractorNodePayload = { + extractorNodeId: string + nodeType: BlockEnum + data: Partial +} + +export type DetectedAgent = { + nodeId: string + name: string +} + +type ReactFlowStoreApi = { + getState: () => ReactFlowState +} + +export type UseMixedVariableExtractorOptions = { + toolNodeId?: string + paramKey: string + language: string + nodesById: Record + nodesMetaDataMap?: NodesMetaDataMap + nodesDefaultConfigs?: Record> + reactFlowStore: ReactFlowStoreApi + handleSyncWorkflowDraft: () => void + configsMap?: ConfigsMap +} + +export function useMixedVariableExtractor({ + toolNodeId, + paramKey, + language, + nodesById, + nodesMetaDataMap, + nodesDefaultConfigs, + reactFlowStore, + handleSyncWorkflowDraft, + configsMap, +}: UseMixedVariableExtractorOptions) { + const assembleExtractorNodeId = useMemo(() => { + if (!toolNodeId || !paramKey) + return '' + return `${toolNodeId}_ext_${paramKey}` + }, [paramKey, toolNodeId]) + + const resolveMentionParameterSchema = useCallback((key: string) => { + if (!toolNodeId) { + return { + name: key, + type: Type.string, + description: '', + } + } + const toolNodeData = nodesById[toolNodeId]?.data as { paramSchemas?: ToolParameter[] } | undefined + const paramSchema = toolNodeData?.paramSchemas?.find(param => param.name === key) + const description = paramSchema?.llm_description + || paramSchema?.human_description?.[language] + || paramSchema?.human_description?.en_US + || '' + return { + name: paramSchema?.name || key, + type: paramSchema?.type || Type.string, + description, + } + }, [language, nodesById, toolNodeId]) + + const ensureExtractorNode = useCallback((payload: ExtractorNodePayload) => { + if (!toolNodeId) + return null + const metaDefault = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial | undefined + const appDefault = nodesDefaultConfigs?.[payload.nodeType] as Partial | undefined + if (!metaDefault && !appDefault) + return null + + const { getNodes, setNodes } = reactFlowStore.getState() + const currentNodes = getNodes() + const existingNode = currentNodes.find(node => node.id === payload.extractorNodeId) + const shouldReplace = existingNode && existingNode.data.type !== payload.nodeType + if (!existingNode || shouldReplace) { + const nextNodes = shouldReplace + ? currentNodes.filter(node => node.id !== payload.extractorNodeId) + : currentNodes + const mergedData = mergeNodeDefaultData({ + nodeType: payload.nodeType, + metaDefault, + appDefault, + overrideData: payload.data, + }) + const resolvedTitle = mergedData.title ?? metaDefault?.title ?? appDefault?.title ?? '' + const resolvedDesc = mergedData.desc ?? metaDefault?.desc ?? appDefault?.desc ?? '' + const { newNode } = generateNewNode({ + id: payload.extractorNodeId, + type: getNodeCustomTypeByNodeDataType(payload.nodeType), + data: { + ...mergedData, + type: payload.nodeType, + title: resolvedTitle, + desc: resolvedDesc, + parent_node_id: toolNodeId, + }, + position: { + x: 0, + y: 0, + }, + hidden: true, + }) + setNodes([...nextNodes, newNode]) + handleSyncWorkflowDraft() + return newNode + } + + return existingNode + }, [handleSyncWorkflowDraft, nodesDefaultConfigs, nodesMetaDataMap, reactFlowStore, toolNodeId]) + + const ensureAssembleExtractorNode = useCallback(() => { + if (!assembleExtractorNodeId) + return '' + const extractorNode = ensureExtractorNode({ + extractorNodeId: assembleExtractorNodeId, + nodeType: BlockEnum.Code, + data: { + outputs: { + result: { + type: VarType.string, + children: null, + }, + }, + }, + }) + if (!extractorNode) + return '' + if (extractorNode.data.type !== BlockEnum.Code) + return assembleExtractorNodeId + + const outputs = (extractorNode.data as CodeNodeType).outputs || {} + const resultOutput = outputs.result + if (!resultOutput || resultOutput.type !== VarType.string) { + const { getNodes, setNodes } = reactFlowStore.getState() + const currentNodes = getNodes() + const nextOutputs = { + ...outputs, + result: { + type: VarType.string, + children: null, + }, + } + setNodes(currentNodes.map((node) => { + if (node.id !== assembleExtractorNodeId) + return node + return { + ...node, + data: { + ...node.data, + outputs: nextOutputs, + }, + } + })) + handleSyncWorkflowDraft() + } + + return assembleExtractorNodeId + }, [assembleExtractorNodeId, ensureExtractorNode, handleSyncWorkflowDraft, reactFlowStore]) + + const removeExtractorNode = useCallback(() => { + if (!toolNodeId || !paramKey) + return + + const extractorNodeId = `${toolNodeId}_ext_${paramKey}` + const { getNodes, setNodes } = reactFlowStore.getState() + const nodes = getNodes() + const hasExtractorNode = nodes.some(node => node.id === extractorNodeId) + if (!hasExtractorNode) + return + + setNodes(nodes.filter(node => node.id !== extractorNodeId)) + handleSyncWorkflowDraft() + }, [handleSyncWorkflowDraft, paramKey, reactFlowStore, toolNodeId]) + + const syncExtractorPromptFromText = useCallback(( + text: string, + detectAgentFromText: (text: string) => DetectedAgent | null, + ) => { + if (!toolNodeId || !paramKey) + return + + const detectedAgent = detectAgentFromText(text) + if (!detectedAgent) + return + + const escapedAgentId = detectedAgent.nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const leadingPattern = new RegExp(`^\\{\\{@${escapedAgentId}\\.context@\\}\\}`) + const promptText = text.replace(leadingPattern, '') + + const extractorNodeId = `${toolNodeId}_ext_${paramKey}` + const { getNodes, setNodes } = reactFlowStore.getState() + const nodes = getNodes() + const extractorNode = nodes.find(node => node.id === extractorNodeId) as WorkflowNode | undefined + if (!extractorNode?.data?.prompt_template) + return + + const currentPromptText = getUserPromptText(extractorNode.data.prompt_template) + const shouldUpdate = !hasUserPromptTemplate(extractorNode.data.prompt_template) + || currentPromptText !== promptText + if (!shouldUpdate) + return + + const nextPromptTemplate = buildPromptTemplateWithText(extractorNode.data.prompt_template, promptText) + const nextNodes = nodes.map((node) => { + if (node.id !== extractorNodeId) + return node + return { + ...node, + data: { + ...node.data, + prompt_template: nextPromptTemplate, + }, + } + }) + setNodes(nextNodes) + handleSyncWorkflowDraft() + }, [handleSyncWorkflowDraft, paramKey, reactFlowStore, toolNodeId]) + + const applyMentionGraphNodeData = useCallback((payload: { + extractorNodeId: string + mentionNodeData: Partial + valueText: string + detectAgentFromText: (text: string) => DetectedAgent | null + }) => { + const { extractorNodeId, mentionNodeData, valueText, detectAgentFromText } = payload + if (!toolNodeId) + return + const hasPromptTemplate = Array.isArray(mentionNodeData.prompt_template) + ? mentionNodeData.prompt_template.length > 0 + : Boolean(mentionNodeData.prompt_template) + const nextData: Partial = {} + if (mentionNodeData.title) + nextData.title = mentionNodeData.title + if (mentionNodeData.desc) + nextData.desc = mentionNodeData.desc + if (mentionNodeData.model && (mentionNodeData.model.provider || mentionNodeData.model.name)) + nextData.model = mentionNodeData.model + if (hasPromptTemplate) + nextData.prompt_template = mentionNodeData.prompt_template + if (typeof mentionNodeData.structured_output_enabled === 'boolean') + nextData.structured_output_enabled = mentionNodeData.structured_output_enabled + if (mentionNodeData.structured_output?.schema) + nextData.structured_output = mentionNodeData.structured_output + if (mentionNodeData.context) + nextData.context = mentionNodeData.context + if (mentionNodeData.vision) + nextData.vision = mentionNodeData.vision + if (Object.prototype.hasOwnProperty.call(mentionNodeData, 'memory')) + nextData.memory = mentionNodeData.memory + + if (Object.keys(nextData).length === 0) + return + + const { getNodes, setNodes } = reactFlowStore.getState() + const currentNodes = getNodes() + const hasExtractorNode = currentNodes.some(node => node.id === extractorNodeId) + if (!hasExtractorNode) + return + + const nextNodes = currentNodes.map((node) => { + if (node.id !== extractorNodeId) + return node + return { + ...node, + data: { + ...node.data, + ...nextData, + type: BlockEnum.LLM, + parent_node_id: toolNodeId, + }, + } + }) + setNodes(nextNodes) + handleSyncWorkflowDraft() + syncExtractorPromptFromText(valueText, detectAgentFromText) + }, [handleSyncWorkflowDraft, reactFlowStore, syncExtractorPromptFromText, toolNodeId]) + + const requestMentionGraph = useCallback(async (payload: { + agentId: string + extractorNodeId: string + valueText: string + detectAgentFromText: (text: string) => DetectedAgent | null + }) => { + if (!toolNodeId || !paramKey) + return + if (!configsMap?.flowId || configsMap.flowType !== FlowType.appFlow) + return + const parameterSchema = resolveMentionParameterSchema(paramKey) + try { + const response = await fetchMentionGraph(configsMap.flowType, configsMap.flowId, { + parent_node_id: toolNodeId, + parameter_key: paramKey, + context_source: [payload.agentId, 'context'], + parameter_schema: parameterSchema, + }) + const mentionNode = response?.graph?.nodes?.find(node => node.id === payload.extractorNodeId) + const mentionNodeData = mentionNode?.data as Partial | undefined + if (!mentionNodeData) + return + applyMentionGraphNodeData({ + extractorNodeId: payload.extractorNodeId, + mentionNodeData, + valueText: payload.valueText, + detectAgentFromText: payload.detectAgentFromText, + }) + } + catch { + } + }, [applyMentionGraphNodeData, configsMap?.flowId, configsMap?.flowType, paramKey, resolveMentionParameterSchema, toolNodeId]) + + return { + assembleExtractorNodeId, + ensureExtractorNode, + ensureAssembleExtractorNode, + removeExtractorNode, + syncExtractorPromptFromText, + requestMentionGraph, + } +} diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 890aaf0bc2..4d751a5a51 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -1,15 +1,11 @@ +import type { DetectedAgent } from './hooks' import type { AgentNode, WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types' import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types' -import type { ToolParameter } from '@/app/components/tools/types' import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types' import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types' -import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types' -import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { CommonNodeType, NodeOutPutVar, - PromptItem, - PromptTemplateItem, ValueSelector, Node as WorkflowNode, } from '@/app/components/workflow/types' @@ -27,42 +23,29 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store' import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/_base/types' import { Type } from '@/app/components/workflow/nodes/llm/types' import { useStore } from '@/app/components/workflow/store' -import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '@/app/components/workflow/types' -import { generateNewNode, getNodeCustomTypeByNodeDataType, mergeNodeDefaultData } from '@/app/components/workflow/utils' +import { BlockEnum } from '@/app/components/workflow/types' import { useGetLanguage } from '@/context/i18n' import { useStrategyProviders } from '@/service/use-strategy' -import { fetchMentionGraph } from '@/service/workflow' -import { FlowType } from '@/types/common' import { cn } from '@/utils/classnames' import ContextGenerateModal from '../context-generate-modal' import SubGraphModal from '../sub-graph-modal' -import AgentHeaderBar from './agent-header-bar' -import Placeholder from './placeholder' +import { AgentHeaderBar, Placeholder } from './components' +import { + AGENT_CONTEXT_VAR_PATTERN, + buildAssemblePlaceholder, + getAgentNodeIdFromContextVar, + useMixedVariableExtractor, +} from './hooks' -/** - * Matches agent context variable syntax: {{@nodeId.context@}} - * Example: {{@agent-123.context@}} - */ -const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g -const AGENT_CONTEXT_VAR_PREFIX = '{{@' -const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' -const getAgentNodeIdFromContextVar = (placeholder: string) => { - if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX)) - return '' - return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length) -} +type WorkflowNodesMap = NonNullable -const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => { - if (!toolNodeId || !paramKey) - return '' - return `{{#${toolNodeId}_ext_${paramKey}.result#}}` -} const DEFAULT_MENTION_CONFIG: MentionConfig = { extractor_node_id: '', output_selector: [], null_strategy: 'use_default', default_value: '', } + type AgentCheckValidContext = { provider?: StrategyPluginDetail strategy?: StrategyDetail @@ -70,78 +53,6 @@ type AgentCheckValidContext = { isReadyForCheckValid: boolean } -type WorkflowNodesMap = NonNullable - -const resolvePromptText = (item?: PromptItem) => { - if (!item) - return '' - if (item.edition_type === EditionType.jinja2) - return item.jinja2_text || item.text || '' - return item.text || '' -} - -const getUserPromptText = (promptTemplate?: PromptTemplateItem[] | PromptItem) => { - if (!promptTemplate) - return '' - if (Array.isArray(promptTemplate)) { - const userPrompt = promptTemplate.find( - item => !isPromptMessageContext(item) && item.role === PromptRole.user, - ) as PromptItem | undefined - return resolvePromptText(userPrompt) - } - return resolvePromptText(promptTemplate) -} - -const hasUserPromptTemplate = (promptTemplate: PromptTemplateItem[] | PromptItem) => { - if (!Array.isArray(promptTemplate)) - return true - return promptTemplate.some(item => !isPromptMessageContext(item) && item.role === PromptRole.user) -} - -const applyPromptText = (item: PromptItem, text: string) => { - if (item.edition_type === EditionType.jinja2) { - return { - ...item, - text, - jinja2_text: text, - } - } - return { - ...item, - text, - } -} - -const buildPromptTemplateWithText = (promptTemplate: PromptTemplateItem[] | PromptItem, text: string) => { - if (!Array.isArray(promptTemplate)) - return applyPromptText(promptTemplate as PromptItem, text) - - const userIndex = promptTemplate.findIndex( - item => !isPromptMessageContext(item) && item.role === PromptRole.user, - ) - if (userIndex >= 0) { - return promptTemplate.map((item, index) => { - if (index !== userIndex || isPromptMessageContext(item)) - return item - return applyPromptText(item as PromptItem, text) - }) as PromptTemplateItem[] - } - - const useJinja = promptTemplate.some( - item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2, - ) - const defaultUserPrompt: PromptItem = useJinja - ? { - role: PromptRole.user, - text, - jinja2_text: text, - edition_type: EditionType.jinja2, - } - : { role: PromptRole.user, text } - - return [...promptTemplate, defaultUserPrompt] as PromptTemplateItem[] -} - type MixedVariableTextInputProps = { readOnly?: boolean nodesOutputVars?: NodeOutPutVar[] @@ -214,138 +125,24 @@ const MixedVariableTextInput = ({ }, {} as Record) }, [nodes]) - const resolveMentionParameterSchema = useCallback((key: string) => { - if (!toolNodeId) { - return { - name: key, - type: Type.string, - description: '', - } - } - const toolNodeData = nodesById[toolNodeId]?.data as { paramSchemas?: ToolParameter[] } | undefined - const paramSchema = toolNodeData?.paramSchemas?.find(param => param.name === key) - const description = paramSchema?.llm_description - || paramSchema?.human_description?.[language] - || paramSchema?.human_description?.en_US - || '' - return { - name: paramSchema?.name || key, - type: paramSchema?.type || Type.string, - description, - } - }, [language, nodesById, toolNodeId]) - - const assembleExtractorNodeId = useMemo(() => { - if (!toolNodeId || !paramKey) - return '' - return `${toolNodeId}_ext_${paramKey}` - }, [paramKey, toolNodeId]) - - const ensureExtractorNode = useCallback((payload: { - extractorNodeId: string - nodeType: BlockEnum - data: Partial - }) => { - if (!toolNodeId) - return null - const metaDefault = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial | undefined - const appDefault = nodesDefaultConfigs?.[payload.nodeType] as Partial | undefined - if (!metaDefault && !appDefault) - return null - - const { getNodes, setNodes } = reactFlowStore.getState() - const currentNodes = getNodes() - const existingNode = currentNodes.find(node => node.id === payload.extractorNodeId) - const shouldReplace = existingNode && existingNode.data.type !== payload.nodeType - if (!existingNode || shouldReplace) { - const nextNodes = shouldReplace - ? currentNodes.filter(node => node.id !== payload.extractorNodeId) - : currentNodes - const mergedData = mergeNodeDefaultData({ - nodeType: payload.nodeType, - metaDefault, - appDefault, - overrideData: payload.data, - }) - const resolvedTitle = mergedData.title ?? metaDefault?.title ?? appDefault?.title ?? '' - const resolvedDesc = mergedData.desc ?? metaDefault?.desc ?? appDefault?.desc ?? '' - const { newNode } = generateNewNode({ - id: payload.extractorNodeId, - type: getNodeCustomTypeByNodeDataType(payload.nodeType), - data: { - ...mergedData, - type: payload.nodeType, - title: resolvedTitle, - desc: resolvedDesc, - parent_node_id: toolNodeId, - }, - position: { - x: 0, - y: 0, - }, - hidden: true, - }) - setNodes([...nextNodes, newNode]) - handleSyncWorkflowDraft() - return newNode - } - - return existingNode - }, [handleSyncWorkflowDraft, nodesDefaultConfigs, nodesMetaDataMap, reactFlowStore, toolNodeId]) - - const ensureAssembleExtractorNode = useCallback(() => { - if (!assembleExtractorNodeId) - return '' - const extractorNode = ensureExtractorNode({ - extractorNodeId: assembleExtractorNodeId, - nodeType: BlockEnum.Code, - data: { - outputs: { - result: { - type: VarType.string, - children: null, - }, - }, - }, - }) - if (!extractorNode) - return '' - if (extractorNode.data.type !== BlockEnum.Code) - return assembleExtractorNodeId - - const outputs = (extractorNode.data as CodeNodeType).outputs || {} - const resultOutput = outputs.result - if (!resultOutput || resultOutput.type !== VarType.string) { - const { getNodes, setNodes } = reactFlowStore.getState() - const currentNodes = getNodes() - const nextOutputs = { - ...outputs, - result: { - type: VarType.string, - children: null, - }, - } - setNodes(currentNodes.map((node) => { - if (node.id !== assembleExtractorNodeId) - return node - return { - ...node, - data: { - ...node.data, - outputs: nextOutputs, - }, - } - })) - handleSyncWorkflowDraft() - } - - return assembleExtractorNodeId - }, [assembleExtractorNodeId, ensureExtractorNode, handleSyncWorkflowDraft, reactFlowStore]) - - type DetectedAgent = { - nodeId: string - name: string - } + const { + assembleExtractorNodeId, + ensureExtractorNode, + ensureAssembleExtractorNode, + removeExtractorNode, + syncExtractorPromptFromText, + requestMentionGraph, + } = useMixedVariableExtractor({ + toolNodeId, + paramKey, + language, + nodesById, + nodesDefaultConfigs, + reactFlowStore, + nodesMetaDataMap, + handleSyncWorkflowDraft, + configsMap, + }) const detectAgentFromText = useCallback((text: string): DetectedAgent | null => { if (!text) @@ -371,7 +168,6 @@ const MixedVariableTextInput = ({ return detectAgentFromText(value) }, [detectAgentFromText, value]) - // Check if value only contains agent context variable without other user input const isOnlyAgentContext = useMemo(() => { if (!detectedAgentFromValue || !value) return false @@ -453,152 +249,6 @@ const MixedVariableTextInput = ({ return getNodeWarning(nodesById[assembleExtractorNodeId]) }, [assembleExtractorNodeId, getNodeWarning, isAssembleValue, nodesById]) - const syncExtractorPromptFromText = useCallback((text: string) => { - if (!toolNodeId || !paramKey) - return - - const detectedAgent = detectAgentFromText(text) - if (!detectedAgent) - return - - const escapedAgentId = detectedAgent.nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const leadingPattern = new RegExp(`^\\{\\{@${escapedAgentId}\\.context@\\}\\}`) - const promptText = text.replace(leadingPattern, '') - - const extractorNodeId = `${toolNodeId}_ext_${paramKey}` - const { getNodes, setNodes } = reactFlowStore.getState() - const nodes = getNodes() - const extractorNode = nodes.find(node => node.id === extractorNodeId) as WorkflowNode | undefined - if (!extractorNode?.data?.prompt_template) - return - - const currentPromptText = getUserPromptText(extractorNode.data.prompt_template) - const shouldUpdate = !hasUserPromptTemplate(extractorNode.data.prompt_template) - || currentPromptText !== promptText - if (!shouldUpdate) - return - - const nextPromptTemplate = buildPromptTemplateWithText(extractorNode.data.prompt_template, promptText) - const nextNodes = nodes.map((node) => { - if (node.id !== extractorNodeId) - return node - return { - ...node, - data: { - ...node.data, - prompt_template: nextPromptTemplate, - }, - } - }) - setNodes(nextNodes) - handleSyncWorkflowDraft() - }, [detectAgentFromText, handleSyncWorkflowDraft, paramKey, reactFlowStore, toolNodeId]) - - const applyMentionGraphNodeData = useCallback((payload: { - extractorNodeId: string - mentionNodeData: Partial - valueText: string - }) => { - const { extractorNodeId, mentionNodeData, valueText } = payload - if (!toolNodeId) - return - const hasPromptTemplate = Array.isArray(mentionNodeData.prompt_template) - ? mentionNodeData.prompt_template.length > 0 - : Boolean(mentionNodeData.prompt_template) - const nextData: Partial = {} - if (mentionNodeData.title) - nextData.title = mentionNodeData.title - if (mentionNodeData.desc) - nextData.desc = mentionNodeData.desc - if (mentionNodeData.model && (mentionNodeData.model.provider || mentionNodeData.model.name)) - nextData.model = mentionNodeData.model - if (hasPromptTemplate) - nextData.prompt_template = mentionNodeData.prompt_template - if (typeof mentionNodeData.structured_output_enabled === 'boolean') - nextData.structured_output_enabled = mentionNodeData.structured_output_enabled - if (mentionNodeData.structured_output?.schema) - nextData.structured_output = mentionNodeData.structured_output - if (mentionNodeData.context) - nextData.context = mentionNodeData.context - if (mentionNodeData.vision) - nextData.vision = mentionNodeData.vision - if (Object.prototype.hasOwnProperty.call(mentionNodeData, 'memory')) - nextData.memory = mentionNodeData.memory - - if (Object.keys(nextData).length === 0) - return - - const { getNodes, setNodes } = reactFlowStore.getState() - const currentNodes = getNodes() - const hasExtractorNode = currentNodes.some(node => node.id === extractorNodeId) - if (!hasExtractorNode) - return - - const nextNodes = currentNodes.map((node) => { - if (node.id !== extractorNodeId) - return node - return { - ...node, - data: { - ...node.data, - ...nextData, - type: BlockEnum.LLM, - parent_node_id: toolNodeId, - }, - } - }) - setNodes(nextNodes) - handleSyncWorkflowDraft() - syncExtractorPromptFromText(valueText) - }, [handleSyncWorkflowDraft, reactFlowStore, syncExtractorPromptFromText, toolNodeId]) - - const requestMentionGraph = useCallback(async (payload: { - agentId: string - extractorNodeId: string - valueText: string - }) => { - if (!toolNodeId || !paramKey) - return - if (!configsMap?.flowId || configsMap.flowType !== FlowType.appFlow) - return - const parameterSchema = resolveMentionParameterSchema(paramKey) - try { - const response = await fetchMentionGraph(configsMap.flowType, configsMap.flowId, { - parent_node_id: toolNodeId, - parameter_key: paramKey, - context_source: [payload.agentId, 'context'], - parameter_schema: parameterSchema, - }) - const mentionNode = response?.graph?.nodes?.find(node => node.id === payload.extractorNodeId) - const mentionNodeData = mentionNode?.data as Partial | undefined - if (!mentionNodeData) - return - applyMentionGraphNodeData({ - extractorNodeId: payload.extractorNodeId, - mentionNodeData, - valueText: payload.valueText, - }) - } - catch { - - } - }, [applyMentionGraphNodeData, configsMap?.flowId, configsMap?.flowType, paramKey, resolveMentionParameterSchema, toolNodeId]) - - const removeExtractorNode = useCallback(() => { - if (!toolNodeId || !paramKey) - return - - const extractorNodeId = `${toolNodeId}_ext_${paramKey}` - const { getNodes, setNodes } = reactFlowStore.getState() - const nodes = getNodes() - const hasExtractorNode = nodes.some(node => node.id === extractorNodeId) - if (!hasExtractorNode) - return - - setNodes(nodes.filter(node => node.id !== extractorNodeId)) - handleSyncWorkflowDraft() - }, [handleSyncWorkflowDraft, paramKey, reactFlowStore, toolNodeId]) - const handleAgentRemove = useCallback(() => { const agentNodeId = detectedAgentFromValue?.nodeId if (!agentNodeId || !onChange) @@ -618,7 +268,6 @@ const MixedVariableTextInput = ({ if (!onChange) return - // compute words after the latest '@' and delete them const valueWithoutTrigger = value.replace(/@[^@\n]*$/, '') const newValue = `{{@${agent.id}.context@}}${valueWithoutTrigger}` @@ -651,15 +300,16 @@ const MixedVariableTextInput = ({ output_selector: paramKey ? ['structured_output', paramKey] : [], } onChange(newValue, VarKindTypeEnum.mention, mentionConfigWithOutputSelector) - syncExtractorPromptFromText(newValue) + syncExtractorPromptFromText(newValue, detectAgentFromText) if (extractorNodeId) { void requestMentionGraph({ agentId: agent.id, extractorNodeId, valueText: newValue, + detectAgentFromText, }) } - }, [ensureExtractorNode, onChange, paramKey, requestMentionGraph, syncExtractorPromptFromText, toolNodeId, value]) + }, [detectAgentFromText, ensureExtractorNode, onChange, paramKey, requestMentionGraph, syncExtractorPromptFromText, toolNodeId, value]) const handleAssembleSelect = useCallback((): ValueSelector | null => { if (!toolNodeId || !paramKey || !assemblePlaceholder) @@ -747,7 +397,7 @@ const MixedVariableTextInput = ({ onChange={(text) => { const hasPlaceholder = new RegExp(AGENT_CONTEXT_VAR_PATTERN.source).test(text) if (hasPlaceholder) - syncExtractorPromptFromText(text) + syncExtractorPromptFromText(text, detectAgentFromText) if (detectedAgentFromValue && !hasPlaceholder) { removeExtractorNode() onChange?.(text, VarKindTypeEnum.mixed, null)