refactor: Refactor mixed-variable-text-input to extract hooks

This commit is contained in:
zhsama
2026-01-21 17:55:34 +08:00
parent 1fcff5f8d1
commit d7ccea8ac5
6 changed files with 483 additions and 384 deletions

View File

@ -0,0 +1,2 @@
export { default as AgentHeaderBar } from './agent-header-bar'
export { default as Placeholder } from './placeholder'

View File

@ -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 (

View File

@ -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'

View File

@ -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<BlockEnum, {
defaultValue?: Partial<LLMNodeType | CodeNodeType>
checkValid?: (data: CommonNodeType, t: (key: string, options?: Record<string, unknown>) => string, moreData?: unknown) => { isValid: boolean, errorMessage?: string }
}>
type ConfigsMap = {
flowId?: string
flowType?: FlowType
}
type ExtractorNodePayload = {
extractorNodeId: string
nodeType: BlockEnum
data: Partial<LLMNodeType | CodeNodeType>
}
export type DetectedAgent = {
nodeId: string
name: string
}
type ReactFlowStoreApi = {
getState: () => ReactFlowState
}
export type UseMixedVariableExtractorOptions = {
toolNodeId?: string
paramKey: string
language: string
nodesById: Record<string, WorkflowNode>
nodesMetaDataMap?: NodesMetaDataMap
nodesDefaultConfigs?: Record<string, Partial<LLMNodeType | CodeNodeType>>
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<LLMNodeType | CodeNodeType> | undefined
const appDefault = nodesDefaultConfigs?.[payload.nodeType] as Partial<LLMNodeType | CodeNodeType> | 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<LLMNodeType> | 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<LLMNodeType>
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<LLMNodeType> = {}
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<LLMNodeType> | 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,
}
}

View File

@ -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<WorkflowVariableBlockType['workflowNodesMap']>
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<WorkflowVariableBlockType['workflowNodesMap']>
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<string, WorkflowNode>)
}, [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<LLMNodeType | CodeNodeType>
}) => {
if (!toolNodeId)
return null
const metaDefault = nodesMetaDataMap?.[payload.nodeType]?.defaultValue as Partial<LLMNodeType | CodeNodeType> | undefined
const appDefault = nodesDefaultConfigs?.[payload.nodeType] as Partial<LLMNodeType | CodeNodeType> | 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<LLMNodeType> | 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<LLMNodeType>
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<LLMNodeType> = {}
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<LLMNodeType> | 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)