import type { ParsedTriplets } from './triplet-parser'; import { toEdgeType } from './triplet-parser'; import type { SearchResult } from './search-nodes'; /** 從 nodeResults + parsed 組成可直接送入 /execute 的 ExecutionGraph * * config 格式(來自 workflow YAML 的 config 欄位): * { node_name: { component: "cmp_xxxxxxxx" | "rec_xxxxxxxx" | canonical_id, ...params } } * * 若 config[node].component 存在,以它覆蓋 searchNodes 偵測到的 componentId。 * config[node] 的其他欄位作為節點靜態參數(node.data),合併進執行 context。 */ export function buildExecutionGraph( parsed: ParsedTriplets, nodeResults: SearchResult['nodeResults'], graphId: string, graphName: string, config?: Record>, ) { const nodes = [...parsed.nodeNames].map(name => { const nr = nodeResults[name]!; const id = name.toLowerCase().replace(/\s+/g, '-'); const nodeConfig = config?.[name] ?? {}; // config[name].component 可以是 hash 或 canonical_id,覆蓋自動偵測的 componentId const componentId = (nodeConfig.component as string | undefined) ?? nr.componentId; // 其他 config 欄位作為 node.data(靜態參數) const { component: _component, ...staticParams } = nodeConfig; const data = Object.keys(staticParams).length > 0 ? staticParams : undefined; return { id, type: nr.type, componentId, label: name, data }; }); const edges = parsed.edges.map(e => { // 「對每個 X」label 抽 iterator:cypher binding 表達 FOREACH 的迭代變數 // 例:'A >> 對每個 paragraph >> B' → type=FOREACH, iterator='paragraph' // getIterableFromContext 會找 ctx.paragraphs(複數)或 ctx.paragraph let iterator: string | undefined; let label = e.label; const foreachMatch = label.match(/^(?:對每個|FOREACH)\s+(\w+)$/i); if (foreachMatch) { iterator = foreachMatch[1]; label = '對每個'; // 改回標準 label 走 SEMANTIC_EDGE_MAP } const edge: { from: string; to: string; type: ReturnType; iterator?: string } = { from: e.from.toLowerCase().replace(/\s+/g, '-'), to: e.to.toLowerCase().replace(/\s+/g, '-'), type: toEdgeType(label), }; if (iterator) edge.iterator = iterator; return edge; }); return { id: graphId, name: graphName, nodes, edges }; }