riffer 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8f9dc7979fb2338edff2e1c6fb7a6a0281126e95ac8b12bd9e268385dd56263
4
- data.tar.gz: a92ec2768bcb0efc3524945f5a69b685ca16dbba40ee852d6c24efdfbde883fd
3
+ metadata.gz: 9fcc8c58e4de016fd9b5aeab39242904039dc57af45a3f2bc9cae2f92d6ca1d3
4
+ data.tar.gz: 000f9e8ef52c7b39977eecc13eabbe3fab61186b035fe9f3f68e18ba0056421f
5
5
  SHA512:
6
- metadata.gz: 62f476d0a84c1ac94520eebf8e11d7773299e0c87a56d9ea473dfbde310ec9a905da9f3446b2a76e5d282e5f52dcf82857397836f3d300d93cab222aceb12065
7
- data.tar.gz: 5518dd2966cb8d62553778fe6428d3e339bfe78293dd0eaed4a82b5500f80d2e78ee6f4648c46f62fffd258c293e7894e624ff117a668fab7f5d3a908d246cae
6
+ metadata.gz: 79e78db4db369e2f37d1783d1ceb1a9ab64a66ad26d7ab644ef66fc075ce16a54ed18092dd892214e0efc8c2ec843ccbe32c9b9a1038331c742ad185daf24c29
7
+ data.tar.gz: b643bf84aeebb2a68d0dadc6865ff868bafe3fab8e3d4d52772901c619fb9b9d9ca4f2f1ca1be3d693c05de02cb2c003522cce1e1cafc8bac769c716cf70284a
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.1"
2
+ ".": "0.7.0"
3
3
  }
data/AGENTS.md ADDED
@@ -0,0 +1,315 @@
1
+ # AI Agent Development Guide
2
+
3
+ This guide provides comprehensive information for AI coding assistants working with the Riffer codebase.
4
+
5
+ ## Project Overview
6
+
7
+ Riffer is a Ruby gem framework for building AI-powered applications and agents. It provides a complete toolkit for integrating artificial intelligence capabilities into Ruby projects with a minimal, well-documented core.
8
+
9
+ Key concepts:
10
+
11
+ - **Agents** – orchestrate messages, LLM calls, and tool execution (`Riffer::Agent`)
12
+ - **Providers** – adapters that implement text generation and streaming (`Riffer::Providers::*`)
13
+ - **Messages** – typed message objects for system, user, assistant, and tool messages (`Riffer::Messages::*`)
14
+ - **StreamEvents** – structured events for streaming (`Riffer::StreamEvents::*`)
15
+
16
+ ## Architecture
17
+
18
+ ### Core Components
19
+
20
+ #### Agent (`lib/riffer/agent.rb`)
21
+
22
+ Base class for AI agents. Subclass and use DSL methods `model` and `instructions` to configure. Orchestrates message flow, LLM calls, and tool execution via a generate/stream loop.
23
+
24
+ Example:
25
+
26
+ ```ruby
27
+ class EchoAgent < Riffer::Agent
28
+ model 'openai/gpt-5-mini' # provider/model
29
+ instructions 'You are an assistant that repeats what the user says.'
30
+ end
31
+
32
+ agent = EchoAgent.new
33
+ puts agent.generate('Hello world')
34
+ ```
35
+
36
+ #### Providers (`lib/riffer/providers/`)
37
+
38
+ Adapters for LLM APIs. Each provider extends `Riffer::Providers::Base` and implements:
39
+
40
+ - `perform_generate_text(messages, model:)` – returns `Riffer::Messages::Assistant`
41
+ - `perform_stream_text(messages, model:)` – returns an `Enumerator` yielding stream events
42
+
43
+ Providers are registered in `Riffer::Providers::Repository::REPO` with identifiers (e.g., `openai`, `amazon_bedrock`).
44
+
45
+ #### Messages (`lib/riffer/messages/`)
46
+
47
+ Typed message objects that extend `Riffer::Messages::Base`:
48
+
49
+ - `System` – system instructions
50
+ - `User` – user input
51
+ - `Assistant` – AI responses
52
+ - `Tool` – tool execution results
53
+
54
+ The `Converter` module handles hash-to-object conversion.
55
+
56
+ #### StreamEvents (`lib/riffer/stream_events/`)
57
+
58
+ Structured events for streaming responses:
59
+
60
+ - `TextDelta` – incremental text chunks
61
+ - `TextDone` – completion signals
62
+ - `ReasoningDelta` – reasoning process chunks
63
+ - `ReasoningDone` – reasoning completion
64
+
65
+ ### Key Patterns
66
+
67
+ - Model strings use `provider/model` format (e.g., `openai/gpt-4`)
68
+ - Configuration via `Riffer.configure { |c| c.openai.api_key = "..." }`
69
+ - Providers use `depends_on` helper for runtime dependency checking
70
+ - Tests use VCR cassettes in `test/fixtures/vcr_cassettes/`
71
+ - Zeitwerk for autoloading – file structure must match module/class names
72
+
73
+ ## Code Style & Standards
74
+
75
+ ### Ruby Version
76
+
77
+ - Minimum Ruby version: 3.2.0
78
+ - Use modern Ruby 3.x features and syntax
79
+
80
+ ### Code Formatting
81
+
82
+ - Use StandardRB for linting and formatting
83
+ - All Ruby files must include `# frozen_string_literal: true` at the top
84
+ - Follow StandardRB conventions (2-space indentation, double quotes for strings)
85
+ - Custom RuboCop rules are defined in `.standard.yml`
86
+
87
+ ### Testing
88
+
89
+ - Use Minitest for all tests with the spec DSL
90
+ - Test files go in `test/` directory with `*_test.rb` suffix
91
+ - Tests must pass before committing
92
+ - Use Minitest assertions: `assert_equal`, `assert_instance_of`, `refute_nil`, etc.
93
+ - Prefer using `setup` and `teardown` methods for test setup/cleanup
94
+
95
+ #### Test Structure
96
+
97
+ ```ruby
98
+ # frozen_string_literal: true
99
+
100
+ require "test_helper"
101
+
102
+ describe Riffer::Feature do
103
+ describe "#method_name" do
104
+ before do
105
+ # setup code
106
+ end
107
+
108
+ it "does something expected" do
109
+ result = Riffer::Feature.method_name(args)
110
+ assert_equal expected, result
111
+ end
112
+
113
+ it "handles edge case" do
114
+ result = Riffer::Feature.method_name(edge_case_args)
115
+ assert_equal edge_case_expected, result
116
+ end
117
+ end
118
+ end
119
+ ```
120
+
121
+ #### Test Coverage
122
+
123
+ - Test public APIs thoroughly
124
+ - Test edge cases and error conditions
125
+ - Mock external dependencies
126
+ - Keep tests fast and isolated
127
+ - Stick to the single assertion rule where possible
128
+
129
+ ### Documentation
130
+
131
+ - Use YARD-style comments for public APIs
132
+ - Document parameters with `@param`
133
+ - Document return values with `@return`
134
+ - Document raised errors with `@raise`
135
+
136
+ ### Comments
137
+
138
+ - Only add comments when the code is ambiguous or not semantically obvious
139
+ - Avoid stating what the code does if it's clear from reading the code itself
140
+ - Use comments to explain **why** something is done, not **what** is being done
141
+ - Comments should add value beyond what the code already expresses
142
+
143
+ ### Error Handling
144
+
145
+ - Define custom errors as subclasses of `Riffer::Error`
146
+ - Use descriptive error messages
147
+ - Document errors that can be raised
148
+
149
+ ## Project Structure
150
+
151
+ ```
152
+ lib/
153
+ riffer.rb # Main entry point, uses Zeitwerk for autoloading
154
+ riffer/
155
+ version.rb # VERSION constant
156
+ config.rb # Configuration class
157
+ core.rb # Core functionality
158
+ agent.rb # Agent class
159
+ messages.rb # Messages namespace/module
160
+ providers.rb # Providers namespace/module
161
+ stream_events.rb # Stream events namespace/module
162
+ helpers/
163
+ class_name_converter.rb # Class name conversion utilities
164
+ dependencies.rb # Dependency management
165
+ validations.rb # Validation helpers
166
+ messages/
167
+ base.rb # Base message class
168
+ assistant.rb # Assistant message
169
+ converter.rb # Message converter
170
+ system.rb # System message
171
+ user.rb # User message
172
+ tool.rb # Tool message
173
+ providers/
174
+ base.rb # Base provider class
175
+ open_ai.rb # OpenAI provider
176
+ amazon_bedrock.rb # Amazon Bedrock provider
177
+ repository.rb # Provider registry
178
+ test.rb # Test provider
179
+ stream_events/
180
+ base.rb # Base stream event
181
+ text_delta.rb # Text delta event
182
+ text_done.rb # Text done event
183
+ reasoning_delta.rb # Reasoning delta event
184
+ reasoning_done.rb # Reasoning done event
185
+ test/
186
+ test_helper.rb # Minitest configuration with VCR
187
+ riffer_test.rb # Main module tests
188
+ riffer/
189
+ [feature]_test.rb # Feature tests mirror lib/riffer/ structure
190
+ ```
191
+
192
+ ## Development Workflow
193
+
194
+ ### Autoloading with Zeitwerk
195
+
196
+ - The project uses Zeitwerk for autoloading
197
+ - File structure must match module/class names
198
+ - No explicit `require` statements needed for lib files
199
+ - Special inflections are configured in `lib/riffer.rb` (e.g., `open_ai.rb` → `OpenAI`)
200
+
201
+ ### Adding New Features
202
+
203
+ 1. Create feature files under `lib/riffer/` following Zeitwerk conventions
204
+ 2. File names should be snake_case, class names should be PascalCase
205
+ 3. Create corresponding tests in `test/riffer/` mirroring the lib structure
206
+ 4. Run tests: `rake test`
207
+ 5. Check code style: `rake standard`
208
+
209
+ ### Adding a New Provider
210
+
211
+ 1. Create `lib/riffer/providers/your_provider.rb` extending `Riffer::Providers::Base`
212
+ 2. Implement `perform_generate_text(messages, model:)` returning `Riffer::Messages::Assistant`
213
+ 3. Implement `perform_stream_text(messages, model:)` returning an `Enumerator` yielding stream events
214
+ 4. Register in `Riffer::Providers::Repository::REPO`
215
+ 5. Add provider config to `Riffer::Config` if needed
216
+ 6. Create corresponding test file in `test/riffer/providers/`
217
+
218
+ ### Dependencies
219
+
220
+ - Add runtime dependencies in `riffer.gemspec` using `spec.add_dependency`
221
+ - Add development dependencies in `Gemfile`
222
+ - Document significant dependencies in README
223
+
224
+ ### Version Management
225
+
226
+ - Update version in `lib/riffer/version.rb`
227
+ - Follow Semantic Versioning (MAJOR.MINOR.PATCH)
228
+ - Update CHANGELOG.md with changes
229
+
230
+ ## AI/Agent Development Context
231
+
232
+ Since Riffer is an AI framework, when working on AI-related features:
233
+
234
+ - Consider integration with common AI APIs (OpenAI, Anthropic, Amazon Bedrock, etc.)
235
+ - Design for extensibility and plugin architecture
236
+ - Handle API rate limiting and retries
237
+ - Implement proper error handling for external services
238
+ - Consider streaming responses where applicable
239
+ - Think about token counting and cost management
240
+ - Support async/concurrent operations where beneficial
241
+
242
+ ## Commands Reference
243
+
244
+ ```bash
245
+ # Install dependencies
246
+ bin/setup
247
+
248
+ # Run tests
249
+ bundle exec rake test
250
+
251
+ # Run a single test file
252
+ bundle exec ruby -Ilib:test test/riffer/agent_test.rb
253
+
254
+ # Run a specific test by name
255
+ bundle exec ruby -Ilib:test test/riffer/agent_test.rb --name "test_something"
256
+
257
+ # Check code style
258
+ bundle exec rake standard
259
+
260
+ # Auto-fix style issues
261
+ bundle exec rake standard:fix
262
+
263
+ # Run both tests and linting (default task)
264
+ bundle exec rake
265
+
266
+ # Interactive console
267
+ bin/console
268
+
269
+ # Generate documentation
270
+ bundle exec rake docs
271
+
272
+ # Install gem locally
273
+ bundle exec rake install
274
+
275
+ # Release new version (maintainers only)
276
+ bundle exec rake release
277
+ ```
278
+
279
+ ## Code Patterns
280
+
281
+ ### Module Structure
282
+
283
+ ```ruby
284
+ # frozen_string_literal: true
285
+
286
+ module Riffer::Feature
287
+ class MyClass
288
+ # Implementation
289
+ end
290
+ end
291
+ ```
292
+
293
+ ### Configuration Example
294
+
295
+ ```ruby
296
+ Riffer.configure do |config|
297
+ config.openai.api_key = ENV['OPENAI_API_KEY']
298
+ end
299
+ ```
300
+
301
+ ### Streaming Example
302
+
303
+ ```ruby
304
+ agent = EchoAgent.new
305
+ agent.stream('Tell me a story').each do |event|
306
+ print event.content
307
+ end
308
+ ```
309
+
310
+ ## Important Notes
311
+
312
+ - Always run `rake` (runs both test and standard) before committing
313
+ - Keep the README updated with new features
314
+ - Follow the Code of Conduct in all interactions
315
+ - This gem is MIT licensed – keep license headers consistent
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0](https://github.com/janeapp/riffer/compare/riffer/v0.6.1...riffer/v0.7.0) (2026-01-21)
9
+
10
+
11
+ ### Features
12
+
13
+ * tool calling support ([#82](https://github.com/janeapp/riffer/issues/82)) ([0b2676a](https://github.com/janeapp/riffer/commit/0b2676a77e93b3fd55041e66a5c8c0ab6762e3d2))
14
+
8
15
  ## [0.6.1](https://github.com/janeapp/riffer/compare/riffer/v0.6.0...riffer/v0.6.1) (2026-01-16)
9
16
 
10
17
 
data/README.md CHANGED
@@ -11,13 +11,15 @@ Riffer is a comprehensive Ruby framework designed to simplify the development of
11
11
  Key concepts:
12
12
 
13
13
  - **Agents** – orchestrate messages, LLM calls, and tool execution (`Riffer::Agent`).
14
+ - **Tools** – define callable functions that agents can use to interact with external systems (`Riffer::Tool`).
14
15
  - **Providers** – adapters that implement text generation and streaming (`Riffer::Providers::*`).
15
16
  - **Messages** – typed message objects for system, user, assistant, and tool messages (`Riffer::Messages::*`).
16
17
 
17
18
  ## Features
18
19
 
19
20
  - Minimal, well-documented core for building AI agents
20
- - Provider abstraction (OpenAI) for pluggable providers
21
+ - Tool calling support with parameter validation
22
+ - Provider abstraction (OpenAI, Amazon Bedrock) for pluggable providers
21
23
  - Streaming support and structured stream events
22
24
  - Message converters and helpers for robust message handling
23
25
 
@@ -76,6 +78,85 @@ agent.stream('Tell me a story').each do |event|
76
78
  end
77
79
  ```
78
80
 
81
+ ### Provider & Model Options
82
+
83
+ Agents support two optional configuration methods for passing options through to the underlying provider:
84
+
85
+ ```ruby
86
+ class MyAgent < Riffer::Agent
87
+ model 'openai/gpt-4o'
88
+ instructions 'You are a helpful assistant.'
89
+
90
+ # Options passed directly to the provider client (e.g., OpenAI::Client)
91
+ provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
92
+
93
+ # Options passed to the model invocation (e.g., reasoning, temperature)
94
+ model_options reasoning: 'medium'
95
+ end
96
+ ```
97
+
98
+ - `provider_options` - Hash of options passed to the provider client during instantiation
99
+ - `model_options` - Hash of options passed to `generate_text` / `stream_text` calls
100
+
101
+ ### Tools
102
+
103
+ Tools allow agents to interact with external systems. Define a tool by extending `Riffer::Tool`:
104
+
105
+ ```ruby
106
+ class WeatherLookupTool < Riffer::Tool
107
+ description "Provides current weather information for a specified city."
108
+
109
+ params do
110
+ required :city, String, description: "The city to look up"
111
+ optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
112
+ end
113
+
114
+ def call(context:, city:, units: nil)
115
+ weather = WeatherService.lookup(city, units: units || "celsius")
116
+ "The weather in #{city} is #{weather.temperature} #{units}."
117
+ end
118
+ end
119
+ ```
120
+
121
+ Register tools with an agent using `uses_tools`:
122
+
123
+ ```ruby
124
+ class WeatherAgent < Riffer::Agent
125
+ model 'openai/gpt-4o'
126
+ instructions 'You are a helpful weather assistant.'
127
+
128
+ uses_tools [WeatherLookupTool]
129
+ end
130
+
131
+ agent = WeatherAgent.new
132
+ puts agent.generate("What's the weather in Toronto?")
133
+ ```
134
+
135
+ Tools can also be dynamically resolved at runtime. The lambda receives `tool_context` when it accepts a parameter, enabling conditional tool resolution based on the current user or request:
136
+
137
+ ```ruby
138
+ class DynamicAgent < Riffer::Agent
139
+ model 'openai/gpt-4o'
140
+
141
+ uses_tools ->(context) {
142
+ tools = [WeatherLookupTool]
143
+ tools << AdminTool if context&.dig(:current_user)&.admin?
144
+ tools
145
+ }
146
+ end
147
+
148
+ agent = DynamicAgent.new
149
+ agent.generate("Do admin things", tool_context: { current_user: current_user })
150
+ ```
151
+
152
+ Pass context to tools using `tool_context`:
153
+
154
+ ```ruby
155
+ agent.generate("Look up my city", tool_context: { user_id: current_user.id })
156
+ ```
157
+
158
+ The `context` keyword argument is passed to every tool's `call` method, allowing tools to access shared state like user information, database connections, or API clients.
159
+
79
160
  ## Development
80
161
 
81
162
  After checking out the repo, run:
data/lib/riffer/agent.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  # Riffer::Agent is the base class for all agents in the Riffer framework.
4
6
  #
5
7
  # Provides orchestration for LLM calls, tool use, and message management.
@@ -40,10 +42,28 @@ class Riffer::Agent
40
42
  @instructions = instructions_text
41
43
  end
42
44
 
43
- def reasoning(level = nil)
44
- return @reasoning if level.nil?
45
- validate_is_string!(level, "reasoning")
46
- @reasoning = level
45
+ # Gets or sets provider options passed to the provider client
46
+ # @param options [Hash, nil] the options to set, or nil to get
47
+ # @return [Hash] the provider options
48
+ def provider_options(options = nil)
49
+ return @provider_options || {} if options.nil?
50
+ @provider_options = options
51
+ end
52
+
53
+ # Gets or sets model options passed to generate_text/stream_text
54
+ # @param options [Hash, nil] the options to set, or nil to get
55
+ # @return [Hash] the model options
56
+ def model_options(options = nil)
57
+ return @model_options || {} if options.nil?
58
+ @model_options = options
59
+ end
60
+
61
+ # Gets or sets the tools used by this agent
62
+ # @param tools_or_lambda [Array<Class>, Proc, nil] tools array or lambda returning tools
63
+ # @return [Array<Class>, Proc, nil] the tools configuration
64
+ def uses_tools(tools_or_lambda = nil)
65
+ return @tools_config if tools_or_lambda.nil?
66
+ @tools_config = tools_or_lambda
47
67
  end
48
68
 
49
69
  # Finds an agent class by identifier
@@ -71,7 +91,6 @@ class Riffer::Agent
71
91
  @messages = []
72
92
  @model_string = self.class.model
73
93
  @instructions_text = self.class.instructions
74
- @reasoning = self.class.reasoning
75
94
 
76
95
  provider_name, model_name = @model_string.split("/", 2)
77
96
 
@@ -83,8 +102,11 @@ class Riffer::Agent
83
102
 
84
103
  # Generates a response from the agent
85
104
  # @param prompt_or_messages [String, Array<Hash, Riffer::Messages::Base>]
105
+ # @param tool_context [Object, nil] optional context object passed to all tool calls
86
106
  # @return [String]
87
- def generate(prompt_or_messages)
107
+ def generate(prompt_or_messages, tool_context: nil)
108
+ @tool_context = tool_context
109
+ @resolved_tools = nil
88
110
  initialize_messages(prompt_or_messages)
89
111
 
90
112
  loop do
@@ -101,26 +123,49 @@ class Riffer::Agent
101
123
 
102
124
  # Streams a response from the agent
103
125
  # @param prompt_or_messages [String, Array<Hash, Riffer::Messages::Base>]
126
+ # @param tool_context [Object, nil] optional context object passed to all tool calls
104
127
  # @return [Enumerator] an enumerator yielding stream events
105
- def stream(prompt_or_messages)
128
+ def stream(prompt_or_messages, tool_context: nil)
129
+ @tool_context = tool_context
130
+ @resolved_tools = nil
106
131
  initialize_messages(prompt_or_messages)
107
132
 
108
133
  Enumerator.new do |yielder|
109
- accumulated_content = ""
134
+ loop do
135
+ accumulated_content = ""
136
+ accumulated_tool_calls = []
137
+ current_tool_call = nil
138
+
139
+ call_llm_stream.each do |event|
140
+ yielder << event
141
+
142
+ case event
143
+ when Riffer::StreamEvents::TextDelta
144
+ accumulated_content += event.content
145
+ when Riffer::StreamEvents::TextDone
146
+ accumulated_content = event.content
147
+ when Riffer::StreamEvents::ToolCallDelta
148
+ current_tool_call ||= {item_id: event.item_id, name: event.name, arguments: ""}
149
+ current_tool_call[:arguments] += event.arguments_delta
150
+ current_tool_call[:name] ||= event.name
151
+ when Riffer::StreamEvents::ToolCallDone
152
+ accumulated_tool_calls << {
153
+ id: event.item_id,
154
+ call_id: event.call_id,
155
+ name: event.name,
156
+ arguments: event.arguments
157
+ }
158
+ current_tool_call = nil
159
+ end
160
+ end
110
161
 
111
- call_llm_stream.each do |event|
112
- yielder << event
162
+ response = Riffer::Messages::Assistant.new(accumulated_content, tool_calls: accumulated_tool_calls)
163
+ @messages << response
113
164
 
114
- case event
115
- when Riffer::StreamEvents::TextDelta
116
- accumulated_content += event.content
117
- when Riffer::StreamEvents::TextDone
118
- accumulated_content = event.content
119
- end
120
- end
165
+ break unless has_tool_calls?(response)
121
166
 
122
- response = Riffer::Messages::Assistant.new(accumulated_content)
123
- @messages << response
167
+ execute_tool_calls(response)
168
+ end
124
169
  end
125
170
  end
126
171
 
@@ -140,18 +185,28 @@ class Riffer::Agent
140
185
  end
141
186
 
142
187
  def call_llm
143
- provider_instance.generate_text(messages: @messages, model: @model_name, reasoning: @reasoning)
188
+ provider_instance.generate_text(
189
+ messages: @messages,
190
+ model: @model_name,
191
+ tools: resolved_tools,
192
+ **self.class.model_options
193
+ )
144
194
  end
145
195
 
146
196
  def call_llm_stream
147
- provider_instance.stream_text(messages: @messages, model: @model_name, reasoning: @reasoning)
197
+ provider_instance.stream_text(
198
+ messages: @messages,
199
+ model: @model_name,
200
+ tools: resolved_tools,
201
+ **self.class.model_options
202
+ )
148
203
  end
149
204
 
150
205
  def provider_instance
151
206
  @provider_instance ||= begin
152
207
  provider_class = Riffer::Providers::Repository.find(@provider_name)
153
208
  raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
154
- provider_class.new
209
+ provider_class.new(**self.class.provider_options)
155
210
  end
156
211
  end
157
212
 
@@ -161,17 +216,71 @@ class Riffer::Agent
161
216
 
162
217
  def execute_tool_calls(response)
163
218
  response.tool_calls.each do |tool_call|
164
- tool_result = execute_tool_call(tool_call)
219
+ result = execute_tool_call(tool_call)
165
220
  @messages << Riffer::Messages::Tool.new(
166
- tool_result,
221
+ result[:content],
167
222
  tool_call_id: tool_call[:id],
168
- name: tool_call[:name]
223
+ name: tool_call[:name],
224
+ error: result[:error],
225
+ error_type: result[:error_type]
169
226
  )
170
227
  end
171
228
  end
172
229
 
173
230
  def execute_tool_call(tool_call)
174
- "Tool execution not implemented yet"
231
+ tool_class = find_tool_class(tool_call[:name])
232
+
233
+ if tool_class.nil?
234
+ return {
235
+ content: "Error: Unknown tool '#{tool_call[:name]}'",
236
+ error: "Unknown tool '#{tool_call[:name]}'",
237
+ error_type: :unknown_tool
238
+ }
239
+ end
240
+
241
+ tool_instance = tool_class.new
242
+ arguments = parse_tool_arguments(tool_call[:arguments])
243
+
244
+ begin
245
+ result = tool_instance.call_with_validation(context: @tool_context, **arguments)
246
+ {content: result.to_s, error: nil, error_type: nil}
247
+ rescue Riffer::ValidationError => e
248
+ {
249
+ content: "Validation error: #{e.message}",
250
+ error: e.message,
251
+ error_type: :validation_error
252
+ }
253
+ rescue => e
254
+ {
255
+ content: "Error executing tool: #{e.message}",
256
+ error: e.message,
257
+ error_type: :execution_error
258
+ }
259
+ end
260
+ end
261
+
262
+ def resolved_tools
263
+ @resolved_tools ||= begin
264
+ config = self.class.uses_tools
265
+ return [] if config.nil?
266
+
267
+ if config.is_a?(Proc)
268
+ (config.arity == 0) ? config.call : config.call(@tool_context)
269
+ else
270
+ config
271
+ end
272
+ end
273
+ end
274
+
275
+ def find_tool_class(name)
276
+ resolved_tools.find { |tool_class| tool_class.name == name }
277
+ end
278
+
279
+ def parse_tool_arguments(arguments)
280
+ return {} if arguments.nil? || arguments.empty?
281
+
282
+ args = arguments.is_a?(String) ? JSON.parse(arguments) : arguments
283
+ args.transform_keys(&:to_sym)
175
284
  end
176
285
 
177
286
  def extract_final_response