langflower 0.0.9 → 0.1.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 (223) hide show
  1. package/README.md +96 -70
  2. package/docs/public/README.md +2 -2
  3. package/docs/public/configuration.md +4 -0
  4. package/docs/public/extending.md +18 -2
  5. package/docs/public/getting-started.md +7 -0
  6. package/docs/public/how-it-works.md +18 -11
  7. package/docs/public/product.md +35 -25
  8. package/package.json +2 -2
  9. package/ui-dist/chunk-QI4IYILJ.js +4 -0
  10. package/ui-dist/{chunk-7UMVYN3A.js → chunk-UG3GFV4Y.js} +1 -1
  11. package/ui-dist/index.html +2 -2
  12. package/ui-dist/main-ODIDCCNX.js +315 -0
  13. package/ui-dist/styles-2JMBAQC3.css +1 -0
  14. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-reducer.js +0 -12
  15. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.d.ts +2 -7
  16. package/vendor/common-nodes/dist/ai/features/llm-loop/llm-loop-types.js +1 -1
  17. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.d.ts +0 -3
  18. package/vendor/common-nodes/dist/ai/features/llm-loop/run-agent-loop.js +22 -36
  19. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.d.ts +1 -7
  20. package/vendor/common-nodes/dist/ai/features/llm-loop/run-llm-loop.js +12 -35
  21. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.d.ts +5 -12
  22. package/vendor/common-nodes/dist/ai/features/llm-session/llm-session-shell.js +48 -21
  23. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.d.ts +11 -0
  24. package/vendor/common-nodes/dist/ai/features/llm-session/run-session-machine.js +2 -2
  25. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.d.ts +0 -3
  26. package/vendor/common-nodes/dist/ai/features/path-choice/run-reactive-path-choice-loop.js +2 -15
  27. package/vendor/common-nodes/dist/ai/features/run-host-services.d.ts +10 -0
  28. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.d.ts +1 -0
  29. package/vendor/common-nodes/dist/ai/features/sub-agent-protocol.js +1 -0
  30. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.d.ts +1 -1
  31. package/vendor/common-nodes/dist/ai/features/ui-schema/llm-recovery-ui-schema.js +1 -1
  32. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.d.ts +1 -0
  33. package/vendor/common-nodes/dist/ai/features/wait-for-subagent-result.js +1 -0
  34. package/vendor/common-nodes/dist/ai/nodes/critique/node.d.ts +3 -3
  35. package/vendor/common-nodes/dist/ai/nodes/critique/node.js +10 -19
  36. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.d.ts +3 -3
  37. package/vendor/common-nodes/dist/ai/nodes/fake-llm/node.js +10 -11
  38. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.d.ts +3 -3
  39. package/vendor/common-nodes/dist/ai/nodes/openai-llm/node.js +7 -10
  40. package/vendor/common-nodes/dist/ai/nodes/review/node.d.ts +3 -3
  41. package/vendor/common-nodes/dist/ai/nodes/review/node.js +12 -19
  42. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.d.ts +5 -5
  43. package/vendor/common-nodes/dist/ai/nodes/sub-agent/node.js +251 -96
  44. package/vendor/common-nodes/dist/catalog.js +12 -0
  45. package/vendor/common-nodes/dist/crawl/crawl/node.js +11 -3
  46. package/vendor/common-nodes/dist/crawl/crawl-tools/node.js +5 -1
  47. package/vendor/common-nodes/dist/crawl/extract-links/node.js +7 -1
  48. package/vendor/common-nodes/dist/crawl/fetch-url/node.js +13 -4
  49. package/vendor/common-nodes/dist/crawl/save-page/node.js +11 -3
  50. package/vendor/common-nodes/dist/embeddings/create-embedding.d.ts +39 -0
  51. package/vendor/common-nodes/dist/embeddings/create-embedding.js +154 -0
  52. package/vendor/common-nodes/dist/embeddings/embed-provider/node.d.ts +53 -0
  53. package/vendor/common-nodes/dist/embeddings/embed-provider/node.js +126 -0
  54. package/vendor/common-nodes/dist/embeddings/embed-similarity/node.d.ts +20 -0
  55. package/vendor/common-nodes/dist/embeddings/embed-similarity/node.js +81 -0
  56. package/vendor/common-nodes/dist/embeddings/embed-text/node.d.ts +53 -0
  57. package/vendor/common-nodes/dist/embeddings/embed-text/node.js +91 -0
  58. package/vendor/common-nodes/dist/embeddings/from-embedding.d.ts +8 -0
  59. package/vendor/common-nodes/dist/embeddings/from-embedding.js +24 -0
  60. package/vendor/common-nodes/dist/embeddings/resolve-embedding-provider-model.d.ts +11 -0
  61. package/vendor/common-nodes/dist/embeddings/resolve-embedding-provider-model.js +8 -0
  62. package/vendor/common-nodes/dist/flow/checkpoint/node.js +7 -3
  63. package/vendor/common-nodes/dist/flow/delay/node.d.ts +2 -6
  64. package/vendor/common-nodes/dist/flow/delay/node.js +13 -9
  65. package/vendor/common-nodes/dist/flow/loop/node.js +7 -1
  66. package/vendor/common-nodes/dist/flow/merge/node.js +7 -1
  67. package/vendor/common-nodes/dist/flow/repeat/node.js +5 -1
  68. package/vendor/common-nodes/dist/flow/router/node.js +7 -1
  69. package/vendor/common-nodes/dist/hitl/chat-input/node.d.ts +2 -2
  70. package/vendor/common-nodes/dist/hitl/chat-input/node.js +8 -4
  71. package/vendor/common-nodes/dist/hitl/review-gate/node.js +11 -5
  72. package/vendor/common-nodes/dist/langflower-tools/node.js +5 -1
  73. package/vendor/common-nodes/dist/logic/assert/node.js +7 -1
  74. package/vendor/common-nodes/dist/logic/compare/node.js +7 -1
  75. package/vendor/common-nodes/dist/logic/gate/node.js +7 -1
  76. package/vendor/common-nodes/dist/logic/if/node.js +7 -1
  77. package/vendor/common-nodes/dist/logic/switch/node.js +7 -1
  78. package/vendor/common-nodes/dist/mcp/mcp-http/node.d.ts +4 -4
  79. package/vendor/common-nodes/dist/mcp/mcp-http/node.js +61 -60
  80. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.d.ts +4 -4
  81. package/vendor/common-nodes/dist/mcp/mcp-stdio/node.js +58 -57
  82. package/vendor/common-nodes/dist/memory/memory-tools/node.js +5 -1
  83. package/vendor/common-nodes/dist/output/finish/node.js +13 -1
  84. package/vendor/common-nodes/dist/output/preview/node.js +9 -2
  85. package/vendor/common-nodes/dist/primitives/boolean/node.js +7 -1
  86. package/vendor/common-nodes/dist/primitives/number/node.js +7 -1
  87. package/vendor/common-nodes/dist/primitives/string/node.js +7 -1
  88. package/vendor/common-nodes/dist/primitives/string-multiline/node.d.ts +17 -0
  89. package/vendor/common-nodes/dist/primitives/string-multiline/node.js +31 -0
  90. package/vendor/common-nodes/dist/text/append-file/node.js +11 -3
  91. package/vendor/common-nodes/dist/text/concat/node.js +5 -1
  92. package/vendor/common-nodes/dist/text/read-file/node.js +9 -3
  93. package/vendor/common-nodes/dist/text/split-paced/node.d.ts +21 -0
  94. package/vendor/common-nodes/dist/text/split-paced/node.js +93 -0
  95. package/vendor/common-nodes/dist/text/write-file/node.js +11 -3
  96. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.d.ts +4 -4
  97. package/vendor/common-nodes/dist/tools/collect-agent-tool-handles.js +3 -11
  98. package/vendor/common-nodes/dist/tools/inventory-tool-round.d.ts +0 -10
  99. package/vendor/common-nodes/dist/tools/inventory-tool-round.js +0 -84
  100. package/vendor/common-nodes/dist/tools/tool-collection/node.d.ts +20 -0
  101. package/vendor/common-nodes/dist/tools/tool-collection/node.js +45 -0
  102. package/vendor/common-nodes/package.json +5 -5
  103. package/vendor/compiler/package.json +1 -1
  104. package/vendor/node-sdk/dist/node-factory/define-embed/embed-handle.d.ts +25 -0
  105. package/vendor/node-sdk/dist/node-factory/define-embed/embed-handle.js +25 -0
  106. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.d.ts +3 -7
  107. package/vendor/node-sdk/dist/node-factory/define-llm-node/default-llm-ports.js +4 -44
  108. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.d.ts +3 -5
  109. package/vendor/node-sdk/dist/node-factory/define-llm-node/define-llm-node.js +3 -5
  110. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.d.ts +1 -0
  111. package/vendor/node-sdk/dist/node-factory/define-llm-node/llm-inventory-wire.js +1 -0
  112. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.d.ts +4 -4
  113. package/vendor/node-sdk/dist/node-factory/define-mcp/mcp-handle.js +2 -1
  114. package/vendor/node-sdk/dist/node-factory/define-node/define-node.d.ts +2 -0
  115. package/vendor/node-sdk/dist/node-factory/define-node/define-node.js +4 -1
  116. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.d.ts +3 -1
  117. package/vendor/node-sdk/dist/node-factory/define-reactive-node/define-reactive-node.js +2 -1
  118. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.d.ts +15 -0
  119. package/vendor/node-sdk/dist/node-factory/define-reactive-node/io-helpers.js +17 -1
  120. package/vendor/node-sdk/dist/node-factory/define-reactive-node/port-meta.d.ts +17 -4
  121. package/vendor/node-sdk/dist/node-factory/define-reactive-node/test/samples/delay-node.js +4 -2
  122. package/vendor/node-sdk/dist/node-factory/define-reactive-node/types.d.ts +6 -7
  123. package/vendor/node-sdk/dist/node-factory/define-reactive-node/ui-schema-inference.d.ts +1 -1
  124. package/vendor/node-sdk/dist/testing/create-node-harness.d.ts +30 -0
  125. package/vendor/node-sdk/dist/testing/create-node-harness.js +68 -0
  126. package/vendor/node-sdk/package.json +5 -5
  127. package/vendor/runtime/dist/port-feed-override.d.ts +13 -0
  128. package/vendor/runtime/dist/port-feed-override.js +21 -0
  129. package/vendor/runtime/dist/port-signal-from-response.d.ts +15 -0
  130. package/vendor/runtime/dist/port-signal-from-response.js +48 -0
  131. package/vendor/runtime/dist/runtime-runner.d.ts +8 -3
  132. package/vendor/runtime/dist/runtime-runner.js +102 -81
  133. package/vendor/runtime/dist/runtime.d.ts +3 -2
  134. package/vendor/runtime/dist/runtime.js +1 -1
  135. package/vendor/runtime/dist/testing/workflows/workflow-events.d.ts +2 -0
  136. package/vendor/runtime/dist/testing/workflows/workflow-events.js +7 -5
  137. package/vendor/runtime/dist/types.d.ts +26 -9
  138. package/vendor/runtime/dist/types.js +3 -0
  139. package/vendor/runtime/package.json +1 -1
  140. package/vendor/server/dist/bootstrap/project-bootstrap.service.d.ts +1 -1
  141. package/vendor/server/dist/bootstrap/seed-skeleton.d.ts +2 -2
  142. package/vendor/server/dist/bootstrap/seed-skeleton.js +17 -2
  143. package/vendor/server/dist/bridge/bind-embed-context.d.ts +7 -0
  144. package/vendor/server/dist/bridge/bind-embed-context.js +16 -0
  145. package/vendor/server/dist/bridge/build-execution-context.d.ts +2 -2
  146. package/vendor/server/dist/bridge/build-execution-context.js +8 -4
  147. package/vendor/server/dist/bridge/emit-bootstrap.js +4 -1
  148. package/vendor/server/dist/bridge/get-live-wired-tools.d.ts +3 -2
  149. package/vendor/server/dist/bridge/get-live-wired-tools.js +8 -5
  150. package/vendor/server/dist/bridge/settings-draft-controller.js +7 -3
  151. package/vendor/server/dist/bridge/wire-config-handlers.js +2 -0
  152. package/vendor/server/dist/bridge/wire-editor-handlers.js +14 -0
  153. package/vendor/server/dist/bridge/wire-runner-handlers.js +2 -2
  154. package/vendor/server/dist/bridge/wire-workflow-handlers.js +23 -9
  155. package/vendor/server/dist/checkpoint/run-checkpoint-session.js +3 -3
  156. package/vendor/server/dist/config/langflower-config.service.d.ts +2 -0
  157. package/vendor/server/dist/config/langflower-config.service.js +30 -0
  158. package/vendor/server/dist/session/build-session-bootstrap.js +1 -0
  159. package/vendor/server/dist/session/langflower-session.d.ts +3 -1
  160. package/vendor/server/dist/session/langflower-session.js +3 -1
  161. package/vendor/server/dist/session/reset-session-execution-feed.d.ts +6 -0
  162. package/vendor/server/dist/session/reset-session-execution-feed.js +8 -0
  163. package/vendor/server/dist/workflow/apply-editor-mutation.js +6 -0
  164. package/vendor/server/dist/workflow/workflow-persisted-inputs.js +8 -6
  165. package/vendor/server/skeleton/instructions.md +15 -3
  166. package/vendor/server/skeleton/nodes/hello-embed/README.md +185 -0
  167. package/vendor/server/skeleton/nodes/hello-embed/hello-embed.nodes.test.ts +181 -0
  168. package/vendor/server/skeleton/nodes/hello-embed/ingest.ts +126 -0
  169. package/vendor/server/skeleton/nodes/hello-embed/lib/chunk-markdown.test.ts +40 -0
  170. package/vendor/server/skeleton/nodes/hello-embed/lib/chunk-markdown.ts +123 -0
  171. package/vendor/server/skeleton/nodes/hello-embed/lib/ingest-search.test.ts +180 -0
  172. package/vendor/server/skeleton/nodes/hello-embed/lib/ingest.ts +128 -0
  173. package/vendor/server/skeleton/nodes/hello-embed/lib/paths.test.ts +20 -0
  174. package/vendor/server/skeleton/nodes/hello-embed/lib/paths.ts +70 -0
  175. package/vendor/server/skeleton/nodes/hello-embed/lib/schema.ts +21 -0
  176. package/vendor/server/skeleton/nodes/hello-embed/lib/search.ts +254 -0
  177. package/vendor/server/skeleton/nodes/hello-embed/lib/text-embedder.ts +14 -0
  178. package/vendor/server/skeleton/nodes/hello-embed/lib/vectors.ts +126 -0
  179. package/vendor/server/skeleton/nodes/hello-embed/lib/walk-markdown.test.ts +62 -0
  180. package/vendor/server/skeleton/nodes/hello-embed/lib/walk-markdown.ts +67 -0
  181. package/vendor/server/skeleton/nodes/hello-embed/package.json +11 -0
  182. package/vendor/server/skeleton/nodes/hello-embed/search-handle.ts +119 -0
  183. package/vendor/server/skeleton/nodes/hello-embed/search.ts +98 -0
  184. package/vendor/server/skeleton/nodes/hello-embed/tsconfig.json +14 -0
  185. package/vendor/server/skeleton/nodes/my-nodes/README.md +13 -3
  186. package/vendor/server/skeleton/nodes/my-nodes/package.json +1 -1
  187. package/vendor/server/skeleton/schemas/langflower-config.schema.json +4 -0
  188. package/vendor/server/skeleton/skills/langflower-helper/SKILL.md +125 -38
  189. package/vendor/server/skeleton/skills/langflower-helper/architecture.md +17 -6
  190. package/vendor/server/skeleton/skills/langflower-helper/layout.md +24 -17
  191. package/vendor/server/skeleton/skills/langflower-node-writer/SKILL.md +8 -1
  192. package/vendor/server/skeleton/skills/langflower-workflow-writer/SKILL.md +12 -8
  193. package/vendor/server/skeleton/workflows/kb-create.json +15 -43
  194. package/vendor/server/skeleton/workflows/kb-ingest.json +88 -0
  195. package/vendor/server/skeleton/workflows/kb-manual-search.json +105 -0
  196. package/vendor/server/skeleton/workflows/kb-navigate.json +8 -22
  197. package/vendor/server/skeleton/workflows/kb-rag.json +192 -0
  198. package/vendor/server/skeleton/workflows/kb-tool.json +111 -0
  199. package/vendor/server/skeleton/workflows/simple-coder.json +18 -46
  200. package/vendor/server/skeleton/workflows/starter.json +8 -22
  201. package/vendor/shared/dist/execution/derive-run-settle-outcome.js +2 -2
  202. package/vendor/shared/dist/execution/derive-run-settle-outcome.test.js +11 -8
  203. package/vendor/shared/dist/langflower-bus-config.d.ts +22 -6
  204. package/vendor/shared/dist/langflower-bus-config.js +23 -7
  205. package/vendor/shared/dist/langflower-config/merge-langflower-config-layers.test.js +8 -0
  206. package/vendor/shared/dist/langflower-config/settings-draft.d.ts +5 -0
  207. package/vendor/shared/dist/langflower-config/settings-draft.js +15 -7
  208. package/vendor/shared/dist/langflower-ws-waits.d.ts +3 -1
  209. package/vendor/shared/dist/langflower-ws-waits.js +2 -2
  210. package/vendor/shared/dist/langflower.d.ts +1 -1
  211. package/vendor/shared/dist/langflower.js +1 -1
  212. package/vendor/shared/dist/types/langflower-bootstrap.d.ts +6 -0
  213. package/vendor/shared/dist/types/langflower-config.d.ts +17 -0
  214. package/vendor/shared/dist/types/langflower-palette.d.ts +2 -1
  215. package/vendor/tools/dist/harness-types.d.ts +1 -1
  216. package/vendor/tools/dist/mcp/build-mcp-handle.d.ts +3 -21
  217. package/vendor/tools/dist/mcp/build-mcp-handle.js +2 -11
  218. package/vendor/tools/dist/mcp/create-system-mcp-handles.d.ts +9 -6
  219. package/vendor/tools/dist/mcp/create-system-mcp-handles.js +21 -17
  220. package/vendor/tools/package.json +1 -0
  221. package/ui-dist/chunk-MDGXX73Z.js +0 -4
  222. package/ui-dist/main-2JJGCNTD.js +0 -313
  223. package/ui-dist/styles-EOXODOI7.css +0 -1
@@ -0,0 +1,185 @@
1
+ # hello-embed
2
+
3
+ Sample custom-node pack: index project markdown into a local SQLite store
4
+ (vectors + FTS5), retrieve with cosine + keyword (RRF), then pack full chunks
5
+ as Question + Context for a wire or an LLM tool.
6
+
7
+ Use the seeded workflows as-is for simple “search my `.md` files” tasks, or
8
+ copy this pack when you need a different corpus, chunker, or ranking.
9
+
10
+ ## What it is
11
+
12
+ Three nodes in this folder, four workflows in `.langflower/workflows/`:
13
+
14
+ | Node | Role |
15
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
16
+ | `hello-embed-ingest` | Walk `**/*.md`, split on headings, embed one chunk at a time → sqlite. Streaming `progress` + `finish`. |
17
+ | `hello-embed-search` | `query` + `embed` → `hits` (JSON) and `text` (`Question` + full-chunk `Context`). Hybrid cosine + FTS5, RRF. |
18
+ | `hello-embed-search-handle` | Same retrieve as an LLM tool: `project_search` on `tools`. |
19
+
20
+ | Workflow | Graph |
21
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
22
+ | `kb-ingest` | Embed provider → ingest → Finish. Progress in the feed. |
23
+ | `kb-manual-search` | Chat / string query → search → Preview. |
24
+ | `kb-tool` | `project_search` wired into an agent `tools` port. |
25
+ | `kb-rag` | Hybrid retrieve → packed `search.text` as LLM `userPrompt`; `project_search` + grep/read; Review ⇄ feedback; Approve → Finish. |
26
+
27
+ The index file defaults to `.langflower/.cache/hello-embed/kb.sqlite` (outside
28
+ this pack, so bootstrap force-seed does not wipe vectors). All three nodes
29
+ share that path via `DEFAULT_SQLITE_PATH` in `lib/paths.ts`.
30
+
31
+ Walk skips `node_modules`, `.git`, and `.langflower/.cache`. Embeddings come
32
+ from a wired `common-embed-provider` (`EmbedHandle`). The pack never sees API
33
+ keys.
34
+
35
+ ## How to use
36
+
37
+ 1. **Settings → Default embedding model** (OpenAI-compatible embeddings).
38
+ 2. Run **KB ingest**. Re-run it after you add/edit markdown or switch the
39
+ embedding model (dimension mismatch is rejected). An index built before
40
+ embeddings `encoding_format: 'float'` may store all-zero vectors — search
41
+ then scores every hit `0.00`; wipe by running ingest again on a current
42
+ Langflower build.
43
+ 3. Then run **KB manual search**, **KB tool**, or **KB RAG**.
44
+
45
+ `langflower start` compiles custom packs on startup, including this one.
46
+ After you edit the pack, Helper `compile_custom_nodes`, Custom → **Update**,
47
+ or restart.
48
+
49
+ Place **one** `common-embed-provider` and fan-out **embed** into ingest and/or
50
+ search nodes. Ingest uses the handle with role `document`; search / the tool
51
+ use role `query`.
52
+
53
+ Optional panel fields: `sqlitePath`, ingest `sourceDir` (empty = project
54
+ root), search `topK`.
55
+
56
+ ## KB RAG
57
+
58
+ RAG means **retrieval-augmented generation**. An LLM does not automatically
59
+ know your private or recently changed project documents. RAG finds the most
60
+ relevant passages first and sends them to the LLM together with the question.
61
+ The model can then answer from that context instead of relying only on what it
62
+ learned during training.
63
+
64
+ RAG is useful when answers must come from a specific knowledge base. It can
65
+ find synonyms and close-in-meaning phrases that plain text matching may miss,
66
+ and it gives the model evidence it can quote and cite. It does not train or
67
+ change the model; it supplies relevant information for the current answer.
68
+
69
+ ### Two phases
70
+
71
+ RAG has a preparation phase and a question-answering phase.
72
+
73
+ 1. **Ingest the documents.** Read the knowledge base, split it into useful
74
+ passages, create embeddings, and save a searchable index. Run **KB ingest**
75
+ before using any search or RAG workflow. Without the index there are no
76
+ passages to retrieve, so `kb-rag` cannot answer from the project docs.
77
+ Run ingest again after the docs or embedding model change.
78
+ 2. **Retrieve, then generate.** Embed the question, search the index, rank the
79
+ best passages, and pack them into `Context`. Send the question and Context
80
+ to the LLM, which writes an answer grounded in those passages.
81
+
82
+ ### Glossary
83
+
84
+ - **Ingest.** Walk the corpus once, split it, embed it, and write an index. In
85
+ this pack: `hello-embed-ingest` / **KB ingest**.
86
+ - **Chunks.** The retrieval unit — a slice of a document, not the whole file.
87
+ Split so one hit is about one thing: a whole README in Context can drown the
88
+ answer, while a tiny fragment may not contain enough evidence. This pack
89
+ uses one markdown heading plus its body.
90
+ - **Vectors.** Fixed-length lists of numbers produced by an embedding model.
91
+ Texts with close meanings have nearby vectors.
92
+ - **FTS5.** SQLite full-text search. It matches exact keywords and tokens in
93
+ headings and bodies.
94
+ - **Cosine similarity.** A way to measure how close two vectors are. This
95
+ sample uses it to rank chunks by meaning.
96
+ - **RRF** (reciprocal rank fusion). A method for combining the vector-search
97
+ and keyword-search rankings into one result list.
98
+ - **Reranking.** A second scoring step that reorders retrieved passages and
99
+ removes weak results before they are sent to the LLM.
100
+ - **HyDE** (Hypothetical Document Embeddings). The LLM drafts a possible
101
+ answer, and that draft is embedded and used to search for related passages.
102
+
103
+ ### Implemented in this sample
104
+
105
+ - **Document ingest.** `hello-embed-ingest` reads project markdown and creates
106
+ a fresh local index.
107
+ - **Chunking.** Each markdown heading and its body becomes one chunk. Chunks
108
+ keep retrieval focused: a whole file may cover too many topics, while a tiny
109
+ fragment may not contain enough evidence.
110
+ - **Embeddings.** The embedding model turns each chunk and the question into
111
+ vectors. Texts with similar meanings get nearby vectors, so search can find
112
+ a relevant passage even when it uses different words.
113
+ - **Searchable index.** SQLite stores the prepared chunks, vectors, and
114
+ text-search data. Questions query this store instead of rebuilding
115
+ embeddings for the whole corpus.
116
+ - **Hybrid retrieval.** Cosine similarity finds passages with similar
117
+ meaning. FTS5 finds exact words such as API names. RRF combines both ranked
118
+ lists so either kind of match can reach the final results.
119
+ - **Context packing.** The best full chunks are formatted as `Question` +
120
+ `Context`. The LLM receives complete section bodies rather than isolated
121
+ matching lines.
122
+ - **Grounded generation.** `kb-rag` always retrieves before the first LLM
123
+ turn. The system prompt asks the agent to answer from Context and cite each
124
+ source by path and heading.
125
+ - **Optional multi-hop retrieval.** After reading the initial Context, the
126
+ agent can call `project_search` with a new query based on what it learned.
127
+ This creates a second retrieval hop when needed, but the tool may not be
128
+ called on every run.
129
+
130
+ ### Extra behavior in `kb-rag`
131
+
132
+ - `grep`, `read`, and `glob` let the agent open the source around a retrieved
133
+ path when it needs more detail.
134
+ - Review sends human feedback back to the same agent session; Approve sends
135
+ the accepted response to Finish.
136
+
137
+ For comparison, **KB manual search** stops after showing retrieved hits.
138
+ **KB tool** gives the model a search tool but lets it decide whether to use it.
139
+ **KB RAG** always performs the first retrieval before generation.
140
+
141
+ ### Not in this sample
142
+
143
+ - **Reranking** with a second model to remove weak results before building
144
+ Context (TBD-009 in `docs/TBD.md`).
145
+ - **HyDE or query rewriting** to improve retrieval for vague questions.
146
+ - **Overlapping or token-window chunks** for documents that do not divide
147
+ cleanly at headings.
148
+ - **Token-budgeted Context** to fit results into smaller model windows.
149
+ - **Automated evaluation** for retrieval quality and answer faithfulness.
150
+
151
+ Copy the pack if you need those. This workflow is a compact RAG example, not a
152
+ complete knowledge-base product.
153
+
154
+ ## Adapt for your case
155
+
156
+ Keep this folder as the sample. For a domain-specific index, copy the pack,
157
+ rename the `type` strings, and change the pieces you actually need:
158
+
159
+ | You need | Start from | Typical change |
160
+ | ------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | Other files (`.ts`, PDFs, XML) | `lib/walk-markdown.ts` | Filter, parser, skip list |
162
+ | Different chunking | `lib/chunk-markdown.ts` | Size windows, code symbols, breadcrumbs |
163
+ | Another store or path | `lib/schema.ts`, `lib/paths.ts` | Table layout, default path, wipe vs upsert |
164
+ | Hybrid retrieve | `lib/search.ts`, `lib/schema.ts` | Cosine + FTS5 fused with RRF (`k = 60`). `hits[].score` is RRF, not raw cosine. Candidate pool `max(topK * 4, 32)` then slice top-K. |
165
+ | Graph search (Preview / Finish) | `search.ts` + `kb-manual-search` | `text` is Question + full-chunk Context (same packing as RAG) |
166
+ | Agent-callable search | `search-handle.ts` + `kb-tool` | ToolHandle `invoke` → packed `text`; sqlite path from params |
167
+ | Retrieve-then-generate | `kb-rag` | Forced hybrid retrieve into LLM `userPrompt`. Wire `hello-embed-search-handle` for a second retrieve; grep/read for files. Review `feedback` → agent; Approve `response` → Finish. Do **not** zip with `common-concat` (`multi: 'zip'` stalls) |
168
+ | Streaming ingest progress | `ingest.ts` | `defineReactiveNode`; `progress` `{ role: 'progress', streaming: true }` (not `result` bubbles); `finish` `{ role: 'none'` } |
169
+ | Safer Stop / resume | ingest embed loop | Sequential one-chunk `embedTexts` (this sample) |
170
+
171
+ Contracts to keep:
172
+
173
+ - Wire **`EmbedHandle`** from `common-embed-provider`, not a raw HTTP client
174
+ and not a `ToolHandle`.
175
+ - Close over resolved `sqlitePath` from `ctx.params` in the tool handler —
176
+ do not put pack paths on `ToolHandlerContext`.
177
+ - One shared default path constant; do not duplicate the string in nodes or
178
+ workflows.
179
+ - Internal `from './file.ts'` needs `allowImportingTsExtensions` (already set
180
+ here, with `noEmit`). Without it `tsc --noEmit` fails and the pack does
181
+ not compile.
182
+
183
+ Host peers stay `@langflower/node-sdk` / `rxjs` / `@rx-evo/stateful-observable`
184
+ (same as `my-nodes`). Add author `dependencies` only if you pull extra
185
+ libraries, then `npm install` inside the pack.
@@ -0,0 +1,181 @@
1
+ import type { EmbedHandle, ToolHandle } from '@langflower/node-sdk';
2
+ import { createNodeHarness } from '@langflower/node-sdk/testing';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import ingestNode from './ingest.ts';
8
+ import { DEFAULT_SQLITE_PATH } from './lib/paths.ts';
9
+ import { l2Normalize } from './lib/vectors.ts';
10
+ import searchNode from './search.ts';
11
+ import searchHandleNode from './search-handle.ts';
12
+
13
+ const sqliteDefaultOf = (definition: {
14
+ readonly uiSchema: readonly {
15
+ readonly field: string;
16
+ readonly default?: unknown;
17
+ }[];
18
+ }): unknown =>
19
+ definition.uiSchema.find((item) => item.field === 'sqlitePath')?.default;
20
+
21
+ const fakeHandle = (): EmbedHandle => ({
22
+ dim: 8,
23
+ embedTexts: async (texts) =>
24
+ texts.map((text) => {
25
+ const values = new Array<number>(8).fill(0);
26
+ if (text.toLowerCase().includes('alpha')) {
27
+ values[0] = 1;
28
+ } else if (text.toLowerCase().includes('beta')) {
29
+ values[1] = 1;
30
+ } else {
31
+ values[7] = 1;
32
+ }
33
+ return l2Normalize(values);
34
+ }),
35
+ });
36
+
37
+ describe('hello-embed nodes', () => {
38
+ let root: string;
39
+
40
+ beforeEach(async () => {
41
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'hello-embed-node-'));
42
+ await fs.writeFile(
43
+ path.join(root, 'notes.md'),
44
+ '# Alpha\nalpha body\n',
45
+ 'utf8',
46
+ );
47
+ });
48
+
49
+ afterEach(async () => {
50
+ await fs.rm(root, { recursive: true, force: true });
51
+ });
52
+
53
+ it('shares DEFAULT_SQLITE_PATH as uiSchema default', () => {
54
+ expect(sqliteDefaultOf(ingestNode)).toBe(DEFAULT_SQLITE_PATH);
55
+ expect(sqliteDefaultOf(searchNode)).toBe(DEFAULT_SQLITE_PATH);
56
+ expect(sqliteDefaultOf(searchHandleNode)).toBe(DEFAULT_SQLITE_PATH);
57
+ });
58
+
59
+ it('marks ingest progress as a growing progress stream', () => {
60
+ const progress = ingestNode.outputsConfigs.find(
61
+ (port) => port.portId === 'progress',
62
+ );
63
+ expect(progress?.feed).toEqual({
64
+ role: 'progress',
65
+ streaming: true,
66
+ });
67
+ const finish = ingestNode.outputsConfigs.find(
68
+ (port) => port.portId === 'finish',
69
+ );
70
+ expect(finish?.feed).toEqual({ role: 'none' });
71
+ });
72
+
73
+ it('ingests then searches through createNodeHarness', async () => {
74
+ const sqliteRel = 'index.sqlite';
75
+ const ingest = createNodeHarness(ingestNode, {
76
+ projectDir: root,
77
+ params: { sqlitePath: sqliteRel },
78
+ });
79
+ const progress = ingest.collect<string>('progress');
80
+ const finished = ingest.next<boolean>('finish');
81
+ ingest.send('embed', fakeHandle());
82
+ ingest.send('trigger', true);
83
+ await expect(finished).resolves.toBe(true);
84
+ expect(progress.values.some((line) => line.includes('notes.md'))).toBe(
85
+ true,
86
+ );
87
+ ingest.dispose();
88
+
89
+ const search = createNodeHarness(searchNode, {
90
+ projectDir: root,
91
+ params: { sqlitePath: sqliteRel, topK: 8 },
92
+ });
93
+ const text = search.next<string>('text');
94
+ const hits = search.next<readonly { heading: string }[]>('hits');
95
+ search.send('embed', fakeHandle());
96
+ search.send('query', 'alpha');
97
+ await expect(text).resolves.toContain('Question:\nalpha');
98
+ await expect(text).resolves.toContain('Context:');
99
+ await expect(text).resolves.toContain('alpha body');
100
+ await expect(hits).resolves.toEqual(
101
+ expect.arrayContaining([
102
+ expect.objectContaining({ heading: 'Alpha' }),
103
+ ]),
104
+ );
105
+ search.dispose();
106
+ });
107
+
108
+ it('search-handle emits project_search against the same sqlite path', async () => {
109
+ const sqliteRel = 'index.sqlite';
110
+ const ingest = createNodeHarness(ingestNode, {
111
+ projectDir: root,
112
+ params: { sqlitePath: sqliteRel },
113
+ });
114
+ const finished = ingest.next<boolean>('finish');
115
+ ingest.send('embed', fakeHandle());
116
+ ingest.send('trigger', true);
117
+ await finished;
118
+ ingest.dispose();
119
+
120
+ const handle = createNodeHarness(searchHandleNode, {
121
+ projectDir: root,
122
+ params: { sqlitePath: sqliteRel },
123
+ });
124
+ const toolsP = handle.next<readonly ToolHandle[]>('tools');
125
+ handle.send('embed', fakeHandle());
126
+ const tools = await toolsP;
127
+ const projectSearch = tools.find(
128
+ (tool) => tool.toolId === 'project_search',
129
+ );
130
+ expect(projectSearch).toBeDefined();
131
+ const text = await projectSearch?.invoke(
132
+ { query: 'alpha' },
133
+ {
134
+ projectDir: root,
135
+ runId: 'test',
136
+ },
137
+ );
138
+ expect(text).toContain('Question:\nalpha');
139
+ expect(text).toContain('alpha body');
140
+ handle.dispose();
141
+ });
142
+
143
+ it('search-handle returns full chunk bodies', async () => {
144
+ const longBody = `alpha ${'y'.repeat(280)}`;
145
+ await fs.writeFile(
146
+ path.join(root, 'notes.md'),
147
+ `# Alpha\n${longBody}\n`,
148
+ 'utf8',
149
+ );
150
+ const sqliteRel = 'index.sqlite';
151
+ const ingest = createNodeHarness(ingestNode, {
152
+ projectDir: root,
153
+ params: { sqlitePath: sqliteRel },
154
+ });
155
+ const finished = ingest.next<boolean>('finish');
156
+ ingest.send('embed', fakeHandle());
157
+ ingest.send('trigger', true);
158
+ await finished;
159
+ ingest.dispose();
160
+
161
+ const handle = createNodeHarness(searchHandleNode, {
162
+ projectDir: root,
163
+ params: { sqlitePath: sqliteRel },
164
+ });
165
+ const toolsP = handle.next<readonly ToolHandle[]>('tools');
166
+ handle.send('embed', fakeHandle());
167
+ const tools = await toolsP;
168
+ const projectSearch = tools.find(
169
+ (tool) => tool.toolId === 'project_search',
170
+ );
171
+ const packed = await projectSearch?.invoke(
172
+ { query: 'alpha' },
173
+ {
174
+ projectDir: root,
175
+ runId: 'test',
176
+ },
177
+ );
178
+ expect(packed).toContain(longBody);
179
+ handle.dispose();
180
+ });
181
+ });
@@ -0,0 +1,126 @@
1
+ import {
2
+ defineReactiveNode,
3
+ EMBED_HANDLE_WIRE_TYPE,
4
+ isEmbedHandle,
5
+ } from '@langflower/node-sdk';
6
+ import { filter, from, map, switchMap } from 'rxjs';
7
+ import { runIngest, type IngestEvent } from './lib/ingest.ts';
8
+ import {
9
+ DEFAULT_SQLITE_PATH,
10
+ resolveSqlitePath,
11
+ resolveUnderProject,
12
+ } from './lib/paths.ts';
13
+ import { textEmbedderFromHandle } from './lib/text-embedder.ts';
14
+
15
+ const asString = (value: unknown, fallback: string): string => {
16
+ if (typeof value === 'string') {
17
+ return value;
18
+ }
19
+ return fallback;
20
+ };
21
+
22
+ type IngestBundle = {
23
+ readonly embedInput: unknown;
24
+ readonly projectDir: string;
25
+ readonly sqlitePath: string;
26
+ readonly sourceDir: string;
27
+ };
28
+
29
+ const ingestEvents = (bundle: IngestBundle): AsyncIterable<IngestEvent> => {
30
+ if (!isEmbedHandle(bundle.embedInput)) {
31
+ throw new Error(
32
+ 'hello-embed-ingest requires a wired embed input from common-embed-provider.',
33
+ );
34
+ }
35
+ if (bundle.projectDir.length === 0) {
36
+ throw new Error('hello-embed-ingest requires ctx.projectDir.');
37
+ }
38
+ return runIngest({
39
+ sqlitePath: resolveSqlitePath(bundle.projectDir, bundle.sqlitePath),
40
+ sourceDir: resolveUnderProject(bundle.projectDir, bundle.sourceDir),
41
+ embedder: textEmbedderFromHandle(bundle.embedInput, 'document'),
42
+ });
43
+ };
44
+
45
+ /**
46
+ * Walk project markdown, embed heading chunks, write sqlite vectors.
47
+ */
48
+ export default defineReactiveNode({
49
+ type: 'hello-embed-ingest',
50
+ displayName: 'Hello Embed Ingest',
51
+ category: 'Hello Embed',
52
+ description: `
53
+ Index project markdown into a local SQLite vector store.
54
+
55
+ Walks \`**/*.md\` (skips node_modules, .git, .langflower/.cache), splits on headings, and embeds one chunk at a time. Progress is a technical stream (\`feed.role: 'progress'\`, \`streaming: true\` — same growing layout as reasoning, caption PROGRESS, not result bubbles). Wire **embed** from common-embed-provider. **finish** fires when the index is written.
56
+ `.trim(),
57
+ uiSchema: [
58
+ {
59
+ field: 'sqlitePath',
60
+ type: 'string',
61
+ label: 'SQLite path',
62
+ default: DEFAULT_SQLITE_PATH,
63
+ },
64
+ {
65
+ field: 'sourceDir',
66
+ type: 'string',
67
+ label: 'Source folder',
68
+ default: '',
69
+ },
70
+ ] as const,
71
+ bind(ctx, { makeInput, configureOutput, combineInputs }) {
72
+ const trigger = makeInput<unknown>('trigger', {
73
+ name: 'trigger',
74
+ dynamic: true,
75
+ required: true,
76
+ description: 'Emit to run ingest.',
77
+ });
78
+ const embed = makeInput<unknown>('embed', {
79
+ name: 'embed',
80
+ wireType: EMBED_HANDLE_WIRE_TYPE,
81
+ required: true,
82
+ description: 'Wire from common-embed-provider (fan-out OK).',
83
+ });
84
+
85
+ const session$ = combineInputs(
86
+ [trigger, embed, ctx],
87
+ ([_trigger, embedInput, ec]) => ({
88
+ embedInput,
89
+ projectDir: String(ec.projectDir ?? ''),
90
+ sqlitePath: asString(ec.params.sqlitePath, DEFAULT_SQLITE_PATH),
91
+ sourceDir: asString(ec.params.sourceDir, ''),
92
+ }),
93
+ ).pipeValue(switchMap((bundle) => from(ingestEvents(bundle))));
94
+
95
+ const progress$ = session$.pipeValue(
96
+ filter(
97
+ (event): event is Extract<IngestEvent, { kind: 'progress' }> =>
98
+ event.kind === 'progress',
99
+ ),
100
+ map((event) =>
101
+ event.text.endsWith('\n') ? event.text : `${event.text}\n`,
102
+ ),
103
+ );
104
+ const finish$ = session$.pipeValue(
105
+ filter(
106
+ (event): event is Extract<IngestEvent, { kind: 'finish' }> =>
107
+ event.kind === 'finish',
108
+ ),
109
+ map(() => true),
110
+ );
111
+
112
+ return {
113
+ inputs: [trigger, embed],
114
+ outputs: [
115
+ configureOutput('progress', progress$, {
116
+ wireType: 'string',
117
+ feed: { role: 'progress', streaming: true },
118
+ }),
119
+ configureOutput('finish', finish$, {
120
+ wireType: 'boolean',
121
+ feed: { role: 'none' },
122
+ }),
123
+ ],
124
+ };
125
+ },
126
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { chunkMarkdown } from './chunk-markdown.ts';
3
+
4
+ describe('chunkMarkdown', () => {
5
+ it('keeps a heading-less file as one intro chunk', () => {
6
+ const chunks = chunkMarkdown('notes.md', 'hello world\n');
7
+ expect(chunks).toHaveLength(1);
8
+ expect(chunks[0]?.heading).toBe('');
9
+ expect(chunks[0]?.id).toBe('notes.md#intro#0');
10
+ expect(chunks[0]?.text).toBe('hello world');
11
+ });
12
+
13
+ it('splits on headings and builds breadcrumbs', () => {
14
+ const source = [
15
+ 'preamble',
16
+ '# Alpha',
17
+ 'a body',
18
+ '## Beta',
19
+ 'b body',
20
+ '# Gamma',
21
+ 'c body',
22
+ ].join('\n');
23
+ const chunks = chunkMarkdown('doc.md', source);
24
+ expect(chunks.map((chunk) => chunk.heading)).toEqual([
25
+ '',
26
+ 'Alpha',
27
+ 'Alpha > Beta',
28
+ 'Gamma',
29
+ ]);
30
+ expect(chunks[2]?.id).toBe('doc.md#alpha-beta#2');
31
+ expect(chunks[2]?.embedText).toContain('doc.md');
32
+ expect(chunks[2]?.text).toBe('b body');
33
+ });
34
+
35
+ it('skips empty heading bodies', () => {
36
+ const chunks = chunkMarkdown('empty.md', '# Only\n\n# Next\ntext\n');
37
+ expect(chunks).toHaveLength(1);
38
+ expect(chunks[0]?.heading).toBe('Next');
39
+ });
40
+ });
@@ -0,0 +1,123 @@
1
+ import { MAX_CHUNKS_PER_FILE } from './paths.ts';
2
+
3
+ export type MarkdownChunk = {
4
+ readonly id: string;
5
+ readonly path: string;
6
+ readonly heading: string;
7
+ readonly text: string;
8
+ readonly embedText: string;
9
+ readonly truncated?: string;
10
+ };
11
+
12
+ const HEADING_RE = /^(#{1,6})\s+(.*)$/;
13
+
14
+ const slugHeading = (heading: string): string => {
15
+ const slug = heading
16
+ .trim()
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '-')
19
+ .replace(/^-+|-+$/g, '');
20
+ return slug.length > 0 ? slug : 'intro';
21
+ };
22
+
23
+ const displayHeading = (heading: string): string =>
24
+ heading.length > 0 ? heading : '(intro)';
25
+
26
+ type OpenSection = {
27
+ readonly depth: number;
28
+ readonly title: string;
29
+ };
30
+
31
+ const breadcrumbOf = (stack: readonly OpenSection[]): string =>
32
+ stack.map((section) => section.title).join(' > ');
33
+
34
+ const flushBody = (
35
+ relPath: string,
36
+ heading: string,
37
+ body: string,
38
+ ordinal: number,
39
+ out: MarkdownChunk[],
40
+ ): number => {
41
+ const text = body.trim();
42
+ if (text.length === 0) {
43
+ return ordinal;
44
+ }
45
+ const id = `${relPath}#${slugHeading(heading)}#${String(ordinal)}`;
46
+ out.push({
47
+ id,
48
+ path: relPath,
49
+ heading,
50
+ text,
51
+ embedText: `${relPath}\n${displayHeading(heading)}\n\n${text}`,
52
+ });
53
+ return ordinal + 1;
54
+ };
55
+
56
+ /**
57
+ * Split markdown into heading chunks. Preamble before the first heading
58
+ * uses heading `""`. Heading-less files become one chunk.
59
+ */
60
+ export const chunkMarkdown = (
61
+ relPath: string,
62
+ source: string,
63
+ ): readonly MarkdownChunk[] => {
64
+ const lines = source.replaceAll('\r\n', '\n').split('\n');
65
+ const out: MarkdownChunk[] = [];
66
+ const stack: OpenSection[] = [];
67
+ let currentHeading = '';
68
+ let body: string[] = [];
69
+ let ordinal = 0;
70
+ let truncated: string | undefined;
71
+
72
+ const flush = (): void => {
73
+ if (out.length >= MAX_CHUNKS_PER_FILE) {
74
+ if (truncated === undefined) {
75
+ truncated = `capped ${relPath} at ${String(MAX_CHUNKS_PER_FILE)} chunks`;
76
+ }
77
+ body = [];
78
+ return;
79
+ }
80
+ ordinal = flushBody(
81
+ relPath,
82
+ currentHeading,
83
+ body.join('\n'),
84
+ ordinal,
85
+ out,
86
+ );
87
+ body = [];
88
+ };
89
+
90
+ for (const line of lines) {
91
+ const match = HEADING_RE.exec(line);
92
+ if (match === null) {
93
+ body.push(line);
94
+ continue;
95
+ }
96
+ flush();
97
+ if (out.length >= MAX_CHUNKS_PER_FILE) {
98
+ break;
99
+ }
100
+ const marks = match[1] ?? '#';
101
+ const title = (match[2] ?? '').trim();
102
+ const depth = marks.length;
103
+ while (
104
+ stack.length > 0 &&
105
+ (stack[stack.length - 1]?.depth ?? 0) >= depth
106
+ ) {
107
+ stack.pop();
108
+ }
109
+ stack.push({ depth, title });
110
+ currentHeading = breadcrumbOf(stack);
111
+ }
112
+ if (out.length < MAX_CHUNKS_PER_FILE) {
113
+ flush();
114
+ }
115
+
116
+ if (truncated !== undefined && out[0] !== undefined) {
117
+ const last = out[out.length - 1];
118
+ if (last !== undefined) {
119
+ out[out.length - 1] = { ...last, truncated };
120
+ }
121
+ }
122
+ return out;
123
+ };