robot_lab 0.0.7 → 0.0.8
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/CHANGELOG.md +60 -3
- data/README.md +99 -23
- data/Rakefile +39 -1
- data/docs/api/core/robot.md +130 -7
- data/docs/api/core/tool.md +28 -2
- data/docs/concepts.md +7 -3
- data/docs/examples/index.md +1 -1
- data/docs/examples/rails-application.md +1 -1
- data/docs/guides/building-robots.md +227 -11
- data/docs/guides/rails-integration.md +145 -17
- data/docs/guides/using-tools.md +45 -4
- data/docs/index.md +62 -6
- data/examples/05_streaming.rb +113 -94
- data/examples/17_skills.rb +242 -0
- data/examples/18_rails/.envrc +3 -0
- data/examples/18_rails/.gitignore +5 -0
- data/examples/18_rails/Gemfile +10 -0
- data/examples/18_rails/README.md +48 -0
- data/examples/18_rails/Rakefile +4 -0
- data/examples/18_rails/app/controllers/application_controller.rb +4 -0
- data/examples/18_rails/app/controllers/chat_controller.rb +46 -0
- data/examples/18_rails/app/jobs/application_job.rb +4 -0
- data/examples/18_rails/app/jobs/robot_run_job.rb +79 -0
- data/examples/18_rails/app/models/application_record.rb +5 -0
- data/examples/18_rails/app/models/robot_lab_result.rb +36 -0
- data/examples/18_rails/app/models/robot_lab_thread.rb +23 -0
- data/examples/18_rails/app/robots/chat_robot.rb +14 -0
- data/examples/18_rails/app/tools/time_tool.rb +9 -0
- data/examples/18_rails/app/views/chat/_user_message.html.erb +1 -0
- data/examples/18_rails/app/views/chat/index.html.erb +67 -0
- data/examples/18_rails/app/views/layouts/application.html.erb +49 -0
- data/examples/18_rails/bin/dev +7 -0
- data/examples/18_rails/bin/rails +6 -0
- data/examples/18_rails/bin/setup +15 -0
- data/examples/18_rails/config/application.rb +33 -0
- data/examples/18_rails/config/cable.yml +2 -0
- data/examples/18_rails/config/database.yml +5 -0
- data/examples/18_rails/config/environment.rb +4 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +3 -0
- data/examples/18_rails/config/routes.rb +6 -0
- data/examples/18_rails/config.ru +4 -0
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +32 -0
- data/examples/README.md +30 -0
- data/examples/prompts/audit_trail.md +8 -0
- data/examples/prompts/incident_responder.md +18 -0
- data/examples/prompts/parameterized_main_test.md +6 -0
- data/examples/prompts/pii_redactor.md +7 -0
- data/examples/prompts/runbook_protocol.md +12 -0
- data/examples/prompts/skill_a_test.md +4 -0
- data/examples/prompts/skill_b_test.md +4 -0
- data/examples/prompts/skill_config_test.md +5 -0
- data/examples/prompts/skill_cycle_a_test.md +6 -0
- data/examples/prompts/skill_cycle_b_test.md +6 -0
- data/examples/prompts/skill_description_test.md +4 -0
- data/examples/prompts/skill_leaf_test.md +4 -0
- data/examples/prompts/skill_nested_test.md +6 -0
- data/examples/prompts/skill_refs_main_test.md +6 -0
- data/examples/prompts/skill_self_ref_test.md +6 -0
- data/examples/prompts/skill_with_params_test.md +6 -0
- data/examples/prompts/sre_compliance.md +10 -0
- data/examples/prompts/structured_output.md +7 -0
- data/examples/prompts/template_with_skills_test.md +6 -0
- data/lib/generators/robot_lab/install_generator.rb +12 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +36 -22
- data/lib/generators/robot_lab/templates/job.rb.tt +92 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +6 -21
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +5 -3
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +41 -35
- data/lib/robot_lab/{rails → rails_integration}/engine.rb +1 -1
- data/lib/robot_lab/{rails → rails_integration}/railtie.rb +1 -1
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +72 -0
- data/lib/robot_lab/robot/template_rendering.rb +181 -4
- data/lib/robot_lab/robot.rb +72 -32
- data/lib/robot_lab/run_config.rb +1 -1
- data/lib/robot_lab/tool.rb +26 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +6 -4
- metadata +53 -18
- data/examples/14_rusty_circuit/scout_notes.md +0 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37a044eb81a0e5c56aa7c5c00f9b4eff600c56eecda8bb2deb18058919a18267
|
|
4
|
+
data.tar.gz: a48253bbceb5ac99f1babf9c4538045886e038fb0c4a827b96b219b5363bf5c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f84d7c82598d281e8c88a4e67b2b0c13bac1142e8dae9834a1b0e84d1055e16837c62b452398da615d477aec834b153ca74f6313c67b806fe174aacd3a0999b
|
|
7
|
+
data.tar.gz: 549afa0eb2e622ad8caf0d928559bb881b3f1307d2e427074a2f96f943418784e580ef9d5b2a06534f9ec57873682189ec6890cd0ed1790e19b66c159fff572e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
> [!CAUTION]
|
|
4
|
-
> This gem is under active development. APIs and features may change without notice.
|
|
5
|
-
|
|
6
3
|
All notable changes to this project will be documented in this file.
|
|
7
4
|
|
|
8
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
@@ -11,6 +8,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
8
|
|
|
12
9
|
## [Unreleased]
|
|
13
10
|
|
|
11
|
+
## [0.0.8] - 2026-02-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Skills as composable templates** — prepend reusable prompt snippets at build time via `skills:` parameter
|
|
16
|
+
- Skills are regular PromptManager templates (no special subdirectory)
|
|
17
|
+
- Recursive expansion — skills can declare nested skills via front matter `skills:` key
|
|
18
|
+
- Depth-first ordering — nested skills appear before their parent
|
|
19
|
+
- Cycle detection via `Set` of visited IDs; cycles log a warning and skip
|
|
20
|
+
- Config cascade — skill₁ → skill₂ → ... → main template → constructor kwargs
|
|
21
|
+
- Shared context — all skills and the main template render with the same `context:` hash
|
|
22
|
+
- Example: `examples/17_skills.rb` — SRE incident response system with flat and recursive skills
|
|
23
|
+
- 20 new tests covering expansion, ordering, cycles, config cascade, and factory passthrough
|
|
24
|
+
- **Streaming content callback (`on_content:`)** — wire streaming at robot build time
|
|
25
|
+
- Stored callback fires on every `run()` call automatically
|
|
26
|
+
- Pass-through block on `run()` for per-call streaming
|
|
27
|
+
- When both exist, both fire (stored first, then runtime block)
|
|
28
|
+
- `effective_streaming_block` merges stored + runtime into a single Proc
|
|
29
|
+
- Added `:on_content` to `RunConfig::CALLBACK_FIELDS`
|
|
30
|
+
- 12 new tests
|
|
31
|
+
- **Graceful tool error handling** — `RobotLab::Tool#call` wraps `execute` with `rescue StandardError`
|
|
32
|
+
- Errors returned as plain string (`"Error (tool_name): message"`) so the LLM can reason about them
|
|
33
|
+
- Class-level `raise_on_error` opt-out for critical tools
|
|
34
|
+
- MCP tools inherit the same wrapper via `Tool.create` subclasses
|
|
35
|
+
- 10 new tests
|
|
36
|
+
- **`RobotRunJob` generator template** (`job.rb.tt`) — turnkey ActiveJob background job for robot runs
|
|
37
|
+
- Resolves robot class via `constantize.build`
|
|
38
|
+
- Wires `TurboStreamCallbacks` when `turbo-rails` is available (graceful no-op otherwise)
|
|
39
|
+
- Persists results via `result.export`; broadcasts completion/error via Turbo Streams
|
|
40
|
+
- `--skip-job` option on install generator
|
|
41
|
+
- **`TurboStreamCallbacks` module** (`lib/robot_lab/rails_integration/turbo_stream_callbacks.rb`)
|
|
42
|
+
- `available?` — runtime check for `Turbo::StreamsChannel`
|
|
43
|
+
- `build_content_callback(stream_name:, target:)` — broadcasts HTML-escaped content chunks
|
|
44
|
+
- `build_tool_call_callback(stream_name:, target:)` — broadcasts tool call badges
|
|
45
|
+
- 13 tests
|
|
46
|
+
- **Rails demo app** (`examples/18_rails/`) — minimal hand-built Rails 8 app exercising all Rails integration
|
|
47
|
+
- ChatRobot with TimeTool, RobotRunJob, Turbo Stream token streaming, SQLite persistence
|
|
48
|
+
- No asset pipeline — Turbo JS via importmap from CDN
|
|
49
|
+
- `:async` adapters for both ActiveJob and ActionCable (no Redis, no Solid Queue)
|
|
50
|
+
- User messages persisted in history; auto-scrolling via MutationObserver; form clears after submit
|
|
51
|
+
- Rake tasks: `examples:rails_setup`, `examples:rails`
|
|
52
|
+
- **Routing robot example** in Rails integration docs — `ClassifierRobot` subclass with `call(result)` override
|
|
53
|
+
- **Custom tool example** in Rails integration docs — `OrderLookup` tool with ActiveRecord
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Bumped version to 0.0.8
|
|
58
|
+
- **Renamed `RobotLab::Rails` → `RobotLab::RailsIntegration`** — eliminates constant shadowing where bare `Rails` inside `module RobotLab` resolved to the gem's own namespace instead of `::Rails`
|
|
59
|
+
- Moved `lib/robot_lab/rails/` → `lib/robot_lab/rails_integration/`
|
|
60
|
+
- Updated all require paths, loader.ignore, generator templates, tests, and documentation
|
|
61
|
+
- Reverted `::Rails` back to bare `Rails` in `config.rb` (shadow eliminated)
|
|
62
|
+
- Rakefile updated with `STANDALONE_APPS` map for standalone demo apps
|
|
63
|
+
- Documentation updates across README, guides, API reference, and examples for all new features
|
|
64
|
+
- Updated Gemfile.lock dependencies
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- `RobotLab::Rails` namespace shadowing `::Rails` in `config.rb` (`NoMethodError: undefined method 'root' for module RobotLab::Rails`)
|
|
69
|
+
- MkDocs broken anchor link in `docs/examples/index.md` (`#with-conversation-history` → `#with-memory`)
|
|
70
|
+
|
|
14
71
|
## [0.0.7] - 2026-02-17 [unreleased]
|
|
15
72
|
|
|
16
73
|
### Added
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# RobotLab
|
|
2
2
|
|
|
3
|
-
> [!
|
|
4
|
-
>
|
|
3
|
+
> [!INFO]
|
|
4
|
+
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory has a good cross section of demo apps](examples/README.md) that show-off the various capabilities of the RobotLab library.
|
|
5
|
+
|
|
5
6
|
<br>
|
|
6
7
|
<table>
|
|
7
8
|
<tr>
|
|
@@ -10,24 +11,27 @@
|
|
|
10
11
|
<em>"Build robots. Solve problems."</em>
|
|
11
12
|
</td>
|
|
12
13
|
<td width="50%" valign="top">
|
|
13
|
-
<strong>Multi-robot LLM workflow orchestration for Ruby</strong><br><br>
|
|
14
|
-
RobotLab enables you to build sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own system prompt, tools, and capabilities.<br><br>
|
|
15
14
|
<strong>Key Features</strong><br>
|
|
16
15
|
|
|
17
16
|
- <strong>Multi-Robot Architecture</strong> - Build with specialized AI agents<br>
|
|
18
17
|
- <strong>Network Orchestration</strong> - Connect robots with flexible routing<br>
|
|
19
|
-
- <strong>
|
|
18
|
+
- <strong>Prompt Templates</strong> - Self-contained .md files with YAML front matter<br>
|
|
19
|
+
- <strong>Composable Skills</strong> - Mix reusable prompt behaviors into any robot<br>
|
|
20
|
+
- <strong>Extensible Tools</strong> - Custom capabilities with graceful error handling<br>
|
|
21
|
+
- <strong>Human-in-the-Loop</strong> - AskUser tool for interactive prompting<br>
|
|
22
|
+
- <strong>Content Streaming</strong> - Stored callbacks, per-call blocks, or both<br>
|
|
20
23
|
- <strong>MCP Integration</strong> - Connect to external tool servers<br>
|
|
21
24
|
- <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
|
|
22
|
-
- <strong>Conversation History</strong> - Persist and restore threads<br>
|
|
23
25
|
- <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
|
|
24
26
|
- <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
|
|
25
|
-
- <strong>
|
|
26
|
-
- <strong>Rails Integration</strong> - Generators
|
|
27
|
+
- <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
|
|
28
|
+
- <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting
|
|
27
29
|
</td>
|
|
28
30
|
</tr>
|
|
29
31
|
</table>
|
|
30
32
|
|
|
33
|
+
<p>RobotLab enables sophisticated AI applications using multiple specialized robots (LLM agents) that work together to accomplish complex tasks. Each robot has its own instructions, skills, tools, and capabilities. Review the [full documentation website](https://madbomber.github.io/robot_lab) snd explore the [many examples](examples/README.md) available as working demo applications.</p>
|
|
34
|
+
|
|
31
35
|
## Installation
|
|
32
36
|
|
|
33
37
|
```bash
|
|
@@ -172,7 +176,35 @@ You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
|
172
176
|
robot = RobotLab.build(template: :github_assistant)
|
|
173
177
|
```
|
|
174
178
|
|
|
175
|
-
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `parameters`, and LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`). Constructor-provided values always take precedence over front matter.
|
|
179
|
+
Front matter supports: `description`, `robot_name`, `tools`, `mcp`, `skills`, `parameters`, and LLM config keys (`model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`). Constructor-provided values always take precedence over front matter.
|
|
180
|
+
|
|
181
|
+
### Composable Skills
|
|
182
|
+
|
|
183
|
+
Skills let you compose robot behaviors from reusable templates. A skill is just a template whose prompt body is prepended before the main template. Use skills to mix in capabilities like "ask clarifying questions", "respond in JSON", or "follow safety guidelines" without creating a dedicated template for every combination.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# Compose a support robot from reusable skills
|
|
187
|
+
robot = RobotLab.build(
|
|
188
|
+
name: "support",
|
|
189
|
+
template: :support_agent,
|
|
190
|
+
skills: [:clarifier, :sentiment_aware, :json_responder],
|
|
191
|
+
context: { company: "Acme Corp" }
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Skills can also be declared in template front matter:
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
---
|
|
199
|
+
description: Support agent with built-in skills
|
|
200
|
+
skills:
|
|
201
|
+
- clarifier
|
|
202
|
+
- sentiment_aware
|
|
203
|
+
---
|
|
204
|
+
You are a support agent for <%= company %>.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Skills are expanded depth-first and can reference other skills (with automatic cycle detection). Config cascades through skills in order — later values override earlier ones, and constructor kwargs always win.
|
|
176
208
|
|
|
177
209
|
### Combining Templates with System Prompts
|
|
178
210
|
|
|
@@ -252,6 +284,28 @@ result = robot
|
|
|
252
284
|
.run("Write a haiku about Ruby programming")
|
|
253
285
|
```
|
|
254
286
|
|
|
287
|
+
## Graceful Tool Error Handling
|
|
288
|
+
|
|
289
|
+
`RobotLab::Tool` automatically catches exceptions in `execute` and returns a plain-text error to the LLM instead of crashing the run. The LLM can then reason about the error and try an alternative approach.
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
tool = RobotLab::Tool.create(name: "fetch_data") do |args|
|
|
293
|
+
raise IOError, "connection refused"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
result = tool.call({})
|
|
297
|
+
# => "Error (fetch_data): connection refused"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This applies to all tools — subclasses, factory tools, and MCP tools. For critical tools where you want exceptions to propagate, opt out per class:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
class CriticalTool < RobotLab::Tool
|
|
304
|
+
self.raise_on_error = true
|
|
305
|
+
# ...
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
255
309
|
## Creating a Robot with Tools
|
|
256
310
|
|
|
257
311
|
```ruby
|
|
@@ -502,26 +556,48 @@ Key features:
|
|
|
502
556
|
|
|
503
557
|
## Streaming
|
|
504
558
|
|
|
505
|
-
|
|
559
|
+
Stream LLM content in real-time using a stored callback, a per-call block, or both. Each receives a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) — use `chunk.content` for the text delta. Chunks also carry `model_id`, `tool_calls`, `thinking`, and token usage on the final chunk.
|
|
560
|
+
|
|
561
|
+
### Stored Callback (`on_content:`)
|
|
562
|
+
|
|
563
|
+
Wire streaming at build time. The callback fires on every `run()` call automatically:
|
|
506
564
|
|
|
507
565
|
```ruby
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
566
|
+
robot = RobotLab.build(
|
|
567
|
+
name: "assistant",
|
|
568
|
+
system_prompt: "You are helpful.",
|
|
569
|
+
on_content: ->(chunk) { print chunk.content }
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
robot.run("Tell me a story") # streams automatically
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Per-Call Block
|
|
576
|
+
|
|
577
|
+
Pass a block to `run()` for one-off streaming:
|
|
516
578
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
579
|
+
```ruby
|
|
580
|
+
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
581
|
+
|
|
582
|
+
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Both Together
|
|
586
|
+
|
|
587
|
+
When both a stored callback and a runtime block are provided, both fire (stored first):
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
robot = RobotLab.build(
|
|
591
|
+
name: "assistant",
|
|
592
|
+
system_prompt: "You are helpful.",
|
|
593
|
+
on_content: ->(chunk) { log_chunk(chunk.content) }
|
|
522
594
|
)
|
|
595
|
+
|
|
596
|
+
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
523
597
|
```
|
|
524
598
|
|
|
599
|
+
The `on_content:` callback participates in the RunConfig cascade, so it can be set at the network or config level and inherited by robots.
|
|
600
|
+
|
|
525
601
|
## Rails Integration
|
|
526
602
|
|
|
527
603
|
```bash
|
data/Rakefile
CHANGED
|
@@ -52,7 +52,12 @@ namespace :examples do
|
|
|
52
52
|
"16_writers_room" => "writers_room.rb"
|
|
53
53
|
}.freeze
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# Subdirectory demos that are standalone apps (not run via `ruby`)
|
|
56
|
+
STANDALONE_APPS = {
|
|
57
|
+
"18_rails" => { setup: "bin/setup", run: "bin/dev" }
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
desc "Run all examples (excludes standalone apps like 18_rails)"
|
|
56
61
|
task :all do
|
|
57
62
|
# Single-file examples
|
|
58
63
|
Dir.glob("examples/*.rb").sort.each do |example|
|
|
@@ -72,6 +77,15 @@ namespace :examples do
|
|
|
72
77
|
puts '=' * 60
|
|
73
78
|
ruby path
|
|
74
79
|
end
|
|
80
|
+
|
|
81
|
+
# Remind about standalone apps
|
|
82
|
+
STANDALONE_APPS.each do |dir, commands|
|
|
83
|
+
puts "\n#{'=' * 60}"
|
|
84
|
+
puts "Skipped: examples/#{dir} (standalone app)"
|
|
85
|
+
puts " Setup: cd examples/#{dir} && #{commands[:setup]}"
|
|
86
|
+
puts " Run: cd examples/#{dir} && #{commands[:run]}"
|
|
87
|
+
puts '=' * 60
|
|
88
|
+
end
|
|
75
89
|
end
|
|
76
90
|
|
|
77
91
|
desc "Run a specific example by number (e.g., rake examples:run[1])"
|
|
@@ -89,6 +103,16 @@ namespace :examples do
|
|
|
89
103
|
dir = Dir.glob("examples/#{padded}_*/").first
|
|
90
104
|
if dir
|
|
91
105
|
dir_name = File.basename(dir)
|
|
106
|
+
|
|
107
|
+
# Check if it's a standalone app
|
|
108
|
+
if STANDALONE_APPS.key?(dir_name)
|
|
109
|
+
commands = STANDALONE_APPS[dir_name]
|
|
110
|
+
puts "Example #{args[:num]} is a standalone app (#{dir_name})."
|
|
111
|
+
puts " Setup: cd examples/#{dir_name} && #{commands[:setup]}"
|
|
112
|
+
puts " Run: cd examples/#{dir_name} && #{commands[:run]}"
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
92
116
|
entry = SUBDIR_ENTRY_POINTS[dir_name]
|
|
93
117
|
if entry && File.exist?(File.join(dir, entry))
|
|
94
118
|
ruby File.join(dir, entry)
|
|
@@ -99,6 +123,20 @@ namespace :examples do
|
|
|
99
123
|
puts "Example #{args[:num]} not found"
|
|
100
124
|
end
|
|
101
125
|
end
|
|
126
|
+
|
|
127
|
+
desc "Setup the Rails demo app (example 18)"
|
|
128
|
+
task :rails_setup do
|
|
129
|
+
Dir.chdir("examples/18_rails") do
|
|
130
|
+
sh "bin/setup"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
desc "Start the Rails demo app (example 18)"
|
|
135
|
+
task :rails do
|
|
136
|
+
Dir.chdir("examples/18_rails") do
|
|
137
|
+
sh "bin/dev"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
102
140
|
end
|
|
103
141
|
|
|
104
142
|
namespace :docs do
|
data/docs/api/core/robot.md
CHANGED
|
@@ -28,8 +28,10 @@ Robot.new(
|
|
|
28
28
|
tools: :none,
|
|
29
29
|
on_tool_call: nil,
|
|
30
30
|
on_tool_result: nil,
|
|
31
|
+
on_content: nil,
|
|
31
32
|
enable_cache: true,
|
|
32
33
|
bus: nil,
|
|
34
|
+
skills: nil,
|
|
33
35
|
temperature: nil,
|
|
34
36
|
top_p: nil,
|
|
35
37
|
top_k: nil,
|
|
@@ -57,8 +59,10 @@ Robot.new(
|
|
|
57
59
|
| `tools` | `Symbol`, `Array` | `:none` | Hierarchical tools config (`:none`, `:inherit`, or tool name array) |
|
|
58
60
|
| `on_tool_call` | `Proc`, `nil` | `nil` | Callback invoked when a tool is called |
|
|
59
61
|
| `on_tool_result` | `Proc`, `nil` | `nil` | Callback invoked when a tool returns a result |
|
|
62
|
+
| `on_content` | `Proc`, `nil` | `nil` | Stored streaming callback invoked with each content chunk (see [Streaming](#streaming)) |
|
|
60
63
|
| `enable_cache` | `Boolean` | `true` | Whether to enable semantic caching |
|
|
61
64
|
| `bus` | `TypedBus::MessageBus`, `nil` | `nil` | Optional message bus for inter-robot communication |
|
|
65
|
+
| `skills` | `Symbol`, `Array<Symbol>`, `nil` | `nil` | Skill templates to prepend (see [Skills](#skills)) |
|
|
62
66
|
| `config` | `RunConfig`, `nil` | `nil` | Shared config merged with explicit kwargs (see [RunConfig](#runconfig)) |
|
|
63
67
|
| `temperature` | `Float`, `nil` | `nil` | Controls randomness (0.0-1.0) |
|
|
64
68
|
| `top_p` | `Float`, `nil` | `nil` | Nucleus sampling threshold |
|
|
@@ -80,6 +84,7 @@ robot = RobotLab.build(
|
|
|
80
84
|
context: {},
|
|
81
85
|
enable_cache: true,
|
|
82
86
|
bus: nil, # Optional TypedBus::MessageBus
|
|
87
|
+
skills: nil, # Optional skill templates
|
|
83
88
|
**options # All other Robot.new parameters
|
|
84
89
|
)
|
|
85
90
|
# => RobotLab::Robot
|
|
@@ -95,6 +100,7 @@ If `name` is omitted, it defaults to `"robot"`.
|
|
|
95
100
|
| `description` | `String`, `nil` | Human-readable description |
|
|
96
101
|
| `template` | `Symbol`, `nil` | Prompt template identifier |
|
|
97
102
|
| `system_prompt` | `String`, `nil` | Inline system prompt |
|
|
103
|
+
| `skills` | `Array<Symbol>`, `nil` | Constructor-provided skill template IDs (nil if none) |
|
|
98
104
|
| `local_tools` | `Array` | Locally defined tools |
|
|
99
105
|
| `mcp_clients` | `Hash<String, MCP::Client>` | Connected MCP clients, keyed by server name |
|
|
100
106
|
| `mcp_tools` | `Array<Tool>` | Tools discovered from MCP servers |
|
|
@@ -119,7 +125,7 @@ Used by tools like [`AskUser`](tool.md#built-in-askuser) that need terminal IO.
|
|
|
119
125
|
### run
|
|
120
126
|
|
|
121
127
|
```ruby
|
|
122
|
-
result = robot.run(message, **kwargs)
|
|
128
|
+
result = robot.run(message, **kwargs, &block)
|
|
123
129
|
# => RobotResult
|
|
124
130
|
```
|
|
125
131
|
|
|
@@ -136,6 +142,9 @@ Primary execution method. Sends a message to the LLM with memory/MCP/tools resol
|
|
|
136
142
|
| `mcp` | `Symbol`, `Array` | `:none` | Runtime MCP override |
|
|
137
143
|
| `tools` | `Symbol`, `Array` | `:none` | Runtime tools override |
|
|
138
144
|
| `**kwargs` | `Hash` | `{}` | Additional keyword arguments passed to `Agent#ask` |
|
|
145
|
+
| `&block` | `Proc` | `nil` | Per-call streaming block, receives each content chunk |
|
|
146
|
+
|
|
147
|
+
When both a stored `on_content` callback and a runtime block are provided, both fire (stored first, then runtime block).
|
|
139
148
|
|
|
140
149
|
**Returns:** `RobotResult`
|
|
141
150
|
|
|
@@ -148,8 +157,8 @@ result = robot.run("What is 2+2?")
|
|
|
148
157
|
# With runtime memory
|
|
149
158
|
result = robot.run("Summarize the data", memory: { data: report })
|
|
150
159
|
|
|
151
|
-
# With streaming block
|
|
152
|
-
result = robot.run("Tell me a story") { |
|
|
160
|
+
# With per-call streaming block
|
|
161
|
+
result = robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
153
162
|
|
|
154
163
|
# With runtime overrides
|
|
155
164
|
result = robot.run("Help me", mcp: :none, tools: :none)
|
|
@@ -408,7 +417,7 @@ robot.to_h
|
|
|
408
417
|
# => Hash
|
|
409
418
|
```
|
|
410
419
|
|
|
411
|
-
Returns a hash representation of the robot including name, description, template, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise).
|
|
420
|
+
Returns a hash representation of the robot including name, description, template, skills, system_prompt, local_tools, mcp_tools, mcp_config, tools_config, mcp_servers, model, and bus (true if configured, omitted otherwise). Nil values are compacted out.
|
|
412
421
|
|
|
413
422
|
## Memory Behavior
|
|
414
423
|
|
|
@@ -437,7 +446,7 @@ Front matter supports two categories of keys:
|
|
|
437
446
|
|
|
438
447
|
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the underlying chat.
|
|
439
448
|
|
|
440
|
-
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
449
|
+
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp`, `skills` — applied to the robot's identity and capabilities. Constructor-provided values always take precedence.
|
|
441
450
|
|
|
442
451
|
| Key | Type | Description |
|
|
443
452
|
|-----|------|-------------|
|
|
@@ -445,6 +454,40 @@ Front matter supports two categories of keys:
|
|
|
445
454
|
| `description` | `String` | Human-readable description |
|
|
446
455
|
| `tools` | `Array<String>` | Tool class names resolved via `Object.const_get` |
|
|
447
456
|
| `mcp` | `Array<Hash>` | MCP server configurations |
|
|
457
|
+
| `skills` | `Array<Symbol>` | Skill templates to prepend (recursive, with cycle detection) |
|
|
458
|
+
|
|
459
|
+
## Skills
|
|
460
|
+
|
|
461
|
+
Skills compose robot behaviors from reusable templates. Each skill is a standard `.md` template whose prompt body is prepended before the main template. Skills are expanded depth-first with automatic cycle detection.
|
|
462
|
+
|
|
463
|
+
**Constructor:** `skills:` accepts `Symbol` or `Array<Symbol>`:
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
robot = RobotLab.build(
|
|
467
|
+
name: "support",
|
|
468
|
+
template: :support,
|
|
469
|
+
skills: [:clarifier, :json_responder]
|
|
470
|
+
)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Front matter:** templates can declare skills via `skills:` key:
|
|
474
|
+
|
|
475
|
+
```markdown
|
|
476
|
+
---
|
|
477
|
+
skills:
|
|
478
|
+
- clarifier
|
|
479
|
+
- json_responder
|
|
480
|
+
---
|
|
481
|
+
Main template body here.
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Constructor `skills:` and front matter `skills:` are combined (constructor first, then front matter). Skills can nest (a skill can declare its own `skills:` in front matter).
|
|
485
|
+
|
|
486
|
+
**Config cascade:** skill config merges in processing order (deepest first). Later values override earlier. Constructor kwargs always win.
|
|
487
|
+
|
|
488
|
+
**Prompt order:** skill bodies are concatenated in expansion order, followed by the main template body. All are joined with `"\n\n"` and set as system instructions via a single `with_instructions` call.
|
|
489
|
+
|
|
490
|
+
**Cycle detection:** if skills form a cycle, the duplicate is skipped with a logger warning.
|
|
448
491
|
|
|
449
492
|
## RunConfig
|
|
450
493
|
|
|
@@ -463,10 +506,78 @@ robot = RobotLab.build(
|
|
|
463
506
|
robot.config #=> RunConfig with model: "claude-sonnet-4", temperature: 0.9, ...
|
|
464
507
|
```
|
|
465
508
|
|
|
466
|
-
RunConfig fields: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`, `mcp`, `tools`, `on_tool_call`, `on_tool_result`, `bus`, `enable_cache`.
|
|
509
|
+
RunConfig fields: `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop`, `mcp`, `tools`, `on_tool_call`, `on_tool_result`, `on_content`, `bus`, `enable_cache`.
|
|
467
510
|
|
|
468
511
|
See [Configuration: RunConfig](../../getting-started/configuration.md#runconfig-shared-operational-defaults) for full details.
|
|
469
512
|
|
|
513
|
+
## Streaming
|
|
514
|
+
|
|
515
|
+
Robots support two complementary approaches for streaming LLM content in real-time.
|
|
516
|
+
|
|
517
|
+
### The Chunk Object
|
|
518
|
+
|
|
519
|
+
Both callbacks and blocks receive a [`RubyLLM::Chunk`](https://rubyllm.com/streaming/#basic-streaming) (subclass of `RubyLLM::Message`). Key accessors:
|
|
520
|
+
|
|
521
|
+
| Accessor | Type | Description |
|
|
522
|
+
|----------|------|-------------|
|
|
523
|
+
| `content` | `String`, `nil` | The text delta for this chunk (`nil` on tool-call or usage-only chunks) |
|
|
524
|
+
| `role` | `Symbol` | Always `:assistant` |
|
|
525
|
+
| `model_id` | `String` | The LLM model ID |
|
|
526
|
+
| `tool_calls` | `Array`, `nil` | Tool call deltas (partial JSON arguments) |
|
|
527
|
+
| `tool_call?` | `Boolean` | Whether this chunk contains tool call data |
|
|
528
|
+
| `thinking` | `Thinking`, `nil` | Extended thinking delta (Anthropic only) |
|
|
529
|
+
| `input_tokens` | `Integer`, `nil` | Input token count (populated on final chunk) |
|
|
530
|
+
| `output_tokens` | `Integer`, `nil` | Output token count (populated on final chunk) |
|
|
531
|
+
| `cached_tokens` | `Integer`, `nil` | Cached prompt tokens (final chunk) |
|
|
532
|
+
|
|
533
|
+
Most chunks carry only `content` (the text delta). The final chunk(s) carry token usage counts. Tool call chunks have `tool_calls` instead of `content`.
|
|
534
|
+
|
|
535
|
+
### Stored Callback (`on_content:`)
|
|
536
|
+
|
|
537
|
+
Wired at build time via constructor or RunConfig. Fires on every `run()` call automatically:
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
robot = RobotLab.build(
|
|
541
|
+
name: "assistant",
|
|
542
|
+
system_prompt: "You are helpful.",
|
|
543
|
+
on_content: ->(chunk) { broadcast(chunk.content) }
|
|
544
|
+
)
|
|
545
|
+
robot.run("Tell me a story") # streams via stored callback
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The `on_content` callback participates in the RunConfig cascade:
|
|
549
|
+
|
|
550
|
+
```ruby
|
|
551
|
+
config = RobotLab::RunConfig.new(
|
|
552
|
+
on_content: ->(chunk) { log(chunk.content) }
|
|
553
|
+
)
|
|
554
|
+
robot = RobotLab.build(name: "bot", config: config)
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Constructor `on_content:` overrides RunConfig `on_content`.
|
|
558
|
+
|
|
559
|
+
### Per-Call Block
|
|
560
|
+
|
|
561
|
+
Pass a block to `run()` for one-off streaming:
|
|
562
|
+
|
|
563
|
+
```ruby
|
|
564
|
+
robot.run("Tell me a story") { |chunk| print chunk.content }
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Both Together
|
|
568
|
+
|
|
569
|
+
When both exist, both fire — stored callback first, then runtime block:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
robot = RobotLab.build(
|
|
573
|
+
name: "bot",
|
|
574
|
+
system_prompt: "You are helpful.",
|
|
575
|
+
on_content: ->(chunk) { log(chunk.content) }
|
|
576
|
+
)
|
|
577
|
+
robot.run("Tell me a story") { |chunk| stream_to_client(chunk.content) }
|
|
578
|
+
# log() fires first, then stream_to_client()
|
|
579
|
+
```
|
|
580
|
+
|
|
470
581
|
## Configuration Hierarchy
|
|
471
582
|
|
|
472
583
|
Tools and MCP servers use hierarchical resolution: **runtime > robot > network > global config**.
|
|
@@ -559,6 +670,18 @@ result = robot.run("Search for popular Ruby repos")
|
|
|
559
670
|
robot.disconnect
|
|
560
671
|
```
|
|
561
672
|
|
|
673
|
+
### Robot with Skills
|
|
674
|
+
|
|
675
|
+
```ruby
|
|
676
|
+
robot = RobotLab.build(
|
|
677
|
+
name: "support",
|
|
678
|
+
template: :support,
|
|
679
|
+
skills: [:clarifier, :safety, :json_responder],
|
|
680
|
+
context: { company: "Acme Corp" }
|
|
681
|
+
)
|
|
682
|
+
result = robot.run("I need help with my order")
|
|
683
|
+
```
|
|
684
|
+
|
|
562
685
|
### Bare Robot with Chaining
|
|
563
686
|
|
|
564
687
|
```ruby
|
|
@@ -628,6 +751,6 @@ bot.send_message(to: :someone, content: "Hello!")
|
|
|
628
751
|
|
|
629
752
|
## See Also
|
|
630
753
|
|
|
631
|
-
- [Building Robots Guide](../../guides/building-robots.md)
|
|
754
|
+
- [Building Robots Guide](../../guides/building-robots.md) (includes [Composable Skills](../../guides/building-robots.md#composable-skills))
|
|
632
755
|
- [Tool](tool.md)
|
|
633
756
|
- [Network](network.md)
|
data/docs/api/core/tool.md
CHANGED
|
@@ -4,7 +4,7 @@ Callable function that robots can use to interact with external systems.
|
|
|
4
4
|
|
|
5
5
|
## Class: `RobotLab::Tool < RubyLLM::Tool`
|
|
6
6
|
|
|
7
|
-
RobotLab::Tool inherits from RubyLLM::Tool, adding a `robot:` constructor parameter
|
|
7
|
+
RobotLab::Tool inherits from RubyLLM::Tool, adding a `robot:` constructor parameter, a `Tool.create` factory for dynamic tools, and graceful error handling that returns plain-text errors to the LLM instead of crashing the run.
|
|
8
8
|
|
|
9
9
|
### Subclass Pattern
|
|
10
10
|
|
|
@@ -51,6 +51,15 @@ Tool.new(robot: nil)
|
|
|
51
51
|
|
|
52
52
|
## Class Methods
|
|
53
53
|
|
|
54
|
+
### raise_on_error / raise_on_error?
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
MyTool.raise_on_error = true
|
|
58
|
+
MyTool.raise_on_error? # => true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Per-class flag controlling whether `call` propagates exceptions from `execute` instead of catching them. Defaults to `false`. Does not affect other tool classes.
|
|
62
|
+
|
|
54
63
|
### Tool.create
|
|
55
64
|
|
|
56
65
|
Factory for dynamic tools (MCP wrappers, inline tools).
|
|
@@ -169,7 +178,24 @@ Whether this is an MCP-provided tool.
|
|
|
169
178
|
result = tool.call(args_hash)
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
Overrides `RubyLLM::Tool#call` with graceful error handling. Converts string keys to symbols and calls `execute(**args)`. If `execute` raises a `StandardError`, the error is caught and returned as a plain-text string the LLM can reason about:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Error (tool_name): exception message
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The error is also logged via `RobotLab.config.logger` at `:warn` level.
|
|
188
|
+
|
|
189
|
+
To propagate exceptions instead of catching them (for critical tools), set `raise_on_error` on the class:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class CriticalTool < RobotLab::Tool
|
|
193
|
+
self.raise_on_error = true
|
|
194
|
+
# ...
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`raise_on_error` is per-class (defaults to `false`) and does not affect other tool classes.
|
|
173
199
|
|
|
174
200
|
### params_schema
|
|
175
201
|
|
data/docs/concepts.md
CHANGED
|
@@ -12,7 +12,9 @@ Each robot has:
|
|
|
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`)
|
|
15
|
-
- **
|
|
15
|
+
- **Skills**: Composable template behaviors prepended before the main template
|
|
16
|
+
- **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances (with automatic error handling)
|
|
17
|
+
- **Streaming**: Real-time content via stored `on_content` callback or per-call block
|
|
16
18
|
- **Memory**: Persistent key-value store across runs
|
|
17
19
|
|
|
18
20
|
```ruby
|
|
@@ -137,7 +139,9 @@ result.continued? # Whether execution continues
|
|
|
137
139
|
|
|
138
140
|
## Tool
|
|
139
141
|
|
|
140
|
-
**Tools** give robots the ability to interact with external systems.
|
|
142
|
+
**Tools** give robots the ability to interact with external systems. `RobotLab::Tool` extends `RubyLLM::Tool` with graceful error handling — if `execute` raises a `StandardError`, the error is caught and returned as a plain-text string (`"Error (tool_name): message"`) so the LLM can reason about it. Critical tools can opt out with `self.raise_on_error = true`.
|
|
143
|
+
|
|
144
|
+
There are two patterns for defining tools:
|
|
141
145
|
|
|
142
146
|
### RubyLLM::Tool Subclass (Preferred)
|
|
143
147
|
|
|
@@ -446,7 +450,7 @@ Templates support two categories of front matter keys:
|
|
|
446
450
|
|
|
447
451
|
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
|
|
448
452
|
|
|
449
|
-
**Robot Extras:** `robot_name`, `description`, `tools`, `mcp` — applied to the robot's identity and capabilities. These make templates self-contained: reading the `.md` file tells you everything about the robot.
|
|
453
|
+
**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.
|
|
450
454
|
|
|
451
455
|
```markdown
|
|
452
456
|
---
|
data/docs/examples/index.md
CHANGED
|
@@ -29,7 +29,7 @@ These examples show how to use RobotLab for common scenarios, from simple chatbo
|
|
|
29
29
|
### Advanced Examples
|
|
30
30
|
|
|
31
31
|
- [Streaming Responses](basic-chat.md#with-streaming)
|
|
32
|
-
- [Persistent Conversations](basic-chat.md#with-
|
|
32
|
+
- [Persistent Conversations](basic-chat.md#with-memory)
|
|
33
33
|
- [MCP Integration](mcp-server.md)
|
|
34
34
|
- [Message Bus Communication](#message-bus)
|
|
35
35
|
- [Spawning Robots](#spawning-robots)
|
|
@@ -88,7 +88,7 @@ RobotLab.config.logger #=> Rails.logger
|
|
|
88
88
|
|
|
89
89
|
### Rails Engine and Railtie
|
|
90
90
|
|
|
91
|
-
RobotLab provides both a Rails Engine (`RobotLab::
|
|
91
|
+
RobotLab provides both a Rails Engine (`RobotLab::RailsIntegration::Engine`) and a Railtie (`RobotLab::RailsIntegration::Railtie`). These are loaded automatically when Rails is detected. The Engine isolates the RobotLab namespace and adds `app/robots` and `app/tools` to the autoload paths. The Railtie loads rake tasks and generators.
|
|
92
92
|
|
|
93
93
|
## Models
|
|
94
94
|
|