robot_lab 0.0.1
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 +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.github/workflows/deploy-yard-docs.yml +52 -0
- data/CHANGELOG.md +55 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +332 -0
- data/Rakefile +67 -0
- data/docs/api/adapters/anthropic.md +121 -0
- data/docs/api/adapters/gemini.md +133 -0
- data/docs/api/adapters/index.md +104 -0
- data/docs/api/adapters/openai.md +134 -0
- data/docs/api/core/index.md +113 -0
- data/docs/api/core/memory.md +314 -0
- data/docs/api/core/network.md +291 -0
- data/docs/api/core/robot.md +273 -0
- data/docs/api/core/state.md +273 -0
- data/docs/api/core/tool.md +353 -0
- data/docs/api/history/active-record-adapter.md +195 -0
- data/docs/api/history/config.md +191 -0
- data/docs/api/history/index.md +132 -0
- data/docs/api/history/thread-manager.md +144 -0
- data/docs/api/index.md +82 -0
- data/docs/api/mcp/client.md +221 -0
- data/docs/api/mcp/index.md +111 -0
- data/docs/api/mcp/server.md +225 -0
- data/docs/api/mcp/transports.md +264 -0
- data/docs/api/messages/index.md +67 -0
- data/docs/api/messages/text-message.md +102 -0
- data/docs/api/messages/tool-call-message.md +144 -0
- data/docs/api/messages/tool-result-message.md +154 -0
- data/docs/api/messages/user-message.md +171 -0
- data/docs/api/streaming/context.md +174 -0
- data/docs/api/streaming/events.md +237 -0
- data/docs/api/streaming/index.md +108 -0
- data/docs/architecture/core-concepts.md +243 -0
- data/docs/architecture/index.md +138 -0
- data/docs/architecture/message-flow.md +320 -0
- data/docs/architecture/network-orchestration.md +216 -0
- data/docs/architecture/robot-execution.md +243 -0
- data/docs/architecture/state-management.md +323 -0
- data/docs/assets/css/custom.css +56 -0
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +216 -0
- data/docs/examples/basic-chat.md +193 -0
- data/docs/examples/index.md +129 -0
- data/docs/examples/mcp-server.md +290 -0
- data/docs/examples/multi-robot-network.md +312 -0
- data/docs/examples/rails-application.md +420 -0
- data/docs/examples/tool-usage.md +310 -0
- data/docs/getting-started/configuration.md +230 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +179 -0
- data/docs/getting-started/quick-start.md +203 -0
- data/docs/guides/building-robots.md +376 -0
- data/docs/guides/creating-networks.md +366 -0
- data/docs/guides/history.md +359 -0
- data/docs/guides/index.md +68 -0
- data/docs/guides/mcp-integration.md +356 -0
- data/docs/guides/memory.md +309 -0
- data/docs/guides/rails-integration.md +432 -0
- data/docs/guides/streaming.md +314 -0
- data/docs/guides/using-tools.md +394 -0
- data/docs/index.md +160 -0
- data/examples/01_simple_robot.rb +38 -0
- data/examples/02_tools.rb +106 -0
- data/examples/03_network.rb +103 -0
- data/examples/04_mcp.rb +219 -0
- data/examples/05_streaming.rb +124 -0
- data/examples/06_prompt_templates.rb +324 -0
- data/examples/07_network_memory.rb +329 -0
- data/examples/prompts/assistant/system.txt.erb +2 -0
- data/examples/prompts/assistant/user.txt.erb +1 -0
- data/examples/prompts/billing/system.txt.erb +7 -0
- data/examples/prompts/billing/user.txt.erb +1 -0
- data/examples/prompts/classifier/system.txt.erb +4 -0
- data/examples/prompts/classifier/user.txt.erb +1 -0
- data/examples/prompts/entity_extractor/system.txt.erb +11 -0
- data/examples/prompts/entity_extractor/user.txt.erb +3 -0
- data/examples/prompts/escalation/system.txt.erb +35 -0
- data/examples/prompts/escalation/user.txt.erb +34 -0
- data/examples/prompts/general/system.txt.erb +4 -0
- data/examples/prompts/general/user.txt.erb +1 -0
- data/examples/prompts/github_assistant/system.txt.erb +6 -0
- data/examples/prompts/github_assistant/user.txt.erb +1 -0
- data/examples/prompts/helper/system.txt.erb +1 -0
- data/examples/prompts/helper/user.txt.erb +1 -0
- data/examples/prompts/keyword_extractor/system.txt.erb +8 -0
- data/examples/prompts/keyword_extractor/user.txt.erb +3 -0
- data/examples/prompts/order_support/system.txt.erb +27 -0
- data/examples/prompts/order_support/user.txt.erb +22 -0
- data/examples/prompts/product_support/system.txt.erb +30 -0
- data/examples/prompts/product_support/user.txt.erb +32 -0
- data/examples/prompts/sentiment_analyzer/system.txt.erb +9 -0
- data/examples/prompts/sentiment_analyzer/user.txt.erb +3 -0
- data/examples/prompts/synthesizer/system.txt.erb +14 -0
- data/examples/prompts/synthesizer/user.txt.erb +15 -0
- data/examples/prompts/technical/system.txt.erb +7 -0
- data/examples/prompts/technical/user.txt.erb +1 -0
- data/examples/prompts/triage/system.txt.erb +16 -0
- data/examples/prompts/triage/user.txt.erb +17 -0
- data/lib/generators/robot_lab/install_generator.rb +78 -0
- data/lib/generators/robot_lab/robot_generator.rb +55 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +41 -0
- data/lib/generators/robot_lab/templates/migration.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/result_model.rb.tt +52 -0
- data/lib/generators/robot_lab/templates/robot.rb.tt +46 -0
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +32 -0
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +53 -0
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +40 -0
- data/lib/robot_lab/adapters/anthropic.rb +163 -0
- data/lib/robot_lab/adapters/base.rb +85 -0
- data/lib/robot_lab/adapters/gemini.rb +193 -0
- data/lib/robot_lab/adapters/openai.rb +159 -0
- data/lib/robot_lab/adapters/registry.rb +81 -0
- data/lib/robot_lab/configuration.rb +143 -0
- data/lib/robot_lab/error.rb +32 -0
- data/lib/robot_lab/errors.rb +70 -0
- data/lib/robot_lab/history/active_record_adapter.rb +146 -0
- data/lib/robot_lab/history/config.rb +115 -0
- data/lib/robot_lab/history/thread_manager.rb +93 -0
- data/lib/robot_lab/mcp/client.rb +210 -0
- data/lib/robot_lab/mcp/server.rb +84 -0
- data/lib/robot_lab/mcp/transports/base.rb +56 -0
- data/lib/robot_lab/mcp/transports/sse.rb +117 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +133 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +139 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +108 -0
- data/lib/robot_lab/memory.rb +882 -0
- data/lib/robot_lab/memory_change.rb +123 -0
- data/lib/robot_lab/message.rb +357 -0
- data/lib/robot_lab/network.rb +350 -0
- data/lib/robot_lab/rails/engine.rb +29 -0
- data/lib/robot_lab/rails/railtie.rb +42 -0
- data/lib/robot_lab/robot.rb +560 -0
- data/lib/robot_lab/robot_result.rb +205 -0
- data/lib/robot_lab/robotic_model.rb +324 -0
- data/lib/robot_lab/state_proxy.rb +188 -0
- data/lib/robot_lab/streaming/context.rb +144 -0
- data/lib/robot_lab/streaming/events.rb +95 -0
- data/lib/robot_lab/streaming/sequence_counter.rb +48 -0
- data/lib/robot_lab/task.rb +117 -0
- data/lib/robot_lab/tool.rb +223 -0
- data/lib/robot_lab/tool_config.rb +112 -0
- data/lib/robot_lab/tool_manifest.rb +234 -0
- data/lib/robot_lab/user_message.rb +118 -0
- data/lib/robot_lab/version.rb +5 -0
- data/lib/robot_lab/waiter.rb +73 -0
- data/lib/robot_lab.rb +195 -0
- data/mkdocs.yml +214 -0
- data/sig/robot_lab.rbs +4 -0
- metadata +442 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Creating Networks
|
|
2
|
+
|
|
3
|
+
Networks orchestrate multiple robots using [SimpleFlow](https://github.com/MadBomber/simple_flow) pipelines with DAG-based execution and optional task activation.
|
|
4
|
+
|
|
5
|
+
## Basic Network
|
|
6
|
+
|
|
7
|
+
Create a network with a sequential pipeline:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
network = RobotLab.create_network(name: "pipeline") do
|
|
11
|
+
task :analyzer, analyzer_robot, depends_on: :none
|
|
12
|
+
task :writer, writer_robot, depends_on: [:analyzer]
|
|
13
|
+
task :reviewer, reviewer_robot, depends_on: [:writer]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
result = network.run(message: "Analyze this document")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Network Properties
|
|
20
|
+
|
|
21
|
+
### Name
|
|
22
|
+
|
|
23
|
+
Identifies the network for logging and debugging:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
network = RobotLab.create_network(name: "customer_service") do
|
|
27
|
+
# ...
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Concurrency
|
|
32
|
+
|
|
33
|
+
Control parallel execution mode:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
network = RobotLab.create_network(name: "parallel", concurrency: :threads) do
|
|
37
|
+
# :auto (default), :threads, or :async
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Adding Tasks
|
|
42
|
+
|
|
43
|
+
### Sequential Tasks
|
|
44
|
+
|
|
45
|
+
Each task depends on the previous:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
network = RobotLab.create_network(name: "pipeline") do
|
|
49
|
+
task :first, robot1, depends_on: :none
|
|
50
|
+
task :second, robot2, depends_on: [:first]
|
|
51
|
+
task :third, robot3, depends_on: [:second]
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Parallel Tasks
|
|
56
|
+
|
|
57
|
+
Tasks with the same dependencies run in parallel:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
network = RobotLab.create_network(name: "parallel_analysis") do
|
|
61
|
+
task :fetch, fetcher, depends_on: :none
|
|
62
|
+
|
|
63
|
+
# These run in parallel after :fetch
|
|
64
|
+
task :sentiment, sentiment_bot, depends_on: [:fetch]
|
|
65
|
+
task :entities, entity_bot, depends_on: [:fetch]
|
|
66
|
+
task :keywords, keyword_bot, depends_on: [:fetch]
|
|
67
|
+
|
|
68
|
+
# This waits for all three to complete
|
|
69
|
+
task :merge, merger, depends_on: [:sentiment, :entities, :keywords]
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Optional Tasks
|
|
74
|
+
|
|
75
|
+
Optional tasks only run when explicitly activated:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
network = RobotLab.create_network(name: "router") do
|
|
79
|
+
task :classifier, classifier_robot, depends_on: :none
|
|
80
|
+
task :billing, billing_robot, depends_on: :optional
|
|
81
|
+
task :technical, technical_robot, depends_on: :optional
|
|
82
|
+
task :general, general_robot, depends_on: :optional
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Per-Task Configuration
|
|
87
|
+
|
|
88
|
+
Tasks can have individual context and configuration that's deep-merged with the network's run parameters:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
network = RobotLab.create_network(name: "support") do
|
|
92
|
+
task :classifier, classifier_robot, depends_on: :none
|
|
93
|
+
task :billing, billing_robot,
|
|
94
|
+
context: { department: "billing", escalation_level: 2 },
|
|
95
|
+
depends_on: :optional
|
|
96
|
+
task :technical, technical_robot,
|
|
97
|
+
context: { department: "technical" },
|
|
98
|
+
tools: [DebugTool, LogTool],
|
|
99
|
+
depends_on: :optional
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Task Options
|
|
104
|
+
|
|
105
|
+
| Option | Description |
|
|
106
|
+
|--------|-------------|
|
|
107
|
+
| `context` | Hash merged with run params (task values override) |
|
|
108
|
+
| `mcp` | MCP servers for this task |
|
|
109
|
+
| `tools` | Tools available to this task |
|
|
110
|
+
| `memory` | Task-specific memory |
|
|
111
|
+
| `depends_on` | `:none`, `[:task1]`, or `:optional` |
|
|
112
|
+
|
|
113
|
+
## Conditional Routing
|
|
114
|
+
|
|
115
|
+
Use optional tasks with custom Robot subclasses for intelligent routing:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class ClassifierRobot < RobotLab::Robot
|
|
119
|
+
def call(result)
|
|
120
|
+
robot_result = run(**extract_run_context(result))
|
|
121
|
+
|
|
122
|
+
new_result = result
|
|
123
|
+
.with_context(@name.to_sym, robot_result)
|
|
124
|
+
.continue(robot_result)
|
|
125
|
+
|
|
126
|
+
# Activate appropriate specialist based on classification
|
|
127
|
+
category = robot_result.last_text_content.to_s.strip.downcase
|
|
128
|
+
|
|
129
|
+
case category
|
|
130
|
+
when /billing/ then new_result.activate(:billing)
|
|
131
|
+
when /technical/ then new_result.activate(:technical)
|
|
132
|
+
else new_result.activate(:general)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
classifier = ClassifierRobot.new(
|
|
138
|
+
name: "classifier",
|
|
139
|
+
system_prompt: "Classify as: billing, technical, or general. Respond with one word."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
network = RobotLab.create_network(name: "support") do
|
|
143
|
+
task :classifier, classifier, depends_on: :none
|
|
144
|
+
task :billing, billing_robot, depends_on: :optional
|
|
145
|
+
task :technical, technical_robot, depends_on: :optional
|
|
146
|
+
task :general, general_robot, depends_on: :optional
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Running Networks
|
|
151
|
+
|
|
152
|
+
### Basic Run
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
result = network.run(message: "Help me with my order")
|
|
156
|
+
|
|
157
|
+
# Get the final response
|
|
158
|
+
puts result.value.last_text_content
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### With Additional Context
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
result = network.run(
|
|
165
|
+
message: "Check my order status",
|
|
166
|
+
customer_id: 123,
|
|
167
|
+
order_id: "ORD-456"
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Accessing Task Results
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
result = network.run(message: "Process this")
|
|
175
|
+
|
|
176
|
+
# Access individual robot results
|
|
177
|
+
classifier_result = result.context[:classifier]
|
|
178
|
+
billing_result = result.context[:billing]
|
|
179
|
+
|
|
180
|
+
# Original run parameters
|
|
181
|
+
original_params = result.context[:run_params]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## SimpleFlow::Result
|
|
185
|
+
|
|
186
|
+
Networks return a `SimpleFlow::Result` object:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
result = network.run(message: "Hello")
|
|
190
|
+
|
|
191
|
+
result.value # The final task's output (RobotResult)
|
|
192
|
+
result.context # Hash of all task results and metadata
|
|
193
|
+
result.halted? # Whether execution was halted early
|
|
194
|
+
result.continued? # Whether execution continued normally
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Patterns
|
|
198
|
+
|
|
199
|
+
### Classifier Pattern
|
|
200
|
+
|
|
201
|
+
Route to specialists based on classification:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
class SupportClassifier < RobotLab::Robot
|
|
205
|
+
def call(result)
|
|
206
|
+
robot_result = run(**extract_run_context(result))
|
|
207
|
+
new_result = result
|
|
208
|
+
.with_context(@name.to_sym, robot_result)
|
|
209
|
+
.continue(robot_result)
|
|
210
|
+
|
|
211
|
+
category = robot_result.last_text_content.to_s.strip.downcase
|
|
212
|
+
new_result.activate(category.to_sym)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
network = RobotLab.create_network(name: "support") do
|
|
217
|
+
task :classifier, SupportClassifier.new(name: "classifier", template: :classifier),
|
|
218
|
+
depends_on: :none
|
|
219
|
+
task :billing, billing_robot, depends_on: :optional
|
|
220
|
+
task :technical, technical_robot, depends_on: :optional
|
|
221
|
+
task :general, general_robot, depends_on: :optional
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Pipeline Pattern
|
|
226
|
+
|
|
227
|
+
Process through sequential stages:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
network = RobotLab.create_network(name: "document_processor") do
|
|
231
|
+
task :extract, extractor, depends_on: :none
|
|
232
|
+
task :analyze, analyzer, depends_on: [:extract]
|
|
233
|
+
task :format, formatter, depends_on: [:analyze]
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Fan-Out/Fan-In Pattern
|
|
238
|
+
|
|
239
|
+
Parallel processing with aggregation:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
network = RobotLab.create_network(name: "multi_analysis") do
|
|
243
|
+
task :prepare, preparer, depends_on: :none
|
|
244
|
+
|
|
245
|
+
# Fan-out: parallel analysis
|
|
246
|
+
task :sentiment, sentiment_analyzer, depends_on: [:prepare]
|
|
247
|
+
task :topics, topic_extractor, depends_on: [:prepare]
|
|
248
|
+
task :entities, entity_recognizer, depends_on: [:prepare]
|
|
249
|
+
|
|
250
|
+
# Fan-in: aggregate results
|
|
251
|
+
task :aggregate, aggregator, depends_on: [:sentiment, :topics, :entities]
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Conditional Continuation
|
|
256
|
+
|
|
257
|
+
A robot can halt execution early:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
class ValidatorRobot < RobotLab::Robot
|
|
261
|
+
def call(result)
|
|
262
|
+
robot_result = run(**extract_run_context(result))
|
|
263
|
+
|
|
264
|
+
if robot_result.last_text_content.include?("INVALID")
|
|
265
|
+
# Stop the pipeline
|
|
266
|
+
result.halt(robot_result)
|
|
267
|
+
else
|
|
268
|
+
# Continue to next task
|
|
269
|
+
result
|
|
270
|
+
.with_context(@name.to_sym, robot_result)
|
|
271
|
+
.continue(robot_result)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Visualization
|
|
278
|
+
|
|
279
|
+
### ASCII Visualization
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
puts network.visualize
|
|
283
|
+
# => ASCII representation of the pipeline
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Mermaid Diagram
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
puts network.to_mermaid
|
|
290
|
+
# => Mermaid graph definition
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Execution Plan
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
puts network.execution_plan
|
|
297
|
+
# => Description of execution order
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Network Introspection
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
network.name # => "support"
|
|
304
|
+
network.robots # => Hash of name => Robot
|
|
305
|
+
network.robot(:billing) # => Robot instance
|
|
306
|
+
network["billing"] # => Robot instance (alias)
|
|
307
|
+
network.available_robots # => Array of Robot instances
|
|
308
|
+
network.to_h # => Hash representation
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Best Practices
|
|
312
|
+
|
|
313
|
+
### 1. Keep Robots Focused
|
|
314
|
+
|
|
315
|
+
Each robot should have a single responsibility:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# Good: focused robots
|
|
319
|
+
task :classify, classifier, depends_on: :none
|
|
320
|
+
task :respond, responder, depends_on: [:classify]
|
|
321
|
+
|
|
322
|
+
# Bad: one robot doing everything
|
|
323
|
+
task :do_everything, mega_robot, depends_on: :none
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 2. Use Context for Data Passing
|
|
327
|
+
|
|
328
|
+
Access previous results via context:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
class ResponderRobot < RobotLab::Robot
|
|
332
|
+
def call(result)
|
|
333
|
+
# Get classifier's output
|
|
334
|
+
classification = result.context[:classifier]&.last_text_content
|
|
335
|
+
|
|
336
|
+
# Use it in this robot's run
|
|
337
|
+
robot_result = run(
|
|
338
|
+
**extract_run_context(result),
|
|
339
|
+
classification: classification
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
result.with_context(@name.to_sym, robot_result).continue(robot_result)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 3. Handle Missing Results
|
|
348
|
+
|
|
349
|
+
Guard against missing optional task results:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
def call(result)
|
|
353
|
+
# Check if optional task ran
|
|
354
|
+
if result.context[:validator]
|
|
355
|
+
# Use validator result
|
|
356
|
+
else
|
|
357
|
+
# Handle missing validation
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Next Steps
|
|
363
|
+
|
|
364
|
+
- [Using Tools](using-tools.md) - Add capabilities to robots
|
|
365
|
+
- [Memory Guide](memory.md) - Persistent memory across runs
|
|
366
|
+
- [API Reference: Network](../api/core/network.md) - Complete API
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Conversation History
|
|
2
|
+
|
|
3
|
+
Persist and restore conversation threads across sessions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
History allows you to:
|
|
8
|
+
|
|
9
|
+
- Save conversation results to a database
|
|
10
|
+
- Restore previous conversations
|
|
11
|
+
- Continue multi-turn interactions
|
|
12
|
+
- Maintain context across sessions
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
### History Config
|
|
17
|
+
|
|
18
|
+
Configure history with callbacks:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
history_config = RobotLab::History::Config.new(
|
|
22
|
+
create_thread: ->(state:, input:, **) {
|
|
23
|
+
# Create a new thread, return thread_id
|
|
24
|
+
{ thread_id: SecureRandom.uuid }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
get: ->(thread_id:, **) {
|
|
28
|
+
# Retrieve history for thread
|
|
29
|
+
# Return Array<RobotResult>
|
|
30
|
+
[]
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
append_user_message: ->(thread_id:, message:, **) {
|
|
34
|
+
# Optional: Store user message
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
38
|
+
# Store new results
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Apply to Network
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
network = RobotLab.create_network do
|
|
47
|
+
name "persistent_chat"
|
|
48
|
+
history history_config
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Callback Reference
|
|
53
|
+
|
|
54
|
+
### create_thread
|
|
55
|
+
|
|
56
|
+
Called when starting a new conversation:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
create_thread: ->(state:, input:, **kwargs) {
|
|
60
|
+
# state - Current State object
|
|
61
|
+
# input - UserMessage or string
|
|
62
|
+
# kwargs - Additional context
|
|
63
|
+
|
|
64
|
+
thread = Thread.create!(
|
|
65
|
+
initial_input: input.to_s,
|
|
66
|
+
user_id: state.data[:user_id]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
{ thread_id: thread.id.to_s } # Must return hash with :thread_id
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### get
|
|
74
|
+
|
|
75
|
+
Called to retrieve existing history:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
get: ->(thread_id:, **kwargs) {
|
|
79
|
+
# thread_id - The thread identifier
|
|
80
|
+
# kwargs - Additional context
|
|
81
|
+
|
|
82
|
+
Result.where(thread_id: thread_id)
|
|
83
|
+
.order(:created_at)
|
|
84
|
+
.map { |r| deserialize_result(r) }
|
|
85
|
+
|
|
86
|
+
# Must return Array<RobotResult>
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### append_user_message (Optional)
|
|
91
|
+
|
|
92
|
+
Called when a user message is added:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
append_user_message: ->(thread_id:, message:, **kwargs) {
|
|
96
|
+
# thread_id - The thread identifier
|
|
97
|
+
# message - UserMessage object
|
|
98
|
+
|
|
99
|
+
Message.create!(
|
|
100
|
+
thread_id: thread_id,
|
|
101
|
+
content: message.content,
|
|
102
|
+
metadata: message.metadata
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### append_results
|
|
108
|
+
|
|
109
|
+
Called after robots finish:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
append_results: ->(thread_id:, new_results:, **kwargs) {
|
|
113
|
+
# thread_id - The thread identifier
|
|
114
|
+
# new_results - Array<RobotResult>
|
|
115
|
+
|
|
116
|
+
new_results.each do |result|
|
|
117
|
+
Result.create!(
|
|
118
|
+
thread_id: thread_id,
|
|
119
|
+
robot_name: result.robot_name,
|
|
120
|
+
output_data: serialize_output(result.output),
|
|
121
|
+
stop_reason: result.stop_reason
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## ActiveRecord Adapter
|
|
128
|
+
|
|
129
|
+
RobotLab includes a built-in ActiveRecord adapter:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
adapter = RobotLab::History::ActiveRecordAdapter.new(
|
|
133
|
+
thread_model: RobotLabThread,
|
|
134
|
+
result_model: RobotLabResult
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
network = RobotLab.create_network do
|
|
138
|
+
history adapter.to_config
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Required Models
|
|
143
|
+
|
|
144
|
+
```ruby title="app/models/robot_lab_thread.rb"
|
|
145
|
+
class RobotLabThread < ApplicationRecord
|
|
146
|
+
has_many :results, class_name: "RobotLabResult", foreign_key: :thread_id
|
|
147
|
+
|
|
148
|
+
# Required columns:
|
|
149
|
+
# - thread_id: string
|
|
150
|
+
# - initial_input: text
|
|
151
|
+
# - input_metadata: jsonb
|
|
152
|
+
# - state_data: jsonb
|
|
153
|
+
# - last_user_message: text
|
|
154
|
+
# - last_user_message_at: datetime
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ruby title="app/models/robot_lab_result.rb"
|
|
159
|
+
class RobotLabResult < ApplicationRecord
|
|
160
|
+
belongs_to :thread, class_name: "RobotLabThread", foreign_key: :thread_id
|
|
161
|
+
|
|
162
|
+
# Required columns:
|
|
163
|
+
# - thread_id: string
|
|
164
|
+
# - robot_name: string
|
|
165
|
+
# - sequence_number: integer
|
|
166
|
+
# - output_data: jsonb
|
|
167
|
+
# - tool_calls_data: jsonb
|
|
168
|
+
# - stop_reason: string
|
|
169
|
+
# - checksum: string
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Using Thread IDs
|
|
174
|
+
|
|
175
|
+
### Start New Thread
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
state = RobotLab.create_state(message: "Hello!")
|
|
179
|
+
result = network.run(state: state)
|
|
180
|
+
|
|
181
|
+
# Thread ID is assigned automatically
|
|
182
|
+
thread_id = state.thread_id
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Continue Existing Thread
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# Option 1: Via UserMessage
|
|
189
|
+
message = RobotLab::UserMessage.new(
|
|
190
|
+
"Continue our conversation",
|
|
191
|
+
thread_id: existing_thread_id
|
|
192
|
+
)
|
|
193
|
+
state = RobotLab.create_state(message: message)
|
|
194
|
+
|
|
195
|
+
# Option 2: Direct assignment
|
|
196
|
+
state = RobotLab.create_state(message: "Continue")
|
|
197
|
+
state.thread_id = existing_thread_id
|
|
198
|
+
|
|
199
|
+
# History is automatically loaded
|
|
200
|
+
result = network.run(state: state)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## ThreadManager
|
|
204
|
+
|
|
205
|
+
For programmatic control:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
manager = RobotLab::History::ThreadManager.new(history_config)
|
|
209
|
+
|
|
210
|
+
# Create thread
|
|
211
|
+
thread_id = manager.create_thread(state: state, input: message)
|
|
212
|
+
|
|
213
|
+
# Load history
|
|
214
|
+
results = manager.get_history(thread_id)
|
|
215
|
+
|
|
216
|
+
# Save state
|
|
217
|
+
manager.save_state(thread_id: thread_id, state: state, since_index: 5)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Serialization
|
|
221
|
+
|
|
222
|
+
### RobotResult
|
|
223
|
+
|
|
224
|
+
Results are serialized via `export`:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
result.export
|
|
228
|
+
# => {
|
|
229
|
+
# robot_name: "assistant",
|
|
230
|
+
# output: [...],
|
|
231
|
+
# tool_calls: [...],
|
|
232
|
+
# stop_reason: "stop",
|
|
233
|
+
# id: "...",
|
|
234
|
+
# created_at: "..."
|
|
235
|
+
# }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Messages
|
|
239
|
+
|
|
240
|
+
Messages serialize to hashes:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
message.to_h
|
|
244
|
+
# => {
|
|
245
|
+
# type: "text",
|
|
246
|
+
# role: "assistant",
|
|
247
|
+
# content: "Hello!",
|
|
248
|
+
# stop_reason: "stop"
|
|
249
|
+
# }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Restore from hash
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
RobotLab::Message.from_hash(hash)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Patterns
|
|
259
|
+
|
|
260
|
+
### Redis-Based History
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
history_config = History::Config.new(
|
|
264
|
+
create_thread: ->(state:, input:, **) {
|
|
265
|
+
thread_id = SecureRandom.uuid
|
|
266
|
+
Redis.current.hset("threads", thread_id, input.to_s)
|
|
267
|
+
{ thread_id: thread_id }
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
get: ->(thread_id:, **) {
|
|
271
|
+
data = Redis.current.lrange("results:#{thread_id}", 0, -1)
|
|
272
|
+
data.map { |json| deserialize_result(JSON.parse(json)) }
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
append_results: ->(thread_id:, new_results:, **) {
|
|
276
|
+
new_results.each do |result|
|
|
277
|
+
Redis.current.rpush("results:#{thread_id}", result.export.to_json)
|
|
278
|
+
end
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Custom Storage
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
class CustomHistoryAdapter
|
|
287
|
+
def initialize(storage)
|
|
288
|
+
@storage = storage
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def to_config
|
|
292
|
+
History::Config.new(
|
|
293
|
+
create_thread: method(:create_thread),
|
|
294
|
+
get: method(:get),
|
|
295
|
+
append_results: method(:append_results)
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private
|
|
300
|
+
|
|
301
|
+
def create_thread(state:, input:, **)
|
|
302
|
+
id = @storage.create_conversation(input: input.to_s)
|
|
303
|
+
{ thread_id: id }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def get(thread_id:, **)
|
|
307
|
+
@storage.fetch_results(thread_id)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def append_results(thread_id:, new_results:, **)
|
|
311
|
+
@storage.store_results(thread_id, new_results)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Best Practices
|
|
317
|
+
|
|
318
|
+
### 1. Handle Missing Threads
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
get: ->(thread_id:, **) {
|
|
322
|
+
thread = Thread.find_by(thread_id: thread_id)
|
|
323
|
+
return [] unless thread
|
|
324
|
+
|
|
325
|
+
thread.results.order(:created_at).map(&:to_robot_result)
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 2. Index for Performance
|
|
330
|
+
|
|
331
|
+
```sql
|
|
332
|
+
CREATE INDEX idx_results_thread_id ON robot_lab_results(thread_id);
|
|
333
|
+
CREATE INDEX idx_results_created_at ON robot_lab_results(created_at);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 3. Clean Up Old Threads
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Periodic cleanup job
|
|
340
|
+
Thread.where("updated_at < ?", 30.days.ago).destroy_all
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 4. Limit History Size
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
get: ->(thread_id:, **) {
|
|
347
|
+
Result.where(thread_id: thread_id)
|
|
348
|
+
.order(created_at: :desc)
|
|
349
|
+
.limit(50) # Last 50 exchanges
|
|
350
|
+
.reverse
|
|
351
|
+
.map(&:to_robot_result)
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Next Steps
|
|
356
|
+
|
|
357
|
+
- [Memory System](memory.md) - In-memory data sharing
|
|
358
|
+
- [State Management](../architecture/state-management.md) - State details
|
|
359
|
+
- [API Reference: History](../api/history/index.md) - Complete API
|