@zerowidth/workbench-sdk 2.0.0-alpha.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/nodes/combine-document-chunks/combine-document-chunks.config.json +8 -0
  2. package/nodes/combine-document-chunks/combine-document-chunks.process.js +9 -2
  3. package/nodes/describe-tables/describe-tables.config.json +59 -0
  4. package/nodes/describe-tables/describe-tables.process.js +40 -0
  5. package/nodes/expand-chunk-context/expand-chunk-context.config.json +87 -0
  6. package/nodes/expand-chunk-context/expand-chunk-context.process.js +34 -0
  7. package/nodes/find-entity-path/find-entity-path.config.json +73 -0
  8. package/nodes/find-entity-path/find-entity-path.process.js +51 -0
  9. package/nodes/get-chunk-by-index/get-chunk-by-index.config.json +8 -0
  10. package/nodes/get-chunk-by-index/get-chunk-by-index.process.js +9 -2
  11. package/nodes/get-entity-neighbors/get-entity-neighbors.config.json +81 -0
  12. package/nodes/get-entity-neighbors/get-entity-neighbors.process.js +51 -0
  13. package/nodes/keyword-search/keyword-search.config.json +67 -0
  14. package/nodes/keyword-search/keyword-search.process.js +45 -0
  15. package/nodes/knowledge-base/knowledge-base.config.json +32 -0
  16. package/nodes/knowledge-base/knowledge-base.process.js +15 -0
  17. package/nodes/list-documents/list-documents.config.json +60 -0
  18. package/nodes/list-documents/list-documents.process.js +25 -0
  19. package/nodes/list-entities/list-entities.config.json +74 -0
  20. package/nodes/list-entities/list-entities.process.js +25 -0
  21. package/nodes/query-knowledge-base/query-knowledge-base.config.json +14 -7
  22. package/nodes/query-knowledge-base/query-knowledge-base.process.js +25 -5
  23. package/nodes/read-chunks/read-chunks.config.json +79 -0
  24. package/nodes/read-chunks/read-chunks.process.js +49 -0
  25. package/nodes/remote-mcp-tool/remote-mcp-tool.config.json +1 -0
  26. package/nodes/semantic-search/semantic-search.config.json +9 -1
  27. package/nodes/semantic-search/semantic-search.process.js +41 -18
  28. package/nodes/tool/tool.config.json +1 -0
  29. package/package.json +12 -7
  30. package/src/index.js +76 -9
  31. package/src/integrations/knowledge-base-interface.js +49 -0
  32. package/src/integrations/sqlite.js +535 -83
  33. package/src/types/knowledge_base.json +9 -0
  34. package/src/utilities/loaders.js +67 -11
  35. package/src/utilities/typers.js +15 -17
  36. package/src/utilities/validators.js +16 -1
  37. package/types/knowledge_base.json +9 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "uuid": { "type": "string" },
5
+ "name": { "type": "string" }
6
+ },
7
+ "required": ["uuid"],
8
+ "additionalProperties": true
9
+ }
@@ -3,7 +3,7 @@ import fs from "fs";
3
3
  import AdmZip from "adm-zip";
4
4
 
5
5
  import { convertImportToNodeType } from "./typers.js";
6
- import { getDirname } from "./helpers.js";
6
+ import { getDirname, isRemoteMCPTool } from "./helpers.js";
7
7
  import { isOAuthKey, OAuthRefreshManager } from "./oauth.js";
8
8
 
9
9
 
@@ -59,11 +59,23 @@ export async function loadNodes(flow) {
59
59
  // Regular node - needs both config and process
60
60
  const processFileUrl = `file://${path.resolve(processPath)}`;
61
61
  const processModule = await import(processFileUrl);
62
-
62
+
63
63
  nodes[type] = {
64
64
  config: configModule.default,
65
65
  process: processModule.default || processModule,
66
66
  };
67
+ } else if (isRemoteMCPTool({ type })) {
68
+ // MCP tool nodes are config-only BY DESIGN: dispatch happens
69
+ // through the LLM plugin loop (see isRemoteMCPTool call
70
+ // sites), never a process function. Register them so flow
71
+ // validation recognizes the type — and force is_plugin so
72
+ // the entry-node scan excludes them (the shipped config says
73
+ // is_constant without is_plugin, which would otherwise queue
74
+ // the node for direct execution it can't perform).
75
+ nodes[type] = {
76
+ config: { ...configModule.default, is_plugin: true },
77
+ process: null,
78
+ };
67
79
  } else {
68
80
  console.warn(`Missing process file for regular node ${type}:`, { processPath });
69
81
  }
@@ -118,7 +130,23 @@ export async function loadIntegrations(config, flow = null) {
118
130
  // Load knowledge base integration if available
119
131
  const knowledgeBaseType = config.knowledgeBase?.type || 'sqlite';
120
132
  const knowledgeBaseConfig = config.knowledgeBase || {};
121
-
133
+
134
+ // Bring-your-own knowledge base: the host passes a ready instance
135
+ // (anything implementing KnowledgeBaseInterface — your own SQL
136
+ // database, a vector store, an HTTP service) and every knowledge
137
+ // node uses it as the flow-global knowledge base. Skips the
138
+ // built-in loader entirely. `instances` (keyed by knowledge-base
139
+ // uuid) covers flows whose nodes reference specific KBs via a
140
+ // Knowledge Base node.
141
+ if (knowledgeBaseConfig.instance) {
142
+ integrations.knowledgeBase = knowledgeBaseConfig.instance;
143
+ }
144
+ if (knowledgeBaseConfig.instances && typeof knowledgeBaseConfig.instances === 'object') {
145
+ for (const [kbUuid, instance] of Object.entries(knowledgeBaseConfig.instances)) {
146
+ if (instance) integrations[`knowledgeBase:${kbUuid}`] = instance;
147
+ }
148
+ }
149
+
122
150
  if (flow?.knowledgeDbPath || knowledgeBaseConfig.enabled !== false) {
123
151
 
124
152
  try {
@@ -133,9 +161,12 @@ export async function loadIntegrations(config, flow = null) {
133
161
  };
134
162
 
135
163
  if (knowledgeBaseType === 'sqlite' && flow?.knowledgeDbPath) {
136
- integrations.knowledgeBase = new KnowledgeBaseIntegration(flow.knowledgeDbPath, integrationOptions);
137
-
138
- } else if (knowledgeBaseType !== 'sqlite') {
164
+ // A host-injected global instance wins over the built-in.
165
+ if (!integrations.knowledgeBase) {
166
+ integrations.knowledgeBase = new KnowledgeBaseIntegration(flow.knowledgeDbPath, integrationOptions);
167
+ }
168
+
169
+ } else if (knowledgeBaseType !== 'sqlite' && !integrations.knowledgeBase) {
139
170
  // For other knowledge base types, pass the config directly
140
171
  integrations.knowledgeBase = new KnowledgeBaseIntegration(knowledgeBaseConfig, integrationOptions);
141
172
 
@@ -145,7 +176,24 @@ export async function loadIntegrations(config, flow = null) {
145
176
  if (knowledgeBaseType === 'sqlite') {
146
177
  integrations.sqlite = integrations.knowledgeBase;
147
178
  }
148
-
179
+
180
+ // Node-level knowledge bases (ADR 0023): a flow can reference several
181
+ // KBs, each attached to a specific search node via a Knowledge Base
182
+ // node. The host resolves every referenced KB's `.db` and passes them
183
+ // keyed by uuid in `flow.knowledgeDbPaths`. Register each as its own
184
+ // first-class integration under `knowledgeBase:<uuid>` — a flat key
185
+ // (not a nested map) so it gets the same `_engineConfig` wiring as
186
+ // any other integration; nodes look theirs up by uuid. Coexists with
187
+ // the flow-global KB above (the fallback when a node has none).
188
+ if (knowledgeBaseType === 'sqlite' && flow?.knowledgeDbPaths && typeof flow.knowledgeDbPaths === 'object') {
189
+ for (const [kbUuid, dbPath] of Object.entries(flow.knowledgeDbPaths)) {
190
+ if (!dbPath) continue;
191
+ // Host-injected per-uuid instances win over file paths.
192
+ if (integrations[`knowledgeBase:${kbUuid}`]) continue;
193
+ integrations[`knowledgeBase:${kbUuid}`] = new KnowledgeBaseIntegration(dbPath, integrationOptions);
194
+ }
195
+ }
196
+
149
197
  } catch (error) {
150
198
  console.warn(`[WARN] Failed to load ${knowledgeBaseType} knowledge base integration:`, error.message);
151
199
  console.warn('[WARN] Error details:', error);
@@ -540,19 +588,27 @@ async function loadFlowImportFolder(folderName, folderEntries) {
540
588
  }
541
589
  }
542
590
 
543
- // Return import definition with metadata
591
+ // Return import definition with metadata. Field order matters:
592
+ // `...orchestrationData` used to be spread LAST, which clobbered
593
+ // `imports: nestedImports` (the loaded definitions array) with the
594
+ // raw orchestration's `imports` — the {id: snapshot} REQUEST map.
595
+ // Nested imports were loaded and then silently discarded, so any
596
+ // import-within-import never resolved. The spread now sits before
597
+ // the fields the loader owns.
544
598
  return {
545
599
  id: `imported-${importId}`,
546
600
  display_name: displayName,
547
601
  snapshot: snapshot,
548
602
  unique_id: importId,
549
603
  folder_name: folderName,
604
+ knowledgeDbPath: knowledgeDbPath,
605
+ // Preserve any additional metadata from orchestration.json
606
+ // (including `id`, which zv1 orchestrations carry and node-type
607
+ // lookups key on — same effective value as before this fix).
608
+ ...orchestrationData,
550
609
  nodes: orchestrationData.nodes,
551
610
  links: orchestrationData.links,
552
611
  imports: nestedImports,
553
- knowledgeDbPath: knowledgeDbPath,
554
- // Preserve any additional metadata from orchestration.json
555
- ...orchestrationData
556
612
  };
557
613
  }
558
614
 
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import Ajv from "ajv";
4
- import zv1 from "../index.js";
4
+ import Workbench from "../index.js";
5
5
 
6
6
  import { getDirname } from "./helpers.js";
7
7
  import { loadTypeConverter } from "./typeConverters.js";
@@ -40,10 +40,14 @@ export async function loadCustomTypes() {
40
40
  // Try to load custom converters for this type
41
41
  const customConverter = await loadTypeConverter(typeName);
42
42
 
43
- // Store both validator and converters
43
+ // Store both validator and converters. Converters are optional — a
44
+ // type with no `<type>.converters.js` (e.g. knowledge_base, a plain
45
+ // reference handle) just gets an empty converter map. (Was referencing
46
+ // an undeclared `typeConverters`, which threw for converter-less types
47
+ // and left them unregistered.)
44
48
  retval[typeName] = {
45
49
  validate: compiledSchema,
46
- converters: customConverter || (typeConverters[typeName] || {})
50
+ converters: customConverter || {}
47
51
  };
48
52
 
49
53
  } catch (err) {
@@ -185,21 +189,15 @@ export function convertType(value, type, options = {}) {
185
189
  export function convertImportToNodeType(importDef) {
186
190
  this.logDebug(`Converting import ${importDef.id} to node type`);
187
191
 
188
- // First ensure any nested imports are processed
192
+ // Nested imports resolve when the import EXECUTES: the internal
193
+ // engine's own loadNodes() registers everything in the def's
194
+ // `imports` array (same path the root engine uses). The old code
195
+ // here converted nested imports to node-type objects and pushed
196
+ // them into the flow's NODES array — type definitions aren't flow
197
+ // nodes, and once nested imports actually load (see the loaders.js
198
+ // spread-order fix) those pushed objects fail flow validation as
199
+ // typeless nodes. The def passes through with its imports intact.
189
200
  let processedImportDef = { ...importDef };
190
- if (importDef.imports && importDef.imports.length > 0) {
191
- this.logDebug(`Processing ${importDef.imports.length} nested imports`);
192
-
193
- // Load nested imports as node types
194
- const nestedNodes = [];
195
- for (const nestedImport of importDef.imports) {
196
- const nodeType = this.convertImportToNodeType(nestedImport);
197
- nestedNodes.push(nodeType);
198
- }
199
-
200
- // Add the nested import nodes to the flow
201
- processedImportDef.nodes = [...processedImportDef.nodes, ...nestedNodes];
202
- }
203
201
  processedImportDef.nodes = processedImportDef.nodes.filter(node => !node.debug_only);
204
202
 
205
203
  // Rest of the existing code...
@@ -47,7 +47,22 @@ export function validateKeys() {
47
47
  * Ensure this flow can run
48
48
  */
49
49
  export function validateFlow(flow) {
50
- // First validate all links reference existing nodes
50
+ // Every node must resolve to a loaded node type. Without this check a
51
+ // node whose type isn't in the catalog (removed model, typo, missing
52
+ // import) simply never executes — the flow "completes" with empty
53
+ // outputs and no signal. Runs after imports are registered, so
54
+ // imported-* types are resolvable here.
55
+ const unknownTypes = [...new Set(
56
+ flow.nodes.filter((node) => !this.nodes[node.type]).map((node) => node.type)
57
+ )];
58
+ if (unknownTypes.length > 0) {
59
+ throw new Error(
60
+ `Flow references unknown node type(s): ${unknownTypes.join(", ")}. ` +
61
+ `The node type may have been removed or renamed, or an import may be missing.`
62
+ );
63
+ }
64
+
65
+ // Validate all links reference existing nodes
51
66
  const nodeIds = new Set(flow.nodes.map(node => node.id));
52
67
  const invalidLinks = flow.links.filter(
53
68
  link => !nodeIds.has(link.from.node_id) || !nodeIds.has(link.to.node_id)
@@ -0,0 +1,9 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "uuid": { "type": "string" },
5
+ "name": { "type": "string" }
6
+ },
7
+ "required": ["uuid"],
8
+ "additionalProperties": true
9
+ }