Files
Arcrun/cypher-executor/src/actions/graph-builder.ts
T
Leo e8fca33f80 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)
Three platform-level improvements that together enable the full
"草稿 → LLM 整理 → KBDB" wiki ingest workflow via cypher binding:

## 1. Nested interpolation in node.data
Previously {{var}} only supported top-level keys, so {{item.content}}
literal-passed through. Now supports dot-path:
  {{paragraph.content}} → ctx.paragraph.content
  {{paragraphs.0.entity}} → ctx.paragraphs[0].entity
Non-string values (object/array) JSON.stringify automatically.

## 2. 對每個 X cypher binding syntax
'A >> 對每個 paragraph >> B' parses into FOREACH edge with
iterator='paragraph'. graph-builder.ts strips the iterator from label
before edge type resolution. Backwards compatible: bare '對每個' still
defaults to item.

## 3. FOREACH preserves outer context
itemContext was previously {...result, [iter]: item}, dropping
top-level api_key etc. Now {...outerCtx, ...result, [iter]: item} so
{{api_key}} interpolation works in foreach body.

## 4. Unified recipe output parsing (sync + resume)
Extracted parseRecipeOutput() helper used by both sync claude_api
result + workflow resume callback. Strips ```json fence, parses,
spreads parsed top-level fields into result so downstream FOREACH
finds 'paragraphs' (not buried in data.paragraphs).

paused state now stores recipe_output_format + required_fields so
resume route can apply same parsing as sync path.

End-to-end verified:
- input(草稿+api_key) → synth(claude_api+recipe) → 對每個 paragraph → write_wiki(kbdb_create_block)
- Real Claude synthesis on Mira daemon: 3 triplets + 2 paragraphs
- Both paragraphs written to KBDB as wiki-page blocks (verified GET)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 16:23:02 +08:00

57 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, Record<string, unknown>>,
) {
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 抽 iteratorcypher 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<typeof toEdgeType>; 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 };
}