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
data/docs/guides/using-tools.md
CHANGED
|
@@ -85,16 +85,29 @@ weather_tool = RobotLab::Tool.create(
|
|
|
85
85
|
|
|
86
86
|
`RobotLab::AskUser` lets a robot ask the user a question via the terminal. The LLM decides when it needs human input and calls the tool with a question, optional choices, and an optional default.
|
|
87
87
|
|
|
88
|
+
`AskUser` reads its IO from `robot.input` / `robot.output`, and it finds the
|
|
89
|
+
robot through its own `robot` accessor — which is only populated when you attach
|
|
90
|
+
an **instance constructed with `robot:`**. Build the robot first, then attach:
|
|
91
|
+
|
|
88
92
|
```ruby
|
|
89
93
|
robot = RobotLab.build(
|
|
90
94
|
name: "onboarding",
|
|
91
|
-
system_prompt: "Walk the user through project setup. Ask questions to understand their needs."
|
|
92
|
-
local_tools: [RobotLab::AskUser]
|
|
95
|
+
system_prompt: "Walk the user through project setup. Ask questions to understand their needs."
|
|
93
96
|
)
|
|
94
|
-
robot.
|
|
97
|
+
robot.local_tools << RobotLab::AskUser.new(robot: robot)
|
|
98
|
+
|
|
99
|
+
robot.run("Help the user set up a new project", tools: :inherit)
|
|
95
100
|
```
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
> [!WARNING]
|
|
103
|
+
> Passing the bare class — `local_tools: [RobotLab::AskUser]` — leaves
|
|
104
|
+
> `tool.robot` as `nil`. The tool then ignores `robot.input`/`robot.output`,
|
|
105
|
+
> reads from the real `$stdin` (blocking your process, and hanging any test
|
|
106
|
+
> suite), and labels its prompt `[Robot]` instead of the robot's name.
|
|
107
|
+
> Note also the `tools: :inherit` above: a plain `run()` sends **zero** tools,
|
|
108
|
+
> so the LLM would never see `AskUser` at all.
|
|
109
|
+
|
|
110
|
+
The tool displays the robot's name and question, then waits for input:
|
|
98
111
|
|
|
99
112
|
```
|
|
100
113
|
[onboarding] What programming language will you use?
|
|
@@ -110,13 +123,27 @@ Features:
|
|
|
110
123
|
- **Multiple choice**: numbered options, user types the number or text
|
|
111
124
|
- **Default value**: shown in the prompt, used when user presses Enter
|
|
112
125
|
|
|
113
|
-
IO is sourced from
|
|
126
|
+
Because the IO is sourced from the robot, a `StringIO` pair makes the tool
|
|
127
|
+
testable without a terminal:
|
|
114
128
|
|
|
115
129
|
```ruby
|
|
116
130
|
robot.input = StringIO.new("2\n")
|
|
117
131
|
robot.output = StringIO.new
|
|
132
|
+
|
|
133
|
+
# ... the LLM calls ask_user(question: "Which language?", choices: %w[Ruby Python Go])
|
|
134
|
+
robot.output.string
|
|
135
|
+
# => "\n[onboarding] Which language?\n 1. Ruby\n 2. Python\n 3. Go\n> "
|
|
118
136
|
```
|
|
119
137
|
|
|
138
|
+
> [!NOTE]
|
|
139
|
+
> The name the LLM sees is **`robot_lab--ask_user`**, not `ask_user`. RubyLLM
|
|
140
|
+
> derives tool names from the full class name including its namespace, so
|
|
141
|
+
> `RobotLab::AskUser` becomes `robot_lab--ask_user`. (A trailing `Tool` is
|
|
142
|
+
> stripped: a top-level `WeatherTool` is exposed as `weather`.) These derived
|
|
143
|
+
> names are what an allowlist must match **for tools attached as instances** —
|
|
144
|
+
> a tool attached as a class matches its class name instead. See
|
|
145
|
+
> [Runtime Tool Filtering](#runtime-tool-filtering).
|
|
146
|
+
|
|
120
147
|
See the [AskUser API reference](../api/core/tool.md#built-in-askuser) for full details.
|
|
121
148
|
|
|
122
149
|
## Attaching Tools to Robots
|
|
@@ -133,6 +160,15 @@ robot = RobotLab.build(
|
|
|
133
160
|
)
|
|
134
161
|
```
|
|
135
162
|
|
|
163
|
+
`local_tools:` and `tools:` are different mechanisms — don't confuse them. `local_tools:` attaches tool **instances or classes**; `tools:` (see [Runtime Tool Filtering](#runtime-tool-filtering) below) is a **name allowlist** that filters which of the already-attached tools are sent for a given turn. Passing an instance or class to `tools:` raises `ArgumentError` immediately, naming the offending class and pointing you at `local_tools:` instead:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
RobotLab.build(name: "bot", tools: [GetWeather.new])
|
|
167
|
+
# => ArgumentError: `tools:` expects tool names (String/Symbol) to allow, but
|
|
168
|
+
# received GetWeather. To attach tool instances or classes, pass them as
|
|
169
|
+
# `local_tools:` (e.g. RobotLab.build(local_tools: [MyTool.new])).
|
|
170
|
+
```
|
|
171
|
+
|
|
136
172
|
### Via Template Front Matter
|
|
137
173
|
|
|
138
174
|
Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via `Object.const_get` and instantiates it:
|
|
@@ -156,13 +192,156 @@ Tool classes must be defined and loaded before building the robot. Unresolvable
|
|
|
156
192
|
|
|
157
193
|
### Via Chaining
|
|
158
194
|
|
|
159
|
-
|
|
195
|
+
`with_tools` is delegated straight to the underlying `RubyLLM::Chat`, so it
|
|
196
|
+
writes tools onto the chat rather than onto the robot's `local_tools`:
|
|
160
197
|
|
|
161
198
|
```ruby
|
|
162
199
|
robot = RobotLab.build(name: "assistant", system_prompt: "...")
|
|
163
200
|
robot.with_tools(GetWeather, CalculatorTool)
|
|
164
201
|
```
|
|
165
202
|
|
|
203
|
+
> [!WARNING]
|
|
204
|
+
> Chained tools do not survive a `run`. Because `run` defaults to
|
|
205
|
+
> `tools: :none`, it replaces the chat's tool list with an empty one before
|
|
206
|
+
> asking the LLM — wiping whatever `with_tools` put there:
|
|
207
|
+
>
|
|
208
|
+
> ```ruby
|
|
209
|
+
> robot.with_tools(GetWeather, CalculatorTool)
|
|
210
|
+
> robot.chat.tools.keys # => [:get_weather, :calculator]
|
|
211
|
+
> robot.run("Hello")
|
|
212
|
+
> robot.chat.tools.keys # => [] <- cleared by the run
|
|
213
|
+
> ```
|
|
214
|
+
>
|
|
215
|
+
> For tools that should persist across runs, attach them with `local_tools:`
|
|
216
|
+
> and pass `tools: :inherit` on each `run`.
|
|
217
|
+
|
|
218
|
+
## Runtime Tool Filtering
|
|
219
|
+
|
|
220
|
+
`tools:` (and `mcp:`) also work as a **per-run** override, passed to `run()` itself, on top of the build-time/network/global hierarchy described in [Hierarchical MCP and Tools](../getting-started/configuration.md#hierarchical-mcp-and-tools):
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
robot.run("What's the weather?", tools: :inherit) # every attached tool
|
|
224
|
+
robot.run("Just chat, no tools needed.", tools: :none) # zero tools this turn
|
|
225
|
+
# Allowlist. This robot attached its tools as CLASSES, so match the class names:
|
|
226
|
+
robot.run("Only use the calculator.", tools: %w[CalculatorTool])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Value | Meaning |
|
|
230
|
+
|-------|---------|
|
|
231
|
+
| `:inherit` | Propagate the parent level's allowlist (build-time / network / global) |
|
|
232
|
+
| `:none`, `[]` | Send **zero** tools this turn |
|
|
233
|
+
| `nil` | No filter at all — every attached tool, **discarding** any parent allowlist |
|
|
234
|
+
| `["name", ...]` | Only these tools, matched against how each was attached |
|
|
235
|
+
|
|
236
|
+
> [!CAUTION]
|
|
237
|
+
> `tools: nil` does **not** send zero tools, and it is **not** the same as
|
|
238
|
+
> `:inherit`. `:inherit` carries the parent's allowlist down; `nil` throws the
|
|
239
|
+
> filter away entirely. With a robot built `tools: %w[GetWeather]` holding
|
|
240
|
+
> `GetWeather` and `CalculatorTool`:
|
|
241
|
+
>
|
|
242
|
+
> ```
|
|
243
|
+
> run(tools: :inherit) -> [:get_weather] # parent allowlist honored
|
|
244
|
+
> run(tools: nil) -> [:get_weather, :calculator] # allowlist discarded
|
|
245
|
+
> ```
|
|
246
|
+
>
|
|
247
|
+
> If you mean zero tools, write `:none`.
|
|
248
|
+
|
|
249
|
+
> [!IMPORTANT]
|
|
250
|
+
> An allowlist entry must match **how the tool was attached**, because the
|
|
251
|
+
> comparison uses `tool.name` and `Class#name` differs from `RubyLLM::Tool#name`:
|
|
252
|
+
>
|
|
253
|
+
> | Attached as | Allowlist that matches |
|
|
254
|
+
> |---|---|
|
|
255
|
+
> | `local_tools: [CalculatorTool]` (class) | `[CalculatorTool]` or `%w[CalculatorTool]` |
|
|
256
|
+
> | `local_tools: [CalculatorTool.new]` (instance) | `%w[calculator]` |
|
|
257
|
+
>
|
|
258
|
+
> Note the constructor's `tools:` is validated and **rejects** classes and
|
|
259
|
+
> instances outright (`ArgumentError`); only task-level and `run()` values accept
|
|
260
|
+
> them.
|
|
261
|
+
|
|
262
|
+
An explicit `:none`/`[]` is useful for a relevance filter that decided no tool is useful for the current message — it now genuinely sends zero tools for that turn (previously an empty allowlist was silently treated as "all tools," which could overflow small-context local models with the full tool set). Each turn's resolved tool set fully **replaces** the chat's tools rather than accumulating, so a later `:none` turn correctly clears whatever a prior turn attached.
|
|
263
|
+
|
|
264
|
+
> [!CAUTION]
|
|
265
|
+
> **Watch the default.** Both `Robot.new`'s and `run()`'s `tools:`/`mcp:`
|
|
266
|
+
> parameters default to `:none`, not `:inherit`. If you build a robot with
|
|
267
|
+
> `local_tools:` and then call `robot.run(message)` with no `tools:` override at
|
|
268
|
+
> all, the runtime default takes the explicit-`:none` path above — sending no
|
|
269
|
+
> tools for that turn. **Pass `tools: :inherit` on the `run()` call** (or on the
|
|
270
|
+
> network `task`, which is forwarded to `run()`) anywhere you need the robot's
|
|
271
|
+
> attached tools available.
|
|
272
|
+
|
|
273
|
+
> [!WARNING]
|
|
274
|
+
> For a **standalone** robot, do not pass `tools: :inherit` at *build* time as a
|
|
275
|
+
> way to turn tools on. Build-time `:inherit` resolves against the level above
|
|
276
|
+
> it, which for a standalone robot is the global `:none` — producing an
|
|
277
|
+
> allowlist of `["none"]` that matches nothing, and the empty result then
|
|
278
|
+
> carries into the runtime pass. Leave build-time `tools:` unset:
|
|
279
|
+
>
|
|
280
|
+
> ```ruby
|
|
281
|
+
> # build tools: unset + run(tools: :inherit) -> [:t1] correct
|
|
282
|
+
> # build tools: :inherit + run(tools: :inherit) -> [] the standalone trap
|
|
283
|
+
> # build tools: :none + run(tools: :inherit) -> [:t1] also fine
|
|
284
|
+
> ```
|
|
285
|
+
>
|
|
286
|
+
> This applies to a robot run on its own. Inside a **network** whose `config:`
|
|
287
|
+
> supplies `tools:`/`mcp:`, the parent level is that network list rather than
|
|
288
|
+
> `:none`, and build-time `:inherit` is *required* — it is the robot's opt-in to
|
|
289
|
+
> the network value. See
|
|
290
|
+
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
291
|
+
|
|
292
|
+
### Tool Capping and Per-Turn Filtering
|
|
293
|
+
|
|
294
|
+
Most LLM providers reject a tool array longer than 128 entries and fail the whole turn. RobotLab clamps the fully-resolved tool list to a ceiling right before handing it to the chat provider — the definitive choke point regardless of how the tools were configured, filtered, or MCP-connected:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
robot = RobotLab.build(
|
|
298
|
+
name: "power_user",
|
|
299
|
+
system_prompt: "...",
|
|
300
|
+
local_tools: many_tools, # say, 150 tools
|
|
301
|
+
config: RobotLab::RunConfig.new(max_tools: 50) # override the default ceiling
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- Default ceiling: **128** tools per turn (`RobotLab::Robot::DEFAULT_MAX_TOOLS`)
|
|
306
|
+
- Override with a **positive** `max_tools:` on `RunConfig` (or the `max_tools:` cascade field — see [Available Fields](../getting-started/configuration.md#available-fields))
|
|
307
|
+
- When a turn's resolved tools exceed the cap, RobotLab logs a warning naming how many were dropped and sends the first `max_tools` entries
|
|
308
|
+
|
|
309
|
+
> [!WARNING]
|
|
310
|
+
> The cap **cannot be disabled.** `max_tools: nil`, `0`, or a negative number
|
|
311
|
+
> all fall back to the 128 default — only a positive integer changes the
|
|
312
|
+
> ceiling. If you attach more than 128 tools and set `max_tools: 0` expecting
|
|
313
|
+
> "unlimited", you get the first 128.
|
|
314
|
+
>
|
|
315
|
+
> The truncation is not silent, though — `cap_tools` logs it at `WARN` through
|
|
316
|
+
> `RobotLab.config.logger` every time it fires:
|
|
317
|
+
>
|
|
318
|
+
> ```
|
|
319
|
+
> [power_user] tool list (150) exceeds max_tools (128); sending 128, dropping 22
|
|
320
|
+
> ```
|
|
321
|
+
|
|
322
|
+
## Skill Scripts and Sandboxing
|
|
323
|
+
|
|
324
|
+
A skill bundle (a directory with a `SKILL.md` plus `scripts/`, discovered via `AgentSkill`) can expose its scripts as tools (`ScriptTool`). Because those scripts run as real OS processes, each `SKILL.md` can declare the capabilities its scripts need directly in front matter, alongside `name`/`description`:
|
|
325
|
+
|
|
326
|
+
```markdown title="skills/deploy-checker/SKILL.md"
|
|
327
|
+
---
|
|
328
|
+
name: deploy-checker
|
|
329
|
+
description: Verifies a deployment's health before promoting it.
|
|
330
|
+
fs_read: ["./data", "/etc/hosts"]
|
|
331
|
+
fs_write: ["./out"]
|
|
332
|
+
network: true
|
|
333
|
+
timeout: 30
|
|
334
|
+
trust: external # or "core" for trusted, always-unconfined skills
|
|
335
|
+
---
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Sandboxing itself is **opt-in and off by default** — see the [`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section). When disabled, scripts run exactly as they always have, unconfined. When enabled:
|
|
339
|
+
|
|
340
|
+
- The global `sandbox:` config is a **ceiling** (`fs_read`, `fs_write`, `network`, `timeout`); each skill's front matter is its **declared** request. The script actually runs under the **intersection** of the two — a path outside the ceiling's roots is dropped even if the skill declares it, `network` requires both sides to allow it, and `timeout` is the smaller of the two.
|
|
341
|
+
- On macOS, confinement is enforced with a generated `sandbox-exec` (Seatbelt) profile: deny-by-default, with narrow allowances for the interpreter to boot, the granted read/write paths, and (optionally) the network. Notably, `$HOME` is never implicitly readable — SSH keys and cloud credentials stay out of reach unless a path under `$HOME` is explicitly granted.
|
|
342
|
+
- Off macOS, or for any skill declaring `trust: core`, sandboxing is a passthrough — confinement is currently macOS-only and is always skipped for trusted "core" skills regardless of platform.
|
|
343
|
+
- A script that runs past its `timeout` is killed (its whole process group) and reported back to the LLM as a timed-out error rather than hanging the turn.
|
|
344
|
+
|
|
166
345
|
## Parameter Types
|
|
167
346
|
|
|
168
347
|
Define parameters on `RubyLLM::Tool` subclasses using `param`:
|
|
@@ -197,10 +376,22 @@ param :active, type: :boolean, desc: "Whether the user is active"
|
|
|
197
376
|
param :tags, type: :array, desc: "List of tags"
|
|
198
377
|
```
|
|
199
378
|
|
|
200
|
-
###
|
|
379
|
+
### Enumerated Values
|
|
380
|
+
|
|
381
|
+
`param` has **no `enum:` option**. Its full signature is
|
|
382
|
+
`param(name, type: "string", desc: nil, description: nil, required: true)` —
|
|
383
|
+
anything else raises `ArgumentError: unknown keyword: :enum`. Express the
|
|
384
|
+
allowed values in the description instead, and validate in `execute`:
|
|
201
385
|
|
|
202
386
|
```ruby
|
|
203
|
-
|
|
387
|
+
STATUSES = %w[pending active completed].freeze
|
|
388
|
+
|
|
389
|
+
param :status, type: :string, desc: "Order status — one of: pending, active, completed"
|
|
390
|
+
|
|
391
|
+
def execute(status:)
|
|
392
|
+
return { error: "status must be one of #{STATUSES.join(', ')}" } unless STATUSES.include?(status)
|
|
393
|
+
# ...
|
|
394
|
+
end
|
|
204
395
|
```
|
|
205
396
|
|
|
206
397
|
### Required vs Optional
|
|
@@ -347,7 +538,39 @@ result = tool.call({ "id" => "missing" })
|
|
|
347
538
|
# => "Error (fetch_resource): connection refused"
|
|
348
539
|
```
|
|
349
540
|
|
|
350
|
-
This applies to all `RobotLab::Tool` variants — subclasses, `Tool.create` factory tools, and MCP tools.
|
|
541
|
+
This applies to all `RobotLab::Tool` variants — subclasses, `Tool.create` factory tools, and MCP tools.
|
|
542
|
+
|
|
543
|
+
### RobotLab::ToolError and Retryability
|
|
544
|
+
|
|
545
|
+
`RobotLab::ToolError` is handled on a separate path from ordinary
|
|
546
|
+
`StandardError`s. Raising one with `retryable: true` appends `" (retryable)"`
|
|
547
|
+
to the text the LLM sees, hinting that another attempt may succeed:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
class FetchResource < RobotLab::Tool
|
|
551
|
+
description "Fetch a resource from an external API"
|
|
552
|
+
param :id, type: :string, desc: "Resource ID"
|
|
553
|
+
|
|
554
|
+
def execute(id:)
|
|
555
|
+
raise RobotLab::ToolError.new("upstream 503", retryable: true)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
FetchResource.new.call({ "id" => "1" })
|
|
560
|
+
# => "Error (fetch_resource): upstream 503 (retryable)"
|
|
561
|
+
|
|
562
|
+
# Without retryable:, no suffix is appended:
|
|
563
|
+
# raise RobotLab::ToolError, "bad input"
|
|
564
|
+
# => "Error (fetch_resource): bad input"
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
> [!WARNING]
|
|
568
|
+
> Only the ordinary `StandardError` path writes to the log
|
|
569
|
+
> (`RobotLab.config.logger.warn("Tool 'name' error: RuntimeError: …")`).
|
|
570
|
+
> A `RobotLab::ToolError` is turned into text for the LLM and logged
|
|
571
|
+
> **nowhere** — if you rely on `ToolError` for expected failures, add your own
|
|
572
|
+
> logging inside `execute`, or those failures leave no trace outside the
|
|
573
|
+
> transcript.
|
|
351
574
|
|
|
352
575
|
### Critical Tools (Opt-Out)
|
|
353
576
|
|
|
@@ -368,6 +591,24 @@ end
|
|
|
368
591
|
|
|
369
592
|
`raise_on_error` is per-class and defaults to `false`. Setting it on one class does not affect others.
|
|
370
593
|
|
|
594
|
+
> [!WARNING]
|
|
595
|
+
> `raise_on_error` does **not** walk the inheritance chain. A subclass of a
|
|
596
|
+
> class that set it silently reverts to `false`:
|
|
597
|
+
>
|
|
598
|
+
> ```ruby
|
|
599
|
+
> class CriticalBase < RobotLab::Tool
|
|
600
|
+
> self.raise_on_error = true
|
|
601
|
+
> end
|
|
602
|
+
>
|
|
603
|
+
> class ChargeCard < CriticalBase; end
|
|
604
|
+
>
|
|
605
|
+
> CriticalBase.raise_on_error? # => true
|
|
606
|
+
> ChargeCard.raise_on_error? # => false <- NOT inherited
|
|
607
|
+
> ```
|
|
608
|
+
>
|
|
609
|
+
> Set `self.raise_on_error = true` on every class that needs it. (This differs
|
|
610
|
+
> from `ractor_safe`, below, which *does* consult the superclass.)
|
|
611
|
+
|
|
371
612
|
### Manual Error Handling
|
|
372
613
|
|
|
373
614
|
You can still handle specific errors inside `execute` for domain-specific responses:
|
|
@@ -389,20 +630,68 @@ class FetchResource < RobotLab::Tool
|
|
|
389
630
|
end
|
|
390
631
|
```
|
|
391
632
|
|
|
633
|
+
## Ractor-Safe Tools
|
|
634
|
+
|
|
635
|
+
A tool class can declare itself safe to run inside a Ractor. When it is, and the
|
|
636
|
+
[`robot_lab-ractor`](https://github.com/MadBomber/robot_lab-ractor) extension
|
|
637
|
+
gem is loaded, `call` dispatches the work to `RobotLab.ractor_pool` instead of
|
|
638
|
+
running it inline on the calling thread — giving real CPU parallelism for
|
|
639
|
+
compute-bound tools.
|
|
640
|
+
|
|
641
|
+
```ruby
|
|
642
|
+
class Fibonacci < RobotLab::Tool
|
|
643
|
+
ractor_safe true
|
|
644
|
+
|
|
645
|
+
description "Compute the nth Fibonacci number"
|
|
646
|
+
param :n, type: :integer, desc: "Which Fibonacci number to compute"
|
|
647
|
+
|
|
648
|
+
def execute(n:)
|
|
649
|
+
a, b = 0, 1
|
|
650
|
+
n.times { a, b = b, a + b }
|
|
651
|
+
a
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
Fibonacci.ractor_safe? # => true
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Dispatch to the pool happens only when **all three** hold:
|
|
659
|
+
|
|
660
|
+
1. `ractor_safe?` is true
|
|
661
|
+
2. the class has a resolvable name — anonymous classes (including every tool
|
|
662
|
+
built by `Tool.create`) fall back to the inline path
|
|
663
|
+
3. the `robot_lab-ractor` extension is loaded
|
|
664
|
+
|
|
665
|
+
Otherwise `execute` runs inline, exactly as an ordinary tool would. Pool size
|
|
666
|
+
comes from the `ractor_pool_size` `RunConfig` field (`:auto` when unset).
|
|
667
|
+
|
|
668
|
+
> [!WARNING]
|
|
669
|
+
> A Ractor-safe tool must be genuinely stateless: it is instantiated **fresh
|
|
670
|
+
> inside the Ractor worker** for every call, so `robot`, captured closures, and
|
|
671
|
+
> mutable class-level state are not available to it, and the result comes back
|
|
672
|
+
> frozen. Do not set `ractor_safe true` on a tool that touches its robot.
|
|
673
|
+
|
|
674
|
+
Unlike `raise_on_error`, `ractor_safe` **does** walk the inheritance chain — a
|
|
675
|
+
subclass of a Ractor-safe tool is Ractor-safe unless it says otherwise.
|
|
676
|
+
|
|
392
677
|
## Tool Callbacks
|
|
393
678
|
|
|
394
|
-
Robots support `on_tool_call` and `on_tool_result` callbacks for monitoring tool usage:
|
|
679
|
+
Robots support `on_tool_call` and `on_tool_result` callbacks for monitoring tool usage. Each receives exactly **one** argument — `on_tool_result` gets the result only, not the originating call:
|
|
395
680
|
|
|
396
681
|
```ruby
|
|
397
682
|
robot = RobotLab.build(
|
|
398
683
|
name: "assistant",
|
|
399
684
|
system_prompt: "...",
|
|
400
685
|
local_tools: [GetWeather],
|
|
401
|
-
on_tool_call: ->(
|
|
686
|
+
on_tool_call: ->(tool_call) { puts "Calling: #{tool_call.name}" },
|
|
402
687
|
on_tool_result: ->(result) { puts "Result: #{result}" }
|
|
403
688
|
)
|
|
689
|
+
|
|
690
|
+
robot.run("What's the weather in Tokyo?", tools: :inherit)
|
|
404
691
|
```
|
|
405
692
|
|
|
693
|
+
See [Streaming](streaming.md#tool-callbacks) for details, including the RubyLLM deprecation warning these emit.
|
|
694
|
+
|
|
406
695
|
## RobotLab::Tool.create with Schema
|
|
407
696
|
|
|
408
697
|
For dynamic tools via `Tool.create`, pass parameters as a JSON Schema hash:
|
data/docs/index.md
CHANGED
|
@@ -50,7 +50,7 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
|
|
|
50
50
|
|
|
51
51
|
Mix reusable prompt behaviors into any robot. Skills expand depth-first with automatic cycle detection and config cascading.
|
|
52
52
|
|
|
53
|
-
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#skills)
|
|
53
|
+
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#composable-skills)
|
|
54
54
|
|
|
55
55
|
- :material-tools:{ .lg .middle } **Extensible Tools**
|
|
56
56
|
|
|
@@ -142,16 +142,35 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
|
|
|
142
142
|
|
|
143
143
|
</div>
|
|
144
144
|
|
|
145
|
+
> [!NOTE]
|
|
146
|
+
> A few of the features above need a gem the core `robot_lab` gem does not install.
|
|
147
|
+
> **Knowledge & Retrieval** (`memory.store_document` / `memory.search_documents`)
|
|
148
|
+
> and **Runtime Skill Matching** (`AgentSkill` catalogs) both require
|
|
149
|
+
> `robot_lab-document_store`; the Redis backing for **Reactive Memory** requires
|
|
150
|
+
> `redis`; Ractor parallelism requires `robot_lab-ractor`; and
|
|
151
|
+
> `robot.compress_history` requires `classifier`. See
|
|
152
|
+
> [Optional Dependencies](getting-started/installation.md#optional-dependencies).
|
|
153
|
+
|
|
145
154
|
## Extension Gems
|
|
146
155
|
|
|
147
156
|
These optional gems extend RobotLab with additional capabilities:
|
|
148
157
|
|
|
149
158
|
| Gem | What it adds |
|
|
150
159
|
|-----|-------------|
|
|
151
|
-
| [robot_lab-
|
|
152
|
-
| [robot_lab-
|
|
153
|
-
| [robot_lab-
|
|
160
|
+
| [robot_lab-a2a](https://github.com/MadBomber/robot_lab-a2a) | Agent2Agent (A2A) protocol adapter over HTTP + SSE |
|
|
161
|
+
| [robot_lab-audit](https://github.com/MadBomber/robot_lab-audit) | SQLite-backed execution audit log, wired through the Hook system |
|
|
162
|
+
| [robot_lab-discovery](https://github.com/MadBomber/robot_lab-discovery) | Zero-configuration mDNS/DNS-SD robot discovery on the local network |
|
|
154
163
|
| [robot_lab-document_store](https://github.com/MadBomber/robot_lab-document_store) | Embedding-based semantic document search powered by fastembed |
|
|
164
|
+
| [robot_lab-durable](https://github.com/MadBomber/robot_lab-durable) | HTM-backed long-term memory — robots accumulate and recall learned facts across sessions |
|
|
165
|
+
| [robot_lab-ractor](https://github.com/MadBomber/robot_lab-ractor) | CPU parallelism for `ractor_safe` tools and robot networks via Ruby Ractors |
|
|
166
|
+
| [robot_lab-rails](https://github.com/MadBomber/robot_lab-rails) | Rails Engine, generators, `RobotLab::Job` base class, Turbo Stream broadcasting |
|
|
167
|
+
| [robot_lab-to](https://github.com/MadBomber/robot_lab-to) | Autonomous overnight agent loop — iterate a robot toward an objective, committing each step |
|
|
168
|
+
| [robot_lab-web](https://github.com/MadBomber/robot_lab-web) | Rails-free Sinatra + HTMX browser console that streams a robot's run over SSE |
|
|
169
|
+
|
|
170
|
+
> [!NOTE]
|
|
171
|
+
> The Rails Engine, generators, `RobotLab::Job`, and Turbo broadcasting are
|
|
172
|
+
> **only** in `robot_lab-rails`. The core gem ships no Railtie and no generators.
|
|
173
|
+
> The former `robot_lab-acp` gem is retired and superseded by `robot_lab-a2a`.
|
|
155
174
|
|
|
156
175
|
## Quick Example
|
|
157
176
|
|
|
@@ -162,7 +181,10 @@ require "robot_lab"
|
|
|
162
181
|
# Set API keys via env vars:
|
|
163
182
|
# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
164
183
|
#
|
|
165
|
-
# Or place a config file at ~/.config/robot_lab/
|
|
184
|
+
# Or place a config file at ~/.config/robot_lab/robot_lab.yml
|
|
185
|
+
# (the filename repeats the app name; config.yml is never read). Keys go at the
|
|
186
|
+
# top level, or under a section named for the current environment
|
|
187
|
+
# (development:/test:/production:). Only a `defaults:` wrapper is ignored.
|
|
166
188
|
# Access config values: RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
167
189
|
|
|
168
190
|
# Create a robot with keyword arguments
|
|
@@ -188,6 +210,16 @@ puts result.last_text_content
|
|
|
188
210
|
robot.with_instructions("Be extra concise.").with_temperature(0.3).run("Explain Ruby in one sentence.")
|
|
189
211
|
```
|
|
190
212
|
|
|
213
|
+
> [!IMPORTANT]
|
|
214
|
+
> `Robot#run` defaults to `tools: :none` and `mcp: :none`, so a plain
|
|
215
|
+
> `robot.run("...")` sends the model **no tools** even when you attached them
|
|
216
|
+
> with `local_tools:` or `mcp:` at build time. Ask for them on the run:
|
|
217
|
+
>
|
|
218
|
+
> ```ruby
|
|
219
|
+
> robot.run("...", tools: :inherit) # attached local tools
|
|
220
|
+
> robot.run("...", mcp: :inherit, tools: :inherit) # MCP servers and their tools
|
|
221
|
+
> ```
|
|
222
|
+
|
|
191
223
|
## Supported LLM Providers
|
|
192
224
|
|
|
193
225
|
RobotLab supports multiple LLM providers through the [ruby_llm](https://github.com/crmne/ruby_llm) library:
|
data/examples/01_simple_robot.rb
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
# Demonstrates creating and running a basic robot with a template.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
#
|
|
9
|
+
# ruby examples/01_simple_robot.rb
|
|
10
10
|
|
|
11
11
|
require_relative "common"
|
|
12
12
|
|
|
13
13
|
# Create a simple robot using a template
|
|
14
14
|
robot = RobotLab.build(
|
|
15
|
-
|
|
15
|
+
**llm_opts,
|
|
16
16
|
name: "helper",
|
|
17
17
|
template: :helper
|
|
18
18
|
)
|
data/examples/02_tools.rb
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# Demonstrates creating a robot with custom tools using RubyLLM::Tool.
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
#
|
|
9
|
+
# ruby examples/02_tools.rb
|
|
10
10
|
|
|
11
11
|
require_relative "common"
|
|
12
12
|
|
|
@@ -78,7 +78,7 @@ end
|
|
|
78
78
|
|
|
79
79
|
# Create robot with tools
|
|
80
80
|
robot = RobotLab.build(
|
|
81
|
-
|
|
81
|
+
**llm_opts,
|
|
82
82
|
name: "assistant",
|
|
83
83
|
template: :assistant,
|
|
84
84
|
local_tools: [Calculator, FortuneCookie]
|
|
@@ -86,11 +86,21 @@ robot = RobotLab.build(
|
|
|
86
86
|
|
|
87
87
|
banner "Robot with Tools"
|
|
88
88
|
|
|
89
|
-
# Run the robot
|
|
90
|
-
|
|
89
|
+
# Run the robot.
|
|
90
|
+
#
|
|
91
|
+
# tools: :inherit is REQUIRED to actually offer the tools to the model.
|
|
92
|
+
# Robot#run defaults to tools: :none, which means "send zero tools this turn"
|
|
93
|
+
# — the robot still holds them in local_tools, but the provider never sees
|
|
94
|
+
# them and the LLM answers from memory instead of calling Calculator.
|
|
95
|
+
# :inherit means "no per-turn filter, use every attached tool".
|
|
96
|
+
result = robot.run(
|
|
97
|
+
"What is 15 multiplied by 7? Also, give me a fortune about my career.",
|
|
98
|
+
tools: :inherit
|
|
99
|
+
)
|
|
91
100
|
|
|
92
101
|
# Display results
|
|
93
102
|
puts "Robot: #{robot.name}"
|
|
103
|
+
puts "Tools offered: #{robot.local_tools.map(&:name).join(', ')}"
|
|
94
104
|
puts "\nOutput:"
|
|
95
105
|
result.output.each do |message|
|
|
96
106
|
puts " #{message.content}" if message.respond_to?(:content)
|
data/examples/03_network.rb
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
# using SimpleFlow's optional step activation.
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
|
-
#
|
|
10
|
+
# ruby examples/03_network.rb
|
|
11
11
|
|
|
12
12
|
require_relative "common"
|
|
13
13
|
|
|
14
|
+
SPECIALIST_TASKS = %i[billing technical general].freeze
|
|
15
|
+
|
|
14
16
|
# Classifier robot that activates the appropriate specialist
|
|
15
17
|
class ClassifierRobot < RobotLab::Robot
|
|
16
18
|
def call(result)
|
|
@@ -36,32 +38,37 @@ class ClassifierRobot < RobotLab::Robot
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
# Shared RunConfig —
|
|
40
|
-
|
|
41
|
+
# Shared RunConfig — operational defaults every robot in this network inherits.
|
|
42
|
+
#
|
|
43
|
+
# RunConfig has no `provider` field (see RunConfig::FIELDS), and an Ollama
|
|
44
|
+
# model is absent from RubyLLM's registry, so provider and model still travel
|
|
45
|
+
# together on each robot via **llm_opts. The RunConfig carries the settings
|
|
46
|
+
# that genuinely are shared.
|
|
47
|
+
shared_config = RobotLab::RunConfig.new(temperature: 0.3, max_tool_rounds: 5)
|
|
41
48
|
|
|
42
|
-
# Create specialized robots (no model: needed — inherited from RunConfig)
|
|
43
49
|
classifier = ClassifierRobot.new(
|
|
50
|
+
**llm_opts,
|
|
44
51
|
name: "classifier",
|
|
45
52
|
template: :classifier,
|
|
46
53
|
config: shared_config
|
|
47
54
|
)
|
|
48
55
|
|
|
49
56
|
billing_robot = RobotLab.build(
|
|
50
|
-
|
|
57
|
+
**llm_opts,
|
|
51
58
|
name: "billing",
|
|
52
59
|
template: :billing,
|
|
53
60
|
config: shared_config
|
|
54
61
|
)
|
|
55
62
|
|
|
56
63
|
technical_robot = RobotLab.build(
|
|
57
|
-
|
|
64
|
+
**llm_opts,
|
|
58
65
|
name: "technical",
|
|
59
66
|
template: :technical,
|
|
60
67
|
config: shared_config
|
|
61
68
|
)
|
|
62
69
|
|
|
63
70
|
general_robot = RobotLab.build(
|
|
64
|
-
|
|
71
|
+
**llm_opts,
|
|
65
72
|
name: "general",
|
|
66
73
|
template: :general,
|
|
67
74
|
config: shared_config
|
|
@@ -76,8 +83,13 @@ network = RobotLab.create_network(name: "support_network", config: shared_config
|
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
banner "Multi-Robot Network"
|
|
79
|
-
puts "
|
|
80
|
-
puts
|
|
86
|
+
puts "Routing plan:"
|
|
87
|
+
puts " Entry task: classifier"
|
|
88
|
+
puts " Optional specialists: #{SPECIALIST_TASKS.join(', ')}"
|
|
89
|
+
puts " Runtime behavior: classifier activates exactly one specialist"
|
|
90
|
+
puts
|
|
91
|
+
puts "Underlying SimpleFlow structure:"
|
|
92
|
+
puts " Optional tasks: #{network.to_h[:optional_tasks].join(', ')}"
|
|
81
93
|
hr
|
|
82
94
|
|
|
83
95
|
# Run the network with a billing question
|
|
@@ -85,6 +97,8 @@ result = network.run(message: "I was charged twice for my subscription last mont
|
|
|
85
97
|
|
|
86
98
|
# Display results
|
|
87
99
|
puts "Network: #{network.name}"
|
|
100
|
+
puts "Executed tasks: #{result.context.keys.grep_v(:run_params).join(', ')}"
|
|
101
|
+
puts "Skipped specialists: #{(SPECIALIST_TASKS - result.context.keys).join(', ')}"
|
|
88
102
|
puts "\nConversation flow:"
|
|
89
103
|
|
|
90
104
|
# Show classifier result
|
data/examples/04_mcp.rb
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
# Prerequisites:
|
|
10
10
|
# 1. Install the GitHub MCP server: brew install github-mcp-server
|
|
11
11
|
# 2. Set environment variables:
|
|
12
|
-
# - ANTHROPIC_API_KEY: Your Anthropic API key
|
|
13
12
|
# - GITHUB_PERSONAL_ACCESS_TOKEN: Your GitHub personal access token
|
|
14
13
|
#
|
|
15
14
|
# Usage:
|
|
16
|
-
#
|
|
15
|
+
# GITHUB_PERSONAL_ACCESS_TOKEN=your_token ruby examples/04_mcp.rb
|
|
17
16
|
#
|
|
18
17
|
# The GitHub MCP server provides tools for:
|
|
19
18
|
# - Searching repositories, code, issues, and users
|
|
@@ -136,7 +135,7 @@ begin
|
|
|
136
135
|
# connect_mcp! forces eager connection; without it MCP clients initialize
|
|
137
136
|
# lazily on the first run() call, leaving mcp_clients empty until then.
|
|
138
137
|
robot = RobotLab.build(
|
|
139
|
-
|
|
138
|
+
**llm_opts,
|
|
140
139
|
name: "github_assistant",
|
|
141
140
|
template: :github_assistant,
|
|
142
141
|
mcp_servers: [github_server]
|
|
@@ -164,7 +163,15 @@ begin
|
|
|
164
163
|
puts "Query: 'What are the top 3 most starred Ruby web frameworks on GitHub?'"
|
|
165
164
|
hr
|
|
166
165
|
|
|
167
|
-
|
|
166
|
+
# mcp: :inherit and tools: :inherit are both required. Both keywords default
|
|
167
|
+
# to :none on run(), which resolves to "no MCP servers this turn" and "send
|
|
168
|
+
# zero tools this turn" — the discovered MCP tools would never reach the
|
|
169
|
+
# provider and the model would answer from its training data.
|
|
170
|
+
result = robot.run(
|
|
171
|
+
"What are the top 3 most starred Ruby web frameworks on GitHub? Just list their names and star counts.",
|
|
172
|
+
mcp: :inherit,
|
|
173
|
+
tools: :inherit
|
|
174
|
+
)
|
|
168
175
|
|
|
169
176
|
puts
|
|
170
177
|
puts "Robot Response:"
|