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/concepts.md
CHANGED
|
@@ -8,7 +8,7 @@ A **Robot** is an LLM-powered agent that inherits from `RubyLLM::Agent`. Each ro
|
|
|
8
8
|
|
|
9
9
|
Each robot has:
|
|
10
10
|
|
|
11
|
-
- **Name**:
|
|
11
|
+
- **Name**: An identifier. Nothing is auto-generated — if you omit `name:` the robot is literally named `"robot"`. The value is load-bearing: a robot still named `"robot"` is treated as "unnamed", which is what lets a template's `robot_name:` front matter key take effect. Give every robot an explicit name.
|
|
12
12
|
- **Template**: A `.md` file with YAML front matter managed by prompt_manager, referenced by symbol
|
|
13
13
|
- **System Prompt**: Inline instructions (can be used alone or combined with a template)
|
|
14
14
|
- **Model**: The LLM model to use (defaults to `RobotLab.config.ruby_llm.model`)
|
|
@@ -28,6 +28,9 @@ robot = RobotLab.build(
|
|
|
28
28
|
model: "claude-sonnet-4"
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# Attached tools are only sent when the run asks for them
|
|
32
|
+
robot.run("Where is order 4471?", tools: :inherit)
|
|
33
|
+
|
|
31
34
|
# Robot with inline system prompt
|
|
32
35
|
robot = RobotLab.build(
|
|
33
36
|
name: "helper",
|
|
@@ -56,13 +59,14 @@ puts result.last_text_content # => "Your name is Alice."
|
|
|
56
59
|
|
|
57
60
|
## Configuration
|
|
58
61
|
|
|
59
|
-
RobotLab uses `MywayConfig` for configuration.
|
|
62
|
+
RobotLab uses `MywayConfig` for configuration. Values are loaded automatically from multiple sources in priority order (lowest to highest):
|
|
60
63
|
|
|
61
64
|
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
62
65
|
2. Environment-specific overrides (development, test, production)
|
|
63
|
-
3. XDG user config (`~/.config/robot_lab/
|
|
66
|
+
3. XDG user config (`~/.config/robot_lab/robot_lab.yml`)
|
|
64
67
|
4. Project config (`./config/robot_lab.yml`)
|
|
65
68
|
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
69
|
+
6. Constructor parameters
|
|
66
70
|
|
|
67
71
|
```ruby
|
|
68
72
|
# Access configuration values
|
|
@@ -73,10 +77,25 @@ RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
|
73
77
|
# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
74
78
|
# ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
|
|
75
79
|
|
|
80
|
+
# A configure block also exists, for runtime-only attributes such as the logger
|
|
81
|
+
RobotLab.configure do |c|
|
|
82
|
+
c.logger = Logger.new(File::NULL)
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Reload configuration
|
|
77
86
|
RobotLab.reload_config!
|
|
78
87
|
```
|
|
79
88
|
|
|
89
|
+
> [!IMPORTANT]
|
|
90
|
+
> Two easy mistakes. First, the user config file is
|
|
91
|
+
> `~/.config/robot_lab/**robot_lab**.yml` — the filename repeats the app name, and
|
|
92
|
+
> `config.yml` is never read. Second, the `defaults:` wrapper used inside the gem's
|
|
93
|
+
> bundled `defaults.yml` is silently ignored in your own files; write keys flat, or
|
|
94
|
+
> under a section named for the current environment. The user file honours a
|
|
95
|
+
> `development:` / `test:` / `production:` section; `./config/robot_lab.yml` must be
|
|
96
|
+
> flat outside Rails but **must** be environment-sectioned under Rails. See
|
|
97
|
+
> [Configuration](getting-started/configuration.md) for the full matrix.
|
|
98
|
+
|
|
80
99
|
## Network
|
|
81
100
|
|
|
82
101
|
A **Network** is a collection of robots orchestrated using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines. Networks provide:
|
|
@@ -106,37 +125,75 @@ result = network.run(message: "I was charged twice for my subscription.")
|
|
|
106
125
|
A **Task** wraps a robot for use in a network pipeline with per-task configuration:
|
|
107
126
|
|
|
108
127
|
- **Context**: Task-specific context deep-merged with network run params
|
|
109
|
-
- **MCP**: MCP servers available to this task (`:none`, `:inherit`, or array)
|
|
110
|
-
- **Tools**: Tools available to this task (`:none`, `:inherit`, or array)
|
|
128
|
+
- **MCP**: MCP servers available to this task (`:none`, `:inherit`, or a name array)
|
|
129
|
+
- **Tools**: Tools available to this task (`:none`, `:inherit`, or a name array)
|
|
111
130
|
- **Memory**: Task-specific memory
|
|
112
131
|
- **Dependencies**: `:none`, `[:task1, :task2]`, or `:optional`
|
|
132
|
+
- **Config**: a `RunConfig` — but see the caveat below
|
|
133
|
+
- **Poller group**: `poller_group:` (defaults to `:default`)
|
|
113
134
|
|
|
114
135
|
```ruby
|
|
136
|
+
# The robot must already have the tools ATTACHED...
|
|
137
|
+
billing_robot = RobotLab.build(
|
|
138
|
+
name: "billing",
|
|
139
|
+
system_prompt: "You handle billing.",
|
|
140
|
+
local_tools: [RefundTool, InvoiceTool, AuditTool] # attached as CLASSES
|
|
141
|
+
)
|
|
142
|
+
|
|
115
143
|
task :billing, billing_robot,
|
|
116
144
|
context: { department: "billing", escalation_level: 2 },
|
|
117
|
-
tools: [RefundTool, InvoiceTool],
|
|
145
|
+
tools: [RefundTool, InvoiceTool], # ...and this SELECTS from them
|
|
118
146
|
depends_on: :optional
|
|
119
147
|
```
|
|
120
148
|
|
|
149
|
+
> [!WARNING]
|
|
150
|
+
> An explicit `tools:` array is a **name allowlist** over tools the robot already
|
|
151
|
+
> has attached — it is not a way to attach new tools, and it is not a
|
|
152
|
+
> local-vs-MCP switch. Attach tools with `local_tools:` when building the robot,
|
|
153
|
+
> then filter here.
|
|
154
|
+
>
|
|
155
|
+
> **The allowlist entries must match the form the tool was attached in.** Matching
|
|
156
|
+
> is a string comparison against each attached tool's `name`, and `Class#name`
|
|
157
|
+
> differs from `RubyLLM::Tool#name`:
|
|
158
|
+
>
|
|
159
|
+
> | Attached as | Matching allowlist entry | Does not match |
|
|
160
|
+
> |---|---|---|
|
|
161
|
+
> | `local_tools: [RefundTool]` (class) | `[RefundTool]` or `%w[RefundTool]` | `%w[refund]` |
|
|
162
|
+
> | `local_tools: [RefundTool.new]` (instance) | `%w[refund]` | `[RefundTool]` |
|
|
163
|
+
>
|
|
164
|
+
> A task-level `config:` is merged into the network config, so like a
|
|
165
|
+
> network-level config it propagates **only `mcp` and `tools`** — not `model`,
|
|
166
|
+
> `temperature`, or callbacks.
|
|
167
|
+
|
|
121
168
|
## SimpleFlow::Result
|
|
122
169
|
|
|
123
170
|
Networks use `SimpleFlow::Result` for data flow between tasks:
|
|
124
171
|
|
|
125
172
|
```ruby
|
|
126
173
|
result.value # Current task's output (RobotResult)
|
|
127
|
-
result.context # Accumulated context
|
|
128
|
-
result.
|
|
129
|
-
result.
|
|
174
|
+
result.context # Accumulated context, keyed by ROBOT name
|
|
175
|
+
result.continue? # Whether the pipeline is still continuing
|
|
176
|
+
result.errors # Accumulated errors
|
|
130
177
|
```
|
|
131
178
|
|
|
179
|
+
> [!NOTE]
|
|
180
|
+
> There is no `halted?` and no `continued?` — both raise `NoMethodError`. The
|
|
181
|
+
> predicate is `continue?`. Note also that `result.context` is keyed by each
|
|
182
|
+
> robot's `name:`, not by its task name.
|
|
183
|
+
|
|
132
184
|
### Result Methods
|
|
133
185
|
|
|
186
|
+
The complete public API is `activate`, `activated_steps`, `context`, `continue`,
|
|
187
|
+
`continue?`, `errors`, `halt`, `value`, `with_context`, and `with_error`.
|
|
188
|
+
|
|
134
189
|
| Method | Purpose |
|
|
135
190
|
|--------|---------|
|
|
136
191
|
| `continue(value)` | Continue to next tasks |
|
|
137
192
|
| `halt(value)` | Stop pipeline execution |
|
|
138
193
|
| `with_context(key, val)` | Add data to context |
|
|
194
|
+
| `with_error(key, message)` | Record an error |
|
|
139
195
|
| `activate(task_name)` | Enable optional task |
|
|
196
|
+
| `activated_steps` | Tasks activated so far |
|
|
140
197
|
|
|
141
198
|
## Tool
|
|
142
199
|
|
|
@@ -170,8 +227,29 @@ robot = RobotLab.build(
|
|
|
170
227
|
system_prompt: "You can do math.",
|
|
171
228
|
local_tools: [Calculator]
|
|
172
229
|
)
|
|
230
|
+
|
|
231
|
+
# tools: :inherit is required -- run() sends no tools by default
|
|
232
|
+
robot.run("What is 17 * 23?", tools: :inherit)
|
|
173
233
|
```
|
|
174
234
|
|
|
235
|
+
> [!WARNING]
|
|
236
|
+
> `Robot#run` defaults to `mcp: :none, tools: :none`, and an explicit `:none`
|
|
237
|
+
> means "send zero tools this turn". Attaching tools with `local_tools:` does
|
|
238
|
+
> **not** by itself make them available — a plain `robot.run("...")` sends the
|
|
239
|
+
> model no tools at all. Pass `tools: :inherit` on the run (and
|
|
240
|
+
> `mcp: :inherit, tools: :inherit` for MCP servers).
|
|
241
|
+
>
|
|
242
|
+
> For a **standalone** robot, do not pass `tools: :inherit` to `RobotLab.build`.
|
|
243
|
+
> Build-time `:inherit` resolves against the parent level, which for a standalone
|
|
244
|
+
> robot is the global `:none` — that produces an allowlist matching nothing and
|
|
245
|
+
> suppresses the tools even when the run asks for them. Leave `tools:` unset.
|
|
246
|
+
>
|
|
247
|
+
> Inside a **network** it means the opposite: build-time `tools: :inherit` is
|
|
248
|
+
> exactly how a robot opts into the allowlist carried by the network's `config:`.
|
|
249
|
+
> With `RunConfig.new(tools: %w[RefundTool])` on the network and `tools: :inherit`
|
|
250
|
+
> on both the robot and its task, the robot sends only `refund`; leaving the
|
|
251
|
+
> robot's `tools:` unset sends everything it has attached instead.
|
|
252
|
+
|
|
175
253
|
### RobotLab::Tool.create Factory
|
|
176
254
|
|
|
177
255
|
```ruby
|
|
@@ -197,16 +275,31 @@ result = robot.run("Hello!")
|
|
|
197
275
|
|
|
198
276
|
result.last_text_content # => "Hi there!" (String or nil)
|
|
199
277
|
result.reply # => alias for last_text_content
|
|
200
|
-
result.output # => [TextMessage
|
|
201
|
-
result.tool_calls # => []
|
|
278
|
+
result.output # => [TextMessage] built from the final response text
|
|
279
|
+
result.tool_calls # => [] (see note below -- effectively always empty)
|
|
202
280
|
result.robot_name # => "assistant"
|
|
203
|
-
result.stop_reason # =>
|
|
281
|
+
result.stop_reason # => nil (always -- see note below)
|
|
204
282
|
result.has_tool_calls? # => false
|
|
205
283
|
result.checksum # => "a1b2c3d4..." (for dedup)
|
|
206
284
|
result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
|
|
207
285
|
result.raw # => raw LLM response object
|
|
208
286
|
```
|
|
209
287
|
|
|
288
|
+
> [!NOTE]
|
|
289
|
+
> `result.tool_calls` and `result.has_tool_calls?` read the **final** assistant
|
|
290
|
+
> message. By the time `run` returns, ruby_llm's tool loop has already completed
|
|
291
|
+
> and that message carries no tool calls — so in practice `tool_calls` is always
|
|
292
|
+
> empty and `has_tool_calls?` is always `false`. To observe tool activity, use
|
|
293
|
+
> the `on_tool_call` / `on_tool_result` callbacks instead. Likewise
|
|
294
|
+
> `result.output` is a single-element array built from the final response text,
|
|
295
|
+
> not a transcript of the whole turn.
|
|
296
|
+
>
|
|
297
|
+
> `result.stop_reason` is likewise **always `nil`**: `RubyLLM::Message` does not
|
|
298
|
+
> define `stop_reason`, and `RobotResult` only populates the field when the
|
|
299
|
+
> response responds to it. It is dropped from `result.export` for the same reason.
|
|
300
|
+
> Because `stopped?` is derived from the absence of tool calls, it is
|
|
301
|
+
> correspondingly always `true`. Do not branch on `"end_turn"` / `"tool_use"`.
|
|
302
|
+
|
|
210
303
|
## Memory
|
|
211
304
|
|
|
212
305
|
**Memory** is a reactive key-value store that provides persistent storage across robot executions. Standalone robots use their own inherent memory; robots in a network share the network's memory.
|
|
@@ -225,10 +318,18 @@ robot.memory.data.category # => "billing" (method-style access)
|
|
|
225
318
|
# Runtime memory injection
|
|
226
319
|
robot.run("Help me", memory: { session_id: "abc123" })
|
|
227
320
|
|
|
228
|
-
# Reset
|
|
321
|
+
# Reset the key-value store (does NOT clear chat history)
|
|
229
322
|
robot.reset_memory
|
|
323
|
+
|
|
324
|
+
# Clear chat history (does NOT touch the key-value store)
|
|
325
|
+
robot.clear_messages(keep_system: true)
|
|
230
326
|
```
|
|
231
327
|
|
|
328
|
+
> [!NOTE]
|
|
329
|
+
> `reset_memory` and `clear_messages` are independent. A robot's conversation
|
|
330
|
+
> history and its key-value memory are two separate stores; clearing one leaves
|
|
331
|
+
> the other intact.
|
|
332
|
+
|
|
232
333
|
### Reserved Memory Keys
|
|
233
334
|
|
|
234
335
|
| Key | Purpose |
|
|
@@ -251,7 +352,7 @@ network.memory.set(:sentiment, { score: 0.8 })
|
|
|
251
352
|
result = network.memory.get(:sentiment, wait: true)
|
|
252
353
|
result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
|
|
253
354
|
|
|
254
|
-
# Multiple keys
|
|
355
|
+
# Multiple keys -- the timeout applies PER MISSING KEY, not to the call as a whole
|
|
255
356
|
results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
256
357
|
|
|
257
358
|
# Subscribe to changes
|
|
@@ -260,6 +361,12 @@ network.memory.subscribe(:status) do |change|
|
|
|
260
361
|
end
|
|
261
362
|
```
|
|
262
363
|
|
|
364
|
+
> [!WARNING]
|
|
365
|
+
> A blocking `get` that times out **raises `RobotLab::AwaitTimeout`** — it does
|
|
366
|
+
> not return `nil`. Wrap it in a `rescue` if a missing key is an acceptable
|
|
367
|
+
> outcome. With several keys, the timeout is applied to each missing key in turn,
|
|
368
|
+
> so `get(:a, :b, :c, wait: 60)` can block for up to 180 seconds.
|
|
369
|
+
|
|
263
370
|
## MCP (Model Context Protocol)
|
|
264
371
|
|
|
265
372
|
**MCP** allows robots to connect to external tool servers:
|
|
@@ -273,8 +380,30 @@ robot = RobotLab.build(
|
|
|
273
380
|
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
274
381
|
]
|
|
275
382
|
)
|
|
383
|
+
|
|
384
|
+
# Connect the servers AND expose their tools for this run
|
|
385
|
+
robot.run("List the Ruby files in ./lib", mcp: :inherit, tools: :inherit)
|
|
276
386
|
```
|
|
277
387
|
|
|
388
|
+
> [!IMPORTANT]
|
|
389
|
+
> `transport:` must be a **nested hash** — `transport: { type: "stdio", command: ..., args: [...] }`.
|
|
390
|
+
> A flat `transport: stdio` with sibling `command:`/`args:` keys raises
|
|
391
|
+
> `NoMethodError: undefined method 'transform_keys' for an instance of String`,
|
|
392
|
+
> and that exception is **swallowed** rather than raised: the robot builds
|
|
393
|
+
> successfully with zero tools. It is not silent, though — a line is logged at
|
|
394
|
+
> `WARN` through `RobotLab.config.logger` (`Robot 'dev' error connecting to MCP
|
|
395
|
+
> server 'fs': undefined method 'transform_keys' for an instance of String`) and
|
|
396
|
+
> the server name lands in `robot.failed_mcp_server_names`. An *invalid* transport
|
|
397
|
+
> type is swallowed the same way; only a direct `MCP::Server.new` raises
|
|
398
|
+
> `ArgumentError`. Valid transport types are `stdio`, `sse`, `ws`,
|
|
399
|
+
> `websocket`, `streamable-http`, and `http` (`streamable_http` with an
|
|
400
|
+
> underscore is invalid).
|
|
401
|
+
>
|
|
402
|
+
> `mcp:` also defaults to `:none` on `run`. `mcp: :inherit` triggers the
|
|
403
|
+
> connection attempt; `tools: :inherit` is additionally required for the MCP
|
|
404
|
+
> tools to reach the model. MCP connection failures are logged and recorded in
|
|
405
|
+
> `robot.failed_mcp_server_names` — they are not raised.
|
|
406
|
+
|
|
278
407
|
MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
|
|
279
408
|
|
|
280
409
|
## Execution Flow
|
|
@@ -452,9 +581,24 @@ robot = RobotLab.build(
|
|
|
452
581
|
|
|
453
582
|
Templates support two categories of front matter keys:
|
|
454
583
|
|
|
455
|
-
**LLM Config:** `model
|
|
456
|
-
|
|
457
|
-
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
|
|
584
|
+
**LLM Config:** only `model` and `temperature` take effect.
|
|
585
|
+
|
|
586
|
+
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills`, `parameters` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
|
|
587
|
+
|
|
588
|
+
> [!WARNING]
|
|
589
|
+
> Front matter is parsed into a `RunConfig` and then applied by dispatching
|
|
590
|
+
> `chat.with_<field>` for each field the chat responds to. The chat object only
|
|
591
|
+
> has `with_model` and `with_temperature`, so **`top_p`, `top_k`, `max_tokens`,
|
|
592
|
+
> `presence_penalty`, `frequency_penalty`, and `stop` are parsed and silently
|
|
593
|
+
> dropped** when declared in front matter. A template declaring all eight yields
|
|
594
|
+
> a chat with empty params.
|
|
595
|
+
>
|
|
596
|
+
> Those six *do* work as constructor keyword arguments or via a `config:`
|
|
597
|
+
> `RunConfig`, because that path goes through `with_params`:
|
|
598
|
+
>
|
|
599
|
+
> ```ruby
|
|
600
|
+
> RobotLab.build(name: "bot", template: :writer, max_tokens: 2000, top_p: 0.3)
|
|
601
|
+
> ```
|
|
458
602
|
|
|
459
603
|
```markdown
|
|
460
604
|
---
|
|
@@ -464,20 +608,31 @@ tools:
|
|
|
464
608
|
- CodeSearchTool
|
|
465
609
|
mcp:
|
|
466
610
|
- name: github
|
|
467
|
-
transport:
|
|
468
|
-
|
|
469
|
-
|
|
611
|
+
transport:
|
|
612
|
+
type: stdio
|
|
613
|
+
command: npx
|
|
614
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
470
615
|
model: claude-sonnet-4
|
|
471
616
|
---
|
|
472
617
|
You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
473
618
|
```
|
|
474
619
|
|
|
620
|
+
> [!IMPORTANT]
|
|
621
|
+
> Note the **nested** `transport:` mapping above. A flat
|
|
622
|
+
> `transport: stdio` with sibling `command:`/`args:` keys raises a `NoMethodError`
|
|
623
|
+
> that is swallowed rather than propagated, leaving you with a robot that has no
|
|
624
|
+
> tools. Look for the `WARN` line on `RobotLab.config.logger` and check
|
|
625
|
+
> `robot.failed_mcp_server_names` — both record the failure.
|
|
626
|
+
|
|
475
627
|
```ruby
|
|
476
628
|
# Template provides everything — minimal constructor
|
|
477
629
|
robot = RobotLab.build(template: :github_assistant)
|
|
630
|
+
|
|
631
|
+
# ...but the tools and MCP servers it declares still have to be requested per run
|
|
632
|
+
robot.run("Open issues in MadBomber/robot_lab?", mcp: :inherit, tools: :inherit)
|
|
478
633
|
```
|
|
479
634
|
|
|
480
|
-
Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values.
|
|
635
|
+
Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values. In the cascade, front matter is the **base**: template front matter → `config:` RunConfig → constructor keyword arguments, with constructor arguments always winning.
|
|
481
636
|
|
|
482
637
|
## Next Steps
|
|
483
638
|
|
data/docs/examples/basic-chat.md
CHANGED
|
@@ -6,11 +6,14 @@ A simple conversational robot example.
|
|
|
6
6
|
|
|
7
7
|
This example demonstrates the minimal setup for a conversational robot that can respond to user messages using `robot.run("message")`.
|
|
8
8
|
|
|
9
|
+
The closest runnable file in this repository is
|
|
10
|
+
[`examples/01_simple_robot.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/01_simple_robot.rb).
|
|
11
|
+
The snippets below wrap that same API in a small REPL.
|
|
12
|
+
|
|
9
13
|
## Complete Example
|
|
10
14
|
|
|
11
15
|
```ruby
|
|
12
16
|
#!/usr/bin/env ruby
|
|
13
|
-
# examples/basic_chat.rb
|
|
14
17
|
|
|
15
18
|
require "bundler/setup"
|
|
16
19
|
require "robot_lab"
|
|
@@ -49,9 +52,15 @@ puts "\nGoodbye!"
|
|
|
49
52
|
|
|
50
53
|
## With Streaming
|
|
51
54
|
|
|
55
|
+
Pass a block to `run` to receive each `RubyLLM::Chunk` as it arrives.
|
|
56
|
+
|
|
57
|
+
> [!WARNING]
|
|
58
|
+
> The yielded object is a `RubyLLM::Chunk`. Read its text with `chunk.content`.
|
|
59
|
+
> There is no `chunk.text` method — a guard like `if chunk.respond_to?(:text)`
|
|
60
|
+
> silently prints nothing.
|
|
61
|
+
|
|
52
62
|
```ruby
|
|
53
63
|
#!/usr/bin/env ruby
|
|
54
|
-
# examples/streaming_chat.rb
|
|
55
64
|
|
|
56
65
|
require "bundler/setup"
|
|
57
66
|
require "robot_lab"
|
|
@@ -73,30 +82,55 @@ loop do
|
|
|
73
82
|
next if input.empty?
|
|
74
83
|
|
|
75
84
|
print "\nAssistant: "
|
|
76
|
-
|
|
77
|
-
print event.text if event.respond_to?(:text)
|
|
78
|
-
end
|
|
85
|
+
assistant.run(input) { |chunk| print chunk.content }
|
|
79
86
|
puts
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
puts "\nGoodbye!"
|
|
83
90
|
```
|
|
84
91
|
|
|
92
|
+
You can also wire streaming once at build time with the `on_content:` callback,
|
|
93
|
+
which fires on every `run`:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
assistant = RobotLab.build(
|
|
97
|
+
name: "assistant",
|
|
98
|
+
system_prompt: "You are a helpful assistant.",
|
|
99
|
+
on_content: ->(chunk) { print chunk.content }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assistant.run("Tell me a one-sentence fact about Ruby.")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When both are supplied, the stored `on_content` callback fires first, then the
|
|
106
|
+
block. See [`examples/05_streaming.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/05_streaming.rb)
|
|
107
|
+
for all four variations (stored callback, per-call block, both, and via `RunConfig`).
|
|
108
|
+
|
|
85
109
|
## With Template
|
|
86
110
|
|
|
111
|
+
Templates are `.md` files with YAML front matter, resolved from the configured
|
|
112
|
+
prompts directory (`ROBOT_LAB_TEMPLATE_PATH`). Parameters declared `null` in the
|
|
113
|
+
front matter are required and are supplied via `context:`.
|
|
114
|
+
|
|
87
115
|
```ruby
|
|
88
116
|
#!/usr/bin/env ruby
|
|
89
|
-
# examples/template_chat.rb
|
|
90
117
|
|
|
91
118
|
require "bundler/setup"
|
|
92
119
|
require "robot_lab"
|
|
93
120
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
121
|
+
# Template file: prompts/support.md
|
|
122
|
+
# ---
|
|
123
|
+
# description: Support assistant
|
|
124
|
+
# parameters:
|
|
125
|
+
# company_name: null
|
|
126
|
+
# tone: friendly
|
|
127
|
+
# ---
|
|
128
|
+
# You are a <%= tone %> support assistant for <%= company_name %>.
|
|
129
|
+
|
|
96
130
|
assistant = RobotLab.build(
|
|
97
131
|
name: "assistant",
|
|
98
|
-
template: :
|
|
99
|
-
context: {
|
|
132
|
+
template: :support,
|
|
133
|
+
context: { company_name: "Acme Corp", tone: "friendly" },
|
|
100
134
|
model: "claude-sonnet-4"
|
|
101
135
|
)
|
|
102
136
|
|
|
@@ -117,11 +151,13 @@ end
|
|
|
117
151
|
puts "\nGoodbye!"
|
|
118
152
|
```
|
|
119
153
|
|
|
154
|
+
A full template-driven network lives in
|
|
155
|
+
[`examples/06_prompt_templates.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/06_prompt_templates.rb).
|
|
156
|
+
|
|
120
157
|
## With Memory
|
|
121
158
|
|
|
122
159
|
```ruby
|
|
123
160
|
#!/usr/bin/env ruby
|
|
124
|
-
# examples/chat_with_memory.rb
|
|
125
161
|
|
|
126
162
|
require "bundler/setup"
|
|
127
163
|
require "robot_lab"
|
|
@@ -153,11 +189,14 @@ end
|
|
|
153
189
|
puts "\nGoodbye!"
|
|
154
190
|
```
|
|
155
191
|
|
|
192
|
+
The full Memory API — subscriptions, `StateProxy`, blocking reads, clone and
|
|
193
|
+
reset — is demonstrated in
|
|
194
|
+
[`examples/10_memory.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/10_memory.rb).
|
|
195
|
+
|
|
156
196
|
## Bare Robot with Chaining
|
|
157
197
|
|
|
158
198
|
```ruby
|
|
159
199
|
#!/usr/bin/env ruby
|
|
160
|
-
# examples/bare_robot.rb
|
|
161
200
|
|
|
162
201
|
require "bundler/setup"
|
|
163
202
|
require "robot_lab"
|
|
@@ -175,27 +214,41 @@ result = robot
|
|
|
175
214
|
puts result.last_text_content
|
|
176
215
|
```
|
|
177
216
|
|
|
217
|
+
> [!NOTE]
|
|
218
|
+
> Only the `with_*` methods that `RubyLLM::Chat` exposes are delegated:
|
|
219
|
+
> `with_context`, `with_headers`, `with_instructions`, `with_model`, `with_params`,
|
|
220
|
+
> `with_schema`, `with_temperature`, `with_thinking`, `with_tool`, `with_tools`
|
|
221
|
+
> (plus RobotLab's own `with_template` and `with_bus`).
|
|
222
|
+
> There is no `with_max_tokens` / `with_top_p` / `with_top_k` — use a constructor
|
|
223
|
+
> kwarg (`max_tokens: 2000`) or `with_params(max_tokens: 2000, top_p: 0.3)`.
|
|
224
|
+
|
|
225
|
+
`examples/09_chaining.rb` walks through chaining and reconfiguration without
|
|
226
|
+
making any LLM calls.
|
|
227
|
+
|
|
178
228
|
## Running
|
|
179
229
|
|
|
180
230
|
```bash
|
|
181
231
|
# Set API key
|
|
182
232
|
export ANTHROPIC_API_KEY="your-key"
|
|
183
233
|
|
|
184
|
-
#
|
|
185
|
-
ruby examples/
|
|
234
|
+
# Simplest runnable robot
|
|
235
|
+
ruby examples/01_simple_robot.rb
|
|
236
|
+
|
|
237
|
+
# Streaming
|
|
238
|
+
ruby examples/05_streaming.rb
|
|
186
239
|
|
|
187
|
-
#
|
|
188
|
-
ruby examples/
|
|
240
|
+
# with_* chaining and reconfiguration (no LLM calls)
|
|
241
|
+
ruby examples/09_chaining.rb
|
|
189
242
|
```
|
|
190
243
|
|
|
191
244
|
## Key Concepts
|
|
192
245
|
|
|
193
246
|
1. **Robot Building**: Use `RobotLab.build(name:, system_prompt:)` or `RobotLab.build(name:, template:)` to create a robot
|
|
194
247
|
2. **Execution**: Call `robot.run("message")` to send a message and get a response
|
|
195
|
-
3. **Response**: Access the text via `result.last_text_content`
|
|
196
|
-
4. **Streaming**: Pass a block to `robot.run("message") { |
|
|
248
|
+
3. **Response**: Access the text via `result.last_text_content` (aliased as `result.reply`)
|
|
249
|
+
4. **Streaming**: Pass a block to `robot.run("message") { |chunk| print chunk.content }`, or set `on_content:` at build time
|
|
197
250
|
5. **Memory**: Access inherent memory via `robot.memory[:key]`
|
|
198
|
-
6. **Chaining**: Configure with `with_*` methods
|
|
251
|
+
6. **Chaining**: Configure with the delegated `with_*` methods, which return `self`
|
|
199
252
|
7. **Conversation History**: The persistent `@chat` maintains history across multiple `run` calls
|
|
200
253
|
|
|
201
254
|
## See Also
|