robot_lab 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +90 -0
  5. data/README.md +203 -46
  6. data/Rakefile +70 -1
  7. data/docs/api/core/index.md +12 -0
  8. data/docs/api/core/robot.md +478 -130
  9. data/docs/api/core/tool.md +205 -209
  10. data/docs/api/history/active-record-adapter.md +174 -94
  11. data/docs/api/history/config.md +186 -93
  12. data/docs/api/history/index.md +57 -61
  13. data/docs/api/history/thread-manager.md +123 -73
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/streaming/context.md +157 -74
  19. data/docs/api/streaming/events.md +114 -166
  20. data/docs/api/streaming/index.md +74 -72
  21. data/docs/architecture/core-concepts.md +361 -112
  22. data/docs/architecture/index.md +97 -59
  23. data/docs/architecture/message-flow.md +138 -129
  24. data/docs/architecture/network-orchestration.md +197 -50
  25. data/docs/architecture/robot-execution.md +199 -146
  26. data/docs/architecture/state-management.md +255 -187
  27. data/docs/concepts.md +312 -48
  28. data/docs/examples/basic-chat.md +89 -77
  29. data/docs/examples/index.md +222 -47
  30. data/docs/examples/mcp-server.md +207 -203
  31. data/docs/examples/multi-robot-network.md +129 -35
  32. data/docs/examples/rails-application.md +159 -160
  33. data/docs/examples/tool-usage.md +295 -204
  34. data/docs/getting-started/configuration.md +275 -162
  35. data/docs/getting-started/index.md +1 -1
  36. data/docs/getting-started/installation.md +22 -13
  37. data/docs/getting-started/quick-start.md +166 -121
  38. data/docs/guides/building-robots.md +417 -212
  39. data/docs/guides/creating-networks.md +94 -24
  40. data/docs/guides/mcp-integration.md +152 -113
  41. data/docs/guides/memory.md +220 -164
  42. data/docs/guides/streaming.md +80 -110
  43. data/docs/guides/using-tools.md +259 -212
  44. data/docs/index.md +50 -37
  45. data/examples/01_simple_robot.rb +6 -9
  46. data/examples/02_tools.rb +6 -9
  47. data/examples/03_network.rb +13 -14
  48. data/examples/04_mcp.rb +5 -8
  49. data/examples/05_streaming.rb +5 -8
  50. data/examples/06_prompt_templates.rb +42 -37
  51. data/examples/07_network_memory.rb +13 -14
  52. data/examples/08_llm_config.rb +140 -0
  53. data/examples/09_chaining.rb +223 -0
  54. data/examples/10_memory.rb +331 -0
  55. data/examples/11_network_introspection.rb +230 -0
  56. data/examples/12_message_bus.rb +74 -0
  57. data/examples/13_spawn.rb +90 -0
  58. data/examples/14_rusty_circuit/comic.rb +143 -0
  59. data/examples/14_rusty_circuit/display.rb +203 -0
  60. data/examples/14_rusty_circuit/heckler.rb +57 -0
  61. data/examples/14_rusty_circuit/open_mic.rb +121 -0
  62. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  63. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  64. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  65. data/examples/14_rusty_circuit/scout.rb +173 -0
  66. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  67. data/examples/14_rusty_circuit/show.log +234 -0
  68. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  71. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  72. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  73. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  74. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  75. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  76. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  77. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  78. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  79. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  80. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  81. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  82. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  83. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  84. data/examples/README.md +197 -0
  85. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  86. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  87. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  88. data/examples/prompts/comedian.md +6 -0
  89. data/examples/prompts/comedy_critic.md +10 -0
  90. data/examples/prompts/configurable.md +9 -0
  91. data/examples/prompts/dispatcher.md +12 -0
  92. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  93. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  94. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  95. data/examples/prompts/frontmatter_named_test.md +5 -0
  96. data/examples/prompts/frontmatter_tools_test.md +6 -0
  97. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  98. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  99. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  100. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  101. data/examples/prompts/llm_config_demo.md +20 -0
  102. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  103. data/examples/prompts/os_advocate.md +13 -0
  104. data/examples/prompts/os_chief.md +13 -0
  105. data/examples/prompts/os_editor.md +13 -0
  106. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  107. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  108. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  109. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  110. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  111. data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
  112. data/lib/robot_lab/adapters/openai.rb +2 -1
  113. data/lib/robot_lab/ask_user.rb +75 -0
  114. data/lib/robot_lab/config/defaults.yml +121 -0
  115. data/lib/robot_lab/config.rb +183 -0
  116. data/lib/robot_lab/error.rb +6 -0
  117. data/lib/robot_lab/mcp/client.rb +1 -1
  118. data/lib/robot_lab/memory.rb +2 -2
  119. data/lib/robot_lab/robot.rb +523 -249
  120. data/lib/robot_lab/robot_message.rb +44 -0
  121. data/lib/robot_lab/robot_result.rb +1 -0
  122. data/lib/robot_lab/robotic_model.rb +1 -1
  123. data/lib/robot_lab/streaming/context.rb +1 -1
  124. data/lib/robot_lab/tool.rb +108 -172
  125. data/lib/robot_lab/tool_config.rb +1 -1
  126. data/lib/robot_lab/tool_manifest.rb +2 -18
  127. data/lib/robot_lab/version.rb +1 -1
  128. data/lib/robot_lab.rb +66 -55
  129. metadata +107 -116
  130. data/examples/prompts/assistant/user.txt.erb +0 -1
  131. data/examples/prompts/billing/user.txt.erb +0 -1
  132. data/examples/prompts/classifier/user.txt.erb +0 -1
  133. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  134. data/examples/prompts/escalation/user.txt.erb +0 -34
  135. data/examples/prompts/general/user.txt.erb +0 -1
  136. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  137. data/examples/prompts/helper/user.txt.erb +0 -1
  138. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  139. data/examples/prompts/order_support/user.txt.erb +0 -22
  140. data/examples/prompts/product_support/user.txt.erb +0 -32
  141. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  142. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  143. data/examples/prompts/technical/user.txt.erb +0 -1
  144. data/examples/prompts/triage/user.txt.erb +0 -17
  145. data/lib/robot_lab/configuration.rb +0 -143
@@ -1,256 +1,228 @@
1
1
  # Using Tools
2
2
 
3
- Tools give robots the ability to interact with external systems.
3
+ Tools give robots the ability to interact with external systems. RobotLab supports three approaches: `RubyLLM::Tool` subclasses, `RobotLab::Tool` subclasses (with robot access), and `RobotLab::Tool.create` for dynamic tools.
4
4
 
5
5
  ## Defining Tools
6
6
 
7
- ### In Robot Builder
7
+ ### RubyLLM::Tool Subclass
8
+
9
+ For reusable tools that don't need robot access:
8
10
 
9
11
  ```ruby
10
- robot = RobotLab.build do
11
- name "assistant"
12
+ class GetWeather < RubyLLM::Tool
13
+ description "Get current weather for a location"
14
+
15
+ param :location, type: :string, desc: "City name or zip code"
16
+ param :unit, type: :string, desc: "Temperature unit", required: false
12
17
 
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) }
18
+ def execute(location:, unit: "celsius")
19
+ WeatherService.current(location, unit: unit)
17
20
  end
18
21
  end
19
22
  ```
20
23
 
21
- ### Standalone Tool
24
+ ### RobotLab::Tool Subclass
22
25
 
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
- ```
26
+ For tools that need access to their owning robot (self-modification, spawning, etc.):
39
27
 
40
- ## Parameter Types
28
+ ```ruby
29
+ class AdjustEnergy < RobotLab::Tool
30
+ description "Adjust the robot's creativity level"
41
31
 
42
- ### String
32
+ param :level, type: "number", desc: "Temperature from 0.0 to 1.0"
43
33
 
44
- ```ruby
45
- parameter :name, type: :string, required: true
34
+ def execute(level:)
35
+ robot.with_temperature(level)
36
+ "Temperature adjusted to #{level}"
37
+ end
38
+ end
46
39
  ```
47
40
 
48
- ### Integer
41
+ Pass `robot: self` when constructing:
49
42
 
50
43
  ```ruby
51
- parameter :count, type: :integer, default: 10
44
+ class MyRobot < RobotLab::Robot
45
+ def initialize
46
+ super(
47
+ name: "creative_bot",
48
+ system_prompt: "You are creative.",
49
+ local_tools: [AdjustEnergy.new(robot: self)]
50
+ )
51
+ end
52
+ end
52
53
  ```
53
54
 
54
- ### Number (Float)
55
+ ### RobotLab::Tool.create (Dynamic Tools)
56
+
57
+ For quick, inline tools use the `Tool.create` factory:
55
58
 
56
59
  ```ruby
57
- parameter :price, type: :number
60
+ get_time = RobotLab::Tool.create(
61
+ name: "get_time",
62
+ description: "Get the current time"
63
+ ) { |_args| Time.now.to_s }
58
64
  ```
59
65
 
60
- ### Boolean
66
+ With parameters:
61
67
 
62
68
  ```ruby
63
- parameter :active, type: :boolean, default: true
69
+ weather_tool = RobotLab::Tool.create(
70
+ name: "get_weather",
71
+ description: "Get current weather for a location",
72
+ parameters: {
73
+ type: "object",
74
+ properties: {
75
+ location: { type: "string", description: "City name" }
76
+ },
77
+ required: ["location"]
78
+ }
79
+ ) { |args| WeatherService.current(args[:location]) }
64
80
  ```
65
81
 
66
- ### Array
82
+ ## Built-in Tools
67
83
 
68
- ```ruby
69
- parameter :tags, type: :array
70
- ```
84
+ ### AskUser
71
85
 
72
- ### Enum
86
+ `RobotLab::AskUser` lets a robot ask the user a question via the terminal. The LLM decides when it needs human input and calls the tool with a question, optional choices, and an optional default.
73
87
 
74
88
  ```ruby
75
- parameter :status, type: :string, enum: %w[pending active completed]
89
+ robot = RobotLab.build(
90
+ name: "onboarding",
91
+ system_prompt: "Walk the user through project setup. Ask questions to understand their needs.",
92
+ local_tools: [RobotLab::AskUser]
93
+ )
94
+ robot.run("Help the user set up a new project")
76
95
  ```
77
96
 
78
- ### With Description
97
+ The tool displays the robot's name and question, then waits for terminal input:
79
98
 
80
- ```ruby
81
- parameter :query,
82
- type: :string,
83
- required: true,
84
- description: "Search query (supports wildcards)"
99
+ ```
100
+ [onboarding] What programming language will you use?
101
+ 1. Ruby
102
+ 2. Python
103
+ 3. Go
104
+ >
85
105
  ```
86
106
 
87
- ## Handler Patterns
88
-
89
- ### Simple Handler
107
+ Features:
90
108
 
91
- ```ruby
92
- handler { |param:, **_| do_something(param) }
93
- ```
109
+ - **Open-ended**: just a question, free-text response
110
+ - **Multiple choice**: numbered options, user types the number or text
111
+ - **Default value**: shown in the prompt, used when user presses Enter
94
112
 
95
- ### With Context Access
113
+ IO is sourced from `robot.input` / `robot.output` (defaulting to `$stdin` / `$stdout`), making it easy to test with `StringIO`:
96
114
 
97
115
  ```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
116
+ robot.input = StringIO.new("2\n")
117
+ robot.output = StringIO.new
104
118
  ```
105
119
 
106
- ### Error Handling
120
+ See the [AskUser API reference](../api/core/tool.md#built-in-askuser) for full details.
107
121
 
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
- ```
122
+ ## Attaching Tools to Robots
120
123
 
121
- ### Async Operations
124
+ ### Via Constructor
125
+
126
+ Pass tools via the `local_tools:` parameter when building a robot:
122
127
 
123
128
  ```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
+ robot = RobotLab.build(
130
+ name: "assistant",
131
+ system_prompt: "You are a helpful assistant with tool access.",
132
+ local_tools: [GetWeather, CalculatorTool]
133
+ )
129
134
  ```
130
135
 
131
- ## Tool Return Values
136
+ ### Via Template Front Matter
132
137
 
133
- ### Structured Data
138
+ Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via `Object.const_get` and instantiates it:
134
139
 
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
140
+ ```markdown title="prompts/weather_bot.md"
141
+ ---
142
+ description: Weather assistant with forecast tools
143
+ tools:
144
+ - GetWeather
145
+ - GetForecast
146
+ ---
147
+ You are a weather assistant. Use your tools to look up weather information.
145
148
  ```
146
149
 
147
- ### Simple Values
148
-
149
150
  ```ruby
150
- handler { |**_| Time.now.to_s }
151
- handler { |**_| 42 }
152
- handler { |**_| true }
151
+ # Tools are resolved from frontmatter — no local_tools: needed
152
+ robot = RobotLab.build(template: :weather_bot)
153
153
  ```
154
154
 
155
- ### Lists
155
+ Tool classes must be defined and loaded before building the robot. Unresolvable names are skipped with a warning. Constructor `local_tools:` overrides frontmatter `tools:` when provided.
156
+
157
+ ### Via Chaining
158
+
159
+ You can also add tools dynamically with chaining:
156
160
 
157
161
  ```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
+ robot = RobotLab.build(name: "assistant", system_prompt: "...")
163
+ robot.with_tools(GetWeather, CalculatorTool)
162
164
  ```
163
165
 
164
- ## Tool Manifests
166
+ ## Parameter Types
165
167
 
166
- Wrap existing tools with modified metadata:
168
+ Define parameters on `RubyLLM::Tool` subclasses using `param`:
169
+
170
+ ### String
167
171
 
168
172
  ```ruby
169
- # Original tool
170
- base_tool = RobotLab::Tool.new(
171
- name: "search",
172
- description: "General search",
173
- handler: ->(q:, **_) { Search.query(q) }
174
- )
173
+ param :name, type: :string, desc: "User's full name"
174
+ ```
175
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
- )
176
+ ### Integer
182
177
 
183
- code_search = RobotLab::ToolManifest.new(
184
- tool: base_tool,
185
- name: "search_code",
186
- description: "Search source code"
187
- )
178
+ ```ruby
179
+ param :count, type: :integer, desc: "Number of results"
188
180
  ```
189
181
 
190
- ## Tool Whitelisting
191
-
192
- ### At Robot Level
182
+ ### Number (Float)
193
183
 
194
184
  ```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
185
+ param :price, type: :number, desc: "Price in dollars"
200
186
  ```
201
187
 
202
- ### At Network Level
188
+ ### Boolean
203
189
 
204
190
  ```ruby
205
- network = RobotLab.create_network do
206
- tools %w[search create_issue] # Global whitelist
207
- end
191
+ param :active, type: :boolean, desc: "Whether the user is active"
208
192
  ```
209
193
 
210
- ### Configuration Hierarchy
194
+ ### Array
211
195
 
196
+ ```ruby
197
+ param :tags, type: :array, desc: "List of tags"
212
198
  ```
213
- Global (RobotLab.configure)
214
- └── Network (tools: [...])
215
- └── Robot (tools: :inherit | :none | [...])
216
- ```
217
-
218
- ## MCP Tools
219
199
 
220
- Use tools from MCP servers:
200
+ ### Enum
221
201
 
222
202
  ```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
203
+ param :status, type: :string, desc: "Order status", enum: %w[pending active completed]
234
204
  ```
235
205
 
236
- ### Filtering MCP Tools
206
+ ### Required vs Optional
207
+
208
+ Parameters are required by default. Mark optional with `required: false`:
237
209
 
238
210
  ```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
211
+ param :query, type: :string, desc: "Search query" # required
212
+ param :limit, type: :integer, desc: "Max results", required: false # optional
243
213
  ```
244
214
 
245
- ## Common Tool Patterns
215
+ ## Tool Patterns
246
216
 
247
217
  ### Database Lookup
248
218
 
249
219
  ```ruby
250
- tool :find_user do
220
+ class FindUser < RubyLLM::Tool
251
221
  description "Find user by email or ID"
252
- parameter :identifier, type: :string, required: true
253
- handler do |identifier:, **_|
222
+
223
+ param :identifier, type: :string, desc: "Email address or user ID"
224
+
225
+ def execute(identifier:)
254
226
  user = User.find_by(id: identifier) || User.find_by(email: identifier)
255
227
  user ? user.to_h : { error: "User not found" }
256
228
  end
@@ -260,10 +232,12 @@ end
260
232
  ### API Integration
261
233
 
262
234
  ```ruby
263
- tool :get_stock_price do
264
- description "Get current stock price"
265
- parameter :symbol, type: :string, required: true
266
- handler do |symbol:, **_|
235
+ class GetStockPrice < RubyLLM::Tool
236
+ description "Get current stock price for a ticker symbol"
237
+
238
+ param :symbol, type: :string, desc: "Stock ticker symbol (e.g. AAPL)"
239
+
240
+ def execute(symbol:)
267
241
  response = HTTP.get("https://api.stocks.example/quote/#{symbol}")
268
242
  JSON.parse(response.body)
269
243
  rescue HTTP::Error => e
@@ -275,10 +249,12 @@ end
275
249
  ### File Operations
276
250
 
277
251
  ```ruby
278
- tool :read_file do
252
+ class ReadFile < RubyLLM::Tool
279
253
  description "Read contents of a file"
280
- parameter :path, type: :string, required: true
281
- handler do |path:, **_|
254
+
255
+ param :path, type: :string, desc: "Absolute path to the file"
256
+
257
+ def execute(path:)
282
258
  if File.exist?(path) && File.readable?(path)
283
259
  { content: File.read(path), size: File.size(path) }
284
260
  else
@@ -288,94 +264,159 @@ tool :read_file do
288
264
  end
289
265
  ```
290
266
 
291
- ### State Modification
267
+ ### Multi-Step Operations
292
268
 
293
269
  ```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
- ```
270
+ class ProcessOrder < RubyLLM::Tool
271
+ description "Validate and process a customer order"
304
272
 
305
- ### Multi-Step Operations
273
+ param :order_id, type: :string, desc: "The order ID to process"
306
274
 
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:, **_|
275
+ def execute(order_id:)
312
276
  order = Order.find(order_id)
313
277
 
314
278
  # Validate
315
279
  return { error: "Invalid order" } unless order.valid?
316
280
 
317
- # Process
281
+ # Process payment
318
282
  result = PaymentProcessor.charge(order)
319
283
  return { error: result[:error] } unless result[:success]
320
284
 
321
- # Update
285
+ # Update status
322
286
  order.update!(status: "paid")
323
287
 
324
- # Store for later reference
325
- state.memory.remember("processed_order", order.id)
326
-
327
288
  { success: true, order_id: order.id, amount: order.total }
328
289
  end
329
290
  end
330
291
  ```
331
292
 
293
+ ## Tool Return Values
294
+
295
+ ### Structured Data
296
+
297
+ Return hashes with consistent structure:
298
+
299
+ ```ruby
300
+ def execute(user_id:)
301
+ user = User.find(user_id)
302
+ {
303
+ id: user.id,
304
+ name: user.name,
305
+ email: user.email,
306
+ created_at: user.created_at.iso8601
307
+ }
308
+ end
309
+ ```
310
+
311
+ ### Simple Values
312
+
313
+ ```ruby
314
+ def execute(**_)
315
+ Time.now.to_s
316
+ end
317
+ ```
318
+
319
+ ### Lists
320
+
321
+ ```ruby
322
+ def execute(query:)
323
+ results = Search.query(query)
324
+ results.map { |r| { id: r.id, title: r.title, score: r.score } }
325
+ end
326
+ ```
327
+
328
+ ## Error Handling
329
+
330
+ Always handle errors gracefully. Return structured error information so the LLM can decide how to proceed:
331
+
332
+ ```ruby
333
+ class FetchResource < RubyLLM::Tool
334
+ description "Fetch a resource from an external API"
335
+
336
+ param :id, type: :string, desc: "Resource ID"
337
+
338
+ def execute(id:)
339
+ result = ExternalAPI.fetch(id)
340
+ { success: true, data: result }
341
+ rescue ExternalAPI::NotFound
342
+ { success: false, error: "Resource not found", id: id }
343
+ rescue ExternalAPI::RateLimited => e
344
+ { success: false, error: "Rate limited", retry_after: e.retry_after }
345
+ rescue StandardError => e
346
+ { success: false, error: "Unexpected error: #{e.message}" }
347
+ end
348
+ end
349
+ ```
350
+
351
+ ## Tool Callbacks
352
+
353
+ Robots support `on_tool_call` and `on_tool_result` callbacks for monitoring tool usage:
354
+
355
+ ```ruby
356
+ robot = RobotLab.build(
357
+ name: "assistant",
358
+ system_prompt: "...",
359
+ local_tools: [GetWeather],
360
+ on_tool_call: ->(call) { puts "Calling: #{call}" },
361
+ on_tool_result: ->(result) { puts "Result: #{result}" }
362
+ )
363
+ ```
364
+
365
+ ## RobotLab::Tool.create with Schema
366
+
367
+ For dynamic tools via `Tool.create`, pass parameters as a JSON Schema hash:
368
+
369
+ ```ruby
370
+ tool = RobotLab::Tool.create(
371
+ name: "search",
372
+ description: "Search for items",
373
+ parameters: {
374
+ type: "object",
375
+ properties: {
376
+ query: { type: "string", description: "Search query" },
377
+ limit: { type: "integer", description: "Max results" }
378
+ },
379
+ required: ["query"]
380
+ }
381
+ ) { |args| Search.query(args[:query], limit: args[:limit] || 10) }
382
+ ```
383
+
332
384
  ## Best Practices
333
385
 
334
386
  ### 1. Clear Descriptions
335
387
 
388
+ Write descriptions that help the LLM understand when and how to use the tool:
389
+
336
390
  ```ruby
337
391
  # 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."
392
+ class SearchOrders < RubyLLM::Tool
393
+ description "Search customer orders by date range, status, or customer email. Returns up to 50 matching orders sorted by date."
394
+ # ...
340
395
  end
341
396
 
342
397
  # Bad: Vague
343
- tool :search do
398
+ class Search < RubyLLM::Tool
344
399
  description "Searches stuff"
400
+ # ...
345
401
  end
346
402
  ```
347
403
 
348
404
  ### 2. Validate Inputs
349
405
 
350
406
  ```ruby
351
- handler do |email:, **_|
407
+ def execute(email:)
352
408
  unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
353
409
  return { error: "Invalid email format" }
354
410
  end
355
- # ... rest of handler
411
+ # ... rest of logic
356
412
  end
357
413
  ```
358
414
 
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
415
+ ### 3. Return Structured Data
375
416
 
376
417
  ```ruby
377
418
  # Good: Structured and consistent
378
- handler do |**_|
419
+ def execute(**_)
379
420
  {
380
421
  success: true,
381
422
  data: { id: 1, name: "Item" },
@@ -384,9 +425,15 @@ handler do |**_|
384
425
  end
385
426
 
386
427
  # Bad: Unstructured
387
- handler { |**_| "Found item with id 1 named Item" }
428
+ def execute(**_)
429
+ "Found item with id 1 named Item"
430
+ end
388
431
  ```
389
432
 
433
+ ### 4. Keep Tools Focused
434
+
435
+ Each tool should do one thing well. Prefer multiple focused tools over one tool that does everything.
436
+
390
437
  ## Next Steps
391
438
 
392
439
  - [MCP Integration](mcp-integration.md) - External tool servers