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,314 @@
|
|
|
1
|
+
# Streaming Responses
|
|
2
|
+
|
|
3
|
+
Stream LLM responses in real-time for better user experience.
|
|
4
|
+
|
|
5
|
+
## Basic Streaming
|
|
6
|
+
|
|
7
|
+
Pass a callback to receive streaming events:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
robot.run(
|
|
11
|
+
state: state,
|
|
12
|
+
network: network,
|
|
13
|
+
streaming: ->(event) {
|
|
14
|
+
puts event.inspect
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Event Types
|
|
20
|
+
|
|
21
|
+
### Text Deltas
|
|
22
|
+
|
|
23
|
+
Receive text as it's generated:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
streaming: ->(event) {
|
|
27
|
+
if event[:event] == "delta"
|
|
28
|
+
print event[:data][:content]
|
|
29
|
+
end
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Tool Calls
|
|
34
|
+
|
|
35
|
+
Know when tools are being called:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
streaming: ->(event) {
|
|
39
|
+
case event[:event]
|
|
40
|
+
when "tool_call.start"
|
|
41
|
+
puts "\nCalling: #{event[:data][:name]}"
|
|
42
|
+
when "tool_call.complete"
|
|
43
|
+
puts "Done: #{event[:data][:result]}"
|
|
44
|
+
end
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Lifecycle Events
|
|
49
|
+
|
|
50
|
+
Track execution lifecycle:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
streaming: ->(event) {
|
|
54
|
+
case event[:event]
|
|
55
|
+
when "run.started"
|
|
56
|
+
puts "Starting run #{event[:data][:run_id]}"
|
|
57
|
+
when "run.completed"
|
|
58
|
+
puts "Completed!"
|
|
59
|
+
when "run.failed"
|
|
60
|
+
puts "Failed: #{event[:data][:error]}"
|
|
61
|
+
end
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Event Reference
|
|
66
|
+
|
|
67
|
+
| Event | Description | Data |
|
|
68
|
+
|-------|-------------|------|
|
|
69
|
+
| `run.started` | Network run began | `run_id`, `network` |
|
|
70
|
+
| `run.completed` | Network run finished | `run_id`, `robot_count` |
|
|
71
|
+
| `run.failed` | Error occurred | `run_id`, `error` |
|
|
72
|
+
| `delta` | Text content chunk | `content` |
|
|
73
|
+
| `tool_call.start` | Tool execution starting | `name`, `input` |
|
|
74
|
+
| `tool_call.complete` | Tool execution done | `name`, `result` |
|
|
75
|
+
|
|
76
|
+
## Streaming Context
|
|
77
|
+
|
|
78
|
+
For advanced control, use `Streaming::Context`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
context = RobotLab::Streaming::Context.new(
|
|
82
|
+
run_id: SecureRandom.uuid,
|
|
83
|
+
message_id: SecureRandom.uuid,
|
|
84
|
+
scope: "network",
|
|
85
|
+
publish: ->(event) { broadcast_to_client(event) }
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Context Properties
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
context.run_id # Unique run identifier
|
|
93
|
+
context.message_id # Unique message identifier
|
|
94
|
+
context.scope # "network" or "robot"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Publishing Events
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
context.publish_event(
|
|
101
|
+
event: "custom.event",
|
|
102
|
+
data: { key: "value" }
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Web Integration
|
|
107
|
+
|
|
108
|
+
### Rails Action Cable
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class ChatChannel < ApplicationCable::Channel
|
|
112
|
+
def receive(data)
|
|
113
|
+
state = RobotLab.create_state(message: data["message"])
|
|
114
|
+
|
|
115
|
+
network.run(
|
|
116
|
+
state: state,
|
|
117
|
+
streaming: ->(event) {
|
|
118
|
+
transmit(event)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Server-Sent Events
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
class StreamController < ApplicationController
|
|
129
|
+
include ActionController::Live
|
|
130
|
+
|
|
131
|
+
def create
|
|
132
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
133
|
+
|
|
134
|
+
state = RobotLab.create_state(message: params[:message])
|
|
135
|
+
|
|
136
|
+
network.run(
|
|
137
|
+
state: state,
|
|
138
|
+
streaming: ->(event) {
|
|
139
|
+
response.stream.write("data: #{event.to_json}\n\n")
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
ensure
|
|
143
|
+
response.stream.close
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### WebSocket
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# Using Faye WebSocket
|
|
152
|
+
ws.on :message do |msg|
|
|
153
|
+
state = RobotLab.create_state(message: msg.data)
|
|
154
|
+
|
|
155
|
+
network.run(
|
|
156
|
+
state: state,
|
|
157
|
+
streaming: ->(event) {
|
|
158
|
+
ws.send(event.to_json)
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Event Filtering
|
|
165
|
+
|
|
166
|
+
### Check Event Type
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
streaming: ->(event) {
|
|
170
|
+
return unless RobotLab::Streaming::Events.delta?(event)
|
|
171
|
+
print event[:data][:content]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Available Predicates
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Streaming::Events.lifecycle?(event) # run.started, run.completed, etc.
|
|
179
|
+
Streaming::Events.delta?(event) # Text content
|
|
180
|
+
Streaming::Events.valid?(event) # Has required fields
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Buffering
|
|
184
|
+
|
|
185
|
+
Buffer content for batch processing:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
buffer = []
|
|
189
|
+
|
|
190
|
+
streaming: ->(event) {
|
|
191
|
+
if event[:event] == "delta"
|
|
192
|
+
buffer << event[:data][:content]
|
|
193
|
+
|
|
194
|
+
# Flush every 10 chunks
|
|
195
|
+
if buffer.size >= 10
|
|
196
|
+
process_batch(buffer.join)
|
|
197
|
+
buffer.clear
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Don't forget final flush
|
|
203
|
+
process_batch(buffer.join) if buffer.any?
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Progress Tracking
|
|
207
|
+
|
|
208
|
+
Track streaming progress:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class StreamProgress
|
|
212
|
+
def initialize
|
|
213
|
+
@chars = 0
|
|
214
|
+
@tools = 0
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def handle(event)
|
|
218
|
+
case event[:event]
|
|
219
|
+
when "delta"
|
|
220
|
+
@chars += event[:data][:content].length
|
|
221
|
+
puts "\rReceived #{@chars} characters..."
|
|
222
|
+
when "tool_call.start"
|
|
223
|
+
@tools += 1
|
|
224
|
+
puts "\nTool call ##{@tools}: #{event[:data][:name]}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
progress = StreamProgress.new
|
|
230
|
+
network.run(state: state, streaming: progress.method(:handle))
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Error Handling
|
|
234
|
+
|
|
235
|
+
Handle streaming errors gracefully:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
streaming: ->(event) {
|
|
239
|
+
case event[:event]
|
|
240
|
+
when "run.failed"
|
|
241
|
+
log_error(event[:data][:error])
|
|
242
|
+
notify_user("An error occurred")
|
|
243
|
+
when "delta"
|
|
244
|
+
begin
|
|
245
|
+
broadcast(event)
|
|
246
|
+
rescue BroadcastError => e
|
|
247
|
+
# Client disconnected, but continue processing
|
|
248
|
+
logger.warn "Broadcast failed: #{e.message}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Disabling Streaming
|
|
255
|
+
|
|
256
|
+
Disable streaming when not needed:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
RobotLab.configure do |config|
|
|
260
|
+
config.streaming_enabled = false
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Or per-run
|
|
264
|
+
network.run(state: state, streaming: nil)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Best Practices
|
|
268
|
+
|
|
269
|
+
### 1. Handle All Event Types
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
streaming: ->(event) {
|
|
273
|
+
case event[:event]
|
|
274
|
+
when "delta" then handle_delta(event)
|
|
275
|
+
when "tool_call.start" then show_tool_indicator(event)
|
|
276
|
+
when "tool_call.complete" then hide_tool_indicator(event)
|
|
277
|
+
when "run.completed" then finalize_response
|
|
278
|
+
when "run.failed" then show_error(event)
|
|
279
|
+
end
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 2. Provide User Feedback
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
streaming: ->(event) {
|
|
287
|
+
case event[:event]
|
|
288
|
+
when "run.started"
|
|
289
|
+
show_typing_indicator
|
|
290
|
+
when "delta"
|
|
291
|
+
update_message(event[:data][:content])
|
|
292
|
+
when "tool_call.start"
|
|
293
|
+
show_status("Looking up information...")
|
|
294
|
+
when "run.completed"
|
|
295
|
+
hide_typing_indicator
|
|
296
|
+
end
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 3. Clean Up Resources
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
begin
|
|
304
|
+
network.run(state: state, streaming: callback)
|
|
305
|
+
ensure
|
|
306
|
+
close_stream_connection
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Next Steps
|
|
311
|
+
|
|
312
|
+
- [Building Robots](building-robots.md) - Robot creation
|
|
313
|
+
- [Creating Networks](creating-networks.md) - Network patterns
|
|
314
|
+
- [API Reference: Streaming](../api/streaming/index.md) - Complete API
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Using Tools
|
|
2
|
+
|
|
3
|
+
Tools give robots the ability to interact with external systems.
|
|
4
|
+
|
|
5
|
+
## Defining Tools
|
|
6
|
+
|
|
7
|
+
### In Robot Builder
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
robot = RobotLab.build do
|
|
11
|
+
name "assistant"
|
|
12
|
+
|
|
13
|
+
tool :get_weather do
|
|
14
|
+
description "Get current weather for a location"
|
|
15
|
+
parameter :location, type: :string, required: true
|
|
16
|
+
handler { |location:, **_| WeatherService.current(location) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Standalone Tool
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
weather_tool = RobotLab::Tool.new(
|
|
25
|
+
name: "get_weather",
|
|
26
|
+
description: "Get current weather for a location",
|
|
27
|
+
parameters: {
|
|
28
|
+
location: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "City name",
|
|
31
|
+
required: true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
handler: ->(location:, **_context) {
|
|
35
|
+
WeatherService.current(location)
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Parameter Types
|
|
41
|
+
|
|
42
|
+
### String
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
parameter :name, type: :string, required: true
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Integer
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
parameter :count, type: :integer, default: 10
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Number (Float)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
parameter :price, type: :number
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Boolean
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
parameter :active, type: :boolean, default: true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Array
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
parameter :tags, type: :array
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Enum
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
parameter :status, type: :string, enum: %w[pending active completed]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### With Description
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
parameter :query,
|
|
82
|
+
type: :string,
|
|
83
|
+
required: true,
|
|
84
|
+
description: "Search query (supports wildcards)"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Handler Patterns
|
|
88
|
+
|
|
89
|
+
### Simple Handler
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
handler { |param:, **_| do_something(param) }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### With Context Access
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
handler do |param:, robot:, network:, state:|
|
|
99
|
+
user_id = state.data[:user_id]
|
|
100
|
+
result = perform_action(param, user_id)
|
|
101
|
+
state.memory.remember("last_action", result[:id])
|
|
102
|
+
result
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Error Handling
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
handler do |id:, **_|
|
|
110
|
+
record = Record.find_by(id: id)
|
|
111
|
+
if record
|
|
112
|
+
{ success: true, data: record.to_h }
|
|
113
|
+
else
|
|
114
|
+
{ success: false, error: "Record not found" }
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
{ success: false, error: e.message }
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Async Operations
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
handler do |url:, **_|
|
|
125
|
+
# Long-running operation
|
|
126
|
+
response = HTTP.timeout(30).get(url)
|
|
127
|
+
{ status: response.status, body: response.body.to_s[0..1000] }
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Tool Return Values
|
|
132
|
+
|
|
133
|
+
### Structured Data
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
handler do |user_id:, **_|
|
|
137
|
+
user = User.find(user_id)
|
|
138
|
+
{
|
|
139
|
+
id: user.id,
|
|
140
|
+
name: user.name,
|
|
141
|
+
email: user.email,
|
|
142
|
+
created_at: user.created_at.iso8601
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Simple Values
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
handler { |**_| Time.now.to_s }
|
|
151
|
+
handler { |**_| 42 }
|
|
152
|
+
handler { |**_| true }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Lists
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
handler do |query:, **_|
|
|
159
|
+
results = Search.query(query)
|
|
160
|
+
results.map { |r| { id: r.id, title: r.title, score: r.score } }
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Tool Manifests
|
|
165
|
+
|
|
166
|
+
Wrap existing tools with modified metadata:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# Original tool
|
|
170
|
+
base_tool = RobotLab::Tool.new(
|
|
171
|
+
name: "search",
|
|
172
|
+
description: "General search",
|
|
173
|
+
handler: ->(q:, **_) { Search.query(q) }
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Customized version
|
|
177
|
+
product_search = RobotLab::ToolManifest.new(
|
|
178
|
+
tool: base_tool,
|
|
179
|
+
name: "search_products",
|
|
180
|
+
description: "Search the product catalog"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
code_search = RobotLab::ToolManifest.new(
|
|
184
|
+
tool: base_tool,
|
|
185
|
+
name: "search_code",
|
|
186
|
+
description: "Search source code"
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Tool Whitelisting
|
|
191
|
+
|
|
192
|
+
### At Robot Level
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
robot = RobotLab.build do
|
|
196
|
+
tools %w[read_file list_directory] # Only these tools
|
|
197
|
+
tools :inherit # Use network's tools
|
|
198
|
+
tools :none # No inherited tools
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### At Network Level
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
network = RobotLab.create_network do
|
|
206
|
+
tools %w[search create_issue] # Global whitelist
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Configuration Hierarchy
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Global (RobotLab.configure)
|
|
214
|
+
└── Network (tools: [...])
|
|
215
|
+
└── Robot (tools: :inherit | :none | [...])
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## MCP Tools
|
|
219
|
+
|
|
220
|
+
Use tools from MCP servers:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
network = RobotLab.create_network do
|
|
224
|
+
mcp [
|
|
225
|
+
{
|
|
226
|
+
name: "github",
|
|
227
|
+
transport: { type: "stdio", command: "mcp-server-github" }
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
# MCP tools automatically available
|
|
232
|
+
# e.g., search_repositories, create_issue, etc.
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Filtering MCP Tools
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
robot = RobotLab.build do
|
|
240
|
+
mcp :inherit # Use network's MCP servers
|
|
241
|
+
tools %w[search_repositories create_issue] # Only these MCP tools
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Common Tool Patterns
|
|
246
|
+
|
|
247
|
+
### Database Lookup
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
tool :find_user do
|
|
251
|
+
description "Find user by email or ID"
|
|
252
|
+
parameter :identifier, type: :string, required: true
|
|
253
|
+
handler do |identifier:, **_|
|
|
254
|
+
user = User.find_by(id: identifier) || User.find_by(email: identifier)
|
|
255
|
+
user ? user.to_h : { error: "User not found" }
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### API Integration
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
tool :get_stock_price do
|
|
264
|
+
description "Get current stock price"
|
|
265
|
+
parameter :symbol, type: :string, required: true
|
|
266
|
+
handler do |symbol:, **_|
|
|
267
|
+
response = HTTP.get("https://api.stocks.example/quote/#{symbol}")
|
|
268
|
+
JSON.parse(response.body)
|
|
269
|
+
rescue HTTP::Error => e
|
|
270
|
+
{ error: "Failed to fetch stock price: #{e.message}" }
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### File Operations
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
tool :read_file do
|
|
279
|
+
description "Read contents of a file"
|
|
280
|
+
parameter :path, type: :string, required: true
|
|
281
|
+
handler do |path:, **_|
|
|
282
|
+
if File.exist?(path) && File.readable?(path)
|
|
283
|
+
{ content: File.read(path), size: File.size(path) }
|
|
284
|
+
else
|
|
285
|
+
{ error: "File not found or not readable" }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### State Modification
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
tool :update_preference do
|
|
295
|
+
description "Update user preference"
|
|
296
|
+
parameter :key, type: :string, required: true
|
|
297
|
+
parameter :value, type: :string, required: true
|
|
298
|
+
handler do |key:, value:, state:, **_|
|
|
299
|
+
state.memory.remember("pref:#{key}", value)
|
|
300
|
+
{ success: true, key: key, value: value }
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Multi-Step Operations
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
tool :process_order do
|
|
309
|
+
description "Process a customer order"
|
|
310
|
+
parameter :order_id, type: :string, required: true
|
|
311
|
+
handler do |order_id:, state:, **_|
|
|
312
|
+
order = Order.find(order_id)
|
|
313
|
+
|
|
314
|
+
# Validate
|
|
315
|
+
return { error: "Invalid order" } unless order.valid?
|
|
316
|
+
|
|
317
|
+
# Process
|
|
318
|
+
result = PaymentProcessor.charge(order)
|
|
319
|
+
return { error: result[:error] } unless result[:success]
|
|
320
|
+
|
|
321
|
+
# Update
|
|
322
|
+
order.update!(status: "paid")
|
|
323
|
+
|
|
324
|
+
# Store for later reference
|
|
325
|
+
state.memory.remember("processed_order", order.id)
|
|
326
|
+
|
|
327
|
+
{ success: true, order_id: order.id, amount: order.total }
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Best Practices
|
|
333
|
+
|
|
334
|
+
### 1. Clear Descriptions
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
# Good: Specific and actionable
|
|
338
|
+
tool :search_orders do
|
|
339
|
+
description "Search customer orders by date range, status, or customer email. Returns up to 50 matching orders."
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Bad: Vague
|
|
343
|
+
tool :search do
|
|
344
|
+
description "Searches stuff"
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 2. Validate Inputs
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
handler do |email:, **_|
|
|
352
|
+
unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
|
353
|
+
return { error: "Invalid email format" }
|
|
354
|
+
end
|
|
355
|
+
# ... rest of handler
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 3. Handle Errors Gracefully
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
handler do |id:, **_|
|
|
363
|
+
result = ExternalAPI.fetch(id)
|
|
364
|
+
{ success: true, data: result }
|
|
365
|
+
rescue ExternalAPI::NotFound
|
|
366
|
+
{ success: false, error: "Resource not found", id: id }
|
|
367
|
+
rescue ExternalAPI::RateLimited => e
|
|
368
|
+
{ success: false, error: "Rate limited", retry_after: e.retry_after }
|
|
369
|
+
rescue StandardError => e
|
|
370
|
+
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 4. Return Structured Data
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# Good: Structured and consistent
|
|
378
|
+
handler do |**_|
|
|
379
|
+
{
|
|
380
|
+
success: true,
|
|
381
|
+
data: { id: 1, name: "Item" },
|
|
382
|
+
metadata: { fetched_at: Time.now.iso8601 }
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Bad: Unstructured
|
|
387
|
+
handler { |**_| "Found item with id 1 named Item" }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Next Steps
|
|
391
|
+
|
|
392
|
+
- [MCP Integration](mcp-integration.md) - External tool servers
|
|
393
|
+
- [Building Robots](building-robots.md) - Robot creation patterns
|
|
394
|
+
- [API Reference: Tool](../api/core/tool.md) - Complete API
|