riffer 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/.agents/architecture.md +113 -0
- data/.agents/code-style.md +42 -0
- data/.agents/providers.md +46 -0
- data/.agents/rdoc.md +51 -0
- data/.agents/testing.md +56 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +28 -0
- data/CHANGELOG.md +17 -0
- data/README.md +26 -36
- data/Rakefile +1 -1
- data/docs/01_OVERVIEW.md +106 -0
- data/docs/02_GETTING_STARTED.md +128 -0
- data/docs/03_AGENTS.md +226 -0
- data/docs/04_TOOLS.md +251 -0
- data/docs/05_MESSAGES.md +173 -0
- data/docs/06_STREAM_EVENTS.md +191 -0
- data/docs/07_CONFIGURATION.md +195 -0
- data/docs_providers/01_PROVIDERS.md +168 -0
- data/docs_providers/02_AMAZON_BEDROCK.md +196 -0
- data/docs_providers/03_ANTHROPIC.md +211 -0
- data/docs_providers/04_OPENAI.md +157 -0
- data/docs_providers/05_TEST_PROVIDER.md +163 -0
- data/docs_providers/06_CUSTOM_PROVIDERS.md +304 -0
- data/lib/riffer/agent.rb +220 -57
- data/lib/riffer/config.rb +20 -12
- data/lib/riffer/core.rb +7 -7
- data/lib/riffer/helpers/class_name_converter.rb +6 -3
- data/lib/riffer/helpers/dependencies.rb +18 -0
- data/lib/riffer/helpers/validations.rb +9 -0
- data/lib/riffer/messages/assistant.rb +23 -1
- data/lib/riffer/messages/base.rb +15 -0
- data/lib/riffer/messages/converter.rb +15 -5
- data/lib/riffer/messages/system.rb +8 -1
- data/lib/riffer/messages/tool.rb +58 -4
- data/lib/riffer/messages/user.rb +8 -1
- data/lib/riffer/messages.rb +7 -0
- data/lib/riffer/providers/amazon_bedrock.rb +128 -13
- data/lib/riffer/providers/anthropic.rb +209 -0
- data/lib/riffer/providers/base.rb +23 -18
- data/lib/riffer/providers/open_ai.rb +119 -39
- data/lib/riffer/providers/repository.rb +9 -4
- data/lib/riffer/providers/test.rb +78 -24
- data/lib/riffer/providers.rb +6 -0
- data/lib/riffer/stream_events/base.rb +13 -1
- data/lib/riffer/stream_events/reasoning_delta.rb +15 -1
- data/lib/riffer/stream_events/reasoning_done.rb +15 -1
- data/lib/riffer/stream_events/text_delta.rb +14 -1
- data/lib/riffer/stream_events/text_done.rb +14 -1
- data/lib/riffer/stream_events/tool_call_delta.rb +35 -0
- data/lib/riffer/stream_events/tool_call_done.rb +40 -0
- data/lib/riffer/stream_events.rb +9 -0
- data/lib/riffer/tool.rb +120 -0
- data/lib/riffer/tools/param.rb +68 -0
- data/lib/riffer/tools/params.rb +118 -0
- data/lib/riffer/tools.rb +9 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +23 -19
- metadata +41 -2
- data/CLAUDE.md +0 -73
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide walks you through installing Riffer and creating your first AI agent.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add Riffer to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'riffer'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install riffer
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Provider Setup
|
|
26
|
+
|
|
27
|
+
Riffer requires an LLM provider. Install the provider gem for your chosen service:
|
|
28
|
+
|
|
29
|
+
### OpenAI
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'openai'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Configure your API key:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Riffer.configure do |config|
|
|
39
|
+
config.openai.api_key = ENV['OPENAI_API_KEY']
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Amazon Bedrock
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
gem 'aws-sdk-bedrockruntime'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Configure your credentials:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
Riffer.configure do |config|
|
|
53
|
+
config.amazon_bedrock.region = 'us-east-1'
|
|
54
|
+
# Optional: Use bearer token auth instead of IAM
|
|
55
|
+
config.amazon_bedrock.api_token = ENV['BEDROCK_API_TOKEN']
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Creating Your First Agent
|
|
60
|
+
|
|
61
|
+
Define an agent by subclassing `Riffer::Agent`:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require 'riffer'
|
|
65
|
+
|
|
66
|
+
Riffer.configure do |config|
|
|
67
|
+
config.openai.api_key = ENV['OPENAI_API_KEY']
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class GreetingAgent < Riffer::Agent
|
|
71
|
+
model 'openai/gpt-4o'
|
|
72
|
+
instructions 'You are a friendly assistant. Greet the user warmly.'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
agent = GreetingAgent.new
|
|
76
|
+
response = agent.generate('Hello!')
|
|
77
|
+
puts response
|
|
78
|
+
# => "Hello! It's wonderful to meet you..."
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Streaming Responses
|
|
82
|
+
|
|
83
|
+
Use `stream` for real-time output:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
agent = GreetingAgent.new
|
|
87
|
+
|
|
88
|
+
agent.stream('Tell me a story').each do |event|
|
|
89
|
+
case event
|
|
90
|
+
when Riffer::StreamEvents::TextDelta
|
|
91
|
+
print event.content
|
|
92
|
+
when Riffer::StreamEvents::TextDone
|
|
93
|
+
puts "\n[Done]"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Adding Tools
|
|
99
|
+
|
|
100
|
+
Tools let agents interact with external systems:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class TimeTool < Riffer::Tool
|
|
104
|
+
description "Gets the current time"
|
|
105
|
+
|
|
106
|
+
def call(context:)
|
|
107
|
+
Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class TimeAgent < Riffer::Agent
|
|
112
|
+
model 'openai/gpt-4o'
|
|
113
|
+
instructions 'You can tell the user the current time.'
|
|
114
|
+
uses_tools [TimeTool]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
agent = TimeAgent.new
|
|
118
|
+
puts agent.generate("What time is it?")
|
|
119
|
+
# => "The current time is 2024-01-15 14:30:00."
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Next Steps
|
|
123
|
+
|
|
124
|
+
- [Agents](03_AGENTS.md) - Agent configuration options
|
|
125
|
+
- [Tools](04_TOOLS.md) - Creating tools with parameters
|
|
126
|
+
- [Messages](05_MESSAGES.md) - Message types and history
|
|
127
|
+
- [Stream Events](06_STREAM_EVENTS.md) - Streaming event types
|
|
128
|
+
- [Providers](providers/01_PROVIDERS.md) - Provider-specific guides
|
data/docs/03_AGENTS.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
Agents are the central orchestrator in Riffer. They manage the conversation flow, call LLM providers, and handle tool execution.
|
|
4
|
+
|
|
5
|
+
## Defining an Agent
|
|
6
|
+
|
|
7
|
+
Create an agent by subclassing `Riffer::Agent`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class MyAgent < Riffer::Agent
|
|
11
|
+
model 'openai/gpt-4o'
|
|
12
|
+
instructions 'You are a helpful assistant.'
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configuration Methods
|
|
17
|
+
|
|
18
|
+
### model
|
|
19
|
+
|
|
20
|
+
Sets the provider and model in `provider/model` format:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class MyAgent < Riffer::Agent
|
|
24
|
+
model 'openai/gpt-4o' # OpenAI
|
|
25
|
+
# or
|
|
26
|
+
model 'amazon_bedrock/anthropic.claude-3-sonnet-20240229-v1:0' # Bedrock
|
|
27
|
+
# or
|
|
28
|
+
model 'test/any' # Test provider
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### instructions
|
|
33
|
+
|
|
34
|
+
Sets system instructions for the agent:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
class MyAgent < Riffer::Agent
|
|
38
|
+
model 'openai/gpt-4o'
|
|
39
|
+
instructions 'You are an expert Ruby programmer. Provide concise answers.'
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### identifier
|
|
44
|
+
|
|
45
|
+
Sets a custom identifier (defaults to snake_case class name):
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class MyAgent < Riffer::Agent
|
|
49
|
+
model 'openai/gpt-4o'
|
|
50
|
+
identifier 'custom_agent_name'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
MyAgent.identifier # => "custom_agent_name"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### uses_tools
|
|
57
|
+
|
|
58
|
+
Registers tools the agent can use:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class MyAgent < Riffer::Agent
|
|
62
|
+
model 'openai/gpt-4o'
|
|
63
|
+
uses_tools [WeatherTool, TimeTool]
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Tools can also be resolved dynamically with a lambda:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class MyAgent < Riffer::Agent
|
|
71
|
+
model 'openai/gpt-4o'
|
|
72
|
+
|
|
73
|
+
uses_tools ->(context) {
|
|
74
|
+
tools = [PublicTool]
|
|
75
|
+
tools << AdminTool if context&.dig(:user)&.admin?
|
|
76
|
+
tools
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### provider_options
|
|
82
|
+
|
|
83
|
+
Passes options to the provider client:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class MyAgent < Riffer::Agent
|
|
87
|
+
model 'openai/gpt-4o'
|
|
88
|
+
provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### model_options
|
|
93
|
+
|
|
94
|
+
Passes options to each LLM request:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class MyAgent < Riffer::Agent
|
|
98
|
+
model 'openai/gpt-4o'
|
|
99
|
+
model_options reasoning: 'medium', temperature: 0.7
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Instance Methods
|
|
104
|
+
|
|
105
|
+
### generate
|
|
106
|
+
|
|
107
|
+
Generates a response synchronously:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
agent = MyAgent.new
|
|
111
|
+
|
|
112
|
+
# With a string prompt
|
|
113
|
+
response = agent.generate('Hello')
|
|
114
|
+
|
|
115
|
+
# With message objects/hashes
|
|
116
|
+
response = agent.generate([
|
|
117
|
+
{role: 'user', content: 'Hello'},
|
|
118
|
+
{role: 'assistant', content: 'Hi there!'},
|
|
119
|
+
{role: 'user', content: 'How are you?'}
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
# With tool context
|
|
123
|
+
response = agent.generate('Look up my orders', tool_context: {user_id: 123})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### stream
|
|
127
|
+
|
|
128
|
+
Streams a response as an Enumerator:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
agent = MyAgent.new
|
|
132
|
+
|
|
133
|
+
agent.stream('Tell me a story').each do |event|
|
|
134
|
+
case event
|
|
135
|
+
when Riffer::StreamEvents::TextDelta
|
|
136
|
+
print event.content
|
|
137
|
+
when Riffer::StreamEvents::TextDone
|
|
138
|
+
puts "\n"
|
|
139
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
140
|
+
puts "[Tool: #{event.name}]"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### messages
|
|
146
|
+
|
|
147
|
+
Access the message history after a generate/stream call:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
agent = MyAgent.new
|
|
151
|
+
agent.generate('Hello')
|
|
152
|
+
|
|
153
|
+
agent.messages.each do |msg|
|
|
154
|
+
puts "#{msg.role}: #{msg.content}"
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### on_message
|
|
159
|
+
|
|
160
|
+
Registers a callback to receive messages as they're added during generation:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
agent.on_message do |message|
|
|
164
|
+
case message.role
|
|
165
|
+
when :assistant
|
|
166
|
+
puts "[Assistant] #{message.content}"
|
|
167
|
+
when :tool
|
|
168
|
+
puts "[Tool:#{message.name}] #{message.content}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Multiple callbacks can be registered. Returns `self` for method chaining:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
agent
|
|
177
|
+
.on_message { |msg| persist_message(msg) }
|
|
178
|
+
.on_message { |msg| log_message(msg) }
|
|
179
|
+
.generate('Hello')
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Works with both `generate` and `stream`. Only emits agent-generated messages (Assistant, Tool), not inputs (System, User).
|
|
183
|
+
|
|
184
|
+
## Class Methods
|
|
185
|
+
|
|
186
|
+
### find
|
|
187
|
+
|
|
188
|
+
Find an agent class by identifier:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
agent_class = Riffer::Agent.find('my_agent')
|
|
192
|
+
agent = agent_class.new
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### all
|
|
196
|
+
|
|
197
|
+
List all agent subclasses:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
Riffer::Agent.all.each do |agent_class|
|
|
201
|
+
puts agent_class.identifier
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Tool Execution Flow
|
|
206
|
+
|
|
207
|
+
When an agent receives a response with tool calls:
|
|
208
|
+
|
|
209
|
+
1. Agent detects `tool_calls` in the assistant message
|
|
210
|
+
2. For each tool call:
|
|
211
|
+
- Finds the matching tool class
|
|
212
|
+
- Validates arguments against the tool's parameter schema
|
|
213
|
+
- Calls the tool's `call` method with `context` and arguments
|
|
214
|
+
- Creates a Tool message with the result
|
|
215
|
+
3. Sends the updated message history back to the LLM
|
|
216
|
+
4. Repeats until no more tool calls
|
|
217
|
+
|
|
218
|
+
## Error Handling
|
|
219
|
+
|
|
220
|
+
Tool execution errors are captured and sent back to the LLM:
|
|
221
|
+
|
|
222
|
+
- `unknown_tool` - Tool not found in registered tools
|
|
223
|
+
- `validation_error` - Arguments failed validation
|
|
224
|
+
- `execution_error` - Tool raised an exception
|
|
225
|
+
|
|
226
|
+
The LLM can use this information to retry or respond appropriately.
|
data/docs/04_TOOLS.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Tools are callable functions that agents can invoke to interact with external systems, fetch data, or perform actions.
|
|
4
|
+
|
|
5
|
+
## Defining a Tool
|
|
6
|
+
|
|
7
|
+
Create a tool by subclassing `Riffer::Tool`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class WeatherTool < Riffer::Tool
|
|
11
|
+
description "Gets the current weather for a city"
|
|
12
|
+
|
|
13
|
+
params do
|
|
14
|
+
required :city, String, description: "The city name"
|
|
15
|
+
optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(context:, city:, units: nil)
|
|
19
|
+
weather = WeatherAPI.fetch(city, units: units || "celsius")
|
|
20
|
+
"The weather in #{city} is #{weather.temperature} #{units}."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration Methods
|
|
26
|
+
|
|
27
|
+
### description
|
|
28
|
+
|
|
29
|
+
Sets a description that helps the LLM understand when to use the tool:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
class SearchTool < Riffer::Tool
|
|
33
|
+
description "Searches the knowledge base for relevant information"
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### identifier / name
|
|
38
|
+
|
|
39
|
+
Sets a custom identifier (defaults to snake_case class name):
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class SearchTool < Riffer::Tool
|
|
43
|
+
identifier 'kb_search'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
SearchTool.identifier # => "kb_search"
|
|
47
|
+
SearchTool.name # => "kb_search" (alias)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### params
|
|
51
|
+
|
|
52
|
+
Defines the tool's parameters using a DSL:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class CreateOrderTool < Riffer::Tool
|
|
56
|
+
params do
|
|
57
|
+
required :product_id, Integer, description: "The product ID"
|
|
58
|
+
required :quantity, Integer, description: "Number of items"
|
|
59
|
+
optional :notes, String, description: "Order notes"
|
|
60
|
+
optional :priority, String, default: "normal", enum: ["low", "normal", "high"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Parameter DSL
|
|
66
|
+
|
|
67
|
+
### required
|
|
68
|
+
|
|
69
|
+
Defines a required parameter:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
params do
|
|
73
|
+
required :name, String, description: "The user's name"
|
|
74
|
+
required :age, Integer, description: "The user's age"
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
|
|
80
|
+
- `description` - Human-readable description for the LLM
|
|
81
|
+
- `enum` - Array of allowed values
|
|
82
|
+
|
|
83
|
+
### optional
|
|
84
|
+
|
|
85
|
+
Defines an optional parameter:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
params do
|
|
89
|
+
optional :limit, Integer, default: 10, description: "Max results"
|
|
90
|
+
optional :format, String, enum: ["json", "xml"], description: "Output format"
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
|
|
96
|
+
- `description` - Human-readable description
|
|
97
|
+
- `default` - Default value when not provided
|
|
98
|
+
- `enum` - Array of allowed values
|
|
99
|
+
|
|
100
|
+
### Supported Types
|
|
101
|
+
|
|
102
|
+
| Ruby Type | JSON Schema Type |
|
|
103
|
+
| -------------------------- | ---------------- |
|
|
104
|
+
| `String` | `string` |
|
|
105
|
+
| `Integer` | `integer` |
|
|
106
|
+
| `Float` | `number` |
|
|
107
|
+
| `TrueClass` / `FalseClass` | `boolean` |
|
|
108
|
+
| `Array` | `array` |
|
|
109
|
+
| `Hash` | `object` |
|
|
110
|
+
|
|
111
|
+
## The call Method
|
|
112
|
+
|
|
113
|
+
Every tool must implement the `call` method:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
def call(context:, **kwargs)
|
|
117
|
+
# context - The tool_context passed to agent.generate()
|
|
118
|
+
# kwargs - Validated parameters
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Accessing Context
|
|
123
|
+
|
|
124
|
+
The `context` argument receives whatever was passed to `tool_context`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class UserOrdersTool < Riffer::Tool
|
|
128
|
+
description "Gets the current user's orders"
|
|
129
|
+
|
|
130
|
+
def call(context:)
|
|
131
|
+
user_id = context&.dig(:user_id)
|
|
132
|
+
return "No user ID provided" unless user_id
|
|
133
|
+
|
|
134
|
+
orders = Order.where(user_id: user_id)
|
|
135
|
+
orders.map(&:to_s).join("\n")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Usage
|
|
140
|
+
agent.generate("Show my orders", tool_context: {user_id: 123})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Return Values
|
|
144
|
+
|
|
145
|
+
Return a string that will be sent back to the LLM:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
def call(context:, query:)
|
|
149
|
+
results = Database.search(query)
|
|
150
|
+
|
|
151
|
+
if results.empty?
|
|
152
|
+
"No results found for '#{query}'"
|
|
153
|
+
else
|
|
154
|
+
results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Timeout Configuration
|
|
160
|
+
|
|
161
|
+
Configure timeouts to prevent tools from running indefinitely. The default timeout is 10 seconds.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class SlowExternalApiTool < Riffer::Tool
|
|
165
|
+
description "Calls a slow external API"
|
|
166
|
+
timeout 30 # 30 seconds
|
|
167
|
+
|
|
168
|
+
def call(context:, query:)
|
|
169
|
+
ExternalAPI.search(query)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When a tool times out, the error is reported to the LLM with error type `:timeout_error`, allowing it to respond appropriately (e.g., suggest retrying or using a different approach).
|
|
175
|
+
|
|
176
|
+
## Validation
|
|
177
|
+
|
|
178
|
+
Arguments are automatically validated before `call` is invoked:
|
|
179
|
+
|
|
180
|
+
- Required parameters must be present
|
|
181
|
+
- Types must match the schema
|
|
182
|
+
- Enum values must be in the allowed list
|
|
183
|
+
|
|
184
|
+
Validation errors are captured and sent back to the LLM as tool results.
|
|
185
|
+
|
|
186
|
+
## JSON Schema Generation
|
|
187
|
+
|
|
188
|
+
Riffer automatically generates JSON Schema for each tool:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
WeatherTool.parameters_schema
|
|
192
|
+
# => {
|
|
193
|
+
# type: "object",
|
|
194
|
+
# properties: {
|
|
195
|
+
# "city" => {type: "string", description: "The city name"},
|
|
196
|
+
# "units" => {type: "string", enum: ["celsius", "fahrenheit"]}
|
|
197
|
+
# },
|
|
198
|
+
# required: ["city"],
|
|
199
|
+
# additionalProperties: false
|
|
200
|
+
# }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Registering Tools with Agents
|
|
204
|
+
|
|
205
|
+
### Static Registration
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
class MyAgent < Riffer::Agent
|
|
209
|
+
model 'openai/gpt-4o'
|
|
210
|
+
uses_tools [WeatherTool, SearchTool]
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Dynamic Registration
|
|
215
|
+
|
|
216
|
+
Use a lambda for context-aware tool resolution:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
class MyAgent < Riffer::Agent
|
|
220
|
+
model 'openai/gpt-4o'
|
|
221
|
+
|
|
222
|
+
uses_tools ->(context) {
|
|
223
|
+
tools = [PublicSearchTool]
|
|
224
|
+
|
|
225
|
+
if context&.dig(:user)&.premium?
|
|
226
|
+
tools << PremiumAnalyticsTool
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if context&.dig(:user)&.admin?
|
|
230
|
+
tools << AdminTool
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
tools
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Error Handling
|
|
239
|
+
|
|
240
|
+
Errors in tools are captured and reported back to the LLM:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
def call(context:, query:)
|
|
244
|
+
raise "API rate limit exceeded"
|
|
245
|
+
rescue => e
|
|
246
|
+
# Error is caught by Riffer and sent as tool result:
|
|
247
|
+
# "Error executing tool: API rate limit exceeded"
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The LLM can then decide how to respond (retry, apologize, ask for different input, etc.).
|