Skip to content

Commit f832cb2

Browse files
committed
Replaced Calculator example with Weather example.
Closes #25
1 parent 9825f4f commit f832cb2

File tree

9 files changed

+159
-144
lines changed

9 files changed

+159
-144
lines changed

.rspec_status

+13-13
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ example_id | status | run_time |
1717
./spec/ruby_llm/chat_spec.rb[1:1:3:2] | passed | 19.22 seconds |
1818
./spec/ruby_llm/chat_spec.rb[1:1:4:1] | passed | 3.15 seconds |
1919
./spec/ruby_llm/chat_spec.rb[1:1:4:2] | passed | 2.51 seconds |
20-
./spec/ruby_llm/chat_streaming_spec.rb[1:1:1:1] | passed | 0.91374 seconds |
21-
./spec/ruby_llm/chat_streaming_spec.rb[1:1:2:1] | passed | 0.50088 seconds |
22-
./spec/ruby_llm/chat_streaming_spec.rb[1:1:3:1] | passed | 5.69 seconds |
23-
./spec/ruby_llm/chat_streaming_spec.rb[1:1:4:1] | passed | 1.22 seconds |
24-
./spec/ruby_llm/chat_tools_spec.rb[1:1:1] | passed | 4.26 seconds |
25-
./spec/ruby_llm/chat_tools_spec.rb[1:1:2] | passed | 6.16 seconds |
26-
./spec/ruby_llm/chat_tools_spec.rb[1:1:3] | passed | 11.15 seconds |
27-
./spec/ruby_llm/chat_tools_spec.rb[1:1:4] | passed | 1.3 seconds |
28-
./spec/ruby_llm/chat_tools_spec.rb[1:1:5] | passed | 2.71 seconds |
29-
./spec/ruby_llm/chat_tools_spec.rb[1:1:6] | passed | 2.43 seconds |
30-
./spec/ruby_llm/chat_tools_spec.rb[1:1:7] | passed | 2.71 seconds |
31-
./spec/ruby_llm/chat_tools_spec.rb[1:1:8] | passed | 4 seconds |
32-
./spec/ruby_llm/chat_tools_spec.rb[1:1:9] | passed | 2.81 seconds |
20+
./spec/ruby_llm/chat_streaming_spec.rb[1:1:1] | passed | 0.65115 seconds |
21+
./spec/ruby_llm/chat_streaming_spec.rb[1:1:2] | passed | 0.50907 seconds |
22+
./spec/ruby_llm/chat_streaming_spec.rb[1:1:3] | passed | 6.69 seconds |
23+
./spec/ruby_llm/chat_streaming_spec.rb[1:1:4] | passed | 0.70777 seconds |
24+
./spec/ruby_llm/chat_tools_spec.rb[1:1:1] | passed | 4.23 seconds |
25+
./spec/ruby_llm/chat_tools_spec.rb[1:1:2] | passed | 8.45 seconds |
26+
./spec/ruby_llm/chat_tools_spec.rb[1:1:3] | passed | 8.22 seconds |
27+
./spec/ruby_llm/chat_tools_spec.rb[1:1:4] | passed | 1.16 seconds |
28+
./spec/ruby_llm/chat_tools_spec.rb[1:1:5] | passed | 2.73 seconds |
29+
./spec/ruby_llm/chat_tools_spec.rb[1:1:6] | passed | 3.33 seconds |
30+
./spec/ruby_llm/chat_tools_spec.rb[1:1:7] | passed | 1.76 seconds |
31+
./spec/ruby_llm/chat_tools_spec.rb[1:1:8] | passed | 3 seconds |
32+
./spec/ruby_llm/chat_tools_spec.rb[1:1:9] | passed | 4.47 seconds |
3333
./spec/ruby_llm/embeddings_spec.rb[1:1:1:1] | passed | 0.33357 seconds |
3434
./spec/ruby_llm/embeddings_spec.rb[1:1:1:2] | passed | 0.43632 seconds |
3535
./spec/ruby_llm/embeddings_spec.rb[1:1:2:1] | passed | 0.65614 seconds |

README.md

+13-7
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,22 @@ RubyLLM.paint "a sunset over mountains in watercolor style"
5959
RubyLLM.embed "Ruby is elegant and expressive"
6060

6161
# Let AI use your code
62-
class Calculator < RubyLLM::Tool
63-
description "Performs calculations"
64-
param :expression, type: :string, desc: "Math expression to evaluate"
65-
66-
def execute(expression:)
67-
eval(expression).to_s
62+
class Weather < RubyLLM::Tool
63+
description "Gets current weather for a location"
64+
param :latitude, desc: "Latitude (e.g., 52.5200)"
65+
param :longitude, desc: "Longitude (e.g., 13.4050)"
66+
67+
def execute(latitude:, longitude:)
68+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
69+
70+
response = Faraday.get(url)
71+
data = JSON.parse(response.body)
72+
rescue => e
73+
{ error: e.message }
6874
end
6975
end
7076

71-
chat.with_tool(Calculator).ask "What's 123 * 456?"
77+
chat.with_tool(Weather).ask "What's the weather in Berlin? (52.5200, 13.4050)"
7278
```
7379

7480
## Installation

docs/guides/error-handling.md

+12-12
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,18 @@ When using tools, errors can be handled within the tool or in the calling code:
9999

100100
```ruby
101101
# Error handling within tools
102-
class Calculator < RubyLLM::Tool
103-
description "Performs calculations"
104-
105-
param :expression,
106-
type: :string,
107-
desc: "Math expression to evaluate"
108-
109-
def execute(expression:)
110-
eval(expression).to_s
111-
rescue StandardError => e
112-
# Return error as structured data
113-
{ error: "Calculation error: #{e.message}" }
102+
class Weather < RubyLLM::Tool
103+
description "Gets current weather for a location"
104+
param :latitude, desc: "Latitude (e.g., 52.5200)"
105+
param :longitude, desc: "Longitude (e.g., 13.4050)"
106+
107+
def execute(latitude:, longitude:)
108+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
109+
110+
response = Faraday.get(url)
111+
data = JSON.parse(response.body)
112+
rescue => e
113+
{ error: e.message }
114114
end
115115
end
116116

docs/guides/rails.md

+8-12
Original file line numberDiff line numberDiff line change
@@ -235,26 +235,22 @@ In your views:
235235
Tools work seamlessly with Rails integration:
236236

237237
```ruby
238-
class Calculator < RubyLLM::Tool
239-
description "Performs arithmetic calculations"
238+
class Weather < RubyLLM::Tool
239+
description "Gets current weather for a location"
240+
param :location, desc: "City name or zip code"
240241

241-
param :expression,
242-
type: :string,
243-
desc: "Math expression to evaluate"
244-
245-
def execute(expression:)
246-
eval(expression).to_s
247-
rescue StandardError => e
248-
"Error: #{e.message}"
242+
def execute(location:)
243+
# Simulate weather lookup
244+
"15°C and sunny in #{location}"
249245
end
250246
end
251247

252248
# Add the tool to your chat
253249
chat = Chat.create!(model_id: 'gpt-4o-mini')
254-
chat.with_tool(Calculator)
250+
chat.with_tool(Weather)
255251

256252
# Ask a question that requires calculation
257-
chat.ask "What's 123 * 456?"
253+
chat.ask "What's the weather in Berlin?"
258254

259255
# Tool calls are persisted
260256
tool_call = chat.messages.find_by(role: 'assistant').tool_calls.first

docs/guides/tools.md

+63-48
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@ Common use cases include:
2929
Tools are defined as Ruby classes that inherit from `RubyLLM::Tool`:
3030

3131
```ruby
32-
class Calculator < RubyLLM::Tool
33-
description "Performs arithmetic calculations"
32+
class Weather < RubyLLM::Tool
33+
description "Gets current weather for a location"
34+
param :latitude, desc: "Latitude (e.g., 52.5200)"
35+
param :longitude, desc: "Longitude (e.g., 13.4050)"
3436

35-
param :expression,
36-
type: :string,
37-
desc: "A mathematical expression to evaluate (e.g. '2 + 2')"
37+
def execute(latitude:, longitude:)
38+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
3839

39-
def execute(expression:)
40-
eval(expression).to_s
41-
rescue StandardError => e
42-
"Error: #{e.message}"
40+
response = Faraday.get(url)
41+
data = JSON.parse(response.body)
42+
rescue => e
43+
{ error: e.message }
4344
end
4445
end
4546
```
@@ -72,37 +73,40 @@ To use a tool, attach it to a chat:
7273
chat = RubyLLM.chat
7374

7475
# Add a tool
75-
chat.with_tool(Calculator)
76+
chat.with_tool(Weather)
7677

77-
# Now you can ask questions that might require calculation
78-
response = chat.ask "What's 123 * 456?"
79-
# => "Let me calculate that for you. 123 * 456 = 56088."
78+
# Now you can ask questions that might require weather data
79+
response = chat.ask "What's the weather in Berlin? (52.5200, 13.4050)?"
80+
# => "The current weather in Berlin is as follows:\n- **Temperature:** 4.6°C\n- **Wind Speed:** 6.6 km/h\n\nPlease note that the weather information is up to date as of March 15, 2025, at 20:15 GMT."
8081
```
8182

8283
### Multiple Tools
8384

8485
You can provide multiple tools to a single chat:
8586

8687
```ruby
87-
class Weather < RubyLLM::Tool
88-
description "Gets current weather for a location"
89-
90-
param :location,
91-
desc: "City name or zip code"
92-
93-
def execute(location:)
94-
# Simulate weather lookup
95-
"72°F and sunny in #{location}"
96-
end
88+
require 'tzinfo'
89+
90+
class TimeInfo < RubyLLM::Tool
91+
description 'Gets the current time in various timezones'
92+
param :timezone,
93+
desc: "Timezone name (e.g., 'UTC', 'America/New_York')"
94+
95+
def execute(timezone:)
96+
time = TZInfo::Timezone.get(timezone).now.strftime('%Y-%m-%d %H:%M:%S')
97+
"Current time in #{timezone}: #{time}"
98+
rescue StandardError => e
99+
{ error: e.message }
100+
end
97101
end
98102

99103
# Add multiple tools
100104
chat = RubyLLM.chat
101-
.with_tools(Calculator, Weather)
105+
.with_tools(Weather, TimeInfo)
102106

103107
# Ask questions that might use either tool
104-
chat.ask "What's the temperature in New York City?"
105-
chat.ask "If it's 72°F in NYC and 54°F in Boston, what's the average?"
108+
chat.ask "What's the temperature in Rome?"
109+
chat.ask "What's the time in Tokyo?"
106110
```
107111

108112
## Custom Initialization
@@ -150,13 +154,16 @@ Here's what happens when a tool is used:
150154
For example:
151155

152156
```ruby
153-
response = chat.ask "What's 123 squared plus 456?"
157+
response = chat.ask "What's the weather like in Paris? Coordinates are 48.8566, 2.3522. Also, what time is it there?"
154158

155159
# Behind the scenes:
156-
# 1. Model decides it needs to calculate
157-
# 2. Model calls Calculator with expression: "123 * 123 + 456"
158-
# 3. Tool returns "15,585"
159-
# 4. Model incorporates this in its response
160+
# 1. Model decides it needs weather data
161+
# 2. Model calls Weather with coordinates for Paris
162+
# 3. Tool returns "Current weather: 22°C, Wind: 8 km/h"
163+
# 4. Model decides it needs time information
164+
# 5. Model calls TimeInfo with timezone "Europe/Paris"
165+
# 6. Tool returns "Current time in Europe/Paris: 2025-03-15 14:30:45 CET"
166+
# 7. Model incorporates both results in its response
160167
```
161168

162169
## Debugging Tools
@@ -168,36 +175,36 @@ Enable debugging to see tool calls in action:
168175
ENV['RUBYLLM_DEBUG'] = 'true'
169176

170177
# Make a request
171-
chat.ask "What's 15329 divided by 437?"
178+
chat.ask "What's the weather in New York? Coordinates are 40.7128, -74.0060"
172179

173180
# Console output:
174-
# D, -- RubyLLM: Tool calculator called with: {"expression"=>"15329 / 437"}
175-
# D, -- RubyLLM: Tool calculator returned: "35.078719"
181+
# D, -- RubyLLM: Tool weather_api called with: {"latitude"=>"40.7128", "longitude"=>"-74.0060"}
182+
# D, -- RubyLLM: Tool weather_api returned: "Current weather: 18°C, Wind: 12 km/h"
176183
```
177184

178185
## Error Handling
179186

180187
Tools can handle errors gracefully:
181188

182189
```ruby
183-
class Calculator < RubyLLM::Tool
184-
description "Performs arithmetic calculations"
185-
186-
param :expression,
187-
type: :string,
188-
desc: "Math expression to evaluate"
189-
190-
def execute(expression:)
191-
eval(expression).to_s
192-
rescue StandardError => e
193-
# Return error as a result
194-
{ error: "Error calculating #{expression}: #{e.message}" }
190+
class Weather < RubyLLM::Tool
191+
description "Gets current weather for a location"
192+
param :latitude, desc: "Latitude (e.g., 52.5200)"
193+
param :longitude, desc: "Longitude (e.g., 13.4050)"
194+
195+
def execute(latitude:, longitude:)
196+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
197+
198+
response = Faraday.get(url)
199+
data = JSON.parse(response.body)
200+
rescue => e
201+
{ error: e.message }
195202
end
196203
end
197204

198205
# When there's an error, the model will receive and explain it
199-
chat.ask "What's 1/0?"
200-
# => "I tried to calculate 1/0, but there was an error: divided by 0"
206+
chat.ask "What's the weather at invalid coordinates 1000, 1000?"
207+
# => "The coordinates 1000, 1000 are not valid for any location on Earth, as latitude must be between -90 and 90, and longitude must be between -180 and 180. Please provide valid coordinates or a city name for weather information."
201208
```
202209

203210
## Advanced Tool Parameters
@@ -236,6 +243,14 @@ class DataAnalysis < RubyLLM::Tool
236243
end
237244
```
238245

246+
## Security Considerations
247+
248+
When implementing tools that process user input (via the AI):
249+
250+
* Avoid using `eval`, `system` or similar methods with unsanitized input
251+
* Remember that AI models might be tricked into producing dangerous inputs
252+
* Validate all inputs and use appropriate sanitization
253+
239254
## When to Use Tools
240255

241256
Tools are best for:

docs/index.md

+13-7
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,22 @@ RubyLLM.paint "a sunset over mountains in watercolor style"
7979
RubyLLM.embed "Ruby is elegant and expressive"
8080

8181
# Let AI use your code
82-
class Calculator < RubyLLM::Tool
83-
description "Performs calculations"
84-
param :expression, type: :string, desc: "Math expression to evaluate"
85-
86-
def execute(expression:)
87-
eval(expression).to_s
82+
class Weather < RubyLLM::Tool
83+
description "Gets current weather for a location"
84+
param :latitude, desc: "Latitude (e.g., 52.5200)"
85+
param :longitude, desc: "Longitude (e.g., 13.4050)"
86+
87+
def execute(latitude:, longitude:)
88+
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
89+
90+
response = Faraday.get(url)
91+
data = JSON.parse(response.body)
92+
rescue => e
93+
{ error: e.message }
8894
end
8995
end
9096

91-
chat.with_tool(Calculator).ask "What's 123 * 456?"
97+
chat.with_tool(Weather).ask "What's the weather in Berlin? (52.5200, 13.4050)"
9298
```
9399

94100
## Quick start

lib/ruby_llm/tool.rb

+12-7
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ def initialize(name, type: 'string', desc: nil, required: true)
1818
# interface for defining parameters and implementing tool behavior.
1919
#
2020
# Example:
21-
# class Calculator < RubyLLM::Tool
22-
# description "Performs arithmetic calculations"
23-
# param :expression, type: :string, desc: "Math expression to evaluate"
21+
# require 'tzinfo'
2422
#
25-
# def execute(expression:)
26-
# eval(expression).to_s
27-
# end
28-
# end
23+
# class TimeInfo < RubyLLM::Tool
24+
# description 'Gets the current time in various timezones'
25+
# param :timezone, desc: "Timezone name (e.g., 'UTC', 'America/New_York')"
26+
#
27+
# def execute(timezone:)
28+
# time = TZInfo::Timezone.get(timezone).now.strftime('%Y-%m-%d %H:%M:%S')
29+
# "Current time in #{timezone}: #{time}"
30+
# rescue StandardError => e
31+
# { error: e.message }
32+
# end
33+
# end
2934
class Tool
3035
class << self
3136
def description(text = nil)

spec/ruby_llm/chat_streaming_spec.rb

-14
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@
66
RSpec.describe RubyLLM::Chat do
77
include_context 'with configured RubyLLM'
88

9-
class Calculator < RubyLLM::Tool # rubocop:disable Lint/ConstantDefinitionInBlock,RSpec/LeakyConstantDeclaration
10-
description 'Performs basic arithmetic'
11-
12-
param :expression,
13-
type: :string,
14-
desc: 'Math expression to evaluate'
15-
16-
def execute(expression:)
17-
eval(expression).to_s # rubocop:disable Security/Eval
18-
rescue StandardError => e
19-
"Error: #{e.message}"
20-
end
21-
end
22-
239
describe 'streaming responses' do
2410
[
2511
'claude-3-5-haiku-20241022',

0 commit comments

Comments
 (0)