import { addEdge, MarkerType } from '@xyflow/react'; import defaultWorkflowData from '../../workflows/rwb1_workflow.json'; import { paraphraseText } from './api.js'; import { normalizeComponentTemplates } from './componentLibrary.js'; import { createBrowserId } from './ids.js'; import { COMPONENT_INPUT_TYPE, COMPONENT_NODE_TYPE, COMPONENT_OUTPUT_TYPE, createComponentSubgraph, createNodeInstance, getNodeDefinition, getNodeHandles, hydrateNode, isComponentBoundaryType, isComponentNodeType, stripRuntimeFromNode, } from './nodeRegistry.js'; export const FLOW_FORMAT = 'nodes-ui-flow'; export const FLOW_VERSION = 2; export const AUTOSAVE_KEY = 'workflow_autosave_react_flow_v3'; export const LEGACY_AUTOSAVE_KEY = 'workflow_autosave'; export const DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1, }; export const DEFAULT_ASSISTANT_ROLE = 'Ты компьютерный ИИ-ассистент. Отвечай кратко, естественно, одним живым абзацем, без Markdown, списков, заголовков и нумерации.'; function normalizeEdge(edge) { return { ...edge, id: String(edge.id), source: String(edge.source), target: String(edge.target), type: edge.type === 'smoothstep' ? 'default' : edge.type || 'default', markerEnd: edge.markerEnd || { type: MarkerType.ArrowClosed }, }; } function sanitizeViewport(viewport) { return viewport && typeof viewport === 'object' ? viewport : null; } export function hydrateGraph(graph) { return { nodes: (graph?.nodes || []).map((node) => hydrateNode(node)), edges: (graph?.edges || []).map((edge) => normalizeEdge(edge)), viewport: sanitizeViewport(graph?.viewport), }; } function serializeGraph(graph) { return { nodes: (graph?.nodes || []).map((node) => stripRuntimeFromNode(node)), edges: (graph?.edges || []).map((edge) => ({ id: edge.id, source: edge.source, sourceHandle: edge.sourceHandle || null, target: edge.target, targetHandle: edge.targetHandle || null, type: edge.type === 'smoothstep' ? 'default' : edge.type || 'default', })), viewport: sanitizeViewport(graph?.viewport), }; } export function createWorkflowDocument(rootGraph, componentTemplates = [], settings = {}) { return { format: FLOW_FORMAT, version: FLOW_VERSION, rootGraph: hydrateGraph(rootGraph), componentTemplates: normalizeComponentTemplates(componentTemplates), settings: settings && typeof settings === 'object' ? settings : {}, }; } export function getGraphAtPath(document, path = []) { let graph = document.rootGraph; for (const nodeId of path) { const node = graph.nodes.find((candidate) => candidate.id === nodeId && isComponentNodeType(candidate.type)); if (!node) { break; } graph = node.data.subgraph || createComponentSubgraph(); } return graph; } export function updateGraphAtPath(document, path, updater) { function updateGraph(graph, remainingPath) { if (remainingPath.length === 0) { return hydrateGraph(updater(graph)); } const [currentNodeId, ...rest] = remainingPath; return hydrateGraph({ ...graph, nodes: graph.nodes.map((node) => { if (node.id !== currentNodeId || !isComponentNodeType(node.type)) { return node; } const nextSubgraph = updateGraph(node.data.subgraph || createComponentSubgraph(), rest); return hydrateNode({ ...node, data: { ...node.data, subgraph: nextSubgraph, }, }); }), }); } return { ...document, rootGraph: updateGraph(document.rootGraph, path), }; } export function createBlankWorkflow() { const startNode = createNodeInstance('basic/start', { x: 40, y: 80 }); const questionNode = createNodeInstance('basic/question', { x: 360, y: 80 }); const dialogNode = createNodeInstance('basic/dialog', { x: 760, y: 80 }); return createWorkflowDocument({ nodes: [startNode, questionNode, dialogNode], edges: [ { id: createBrowserId('edge'), source: startNode.id, sourceHandle: 'dialog', target: questionNode.id, targetHandle: 'dialog-in', type: 'default', markerEnd: { type: MarkerType.ArrowClosed }, }, { id: createBrowserId('edge'), source: questionNode.id, sourceHandle: 'dialog', target: dialogNode.id, targetHandle: 'dialog', type: 'default', markerEnd: { type: MarkerType.ArrowClosed }, }, ], viewport: null, }); } export function createDefaultWorkflow() { return parseWorkflowData(defaultWorkflowData); } function convertLegacyNode(legacyNode) { const type = legacyNode.type; const data = {}; if (type === 'basic/text') { data.text = legacyNode.properties?.text || ''; data.width = legacyNode.size?.[0]; data.height = legacyNode.size?.[1]; } if (type === 'basic/classifier') { data.options = legacyNode.properties?.options || ''; data.width = legacyNode.size?.[0]; } if (type === 'basic/request' || type === 'basic/randomlist') { data.width = legacyNode.size?.[0]; } if (type === 'basic/dialog') { data.width = legacyNode.size?.[0]; data.height = legacyNode.size?.[1]; } if (type === 'basic/script') { data.width = legacyNode.size?.[0]; data.hasScriptOutput = Boolean(legacyNode.properties?.hasScriptOutput) || Boolean((legacyNode.outputs || []).some((output) => output.name?.toLowerCase().includes('script'))); data.entries = (legacyNode.inputs || []) .slice(1) .filter((input) => input?.name?.startsWith('user') || input?.name?.startsWith('character')) .map((input, index) => ({ id: `${input.name?.startsWith('user') ? 'user' : 'character'}-${index}`, kind: input.name?.startsWith('user') ? 'user' : 'character', })); } if (type === 'basic/ifelse') { data.width = legacyNode.size?.[0]; data.conditions = (legacyNode.properties?.conditions || [{ keyword: '' }]).map((condition, index) => ({ id: `condition-${index}`, keyword: condition.keyword || '', })); } return hydrateNode({ id: String(legacyNode.id), type, position: { x: legacyNode.pos?.[0] || 0, y: legacyNode.pos?.[1] || 0, }, data, }); } function convertLiteGraphWorkflow(data) { const supportedNodes = (data.nodes || []).filter((node) => getNodeDefinition(node.type)); const convertedNodes = supportedNodes.map(convertLegacyNode); const nodeLookup = new Map(convertedNodes.map((node) => [String(node.id), node])); const edges = (data.links || []) .filter(Boolean) .map((link) => { const [legacyId, originId, originSlot, targetId, targetSlot] = link; const sourceNode = nodeLookup.get(String(originId)); const targetNode = nodeLookup.get(String(targetId)); if (!sourceNode || !targetNode) { return null; } const sourceHandles = getNodeHandles(sourceNode.type, sourceNode.data).outputs; const targetHandles = getNodeHandles(targetNode.type, targetNode.data).inputs; const sourceHandle = sourceHandles[originSlot]?.id || sourceHandles[0]?.id || null; const targetHandle = targetHandles[targetSlot]?.id || targetHandles[0]?.id || null; return normalizeEdge({ id: String(legacyId), source: sourceNode.id, sourceHandle, target: targetNode.id, targetHandle, }); }) .filter(Boolean); return { nodes: convertedNodes, edges, viewport: null, }; } export function parseWorkflowData(data) { if (!data || typeof data !== 'object') { throw new Error('Invalid workflow file.'); } if (data.format === FLOW_FORMAT && data.rootGraph) { return createWorkflowDocument(data.rootGraph, data.componentTemplates || [], data.settings || {}); } if (data.format === FLOW_FORMAT && Array.isArray(data.nodes) && Array.isArray(data.edges)) { return createWorkflowDocument({ nodes: data.nodes, edges: data.edges, viewport: data.viewport || null, }, data.componentTemplates || [], data.settings || {}); } if (Array.isArray(data.nodes) && Array.isArray(data.links)) { return createWorkflowDocument(convertLiteGraphWorkflow(data)); } throw new Error('Unsupported workflow format.'); } export function serializeWorkflow(document) { return { format: FLOW_FORMAT, version: FLOW_VERSION, rootGraph: serializeGraph(document.rootGraph), componentTemplates: normalizeComponentTemplates(document.componentTemplates || []), settings: document.settings && typeof document.settings === 'object' ? document.settings : {}, }; } function buildEdgeDefaults(connection) { return { ...connection, id: createBrowserId('edge'), type: 'default', markerEnd: { type: MarkerType.ArrowClosed }, }; } function hasPath(sourceId, targetId, edges, visited = new Set()) { if (sourceId === targetId) { return true; } if (visited.has(sourceId)) { return false; } visited.add(sourceId); return edges .filter((edge) => edge.source === sourceId) .some((edge) => hasPath(edge.target, targetId, edges, visited)); } export function isValidConnection(connection, nodes, edges) { if (!connection.source || !connection.target) { return false; } if (connection.source === connection.target) { return false; } const sourceNode = nodes.find((node) => node.id === connection.source); const targetNode = nodes.find((node) => node.id === connection.target); if (!sourceNode || !targetNode) { return false; } const sourceHandles = getNodeHandles(sourceNode.type, sourceNode.data).outputs; const targetHandles = getNodeHandles(targetNode.type, targetNode.data).inputs; const sourceHandle = sourceHandles.find((handle) => handle.id === connection.sourceHandle); const targetHandle = targetHandles.find((handle) => handle.id === connection.targetHandle); if (!sourceHandle || !targetHandle) { return false; } const sameConnection = edges.some( (edge) => edge.source === connection.source && edge.sourceHandle === connection.sourceHandle && edge.target === connection.target && edge.targetHandle === connection.targetHandle, ); if (sameConnection) { return false; } const outputType = sourceHandle.dataType; const inputType = targetHandle.dataType; const typesCompatible = outputType === '*' || inputType === '*' || outputType === inputType; if (!typesCompatible) { return false; } return true; } export function connectEdge(connection, edges) { return addEdge(buildEdgeDefaults(connection), edges); } function getNodeSize(node) { return { width: node.style?.width || node.data?.width || 260, height: node.data?.height || 180, }; } function getFirstOutputHandleId(node) { return getNodeHandles(node.type, node.data).outputs[0]?.id || 'output'; } function getHandleLabel(node, direction, handleId, fallback) { const handles = getNodeHandles(node.type, node.data)[direction] || []; const handle = handles.find((candidate) => candidate.id === handleId) || handles[0]; return handle?.label || handleId || fallback; } function createComponentHandleId(baseId, index, total) { return total === 1 ? baseId : `${baseId}-${index}`; } function getComponentBoundaryExternalHandleId(node, fallbackId, index = 0) { const rawHandleId = node?.data?.externalHandleId; if (typeof rawHandleId === 'string' && rawHandleId.trim()) { return rawHandleId.trim(); } return index === 0 ? fallbackId : `${fallbackId}-${index}`; } function collectComponentOutputs(graph) { const outputNodes = (graph.nodes || []).filter((node) => node.type === COMPONENT_OUTPUT_TYPE); if (outputNodes.length === 0) { return { outputs: { output: null }, lastOutput: null, }; } const outputs = Object.fromEntries( outputNodes.map((node, index) => [ getComponentBoundaryExternalHandleId(node, 'output', index), node.data?.runtime?.value ?? null, ]), ); const values = Object.values(outputs); return { outputs, lastOutput: values.length === 1 ? values[0] : outputs, }; } function cloneEdgeForSubgraph(edge) { return normalizeEdge({ id: createBrowserId('edge'), source: edge.source, sourceHandle: edge.sourceHandle || null, target: edge.target, targetHandle: edge.targetHandle || null, type: edge.type === 'smoothstep' ? 'default' : edge.type || 'default', }); } function createExternalEdge(source, sourceHandle, target, targetHandle) { return normalizeEdge({ id: createBrowserId('edge'), source, sourceHandle, target, targetHandle, type: 'default', }); } export function createComponentFromSelection(graph) { const selectedNodes = graph.nodes.filter((node) => node.selected); if (selectedNodes.length === 0) { return { error: 'Select at least one node to create a component.' }; } if (selectedNodes.some((node) => isComponentBoundaryType(node.type))) { return { error: 'Component Input/Output nodes cannot be grouped into another component.' }; } const selectedIds = new Set(selectedNodes.map((node) => node.id)); const internalEdges = graph.edges.filter( (edge) => selectedIds.has(edge.source) && selectedIds.has(edge.target), ); const incomingExternalEdges = graph.edges.filter( (edge) => !selectedIds.has(edge.source) && selectedIds.has(edge.target), ); const outgoingExternalEdges = graph.edges.filter( (edge) => selectedIds.has(edge.source) && !selectedIds.has(edge.target), ); const bounds = selectedNodes.reduce( (acc, node) => { const { width, height } = getNodeSize(node); return { minX: Math.min(acc.minX, node.position.x), minY: Math.min(acc.minY, node.position.y), maxX: Math.max(acc.maxX, node.position.x + width), maxY: Math.max(acc.maxY, node.position.y + height), }; }, { minX: Number.POSITIVE_INFINITY, minY: Number.POSITIVE_INFINITY, maxX: Number.NEGATIVE_INFINITY, maxY: Number.NEGATIVE_INFINITY, }, ); const offsetX = 180 - bounds.minX; const offsetY = 60 - bounds.minY; const nodeLookup = new Map(graph.nodes.map((node) => [node.id, node])); const outputX = Math.max(520, bounds.maxX - bounds.minX + 240); const portY = (index) => 80 + index * 120; const inputPorts = incomingExternalEdges.length > 0 ? incomingExternalEdges.map((edge, index) => { const targetNode = nodeLookup.get(edge.target); const label = targetNode ? getHandleLabel(targetNode, 'inputs', edge.targetHandle || null, edge.targetHandle || 'input') : edge.targetHandle || 'input'; const externalHandleId = createComponentHandleId('input', index, incomingExternalEdges.length); return { edge, externalHandleId, node: createNodeInstance(COMPONENT_INPUT_TYPE, { x: 30, y: portY(index) }, { data: { title: `Input: ${label}`, externalHandleId, externalLabel: label, }, }), }; }) : [ { edge: null, externalHandleId: 'input', node: createNodeInstance(COMPONENT_INPUT_TYPE, { x: 30, y: 110 }, { data: { externalHandleId: 'input', externalLabel: 'input', }, }), }, ]; const outputPorts = outgoingExternalEdges.length > 0 ? outgoingExternalEdges.map((edge, index) => { const sourceNode = nodeLookup.get(edge.source); const label = sourceNode ? getHandleLabel(sourceNode, 'outputs', edge.sourceHandle || null, edge.sourceHandle || 'output') : edge.sourceHandle || 'output'; const externalHandleId = createComponentHandleId('output', index, outgoingExternalEdges.length); return { edge, externalHandleId, node: createNodeInstance(COMPONENT_OUTPUT_TYPE, { x: outputX, y: portY(index) }, { data: { title: `Output: ${label}`, externalHandleId, externalLabel: label, }, }), }; }) : [ { edge: null, externalHandleId: 'output', node: createNodeInstance(COMPONENT_OUTPUT_TYPE, { x: outputX, y: 110 }, { data: { externalHandleId: 'output', externalLabel: 'output', }, }), }, ]; const normalizedSelectedNodes = selectedNodes.map((node) => hydrateNode({ ...node, selected: false, position: { x: node.position.x + offsetX, y: node.position.y + offsetY, }, }), ); const subgraphEdges = internalEdges.map((edge) => cloneEdgeForSubgraph(edge)); inputPorts.forEach((port) => { if (!port.edge) { return; } subgraphEdges.push( createExternalEdge(port.node.id, 'output', port.edge.target, port.edge.targetHandle || null), ); }); if (outgoingExternalEdges.length > 0) { outputPorts.forEach((port) => { subgraphEdges.push( createExternalEdge(port.edge.source, port.edge.sourceHandle || null, port.node.id, 'input'), ); }); } else { const sinkNodes = normalizedSelectedNodes.filter((node) => { if (isComponentBoundaryType(node.type)) { return false; } const hasOutgoingInternalEdge = internalEdges.some((edge) => edge.source === node.id); const hasOutputs = getNodeHandles(node.type, node.data).outputs.length > 0; return !hasOutgoingInternalEdge && hasOutputs; }); if (sinkNodes.length === 1) { subgraphEdges.push( createExternalEdge( sinkNodes[0].id, getFirstOutputHandleId(sinkNodes[0]), outputPorts[0].node.id, 'input', ), ); } } const componentNode = createNodeInstance( COMPONENT_NODE_TYPE, { x: bounds.minX, y: bounds.minY }, { data: { title: 'Component', description: '', subgraph: { nodes: [ ...inputPorts.map((port) => port.node), ...normalizedSelectedNodes, ...outputPorts.map((port) => port.node), ], edges: subgraphEdges, viewport: null, }, }, }, ); const remainingNodes = graph.nodes .filter((node) => !selectedIds.has(node.id)) .map((node) => ({ ...node, selected: false })); const remainingEdges = graph.edges.filter( (edge) => !selectedIds.has(edge.source) && !selectedIds.has(edge.target), ); const replacementEdges = [...remainingEdges]; inputPorts.forEach((port) => { if (!port.edge) { return; } replacementEdges.push( createExternalEdge( port.edge.source, port.edge.sourceHandle || null, componentNode.id, port.externalHandleId, ), ); }); outputPorts.forEach((port) => { if (!port.edge) { return; } replacementEdges.push( createExternalEdge( componentNode.id, port.externalHandleId, port.edge.target, port.edge.targetHandle || null, ), ); }); return { graph: hydrateGraph({ ...graph, nodes: [...remainingNodes, { ...componentNode, selected: true }], edges: replacementEdges, }), componentNodeId: componentNode.id, }; } function buildIncomingEdgeMap(edges) { const incoming = new Map(); const outgoing = new Map(); edges.forEach((edge) => { if (!incoming.has(edge.target)) { incoming.set(edge.target, []); } if (!outgoing.has(edge.source)) { outgoing.set(edge.source, []); } incoming.get(edge.target).push(edge); outgoing.get(edge.source).push(edge); }); return { incoming, outgoing }; } function splitFeedbackEdges(edges) { const acyclicEdges = []; const feedbackEdges = []; edges.forEach((edge) => { if (hasPath(edge.target, edge.source, acyclicEdges)) { feedbackEdges.push(edge); } else { acyclicEdges.push(edge); } }); return { acyclicEdges, feedbackEdges }; } function topologicalSort(nodes, incomingMap) { const nodeLookup = new Map(nodes.map((node) => [node.id, node])); const visited = new Set(); const visiting = new Set(); const sorted = []; function visit(node) { if (visited.has(node.id)) { return; } if (visiting.has(node.id)) { throw new Error('Workflow contains a circular dependency.'); } visiting.add(node.id); const incomingEdges = incomingMap.get(node.id) || []; incomingEdges.forEach((edge) => { const sourceNode = nodeLookup.get(edge.source); if (sourceNode) { visit(sourceNode); } }); visiting.delete(node.id); visited.add(node.id); sorted.push(node); } nodes.forEach((node) => visit(node)); return sorted; } function collectReachableNodeIds(startIds, edges) { const reachable = new Set(startIds); const outgoingBySource = new Map(); edges.forEach((edge) => { outgoingBySource.set(edge.source, [...(outgoingBySource.get(edge.source) || []), edge]); }); const queue = [...startIds]; while (queue.length > 0) { const nodeId = queue.shift(); (outgoingBySource.get(nodeId) || []).forEach((edge) => { if (!reachable.has(edge.target)) { reachable.add(edge.target); queue.push(edge.target); } }); } return reachable; } function includeInputDependencyNodeIds(nodeIds, edges) { const required = new Set(nodeIds); const incomingByTarget = new Map(); edges.forEach((edge) => { incomingByTarget.set(edge.target, [...(incomingByTarget.get(edge.target) || []), edge]); }); const queue = [...nodeIds]; while (queue.length > 0) { const nodeId = queue.shift(); (incomingByTarget.get(nodeId) || []).forEach((edge) => { if (!required.has(edge.source)) { required.add(edge.source); queue.push(edge.source); } }); } return required; } function getRuntimeOrder(nodes, edges, incomingMap) { const sortedNodes = topologicalSort(nodes, incomingMap); const startIds = nodes .filter((node) => node.type === 'basic/start') .map((node) => node.id); if (startIds.length === 0) { return sortedNodes; } const reachable = collectReachableNodeIds(startIds, edges); const required = includeInputDependencyNodeIds(reachable, edges); return sortedNodes.filter((node) => required.has(node.id)); } function prepareGraphForRun(graph) { return hydrateGraph({ ...graph, nodes: graph.nodes.map((node) => hydrateNode({ ...node, data: { ...node.data, runtime: { type: node.type, status: 'idle', error: '', inputs: {}, outputs: {}, ...(node.type === 'basic/dialog' ? { messages: [], cleared: true, } : {}), }, }, }), ), }); } function updateNodeInGraph(graph, nodeId, updater) { return hydrateGraph({ ...graph, nodes: graph.nodes.map((node) => { if (node.id !== nodeId) { return node; } return hydrateNode(updater(node)); }), }); } function updateNodeRuntime(graph, nodeId, runtimePatch) { return updateNodeInGraph(graph, nodeId, (node) => ({ ...node, data: { ...node.data, runtime: { ...(node.data.runtime || {}), ...runtimePatch, }, }, })); } function collectInputsForNode(incomingEdges, outputsByNode) { const valuesByHandle = {}; (incomingEdges || []).forEach((edge) => { const sourceOutputs = outputsByNode.get(edge.source) || {}; const handleId = edge.targetHandle || 'input'; const value = sourceOutputs[edge.sourceHandle]; valuesByHandle[handleId] = [...(valuesByHandle[handleId] || []), value]; }); return Object.fromEntries( Object.entries(valuesByHandle).map(([handleId, values]) => [ handleId, values.find((value) => value !== null && value !== undefined) ?? values[0], ]), ); } function hasBlockedIncomingValue(incomingEdges, inputValues) { const targetHandles = new Set((incomingEdges || []).map((edge) => edge.targetHandle || 'input')); return [...targetHandles].some((handleId) => inputValues[handleId] === null || inputValues[handleId] === undefined); } function renderMemoryTemplate(text, memory = {}) { return String(text || '').replace(/\{([a-zA-Z0-9_.-]+)\}/g, (match, key) => { const value = memory[key]; return value === undefined || value === null || value === '' ? match : String(value); }); } function buildQuestionResult(node, inputValues, answer, memory = {}, questionOverride = '') { const question = questionOverride || renderMemoryTemplate(inputValues.question || node.data.question || '', memory); const dialogIn = Array.isArray(inputValues['dialog-in']) ? inputValues['dialog-in'] : []; const messages = [ ...dialogIn, ...(String(question || '').trim() ? [{ type: 'character', text: question }] : []), { type: 'user', text: answer, }, ]; return { outputs: { answer, question, dialog: messages, turn: { answer, question, dialog: messages, }, }, runtime: { question, answer, messages, }, }; } function getFeedbackJump(runState, sourceNodeId, outputs = {}) { const feedbackEdges = runState.feedbackOutgoing?.get(sourceNodeId) || []; for (const edge of feedbackEdges) { const outputValue = outputs[edge.sourceHandle]; if (outputValue === null || outputValue === undefined) { continue; } const targetIndex = runState.order.indexOf(edge.target); if (targetIndex === -1) { continue; } return { edge, targetIndex, outputValue, }; } return null; } export function createInteractiveWorkflowRun(graph) { const workingGraph = prepareGraphForRun(graph); const { acyclicEdges, feedbackEdges } = splitFeedbackEdges(workingGraph.edges); const { incoming, outgoing } = buildIncomingEdgeMap(acyclicEdges); const { outgoing: feedbackOutgoing } = buildIncomingEdgeMap(feedbackEdges); const sortedNodes = getRuntimeOrder(workingGraph.nodes, workingGraph.edges, incoming); return { graph: workingGraph, incoming, outgoing, feedbackOutgoing, order: sortedNodes.map((node) => node.id), cursor: 0, outputsByNode: new Map(), inputOverridesByNode: new Map(), memory: {}, assistantRole: DEFAULT_ASSISTANT_ROLE, retryCountsByNode: new Map(), componentRunsByNode: new Map(), pendingQuestion: null, completed: false, }; } function wait(ms) { if (!ms) { return Promise.resolve(); } return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } export async function continueInteractiveWorkflowRun(runState, options) { const { backendUrl, llmProvider = 'ollama', llmSessionId = '', userAnswer = null, externalInputValue = null, externalInputValues = null, knowledgeContextId = '', testRun = null, onGraphChange = null, onEvent = null, animate = true, } = options; let workingGraph = runState.graph; if (onGraphChange) { onGraphChange(workingGraph); } if (runState.pendingQuestion) { if (runState.pendingQuestion.componentNodeId) { const componentNodeId = runState.pendingQuestion.componentNodeId; const componentNode = workingGraph.nodes.find( (candidate) => candidate.id === componentNodeId && isComponentNodeType(candidate.type), ); const componentRun = runState.componentRunsByNode?.get(componentNodeId); const componentInputValues = runState.pendingQuestion.componentInputValues || {}; if (!componentNode || !componentRun) { throw new Error('Pending component question was not found.'); } componentRun.memory = runState.memory || {}; componentRun.assistantRole = runState.assistantRole || DEFAULT_ASSISTANT_ROLE; const componentResult = await continueInteractiveWorkflowRun(componentRun, { backendUrl, llmProvider, llmSessionId, userAnswer, externalInputValue: componentInputValues.input ?? null, externalInputValues: componentInputValues, knowledgeContextId, testRun, animate: false, onGraphChange: (nextSubgraph) => { workingGraph = updateNodeInGraph(workingGraph, componentNodeId, (currentNode) => ({ ...currentNode, data: { ...currentNode.data, subgraph: nextSubgraph, }, })); if (onGraphChange) { onGraphChange(workingGraph); } }, onEvent, }); runState.memory = componentResult.state.memory || runState.memory || {}; runState.assistantRole = componentResult.state.assistantRole || runState.assistantRole || DEFAULT_ASSISTANT_ROLE; runState.componentRunsByNode.set(componentNodeId, componentResult.state); workingGraph = updateNodeInGraph(workingGraph, componentNodeId, (currentNode) => ({ ...currentNode, data: { ...currentNode.data, subgraph: componentResult.state.graph, }, })); if (componentResult.status === 'paused') { const pendingQuestion = { ...componentResult.pendingQuestion, componentNodeId, componentInputValues, }; runState.pendingQuestion = pendingQuestion; workingGraph = updateNodeRuntime(workingGraph, componentNodeId, { status: 'waiting', error: '', inputs: componentInputValues, outputs: {}, }); if (onGraphChange) { onGraphChange(workingGraph); } return { status: 'paused', state: { ...runState, graph: workingGraph, pendingQuestion, }, pendingQuestion, }; } if (componentResult.status === 'restart') { runState.pendingQuestion = null; return { status: 'restart', state: { ...runState, graph: workingGraph, pendingQuestion: null, }, pendingQuestion: null, }; } const componentOutputs = collectComponentOutputs(componentResult.state.graph); runState.outputsByNode.set(componentNodeId, componentOutputs.outputs); runState.componentRunsByNode.delete(componentNodeId); runState.pendingQuestion = null; runState.cursor += 1; workingGraph = updateNodeRuntime(workingGraph, componentNodeId, { status: 'success', error: '', inputs: componentInputValues, outputs: componentOutputs.outputs, lastOutput: componentOutputs.lastOutput, }); if (onGraphChange) { onGraphChange(workingGraph); } } else { if (userAnswer === null || userAnswer === undefined || String(userAnswer).trim() === '') { return { status: 'paused', state: { ...runState, graph: workingGraph, }, pendingQuestion: runState.pendingQuestion, }; } const pendingNode = workingGraph.nodes.find((candidate) => candidate.id === runState.pendingQuestion.nodeId); if (!pendingNode) { throw new Error('Pending question node was not found.'); } const answer = String(userAnswer).trim(); if (runState.pendingQuestion.retryForNodeId) { const retryDialog = [ ...(Array.isArray(runState.pendingQuestion.inputs?.['dialog-in']) ? runState.pendingQuestion.inputs['dialog-in'] : []), ...(String(runState.pendingQuestion.question || '').trim() ? [{ type: 'character', text: runState.pendingQuestion.question }] : []), { type: 'user', text: answer, }, ]; runState.inputOverridesByNode.set(runState.pendingQuestion.retryForNodeId, { answer, question: runState.pendingQuestion.question, 'dialog-in': retryDialog, }); if (onEvent) { onEvent({ type: 'user-answer', nodeId: pendingNode.id, answer, }); } workingGraph = updateNodeRuntime(workingGraph, pendingNode.id, { status: 'running', error: '', }); runState.pendingQuestion = null; if (onGraphChange) { onGraphChange(workingGraph); } } else { const result = buildQuestionResult( pendingNode, runState.pendingQuestion.inputs, answer, runState.memory || {}, runState.pendingQuestion.question, ); runState.outputsByNode.set(pendingNode.id, result.outputs); workingGraph = updateNodeRuntime(workingGraph, pendingNode.id, { status: 'success', error: '', inputs: runState.pendingQuestion.inputs, outputs: result.outputs, ...result.runtime, }); if (onEvent) { onEvent({ type: 'user-answer', nodeId: pendingNode.id, answer, }); } runState.pendingQuestion = null; runState.cursor += 1; if (onGraphChange) { onGraphChange(workingGraph); } } } } while (runState.cursor < runState.order.length) { const nodeId = runState.order[runState.cursor]; const node = workingGraph.nodes.find((candidate) => candidate.id === nodeId); if (!node) { runState.cursor += 1; continue; } const definition = getNodeDefinition(node.type); const incomingEdges = runState.incoming.get(node.id) || []; const inputValues = { ...collectInputsForNode(incomingEdges, runState.outputsByNode), ...(runState.inputOverridesByNode.get(node.id) || {}), }; if (incomingEdges.length > 0 && hasBlockedIncomingValue(incomingEdges, inputValues)) { runState.outputsByNode.set(node.id, {}); workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'skipped', error: '', inputs: inputValues, outputs: {}, }); if (onEvent) { onEvent({ type: 'skip', nodeId: node.id, title: node.data?.title || node.type, }); } if (onGraphChange) { onGraphChange(workingGraph); } runState.cursor += 1; continue; } workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'running', error: '', inputs: inputValues, }); if (onEvent) { onEvent({ type: 'node-start', nodeId: node.id, title: node.data?.title || node.type, }); } if (onGraphChange) { onGraphChange(workingGraph); } await wait(animate ? 80 : 0); if (node.type === 'basic/question') { const renderedQuestion = renderMemoryTemplate(inputValues.question || node.data.question || '', runState.memory || {}); const question = node.data?.paraphrase ? await paraphraseText(backendUrl, renderedQuestion, 'question', testRun, node.id, llmProvider, llmSessionId, runState.assistantRole || DEFAULT_ASSISTANT_ROLE) : renderedQuestion; const pendingQuestion = { nodeId: node.id, question, inputs: inputValues, }; workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'waiting', error: '', inputs: inputValues, outputs: {}, question, }); if (onEvent) { onEvent({ type: 'assistant-question', nodeId: node.id, question, }); } if (onGraphChange) { onGraphChange(workingGraph); } return { status: 'paused', state: { ...runState, graph: workingGraph, pendingQuestion, }, pendingQuestion, }; } try { let result; if (isComponentNodeType(node.type)) { const componentSubgraph = node.data.subgraph || createComponentSubgraph(); const componentRun = createInteractiveWorkflowRun(componentSubgraph); componentRun.memory = runState.memory || {}; componentRun.assistantRole = runState.assistantRole || DEFAULT_ASSISTANT_ROLE; runState.componentRunsByNode.set(node.id, componentRun); const subgraphResult = await continueInteractiveWorkflowRun(componentRun, { backendUrl, llmProvider, llmSessionId, externalInputValue: inputValues.input ?? null, externalInputValues: inputValues, knowledgeContextId, testRun, animate: false, onGraphChange: (nextSubgraph) => { workingGraph = updateNodeInGraph(workingGraph, node.id, (currentNode) => ({ ...currentNode, data: { ...currentNode.data, subgraph: nextSubgraph, }, })); if (onGraphChange) { onGraphChange(workingGraph); } }, onEvent, }); runState.memory = subgraphResult.state.memory || runState.memory || {}; runState.assistantRole = subgraphResult.state.assistantRole || runState.assistantRole || DEFAULT_ASSISTANT_ROLE; runState.componentRunsByNode.set(node.id, subgraphResult.state); if (subgraphResult.status === 'paused') { const pendingQuestion = { ...subgraphResult.pendingQuestion, componentNodeId: node.id, componentInputValues: inputValues, }; workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'waiting', error: '', inputs: inputValues, outputs: {}, }); if (onGraphChange) { onGraphChange(workingGraph); } return { status: 'paused', state: { ...runState, graph: workingGraph, pendingQuestion, }, pendingQuestion, }; } if (subgraphResult.status === 'restart') { return { status: 'restart', state: { ...runState, graph: workingGraph, pendingQuestion: null, }, pendingQuestion: null, }; } const componentResult = collectComponentOutputs(subgraphResult.state.graph); runState.componentRunsByNode.delete(node.id); result = { outputs: componentResult.outputs, runtime: { lastOutput: componentResult.lastOutput, assistantRole: runState.assistantRole || DEFAULT_ASSISTANT_ROLE, }, subgraph: subgraphResult.graph, }; } else if (definition?.execute) { const nodeLookup = new Map(workingGraph.nodes.map((candidate) => [candidate.id, candidate])); result = await definition.execute({ node: nodeLookup.get(node.id), inputs: inputValues, backendUrl, llmProvider, llmSessionId, externalInputValue, externalInputValues, memory: runState.memory || {}, assistantRole: runState.assistantRole || DEFAULT_ASSISTANT_ROLE, knowledgeContextId, testRun, connectedOutputHandles: new Set((runState.outgoing.get(node.id) || []).map((edge) => edge.sourceHandle)), outgoingEdges: runState.outgoing.get(node.id) || [], nodeLookup, }); } else { result = { outputs: {}, runtime: {}, }; } if (result.subgraph) { workingGraph = updateNodeInGraph(workingGraph, node.id, (currentNode) => ({ ...currentNode, data: { ...currentNode.data, subgraph: result.subgraph, }, })); } if (result.runtimeAction?.type === 'restart') { workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'success', error: '', inputs: inputValues, outputs: result.outputs || {}, ...(result.runtime || {}), }); if (onEvent) { onEvent({ type: 'restart', nodeId: node.id, title: node.data?.title || node.type, }); } if (onGraphChange) { onGraphChange(workingGraph); } return { status: 'restart', state: { ...runState, graph: workingGraph, pendingQuestion: null, }, pendingQuestion: null, }; } const canRetryUnclear = (node.type === 'basic/semantic-branch' || node.type === 'basic/save-memory') && result.runtime?.matchId === 'unclear' && node.data?.retryOnUnclear !== false; if (canRetryUnclear) { const retryCount = runState.retryCountsByNode.get(node.id) || 0; const defaultRetryQuestion = 'Не смогла уверенно понять ответ. Пожалуйста, ответьте ближе к одному из вариантов.'; const rawRetryQuestion = typeof node.data?.retryQuestion === 'string' ? node.data.retryQuestion : defaultRetryQuestion; const retryQuestionTemplate = renderMemoryTemplate( rawRetryQuestion, runState.memory || {}, ); const renderedRetryQuestion = node.type === 'basic/save-memory' ? retryQuestionTemplate.replace(/\{text\}/g, inputValues.text || '') : retryQuestionTemplate; runState.retryCountsByNode.set(node.id, retryCount + 1); const retryQuestion = node.data?.retryParaphrase ? await paraphraseText(backendUrl, renderedRetryQuestion, 'question', testRun, node.id, llmProvider, llmSessionId, runState.assistantRole || DEFAULT_ASSISTANT_ROLE) : renderedRetryQuestion; const pendingQuestion = { nodeId: node.id, retryForNodeId: node.id, question: retryQuestion, inputs: inputValues, }; workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'waiting', error: '', inputs: inputValues, outputs: {}, result: 'unclear', matchId: 'unclear', retryCount: retryCount + 1, }); if (onEvent) { onEvent({ type: 'assistant-question', nodeId: node.id, question: retryQuestion, }); } if (onGraphChange) { onGraphChange(workingGraph); } return { status: 'paused', state: { ...runState, graph: workingGraph, pendingQuestion, }, pendingQuestion, }; } runState.outputsByNode.set(node.id, result.outputs || {}); runState.inputOverridesByNode.delete(node.id); if (result.memoryPatch && typeof result.memoryPatch === 'object') { runState.memory = { ...(runState.memory || {}), ...result.memoryPatch, }; } if (typeof result.assistantRolePatch === 'string') { runState.assistantRole = result.assistantRolePatch || DEFAULT_ASSISTANT_ROLE; } workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'success', error: '', inputs: inputValues, outputs: result.outputs || {}, ...(result.runtime || {}), assistantRole: runState.assistantRole || DEFAULT_ASSISTANT_ROLE, }); if (onEvent) { onEvent({ type: 'node-success', nodeId: node.id, title: node.data?.title || node.type, }); if (result.runtime?.assistantMessage) { onEvent({ type: 'assistant-message', nodeId: node.id, message: result.runtime.assistantMessage, }); } if (result.memoryPatch && typeof result.memoryPatch === 'object') { onEvent({ type: 'memory-update', nodeId: node.id, memoryPatch: result.memoryPatch, }); } if (typeof result.assistantRolePatch === 'string') { onEvent({ type: 'assistant-role-update', nodeId: node.id, assistantRole: runState.assistantRole || DEFAULT_ASSISTANT_ROLE, }); } } if (onGraphChange) { onGraphChange(workingGraph); } const feedbackJump = getFeedbackJump(runState, node.id, result.outputs || {}); if (feedbackJump) { const currentOverrides = runState.inputOverridesByNode.get(feedbackJump.edge.target) || {}; runState.inputOverridesByNode.set(feedbackJump.edge.target, { ...currentOverrides, [feedbackJump.edge.targetHandle || 'input']: feedbackJump.outputValue, }); runState.cursor = feedbackJump.targetIndex; await wait(animate ? 24 : 0); continue; } } catch (error) { runState.outputsByNode.set(node.id, {}); workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'error', error: error instanceof Error ? error.message : String(error), inputs: inputValues, outputs: {}, }); if (onEvent) { onEvent({ type: 'node-error', nodeId: node.id, title: node.data?.title || node.type, error: error instanceof Error ? error.message : String(error), }); } if (onGraphChange) { onGraphChange(workingGraph); } } runState.cursor += 1; await wait(animate ? 24 : 0); } return { status: 'complete', state: { ...runState, graph: workingGraph, completed: true, pendingQuestion: null, }, pendingQuestion: null, }; } async function executeGraph(graph, options) { const { backendUrl, llmProvider = 'ollama', llmSessionId = '', externalInputValue = null, externalInputValues = null, onGraphChange = null, animate = false, } = options; let workingGraph = prepareGraphForRun(graph); const { acyclicEdges } = splitFeedbackEdges(workingGraph.edges); const { incoming, outgoing } = buildIncomingEdgeMap(acyclicEdges); const sortedNodes = getRuntimeOrder(workingGraph.nodes, workingGraph.edges, incoming); const outputsByNode = new Map(); if (onGraphChange) { onGraphChange(workingGraph); } for (const node of sortedNodes) { const definition = getNodeDefinition(node.type); const inputValues = collectInputsForNode(incoming.get(node.id), outputsByNode); workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'running', error: '', inputs: inputValues, }); if (onGraphChange) { onGraphChange(workingGraph); } await wait(animate ? 80 : 0); try { let result; if (isComponentNodeType(node.type)) { const componentSubgraph = node.data.subgraph || createComponentSubgraph(); const subgraphResult = await executeGraph(componentSubgraph, { backendUrl, llmProvider, llmSessionId, externalInputValue: inputValues.input ?? null, externalInputValues: inputValues, animate: false, }); const componentResult = collectComponentOutputs(subgraphResult.graph); result = { outputs: componentResult.outputs, runtime: { lastOutput: componentResult.lastOutput, }, subgraph: subgraphResult.graph, }; } else if (definition?.execute) { const nodeLookup = new Map(workingGraph.nodes.map((candidate) => [candidate.id, candidate])); result = await definition.execute({ node: nodeLookup.get(node.id), inputs: inputValues, backendUrl, llmProvider, llmSessionId, externalInputValue, externalInputValues, connectedOutputHandles: new Set((outgoing.get(node.id) || []).map((edge) => edge.sourceHandle)), outgoingEdges: outgoing.get(node.id) || [], nodeLookup, }); } else { result = { outputs: {}, runtime: {}, }; } if (result.subgraph) { workingGraph = updateNodeInGraph(workingGraph, node.id, (currentNode) => ({ ...currentNode, data: { ...currentNode.data, subgraph: result.subgraph, }, })); } outputsByNode.set(node.id, result.outputs || {}); workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'success', error: '', inputs: inputValues, outputs: result.outputs || {}, ...(result.runtime || {}), }); if (onGraphChange) { onGraphChange(workingGraph); } } catch (error) { outputsByNode.set(node.id, {}); workingGraph = updateNodeRuntime(workingGraph, node.id, { status: 'error', error: error instanceof Error ? error.message : String(error), inputs: inputValues, outputs: {}, }); if (onGraphChange) { onGraphChange(workingGraph); } } await wait(animate ? 24 : 0); } return { graph: workingGraph, order: sortedNodes.map((node) => node.id), }; } export async function executeWorkflow({ graph, backendUrl, llmProvider = 'ollama', llmSessionId = '', onGraphChange }) { return executeGraph(graph, { backendUrl, llmProvider, llmSessionId, onGraphChange, animate: true, }); }