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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
data/docs/examples/tool-usage.md
DELETED
|
@@ -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)
|