robot_lab 0.3.0 → 0.3.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.
Files changed (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,468 +0,0 @@
1
- # Tool Usage
2
-
3
- Robots with external capabilities through tools.
4
-
5
- ## Overview
6
-
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
-
9
- > [!WARNING]
10
- > **Attaching a tool is not the same as sending it.** `Robot#run` defaults to
11
- > `tools: :none`, so a plain `robot.run("...")` sends the LLM zero tools even when
12
- > `local_tools:` were supplied at build time. Pass `tools: :inherit` on every call
13
- > that should be able to use them:
14
- >
15
- > ```ruby
16
- > robot.run("What is 15 * 7?", tools: :inherit)
17
- > ```
18
- >
19
- > For a **standalone** robot, do not pass `tools: :inherit` in the constructor:
20
- > at build time it resolves against the parent level (`:none`) and yields an
21
- > allowlist matching nothing, suppressing the tools even when the run asks for
22
- > them. Leave `tools:` unset there.
23
- >
24
- > Inside a **network**, the opposite holds — build-time `tools: :inherit` is how a
25
- > robot opts into the network `config:`'s allowlist. See
26
- > [Creating Networks](../guides/creating-networks.md).
27
- >
28
- > An explicit array (`tools: [Calculator]`) is an allowlist, not a local-vs-MCP
29
- > switch, and its entries must match how each tool was attached — this page
30
- > attaches classes, so use class names.
31
-
32
- ## RubyLLM::Tool Subclass Pattern
33
-
34
- The primary way to define tools is by subclassing `RubyLLM::Tool`:
35
-
36
- ```ruby
37
- #!/usr/bin/env ruby
38
- # Tool definitions mirror examples/02_tools.rb
39
-
40
- require "bundler/setup"
41
- require "robot_lab"
42
-
43
- # Define tools as RubyLLM::Tool subclasses
44
- class Calculator < RubyLLM::Tool
45
- description "Performs basic arithmetic operations"
46
-
47
- param :operation,
48
- type: "string",
49
- desc: "The operation to perform (add, subtract, multiply, divide)"
50
-
51
- param :a,
52
- type: "number",
53
- desc: "First operand"
54
-
55
- param :b,
56
- type: "number",
57
- desc: "Second operand"
58
-
59
- def execute(operation:, a:, b:)
60
- case operation
61
- when "add" then a + b
62
- when "subtract" then a - b
63
- when "multiply" then a * b
64
- when "divide" then a.to_f / b
65
- else "Unknown operation: #{operation}"
66
- end
67
- end
68
- end
69
-
70
- class FortuneCookie < RubyLLM::Tool
71
- description "Get a fortune cookie message with wisdom and lucky numbers"
72
-
73
- param :category,
74
- type: "string",
75
- desc: "The category of fortune (wisdom, love, career, adventure)"
76
-
77
- FORTUNES = {
78
- "wisdom" => [
79
- "The obstacle in the path becomes the path.",
80
- "A journey of a thousand miles begins with a single step."
81
- ],
82
- "career" => [
83
- "Opportunity dances with those already on the dance floor.",
84
- "Your work is your signature. Sign it with excellence."
85
- ]
86
- }.freeze
87
-
88
- def execute(category:)
89
- {
90
- category: category,
91
- fortune: FORTUNES.fetch(category, FORTUNES["wisdom"]).sample,
92
- lucky_numbers: Array.new(6) { rand(1..49) }.sort
93
- }
94
- end
95
- end
96
-
97
- # Create robot with tools via local_tools
98
- robot = RobotLab.build(
99
- name: "assistant",
100
- system_prompt: "You help with math and dispense fortune cookies.",
101
- local_tools: [Calculator, FortuneCookie],
102
- model: "claude-sonnet-4"
103
- )
104
-
105
- # Run the robot -- tools: :inherit is what actually sends Calculator and
106
- # FortuneCookie to the LLM
107
- result = robot.run(
108
- "What is 15 multiplied by 7? Also, give me a career fortune.",
109
- tools: :inherit
110
- )
111
-
112
- # Display results
113
- puts "Response: #{result.last_text_content}"
114
-
115
- # Confirm which tools were sent for this turn
116
- puts "Tools sent: #{robot.chat.tools.keys.join(', ')}"
117
- ```
118
-
119
- > [!NOTE]
120
- > `result.tool_calls` is effectively always empty. It reads the *final* assistant
121
- > message, and by the time ruby_llm's tool loop has finished that message contains
122
- > only text. To observe tool activity, use the `on_tool_call:` callback (below) or
123
- > the `:tool_call` hooks.
124
-
125
- ## RobotLab::Tool.create Pattern
126
-
127
- For simpler tools that do not need their own class, use `RobotLab::Tool.create`:
128
-
129
- ```ruby
130
- require "robot_lab"
131
-
132
- # Define an inline tool
133
- get_time = RobotLab::Tool.create(
134
- name: "get_time",
135
- description: "Get the current time"
136
- ) { |_args| Time.now.to_s }
137
-
138
- # Define a tool with parameters (JSON Schema)
139
- weather_tool = RobotLab::Tool.create(
140
- name: "get_weather",
141
- description: "Get weather for a city",
142
- parameters: {
143
- type: "object",
144
- properties: {
145
- city: { type: "string", description: "City name" }
146
- },
147
- required: ["city"]
148
- }
149
- ) { |args| { city: args[:city], temperature: "72F", condition: "sunny" } }
150
-
151
- robot = RobotLab.build(
152
- name: "weather_bot",
153
- system_prompt: "You provide weather and time information.",
154
- local_tools: [get_time, weather_tool],
155
- model: "claude-sonnet-4"
156
- )
157
-
158
- result = robot.run("What time is it and what's the weather in New York?", tools: :inherit)
159
- puts result.last_text_content
160
- ```
161
-
162
- The block receives a single hash of symbol-keyed arguments.
163
-
164
- > [!NOTE]
165
- > `RobotLab::Tool.create(parameters:)` accepts a JSON-Schema-shaped hash, but only
166
- > `type` and `description` are read off each property, plus the top-level `required`
167
- > list. Anything else (`enum`, `default`, nested `items`) is ignored — ruby_llm's
168
- > `param` DSL supports only `type:`, `desc:`/`description:`, and `required:`.
169
-
170
- ## Weather API Integration
171
-
172
- ```ruby
173
- #!/usr/bin/env ruby
174
-
175
- require "bundler/setup"
176
- require "robot_lab"
177
- require "http"
178
- require "json"
179
-
180
- class GetWeather < RubyLLM::Tool
181
- description "Get current weather for a city"
182
-
183
- param :city,
184
- type: "string",
185
- desc: "City name (e.g., 'New York', 'London')"
186
-
187
- def execute(city:)
188
- response = HTTP.get(
189
- "https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
190
- )
191
-
192
- if response.status.success?
193
- data = JSON.parse(response.body)
194
- current = data["current_condition"].first
195
-
196
- {
197
- city: city,
198
- temperature_f: current["temp_F"],
199
- temperature_c: current["temp_C"],
200
- condition: current["weatherDesc"].first["value"],
201
- humidity: current["humidity"],
202
- wind_mph: current["windspeedMiles"]
203
- }
204
- else
205
- { error: "Could not fetch weather for #{city}" }
206
- end
207
- rescue HTTP::Error => e
208
- { error: "Network error: #{e.message}" }
209
- end
210
- end
211
-
212
- class GetForecast < RubyLLM::Tool
213
- description "Get weather forecast for upcoming days"
214
-
215
- param :city, type: "string", desc: "City name"
216
- param :days, type: "integer", desc: "Number of days (default 3)"
217
-
218
- def execute(city:, days: 3)
219
- response = HTTP.get(
220
- "https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
221
- )
222
-
223
- if response.status.success?
224
- data = JSON.parse(response.body)
225
- data["weather"].take(days).map do |day|
226
- {
227
- date: day["date"],
228
- high_f: day["maxtempF"],
229
- low_f: day["mintempF"],
230
- condition: day["hourly"].first["weatherDesc"].first["value"]
231
- }
232
- end
233
- else
234
- { error: "Could not fetch forecast" }
235
- end
236
- rescue HTTP::Error => e
237
- { error: "Network error: #{e.message}" }
238
- end
239
- end
240
-
241
- # Create weather assistant
242
- weather_bot = RobotLab.build(
243
- name: "weather_assistant",
244
- description: "Provides weather information",
245
- system_prompt: <<~PROMPT,
246
- You are a helpful weather assistant. Use your tools to look up weather.
247
- Always provide temperatures in both Fahrenheit and Celsius.
248
- Include relevant advice based on conditions (umbrella, sunscreen, etc).
249
- PROMPT
250
- local_tools: [GetWeather, GetForecast],
251
- model: "claude-sonnet-4"
252
- )
253
-
254
- # Interactive session
255
- puts "Weather Assistant (type 'quit' to exit)"
256
- puts "-" * 50
257
-
258
- loop do
259
- print "\nYou: "
260
- input = gets&.chomp
261
-
262
- break if input.nil? || input.downcase == "quit"
263
- next if input.empty?
264
-
265
- result = weather_bot.run(input, tools: :inherit)
266
- puts "\nAssistant: #{result.last_text_content}"
267
- end
268
-
269
- puts "\nGoodbye!"
270
- ```
271
-
272
- ## Database Integration
273
-
274
- ```ruby
275
- require "robot_lab"
276
-
277
- # Mock database
278
- ORDERS = {
279
- "ORD001" => { id: "ORD001", status: "shipped", items: ["Widget"], total: 29.99 },
280
- "ORD002" => { id: "ORD002", status: "processing", items: ["Gadget", "Gizmo"], total: 89.99 }
281
- }
282
-
283
- class GetOrder < RubyLLM::Tool
284
- description "Look up an order by ID"
285
-
286
- param :order_id, type: "string", desc: "The order ID to look up"
287
-
288
- def execute(order_id:)
289
- order = ORDERS[order_id.upcase]
290
- order || { error: "Order not found" }
291
- end
292
- end
293
-
294
- class ListOrders < RubyLLM::Tool
295
- description "List recent orders"
296
-
297
- param :limit, type: "integer", desc: "Maximum number of orders to return"
298
-
299
- def execute(limit: 5)
300
- ORDERS.values.take(limit)
301
- end
302
- end
303
-
304
- class CancelOrder < RubyLLM::Tool
305
- description "Cancel an order"
306
-
307
- param :order_id, type: "string", desc: "The order ID to cancel"
308
- param :reason, type: "string", desc: "Reason for cancellation"
309
-
310
- def execute(order_id:, reason: nil)
311
- order = ORDERS[order_id.upcase]
312
-
313
- if order.nil?
314
- { success: false, error: "Order not found" }
315
- elsif order[:status] == "shipped"
316
- { success: false, error: "Cannot cancel shipped orders" }
317
- else
318
- order[:status] = "cancelled"
319
- order[:cancel_reason] = reason
320
- { success: true, message: "Order #{order_id} cancelled" }
321
- end
322
- end
323
- end
324
-
325
- order_bot = RobotLab.build(
326
- name: "order_assistant",
327
- system_prompt: "You help customers check and manage their orders.",
328
- local_tools: [GetOrder, ListOrders, CancelOrder],
329
- model: "claude-sonnet-4"
330
- )
331
-
332
- # Run with a question
333
- result = order_bot.run("What's the status of order ORD001?", tools: :inherit)
334
- puts result.last_text_content
335
- ```
336
-
337
- ## Tool Call Callbacks
338
-
339
- Use `on_tool_call` and `on_tool_result` to monitor tool execution:
340
-
341
- ```ruby
342
- robot = RobotLab.build(
343
- name: "monitored_bot",
344
- system_prompt: "You help with calculations.",
345
- local_tools: [Calculator],
346
- model: "claude-sonnet-4",
347
- on_tool_call: ->(tool_call) {
348
- puts "[Tool Call] #{tool_call.name}: #{tool_call.arguments}"
349
- },
350
- # NOTE: one argument -- the result. There is no tool_call parameter here.
351
- on_tool_result: ->(result) {
352
- puts "[Tool Result] #{result}"
353
- }
354
- )
355
-
356
- result = robot.run("What is 42 * 17?", tools: :inherit)
357
- ```
358
-
359
- > [!WARNING]
360
- > `on_tool_result` receives **exactly one** argument, the tool's return value.
361
- > Writing `->(tool_call, result)` raises
362
- > `ArgumentError: wrong number of arguments (given 1, expected 2)` the first time a
363
- > tool runs.
364
- >
365
- > Both callbacks map onto ruby_llm's legacy hooks and are deprecated as of
366
- > ruby_llm 1.16 — wiring either one emits
367
- > ``` `on_tool_call` is deprecated and will be removed in RubyLLM 2.0. Use `before_tool_call` instead. ```
368
- > The additive replacements are `before_message`, `after_message`,
369
- > `before_tool_call`, and `after_tool_result`. RobotLab's own
370
- > [Hook system](../guides/hooks.md) is the supported way to observe tool activity.
371
-
372
- ## Running
373
-
374
- ```bash
375
- export ANTHROPIC_API_KEY="your-key"
376
-
377
- # Tool definitions and a robot that uses them
378
- ruby examples/02_tools.rb
379
-
380
- # Tool loop circuit breaker (max_tool_rounds)
381
- ruby examples/20_circuit_breaker.rb
382
-
383
- # Ractor-safe CPU tools (no LLM calls)
384
- ruby examples/29_ractor_tools.rb
385
- ```
386
-
387
- ## Interactive User Input
388
-
389
- Use the built-in `RobotLab::AskUser` tool to let robots ask the user questions during execution:
390
-
391
- ```ruby
392
- require "robot_lab"
393
-
394
- robot = RobotLab.build(
395
- name: "interviewer",
396
- system_prompt: <<~PROMPT,
397
- You are a project setup assistant. Interview the user to understand their
398
- needs, then summarize the project plan. Use the robot_lab--ask_user tool to
399
- gather information one question at a time.
400
- PROMPT
401
- model: "claude-sonnet-4"
402
- )
403
-
404
- # Attach an INSTANCE bound to the robot so the tool uses robot.input/robot.output
405
- robot.local_tools << RobotLab::AskUser.new(robot: robot)
406
-
407
- result = robot.run("Help me plan a new web application", tools: :inherit)
408
- puts "\nProject Plan:\n#{result.last_text_content}"
409
- ```
410
-
411
- > [!WARNING]
412
- > Pass an **instance**, not the class. `RobotLab::Tool` derives its LLM-visible
413
- > name from the full class name, so `RobotLab::AskUser` is presented to the model
414
- > as **`robot_lab--ask_user`**, not `ask_user` — name it that way in your system
415
- > prompt.
416
- >
417
- > An instance created without `robot:` has `tool.robot == nil`, so it ignores
418
- > `robot.input` / `robot.output`, falls back to `$stdin` / `$stdout`, and labels
419
- > every prompt `[Robot]` instead of the robot's name. Always use
420
- > `RobotLab::AskUser.new(robot: robot)`.
421
-
422
- The robot will ask questions interactively:
423
-
424
- ```
425
- [interviewer] What programming language would you like to use?
426
- 1. Ruby
427
- 2. Python
428
- 3. TypeScript
429
- > 1
430
-
431
- [interviewer] Will you need a database?
432
- > [yes]
433
-
434
- [interviewer] What's the main purpose of the application?
435
- > Customer support portal
436
- ```
437
-
438
- For testing, inject `StringIO` objects on the robot *before* building the tool
439
- instance (the tool reads them through its `robot` reference at call time):
440
-
441
- ```ruby
442
- robot.input = StringIO.new("Ruby\nyes\nCustomer portal\n")
443
- robot.output = StringIO.new
444
- ```
445
-
446
- `RobotLab::AskUser` can also be driven directly, outside an LLM turn — see
447
- [`examples/06_prompt_templates.rb`](https://github.com/MadBomber/robot_lab/blob/main/examples/06_prompt_templates.rb),
448
- which calls `RobotLab::AskUser.new.call("question" => ..., "default" => ...)` to
449
- collect a value before any robot is built.
450
-
451
- ## Key Concepts
452
-
453
- 1. **RubyLLM::Tool subclass**: Define a class with `description`, `param`, and `execute` method
454
- 2. **RobotLab::Tool subclass**: Same DSL plus a `robot` accessor for robot-aware tools
455
- 3. **RobotLab::Tool.create**: Use `RobotLab::Tool.create(name:, description:, &block)` for dynamic tools
456
- 4. **Built-in tools**: `RobotLab::AskUser` (LLM-visible name `robot_lab--ask_user`) for interactive terminal input
457
- 5. **local_tools**: Pass tool classes/instances via `local_tools:` to `RobotLab.build` or `Robot.new` — this *attaches* them
458
- 6. **tools: :inherit**: Required on `run` to actually *send* the attached tools; the default is `tools: :none`
459
- 7. **Frontmatter tools**: Declare tool class names in template YAML front matter (`tools: [Calculator]`); they populate `local_tools`, and still need `tools: :inherit` at run time. Constructor `local_tools:` overrides the front-matter list
460
- 8. **Error Handling**: Raised exceptions are caught and returned to the LLM as text (`"Error (tool_name): message"`); a `RobotLab::ToolError` with `retryable: true` appends `" (retryable)"`. Set `self.raise_on_error = true` on a class to opt out — note this is per-class and is *not* inherited by subclasses
461
- 9. **Tool cap**: At most `DEFAULT_MAX_TOOLS` (128) tools are sent. Setting `max_tools` to nil, 0, or a negative number falls back to 128 — the cap cannot be disabled
462
- 10. **Result Access**: Use `result.last_text_content` (alias `result.reply`) for the final response. `result.tool_calls` is effectively always empty — observe tool activity with `on_tool_call:` or the Hook system instead
463
-
464
- ## See Also
465
-
466
- - [Using Tools Guide](../guides/using-tools.md)
467
- - [Tool API](../api/core/tool.md)
468
- - [Robot API](../api/core/robot.md)