Spaces:
Sleeping
Sleeping
| 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, | |
| }); | |
| } | |