robot_lab 0.3.0 → 0.5.0

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. 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)