@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
@@ -25,6 +25,14 @@
25
25
  "type": "string",
26
26
  "description": "String to use as separator between chunks",
27
27
  "default": "\n\n---\n\n"
28
+ },
29
+ {
30
+ "name": "knowledge_base",
31
+ "display_name": "Knowledge Base",
32
+ "type": "knowledge_base",
33
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
34
+ "required": false,
35
+ "default": null
28
36
  }
29
37
  ],
30
38
  "outputs": [
@@ -1,7 +1,14 @@
1
1
  export default async ({inputs, settings, config, nodeConfig}) => {
2
2
  try {
3
- // Get knowledge base integration
4
- const knowledgeBase = config.integrations?.knowledgeBase || config.integrations?.sqlite;
3
+ // Resolve the KB from a knowledge_base handle (shared wiring), falling
4
+ // back to the flow-global KB. See ADR 0023.
5
+ const kbRef = inputs.knowledge_base;
6
+ const knowledgeBase =
7
+ (kbRef && kbRef.uuid
8
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
9
+ : null) ||
10
+ config.integrations?.knowledgeBase ||
11
+ config.integrations?.sqlite;
5
12
 
6
13
  if (!knowledgeBase) {
7
14
  throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
@@ -0,0 +1,59 @@
1
+ {
2
+ "display_name": "Describe Tables",
3
+ "tagline": "See a KB's tables + columns",
4
+ "description": "List the tables in a Tabular knowledge base with their columns, types, and row counts — so a model can discover the schema before writing SQL for the Query Knowledge Base node. Wire a Knowledge Base node into knowledge_base, same as the other KB nodes. Returns [] for a docs KB.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": false,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "knowledge_base",
12
+ "display_name": "Knowledge Base",
13
+ "type": "knowledge_base",
14
+ "description": "The knowledge base to describe, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
15
+ "required": false,
16
+ "default": null
17
+ },
18
+ {
19
+ "name": "sample_rows",
20
+ "display_name": "Sample Rows",
21
+ "type": "number",
22
+ "description": "Rows to sample per table (0 = none). Samples help an LLM write correct SQL.",
23
+ "default": 5
24
+ }
25
+ ],
26
+ "outputs": [
27
+ {
28
+ "name": "schema",
29
+ "display_name": "Schema (for LLM)",
30
+ "type": "string",
31
+ "description": "Ready-to-paste schema block: CREATE TABLE DDL + a few sample rows (as comments) per table. Feed into an LLM node's context to write SQL."
32
+ },
33
+ {
34
+ "name": "tables",
35
+ "display_name": "Tables",
36
+ "type": "array",
37
+ "description": "[{ table_name, source_name, row_count, columns: [{ name, type }], ddl, sample_rows }]"
38
+ },
39
+ {
40
+ "name": "count",
41
+ "display_name": "Table Count",
42
+ "type": "number",
43
+ "description": "Number of tables"
44
+ },
45
+ {
46
+ "name": "success",
47
+ "display_name": "Success",
48
+ "type": "boolean",
49
+ "description": "Whether it completed successfully"
50
+ },
51
+ {
52
+ "name": "error",
53
+ "display_name": "Error",
54
+ "type": "string",
55
+ "description": "Error message if it failed"
56
+ }
57
+ ],
58
+ "settings": []
59
+ }
@@ -0,0 +1,40 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search). ADR 0023.
4
+ const kbRef = inputs.knowledge_base;
5
+ const knowledgeBase =
6
+ (kbRef && kbRef.uuid
7
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
8
+ : null) ||
9
+ config.integrations?.knowledgeBase ||
10
+ config.integrations?.sqlite;
11
+
12
+ if (!knowledgeBase) {
13
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
14
+ }
15
+
16
+ const sampleLimit =
17
+ typeof inputs.sample_rows === "number" ? inputs.sample_rows : 5;
18
+ const tables = await knowledgeBase.listTables({ sampleLimit });
19
+
20
+ // A ready-to-paste schema block for an LLM: CREATE TABLE DDL + a few
21
+ // sample rows (as comments) per table. Drop `schema` straight into an
22
+ // LLM node's context to write SQL for the Query Knowledge Base node.
23
+ const schema = tables
24
+ .map((t) => {
25
+ const lines = [t.ddl];
26
+ if (Array.isArray(t.sample_rows) && t.sample_rows.length > 0) {
27
+ lines.push("-- sample rows:");
28
+ for (const row of t.sample_rows) {
29
+ lines.push("-- " + JSON.stringify(row));
30
+ }
31
+ }
32
+ return lines.join("\n");
33
+ })
34
+ .join("\n\n");
35
+
36
+ return { tables, schema, count: tables.length, success: true, error: null };
37
+ } catch (error) {
38
+ return { tables: [], schema: "", count: 0, success: false, error: error.message };
39
+ }
40
+ };
@@ -0,0 +1,87 @@
1
+ {
2
+ "display_name": "Expand Chunk Context",
3
+ "tagline": "Grab chunks around a hit",
4
+ "description": "Given a chunk (its document_id + chunk_index, e.g. from a search result), return the surrounding chunks — the N before and N after — plus their combined text. The classic RAG move: search finds one chunk, this widens the window so the model has the context around it.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": false,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "document_id",
12
+ "display_name": "Document ID",
13
+ "type": "string",
14
+ "description": "The document the anchor chunk belongs to",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "chunk_index",
19
+ "display_name": "Chunk Index",
20
+ "type": "number",
21
+ "description": "The anchor chunk's index (e.g. from a search result)",
22
+ "required": true
23
+ },
24
+ {
25
+ "name": "knowledge_base",
26
+ "display_name": "Knowledge Base",
27
+ "type": "knowledge_base",
28
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
29
+ "required": false,
30
+ "default": null
31
+ },
32
+ {
33
+ "name": "before",
34
+ "display_name": "Chunks Before",
35
+ "type": "number",
36
+ "description": "How many chunks before the anchor to include",
37
+ "default": 1
38
+ },
39
+ {
40
+ "name": "after",
41
+ "display_name": "Chunks After",
42
+ "type": "number",
43
+ "description": "How many chunks after the anchor to include",
44
+ "default": 2
45
+ },
46
+ {
47
+ "name": "separator",
48
+ "display_name": "Separator",
49
+ "type": "string",
50
+ "description": "Joins chunk contents in the combined output",
51
+ "default": "\n\n"
52
+ }
53
+ ],
54
+ "outputs": [
55
+ {
56
+ "name": "chunks",
57
+ "display_name": "Chunks",
58
+ "type": "array",
59
+ "description": "The window of chunks, ascending by index"
60
+ },
61
+ {
62
+ "name": "content",
63
+ "display_name": "Combined Content",
64
+ "type": "string",
65
+ "description": "The window's chunk contents joined by the separator"
66
+ },
67
+ {
68
+ "name": "count",
69
+ "display_name": "Count",
70
+ "type": "number",
71
+ "description": "Number of chunks in the window"
72
+ },
73
+ {
74
+ "name": "success",
75
+ "display_name": "Success",
76
+ "type": "boolean",
77
+ "description": "Whether it completed successfully"
78
+ },
79
+ {
80
+ "name": "error",
81
+ "display_name": "Error",
82
+ "type": "string",
83
+ "description": "Error message if it failed"
84
+ }
85
+ ],
86
+ "settings": []
87
+ }
@@ -0,0 +1,34 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search). ADR 0023.
4
+ const kbRef = inputs.knowledge_base;
5
+ const knowledgeBase =
6
+ (kbRef && kbRef.uuid
7
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
8
+ : null) ||
9
+ config.integrations?.knowledgeBase ||
10
+ config.integrations?.sqlite;
11
+
12
+ if (!knowledgeBase) {
13
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
14
+ }
15
+
16
+ const { document_id, chunk_index, before = 1, after = 2, separator = "\n\n" } = inputs;
17
+ if (!document_id || typeof document_id !== 'string') {
18
+ throw new Error("document_id is required and must be a string");
19
+ }
20
+ if (chunk_index === undefined || chunk_index === null || Number.isNaN(Number(chunk_index))) {
21
+ throw new Error("chunk_index is required and must be a number");
22
+ }
23
+
24
+ const chunks = await knowledgeBase.getChunkWindow(document_id, Number(chunk_index), {
25
+ before,
26
+ after,
27
+ });
28
+ const content = chunks.map((c) => c.content ?? '').join(separator);
29
+
30
+ return { chunks, content, count: chunks.length, success: true, error: null };
31
+ } catch (error) {
32
+ return { chunks: [], content: '', count: 0, success: false, error: error.message };
33
+ }
34
+ };
@@ -0,0 +1,73 @@
1
+ {
2
+ "display_name": "Find Entity Path",
3
+ "tagline": "Connect two graph entities",
4
+ "description": "Find the shortest chain of relations connecting two entities in a Graph knowledge base (breadth-first, direction-agnostic). Answers \"how are X and Y related?\" with the hop-by-hop path. Wire a Knowledge Base node into knowledge_base, same as the search nodes.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": true,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "from",
12
+ "display_name": "From Entity",
13
+ "type": "string",
14
+ "description": "The starting entity — its name (case-insensitive) or id.",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "to",
19
+ "display_name": "To Entity",
20
+ "type": "string",
21
+ "description": "The target entity — its name (case-insensitive) or id.",
22
+ "required": true
23
+ },
24
+ {
25
+ "name": "knowledge_base",
26
+ "display_name": "Knowledge Base",
27
+ "type": "knowledge_base",
28
+ "description": "The knowledge base to traverse, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
29
+ "required": false,
30
+ "default": null
31
+ },
32
+ {
33
+ "name": "max_depth",
34
+ "display_name": "Max Depth",
35
+ "type": "number",
36
+ "description": "Maximum number of hops to search before giving up.",
37
+ "default": 4
38
+ }
39
+ ],
40
+ "outputs": [
41
+ {
42
+ "name": "found",
43
+ "display_name": "Found",
44
+ "type": "boolean",
45
+ "description": "Whether a path exists within max_depth"
46
+ },
47
+ {
48
+ "name": "hops",
49
+ "display_name": "Hops",
50
+ "type": "number",
51
+ "description": "Number of relations in the path (0 when from = to)"
52
+ },
53
+ {
54
+ "name": "steps",
55
+ "display_name": "Steps",
56
+ "type": "array",
57
+ "description": "The path, node by node: [{ node, via_edge? }] — via_edge names the relation that reached each node"
58
+ },
59
+ {
60
+ "name": "success",
61
+ "display_name": "Success",
62
+ "type": "boolean",
63
+ "description": "Whether the search completed successfully"
64
+ },
65
+ {
66
+ "name": "error",
67
+ "display_name": "Error",
68
+ "type": "string",
69
+ "description": "Error message if it failed (including unknown entities)"
70
+ }
71
+ ],
72
+ "settings": []
73
+ }
@@ -0,0 +1,51 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search): a
4
+ // knowledge_base handle names a KB by uuid, falling back to the
5
+ // flow-global KB. See ADR 0023.
6
+ const kbRef = inputs.knowledge_base;
7
+ const knowledgeBase =
8
+ (kbRef && kbRef.uuid
9
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
10
+ : null) ||
11
+ config.integrations?.knowledgeBase ||
12
+ config.integrations?.sqlite;
13
+
14
+ if (!knowledgeBase) {
15
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
16
+ }
17
+
18
+ const { from, to, max_depth = 4 } = inputs;
19
+ if (typeof from !== "string" || from.trim().length === 0) {
20
+ throw new Error("A from entity name or id is required.");
21
+ }
22
+ if (typeof to !== "string" || to.trim().length === 0) {
23
+ throw new Error("A to entity name or id is required.");
24
+ }
25
+
26
+ const result = await knowledgeBase.findPath(from.trim(), to.trim(), {
27
+ max_depth,
28
+ });
29
+
30
+ if (!result.from || !result.to) {
31
+ const missing = !result.from ? from : to;
32
+ return {
33
+ found: false,
34
+ hops: 0,
35
+ steps: [],
36
+ success: false,
37
+ error: `No entity named "${missing}" in this knowledge base. Use List Entities to see what exists.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ found: result.found,
43
+ hops: result.hops,
44
+ steps: result.steps,
45
+ success: true,
46
+ error: null,
47
+ };
48
+ } catch (error) {
49
+ return { found: false, hops: 0, steps: [], success: false, error: error.message };
50
+ }
51
+ };
@@ -18,6 +18,14 @@
18
18
  "type": "number",
19
19
  "description": "The index of the chunk to retrieve (0-based)",
20
20
  "required": true
21
+ },
22
+ {
23
+ "name": "knowledge_base",
24
+ "display_name": "Knowledge Base",
25
+ "type": "knowledge_base",
26
+ "description": "The knowledge base, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
27
+ "required": false,
28
+ "default": null
21
29
  }
22
30
  ],
23
31
  "outputs": [
@@ -1,7 +1,14 @@
1
1
  export default async ({inputs, settings, config, nodeConfig}) => {
2
2
  try {
3
- // Get knowledge base integration
4
- const knowledgeBase = config.integrations?.knowledgeBase || config.integrations?.sqlite;
3
+ // Resolve the KB from a knowledge_base handle (shared wiring), falling
4
+ // back to the flow-global KB. See ADR 0023.
5
+ const kbRef = inputs.knowledge_base;
6
+ const knowledgeBase =
7
+ (kbRef && kbRef.uuid
8
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
9
+ : null) ||
10
+ config.integrations?.knowledgeBase ||
11
+ config.integrations?.sqlite;
5
12
 
6
13
  if (!knowledgeBase) {
7
14
  throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
@@ -0,0 +1,81 @@
1
+ {
2
+ "display_name": "Get Entity Neighbors",
3
+ "tagline": "Walk one hop in a graph",
4
+ "description": "Look up an entity in a Graph knowledge base (by name or id) and return every relation touching it, each with the entity on the far side — one hop of graph traversal. Filter by direction or relation type. Wire a Knowledge Base node into knowledge_base, same as the search nodes.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": true,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "entity",
12
+ "display_name": "Entity",
13
+ "type": "string",
14
+ "description": "The entity to look up — its name (case-insensitive) or id.",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "knowledge_base",
19
+ "display_name": "Knowledge Base",
20
+ "type": "knowledge_base",
21
+ "description": "The knowledge base to traverse, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
22
+ "required": false,
23
+ "default": null
24
+ },
25
+ {
26
+ "name": "direction",
27
+ "display_name": "Direction",
28
+ "type": "string",
29
+ "description": "Which relations to include: both, out (entity is the source), or in (entity is the target).",
30
+ "default": "both",
31
+ "options": ["both", "out", "in"]
32
+ },
33
+ {
34
+ "name": "relation_type",
35
+ "display_name": "Relation Type",
36
+ "type": "string",
37
+ "description": "Optional: only relations of this type (e.g. works_at). Case-insensitive.",
38
+ "default": null
39
+ },
40
+ {
41
+ "name": "limit",
42
+ "display_name": "Limit",
43
+ "type": "number",
44
+ "description": "Maximum number of neighbors to return",
45
+ "default": 50
46
+ }
47
+ ],
48
+ "outputs": [
49
+ {
50
+ "name": "entity",
51
+ "display_name": "Entity",
52
+ "type": "object",
53
+ "description": "The resolved entity row (null if not found)"
54
+ },
55
+ {
56
+ "name": "neighbors",
57
+ "display_name": "Neighbors",
58
+ "type": "array",
59
+ "description": "Relations touching the entity: { edge_id, relation_type, description, direction, node }"
60
+ },
61
+ {
62
+ "name": "count",
63
+ "display_name": "Count",
64
+ "type": "number",
65
+ "description": "Number of neighbors returned"
66
+ },
67
+ {
68
+ "name": "success",
69
+ "display_name": "Success",
70
+ "type": "boolean",
71
+ "description": "Whether the lookup completed successfully"
72
+ },
73
+ {
74
+ "name": "error",
75
+ "display_name": "Error",
76
+ "type": "string",
77
+ "description": "Error message if it failed (including entity not found)"
78
+ }
79
+ ],
80
+ "settings": []
81
+ }
@@ -0,0 +1,51 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Shared KB wiring (see semantic-search / keyword-search): a
4
+ // knowledge_base handle names a KB by uuid, falling back to the
5
+ // flow-global KB. See ADR 0023.
6
+ const kbRef = inputs.knowledge_base;
7
+ const knowledgeBase =
8
+ (kbRef && kbRef.uuid
9
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
10
+ : null) ||
11
+ config.integrations?.knowledgeBase ||
12
+ config.integrations?.sqlite;
13
+
14
+ if (!knowledgeBase) {
15
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
16
+ }
17
+
18
+ const { entity, direction = "both", relation_type = null, limit = 50 } = inputs;
19
+ if (typeof entity !== "string" || entity.trim().length === 0) {
20
+ throw new Error("An entity name or id is required.");
21
+ }
22
+
23
+ const result = await knowledgeBase.getNeighbors(entity.trim(), {
24
+ direction,
25
+ relation_type,
26
+ limit,
27
+ });
28
+
29
+ if (!result.entity) {
30
+ // Soft-fail with a helpful message so an agent can recover by
31
+ // listing entities instead of dead-ending.
32
+ return {
33
+ entity: null,
34
+ neighbors: [],
35
+ count: 0,
36
+ success: false,
37
+ error: `No entity named "${entity}" in this knowledge base. Use List Entities to see what exists.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ entity: result.entity,
43
+ neighbors: result.neighbors,
44
+ count: result.neighbors.length,
45
+ success: true,
46
+ error: null,
47
+ };
48
+ } catch (error) {
49
+ return { entity: null, neighbors: [], count: 0, success: false, error: error.message };
50
+ }
51
+ };
@@ -0,0 +1,67 @@
1
+ {
2
+ "display_name": "Keyword Search",
3
+ "tagline": "Search by exact words",
4
+ "description": "Search a knowledge base for chunks containing a word or phrase (case-insensitive substring match). Free and instant — no embeddings. The exact-term complement to Semantic Search; wire a Knowledge Base node into its knowledge_base input the same way.",
5
+ "category": "knowledge",
6
+ "is_constant": false,
7
+ "is_plugin": true,
8
+ "is_output": false,
9
+ "inputs": [
10
+ {
11
+ "name": "query",
12
+ "display_name": "Search Text",
13
+ "type": "string",
14
+ "description": "Word or phrase to match within chunk content",
15
+ "required": true
16
+ },
17
+ {
18
+ "name": "knowledge_base",
19
+ "display_name": "Knowledge Base",
20
+ "type": "knowledge_base",
21
+ "description": "The knowledge base to search, from a Knowledge Base node. If omitted, falls back to the flow's default knowledge base.",
22
+ "required": false,
23
+ "default": null
24
+ },
25
+ {
26
+ "name": "limit",
27
+ "display_name": "Result Limit",
28
+ "type": "number",
29
+ "description": "Maximum number of results to return",
30
+ "default": 10
31
+ },
32
+ {
33
+ "name": "document_id",
34
+ "display_name": "Document ID Filter",
35
+ "type": "string",
36
+ "description": "Optional: restrict results to a specific document uuid.",
37
+ "default": null
38
+ }
39
+ ],
40
+ "outputs": [
41
+ {
42
+ "name": "results",
43
+ "display_name": "Search Results",
44
+ "type": "array",
45
+ "description": "Matching chunks, most matches first"
46
+ },
47
+ {
48
+ "name": "count",
49
+ "display_name": "Result Count",
50
+ "type": "number",
51
+ "description": "Number of results returned"
52
+ },
53
+ {
54
+ "name": "success",
55
+ "display_name": "Success",
56
+ "type": "boolean",
57
+ "description": "Whether the search completed successfully"
58
+ },
59
+ {
60
+ "name": "error",
61
+ "display_name": "Error",
62
+ "type": "string",
63
+ "description": "Error message if search failed"
64
+ }
65
+ ],
66
+ "settings": []
67
+ }
@@ -0,0 +1,45 @@
1
+ export default async ({ inputs, config }) => {
2
+ try {
3
+ // Resolve which knowledge base to search — SAME wiring as semantic-search
4
+ // and query-knowledge-base: a `knowledge_base` handle wired in from a
5
+ // Knowledge Base node names a specific KB by uuid (resolved to a per-KB
6
+ // integration keyed knowledgeBase:<uuid>), falling back to the
7
+ // flow-global KB. See ADR 0023.
8
+ const kbRef = inputs.knowledge_base;
9
+ const knowledgeBase =
10
+ (kbRef && kbRef.uuid
11
+ ? config.integrations?.[`knowledgeBase:${kbRef.uuid}`]
12
+ : null) ||
13
+ config.integrations?.knowledgeBase ||
14
+ config.integrations?.sqlite;
15
+
16
+ if (!knowledgeBase) {
17
+ throw new Error("Knowledge base integration not found. Make sure a knowledge database is available.");
18
+ }
19
+
20
+ const { query, limit = 10, document_id = null } = inputs;
21
+
22
+ if (!query || typeof query !== 'string') {
23
+ throw new Error("Query is required and must be a string");
24
+ }
25
+
26
+ // No embeddings — a case-insensitive substring match over chunk content.
27
+ const results = await knowledgeBase.keywordSearch(query, { limit, document_id });
28
+
29
+ return {
30
+ results: results,
31
+ count: results.length,
32
+ success: true,
33
+ error: null
34
+ };
35
+
36
+ } catch (error) {
37
+ // Return error information instead of throwing to prevent engine crash
38
+ return {
39
+ results: [],
40
+ count: 0,
41
+ success: false,
42
+ error: error.message
43
+ };
44
+ }
45
+ };