robot_lab 0.0.1 → 0.0.6
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 +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- 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/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- 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 +360 -116
- 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 +311 -49
- 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 +347 -154
- 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 +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- 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 +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -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 +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -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 +156 -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/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -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 +0 -13
- 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 +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -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/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- 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/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
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,121 @@ 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)
|
|
91
143
|
|
|
92
144
|
```ruby
|
|
93
|
-
|
|
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.create Factory
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
tool = RobotLab::Tool.create(
|
|
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
|
+
) { |args| WeatherService.current(args[:location]) }
|
|
103
184
|
```
|
|
104
185
|
|
|
105
|
-
##
|
|
186
|
+
## RobotResult
|
|
106
187
|
|
|
107
|
-
|
|
188
|
+
`RobotResult` captures the output of a single `robot.run(...)` call:
|
|
108
189
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
190
|
+
```ruby
|
|
191
|
+
result = robot.run("Hello!")
|
|
192
|
+
|
|
193
|
+
result.last_text_content # => "Hi there!" (String or nil)
|
|
194
|
+
result.output # => [TextMessage, ...] array of output messages
|
|
195
|
+
result.tool_calls # => [] array of tool call results
|
|
196
|
+
result.robot_name # => "assistant"
|
|
197
|
+
result.stop_reason # => "end_turn" or nil
|
|
198
|
+
result.has_tool_calls? # => false
|
|
199
|
+
result.checksum # => "a1b2c3d4..." (for dedup)
|
|
200
|
+
```
|
|
115
201
|
|
|
116
202
|
## Memory
|
|
117
203
|
|
|
118
|
-
**Memory** provides persistent storage across robot executions
|
|
204
|
+
**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
205
|
|
|
120
206
|
```ruby
|
|
121
|
-
#
|
|
207
|
+
# Standalone robot with inherent memory
|
|
122
208
|
robot = RobotLab.build(name: "assistant", system_prompt: "You are helpful.")
|
|
123
|
-
robot.run(
|
|
124
|
-
robot.run(
|
|
209
|
+
robot.run("My name is Alice")
|
|
210
|
+
robot.run("What's my name?") # Memory persists across runs
|
|
125
211
|
|
|
126
|
-
# Access robot's memory
|
|
212
|
+
# Access robot's memory directly
|
|
127
213
|
robot.memory[:user_id] = 123
|
|
128
214
|
robot.memory.data[:category] = "billing"
|
|
215
|
+
robot.memory.data.category # => "billing" (method-style access)
|
|
129
216
|
|
|
130
217
|
# Runtime memory injection
|
|
131
|
-
robot.run(
|
|
218
|
+
robot.run("Help me", memory: { session_id: "abc123" })
|
|
132
219
|
|
|
133
220
|
# Reset memory
|
|
134
221
|
robot.reset_memory
|
|
135
222
|
```
|
|
136
223
|
|
|
224
|
+
### Reserved Memory Keys
|
|
225
|
+
|
|
226
|
+
| Key | Purpose |
|
|
227
|
+
|-----|---------|
|
|
228
|
+
| `:data` | Runtime data (StateProxy for method-style access) |
|
|
229
|
+
| `:results` | Accumulated robot results |
|
|
230
|
+
| `:messages` | Conversation history |
|
|
231
|
+
| `:session_id` | Session identifier for history persistence |
|
|
232
|
+
| `:cache` | Semantic cache instance (RubyLLM::SemanticCache) |
|
|
233
|
+
|
|
234
|
+
### Reactive Memory in Networks
|
|
235
|
+
|
|
236
|
+
In a network, shared memory supports pub/sub semantics for inter-robot communication:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# Robot A writes to shared memory
|
|
240
|
+
network.memory.set(:sentiment, { score: 0.8 })
|
|
241
|
+
|
|
242
|
+
# Robot B reads (blocking until available)
|
|
243
|
+
result = network.memory.get(:sentiment, wait: true)
|
|
244
|
+
result = network.memory.get(:sentiment, wait: 30) # timeout in seconds
|
|
245
|
+
|
|
246
|
+
# Multiple keys
|
|
247
|
+
results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
248
|
+
|
|
249
|
+
# Subscribe to changes
|
|
250
|
+
network.memory.subscribe(:status) do |change|
|
|
251
|
+
puts "#{change.key} changed by #{change.writer}: #{change.value}"
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
137
255
|
## MCP (Model Context Protocol)
|
|
138
256
|
|
|
139
257
|
**MCP** allows robots to connect to external tool servers:
|
|
@@ -141,6 +259,7 @@ robot.reset_memory
|
|
|
141
259
|
```ruby
|
|
142
260
|
robot = RobotLab.build(
|
|
143
261
|
name: "developer",
|
|
262
|
+
system_prompt: "You are a developer assistant.",
|
|
144
263
|
mcp: [
|
|
145
264
|
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } },
|
|
146
265
|
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
@@ -148,6 +267,8 @@ robot = RobotLab.build(
|
|
|
148
267
|
)
|
|
149
268
|
```
|
|
150
269
|
|
|
270
|
+
MCP configuration follows a hierarchical resolution: `runtime > robot > network > global config`. Values can be `:none`, `:inherit`, or explicit arrays.
|
|
271
|
+
|
|
151
272
|
## Execution Flow
|
|
152
273
|
|
|
153
274
|
```mermaid
|
|
@@ -160,11 +281,12 @@ sequenceDiagram
|
|
|
160
281
|
participant LLM
|
|
161
282
|
participant Tool
|
|
162
283
|
|
|
163
|
-
User->>Network: run(message, context)
|
|
164
|
-
Network->>Pipeline:
|
|
284
|
+
User->>Network: run(message: "...", **context)
|
|
285
|
+
Network->>Pipeline: call_parallel(initial_result)
|
|
165
286
|
Pipeline->>Task: call(result)
|
|
166
287
|
Task->>Robot: call(enhanced_result)
|
|
167
|
-
Robot->>
|
|
288
|
+
Robot->>Robot: extract_run_context(result)
|
|
289
|
+
Robot->>LLM: ask(message)
|
|
168
290
|
|
|
169
291
|
alt Tool Call
|
|
170
292
|
LLM-->>Robot: tool_call
|
|
@@ -175,7 +297,7 @@ sequenceDiagram
|
|
|
175
297
|
|
|
176
298
|
LLM-->>Robot: response
|
|
177
299
|
Robot-->>Task: RobotResult
|
|
178
|
-
Task-->>Pipeline: result.continue(
|
|
300
|
+
Task-->>Pipeline: result.continue(robot_result)
|
|
179
301
|
|
|
180
302
|
alt Optional Task Activated
|
|
181
303
|
Pipeline->>Task: call activated task
|
|
@@ -185,21 +307,26 @@ sequenceDiagram
|
|
|
185
307
|
Network-->>User: SimpleFlow::Result
|
|
186
308
|
```
|
|
187
309
|
|
|
188
|
-
## Conditional Routing
|
|
310
|
+
## Conditional Routing with ClassifierRobot
|
|
189
311
|
|
|
190
|
-
Use custom Robot
|
|
312
|
+
Use a custom Robot subclass to implement intelligent routing. Override `call(result)` to inspect the LLM output and activate optional tasks:
|
|
191
313
|
|
|
192
314
|
```ruby
|
|
193
315
|
class ClassifierRobot < RobotLab::Robot
|
|
194
316
|
def call(result)
|
|
195
|
-
|
|
317
|
+
# Extract context and message from the pipeline result
|
|
318
|
+
context = extract_run_context(result)
|
|
319
|
+
message = context.delete(:message)
|
|
320
|
+
|
|
321
|
+
# Run the robot to classify the input
|
|
322
|
+
robot_result = run(message, **context)
|
|
196
323
|
|
|
197
324
|
new_result = result
|
|
198
325
|
.with_context(@name.to_sym, robot_result)
|
|
199
326
|
.continue(robot_result)
|
|
200
327
|
|
|
201
|
-
# Activate appropriate specialist
|
|
202
|
-
category = robot_result.last_text_content.to_s.downcase
|
|
328
|
+
# Activate the appropriate specialist based on classification
|
|
329
|
+
category = robot_result.last_text_content.to_s.strip.downcase
|
|
203
330
|
case category
|
|
204
331
|
when /billing/ then new_result.activate(:billing)
|
|
205
332
|
when /technical/ then new_result.activate(:technical)
|
|
@@ -207,8 +334,143 @@ class ClassifierRobot < RobotLab::Robot
|
|
|
207
334
|
end
|
|
208
335
|
end
|
|
209
336
|
end
|
|
337
|
+
|
|
338
|
+
# Build the classifier (uses a .md template with YAML front matter)
|
|
339
|
+
classifier = ClassifierRobot.new(
|
|
340
|
+
name: "classifier",
|
|
341
|
+
template: :classifier,
|
|
342
|
+
model: "claude-sonnet-4"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Build specialist robots
|
|
346
|
+
billing_robot = RobotLab.build(name: "billing", template: :billing)
|
|
347
|
+
technical_robot = RobotLab.build(name: "technical", template: :technical)
|
|
348
|
+
general_robot = RobotLab.build(name: "general", template: :general)
|
|
349
|
+
|
|
350
|
+
# Create network with optional task routing
|
|
351
|
+
network = RobotLab.create_network(name: "support_network") do
|
|
352
|
+
task :classifier, classifier, depends_on: :none
|
|
353
|
+
task :billing, billing_robot, depends_on: :optional
|
|
354
|
+
task :technical, technical_robot, depends_on: :optional
|
|
355
|
+
task :general, general_robot, depends_on: :optional
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
result = network.run(message: "I was charged twice for my subscription.")
|
|
359
|
+
puts result.value.last_text_content
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Message Bus
|
|
363
|
+
|
|
364
|
+
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.
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
bus = TypedBus::MessageBus.new
|
|
368
|
+
|
|
369
|
+
bob = RobotLab.build(name: "bob", system_prompt: "You tell jokes.", bus: bus)
|
|
370
|
+
alice = RobotLab.build(name: "alice", system_prompt: "You evaluate jokes.", bus: bus)
|
|
371
|
+
|
|
372
|
+
# Register handlers
|
|
373
|
+
bob.on_message do |message|
|
|
374
|
+
joke = bob.run(message.content.to_s).last_text_content
|
|
375
|
+
bob.send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
alice.on_message do |message|
|
|
379
|
+
verdict = alice.run("Is this funny? #{message.content}").last_text_content
|
|
380
|
+
# Send another request if not satisfied
|
|
381
|
+
alice.send_message(to: :bob, content: "Try again.") unless verdict.start_with?("FUNNY")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Start the conversation
|
|
385
|
+
alice.send_message(to: :bob, content: "Tell me a robot joke.")
|
|
210
386
|
```
|
|
211
387
|
|
|
388
|
+
Key features:
|
|
389
|
+
|
|
390
|
+
- **Typed channels** — only `RobotMessage` objects accepted per channel
|
|
391
|
+
- **Auto-ACK** — 1-arg `on_message` blocks auto-acknowledge; 2-arg blocks give manual control
|
|
392
|
+
- **Reply correlation** — `send_reply(to:, content:, in_reply_to:)` tracks threads via `in_reply_to`
|
|
393
|
+
- **Independent of Network** — bus works without a Network pipeline
|
|
394
|
+
|
|
395
|
+
### Dynamic Spawning
|
|
396
|
+
|
|
397
|
+
Robots can create new robots at runtime using `spawn`. The bus is created lazily:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
dispatcher = RobotLab.build(name: "dispatcher", system_prompt: "You delegate work.")
|
|
401
|
+
|
|
402
|
+
# spawn creates a child on the same bus (bus created automatically)
|
|
403
|
+
helper = dispatcher.spawn(name: "helper", system_prompt: "You answer questions.")
|
|
404
|
+
answer = helper.run("What is 2+2?").last_text_content
|
|
405
|
+
helper.send_message(to: :dispatcher, content: answer)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Robots can also join a bus after creation with `with_bus`:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
bot = RobotLab.build(name: "latecomer", system_prompt: "Hello.")
|
|
412
|
+
bot.with_bus(existing_bus)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Multiple robots with the same name enable fan-out — messages sent to that name are delivered to all subscribers.
|
|
416
|
+
|
|
417
|
+
## Templates
|
|
418
|
+
|
|
419
|
+
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).
|
|
420
|
+
|
|
421
|
+
```markdown
|
|
422
|
+
---
|
|
423
|
+
description: Customer support classifier
|
|
424
|
+
model: claude-sonnet-4
|
|
425
|
+
temperature: 0.3
|
|
426
|
+
---
|
|
427
|
+
You are a request classifier. Analyze the user's request and classify it
|
|
428
|
+
as either "billing", "technical", or "general".
|
|
429
|
+
|
|
430
|
+
Respond with ONLY the category name, nothing else.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Reference templates by symbol when building robots:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
robot = RobotLab.build(
|
|
437
|
+
name: "classifier",
|
|
438
|
+
template: :classifier, # loads prompts/classifier.md
|
|
439
|
+
context: { tone: "professional" } # variables passed to the template
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Front Matter Keys
|
|
444
|
+
|
|
445
|
+
Templates support two categories of front matter keys:
|
|
446
|
+
|
|
447
|
+
**LLM Config:** `model`, `temperature`, `top_p`, `top_k`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `stop` — applied to the robot's chat configuration.
|
|
448
|
+
|
|
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.
|
|
450
|
+
|
|
451
|
+
```markdown
|
|
452
|
+
---
|
|
453
|
+
description: GitHub assistant with MCP tool access
|
|
454
|
+
robot_name: github_bot
|
|
455
|
+
tools:
|
|
456
|
+
- CodeSearchTool
|
|
457
|
+
mcp:
|
|
458
|
+
- name: github
|
|
459
|
+
transport: stdio
|
|
460
|
+
command: npx
|
|
461
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
462
|
+
model: claude-sonnet-4
|
|
463
|
+
---
|
|
464
|
+
You are a GitHub assistant. Use available tools to help with repository tasks.
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
# Template provides everything — minimal constructor
|
|
469
|
+
robot = RobotLab.build(template: :github_assistant)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Constructor-provided values (`local_tools:`, `mcp:`, `name:`, `description:`) always take precedence over front matter values.
|
|
473
|
+
|
|
212
474
|
## Next Steps
|
|
213
475
|
|
|
214
476
|
- [Quick Start Guide](getting-started/quick-start.md) - Build your first robot
|