@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.
- package/nodes/combine-document-chunks/combine-document-chunks.config.json +8 -0
- package/nodes/combine-document-chunks/combine-document-chunks.process.js +9 -2
- package/nodes/describe-tables/describe-tables.config.json +59 -0
- package/nodes/describe-tables/describe-tables.process.js +40 -0
- package/nodes/expand-chunk-context/expand-chunk-context.config.json +87 -0
- package/nodes/expand-chunk-context/expand-chunk-context.process.js +34 -0
- package/nodes/find-entity-path/find-entity-path.config.json +73 -0
- package/nodes/find-entity-path/find-entity-path.process.js +51 -0
- package/nodes/get-chunk-by-index/get-chunk-by-index.config.json +8 -0
- package/nodes/get-chunk-by-index/get-chunk-by-index.process.js +9 -2
- package/nodes/get-entity-neighbors/get-entity-neighbors.config.json +81 -0
- package/nodes/get-entity-neighbors/get-entity-neighbors.process.js +51 -0
- package/nodes/keyword-search/keyword-search.config.json +67 -0
- package/nodes/keyword-search/keyword-search.process.js +45 -0
- package/nodes/knowledge-base/knowledge-base.config.json +32 -0
- package/nodes/knowledge-base/knowledge-base.process.js +15 -0
- package/nodes/list-documents/list-documents.config.json +60 -0
- package/nodes/list-documents/list-documents.process.js +25 -0
- package/nodes/list-entities/list-entities.config.json +74 -0
- package/nodes/list-entities/list-entities.process.js +25 -0
- package/nodes/query-knowledge-base/query-knowledge-base.config.json +14 -7
- package/nodes/query-knowledge-base/query-knowledge-base.process.js +25 -5
- package/nodes/read-chunks/read-chunks.config.json +79 -0
- package/nodes/read-chunks/read-chunks.process.js +49 -0
- package/nodes/remote-mcp-tool/remote-mcp-tool.config.json +1 -0
- package/nodes/semantic-search/semantic-search.config.json +9 -1
- package/nodes/semantic-search/semantic-search.process.js +41 -18
- package/nodes/tool/tool.config.json +1 -0
- package/package.json +12 -7
- package/src/index.js +76 -9
- package/src/integrations/knowledge-base-interface.js +49 -0
- package/src/integrations/sqlite.js +535 -83
- package/src/types/knowledge_base.json +9 -0
- package/src/utilities/loaders.js +67 -11
- package/src/utilities/typers.js +15 -17
- package/src/utilities/validators.js +16 -1
- package/types/knowledge_base.json +9 -0
package/src/utilities/loaders.js
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
package/src/utilities/typers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import Ajv from "ajv";
|
|
4
|
-
import
|
|
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 ||
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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)
|