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