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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +315 -0
- data/CHANGELOG.md +7 -0
- data/README.md +82 -1
- data/lib/riffer/agent.rb +135 -26
- data/lib/riffer/messages/tool.rb +14 -3
- data/lib/riffer/providers/amazon_bedrock.rb +120 -9
- data/lib/riffer/providers/base.rb +8 -8
- data/lib/riffer/providers/open_ai.rb +112 -38
- data/lib/riffer/providers/test.rb +60 -24
- data/lib/riffer/stream_events/tool_call_delta.rb +28 -0
- data/lib/riffer/stream_events/tool_call_done.rb +30 -0
- data/lib/riffer/tool.rb +88 -0
- data/lib/riffer/tools/param.rb +65 -0
- data/lib/riffer/tools/params.rb +112 -0
- data/lib/riffer/tools.rb +4 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +4 -0
- metadata +8 -2
- data/CLAUDE.md +0 -73
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fcc8c58e4de016fd9b5aeab39242904039dc57af45a3f2bc9cae2f92d6ca1d3
|
|
4
|
+
data.tar.gz: 000f9e8ef52c7b39977eecc13eabbe3fab61186b035fe9f3f68e18ba0056421f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79e78db4db369e2f37d1783d1ceb1a9ab64a66ad26d7ab644ef66fc075ce16a54ed18092dd892214e0efc8c2ec843ccbe32c9b9a1038331c742ad185daf24c29
|
|
7
|
+
data.tar.gz: b643bf84aeebb2a68d0dadc6865ff868bafe3fab8e3d4d52772901c619fb9b9d9ca4f2f1ca1be3d693c05de02cb2c003522cce1e1cafc8bac769c716cf70284a
|
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
|
-
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
162
|
+
response = Riffer::Messages::Assistant.new(accumulated_content, tool_calls: accumulated_tool_calls)
|
|
163
|
+
@messages << response
|
|
113
164
|
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
219
|
+
result = execute_tool_call(tool_call)
|
|
165
220
|
@messages << Riffer::Messages::Tool.new(
|
|
166
|
-
|
|
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
|
-
|
|
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
|