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,102 +1,222 @@
1
1
  # Tool Usage
2
2
 
3
- Robots with external API integration.
3
+ Robots with external capabilities through tools.
4
4
 
5
5
  ## Overview
6
6
 
7
- This example demonstrates how to give robots access to external systems through tools, including API calls, database queries, and calculations.
7
+ This example demonstrates how to give robots access to external systems through tools. Tools are defined as `RubyLLM::Tool` subclasses or `RobotLab::Tool` instances and passed to robots via the `local_tools:` parameter.
8
8
 
9
- ## Complete Example
9
+ ## RubyLLM::Tool Subclass Pattern
10
+
11
+ The primary way to define tools is by subclassing `RubyLLM::Tool`:
10
12
 
11
13
  ```ruby
12
14
  #!/usr/bin/env ruby
13
- # examples/weather_assistant.rb
15
+ # examples/tool_usage.rb
14
16
 
15
17
  require "bundler/setup"
16
18
  require "robot_lab"
17
- require "http"
18
- require "json"
19
19
 
20
- RobotLab.configure do |config|
21
- config.default_model = "claude-sonnet-4"
20
+ # Define tools as RubyLLM::Tool subclasses
21
+ class Calculator < RubyLLM::Tool
22
+ description "Performs basic arithmetic operations"
23
+
24
+ param :operation,
25
+ type: "string",
26
+ desc: "The operation to perform (add, subtract, multiply, divide)"
27
+
28
+ param :a,
29
+ type: "number",
30
+ desc: "First operand"
31
+
32
+ param :b,
33
+ type: "number",
34
+ desc: "Second operand"
35
+
36
+ def execute(operation:, a:, b:)
37
+ case operation
38
+ when "add" then a + b
39
+ when "subtract" then a - b
40
+ when "multiply" then a * b
41
+ when "divide" then a.to_f / b
42
+ else "Unknown operation: #{operation}"
43
+ end
44
+ end
22
45
  end
23
46
 
24
- # Weather assistant with API integration
25
- weather_bot = RobotLab.build do
26
- name "weather_assistant"
27
- description "Provides weather information"
47
+ class FortuneCookie < RubyLLM::Tool
48
+ description "Get a fortune cookie message with wisdom and lucky numbers"
49
+
50
+ param :category,
51
+ type: "string",
52
+ desc: "The category of fortune (wisdom, love, career, adventure)"
53
+
54
+ FORTUNES = {
55
+ "wisdom" => [
56
+ "The obstacle in the path becomes the path.",
57
+ "A journey of a thousand miles begins with a single step."
58
+ ],
59
+ "career" => [
60
+ "Opportunity dances with those already on the dance floor.",
61
+ "Your work is your signature. Sign it with excellence."
62
+ ]
63
+ }.freeze
64
+
65
+ def execute(category:)
66
+ {
67
+ category: category,
68
+ fortune: FORTUNES.fetch(category, FORTUNES["wisdom"]).sample,
69
+ lucky_numbers: Array.new(6) { rand(1..49) }.sort
70
+ }
71
+ end
72
+ end
28
73
 
29
- template <<~PROMPT
30
- You are a helpful weather assistant. You can look up current weather
31
- conditions for any city. When users ask about weather, use the
32
- get_weather tool to fetch real data.
74
+ # Create robot with tools via local_tools
75
+ robot = RobotLab.build(
76
+ name: "assistant",
77
+ system_prompt: "You help with math and dispense fortune cookies.",
78
+ local_tools: [Calculator, FortuneCookie],
79
+ model: "claude-sonnet-4"
80
+ )
33
81
 
34
- Always provide temperatures in both Fahrenheit and Celsius.
35
- Include relevant advice based on conditions (umbrella, sunscreen, etc).
36
- PROMPT
82
+ # Run the robot
83
+ result = robot.run("What is 15 multiplied by 7? Also, give me a career fortune.")
37
84
 
38
- tool :get_weather do
39
- description "Get current weather for a city"
85
+ # Display results
86
+ puts "Response: #{result.last_text_content}"
40
87
 
41
- parameter :city, type: :string, required: true,
42
- description: "City name (e.g., 'New York', 'London')"
88
+ if result.tool_calls.any?
89
+ puts "\nTool calls made:"
90
+ result.tool_calls.each do |tc|
91
+ tool_info = tc.respond_to?(:tool) ? tc.tool : tc
92
+ puts " #{tool_info[:name] || tool_info}"
93
+ end
94
+ end
95
+ ```
43
96
 
44
- handler do |city:, **_|
45
- # Using wttr.in API (free, no key required)
46
- response = HTTP.get("https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1")
97
+ ## RobotLab::Tool.create Pattern
47
98
 
48
- if response.status.success?
49
- data = JSON.parse(response.body)
50
- current = data["current_condition"].first
99
+ For simpler tools that do not need their own class, use `RobotLab::Tool.create`:
51
100
 
52
- {
53
- city: city,
54
- temperature_f: current["temp_F"],
55
- temperature_c: current["temp_C"],
56
- condition: current["weatherDesc"].first["value"],
57
- humidity: current["humidity"],
58
- wind_mph: current["windspeedMiles"],
59
- feels_like_f: current["FeelsLikeF"],
60
- uv_index: current["uvIndex"]
61
- }
62
- else
63
- { error: "Could not fetch weather for #{city}" }
64
- end
65
- rescue HTTP::Error => e
66
- { error: "Network error: #{e.message}" }
101
+ ```ruby
102
+ require "robot_lab"
103
+
104
+ # Define an inline tool
105
+ get_time = RobotLab::Tool.create(
106
+ name: "get_time",
107
+ description: "Get the current time"
108
+ ) { |_args| Time.now.to_s }
109
+
110
+ # Define a tool with parameters (JSON Schema)
111
+ weather_tool = RobotLab::Tool.create(
112
+ name: "get_weather",
113
+ description: "Get weather for a city",
114
+ parameters: {
115
+ type: "object",
116
+ properties: {
117
+ city: { type: "string", description: "City name" }
118
+ },
119
+ required: ["city"]
120
+ }
121
+ ) { |args| { city: args[:city], temperature: "72F", condition: "sunny" } }
122
+
123
+ robot = RobotLab.build(
124
+ name: "weather_bot",
125
+ system_prompt: "You provide weather and time information.",
126
+ local_tools: [get_time, weather_tool],
127
+ model: "claude-sonnet-4"
128
+ )
129
+
130
+ result = robot.run("What time is it and what's the weather in New York?")
131
+ puts result.last_text_content
132
+ ```
133
+
134
+ ## Weather API Integration
135
+
136
+ ```ruby
137
+ #!/usr/bin/env ruby
138
+ # examples/weather_assistant.rb
139
+
140
+ require "bundler/setup"
141
+ require "robot_lab"
142
+ require "http"
143
+ require "json"
144
+
145
+ class GetWeather < RubyLLM::Tool
146
+ description "Get current weather for a city"
147
+
148
+ param :city,
149
+ type: "string",
150
+ desc: "City name (e.g., 'New York', 'London')"
151
+
152
+ def execute(city:)
153
+ response = HTTP.get(
154
+ "https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
155
+ )
156
+
157
+ if response.status.success?
158
+ data = JSON.parse(response.body)
159
+ current = data["current_condition"].first
160
+
161
+ {
162
+ city: city,
163
+ temperature_f: current["temp_F"],
164
+ temperature_c: current["temp_C"],
165
+ condition: current["weatherDesc"].first["value"],
166
+ humidity: current["humidity"],
167
+ wind_mph: current["windspeedMiles"]
168
+ }
169
+ else
170
+ { error: "Could not fetch weather for #{city}" }
67
171
  end
172
+ rescue HTTP::Error => e
173
+ { error: "Network error: #{e.message}" }
68
174
  end
175
+ end
69
176
 
70
- tool :get_forecast do
71
- description "Get weather forecast for upcoming days"
72
-
73
- parameter :city, type: :string, required: true
74
- parameter :days, type: :integer, default: 3
177
+ class GetForecast < RubyLLM::Tool
178
+ description "Get weather forecast for upcoming days"
75
179
 
76
- handler do |city:, days: 3, **_|
77
- response = HTTP.get("https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1")
180
+ param :city, type: "string", desc: "City name"
181
+ param :days, type: "integer", desc: "Number of days (default 3)"
78
182
 
79
- if response.status.success?
80
- data = JSON.parse(response.body)
183
+ def execute(city:, days: 3)
184
+ response = HTTP.get(
185
+ "https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
186
+ )
81
187
 
82
- data["weather"].take(days).map do |day|
83
- {
84
- date: day["date"],
85
- high_f: day["maxtempF"],
86
- low_f: day["mintempF"],
87
- condition: day["hourly"].first["weatherDesc"].first["value"]
88
- }
89
- end
90
- else
91
- { error: "Could not fetch forecast" }
188
+ if response.status.success?
189
+ data = JSON.parse(response.body)
190
+ data["weather"].take(days).map do |day|
191
+ {
192
+ date: day["date"],
193
+ high_f: day["maxtempF"],
194
+ low_f: day["mintempF"],
195
+ condition: day["hourly"].first["weatherDesc"].first["value"]
196
+ }
92
197
  end
93
- rescue HTTP::Error => e
94
- { error: "Network error: #{e.message}" }
198
+ else
199
+ { error: "Could not fetch forecast" }
95
200
  end
201
+ rescue HTTP::Error => e
202
+ { error: "Network error: #{e.message}" }
96
203
  end
97
204
  end
98
205
 
99
- # Run interactive session
206
+ # Create weather assistant
207
+ weather_bot = RobotLab.build(
208
+ name: "weather_assistant",
209
+ description: "Provides weather information",
210
+ system_prompt: <<~PROMPT,
211
+ You are a helpful weather assistant. Use your tools to look up weather.
212
+ Always provide temperatures in both Fahrenheit and Celsius.
213
+ Include relevant advice based on conditions (umbrella, sunscreen, etc).
214
+ PROMPT
215
+ local_tools: [GetWeather, GetForecast],
216
+ model: "claude-sonnet-4"
217
+ )
218
+
219
+ # Interactive session
100
220
  puts "Weather Assistant (type 'quit' to exit)"
101
221
  puts "-" * 50
102
222
 
@@ -107,18 +227,8 @@ loop do
107
227
  break if input.nil? || input.downcase == "quit"
108
228
  next if input.empty?
109
229
 
110
- state = RobotLab.create_state(message: input)
111
-
112
- print "\nAssistant: "
113
- weather_bot.run(state: state) do |event|
114
- case event.type
115
- when :text_delta
116
- print event.text
117
- when :tool_call
118
- puts "\n[Checking weather for #{event.input[:city]}...]"
119
- end
120
- end
121
- puts
230
+ result = weather_bot.run(input)
231
+ puts "\nAssistant: #{result.last_text_content}"
122
232
  end
123
233
 
124
234
  puts "\nGoodbye!"
@@ -137,174 +247,155 @@ ORDERS = {
137
247
  "ORD002" => { id: "ORD002", status: "processing", items: ["Gadget", "Gizmo"], total: 89.99 }
138
248
  }
139
249
 
140
- order_bot = RobotLab.build do
141
- name "order_assistant"
142
- template "You help customers check their orders."
143
-
144
- tool :get_order do
145
- description "Look up an order by ID"
146
- parameter :order_id, type: :string, required: true
250
+ class GetOrder < RubyLLM::Tool
251
+ description "Look up an order by ID"
147
252
 
148
- handler do |order_id:, state:, **_|
149
- # Verify user owns this order
150
- user_id = state.data[:user_id]
151
- order = ORDERS[order_id.upcase]
253
+ param :order_id, type: "string", desc: "The order ID to look up"
152
254
 
153
- if order
154
- order
155
- else
156
- { error: "Order not found" }
157
- end
158
- end
255
+ def execute(order_id:)
256
+ order = ORDERS[order_id.upcase]
257
+ order || { error: "Order not found" }
159
258
  end
259
+ end
160
260
 
161
- tool :list_orders do
162
- description "List user's recent orders"
163
- parameter :limit, type: :integer, default: 5
261
+ class ListOrders < RubyLLM::Tool
262
+ description "List recent orders"
164
263
 
165
- handler do |limit:, state:, **_|
166
- user_id = state.data[:user_id]
167
- # Filter by user in real implementation
168
- ORDERS.values.take(limit)
169
- end
264
+ param :limit, type: "integer", desc: "Maximum number of orders to return"
265
+
266
+ def execute(limit: 5)
267
+ ORDERS.values.take(limit)
170
268
  end
269
+ end
171
270
 
172
- tool :cancel_order do
173
- description "Cancel an order"
174
- parameter :order_id, type: :string, required: true
175
- parameter :reason, type: :string
176
-
177
- handler do |order_id:, reason: nil, state:, **_|
178
- order = ORDERS[order_id.upcase]
179
-
180
- if order.nil?
181
- { success: false, error: "Order not found" }
182
- elsif order[:status] == "shipped"
183
- { success: false, error: "Cannot cancel shipped orders" }
184
- else
185
- order[:status] = "cancelled"
186
- order[:cancel_reason] = reason
187
- { success: true, message: "Order #{order_id} cancelled" }
188
- end
271
+ class CancelOrder < RubyLLM::Tool
272
+ description "Cancel an order"
273
+
274
+ param :order_id, type: "string", desc: "The order ID to cancel"
275
+ param :reason, type: "string", desc: "Reason for cancellation"
276
+
277
+ def execute(order_id:, reason: nil)
278
+ order = ORDERS[order_id.upcase]
279
+
280
+ if order.nil?
281
+ { success: false, error: "Order not found" }
282
+ elsif order[:status] == "shipped"
283
+ { success: false, error: "Cannot cancel shipped orders" }
284
+ else
285
+ order[:status] = "cancelled"
286
+ order[:cancel_reason] = reason
287
+ { success: true, message: "Order #{order_id} cancelled" }
189
288
  end
190
289
  end
191
290
  end
192
291
 
193
- # Run with user context
194
- state = RobotLab.create_state(
195
- message: "What's the status of order ORD001?",
196
- data: { user_id: "user_123" }
292
+ order_bot = RobotLab.build(
293
+ name: "order_assistant",
294
+ system_prompt: "You help customers check and manage their orders.",
295
+ local_tools: [GetOrder, ListOrders, CancelOrder],
296
+ model: "claude-sonnet-4"
197
297
  )
198
298
 
199
- result = order_bot.run(state: state)
200
- puts result.output.first.content
299
+ # Run with a question
300
+ result = order_bot.run("What's the status of order ORD001?")
301
+ puts result.last_text_content
201
302
  ```
202
303
 
203
- ## Calculator Tool
304
+ ## Tool Call Callbacks
204
305
 
205
- ```ruby
206
- # examples/math_assistant.rb
306
+ Use `on_tool_call` and `on_tool_result` to monitor tool execution:
207
307
 
208
- require "robot_lab"
209
- require "dentaku"
308
+ ```ruby
309
+ robot = RobotLab.build(
310
+ name: "monitored_bot",
311
+ system_prompt: "You help with calculations.",
312
+ local_tools: [Calculator],
313
+ model: "claude-sonnet-4",
314
+ on_tool_call: ->(tool_call) {
315
+ puts "[Tool Call] #{tool_call.name}: #{tool_call.arguments}"
316
+ },
317
+ on_tool_result: ->(tool_call, result) {
318
+ puts "[Tool Result] #{tool_call.name}: #{result}"
319
+ }
320
+ )
210
321
 
211
- calculator = Dentaku::Calculator.new
322
+ result = robot.run("What is 42 * 17?")
323
+ ```
212
324
 
213
- math_bot = RobotLab.build do
214
- name "math_assistant"
215
- template "You help with mathematical calculations."
325
+ ## Running
216
326
 
217
- tool :calculate do
218
- description "Evaluate a mathematical expression"
219
- parameter :expression, type: :string, required: true,
220
- description: "Math expression like '2 + 2' or 'sqrt(16)'"
327
+ ```bash
328
+ export ANTHROPIC_API_KEY="your-key"
221
329
 
222
- handler do |expression:, **_|
223
- result = calculator.evaluate(expression)
224
- { expression: expression, result: result }
225
- rescue => e
226
- { error: "Invalid expression: #{e.message}" }
227
- end
228
- end
330
+ # Tool usage example
331
+ ruby examples/tool_usage.rb
229
332
 
230
- tool :solve_equation do
231
- description "Solve for a variable"
232
- parameter :equation, type: :string, required: true
233
- parameter :variable, type: :string, required: true
333
+ # Weather assistant
334
+ ruby examples/weather_assistant.rb
234
335
 
235
- handler do |equation:, variable:, **_|
236
- result = calculator.solve(equation, variable.to_sym)
237
- { equation: equation, variable: variable, solutions: result }
238
- rescue => e
239
- { error: "Could not solve: #{e.message}" }
240
- end
241
- end
242
- end
336
+ # Order lookup
337
+ ruby examples/order_assistant.rb
243
338
  ```
244
339
 
245
- ## Multi-Tool Example
340
+ ## Interactive User Input
246
341
 
247
- ```ruby
248
- # examples/research_assistant.rb
342
+ Use the built-in `RobotLab::AskUser` tool to let robots ask the user questions during execution:
249
343
 
250
- research_bot = RobotLab.build do
251
- name "research_assistant"
252
- template "You help with research tasks."
344
+ ```ruby
345
+ require "robot_lab"
253
346
 
254
- tool :web_search do
255
- description "Search the web"
256
- parameter :query, type: :string, required: true
257
- handler { |query:, **_| SearchAPI.search(query) }
258
- end
347
+ robot = RobotLab.build(
348
+ name: "interviewer",
349
+ system_prompt: <<~PROMPT,
350
+ You are a project setup assistant. Interview the user to understand their
351
+ needs, then summarize the project plan. Use the ask_user tool to gather
352
+ information one question at a time.
353
+ PROMPT
354
+ local_tools: [RobotLab::AskUser],
355
+ model: "claude-sonnet-4"
356
+ )
259
357
 
260
- tool :read_url do
261
- description "Read content from a URL"
262
- parameter :url, type: :string, required: true
263
- handler { |url:, **_| HTTP.get(url).body.to_s }
264
- end
358
+ result = robot.run("Help me plan a new web application")
359
+ puts "\nProject Plan:\n#{result.last_text_content}"
360
+ ```
265
361
 
266
- tool :summarize do
267
- description "Summarize text"
268
- parameter :text, type: :string, required: true
269
- parameter :length, type: :string, enum: %w[short medium long], default: "medium"
270
- handler { |text:, length:, **_| Summarizer.summarize(text, length) }
271
- end
362
+ The robot will ask questions interactively:
272
363
 
273
- tool :save_note do
274
- description "Save a research note"
275
- parameter :title, type: :string, required: true
276
- parameter :content, type: :string, required: true
277
- handler do |title:, content:, state:, **_|
278
- notes = state.memory.recall("notes") || []
279
- notes << { title: title, content: content, created: Time.now }
280
- state.memory.remember("notes", notes)
281
- { saved: true, total_notes: notes.size }
282
- end
283
- end
284
- end
285
364
  ```
365
+ [interviewer] What programming language would you like to use?
366
+ 1. Ruby
367
+ 2. Python
368
+ 3. TypeScript
369
+ > 1
286
370
 
287
- ## Running
371
+ [interviewer] Will you need a database?
372
+ > [yes]
288
373
 
289
- ```bash
290
- export ANTHROPIC_API_KEY="your-key"
374
+ [interviewer] What's the main purpose of the application?
375
+ > Customer support portal
376
+ ```
291
377
 
292
- # Weather assistant
293
- ruby examples/weather_assistant.rb
378
+ For testing, inject `StringIO` objects:
294
379
 
295
- # Order lookup
296
- ruby examples/order_assistant.rb
380
+ ```ruby
381
+ robot.input = StringIO.new("Ruby\nyes\nCustomer portal\n")
382
+ robot.output = StringIO.new
297
383
  ```
298
384
 
299
385
  ## Key Concepts
300
386
 
301
- 1. **Tool Definition**: Use the `tool` DSL with description and parameters
302
- 2. **Handler**: Receives parameters plus state, robot, network context
303
- 3. **Error Handling**: Return error hashes for graceful failures
304
- 4. **State Access**: Tools can read/write state and memory
387
+ 1. **RubyLLM::Tool subclass**: Define a class with `description`, `param`, and `execute` method
388
+ 2. **RobotLab::Tool subclass**: Same DSL plus `robot` accessor for robot-aware tools
389
+ 3. **RobotLab::Tool.create**: Use `RobotLab::Tool.create(name:, description:, &block)` for dynamic tools
390
+ 4. **Built-in tools**: `RobotLab::AskUser` for interactive terminal input
391
+ 5. **local_tools**: Pass tool classes/instances via `local_tools:` parameter to `RobotLab.build` or `Robot.new`
392
+ 6. **Frontmatter tools**: Declare tool class names in template YAML front matter (`tools: [Calculator]`) for self-contained templates
393
+ 7. **Error Handling**: Return error hashes (e.g., `{ error: "message" }`) for graceful failures
394
+ 8. **Callbacks**: Use `on_tool_call:` and `on_tool_result:` for monitoring
395
+ 9. **Result Access**: Check `result.tool_calls` for tool call history, `result.last_text_content` for the final response
305
396
 
306
397
  ## See Also
307
398
 
308
399
  - [Using Tools Guide](../guides/using-tools.md)
309
400
  - [Tool API](../api/core/tool.md)
310
- - [Memory Guide](../guides/memory.md)
401
+ - [Robot API](../api/core/robot.md)