scout-ai 1.2.3 → 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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +138 -50
  3. data/README.md +171 -290
  4. data/Rakefile +17 -1
  5. data/VERSION +1 -1
  6. data/doc/Improvements.md +325 -0
  7. data/doc/StartHere.md +110 -0
  8. data/doc/developer/Architecture.md +126 -0
  9. data/doc/developer/Backends.md +199 -0
  10. data/doc/developer/ChatLifecycle.md +183 -0
  11. data/doc/developer/DelegationInternals.md +295 -0
  12. data/doc/developer/DesignPrinciples.md +245 -0
  13. data/doc/developer/PromptProcessing.md +292 -0
  14. data/doc/developer/Provenance.md +317 -0
  15. data/doc/user/BuildingAgents.md +345 -0
  16. data/doc/user/Cookbook.md +333 -0
  17. data/doc/user/CoreConcepts.md +181 -0
  18. data/doc/user/Delegation.md +191 -0
  19. data/doc/user/GettingStarted.md +159 -0
  20. data/doc/user/ManagingContext.md +163 -0
  21. data/doc/user/MultiAgentWorkflows.md +256 -0
  22. data/doc/user/Python.md +159 -0
  23. data/doc/user/RunningInference.md +200 -0
  24. data/doc/user/ToolCalling.md +193 -0
  25. data/doc/user/WritingChats.md +197 -0
  26. data/lib/scout/llm/agent/chat.rb +61 -11
  27. data/lib/scout/llm/agent/delegate.rb +274 -65
  28. data/lib/scout/llm/agent/iterate.rb +2 -2
  29. data/lib/scout/llm/agent/save.rb +273 -0
  30. data/lib/scout/llm/agent/workflow.rb +164 -0
  31. data/lib/scout/llm/agent.rb +86 -61
  32. data/lib/scout/llm/ask.rb +62 -17
  33. data/lib/scout/llm/backends/anthropic.rb +9 -2
  34. data/lib/scout/llm/backends/bedrock.rb +15 -3
  35. data/lib/scout/llm/backends/default.rb +183 -99
  36. data/lib/scout/llm/backends/glm.rb +58 -0
  37. data/lib/scout/llm/backends/huggingface.rb +196 -26
  38. data/lib/scout/llm/backends/ollama.rb +13 -1
  39. data/lib/scout/llm/backends/openai.rb +0 -2
  40. data/lib/scout/llm/backends/openwebui.rb +20 -13
  41. data/lib/scout/llm/backends/relay.rb +22 -22
  42. data/lib/scout/llm/backends/responses.rb +1 -1
  43. data/lib/scout/llm/chat/agent_meta.rb +264 -0
  44. data/lib/scout/llm/chat/annotation.rb +39 -10
  45. data/lib/scout/llm/chat/parse.rb +28 -6
  46. data/lib/scout/llm/chat/persist.rb +25 -0
  47. data/lib/scout/llm/chat/process/clear.rb +41 -6
  48. data/lib/scout/llm/chat/process/files.rb +21 -6
  49. data/lib/scout/llm/chat/process/meta.rb +421 -34
  50. data/lib/scout/llm/chat/process/options.rb +21 -1
  51. data/lib/scout/llm/chat/process/tools.rb +56 -15
  52. data/lib/scout/llm/chat/process.rb +4 -0
  53. data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
  54. data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
  55. data/lib/scout/llm/chat/prompt.rb +48 -0
  56. data/lib/scout/llm/chat/provenance.rb +775 -0
  57. data/lib/scout/llm/chat/tool_calls.rb +76 -0
  58. data/lib/scout/llm/chat.rb +18 -2
  59. data/lib/scout/llm/embed.rb +11 -3
  60. data/lib/scout/llm/image.rb +86 -0
  61. data/lib/scout/llm/mcp.rb +10 -2
  62. data/lib/scout/llm/rag.rb +3 -3
  63. data/lib/scout/llm/tools/call.rb +160 -11
  64. data/lib/scout/llm/tools/knowledge_base.rb +1 -1
  65. data/lib/scout/llm/tools/workflow.rb +32 -16
  66. data/lib/scout/model/python/huggingface/causal.rb +23 -5
  67. data/lib/scout/model/python/huggingface.rb +2 -1
  68. data/lib/scout-ai.rb +1 -0
  69. data/python/README.md +197 -14
  70. data/python/scout_ai/huggingface/eval.py +245 -34
  71. data/python/tests/test_huggingface_eval.py +58 -0
  72. data/research/ChatAnalyst-required-changes.md +167 -0
  73. data/research/agent-delegation-analysis.md +810 -0
  74. data/research/agent-meta-provenance-integration-plan.md +622 -0
  75. data/research/agent-workflow-analysis.md +1120 -0
  76. data/research/backends-analysis.md +836 -0
  77. data/research/chat-core-analysis.md +946 -0
  78. data/research/chatanalyst-provenance/00-baseline.md +30 -0
  79. data/research/chatanalyst-provenance/01-repo-map.md +60 -0
  80. data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
  81. data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
  82. data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
  83. data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
  84. data/research/chatanalyst-provenance/07-critic-review.md +25 -0
  85. data/research/chatanalyst-provenance/final-report.md +45 -0
  86. data/research/chatanalyst-provenance/resumption.md +37 -0
  87. data/research/coding-philosophy-analysis.md +928 -0
  88. data/research/commands-analysis.md +947 -0
  89. data/research/multi-agent-patterns-analysis.md +853 -0
  90. data/research/prompt-strategies-analysis.md +630 -0
  91. data/research/prov-verbosity-fix-notes.md +77 -0
  92. data/research/provenance-analysis.md +469 -0
  93. data/research/provenance-navigation-design.md +640 -0
  94. data/research/synthesis-report.md +487 -0
  95. data/research/tools-system-analysis.md +779 -0
  96. data/scout-ai.gemspec +100 -11
  97. data/scout_commands/agent/ask +13 -3
  98. data/scout_commands/agent/kb +2 -0
  99. data/scout_commands/llm/ask +11 -4
  100. data/scout_commands/llm/md +76 -0
  101. data/scout_commands/llm/process_queries +48 -0
  102. data/scout_commands/llm/prov +602 -0
  103. data/scout_commands/llm/word +71 -0
  104. data/scout_commands/workflow/mcp +43 -0
  105. data/share/word/reference.docx +0 -0
  106. data/test/etc/AI/mock.yaml +11 -0
  107. data/test/fixtures/backends/anthropic.json +19 -0
  108. data/test/fixtures/backends/anthropic_tool_use.json +24 -0
  109. data/test/fixtures/backends/bedrock.json +8 -0
  110. data/test/fixtures/backends/bedrock_embedding.json +3 -0
  111. data/test/fixtures/backends/bedrock_tool_use.json +17 -0
  112. data/test/fixtures/backends/ollama.json +16 -0
  113. data/test/fixtures/backends/ollama_tool_call.json +27 -0
  114. data/test/fixtures/backends/openai_chat.json +21 -0
  115. data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
  116. data/test/fixtures/backends/responses.json +33 -0
  117. data/test/fixtures/backends/responses_tool_call.json +28 -0
  118. data/test/integration/README.md +32 -0
  119. data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
  120. data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
  121. data/test/integration/scout/llm/backends/test_relay.rb +52 -0
  122. data/test/integration/scout/llm/test_infrastructure.rb +74 -0
  123. data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
  124. data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
  125. data/test/integration/scout/model/test_base.rb +91 -0
  126. data/test/scout/llm/agent/test_chat.rb +8 -2
  127. data/test/scout/llm/agent/test_save.rb +413 -0
  128. data/test/scout/llm/agent/test_workflow.rb +110 -0
  129. data/test/scout/llm/backends/test_anthropic.rb +93 -10
  130. data/test/scout/llm/backends/test_bedrock.rb +118 -2
  131. data/test/scout/llm/backends/test_huggingface.rb +137 -42
  132. data/test/scout/llm/backends/test_ollama.rb +70 -20
  133. data/test/scout/llm/backends/test_openwebui.rb +42 -40
  134. data/test/scout/llm/backends/test_relay.rb +4 -2
  135. data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
  136. data/test/scout/llm/chat/process/test_meta.rb +518 -0
  137. data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
  138. data/test/scout/llm/chat/test_agent_meta.rb +357 -0
  139. data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
  140. data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
  141. data/test/scout/llm/chat/test_parse.rb +70 -15
  142. data/test/scout/llm/chat/test_prov_cli.rb +274 -0
  143. data/test/scout/llm/chat/test_provenance.rb +240 -0
  144. data/test/scout/llm/chat/test_tool_calls.rb +38 -0
  145. data/test/scout/llm/test_agent.rb +13 -36
  146. data/test/scout/llm/test_ask.rb +75 -52
  147. data/test/scout/llm/test_chat.rb +107 -13
  148. data/test/scout/llm/test_embed.rb +48 -0
  149. data/test/scout/llm/test_rag.rb +23 -16
  150. data/test/scout/llm/test_tools.rb +12 -1
  151. data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
  152. data/test/scout/llm/tools/test_mcp.rb +5 -3
  153. data/test/scout/llm/tools/test_workflow.rb +23 -2
  154. data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
  155. data/test/scout/model/python/huggingface/test_causal.rb +9 -3
  156. data/test/scout/model/python/huggingface/test_classification.rb +11 -2
  157. data/test/scout/model/python/test_torch.rb +2 -0
  158. data/test/scout/model/python/torch/test_helpers.rb +4 -0
  159. data/test/scout/model/test_base.rb +4 -2
  160. data/test/support/availability.rb +231 -0
  161. data/test/support/fake_clients.rb +138 -0
  162. data/test/support/fixtures.rb +21 -0
  163. data/test/support/infrastructure_probes.rb +136 -0
  164. data/test/support/mock_backend.rb +215 -0
  165. data/test/test_helper.rb +32 -2
  166. metadata +99 -10
  167. data/doc/Agent.md +0 -327
  168. data/doc/Chat.md +0 -458
  169. data/doc/LLM.md +0 -340
  170. data/doc/RAG.md +0 -129
  171. data/scout_commands/documenter +0 -148
  172. data/test/scout/llm/backends/test_openai.rb +0 -192
  173. data/test/scout/llm/backends/test_responses.rb +0 -238
  174. data/test/scout/llm/test_parse.rb +0 -98
data/doc/LLM.md DELETED
@@ -1,340 +0,0 @@
1
- # LLM
2
-
3
- This package provides a compact layer to talk to multiple LLM backends while keeping:
4
-
5
- - **configuration reproducible** (named endpoints)
6
- - **prompts reproducible** (chat files)
7
- - **tool calling first-class** (Workflow tasks, KnowledgeBase databases, MCP tools)
8
-
9
- The core entry points are:
10
-
11
- - `LLM.ask(question, options={}, &block)` — ask a model (optionally with tools)
12
- - `LLM.chat(...)` — compile/expand a chat file or message array into final messages
13
- - `LLM.options(messages)` — extract per-chat options (`endpoint`, `model`, `format`, …)
14
- - `LLM.print(messages)` — serialize messages back to chat-file format
15
- - `LLM.embed(text, options={})` — embeddings (not the focus of this document)
16
-
17
- For stateful multi-turn conversations, see `doc/Agent.md`.
18
-
19
- For the full chat-file role reference, see `doc/Chat.md`.
20
-
21
- ---
22
-
23
- ## 1. Endpoints: configuring inference points (`~/.scout/etc/AI/<endpoint>`)
24
-
25
- Scout-AI uses **named endpoints** so colleagues can agree on short names like `nano` or `deep`.
26
-
27
- At runtime, `LLM.ask(..., endpoint: :nano)` will load:
28
-
29
- ```text
30
- ~/.scout/etc/AI/nano
31
- ```
32
-
33
- This file is YAML.
34
-
35
- Minimum useful keys:
36
-
37
- - `backend`: which backend to use (`responses`, `openai`, `anthropic`, `ollama`, `vllm`, `openwebui`, `bedrock`, `relay`)
38
- - `url`: server URL (only for backends that need one, e.g. `ollama`, `openwebui`)
39
- - `model`: backend-specific model id
40
-
41
- Any **additional keys are passed to the backend** and may be interpreted there.
42
-
43
- Examples:
44
-
45
- `~/.scout/etc/AI/nano`
46
-
47
- ```yaml
48
- backend: responses
49
- model: gpt-5-nano
50
- ```
51
-
52
- `~/.scout/etc/AI/deep`
53
-
54
- ```yaml
55
- backend: responses
56
- model: gpt-5
57
- reasoning_effort: high
58
- text_verbosity: high
59
- ```
60
-
61
- `~/.scout/etc/AI/ollama`
62
-
63
- ```yaml
64
- backend: ollama
65
- url: http://localhost:11434
66
- model: llama3.1
67
- ```
68
-
69
- How they are used:
70
-
71
- ```ruby
72
- LLM.ask "Say hi", endpoint: :nano
73
- LLM.ask "Think harder", endpoint: :deep
74
- ```
75
-
76
- From the CLI:
77
-
78
- ```bash
79
- scout-ai llm ask -e nano "Say hi"
80
- ```
81
-
82
- ---
83
-
84
- ## 2. `LLM.ask`: the core call
85
-
86
- ### 2.1 Input types
87
-
88
- The `question` argument can be:
89
-
90
- - a String (free text)
91
- - a **path to a chat file** (if the string looks like a filename and exists)
92
- - an Array of `{role:, content:}` hashes
93
- - a `Chat` (which is an annotated array)
94
-
95
- Internally `LLM.ask` always compiles input via `LLM.chat` first.
96
-
97
- ### 2.2 Option resolution order
98
-
99
- Options come from multiple places:
100
-
101
- 1) options extracted from the compiled messages (`LLM.options(messages)`)
102
- 2) options passed explicitly to `LLM.ask(question, options)`
103
- 3) endpoint YAML (`~/.scout/etc/AI/<endpoint>`) is merged in (as defaults)
104
-
105
- ### 2.3 Caching (`persist`)
106
-
107
- `LLM.ask` caches responses by default via `Persist.persist`.
108
-
109
- - default: `persist: true`
110
- - set `persist: false` to disable caching
111
-
112
- The cache key includes the endpoint name, messages, and most options.
113
-
114
- ### 2.4 Return value
115
-
116
- - default: returns the assistant content string
117
- - if `return_messages: true`: returns the full message trace (including tool calls)
118
-
119
- ---
120
-
121
- ## 3. Chat compilation (`LLM.chat`) and why it matters
122
-
123
- `LLM.chat` is what makes chat files powerful.
124
-
125
- It expands special roles **before** calling the backend:
126
-
127
- - imports: `import`, `continue`, `last`
128
- - pruning: `clear`, `skip`
129
- - workflow execution: `task`, `inline_task`, `exec_task`, and then `job`, `inline_job`
130
- - file inclusion: `file`, `directory`, `image`, `pdf`
131
-
132
- See `doc/Chat.md` for exact behavior and role syntax.
133
-
134
- ---
135
-
136
- ## 4. Backends (and what is “passed through”)
137
-
138
- Backends are selected by `backend:` (from endpoint YAML, env/config, or explicit options).
139
-
140
- Currently supported backends (see `lib/scout/llm/backends/`):
141
-
142
- - `responses` — OpenAI Responses API style (default). Supports:
143
- - JSON Schema responses (`format:` as schema)
144
- - multimodal (`image:`, `pdf:` roles)
145
- - session continuation (`previous_response_id`)
146
- - optional web search tool (`websearch`)
147
- - `openai` — OpenAI Chat Completions style
148
- - `anthropic` — Anthropic Messages API style
149
- - `ollama` — local Ollama server
150
- - `vllm` — vLLM OpenAI-compatible server
151
- - `openwebui` — OpenWebUI-compatible chat/completions
152
- - `bedrock` — AWS Bedrock runtime
153
- - `relay` — simple SCP/poll relay
154
-
155
- ### 4.1 Common backend options
156
-
157
- Many backends accept the same high-level keys:
158
-
159
- - `model`, `url`, `key`
160
- - `request_timeout`
161
- - `log_errors`
162
- - `tools`
163
-
164
- And then backend-specific keys are forwarded (for example: reasoning/text options for `responses`).
165
-
166
- ### 4.2 Responses session continuation (`previous_response_id`)
167
-
168
- If you use the Responses backend and provide a `previous_response_id`, Scout-AI will:
169
-
170
- - keep only the messages after the most recent `previous_response_id` marker (so you do not resend the entire conversation)
171
- - pass `previous_response_id` to the backend
172
-
173
- If you set `previous_response: false`, session continuation is disabled.
174
-
175
- ---
176
-
177
- ## 5. Tools and function calling
178
-
179
- Scout-AI represents tools as a **hash**:
180
-
181
- ```ruby
182
- tools = {
183
- "tool_name" => [handler, tool_definition]
184
- }
185
- ```
186
-
187
- Where:
188
-
189
- - `handler` can be:
190
- - a `Workflow`
191
- - a `KnowledgeBase`
192
- - a `Proc` (custom tool implementation)
193
- - `tool_definition` is an OpenAI-style function schema (adapted per backend)
194
-
195
- ### 5.1 Providing tools
196
-
197
- You can provide tools in three main ways:
198
-
199
- 1) programmatically: `LLM.ask(..., tools: {...})`
200
- 2) via chat roles: `tool:`, `kb:`, `association:`, `mcp:`
201
- 3) via an Agent: `LLM::Agent` automatically exports a workflow/KB as tools
202
-
203
- ### 5.2 Workflow tools
204
-
205
- ```ruby
206
- workflow_tools = LLM.workflow_tools(Baking)
207
- LLM.ask "Bake muffins", tools: workflow_tools
208
- ```
209
-
210
- Or in a chat file:
211
-
212
- ```text
213
- tool: Baking bake_muffin_tray
214
- ```
215
-
216
- ### 5.3 KnowledgeBase tools
217
-
218
- ```ruby
219
- kb_tools = LLM.knowledge_base_tool_definition(kb)
220
- LLM.ask "Query relationships", tools: kb_tools
221
- ```
222
-
223
- Or in a chat file:
224
-
225
- ```text
226
- association: brothers test/data/person/brothers undirected=true
227
- ```
228
-
229
- ### 5.4 Tool execution and output limits
230
-
231
- Tool calls are executed by `LLM.process_calls`.
232
-
233
- To protect the context window, tool outputs are capped (config key `:max_content_length` under `:llm_tools`).
234
-
235
- If a tool returns more than the max length, the content is replaced by an exception-like JSON string.
236
-
237
- ---
238
-
239
- ## 6. Convenience helpers
240
-
241
- ### 6.1 `LLM.workflow_ask`
242
-
243
- ```ruby
244
- LLM.workflow_ask(Baking, "Bake muffins", endpoint: :nano)
245
- ```
246
-
247
- ### 6.2 `LLM.knowledge_base_ask`
248
-
249
- ```ruby
250
- LLM.knowledge_base_ask(kb, "Who is X's brother in law?", endpoint: :nano)
251
- ```
252
-
253
- ---
254
-
255
- ## 7. CLI usage
256
-
257
- ### 7.1 `scout-ai llm ask`
258
-
259
- The main “stateless” CLI.
260
-
261
- ```bash
262
- scout-ai llm ask -e nano "Say hi"
263
- ```
264
-
265
- For convenience prompt does not need quotes.
266
-
267
- ```bash
268
- scout-ai llm ask -e nano Say hi
269
- ```
270
-
271
- Useful flags:
272
-
273
- - `-e/--endpoint <name>`
274
- - `-m/--model <model_id>`
275
- - `-b/--backend <backend>`
276
- - `-c/--chat <file>` — read/append to a chat file
277
- - `-i/--imports <a,b,c>` — import other chat files
278
- - `-t/--template <name_or_path>` — templates with optional `???` placeholder
279
- - `-f/--file <path>` — inline a file into the prompt
280
- - `-d/--dry_run` — print the compiled conversation (no model call)
281
-
282
- The configuration files under etc/config can be used to setup default
283
- endpoints or models.
284
-
285
- ```config
286
- # ~/.scout/etc/config
287
- endpoint nano ask
288
- ```
289
-
290
- ### 7.2 `scout-ai agent ask`
291
-
292
- The “agent-aware” CLI.
293
-
294
- ```bash
295
- scout-ai agent ask Baking Bake a tray of muffins
296
- ```
297
-
298
- It loads the agent (workflow + knowledge base + start_chat) and runs a stateful conversation.
299
-
300
- See `doc/Agent.md`.
301
-
302
- ---
303
-
304
- ## 8. Programmatic use (control loops)
305
-
306
- For loops, branching, delegation, and structured iteration, use `LLM::Agent`.
307
-
308
- See `doc/Agent.md` for the full API and examples.
309
-
310
- ---
311
-
312
- ## 9. Minimal end-to-end example
313
-
314
- 1) Create an endpoint file:
315
-
316
- ```yaml
317
- # ~/.scout/etc/AI/nano
318
- backend: responses
319
- model: gpt-5-nano
320
- ```
321
-
322
- 2) Ask from the CLI:
323
-
324
- ```bash
325
- scout-ai llm ask -e nano Say hi
326
- ```
327
-
328
- 3) Use a chat file with a workflow tool:
329
-
330
- ```text
331
- user:
332
-
333
- tool: Baking bake_muffin_tray
334
-
335
- Bake muffins using the tool.
336
- ```
337
-
338
- ```bash
339
- scout-ai llm ask -c baking.chat -e nano
340
- ```
data/doc/RAG.md DELETED
@@ -1,129 +0,0 @@
1
- # RAG (Retrieval-Augmented Generation) module
2
-
3
- This document explains how to use the RAG helper provided in Scout (lib/scout/llm/rag.rb).
4
-
5
- Audience: AI agents and developers integrating retrieval-augmented flows into other applications.
6
-
7
- Overview
8
- --------
9
- LLM::RAG provides a thin helper to build a nearest-neighbor index over embedding vectors using the hnswlib library. It expects an array of fixed-size numeric vectors (Float arrays) and returns an HNSW index that can be queried with another vector to find the nearest neighbors.
10
-
11
- The RAG.index method is intentionally small and focused:
12
-
13
- - It requires the `hnswlib` Ruby gem at runtime (loaded inside the method).
14
- - It uses L2 (Euclidean) distance by default.
15
- - It sets the index dimension to the length of the first vector and initializes the HNSW index with the number of elements supplied.
16
- - Each vector is added in order; the integer ID stored in the index is the zero-based position in the input array.
17
-
18
- Prerequisites
19
- -------------
20
- - Ruby environment with the Scout gem code available.
21
- - The `hnswlib` Ruby gem installed (the method requires it dynamically):
22
-
23
- gem install hnswlib
24
-
25
- - An embedding function that produces fixed-length numeric vectors. Scout exposes LLM.embed(...) which delegates to configured backends (OpenAI, Ollama, etc.). Ensure your embedding backend is configured and working.
26
-
27
- Basic usage
28
- -----------
29
- The common RAG flow is:
30
-
31
- 1. Prepare a corpus (array of documents or chunks).
32
- 2. Compute embeddings for each document.
33
- 3. Build an HNSW index from those embeddings using LLM::RAG.index.
34
- 4. For a query, compute its embedding and run a nearest-neighbor search on the index.
35
- 5. Map matched neighbor indices back to the original documents.
36
-
37
- Example (Ruby)
38
- ---------------
39
- This example shows a minimal end-to-end flow using Scout's LLM.embed helper to compute embeddings and LLM::RAG to build and query an index.
40
-
41
- ```ruby
42
- # `documents` is an array of strings (documents/chunks).
43
- documents = [
44
- "How to make espresso at home",
45
- "Machine learning: an introduction",
46
- "Ruby concurrency primitives and patterns",
47
- "Cooking guide: baking sourdough"
48
- ]
49
-
50
- # 1) Compute embeddings for each document.
51
- # Use whatever embed model/backend you have configured. Pass model: if needed.
52
- embeddings = documents.map do |doc|
53
- # returns an Array<Float> of fixed length
54
- LLM.embed(doc, model: 'mxbai-embed-large')
55
- end
56
-
57
- # 2) Build the HNSW index
58
- index = LLM::RAG.index(embeddings)
59
-
60
- # 3) For a query, compute its embedding
61
- query = "best way to brew espresso"
62
- query_vec = LLM.embed(query, model: 'mxbai-embed-large')
63
-
64
- # 4) Run nearest-neighbor search
65
- # search_knn returns two arrays: node indices and distances/scores
66
- k = 3
67
- nodes, scores = index.search_knn(query_vec, k)
68
-
69
- # 5) Map indices back to original documents
70
- results = nodes.map { |i| documents[i] }
71
-
72
- puts "Top #{k} results:"
73
- results.each_with_index do |doc, idx|
74
- puts "#{idx + 1}. #{doc} (score=#{scores[idx]})"
75
- end
76
- ```
77
-
78
- Notes and best practices
79
- ------------------------
80
- - Vector dimensionality: All vectors passed to LLM::RAG.index must have identical length. The code inspects `data.first.length` to determine the index dimension.
81
- - Index IDs: The HNSW index stores integer IDs equal to the input array index. Keep a mapping from those indices to your document IDs/metadata (for instance, an array of document IDs parallel to the embeddings array).
82
- - Persistence: The RAG helper code only constructs and populates the index in memory. The underlying `hnswlib` gem typically offers persistence APIs (save/load). To persist or reload an index, consult the `hnswlib` gem documentation for the correct methods and usage patterns.
83
- - Memory and performance: HNSW indexes keep data in memory and can be large for many vectors. Choose your chunking strategy and max dataset size accordingly.
84
- - Distance metric: The current implementation uses the `'l2'` (Euclidean) space. If your application needs cosine similarity, either normalize vectors before indexing (common practice) or check whether the hnswlib Ruby binding supports a cosine space and adapt accordingly.
85
-
86
- Example: utility wrapper
87
- ------------------------
88
- Here is a small utility that wraps the typical pattern and returns the top-k documents and scores for a query.
89
-
90
- ```ruby
91
- # documents: Array of items (strings or objects). If objects, provide a `to_embedding_source` or pass a block to extract text.
92
- # embed_opts: options forwarded to LLM.embed (e.g. model: ...)
93
- def build_rag_index(documents, embed_opts = {})
94
- # compute embeddings in order
95
- embeddings = documents.map { |d| LLM.embed(d, embed_opts) }
96
- index = LLM::RAG.index(embeddings)
97
- [index, embeddings]
98
- end
99
-
100
- def rag_query(index, documents, query, k = 5, embed_opts = {})
101
- qvec = LLM.embed(query, embed_opts)
102
- nodes, scores = index.search_knn(qvec, k)
103
- results = nodes.map { |i| { doc: documents[i], score: scores[nodes.index(i)] } }
104
- results
105
- end
106
-
107
- # Usage:
108
- # index, embs = build_rag_index(documents, model: 'mxbai-embed-large')
109
- # top = rag_query(index, documents, 'how to make coffee', 3, model: 'mxbai-embed-large')
110
- ```
111
-
112
- Troubleshooting
113
- ---------------
114
- - "NoMethodError" or "uninitialized constant Hnswlib": ensure the `hnswlib` gem is installed and available to your Ruby runtime.
115
- - Inconsistent dimensions: If you see errors related to dimension mismatch, confirm every embedding vector has the same length and is numeric.
116
- - Mapping errors: Remember the index IDs correspond to the zero-based position in the `data` array passed to LLM::RAG.index. Keep a parallel array or map to metadata (IDs, titles, etc.).
117
-
118
- Further integration
119
- -------------------
120
- - Use chunking for long documents: split long documents into smaller passages, embed each passage, and keep a mapping from passage index to parent document.
121
- - Use result reranking: after retrieval, you can rerank retrieved documents with more expensive cross-encoders or scoring functions.
122
- - Combine with generative models: feed retrieved passages into an LLM prompt to produce answers grounded in retrieved content.
123
-
124
- References
125
- ----------
126
- - lib/scout/llm/rag.rb (implementation)
127
- - hnswlib Ruby gem (install and persistence documentation)
128
- - Scout LLM embedding helpers (lib/scout/llm/embed.rb)
129
-
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'scout'
4
-
5
- $0 = "scout-ai #{$previous_commands.any? ? $previous_commands*" " + " " : "" }#{ File.basename(__FILE__) }" if $previous_commands
6
-
7
- options = SOPT.setup <<EOF
8
- Scout documenter tool
9
-
10
- $ #{$0} [<options>] <topic>
11
-
12
- Generates technical, example-driven documentation for a given Scout topic by
13
- analyzing Ruby source and corresponding test files. For a specified topic, it
14
- locates main and subtopic files, invokes an LLM agent to synthesize markdown
15
- documentation using real test behavior and examples, then outputs comprehensive
16
- topic and subtopic documentation files.
17
-
18
- -h--help Print this help
19
- EOF
20
- if options[:help]
21
- if defined? scout_usage
22
- scout_usage
23
- else
24
- puts SOPT.doc
25
- end
26
- exit 0
27
- end
28
-
29
- require 'scout-ai'
30
-
31
- topic = ARGV.first
32
-
33
- raise MissingParameterException if topic.nil?
34
-
35
- src_files = Scout.lib.scout.glob("#{topic}*/**/*") + Scout.lib.scout.glob("#{topic}.rb")
36
- src_files.collect! do |file|
37
- file.sub(Dir.pwd, '.')
38
- end
39
-
40
- bin_files = Scout.scout_commands.glob("**/*").select{|file| file.include?(topic) && ! file.directory? }
41
-
42
- main = src_files.select{|f| f.split("/").length == 4}.first
43
- subtopics = src_files.select{|f| f.split("/").length == 5}.collect{|f| File.basename(f).sub(".rb",'') }.uniq
44
-
45
- def source_to_test(file)
46
- file.sub(%r{\A./lib/}, './test/').sub(%r{([^/]+)\.rb\z}, 'test_\1.rb')
47
- end
48
-
49
- documenter = LLM::Agent.new
50
-
51
- documenter.start_chat.system <<-EOF
52
- You are a world-class Ruby documentation author. For each (source, test) file
53
- pair given, produce technically precise module- and file-level documentation,
54
- incorporating specific code usage and behavior from the test file as worked
55
- examples, code idioms, and edge-case handling.
56
-
57
- Never insert your own example code: always use live content from the tests as examples.
58
-
59
- Integrate documentation and test-derived examples smoothly.
60
-
61
- You will be given first the main topic documentation for the main file and
62
- test_file, then you will be asked to produce documentation for a subtopic.
63
-
64
- Finally you will be ask to aggregate all the documentation portions into
65
- a final topic documentation file
66
-
67
- User markdown
68
-
69
- Avoid initial and final comments like: Certainly! I'll do this and that
70
- EOF
71
-
72
- documenter.start_chat.file main
73
- documenter.start_chat.file source_to_test(main)
74
-
75
- documenter.start_chat.user <<-EOF
76
- This is the basic topic file. Write the markdown documentation for it.
77
- EOF
78
-
79
- docs = {}
80
- subtopics.each do |subtopic|
81
- src = src_files.select{|f| f.include? subtopic}.select{|f| f.end_with?(".rb") }
82
- test = src.collect{|f| source_to_test(f) }.select{|f| Open.exists? f }.select{|f| f.end_with?(".rb")}
83
-
84
- documenter.start
85
- (src + test + bin_files).each do |file|
86
- documenter.file file
87
- end
88
-
89
- documenter.start_chat.user <<-EOF
90
- Write documentation for topic #{topic} subtopic #{subtopic}
91
- EOF
92
- docs[subtopic] = documenter.respond
93
- end
94
-
95
- documenter.start
96
-
97
- docs.each do |subtopic, documentation|
98
- documenter.user <<-EOF
99
- Please construct a comprehensive documentation on topic #{topic}. For each
100
- subtopic reproduce all the most important from the original documentation
101
- files. The subtopic documentation files will not be available anymore, so
102
- don't leave anything imporant out
103
-
104
- <file subtopic=#{subtopic}>
105
- #{documentation}
106
- <file>
107
- EOF
108
- end
109
-
110
- main_documentation = documenter.chat
111
-
112
- documenter.start_chat.user <<-EOF
113
- This is the revise documentation for the topic:
114
-
115
- ---
116
-
117
- #{main_documentation}
118
-
119
- ---
120
- EOF
121
-
122
- revised_subtopics = {}
123
- docs.each do |subtopic, documentation|
124
- documenter.start
125
-
126
- src = src_files.select{|f| f.include? subtopic}.select{|f| f.end_with?(".rb") }
127
- test = src.collect{|f| source_to_test(f) }.select{|f| Open.exists? f }.select{|f| f.end_with?(".rb")}
128
-
129
- documenter.start
130
- (src + test + bin_files).each do |file|
131
- documenter.file file
132
- end
133
-
134
- documenter.user <<-EOF
135
- Please revise the subtopic documentation in light of the revised main_documentation
136
-
137
- <file subtopic=#{subtopic}>
138
- #{documentation}
139
- <file>
140
- EOF
141
- revised_subtopics[subtopic] = documenter.respond
142
- end
143
-
144
-
145
- Open.write Scout.doc.lib.scout[topic + '.md'].find(:current), main_documentation
146
- revised_subtopics.each do |subtopic,documentation|
147
- Open.write Scout.doc.lib.scout[topic][subtopic + '.md'].find(:current), documentation
148
- end