robot_lab 0.2.1 → 0.2.7
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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +185 -64
- data/Rakefile +28 -12
- data/docs/api/core/index.md +41 -14
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +285 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +636 -60
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +152 -14
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +83 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +118 -30
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +153 -38
- data/docs/architecture/robot-execution.md +173 -91
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +299 -93
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +296 -50
- data/docs/guides/creating-networks.md +220 -31
- data/docs/guides/hooks.md +1084 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +37 -6
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +166 -51
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +300 -11
- data/docs/index.md +37 -5
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +23 -9
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +266 -0
- data/examples/README.md +203 -42
- data/examples/common.rb +79 -7
- data/examples/xyzzy.rb +97 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +14 -9
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +90 -18
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +18 -5
- data/lib/robot_lab/robot.rb +233 -89
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +70 -15
- data/mkdocs.yml +7 -1
- metadata +41 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
|
@@ -6,11 +6,22 @@ Customer service system with intelligent routing using SimpleFlow pipelines.
|
|
|
6
6
|
|
|
7
7
|
This example demonstrates a multi-robot network where a classifier routes customer inquiries to specialized support robots using SimpleFlow's optional task activation.
|
|
8
8
|
|
|
9
|
+
The runnable version is
|
|
10
|
+
[`examples/03_network.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/03_network.rb);
|
|
11
|
+
the snippets here expand it with a fourth specialist and per-task configuration.
|
|
12
|
+
|
|
13
|
+
> [!IMPORTANT]
|
|
14
|
+
> Two names are in play and they are easy to confuse.
|
|
15
|
+
> `result.activate(:x)` takes a **task** name (the first argument to `task`), while
|
|
16
|
+
> `result.with_context(@name.to_sym, ...)` keys the context by the **robot's** name.
|
|
17
|
+
> Activating a task name that was never declared **raises `ArgumentError` and aborts
|
|
18
|
+
> the run** — it is not a silent no-op. Give each robot the same name as its task and
|
|
19
|
+
> both problems disappear.
|
|
20
|
+
|
|
9
21
|
## Complete Example
|
|
10
22
|
|
|
11
23
|
```ruby
|
|
12
24
|
#!/usr/bin/env ruby
|
|
13
|
-
# examples/customer_service.rb
|
|
14
25
|
|
|
15
26
|
require "bundler/setup"
|
|
16
27
|
require "robot_lab"
|
|
@@ -185,17 +196,26 @@ class ClassifierRobot < RobotLab::Robot
|
|
|
185
196
|
.with_context(@name.to_sym, robot_result)
|
|
186
197
|
.continue(robot_result)
|
|
187
198
|
|
|
188
|
-
# 5. Activate the appropriate optional task based on output
|
|
199
|
+
# 5. Activate the appropriate optional task based on output.
|
|
200
|
+
# These symbols MUST match the task names declared in create_network.
|
|
189
201
|
category = robot_result.last_text_content.to_s.strip.downcase
|
|
190
202
|
case category
|
|
191
|
-
when /billing/ then new_result.activate(:
|
|
192
|
-
when /technical/ then new_result.activate(:
|
|
193
|
-
else new_result.activate(:
|
|
203
|
+
when /billing/ then new_result.activate(:billing_agent)
|
|
204
|
+
when /technical/ then new_result.activate(:tech_agent)
|
|
205
|
+
else new_result.activate(:general_agent)
|
|
194
206
|
end
|
|
195
207
|
end
|
|
196
208
|
end
|
|
197
209
|
```
|
|
198
210
|
|
|
211
|
+
> [!WARNING]
|
|
212
|
+
> `activate` validates its argument and **raises**. Activating `:billing` when the
|
|
213
|
+
> network declared `task :billing_agent, ...` aborts the run with
|
|
214
|
+
> `ArgumentError: Step :classifier attempted to activate unknown step :billing`.
|
|
215
|
+
> Activating a task that was *not* declared `depends_on: :optional` raises too:
|
|
216
|
+
> `ArgumentError: Step :classifier attempted to activate non-optional step :billing_agent.`
|
|
217
|
+
> Use `result.activated_steps` to inspect what routing actually did.
|
|
218
|
+
|
|
199
219
|
!!! note "extract_run_context"
|
|
200
220
|
The `extract_run_context(result)` method is a protected helper on `Robot`. It extracts `run_params` from the SimpleFlow result context, handles value propagation from previous steps, and separates robot-specific params (`mcp:`, `tools:`, `memory:`, `network_memory:`) from the message and other context.
|
|
201
221
|
|
|
@@ -245,22 +265,43 @@ end
|
|
|
245
265
|
|
|
246
266
|
## Per-Task Configuration
|
|
247
267
|
|
|
248
|
-
Tasks can
|
|
268
|
+
Tasks can carry their own context, a tool allowlist, and MCP servers:
|
|
249
269
|
|
|
250
270
|
```ruby
|
|
271
|
+
# Tools must already be ATTACHED to the robot...
|
|
272
|
+
billing_agent = RobotLab.build(
|
|
273
|
+
name: "billing_agent",
|
|
274
|
+
system_prompt: "You handle billing.",
|
|
275
|
+
local_tools: [RefundTool, InvoiceTool, AuditTool]
|
|
276
|
+
)
|
|
277
|
+
|
|
251
278
|
network = RobotLab.create_network(name: "support") do
|
|
252
279
|
task :classifier, classifier, depends_on: :none
|
|
253
280
|
task :billing_agent, billing_agent,
|
|
254
281
|
context: { department: "billing", escalation_level: 2 },
|
|
255
|
-
tools: [RefundTool, InvoiceTool],
|
|
282
|
+
tools: [RefundTool, InvoiceTool], # ...and this SELECTS from them
|
|
256
283
|
depends_on: :optional
|
|
257
284
|
task :tech_agent, tech_agent,
|
|
258
285
|
context: { department: "technical" },
|
|
259
286
|
mcp: [filesystem_server],
|
|
287
|
+
tools: :inherit, # send every discovered MCP tool
|
|
260
288
|
depends_on: :optional
|
|
261
289
|
end
|
|
262
290
|
```
|
|
263
291
|
|
|
292
|
+
> [!WARNING]
|
|
293
|
+
> Per-task `tools:` is an **allowlist over tools the robot already has** — it never
|
|
294
|
+
> attaches anything. Give a task `tools: [RefundTool]` for a robot whose
|
|
295
|
+
> `local_tools` are empty and it sends zero tools.
|
|
296
|
+
>
|
|
297
|
+
> Matching is by string comparison against each attached tool's `name`, so the two
|
|
298
|
+
> sides must agree. `local_tools: [RefundTool]` (the class) matches
|
|
299
|
+
> `tools: [RefundTool]`, but `local_tools: [RefundTool.new]` (an instance, whose
|
|
300
|
+
> name is `"refund"`) does not — use `tools: %w[refund]` there.
|
|
301
|
+
>
|
|
302
|
+
> Omitting `tools:` from a task means `:none`: the robot sends no tools at all.
|
|
303
|
+
> Use `tools: :inherit` for "send everything attached".
|
|
304
|
+
|
|
264
305
|
The `Task` wrapper deep-merges per-task context with the network's run params before delegating to the robot's `call` method.
|
|
265
306
|
|
|
266
307
|
## Pipeline Pattern
|
|
@@ -298,13 +339,19 @@ puts result.value.last_text_content
|
|
|
298
339
|
Fan-out / fan-in pattern where multiple robots analyze in parallel and a synthesizer merges results:
|
|
299
340
|
|
|
300
341
|
```ruby
|
|
342
|
+
# Robot names deliberately match their task names -- result.context is keyed
|
|
343
|
+
# by the ROBOT's name, so mismatched names produce nil lookups below.
|
|
344
|
+
sentiment = RobotLab.build(name: "sentiment", system_prompt: "Score sentiment.")
|
|
345
|
+
entities = RobotLab.build(name: "entities", system_prompt: "Extract entities.")
|
|
346
|
+
keywords = RobotLab.build(name: "keywords", system_prompt: "Extract keywords.")
|
|
347
|
+
|
|
301
348
|
network = RobotLab.create_network(name: "multi_analysis") do
|
|
302
349
|
task :prepare, preparer, depends_on: :none
|
|
303
350
|
|
|
304
351
|
# These run in parallel (all depend on :prepare)
|
|
305
|
-
task :sentiment,
|
|
306
|
-
task :entities,
|
|
307
|
-
task :keywords,
|
|
352
|
+
task :sentiment, sentiment, depends_on: [:prepare]
|
|
353
|
+
task :entities, entities, depends_on: [:prepare]
|
|
354
|
+
task :keywords, keywords, depends_on: [:prepare]
|
|
308
355
|
|
|
309
356
|
# Waits for all three to complete
|
|
310
357
|
task :summarize, summarizer, depends_on: [:sentiment, :entities, :keywords]
|
|
@@ -312,13 +359,19 @@ end
|
|
|
312
359
|
|
|
313
360
|
result = network.run(message: "Analyze this text")
|
|
314
361
|
|
|
315
|
-
# Access parallel results from context
|
|
362
|
+
# Access parallel results from context (keyed by robot name)
|
|
316
363
|
puts "Sentiment: #{result.context[:sentiment].last_text_content}"
|
|
317
364
|
puts "Entities: #{result.context[:entities].last_text_content}"
|
|
318
365
|
puts "Keywords: #{result.context[:keywords].last_text_content}"
|
|
319
366
|
puts "Summary: #{result.value.last_text_content}"
|
|
320
367
|
```
|
|
321
368
|
|
|
369
|
+
> [!WARNING]
|
|
370
|
+
> `result.context` is keyed by `@name` of the robot that wrote it, not by the task
|
|
371
|
+
> name. Had these robots kept names like `sentiment_analyzer`, the lookups above
|
|
372
|
+
> would return `nil` even though every task ran. Keep robot name and task name
|
|
373
|
+
> identical.
|
|
374
|
+
|
|
322
375
|
## Shared Memory in Networks
|
|
323
376
|
|
|
324
377
|
Networks provide a shared `Memory` instance that all robots can read and write. This is especially useful for parallel robots that need to coordinate:
|
|
@@ -374,30 +427,47 @@ network = RobotLab.create_network(name: "validated_pipeline") do
|
|
|
374
427
|
end
|
|
375
428
|
|
|
376
429
|
result = network.run(message: "Process this")
|
|
377
|
-
if result.
|
|
378
|
-
puts "Validation failed: #{result.value.last_text_content}"
|
|
379
|
-
else
|
|
430
|
+
if result.continue?
|
|
380
431
|
puts "Processing complete: #{result.value.last_text_content}"
|
|
432
|
+
else
|
|
433
|
+
puts "Validation failed: #{result.value.last_text_content}"
|
|
381
434
|
end
|
|
382
435
|
```
|
|
383
436
|
|
|
437
|
+
> [!WARNING]
|
|
438
|
+
> `SimpleFlow::Result` has **no** `halted?` or `continued?` predicate — calling
|
|
439
|
+
> either raises `NoMethodError`. The predicate is `continue?`, which returns
|
|
440
|
+
> `false` after `halt`. The full public API is exactly `activate`,
|
|
441
|
+
> `activated_steps`, `context`, `continue`, `continue?`, `errors`, `halt`, `value`,
|
|
442
|
+
> `with_context`, and `with_error`. `with_value` is **private** — calling it raises
|
|
443
|
+
> `NoMethodError`; use `continue(value)` or `halt(value)` to set the value.
|
|
444
|
+
|
|
384
445
|
## Running
|
|
385
446
|
|
|
386
447
|
```bash
|
|
387
448
|
export ANTHROPIC_API_KEY="your-key"
|
|
388
|
-
|
|
449
|
+
|
|
450
|
+
# Classifier + three specialists with optional-task routing
|
|
451
|
+
ruby examples/03_network.rb
|
|
452
|
+
|
|
453
|
+
# Shared reactive memory across concurrent robots
|
|
454
|
+
ruby examples/07_network_memory.rb
|
|
455
|
+
|
|
456
|
+
# Network visualization and introspection (no LLM calls)
|
|
457
|
+
ruby examples/11_network_introspection.rb
|
|
389
458
|
```
|
|
390
459
|
|
|
391
460
|
## Key Concepts
|
|
392
461
|
|
|
393
|
-
1. **SimpleFlow Pipeline**: DAG-based execution with dependency management via `depends_on:`
|
|
462
|
+
1. **SimpleFlow Pipeline**: DAG-based execution with dependency management via `depends_on:`. The DSL method is `task` — there is no `step`, no `router:` kwarg, and no `Router` class
|
|
394
463
|
2. **Optional Tasks**: Use `depends_on: :optional` for tasks activated dynamically by classifiers
|
|
395
|
-
3. **Robot#call Override**:
|
|
464
|
+
3. **Robot#call Override**: Routing is written in Ruby — subclass `Robot`, override `call`, and call `result.activate(:task_name)`
|
|
396
465
|
4. **extract_run_context**: Helper method to extract message and params from `SimpleFlow::Result`
|
|
397
|
-
5. **Context Flow**:
|
|
398
|
-
6. **
|
|
399
|
-
7. **
|
|
400
|
-
8. **
|
|
466
|
+
5. **Context Flow**: `result.context` is keyed by the **robot's** name (`with_context(@name.to_sym, ...)`), while `activate` takes a **task** name — keep the two identical
|
|
467
|
+
6. **Result predicates**: `continue?` (not `halted?`), plus `activated_steps` to see what routing actually did
|
|
468
|
+
7. **Parallel Execution**: Tasks with the same dependencies run concurrently; cap in-flight LLM calls with `RunConfig.new(max_concurrent_robots: N)`
|
|
469
|
+
8. **Shared Memory**: Network memory (`network_memory:`) enables inter-robot communication
|
|
470
|
+
9. **Per-Task Configuration**: Each task can carry its own context, MCP servers, and a `tools:` allowlist over the robot's already-attached tools
|
|
401
471
|
|
|
402
472
|
## See Also
|
|
403
473
|
|
data/docs/examples/tool-usage.md
CHANGED
|
@@ -6,13 +6,36 @@ Robots with external capabilities through tools.
|
|
|
6
6
|
|
|
7
7
|
This example demonstrates how to give robots access to external systems through tools. Tools are defined as `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances and passed to robots via the `local_tools:` parameter.
|
|
8
8
|
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> **Attaching a tool is not the same as sending it.** `Robot#run` defaults to
|
|
11
|
+
> `tools: :none`, so a plain `robot.run("...")` sends the LLM zero tools even when
|
|
12
|
+
> `local_tools:` were supplied at build time. Pass `tools: :inherit` on every call
|
|
13
|
+
> that should be able to use them:
|
|
14
|
+
>
|
|
15
|
+
> ```ruby
|
|
16
|
+
> robot.run("What is 15 * 7?", tools: :inherit)
|
|
17
|
+
> ```
|
|
18
|
+
>
|
|
19
|
+
> For a **standalone** robot, do not pass `tools: :inherit` in the constructor:
|
|
20
|
+
> at build time it resolves against the parent level (`:none`) and yields an
|
|
21
|
+
> allowlist matching nothing, suppressing the tools even when the run asks for
|
|
22
|
+
> them. Leave `tools:` unset there.
|
|
23
|
+
>
|
|
24
|
+
> Inside a **network**, the opposite holds — build-time `tools: :inherit` is how a
|
|
25
|
+
> robot opts into the network `config:`'s allowlist. See
|
|
26
|
+
> [Creating Networks](../guides/creating-networks.md).
|
|
27
|
+
>
|
|
28
|
+
> An explicit array (`tools: [Calculator]`) is an allowlist, not a local-vs-MCP
|
|
29
|
+
> switch, and its entries must match how each tool was attached — this page
|
|
30
|
+
> attaches classes, so use class names.
|
|
31
|
+
|
|
9
32
|
## RubyLLM::Tool Subclass Pattern
|
|
10
33
|
|
|
11
34
|
The primary way to define tools is by subclassing `RubyLLM::Tool`:
|
|
12
35
|
|
|
13
36
|
```ruby
|
|
14
37
|
#!/usr/bin/env ruby
|
|
15
|
-
# examples/
|
|
38
|
+
# Tool definitions mirror examples/02_tools.rb
|
|
16
39
|
|
|
17
40
|
require "bundler/setup"
|
|
18
41
|
require "robot_lab"
|
|
@@ -79,21 +102,26 @@ robot = RobotLab.build(
|
|
|
79
102
|
model: "claude-sonnet-4"
|
|
80
103
|
)
|
|
81
104
|
|
|
82
|
-
# Run the robot
|
|
83
|
-
|
|
105
|
+
# Run the robot -- tools: :inherit is what actually sends Calculator and
|
|
106
|
+
# FortuneCookie to the LLM
|
|
107
|
+
result = robot.run(
|
|
108
|
+
"What is 15 multiplied by 7? Also, give me a career fortune.",
|
|
109
|
+
tools: :inherit
|
|
110
|
+
)
|
|
84
111
|
|
|
85
112
|
# Display results
|
|
86
113
|
puts "Response: #{result.last_text_content}"
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
result.tool_calls.each do |tc|
|
|
91
|
-
tool_info = tc.respond_to?(:tool) ? tc.tool : tc
|
|
92
|
-
puts " #{tool_info[:name] || tool_info}"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
115
|
+
# Confirm which tools were sent for this turn
|
|
116
|
+
puts "Tools sent: #{robot.chat.tools.keys.join(', ')}"
|
|
95
117
|
```
|
|
96
118
|
|
|
119
|
+
> [!NOTE]
|
|
120
|
+
> `result.tool_calls` is effectively always empty. It reads the *final* assistant
|
|
121
|
+
> message, and by the time ruby_llm's tool loop has finished that message contains
|
|
122
|
+
> only text. To observe tool activity, use the `on_tool_call:` callback (below) or
|
|
123
|
+
> the `:tool_call` hooks.
|
|
124
|
+
|
|
97
125
|
## RobotLab::Tool.create Pattern
|
|
98
126
|
|
|
99
127
|
For simpler tools that do not need their own class, use `RobotLab::Tool.create`:
|
|
@@ -127,15 +155,22 @@ robot = RobotLab.build(
|
|
|
127
155
|
model: "claude-sonnet-4"
|
|
128
156
|
)
|
|
129
157
|
|
|
130
|
-
result = robot.run("What time is it and what's the weather in New York?")
|
|
158
|
+
result = robot.run("What time is it and what's the weather in New York?", tools: :inherit)
|
|
131
159
|
puts result.last_text_content
|
|
132
160
|
```
|
|
133
161
|
|
|
162
|
+
The block receives a single hash of symbol-keyed arguments.
|
|
163
|
+
|
|
164
|
+
> [!NOTE]
|
|
165
|
+
> `RobotLab::Tool.create(parameters:)` accepts a JSON-Schema-shaped hash, but only
|
|
166
|
+
> `type` and `description` are read off each property, plus the top-level `required`
|
|
167
|
+
> list. Anything else (`enum`, `default`, nested `items`) is ignored — ruby_llm's
|
|
168
|
+
> `param` DSL supports only `type:`, `desc:`/`description:`, and `required:`.
|
|
169
|
+
|
|
134
170
|
## Weather API Integration
|
|
135
171
|
|
|
136
172
|
```ruby
|
|
137
173
|
#!/usr/bin/env ruby
|
|
138
|
-
# examples/weather_assistant.rb
|
|
139
174
|
|
|
140
175
|
require "bundler/setup"
|
|
141
176
|
require "robot_lab"
|
|
@@ -227,7 +262,7 @@ loop do
|
|
|
227
262
|
break if input.nil? || input.downcase == "quit"
|
|
228
263
|
next if input.empty?
|
|
229
264
|
|
|
230
|
-
result = weather_bot.run(input)
|
|
265
|
+
result = weather_bot.run(input, tools: :inherit)
|
|
231
266
|
puts "\nAssistant: #{result.last_text_content}"
|
|
232
267
|
end
|
|
233
268
|
|
|
@@ -237,8 +272,6 @@ puts "\nGoodbye!"
|
|
|
237
272
|
## Database Integration
|
|
238
273
|
|
|
239
274
|
```ruby
|
|
240
|
-
# examples/order_assistant.rb
|
|
241
|
-
|
|
242
275
|
require "robot_lab"
|
|
243
276
|
|
|
244
277
|
# Mock database
|
|
@@ -297,7 +330,7 @@ order_bot = RobotLab.build(
|
|
|
297
330
|
)
|
|
298
331
|
|
|
299
332
|
# Run with a question
|
|
300
|
-
result = order_bot.run("What's the status of order ORD001?")
|
|
333
|
+
result = order_bot.run("What's the status of order ORD001?", tools: :inherit)
|
|
301
334
|
puts result.last_text_content
|
|
302
335
|
```
|
|
303
336
|
|
|
@@ -314,27 +347,41 @@ robot = RobotLab.build(
|
|
|
314
347
|
on_tool_call: ->(tool_call) {
|
|
315
348
|
puts "[Tool Call] #{tool_call.name}: #{tool_call.arguments}"
|
|
316
349
|
},
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
# NOTE: one argument -- the result. There is no tool_call parameter here.
|
|
351
|
+
on_tool_result: ->(result) {
|
|
352
|
+
puts "[Tool Result] #{result}"
|
|
319
353
|
}
|
|
320
354
|
)
|
|
321
355
|
|
|
322
|
-
result = robot.run("What is 42 * 17?")
|
|
356
|
+
result = robot.run("What is 42 * 17?", tools: :inherit)
|
|
323
357
|
```
|
|
324
358
|
|
|
359
|
+
> [!WARNING]
|
|
360
|
+
> `on_tool_result` receives **exactly one** argument, the tool's return value.
|
|
361
|
+
> Writing `->(tool_call, result)` raises
|
|
362
|
+
> `ArgumentError: wrong number of arguments (given 1, expected 2)` the first time a
|
|
363
|
+
> tool runs.
|
|
364
|
+
>
|
|
365
|
+
> Both callbacks map onto ruby_llm's legacy hooks and are deprecated as of
|
|
366
|
+
> ruby_llm 1.16 — wiring either one emits
|
|
367
|
+
> ``` `on_tool_call` is deprecated and will be removed in RubyLLM 2.0. Use `before_tool_call` instead. ```
|
|
368
|
+
> The additive replacements are `before_message`, `after_message`,
|
|
369
|
+
> `before_tool_call`, and `after_tool_result`. RobotLab's own
|
|
370
|
+
> [Hook system](../guides/hooks.md) is the supported way to observe tool activity.
|
|
371
|
+
|
|
325
372
|
## Running
|
|
326
373
|
|
|
327
374
|
```bash
|
|
328
375
|
export ANTHROPIC_API_KEY="your-key"
|
|
329
376
|
|
|
330
|
-
# Tool
|
|
331
|
-
ruby examples/
|
|
377
|
+
# Tool definitions and a robot that uses them
|
|
378
|
+
ruby examples/02_tools.rb
|
|
332
379
|
|
|
333
|
-
#
|
|
334
|
-
ruby examples/
|
|
380
|
+
# Tool loop circuit breaker (max_tool_rounds)
|
|
381
|
+
ruby examples/20_circuit_breaker.rb
|
|
335
382
|
|
|
336
|
-
#
|
|
337
|
-
ruby examples/
|
|
383
|
+
# Ractor-safe CPU tools (no LLM calls)
|
|
384
|
+
ruby examples/29_ractor_tools.rb
|
|
338
385
|
```
|
|
339
386
|
|
|
340
387
|
## Interactive User Input
|
|
@@ -348,17 +395,30 @@ robot = RobotLab.build(
|
|
|
348
395
|
name: "interviewer",
|
|
349
396
|
system_prompt: <<~PROMPT,
|
|
350
397
|
You are a project setup assistant. Interview the user to understand their
|
|
351
|
-
needs, then summarize the project plan. Use the ask_user tool to
|
|
352
|
-
information one question at a time.
|
|
398
|
+
needs, then summarize the project plan. Use the robot_lab--ask_user tool to
|
|
399
|
+
gather information one question at a time.
|
|
353
400
|
PROMPT
|
|
354
|
-
local_tools: [RobotLab::AskUser],
|
|
355
401
|
model: "claude-sonnet-4"
|
|
356
402
|
)
|
|
357
403
|
|
|
358
|
-
|
|
404
|
+
# Attach an INSTANCE bound to the robot so the tool uses robot.input/robot.output
|
|
405
|
+
robot.local_tools << RobotLab::AskUser.new(robot: robot)
|
|
406
|
+
|
|
407
|
+
result = robot.run("Help me plan a new web application", tools: :inherit)
|
|
359
408
|
puts "\nProject Plan:\n#{result.last_text_content}"
|
|
360
409
|
```
|
|
361
410
|
|
|
411
|
+
> [!WARNING]
|
|
412
|
+
> Pass an **instance**, not the class. `RobotLab::Tool` derives its LLM-visible
|
|
413
|
+
> name from the full class name, so `RobotLab::AskUser` is presented to the model
|
|
414
|
+
> as **`robot_lab--ask_user`**, not `ask_user` — name it that way in your system
|
|
415
|
+
> prompt.
|
|
416
|
+
>
|
|
417
|
+
> An instance created without `robot:` has `tool.robot == nil`, so it ignores
|
|
418
|
+
> `robot.input` / `robot.output`, falls back to `$stdin` / `$stdout`, and labels
|
|
419
|
+
> every prompt `[Robot]` instead of the robot's name. Always use
|
|
420
|
+
> `RobotLab::AskUser.new(robot: robot)`.
|
|
421
|
+
|
|
362
422
|
The robot will ask questions interactively:
|
|
363
423
|
|
|
364
424
|
```
|
|
@@ -375,24 +435,31 @@ The robot will ask questions interactively:
|
|
|
375
435
|
> Customer support portal
|
|
376
436
|
```
|
|
377
437
|
|
|
378
|
-
For testing, inject `StringIO` objects
|
|
438
|
+
For testing, inject `StringIO` objects on the robot *before* building the tool
|
|
439
|
+
instance (the tool reads them through its `robot` reference at call time):
|
|
379
440
|
|
|
380
441
|
```ruby
|
|
381
442
|
robot.input = StringIO.new("Ruby\nyes\nCustomer portal\n")
|
|
382
443
|
robot.output = StringIO.new
|
|
383
444
|
```
|
|
384
445
|
|
|
446
|
+
`RobotLab::AskUser` can also be driven directly, outside an LLM turn — see
|
|
447
|
+
[`examples/06_prompt_templates.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/06_prompt_templates.rb),
|
|
448
|
+
which calls `RobotLab::AskUser.new.call("question" => ..., "default" => ...)` to
|
|
449
|
+
collect a value before any robot is built.
|
|
450
|
+
|
|
385
451
|
## Key Concepts
|
|
386
452
|
|
|
387
453
|
1. **RubyLLM::Tool subclass**: Define a class with `description`, `param`, and `execute` method
|
|
388
|
-
2. **RobotLab::Tool subclass**: Same DSL plus `robot` accessor for robot-aware tools
|
|
454
|
+
2. **RobotLab::Tool subclass**: Same DSL plus a `robot` accessor for robot-aware tools
|
|
389
455
|
3. **RobotLab::Tool.create**: Use `RobotLab::Tool.create(name:, description:, &block)` for dynamic tools
|
|
390
|
-
4. **Built-in tools**: `RobotLab::AskUser` for interactive terminal input
|
|
391
|
-
5. **local_tools**: Pass tool classes/instances via `local_tools:`
|
|
392
|
-
6. **
|
|
393
|
-
7. **
|
|
394
|
-
8. **
|
|
395
|
-
9. **
|
|
456
|
+
4. **Built-in tools**: `RobotLab::AskUser` (LLM-visible name `robot_lab--ask_user`) for interactive terminal input
|
|
457
|
+
5. **local_tools**: Pass tool classes/instances via `local_tools:` to `RobotLab.build` or `Robot.new` — this *attaches* them
|
|
458
|
+
6. **tools: :inherit**: Required on `run` to actually *send* the attached tools; the default is `tools: :none`
|
|
459
|
+
7. **Frontmatter tools**: Declare tool class names in template YAML front matter (`tools: [Calculator]`); they populate `local_tools`, and still need `tools: :inherit` at run time. Constructor `local_tools:` overrides the front-matter list
|
|
460
|
+
8. **Error Handling**: Raised exceptions are caught and returned to the LLM as text (`"Error (tool_name): message"`); a `RobotLab::ToolError` with `retryable: true` appends `" (retryable)"`. Set `self.raise_on_error = true` on a class to opt out — note this is per-class and is *not* inherited by subclasses
|
|
461
|
+
9. **Tool cap**: At most `DEFAULT_MAX_TOOLS` (128) tools are sent. Setting `max_tools` to nil, 0, or a negative number falls back to 128 — the cap cannot be disabled
|
|
462
|
+
10. **Result Access**: Use `result.last_text_content` (alias `result.reply`) for the final response. `result.tool_calls` is effectively always empty — observe tool activity with `on_tool_call:` or the Hook system instead
|
|
396
463
|
|
|
397
464
|
## See Also
|
|
398
465
|
|