robot_lab 0.0.1 → 0.0.4
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/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +90 -0
- data/README.md +203 -46
- data/Rakefile +70 -1
- data/docs/api/core/index.md +12 -0
- data/docs/api/core/robot.md +478 -130
- data/docs/api/core/tool.md +205 -209
- data/docs/api/history/active-record-adapter.md +174 -94
- data/docs/api/history/config.md +186 -93
- data/docs/api/history/index.md +57 -61
- data/docs/api/history/thread-manager.md +123 -73
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +361 -112
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +312 -48
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +275 -162
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +417 -212
- data/docs/guides/creating-networks.md +94 -24
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/streaming.md +80 -110
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +50 -37
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +13 -14
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +140 -0
- data/examples/09_chaining.rb +223 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +230 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +57 -0
- data/examples/14_rusty_circuit/open_mic.rb +121 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +173 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
- data/lib/robot_lab/adapters/openai.rb +2 -1
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +2 -2
- data/lib/robot_lab/robot.rb +523 -249
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/robotic_model.rb +1 -1
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +66 -55
- metadata +107 -116
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- data/lib/robot_lab/configuration.rb +0 -143
data/docs/concepts.md
CHANGED
|
@@ -4,27 +4,74 @@ Understanding the fundamental concepts in RobotLab will help you build effective
|
|
|
4
4
|
|
|
5
5
|
## Robot
|
|
6
6
|
|
|
7
|
-
A **Robot** is an LLM-powered agent
|
|
7
|
+
A **Robot** is an LLM-powered agent that inherits from `RubyLLM::Agent`. Each robot wraps a persistent chat session created at initialization and provides template-based prompts, tools, memory, and MCP integration. Robots are created using keyword arguments via the `RobotLab.build` factory method.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
9
|
+
Each robot has:
|
|
10
|
+
|
|
11
|
+
- **Name**: A unique identifier (auto-generated if omitted)
|
|
12
|
+
- **Template**: A `.md` file with YAML front matter managed by prompt_manager, referenced by symbol
|
|
13
|
+
- **System Prompt**: Inline instructions (can be used alone or combined with a template)
|
|
14
|
+
- **Model**: The LLM model to use (defaults to `RobotLab.config.ruby_llm.model`)
|
|
15
|
+
- **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances
|
|
16
|
+
- **Memory**: Persistent key-value store across runs
|
|
14
17
|
|
|
15
18
|
```ruby
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# Robot with template (references prompts/support.md)
|
|
20
|
+
robot = RobotLab.build(
|
|
21
|
+
name: "support_agent",
|
|
22
|
+
template: :support,
|
|
23
|
+
context: { tone: "friendly", department: "billing" },
|
|
24
|
+
local_tools: [OrderLookup, RefundProcessor],
|
|
25
|
+
model: "claude-sonnet-4"
|
|
26
|
+
)
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
# Robot with inline system prompt
|
|
29
|
+
robot = RobotLab.build(
|
|
30
|
+
name: "helper",
|
|
31
|
+
system_prompt: "You are a friendly customer support agent."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Bare robot configured via chaining
|
|
35
|
+
robot = RobotLab.build(name: "bot")
|
|
36
|
+
robot.with_instructions("Be concise.").with_temperature(0.3).run("Hello")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The primary method is `robot.run("message")`, which takes a positional string argument and returns a `RobotResult`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
result = robot.run("What is 2 + 2?")
|
|
43
|
+
puts result.last_text_content # => "4"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Standalone robots persist their conversation history and memory across runs:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
robot.run("My name is Alice.")
|
|
50
|
+
result = robot.run("What is my name?")
|
|
51
|
+
puts result.last_text_content # => "Your name is Alice."
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
RobotLab uses `MywayConfig` for configuration. There is no `RobotLab.configure` block. Instead, configuration is loaded automatically from multiple sources in priority order:
|
|
57
|
+
|
|
58
|
+
1. Bundled defaults (`lib/robot_lab/config/defaults.yml`)
|
|
59
|
+
2. Environment-specific overrides (development, test, production)
|
|
60
|
+
3. XDG user config (`~/.config/robot_lab/config.yml`)
|
|
61
|
+
4. Project config (`./config/robot_lab.yml`)
|
|
62
|
+
5. Environment variables (`ROBOT_LAB_*` prefix)
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# Access configuration values
|
|
66
|
+
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
|
|
67
|
+
RobotLab.config.ruby_llm.request_timeout #=> 120
|
|
68
|
+
|
|
69
|
+
# Set API keys via environment variables
|
|
70
|
+
# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
71
|
+
# ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
|
|
72
|
+
|
|
73
|
+
# Reload configuration
|
|
74
|
+
RobotLab.reload_config!
|
|
28
75
|
```
|
|
29
76
|
|
|
30
77
|
## Network
|
|
@@ -35,6 +82,7 @@ A **Network** is a collection of robots orchestrated using [SimpleFlow](https://
|
|
|
35
82
|
- **Parallel Execution**: Tasks with the same dependencies run concurrently
|
|
36
83
|
- **Optional Task Activation**: Dynamic routing based on robot output
|
|
37
84
|
- **Per-Task Configuration**: Each task can have its own context, tools, and MCP servers
|
|
85
|
+
- **Shared Memory**: All robots in a network share a reactive memory instance
|
|
38
86
|
|
|
39
87
|
```ruby
|
|
40
88
|
network = RobotLab.create_network(name: "customer_service") do
|
|
@@ -46,6 +94,8 @@ network = RobotLab.create_network(name: "customer_service") do
|
|
|
46
94
|
context: { department: "technical" },
|
|
47
95
|
depends_on: :optional
|
|
48
96
|
end
|
|
97
|
+
|
|
98
|
+
result = network.run(message: "I was charged twice for my subscription.")
|
|
49
99
|
```
|
|
50
100
|
|
|
51
101
|
## Task
|
|
@@ -53,8 +103,8 @@ end
|
|
|
53
103
|
A **Task** wraps a robot for use in a network pipeline with per-task configuration:
|
|
54
104
|
|
|
55
105
|
- **Context**: Task-specific context deep-merged with network run params
|
|
56
|
-
- **MCP**: MCP servers available to this task
|
|
57
|
-
- **Tools**: Tools available to this task
|
|
106
|
+
- **MCP**: MCP servers available to this task (`:none`, `:inherit`, or array)
|
|
107
|
+
- **Tools**: Tools available to this task (`:none`, `:inherit`, or array)
|
|
58
108
|
- **Memory**: Task-specific memory
|
|
59
109
|
- **Dependencies**: `:none`, `[:task1, :task2]`, or `:optional`
|
|
60
110
|
|
|
@@ -87,53 +137,123 @@ result.continued? # Whether execution continues
|
|
|
87
137
|
|
|
88
138
|
## Tool
|
|
89
139
|
|
|
90
|
-
**Tools**
|
|
140
|
+
**Tools** give robots the ability to interact with external systems. There are two patterns for defining tools:
|
|
141
|
+
|
|
142
|
+
### RubyLLM::Tool Subclass (Preferred)
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class Calculator < RubyLLM::Tool
|
|
146
|
+
description "Performs basic arithmetic operations"
|
|
147
|
+
|
|
148
|
+
param :operation, type: "string", desc: "The operation (add, subtract, multiply, divide)"
|
|
149
|
+
param :a, type: "number", desc: "First operand"
|
|
150
|
+
param :b, type: "number", desc: "Second operand"
|
|
151
|
+
|
|
152
|
+
def execute(operation:, a:, b:)
|
|
153
|
+
case operation
|
|
154
|
+
when "add" then a + b
|
|
155
|
+
when "subtract" then a - b
|
|
156
|
+
when "multiply" then a * b
|
|
157
|
+
when "divide" then a.to_f / b
|
|
158
|
+
else "Unknown operation: #{operation}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
robot = RobotLab.build(
|
|
164
|
+
name: "math_bot",
|
|
165
|
+
system_prompt: "You can do math.",
|
|
166
|
+
local_tools: [Calculator]
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### RobotLab::Tool with Block Handler
|
|
91
171
|
|
|
92
172
|
```ruby
|
|
93
173
|
tool = RobotLab::Tool.new(
|
|
94
174
|
name: "get_weather",
|
|
95
175
|
description: "Get current weather for a location",
|
|
96
176
|
parameters: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
location: { type: "string", description: "City name" }
|
|
180
|
+
},
|
|
181
|
+
required: ["location"]
|
|
101
182
|
}
|
|
102
|
-
)
|
|
183
|
+
) do |input, **_opts|
|
|
184
|
+
WeatherService.current(input[:location])
|
|
185
|
+
end
|
|
103
186
|
```
|
|
104
187
|
|
|
105
|
-
##
|
|
188
|
+
## RobotResult
|
|
106
189
|
|
|
107
|
-
|
|
190
|
+
`RobotResult` captures the output of a single `robot.run(...)` call:
|
|
108
191
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
192
|
+
```ruby
|
|
193
|
+
result = robot.run("Hello!")
|
|
194
|
+
|
|
195
|
+
result.last_text_content # => "Hi there!" (String or nil)
|
|
196
|
+
result.output # => [TextMessage, ...] array of output messages
|
|
197
|
+
result.tool_calls # => [] array of tool call results
|
|
198
|
+
result.robot_name # => "assistant"
|
|
199
|
+
result.stop_reason # => "end_turn" or nil
|
|
200
|
+
result.has_tool_calls? # => false
|
|
201
|
+
result.checksum # => "a1b2c3d4..." (for dedup)
|
|
202
|
+
```
|
|
115
203
|
|
|
116
204
|
## Memory
|
|
117
205
|
|
|
118
|
-
**Memory** provides persistent storage across robot executions
|
|
206
|
+
**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.
|
|
119
207
|
|
|
120
208
|
```ruby
|
|
121
|
-
#
|
|
209
|
+
# Standalone robot with inherent memory
|
|
122
210
|
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
123
|
-
robot.run(
|
|
124
|
-
robot.run(
|
|
211
|
+
robot.run("My name is Alice")
|
|
212
|
+
robot.run("What's my name?") # Memory persists across runs
|
|
125
213
|
|
|
126
|
-
# Access robot's memory
|
|
214
|
+
# Access robot's memory directly
|
|
127
215
|
robot.memory[:user_id] = 123
|
|
128
216
|
robot.memory.data[:category] = "billing"
|
|
217
|
+
robot.memory.data.category # => "billing" (method-style access)
|
|
129
218
|
|
|
130
219
|
# Runtime memory injection
|
|
131
|
-
robot.run(
|
|
220
|
+
robot.run("Help me", memory: { session_id: "abc123" })
|
|
132
221
|
|
|
133
222
|
# Reset memory
|
|
134
223
|
robot.reset_memory
|
|
135
224
|
```
|
|
136
225
|
|
|
226
|
+
### Reserved Memory Keys
|
|
227
|
+
|
|
228
|
+
| Key | Purpose |
|
|
229
|
+
|-----|---------|
|
|
230
|
+
| `:data` | Runtime data (StateProxy for method-style access) |
|
|
231
|
+
| `:results` | Accumulated robot results |
|
|
232
|
+
| `:messages` | Conversation history |
|
|
233
|
+
| `:session_id` | Session identifier for history persistence |
|
|
234
|
+
| `:cache` | Semantic cache instance (RubyLLM::SemanticCache) |
|
|
235
|
+
|
|
236
|
+
### Reactive Memory in Networks
|
|
237
|
+
|
|
238
|
+
In a network, shared memory supports pub/sub semantics for inter-robot communication:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Robot A writes to shared memory
|
|
242
|
+
network.memory.set(:sentiment, { score: 0.8 })
|
|
243
|
+
|
|
244
|
+
# Robot B reads (blocking until available)
|
|
245
|
+
result = network.memory.get(:sentiment, wait: true)
|
|
246
|
+
result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
|
|
247
|
+
|
|
248
|
+
# Multiple keys
|
|
249
|
+
results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
250
|
+
|
|
251
|
+
# Subscribe to changes
|
|
252
|
+
network.memory.subscribe(:status) do |change|
|
|
253
|
+
puts "#{change.key} changed by #{change.writer}: #{change.value}"
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
137
257
|
## MCP (Model Context Protocol)
|
|
138
258
|
|
|
139
259
|
**MCP** allows robots to connect to external tool servers:
|
|
@@ -141,6 +261,7 @@ robot.reset_memory
|
|
|
141
261
|
```ruby
|
|
142
262
|
robot = RobotLab.build(
|
|
143
263
|
name: "developer",
|
|
264
|
+
system_prompt: "You are a developer assistant.",
|
|
144
265
|
mcp: [
|
|
145
266
|
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } },
|
|
146
267
|
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
@@ -148,6 +269,8 @@ robot = RobotLab.build(
|
|
|
148
269
|
)
|
|
149
270
|
```
|
|
150
271
|
|
|
272
|
+
MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
|
|
273
|
+
|
|
151
274
|
## Execution Flow
|
|
152
275
|
|
|
153
276
|
```mermaid
|
|
@@ -160,11 +283,12 @@ sequenceDiagram
|
|
|
160
283
|
participant LLM
|
|
161
284
|
participant Tool
|
|
162
285
|
|
|
163
|
-
User->>Network: run(message, context)
|
|
164
|
-
Network->>Pipeline:
|
|
286
|
+
User->>Network: run(message: "...", **context)
|
|
287
|
+
Network->>Pipeline: call_parallel(initial_result)
|
|
165
288
|
Pipeline->>Task: call(result)
|
|
166
289
|
Task->>Robot: call(enhanced_result)
|
|
167
|
-
Robot->>
|
|
290
|
+
Robot->>Robot: extract_run_context(result)
|
|
291
|
+
Robot->>LLM: ask(message)
|
|
168
292
|
|
|
169
293
|
alt Tool Call
|
|
170
294
|
LLM-->>Robot: tool_call
|
|
@@ -175,7 +299,7 @@ sequenceDiagram
|
|
|
175
299
|
|
|
176
300
|
LLM-->>Robot: response
|
|
177
301
|
Robot-->>Task: RobotResult
|
|
178
|
-
Task-->>Pipeline: result.continue(
|
|
302
|
+
Task-->>Pipeline: result.continue(robot_result)
|
|
179
303
|
|
|
180
304
|
alt Optional Task Activated
|
|
181
305
|
Pipeline->>Task: call activated task
|
|
@@ -185,21 +309,26 @@ sequenceDiagram
|
|
|
185
309
|
Network-->>User: SimpleFlow::Result
|
|
186
310
|
```
|
|
187
311
|
|
|
188
|
-
## Conditional Routing
|
|
312
|
+
## Conditional Routing with ClassifierRobot
|
|
189
313
|
|
|
190
|
-
Use custom Robot
|
|
314
|
+
Use a custom Robot subclass to implement intelligent routing. Override `call(result)` to inspect the LLM output and activate optional tasks:
|
|
191
315
|
|
|
192
316
|
```ruby
|
|
193
317
|
class ClassifierRobot < RobotLab::Robot
|
|
194
318
|
def call(result)
|
|
195
|
-
|
|
319
|
+
# Extract context and message from the pipeline result
|
|
320
|
+
context = extract_run_context(result)
|
|
321
|
+
message = context.delete(:message)
|
|
322
|
+
|
|
323
|
+
# Run the robot to classify the input
|
|
324
|
+
robot_result = run(message, **context)
|
|
196
325
|
|
|
197
326
|
new_result = result
|
|
198
327
|
.with_context(@name.to_sym, robot_result)
|
|
199
328
|
.continue(robot_result)
|
|
200
329
|
|
|
201
|
-
# Activate appropriate specialist
|
|
202
|
-
category = robot_result.last_text_content.to_s.downcase
|
|
330
|
+
# Activate the appropriate specialist based on classification
|
|
331
|
+
category = robot_result.last_text_content.to_s.strip.downcase
|
|
203
332
|
case category
|
|
204
333
|
when /billing/ then new_result.activate(:billing)
|
|
205
334
|
when /technical/ then new_result.activate(:technical)
|
|
@@ -207,8 +336,143 @@ class ClassifierRobot < RobotLab::Robot
|
|
|
207
336
|
end
|
|
208
337
|
end
|
|
209
338
|
end
|
|
339
|
+
|
|
340
|
+
# Build the classifier (uses a .md template with YAML front matter)
|
|
341
|
+
classifier = ClassifierRobot.new(
|
|
342
|
+
name: "classifier",
|
|
343
|
+
template: :classifier,
|
|
344
|
+
model: "claude-sonnet-4"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Build specialist robots
|
|
348
|
+
billing_robot = RobotLab.build(name: "billing", template: :billing)
|
|
349
|
+
technical_robot = RobotLab.build(name: "technical", template: :technical)
|
|
350
|
+
general_robot = RobotLab.build(name: "general", template: :general)
|
|
351
|
+
|
|
352
|
+
# Create network with optional task routing
|
|
353
|
+
network = RobotLab.create_network(name: "support_network") do
|
|
354
|
+
task :classifier, classifier, depends_on: :none
|
|
355
|
+
task :billing, billing_robot, depends_on: :optional
|
|
356
|
+
task :technical, technical_robot, depends_on: :optional
|
|
357
|
+
task :general, general_robot, depends_on: :optional
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
result = network.run(message: "I was charged twice for my subscription.")
|
|
361
|
+
puts result.value.last_text_content
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Message Bus
|
|
365
|
+
|
|
366
|
+
The **Message Bus** enables bidirectional, cyclic communication between robots via `typed_bus`. While Networks enforce DAG-based execution, the bus supports negotiation loops, convergence patterns, and multi-turn dialogues.
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
bus = TypedBus::MessageBus.new
|
|
370
|
+
|
|
371
|
+
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
372
|
+
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
373
|
+
|
|
374
|
+
# Register handlers
|
|
375
|
+
bob.on_message do |message|
|
|
376
|
+
joke = bob.run(message.content.to_s).last_text_content
|
|
377
|
+
bob.reply(message, joke)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
alice.on_message do |message|
|
|
381
|
+
verdict = alice.run("Is this funny? #{message.content}").last_text_content
|
|
382
|
+
# Send another request if not satisfied
|
|
383
|
+
alice.send_message(to: :bob, content: "Try again.") unless verdict.start_with?("FUNNY")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Start the conversation
|
|
387
|
+
alice.send_message(to: :bob, content: "Tell me a robot joke.")
|
|
210
388
|
```
|
|
211
389
|
|
|
390
|
+
Key features:
|
|
391
|
+
|
|
392
|
+
- **Typed channels** — only `RobotMessage` objects accepted per channel
|
|
393
|
+
- **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
|
|
394
|
+
- **Reply correlation** — `reply(message, content)` tracks threads via `in_reply_to`
|
|
395
|
+
- **Independent of Network** — bus works without a Network pipeline
|
|
396
|
+
|
|
397
|
+
### Dynamic Spawning
|
|
398
|
+
|
|
399
|
+
Robots can create new robots at runtime using `spawn`. The bus is created lazily:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
|
|
403
|
+
|
|
404
|
+
# spawn creates a child on the same bus (bus created automatically)
|
|
405
|
+
helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
|
|
406
|
+
answer = helper.run("What is 2+2?").last_text_content
|
|
407
|
+
helper.send_message(to: :dispatcher, content: answer)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Robots can also join a bus after creation with `with_bus`:
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
|
|
414
|
+
bot.with_bus(existing_bus)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Multiple robots with the same name enable fan-out — messages sent to that name are delivered to all subscribers.
|
|
418
|
+
|
|
419
|
+
## Templates
|
|
420
|
+
|
|
421
|
+
Templates are `.md` files with YAML front matter, managed by the prompt_manager gem. They live in the configured template path (default: `./prompts/` or `app/prompts/` in Rails).
|
|
422
|
+
|
|
423
|
+
```markdown
|
|
424
|
+
---
|
|
425
|
+
description: Customer support classifier
|
|
426
|
+
model: claude-sonnet-4
|
|
427
|
+
temperature: 0.3
|
|
428
|
+
---
|
|
429
|
+
You are a request classifier. Analyze the user's request and classify it
|
|
430
|
+
as either "billing", "technical", or "general".
|
|
431
|
+
|
|
432
|
+
Respond with ONLY the category name, nothing else.
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Reference templates by symbol when building robots:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
robot = RobotLab.build(
|
|
439
|
+
name: "classifier",
|
|
440
|
+
template: :classifier, # loads prompts/classifier.md
|
|
441
|
+
context: { tone: "professional" } # variables passed to the template
|
|
442
|
+
)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Front Matter Keys
|
|
446
|
+
|
|
447
|
+
Templates support two categories of front matter keys:
|
|
448
|
+
|
|
449
|
+
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
|
|
450
|
+
|
|
451
|
+
**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.
|
|
452
|
+
|
|
453
|
+
```markdown
|
|
454
|
+
---
|
|
455
|
+
description: GitHub assistant with MCP tool access
|
|
456
|
+
robot_name: github_bot
|
|
457
|
+
tools:
|
|
458
|
+
- CodeSearchTool
|
|
459
|
+
mcp:
|
|
460
|
+
- name: github
|
|
461
|
+
transport: stdio
|
|
462
|
+
command: npx
|
|
463
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
464
|
+
model: claude-sonnet-4
|
|
465
|
+
---
|
|
466
|
+
You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
# Template provides everything — minimal constructor
|
|
471
|
+
robot = RobotLab.build(template: :github_assistant)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values.
|
|
475
|
+
|
|
212
476
|
## Next Steps
|
|
213
477
|
|
|
214
478
|
- [Quick Start Guide](getting-started/quick-start.md) - Build your first robot
|