robot_lab 0.0.8 → 0.0.11
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 +71 -0
- data/README.md +106 -4
- data/Rakefile +2 -1
- data/docs/api/core/robot.md +336 -1
- data/docs/api/mcp/client.md +1 -0
- data/docs/api/mcp/server.md +27 -8
- data/docs/api/mcp/transports.md +21 -6
- data/docs/architecture/core-concepts.md +1 -1
- data/docs/architecture/robot-execution.md +20 -2
- data/docs/concepts.md +4 -0
- data/docs/guides/building-robots.md +18 -0
- data/docs/guides/creating-networks.md +39 -0
- data/docs/guides/index.md +10 -0
- data/docs/guides/knowledge.md +182 -0
- data/docs/guides/mcp-integration.md +180 -2
- data/docs/guides/memory.md +2 -0
- data/docs/guides/observability.md +486 -0
- data/docs/guides/ractor-parallelism.md +364 -0
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +1538 -0
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +258 -0
- data/examples/14_rusty_circuit/.gitignore +1 -0
- data/examples/14_rusty_circuit/open_mic.rb +1 -1
- data/examples/19_token_tracking.rb +128 -0
- data/examples/20_circuit_breaker.rb +153 -0
- data/examples/21_learning_loop.rb +164 -0
- data/examples/22_context_compression.rb +179 -0
- data/examples/23_convergence.rb +137 -0
- data/examples/24_structured_delegation.rb +150 -0
- data/examples/25_history_search/conversation.jsonl +30 -0
- data/examples/25_history_search.rb +136 -0
- data/examples/26_document_store/api_versioning_adr.md +52 -0
- data/examples/26_document_store/incident_postmortem.md +46 -0
- data/examples/26_document_store/postgres_runbook.md +49 -0
- data/examples/26_document_store/redis_caching_guide.md +48 -0
- data/examples/26_document_store/sidekiq_guide.md +51 -0
- data/examples/26_document_store.rb +147 -0
- data/examples/27_incident_response/incident_response.rb +244 -0
- data/examples/28_mcp_discovery.rb +112 -0
- data/examples/29_ractor_tools.rb +243 -0
- data/examples/30_ractor_network.rb +256 -0
- data/examples/README.md +136 -0
- data/examples/prompts/skill_with_mcp_test.md +9 -0
- data/examples/prompts/skill_with_robot_name_test.md +5 -0
- data/examples/prompts/skill_with_tools_test.md +6 -0
- data/lib/robot_lab/bus_poller.rb +149 -0
- data/lib/robot_lab/convergence.rb +69 -0
- data/lib/robot_lab/delegation_future.rb +93 -0
- data/lib/robot_lab/document_store.rb +155 -0
- data/lib/robot_lab/error.rb +25 -0
- data/lib/robot_lab/history_compressor.rb +205 -0
- data/lib/robot_lab/mcp/client.rb +23 -9
- data/lib/robot_lab/mcp/connection_poller.rb +187 -0
- data/lib/robot_lab/mcp/server.rb +26 -3
- data/lib/robot_lab/mcp/server_discovery.rb +110 -0
- data/lib/robot_lab/mcp/transports/base.rb +10 -2
- data/lib/robot_lab/mcp/transports/stdio.rb +58 -26
- data/lib/robot_lab/memory.rb +103 -6
- data/lib/robot_lab/network.rb +44 -9
- data/lib/robot_lab/ractor_boundary.rb +42 -0
- data/lib/robot_lab/ractor_job.rb +37 -0
- data/lib/robot_lab/ractor_memory_proxy.rb +85 -0
- data/lib/robot_lab/ractor_network_scheduler.rb +154 -0
- data/lib/robot_lab/ractor_worker_pool.rb +117 -0
- data/lib/robot_lab/robot/bus_messaging.rb +43 -65
- data/lib/robot_lab/robot/history_search.rb +69 -0
- data/lib/robot_lab/robot/mcp_management.rb +61 -4
- data/lib/robot_lab/robot.rb +351 -11
- data/lib/robot_lab/robot_result.rb +26 -5
- data/lib/robot_lab/run_config.rb +1 -1
- data/lib/robot_lab/text_analysis.rb +103 -0
- data/lib/robot_lab/tool.rb +42 -3
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +49 -29
- data/lib/robot_lab.rb +25 -0
- data/mkdocs.yml +1 -0
- metadata +71 -2
data/docs/api/mcp/server.md
CHANGED
|
@@ -13,31 +13,42 @@ server = RobotLab::MCP::Server.new(
|
|
|
13
13
|
name: "filesystem",
|
|
14
14
|
transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
|
|
15
15
|
)
|
|
16
|
+
|
|
17
|
+
# With custom timeout
|
|
18
|
+
server = RobotLab::MCP::Server.new(
|
|
19
|
+
name: "slow_server",
|
|
20
|
+
transport: { type: "stdio", command: "heavy-mcp-server" },
|
|
21
|
+
timeout: 30
|
|
22
|
+
)
|
|
16
23
|
```
|
|
17
24
|
|
|
18
25
|
## Constructor
|
|
19
26
|
|
|
20
27
|
```ruby
|
|
21
|
-
Server.new(name:, transport:)
|
|
28
|
+
Server.new(name:, transport:, timeout: nil, **_extra)
|
|
22
29
|
```
|
|
23
30
|
|
|
24
31
|
**Parameters:**
|
|
25
32
|
|
|
26
|
-
| Name | Type | Description |
|
|
27
|
-
|
|
28
|
-
| `name` | `String` | Unique server identifier |
|
|
29
|
-
| `transport` | `Hash` | Transport configuration (must include `type`) |
|
|
33
|
+
| Name | Type | Default | Description |
|
|
34
|
+
|------|------|---------|-------------|
|
|
35
|
+
| `name` | `String` | **required** | Unique server identifier |
|
|
36
|
+
| `transport` | `Hash` | **required** | Transport configuration (must include `type`) |
|
|
37
|
+
| `timeout` | `Numeric`, `nil` | `15` | Request timeout in seconds. Values >= 1000 are auto-converted from milliseconds. Minimum 1 second |
|
|
30
38
|
|
|
31
39
|
**Raises:** `ArgumentError` if:
|
|
32
40
|
- The transport type is not one of the valid types
|
|
33
41
|
- A stdio transport is missing the `:command` key
|
|
34
42
|
- A network transport (ws, websocket, sse, streamable-http, http) is missing the `:url` key
|
|
35
43
|
|
|
36
|
-
##
|
|
44
|
+
## Constants
|
|
37
45
|
|
|
38
46
|
```ruby
|
|
39
47
|
RobotLab::MCP::Server::VALID_TRANSPORT_TYPES
|
|
40
48
|
# => ["stdio", "sse", "ws", "websocket", "streamable-http", "http"]
|
|
49
|
+
|
|
50
|
+
RobotLab::MCP::Server::DEFAULT_TIMEOUT
|
|
51
|
+
# => 15 (seconds)
|
|
41
52
|
```
|
|
42
53
|
|
|
43
54
|
## Attributes
|
|
@@ -58,6 +69,14 @@ server.transport # => Hash
|
|
|
58
69
|
|
|
59
70
|
The normalized transport configuration hash (keys are symbols, type is downcased).
|
|
60
71
|
|
|
72
|
+
### timeout
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
server.timeout # => Numeric
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Request timeout in seconds. Defaults to `DEFAULT_TIMEOUT` (15). Values >= 1000 passed to the constructor are auto-converted from milliseconds to seconds. The minimum is 1 second.
|
|
79
|
+
|
|
61
80
|
## Methods
|
|
62
81
|
|
|
63
82
|
### transport_type
|
|
@@ -71,10 +90,10 @@ Returns the transport type string (e.g., `"stdio"`, `"ws"`, `"sse"`).
|
|
|
71
90
|
### to_h
|
|
72
91
|
|
|
73
92
|
```ruby
|
|
74
|
-
server.to_h # => { name: "...", transport: { ... } }
|
|
93
|
+
server.to_h # => { name: "...", transport: { ... }, timeout: 15 }
|
|
75
94
|
```
|
|
76
95
|
|
|
77
|
-
Converts the server configuration to a hash representation.
|
|
96
|
+
Converts the server configuration to a hash representation (includes `timeout`).
|
|
78
97
|
|
|
79
98
|
## Transport Configuration Options
|
|
80
99
|
|
data/docs/api/mcp/transports.md
CHANGED
|
@@ -21,7 +21,10 @@ All transports inherit from `RobotLab::MCP::Transports::Base` and implement:
|
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
23
|
class RobotLab::MCP::Transports::Base
|
|
24
|
-
|
|
24
|
+
DEFAULT_TIMEOUT = 15 # seconds
|
|
25
|
+
|
|
26
|
+
attr_reader :config # => Hash (symbolized keys, :timeout removed)
|
|
27
|
+
attr_reader :timeout # => Numeric (seconds, extracted from config)
|
|
25
28
|
|
|
26
29
|
def connect # Establish connection, returns self
|
|
27
30
|
def send_request(message) # Send JSON-RPC message, returns Hash response
|
|
@@ -30,11 +33,13 @@ class RobotLab::MCP::Transports::Base
|
|
|
30
33
|
end
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
The `timeout` is extracted from the config hash during initialization (and removed from `config`). If not provided, it defaults to `DEFAULT_TIMEOUT` (15 seconds). The timeout is propagated from `MCP::Server` through `MCP::Client` to the transport.
|
|
37
|
+
|
|
33
38
|
## Stdio Transport
|
|
34
39
|
|
|
35
40
|
**Class:** `RobotLab::MCP::Transports::Stdio`
|
|
36
41
|
|
|
37
|
-
Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP `initialize` and `notifications/initialized` on connect.
|
|
42
|
+
Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP `initialize` and `notifications/initialized` on connect. All blocking I/O is wrapped with `Timeout.timeout` so a missing or hung server cannot block the caller forever.
|
|
38
43
|
|
|
39
44
|
### Configuration
|
|
40
45
|
|
|
@@ -43,7 +48,8 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
43
48
|
type: "stdio",
|
|
44
49
|
command: "mcp-server-filesystem", # Required: executable command
|
|
45
50
|
args: ["--root", "/data"], # Optional: command arguments
|
|
46
|
-
env: { "DEBUG" => "true" }
|
|
51
|
+
env: { "DEBUG" => "true" }, # Optional: environment variables
|
|
52
|
+
timeout: 10 # Optional: request timeout in seconds (default: 15)
|
|
47
53
|
}
|
|
48
54
|
```
|
|
49
55
|
|
|
@@ -52,14 +58,18 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
52
58
|
| `command` | `String` | Yes | Executable command to spawn |
|
|
53
59
|
| `args` | `Array<String>` | No | Command arguments |
|
|
54
60
|
| `env` | `Hash` | No | Environment variables (merged with current env) |
|
|
61
|
+
| `timeout` | `Numeric` | No | Request timeout in seconds (default: 15) |
|
|
55
62
|
|
|
56
63
|
### Behavior
|
|
57
64
|
|
|
58
65
|
- Uses `Open3.popen3` to spawn the subprocess
|
|
66
|
+
- Verifies the process actually started (raises `MCPError` if it exits immediately)
|
|
59
67
|
- Writes JSON-RPC messages to stdin (one per line)
|
|
60
68
|
- Reads responses from stdout, skipping notifications (messages without `id`)
|
|
69
|
+
- All blocking reads are wrapped with `Timeout.timeout` — raises `MCPError` if the server does not respond within the timeout period
|
|
61
70
|
- `connected?` returns `true` when the subprocess is alive
|
|
62
|
-
- `close`
|
|
71
|
+
- `close` calls `cleanup_process` to reliably close stdin, stdout, stderr and kill the subprocess
|
|
72
|
+
- Handles `Errno::ENOENT` (command not found), `Errno::EPIPE` / `IOError` (broken pipe / connection lost), and `Timeout::Error` (hung server) with clear error messages
|
|
63
73
|
|
|
64
74
|
### Example
|
|
65
75
|
|
|
@@ -67,7 +77,8 @@ Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (o
|
|
|
67
77
|
transport = RobotLab::MCP::Transports::Stdio.new(
|
|
68
78
|
command: "mcp-server-filesystem",
|
|
69
79
|
args: ["--root", "/data"],
|
|
70
|
-
env: { "DEBUG" => "true" }
|
|
80
|
+
env: { "DEBUG" => "true" },
|
|
81
|
+
timeout: 10
|
|
71
82
|
)
|
|
72
83
|
|
|
73
84
|
transport.connect
|
|
@@ -243,7 +254,11 @@ end
|
|
|
243
254
|
Specific error cases:
|
|
244
255
|
- **Not connected** -- calling `send_request` before `connect` raises `MCPError`
|
|
245
256
|
- **Missing gem** -- WebSocket, SSE, and HTTP transports raise `MCPError` with a `LoadError` message if required gems are not installed
|
|
246
|
-
- **No response** -- Stdio transport raises `MCPError` if the subprocess produces no output
|
|
257
|
+
- **No response** -- Stdio transport raises `MCPError` if the subprocess produces no output (EOF on stdout)
|
|
258
|
+
- **Command not found** -- Stdio transport raises `MCPError` with the original `Errno::ENOENT` message
|
|
259
|
+
- **Timeout** -- Stdio transport raises `MCPError` if the server does not respond within the configured timeout
|
|
260
|
+
- **Broken pipe** -- Stdio transport raises `MCPError` and marks itself disconnected on `Errno::EPIPE` or `IOError`
|
|
261
|
+
- **Immediate exit** -- Stdio transport raises `MCPError` if the server process exits immediately after spawn
|
|
247
262
|
|
|
248
263
|
## See Also
|
|
249
264
|
|
|
@@ -9,7 +9,7 @@ A Robot is the primary unit of computation in RobotLab. It is a subclass of `Rub
|
|
|
9
9
|
- A unique identity (name, description)
|
|
10
10
|
- A personality (system prompt and/or template)
|
|
11
11
|
- Capabilities (tools, MCP connections)
|
|
12
|
-
- Model and inference configuration
|
|
12
|
+
- Model, provider, and inference configuration
|
|
13
13
|
- Inherent memory (key-value store)
|
|
14
14
|
|
|
15
15
|
### Robot Anatomy
|
|
@@ -164,7 +164,8 @@ def build_result(response, _memory)
|
|
|
164
164
|
robot_name: @name,
|
|
165
165
|
output: output,
|
|
166
166
|
tool_calls: normalize_tool_calls(tool_calls),
|
|
167
|
-
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil
|
|
167
|
+
stop_reason: response.respond_to?(:stop_reason) ? response.stop_reason : nil,
|
|
168
|
+
raw: response
|
|
168
169
|
)
|
|
169
170
|
end
|
|
170
171
|
```
|
|
@@ -182,9 +183,12 @@ result.tool_calls # => [ToolResultMessage, ...]
|
|
|
182
183
|
result.stop_reason # => "stop" or nil
|
|
183
184
|
result.created_at # => Time
|
|
184
185
|
result.id # => UUID string
|
|
186
|
+
result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
|
|
187
|
+
result.raw # => raw LLM response object
|
|
185
188
|
|
|
186
189
|
# Convenience methods
|
|
187
190
|
result.last_text_content # => "Hi there!" (last text message content)
|
|
191
|
+
result.reply # => alias for last_text_content
|
|
188
192
|
result.has_tool_calls? # => false
|
|
189
193
|
result.stopped? # => true
|
|
190
194
|
```
|
|
@@ -275,17 +279,31 @@ sequenceDiagram
|
|
|
275
279
|
Robot-->>SF: result.continue(robot_result)
|
|
276
280
|
```
|
|
277
281
|
|
|
278
|
-
The `Task` wrapper deep-merges per-task configuration (context, mcp, tools) before delegating to the robot's `call`. The base `Robot#call` extracts the message and
|
|
282
|
+
The `Task` wrapper deep-merges per-task configuration (context, mcp, tools) before delegating to the robot's `call`. The base `Robot#call` extracts the message, calls `run`, and records the elapsed time in `RobotResult#duration`. If the robot raises any exception, the error is caught and wrapped in a `RobotResult` so one failing robot does not crash the entire pipeline:
|
|
279
283
|
|
|
280
284
|
```ruby
|
|
281
285
|
def call(result)
|
|
282
286
|
run_context = extract_run_context(result)
|
|
283
287
|
message = run_context.delete(:message)
|
|
288
|
+
|
|
289
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
284
290
|
robot_result = run(message, **run_context)
|
|
291
|
+
robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
285
292
|
|
|
286
293
|
result
|
|
287
294
|
.with_context(@name.to_sym, robot_result)
|
|
288
295
|
.continue(robot_result)
|
|
296
|
+
rescue Exception => e
|
|
297
|
+
# Error is wrapped in a RobotResult with the elapsed duration
|
|
298
|
+
error_result = RobotResult.new(
|
|
299
|
+
robot_name: @name,
|
|
300
|
+
output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.message}")]
|
|
301
|
+
)
|
|
302
|
+
error_result.duration = elapsed
|
|
303
|
+
|
|
304
|
+
result
|
|
305
|
+
.with_context(@name.to_sym, error_result)
|
|
306
|
+
.continue(error_result)
|
|
289
307
|
end
|
|
290
308
|
```
|
|
291
309
|
|
data/docs/concepts.md
CHANGED
|
@@ -12,6 +12,7 @@ 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
|
+
- **Provider**: Optional LLM provider for local models (Ollama, GPUStack, etc.)
|
|
15
16
|
- **Skills**: Composable template behaviors prepended before the main template
|
|
16
17
|
- **Local Tools**: `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances (with automatic error handling)
|
|
17
18
|
- **Streaming**: Real-time content via stored `on_content` callback or per-call block
|
|
@@ -195,12 +196,15 @@ tool = RobotLab::Tool.create(
|
|
|
195
196
|
result = robot.run("Hello!")
|
|
196
197
|
|
|
197
198
|
result.last_text_content # => "Hi there!" (String or nil)
|
|
199
|
+
result.reply # => alias for last_text_content
|
|
198
200
|
result.output # => [TextMessage, ...] array of output messages
|
|
199
201
|
result.tool_calls # => [] array of tool call results
|
|
200
202
|
result.robot_name # => "assistant"
|
|
201
203
|
result.stop_reason # => "end_turn" or nil
|
|
202
204
|
result.has_tool_calls? # => false
|
|
203
205
|
result.checksum # => "a1b2c3d4..." (for dedup)
|
|
206
|
+
result.duration # => Float or nil (elapsed seconds, set in pipeline execution)
|
|
207
|
+
result.raw # => raw LLM response object
|
|
204
208
|
```
|
|
205
209
|
|
|
206
210
|
## Memory
|
|
@@ -50,6 +50,21 @@ robot = RobotLab.build(
|
|
|
50
50
|
)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Provider
|
|
54
|
+
|
|
55
|
+
For local LLM providers (Ollama, GPUStack, LM Studio, etc.), use the `provider:` parameter. This tells RubyLLM to skip model validation and connect directly:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
robot = RobotLab.build(
|
|
59
|
+
name: "local_bot",
|
|
60
|
+
model: "llama3.2",
|
|
61
|
+
provider: :ollama,
|
|
62
|
+
system_prompt: "You are a helpful assistant."
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
When `provider:` is set, `assume_model_exists: true` is automatically applied. The provider is available via `robot.provider`.
|
|
67
|
+
|
|
53
68
|
### System Prompt
|
|
54
69
|
|
|
55
70
|
An inline string that defines the robot's personality and behavior:
|
|
@@ -479,10 +494,13 @@ The `run` method returns a `RobotResult` with:
|
|
|
479
494
|
|
|
480
495
|
```ruby
|
|
481
496
|
result.last_text_content # => "Hi there! How can I help?"
|
|
497
|
+
result.reply # => alias for last_text_content
|
|
482
498
|
result.output # => Array of output messages
|
|
483
499
|
result.tool_calls # => Array of tool call results
|
|
484
500
|
result.robot_name # => "assistant"
|
|
485
501
|
result.stop_reason # => stop reason from the LLM
|
|
502
|
+
result.duration # => Float (elapsed seconds, set in pipeline execution)
|
|
503
|
+
result.raw # => raw LLM response object
|
|
486
504
|
```
|
|
487
505
|
|
|
488
506
|
### With Runtime Memory
|
|
@@ -124,6 +124,7 @@ end
|
|
|
124
124
|
| `memory` | Task-specific memory |
|
|
125
125
|
| `config` | Per-task `RunConfig` (merged on top of network's config) |
|
|
126
126
|
| `depends_on` | `:none`, `[:task1]`, or `:optional` |
|
|
127
|
+
| `poller_group` | Bus delivery group label (`:default`, `:slow`, etc.) |
|
|
127
128
|
|
|
128
129
|
## Conditional Routing
|
|
129
130
|
|
|
@@ -164,6 +165,26 @@ network = RobotLab.create_network(name: "support") do
|
|
|
164
165
|
end
|
|
165
166
|
```
|
|
166
167
|
|
|
168
|
+
## Poller Groups
|
|
169
|
+
|
|
170
|
+
Each network maintains a shared `BusPoller` that serializes TypedBus deliveries on a per-robot basis: if a robot is already processing a message, new deliveries are queued and drained after the current one completes. This prevents re-entrancy without blocking other robots.
|
|
171
|
+
|
|
172
|
+
Named **poller groups** let you label tasks so slow robots are identifiable in logs and monitoring without needing separate infrastructure:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
network = RobotLab.create_network(name: "mixed_speed") do
|
|
176
|
+
# Fast robots on the default group
|
|
177
|
+
task :fetcher, fetcher_robot, depends_on: :none
|
|
178
|
+
task :summarize, summarizer, depends_on: [:fetcher]
|
|
179
|
+
|
|
180
|
+
# Slow robots with expensive LLM calls — label them :slow
|
|
181
|
+
task :analyst, analyst_robot, depends_on: [:fetcher], poller_group: :slow
|
|
182
|
+
task :writer, writer_robot, depends_on: [:analyst], poller_group: :slow
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Group labels are informational — there is no separate queue per group. In Async execution, robots naturally yield during LLM HTTP calls, so fast and slow robots interleave without explicit isolation.
|
|
187
|
+
|
|
167
188
|
## Running Networks
|
|
168
189
|
|
|
169
190
|
### Basic Run
|
|
@@ -291,6 +312,24 @@ network = RobotLab.create_network(name: "multi_analysis") do
|
|
|
291
312
|
end
|
|
292
313
|
```
|
|
293
314
|
|
|
315
|
+
### Pipeline Error Resilience
|
|
316
|
+
|
|
317
|
+
When a robot raises an exception during pipeline execution, the error is caught and wrapped in a `RobotResult` with the error message as content. This ensures one failing robot does not crash the entire network:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# If billing_robot raises an error, the network continues
|
|
321
|
+
# The error is available in the result context:
|
|
322
|
+
result = network.run(message: "Process this")
|
|
323
|
+
billing_result = result.context[:billing]
|
|
324
|
+
|
|
325
|
+
if billing_result&.last_text_content&.start_with?("Error:")
|
|
326
|
+
puts "Billing failed: #{billing_result.last_text_content}"
|
|
327
|
+
puts "Took: #{billing_result.duration}s"
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Each robot's `RobotResult` includes a `duration` field (elapsed seconds) that is set automatically during pipeline execution, even for errored results.
|
|
332
|
+
|
|
294
333
|
### Conditional Continuation
|
|
295
334
|
|
|
296
335
|
A robot can halt execution early:
|
data/docs/guides/index.md
CHANGED
|
@@ -38,6 +38,14 @@ If you're new to RobotLab, start here:
|
|
|
38
38
|
|
|
39
39
|
Share data between robots with the memory system
|
|
40
40
|
|
|
41
|
+
- [:octicons-pulse-24: **Observability & Safety**](observability.md)
|
|
42
|
+
|
|
43
|
+
Token tracking, circuit breakers, and learning accumulation
|
|
44
|
+
|
|
45
|
+
- [:material-cpu-64-bit: **Ractor Parallelism**](ractor-parallelism.md)
|
|
46
|
+
|
|
47
|
+
True CPU parallelism for tools and robot pipelines via Ruby Ractors
|
|
48
|
+
|
|
41
49
|
</div>
|
|
42
50
|
|
|
43
51
|
## Framework Integration
|
|
@@ -61,3 +69,5 @@ If you're new to RobotLab, start here:
|
|
|
61
69
|
| [Streaming](streaming.md) | Real-time responses | 5 min |
|
|
62
70
|
| [Memory](memory.md) | Shared data store | 5 min |
|
|
63
71
|
| [Rails Integration](rails-integration.md) | Rails application setup | 15 min |
|
|
72
|
+
| [Observability & Safety](observability.md) | Token tracking, circuit breaker, learning loop | 10 min |
|
|
73
|
+
| [Ractor Parallelism](ractor-parallelism.md) | CPU-parallel tools and robot pipelines | 15 min |
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Knowledge & Retrieval
|
|
2
|
+
|
|
3
|
+
Facilities for searching and retrieving knowledge from a robot's history and from external documents:
|
|
4
|
+
|
|
5
|
+
- **Chat History Search** — semantic search over accumulated conversation turns
|
|
6
|
+
- **Embedding-Based Document Store** — lightweight RAG: store arbitrary text, search by meaning
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Chat History Search
|
|
11
|
+
|
|
12
|
+
### The Problem
|
|
13
|
+
|
|
14
|
+
Long-running robots accumulate many conversation turns. When you need to recall what was discussed earlier on a specific topic, re-sending the full history wastes tokens. `search_history` gives you a focused slice of the most relevant past messages without touching the LLM.
|
|
15
|
+
|
|
16
|
+
### robot.search_history
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
results = robot.search_history(query, limit: 5)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Scores every message in the robot's conversation history against `query` using stemmed term-frequency cosine similarity (via the `classifier` gem). Returns up to `limit` `HistoryResult` objects sorted by score descending.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
results = robot.search_history("quarterly revenue", limit: 3)
|
|
26
|
+
|
|
27
|
+
results.each do |r|
|
|
28
|
+
puts "[#{r.role}] score=#{r.score.round(3)} idx=#{r.index}"
|
|
29
|
+
puts " #{r.text}"
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### HistoryResult Fields
|
|
34
|
+
|
|
35
|
+
| Field | Type | Description |
|
|
36
|
+
|-------|------|-------------|
|
|
37
|
+
| `text` | String | The message text |
|
|
38
|
+
| `role` | Symbol | `:user`, `:assistant`, or `:system` |
|
|
39
|
+
| `score` | Float (0.0–1.0) | Cosine similarity with the query |
|
|
40
|
+
| `index` | Integer | Position in `@chat.messages` |
|
|
41
|
+
|
|
42
|
+
### Typical Scores
|
|
43
|
+
|
|
44
|
+
| Relationship | Typical Score |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Direct answer to the query | 0.50 – 0.80 |
|
|
47
|
+
| Same topic, different phrasing | 0.20 – 0.50 |
|
|
48
|
+
| Unrelated | < 0.10 |
|
|
49
|
+
|
|
50
|
+
### Short Messages
|
|
51
|
+
|
|
52
|
+
Messages shorter than 20 characters are skipped — they produce no meaningful term vector.
|
|
53
|
+
|
|
54
|
+
### Full Example
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
robot = RobotLab.build(name: "analyst", system_prompt: "You are a financial analyst.")
|
|
58
|
+
|
|
59
|
+
# … after several robot.run() calls …
|
|
60
|
+
|
|
61
|
+
hits = robot.search_history("customer acquisition cost")
|
|
62
|
+
hits.each { |r| puts "#{r.role} (#{r.score.round(2)}): #{r.text}" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### RAG Pattern — Retrieve Then Generate
|
|
66
|
+
|
|
67
|
+
Use `search_history` to inject only the relevant past context into the next call:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
hits = robot.search_history(user_query, limit: 3)
|
|
71
|
+
context = hits.map(&:text).join("\n")
|
|
72
|
+
|
|
73
|
+
robot.run("Recall context:\n#{context}\n\nNew question: #{user_query}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Optional Dependency
|
|
77
|
+
|
|
78
|
+
`search_history` requires the `classifier` gem:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
gem "classifier", "~> 2.3"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Without it, calling `search_history` raises `RobotLab::DependencyError` with an install hint.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Embedding-Based Document Store
|
|
89
|
+
|
|
90
|
+
### The Problem
|
|
91
|
+
|
|
92
|
+
Sometimes the knowledge you need isn't in the conversation history — it's in a README, a product spec, a changelog. `store_document` / `search_documents` embed arbitrary text with `fastembed` and retrieve the most relevant chunk at query time.
|
|
93
|
+
|
|
94
|
+
### memory.store_document / memory.search_documents
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
memory.store_document(:readme, File.read("README.md"))
|
|
98
|
+
memory.store_document(:changelog, File.read("CHANGELOG.md"))
|
|
99
|
+
|
|
100
|
+
hits = memory.search_documents("how to configure redis", limit: 3)
|
|
101
|
+
hits.each { |h| puts "#{h[:key]} (#{h[:score].round(3)}): #{h[:text][0..80]}" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Each result hash contains:
|
|
105
|
+
|
|
106
|
+
| Key | Type | Description |
|
|
107
|
+
|-----|------|-------------|
|
|
108
|
+
| `:key` | Symbol | The key the document was stored under |
|
|
109
|
+
| `:text` | String | The full stored text |
|
|
110
|
+
| `:score` | Float (0.0–1.0) | Cosine similarity with the query |
|
|
111
|
+
|
|
112
|
+
### Standalone DocumentStore
|
|
113
|
+
|
|
114
|
+
The `Memory` methods delegate to `RobotLab::DocumentStore`, which can also be used directly:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
store = RobotLab::DocumentStore.new
|
|
118
|
+
store.store(:doc_a, "Ruby on Rails is a full-stack web framework.")
|
|
119
|
+
store.store(:doc_b, "Postgres is an advanced relational database.")
|
|
120
|
+
|
|
121
|
+
results = store.search("relational database SQL", limit: 2)
|
|
122
|
+
puts results.first[:key] # => :doc_b
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Management methods:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
store.size # => 2
|
|
129
|
+
store.keys # => [:doc_a, :doc_b]
|
|
130
|
+
store.empty? # => false
|
|
131
|
+
store.delete(:doc_a)
|
|
132
|
+
store.clear
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Embedding Model
|
|
136
|
+
|
|
137
|
+
Default: `BAAI/bge-small-en-v1.5` (~23 MB, downloaded on first use, cached in `~/.cache/fastembed/`).
|
|
138
|
+
|
|
139
|
+
Documents are embedded with a `"passage: "` prefix and queries with `"query: "` prefix — the standard retrieval convention for BGE models.
|
|
140
|
+
|
|
141
|
+
Custom model:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
store = RobotLab::DocumentStore.new(model_name: "BAAI/bge-base-en-v1.5")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### RAG Pattern
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# 1. Index your knowledge base at startup
|
|
151
|
+
memory.store_document(:readme, File.read("README.md"))
|
|
152
|
+
memory.store_document(:changelog, File.read("CHANGELOG.md"))
|
|
153
|
+
memory.store_document(:api_docs, File.read("docs/api.md"))
|
|
154
|
+
|
|
155
|
+
# 2. At query time, retrieve the most relevant chunks
|
|
156
|
+
hits = memory.search_documents(user_query, limit: 3)
|
|
157
|
+
context = hits.map { |h| h[:text] }.join("\n\n")
|
|
158
|
+
|
|
159
|
+
# 3. Pass context to your robot
|
|
160
|
+
result = robot.run("Use the following context:\n#{context}\n\nQuestion: #{user_query}")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Memory API Summary
|
|
164
|
+
|
|
165
|
+
| Method | Description |
|
|
166
|
+
|--------|-------------|
|
|
167
|
+
| `memory.store_document(key, text)` | Embed and store a document |
|
|
168
|
+
| `memory.search_documents(query, limit: 5)` | Search by semantic similarity |
|
|
169
|
+
| `memory.document_keys` | List stored keys |
|
|
170
|
+
| `memory.delete_document(key)` | Remove a document |
|
|
171
|
+
|
|
172
|
+
### Dependency
|
|
173
|
+
|
|
174
|
+
`fastembed` is a core RobotLab dependency — no optional gem required. The ONNX model is downloaded on first use.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## See Also
|
|
179
|
+
|
|
180
|
+
- [Observability Guide](observability.md)
|
|
181
|
+
- [Example 25 — Chat History Search](../../examples/25_history_search.rb)
|
|
182
|
+
- [Example 26 — Embedding-Based Document Store](../../examples/26_document_store.rb)
|