riffer 0.40.0 → 0.42.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.md → .claude/CLAUDE.md} +0 -8
- data/.claude/rules/comments.md +13 -0
- data/.claude/rules/rbs-inline.md +93 -0
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +22 -0
- data/README.md +20 -24
- data/docs/AGENTS.md +24 -14
- data/docs/AGENT_LIFECYCLE.md +2 -2
- data/docs/CONFIGURATION.md +105 -21
- data/docs/SERIALIZATION.md +4 -3
- data/docs/TOOLS.md +18 -1
- data/docs/providers/AMAZON_BEDROCK.md +14 -8
- data/docs/providers/ANTHROPIC.md +9 -7
- data/docs/providers/AZURE_OPENAI.md +12 -12
- data/docs/providers/CUSTOM_PROVIDERS.md +32 -16
- data/docs/providers/GEMINI.md +28 -5
- data/docs/providers/OPENAI.md +21 -5
- data/docs/providers/OPENROUTER.md +11 -6
- data/docs/providers/PROVIDERS.md +16 -1
- data/lib/riffer/agent/config.rb +0 -6
- data/lib/riffer/agent/run.rb +2 -4
- data/lib/riffer/agent/serializer.rb +0 -2
- data/lib/riffer/agent.rb +4 -26
- data/lib/riffer/config.rb +6 -6
- data/lib/riffer/evals/judge.rb +3 -5
- data/lib/riffer/guardrail.rb +1 -1
- data/lib/riffer/helpers/identifier.rb +41 -0
- data/lib/riffer/providers/amazon_bedrock.rb +31 -19
- data/lib/riffer/providers/anthropic.rb +23 -8
- data/lib/riffer/providers/azure_open_ai.rb +19 -11
- data/lib/riffer/providers/base.rb +32 -1
- data/lib/riffer/providers/gemini/client.rb +120 -0
- data/lib/riffer/providers/gemini.rb +13 -62
- data/lib/riffer/providers/mock.rb +5 -4
- data/lib/riffer/providers/open_ai.rb +23 -8
- data/lib/riffer/providers/open_router.rb +23 -9
- data/lib/riffer/registrable.rb +81 -0
- data/lib/riffer/tool.rb +1 -0
- data/lib/riffer/tools/toolable.rb +2 -3
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +3 -0
- data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -2
- data/sig/_private/riffer/providers/anthropic.rbs +4 -2
- data/sig/_private/riffer/providers/gemini.rbs +7 -0
- data/sig/_private/riffer/providers/open_ai.rbs +4 -2
- data/sig/_private/riffer/providers/open_router.rbs +4 -2
- data/sig/generated/riffer/agent/config.rbs +1 -5
- data/sig/generated/riffer/agent.rbs +2 -18
- data/sig/generated/riffer/config.rbs +25 -15
- data/sig/generated/riffer/evals/judge.rbs +2 -4
- data/sig/generated/riffer/helpers/identifier.rbs +19 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +13 -2
- data/sig/generated/riffer/providers/anthropic.rbs +13 -2
- data/sig/generated/riffer/providers/azure_open_ai.rbs +14 -4
- data/sig/generated/riffer/providers/base.rbs +22 -0
- data/sig/generated/riffer/providers/gemini/client.rbs +65 -0
- data/sig/generated/riffer/providers/gemini.rbs +7 -23
- data/sig/generated/riffer/providers/mock.rbs +4 -3
- data/sig/generated/riffer/providers/open_ai.rbs +13 -2
- data/sig/generated/riffer/providers/open_router.rbs +16 -3
- data/sig/generated/riffer/registrable.rbs +51 -0
- data/sig/generated/riffer/tool.rbs +2 -0
- data/sig/generated/riffer/tools/toolable.rbs +3 -1
- data/sig/generated/riffer.rbs +4 -0
- data/sig/manual/riffer/agent.rbs +7 -0
- data/sig/manual/riffer/helpers/identifier.rbs +5 -0
- data/sig/manual/riffer/tool.rbs +7 -0
- metadata +14 -11
- data/.agents/architecture.md +0 -265
- data/.agents/code-style.md +0 -110
- data/.agents/providers.md +0 -45
- data/.agents/rbs-inline.md +0 -169
- data/.agents/testing.md +0 -60
- data/CLAUDE.md +0 -1
- data/lib/riffer/helpers/class_name_converter.rb +0 -22
- data/sig/generated/riffer/helpers/class_name_converter.rbs +0 -12
- data/sig/manual/riffer/helpers/class_name_converter.rbs +0 -5
|
@@ -14,9 +14,8 @@ gem 'openai'
|
|
|
14
14
|
|
|
15
15
|
Credentials are resolved in order:
|
|
16
16
|
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3. Environment variables (`AZURE_OPENAI_API_KEY` / `AZURE_OPENAI_ENDPOINT`)
|
|
17
|
+
1. Global config (`Riffer.config.azure_openai.api_key` / `.endpoint`)
|
|
18
|
+
2. Environment variables (`AZURE_OPENAI_API_KEY` / `AZURE_OPENAI_ENDPOINT`)
|
|
20
19
|
|
|
21
20
|
### Global config
|
|
22
21
|
|
|
@@ -27,16 +26,20 @@ Riffer.configure do |config|
|
|
|
27
26
|
end
|
|
28
27
|
```
|
|
29
28
|
|
|
30
|
-
###
|
|
29
|
+
### Custom client
|
|
30
|
+
|
|
31
|
+
Azure AD token auth, timeouts, and retries are configured on your own `OpenAI::Client`. A `Proc` is the right shape for expiring AD tokens — it is resolved on every LLM call:
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
Riffer.configure do |config|
|
|
35
|
+
config.azure_openai.client = -> {
|
|
36
|
+
OpenAI::Client.new(api_key: AzureAd.current_token, base_url: ENV['AZURE_OPENAI_ENDPOINT'])
|
|
37
|
+
}
|
|
37
38
|
end
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
42
|
+
|
|
40
43
|
### Environment variables only
|
|
41
44
|
|
|
42
45
|
If `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` are set, no explicit configuration is needed:
|
|
@@ -151,10 +154,7 @@ end
|
|
|
151
154
|
## Direct Provider Usage
|
|
152
155
|
|
|
153
156
|
```ruby
|
|
154
|
-
provider = Riffer::Providers::AzureOpenAI.new
|
|
155
|
-
api_key: ENV['AZURE_OPENAI_API_KEY'],
|
|
156
|
-
base_url: ENV['AZURE_OPENAI_ENDPOINT']
|
|
157
|
-
)
|
|
157
|
+
provider = Riffer::Providers::AzureOpenAI.new
|
|
158
158
|
|
|
159
159
|
response = provider.generate_text(
|
|
160
160
|
prompt: "Hello!",
|
|
@@ -8,14 +8,14 @@ Extend `Riffer::Providers::Base` and implement the five required hook methods:
|
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
11
|
-
def initialize(**options)
|
|
12
|
-
# Initialize your client
|
|
13
|
-
@api_key = options[:api_key] || ENV['MY_PROVIDER_API_KEY']
|
|
14
|
-
@client = MyProviderClient.new(api_key: @api_key)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
11
|
private
|
|
18
12
|
|
|
13
|
+
# Client hook — see "Client resolution" below.
|
|
14
|
+
|
|
15
|
+
def build_client
|
|
16
|
+
MyProviderClient.new(api_key: ENV['MY_PROVIDER_API_KEY'])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
19
|
# Hook methods (matching base.rb order)
|
|
20
20
|
|
|
21
21
|
def build_request_params(messages, model, options)
|
|
@@ -35,11 +35,11 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def execute_generate(params)
|
|
38
|
-
|
|
38
|
+
client.generate(**params)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def execute_stream(params, yielder)
|
|
42
|
-
|
|
42
|
+
client.stream(**params) do |chunk|
|
|
43
43
|
case chunk.type
|
|
44
44
|
when :text
|
|
45
45
|
yielder << Riffer::StreamEvents::TextDelta.new(chunk.content)
|
|
@@ -121,16 +121,30 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
121
121
|
end
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
## Client resolution
|
|
125
|
+
|
|
126
|
+
Riffer constructs providers with `provider_class.new`, so `initialize` takes no arguments; read credentials from configuration inside `build_client`. `Riffer::Providers::Base` provides a private `client` method for your `execute_generate`/`execute_stream` to call. It resolves, in order:
|
|
127
|
+
|
|
128
|
+
1. **A configured client** — whatever `global_client` returns: a client instance, or a no-argument `Proc` resolved on **every** call. Override that hook to read the client off your own configuration; it defaults to `nil`.
|
|
129
|
+
2. **A memoized client** from `build_client` — implement this hook to build your SDK client from configured credentials.
|
|
130
|
+
|
|
131
|
+
This gives your provider the same "works out of the box, bring your own client in production" behavior as the built-ins. See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
132
|
+
|
|
124
133
|
## Using depends_on
|
|
125
134
|
|
|
126
135
|
For lazy loading of external gems:
|
|
127
136
|
|
|
128
137
|
```ruby
|
|
129
138
|
class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
130
|
-
def initialize
|
|
139
|
+
def initialize
|
|
140
|
+
super
|
|
131
141
|
depends_on "my_provider_gem" # Only loaded when provider is used
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
def build_client
|
|
147
|
+
::MyProviderGem::Client.new(api_key: ENV["MY_PROVIDER_API_KEY"])
|
|
134
148
|
end
|
|
135
149
|
end
|
|
136
150
|
```
|
|
@@ -291,17 +305,19 @@ end
|
|
|
291
305
|
# lib/riffer/providers/my_provider.rb
|
|
292
306
|
|
|
293
307
|
class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
294
|
-
def initialize
|
|
308
|
+
def initialize
|
|
309
|
+
super
|
|
295
310
|
depends_on "my_provider_gem"
|
|
296
|
-
|
|
297
|
-
api_key = options[:api_key] || ENV["MY_PROVIDER_API_KEY"]
|
|
298
|
-
@client = ::MyProviderGem::Client.new(api_key: api_key)
|
|
299
311
|
end
|
|
300
312
|
|
|
301
313
|
private
|
|
302
314
|
|
|
303
315
|
# Hook methods
|
|
304
316
|
|
|
317
|
+
def build_client
|
|
318
|
+
::MyProviderGem::Client.new(api_key: ENV["MY_PROVIDER_API_KEY"])
|
|
319
|
+
end
|
|
320
|
+
|
|
305
321
|
def build_request_params(messages, model, options)
|
|
306
322
|
system_message = extract_system(messages)
|
|
307
323
|
conversation = messages.reject { |m| m.is_a?(Riffer::Messages::System) }
|
|
@@ -323,13 +339,13 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
323
339
|
end
|
|
324
340
|
|
|
325
341
|
def execute_generate(params)
|
|
326
|
-
|
|
342
|
+
client.create(**params)
|
|
327
343
|
end
|
|
328
344
|
|
|
329
345
|
def execute_stream(params, yielder)
|
|
330
346
|
accumulated_text = ""
|
|
331
347
|
|
|
332
|
-
|
|
348
|
+
client.stream(**params) do |event|
|
|
333
349
|
case event.type
|
|
334
350
|
when :text_delta
|
|
335
351
|
accumulated_text += event.text
|
data/docs/providers/GEMINI.md
CHANGED
|
@@ -12,15 +12,38 @@ Riffer.configure do |config|
|
|
|
12
12
|
end
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## HTTP Client
|
|
16
|
+
|
|
17
|
+
Gemini has no vendor SDK, so riffer ships its own transport: `Riffer::Providers::Gemini::Client`. The provider builds one from the configured `api_key` by default; construct your own to tune the HTTP knobs and assign it to `config.gemini.client`:
|
|
16
18
|
|
|
17
19
|
```ruby
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
Riffer.configure do |config|
|
|
21
|
+
config.gemini.client = Riffer::Providers::Gemini::Client.new(
|
|
22
|
+
api_key: ENV['GEMINI_API_KEY'],
|
|
23
|
+
read_timeout: 120
|
|
24
|
+
)
|
|
21
25
|
end
|
|
22
26
|
```
|
|
23
27
|
|
|
28
|
+
| Option | Default | Description |
|
|
29
|
+
| --------------- | ------------------------------------------- | -------------------------------------- |
|
|
30
|
+
| `api_key` | `nil` | Sent as the `x-goog-api-key` header |
|
|
31
|
+
| `base_url` | `https://generativelanguage.googleapis.com` | API origin (proxies, regional mirrors) |
|
|
32
|
+
| `open_timeout` | `10` | Connection-open timeout in seconds |
|
|
33
|
+
| `read_timeout` | `60` | Read timeout in seconds |
|
|
34
|
+
| `write_timeout` | `nil` | Write timeout in seconds |
|
|
35
|
+
| `proxy_address` | `nil` | HTTP proxy host |
|
|
36
|
+
| `proxy_port` | `nil` | HTTP proxy port |
|
|
37
|
+
|
|
38
|
+
The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
39
|
+
|
|
40
|
+
The class is a default implementation, not a required base: any object implementing the two-method contract works, e.g. a Faraday-based or instrumented transport.
|
|
41
|
+
|
|
42
|
+
| Method | Contract |
|
|
43
|
+
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `post(path, body)` | POST `body` as JSON to `path`; return the parsed response `Hash` (symbol keys); raise `Riffer::Error` on a non-success status |
|
|
45
|
+
| `post_stream(path, body) { \|chunk\| }` | POST `body` as JSON to `path`; yield raw response body chunks; raise `Riffer::Error` on a non-success status |
|
|
46
|
+
|
|
24
47
|
## Supported Models
|
|
25
48
|
|
|
26
49
|
Use Gemini model IDs in the `gemini/model` format:
|
|
@@ -62,7 +85,7 @@ model_options topP: 0.9
|
|
|
62
85
|
### Basic Generation
|
|
63
86
|
|
|
64
87
|
```ruby
|
|
65
|
-
provider = Riffer::Providers::Gemini.new
|
|
88
|
+
provider = Riffer::Providers::Gemini.new
|
|
66
89
|
|
|
67
90
|
response = provider.generate_text(
|
|
68
91
|
prompt: "Hello!",
|
data/docs/providers/OPENAI.md
CHANGED
|
@@ -20,12 +20,28 @@ Riffer.configure do |config|
|
|
|
20
20
|
end
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Both `api_key` and `base_url` resolve in order: `Riffer.config.openai.*` → the OpenAI SDK's own `OPENAI_API_KEY` / `OPENAI_BASE_URL` lookup. Leaving one unset in riffer means the SDK resolves it, so an `OPENAI_BASE_URL` gateway is honored without any riffer configuration.
|
|
24
|
+
|
|
25
|
+
For anything beyond the API key — timeouts, retries, proxies — supply your own `OpenAI::Client`:
|
|
24
26
|
|
|
25
27
|
```ruby
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
Riffer.configure do |config|
|
|
29
|
+
config.openai.client = OpenAI::Client.new(
|
|
30
|
+
api_key: ENV['OPENAI_API_KEY'],
|
|
31
|
+
timeout: 30,
|
|
32
|
+
max_retries: 4
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
38
|
+
|
|
39
|
+
For OpenAI-compatible servers (LiteLLM, vLLM, corporate gateways), configure a `base_url`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Riffer.configure do |config|
|
|
43
|
+
config.openai.api_key = ENV['GATEWAY_KEY']
|
|
44
|
+
config.openai.base_url = 'http://localhost:4000/v1'
|
|
29
45
|
end
|
|
30
46
|
```
|
|
31
47
|
|
|
@@ -185,7 +201,7 @@ The provider converts Riffer messages to OpenAI format:
|
|
|
185
201
|
## Direct Provider Usage
|
|
186
202
|
|
|
187
203
|
```ruby
|
|
188
|
-
provider = Riffer::Providers::OpenAI.new
|
|
204
|
+
provider = Riffer::Providers::OpenAI.new
|
|
189
205
|
|
|
190
206
|
response = provider.generate_text(
|
|
191
207
|
prompt: "Hello!",
|
|
@@ -24,16 +24,21 @@ Riffer.configure do |config|
|
|
|
24
24
|
end
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
The `api_key` resolves in order: `Riffer.config.openrouter.api_key` → `ENV['OPENROUTER_API_KEY']`.
|
|
28
|
+
|
|
29
|
+
For anything beyond the API key — timeouts, retries, proxies — supply your own `OpenAI::Client` pinned to the OpenRouter endpoint:
|
|
28
30
|
|
|
29
31
|
```ruby
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
Riffer.configure do |config|
|
|
33
|
+
config.openrouter.client = OpenAI::Client.new(
|
|
34
|
+
api_key: ENV['OPENROUTER_API_KEY'],
|
|
35
|
+
base_url: 'https://openrouter.ai/api/v1',
|
|
36
|
+
timeout: 60
|
|
37
|
+
)
|
|
33
38
|
end
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
The
|
|
41
|
+
The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
37
42
|
|
|
38
43
|
## Supported Models
|
|
39
44
|
|
|
@@ -229,7 +234,7 @@ User messages with files become multi-part content (`image_url` for images, `fil
|
|
|
229
234
|
## Direct Provider Usage
|
|
230
235
|
|
|
231
236
|
```ruby
|
|
232
|
-
provider = Riffer::Providers::OpenRouter.new
|
|
237
|
+
provider = Riffer::Providers::OpenRouter.new
|
|
233
238
|
|
|
234
239
|
response = provider.generate_text(
|
|
235
240
|
prompt: 'Hello!',
|
data/docs/providers/PROVIDERS.md
CHANGED
|
@@ -30,6 +30,21 @@ class MyAgent < Riffer::Agent
|
|
|
30
30
|
end
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Credentials and Clients
|
|
34
|
+
|
|
35
|
+
Every credential comes from configuration: `Riffer::Providers::OpenAI.new` takes no arguments.
|
|
36
|
+
|
|
37
|
+
| Provider | Configured credentials |
|
|
38
|
+
| -------------- | ----------------------------------------------------------------- |
|
|
39
|
+
| OpenAI | `config.openai.api_key`, `config.openai.base_url` |
|
|
40
|
+
| Azure OpenAI | `config.azure_openai.api_key`, `config.azure_openai.endpoint` |
|
|
41
|
+
| Anthropic | `config.anthropic.api_key` |
|
|
42
|
+
| Amazon Bedrock | `config.amazon_bedrock.api_token`, `config.amazon_bedrock.region` |
|
|
43
|
+
| Gemini | `config.gemini.api_key` |
|
|
44
|
+
| OpenRouter | `config.openrouter.api_key` |
|
|
45
|
+
|
|
46
|
+
Out of the box, each provider builds an SDK client from these credentials. Everything else — timeouts, retries, proxies, custom auth — is configured by assigning your own client (an instance, or a `Proc` resolved on every LLM call) to `Riffer.config.<provider>.client`. See [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
|
|
47
|
+
|
|
33
48
|
## Provider Interface
|
|
34
49
|
|
|
35
50
|
All providers inherit from `Riffer::Providers::Base` and implement:
|
|
@@ -39,7 +54,7 @@ All providers inherit from `Riffer::Providers::Base` and implement:
|
|
|
39
54
|
Generates a response synchronously:
|
|
40
55
|
|
|
41
56
|
```ruby
|
|
42
|
-
provider = Riffer::Providers::OpenAI.new
|
|
57
|
+
provider = Riffer::Providers::OpenAI.new
|
|
43
58
|
|
|
44
59
|
response = provider.generate_text(
|
|
45
60
|
prompt: "Hello!",
|
data/lib/riffer/agent/config.rb
CHANGED
|
@@ -16,9 +16,6 @@ class Riffer::Agent::Config
|
|
|
16
16
|
# The configured instructions.
|
|
17
17
|
attr_reader :instructions #: (String | Proc)?
|
|
18
18
|
|
|
19
|
-
# Options passed to the provider client.
|
|
20
|
-
attr_accessor :provider_options #: Hash[Symbol, untyped]
|
|
21
|
-
|
|
22
19
|
# Options passed to generate_text/stream_text.
|
|
23
20
|
attr_accessor :model_options #: Hash[Symbol, untyped]
|
|
24
21
|
|
|
@@ -50,7 +47,6 @@ class Riffer::Agent::Config
|
|
|
50
47
|
# ?identifier: String?,
|
|
51
48
|
# ?model: (String | Proc)?,
|
|
52
49
|
# ?instructions: (String | Proc)?,
|
|
53
|
-
# ?provider_options: Hash[Symbol, untyped],
|
|
54
50
|
# ?model_options: Hash[Symbol, untyped],
|
|
55
51
|
# ?structured_output: Riffer::Params?,
|
|
56
52
|
# ?max_steps: Numeric?,
|
|
@@ -64,7 +60,6 @@ class Riffer::Agent::Config
|
|
|
64
60
|
identifier: nil,
|
|
65
61
|
model: nil,
|
|
66
62
|
instructions: nil,
|
|
67
|
-
provider_options: {},
|
|
68
63
|
model_options: {},
|
|
69
64
|
structured_output: nil,
|
|
70
65
|
max_steps: DEFAULT_MAX_STEPS,
|
|
@@ -74,7 +69,6 @@ class Riffer::Agent::Config
|
|
|
74
69
|
skills_config: nil,
|
|
75
70
|
guardrails: { before: [], after: [] }
|
|
76
71
|
)
|
|
77
|
-
@provider_options = provider_options
|
|
78
72
|
@model_options = model_options
|
|
79
73
|
@max_steps = max_steps
|
|
80
74
|
@tools_config = tools_config
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -465,10 +465,8 @@ module Riffer::Agent::Run
|
|
|
465
465
|
tripwire = response.tripwire
|
|
466
466
|
return unless tripwire
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
span.set_attribute("riffer.tripwire.guardrail", Riffer::Helpers::ClassNameConverter.convert(class_name))
|
|
471
|
-
end
|
|
468
|
+
identifier = Riffer::Helpers::Identifier.for(tripwire.guardrail)
|
|
469
|
+
span.set_attribute("riffer.tripwire.guardrail", identifier) unless identifier.empty?
|
|
472
470
|
span.set_attribute("riffer.tripwire.reason", tripwire.reason)
|
|
473
471
|
span.set_attribute("riffer.tripwire.phase", tripwire.phase.to_s)
|
|
474
472
|
end
|
|
@@ -37,7 +37,6 @@ module Riffer::Agent::Serializer
|
|
|
37
37
|
model: "#{agent.provider_name}/#{agent.model_name}",
|
|
38
38
|
instructions: agent.instruction_message&.content,
|
|
39
39
|
model_options: config.model_options,
|
|
40
|
-
provider_options: config.provider_options,
|
|
41
40
|
max_steps: encode_max_steps(config.max_steps),
|
|
42
41
|
structured_output: config.structured_output&.to_json_schema(strict: false),
|
|
43
42
|
tools: agent.tools.map { |tool_class| tool_descriptor(tool_class) },
|
|
@@ -96,7 +95,6 @@ module Riffer::Agent::Serializer
|
|
|
96
95
|
identifier: hash[:identifier],
|
|
97
96
|
model: hash[:model],
|
|
98
97
|
instructions: hash[:instructions],
|
|
99
|
-
provider_options: hash[:provider_options] || {},
|
|
100
98
|
model_options: hash[:model_options] || {},
|
|
101
99
|
structured_output: decode_structured_output(hash[:structured_output]),
|
|
102
100
|
max_steps: decode_max_steps(hash),
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -15,6 +15,8 @@ require "json"
|
|
|
15
15
|
# agent.generate('Hello!')
|
|
16
16
|
#
|
|
17
17
|
class Riffer::Agent
|
|
18
|
+
extend Riffer::Registrable
|
|
19
|
+
|
|
18
20
|
# @rbs self.@config: Riffer::Agent::Config?
|
|
19
21
|
|
|
20
22
|
INTERRUPT_MAX_STEPS = :max_steps #: Symbol
|
|
@@ -31,7 +33,7 @@ class Riffer::Agent
|
|
|
31
33
|
#--
|
|
32
34
|
#: (?String?) -> String
|
|
33
35
|
def self.identifier(value = nil)
|
|
34
|
-
value.nil? ? (config.identifier || Riffer::Helpers::
|
|
36
|
+
value.nil? ? (config.identifier || Riffer::Helpers::Identifier.for(self)) : (config.identifier = value)
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# Gets or sets the model string (e.g., "openai/gpt-4o").
|
|
@@ -57,14 +59,6 @@ class Riffer::Agent
|
|
|
57
59
|
value.nil? ? config.instructions : (config.instructions = value)
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
# Gets or sets provider options passed to the provider client.
|
|
61
|
-
#
|
|
62
|
-
#--
|
|
63
|
-
#: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
64
|
-
def self.provider_options(options = nil)
|
|
65
|
-
options.nil? ? config.provider_options : (config.provider_options = options)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
62
|
# Gets or sets model options passed to generate_text/stream_text.
|
|
69
63
|
#
|
|
70
64
|
#--
|
|
@@ -152,22 +146,6 @@ class Riffer::Agent
|
|
|
152
146
|
config.skills_config
|
|
153
147
|
end
|
|
154
148
|
|
|
155
|
-
# Finds an agent class by identifier.
|
|
156
|
-
#
|
|
157
|
-
#--
|
|
158
|
-
#: (String) -> singleton(Riffer::Agent)?
|
|
159
|
-
def self.find(identifier)
|
|
160
|
-
all.find { |agent_class| agent_class.identifier == identifier.to_s }
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Returns all agent subclasses.
|
|
164
|
-
#
|
|
165
|
-
#--
|
|
166
|
-
#: () -> Array[singleton(Riffer::Agent)]
|
|
167
|
-
def self.all
|
|
168
|
-
subclasses #: Array[singleton(Riffer::Agent)]
|
|
169
|
-
end
|
|
170
|
-
|
|
171
149
|
# Generates a response using a new agent instance.
|
|
172
150
|
#--
|
|
173
151
|
#: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
|
|
@@ -408,7 +386,7 @@ class Riffer::Agent
|
|
|
408
386
|
provider_class = Riffer::Providers::Repository.find(@provider_name)
|
|
409
387
|
raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
|
|
410
388
|
|
|
411
|
-
provider_class.new
|
|
389
|
+
provider_class.new
|
|
412
390
|
end
|
|
413
391
|
|
|
414
392
|
#--
|
data/lib/riffer/config.rb
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
# Configuration for the Riffer framework.
|
|
5
5
|
class Riffer::Config
|
|
6
|
-
AmazonBedrock = Struct.new(:api_token, :region)
|
|
7
|
-
Anthropic = Struct.new(:api_key)
|
|
8
|
-
AzureOpenAI = Struct.new(:api_key, :endpoint)
|
|
9
|
-
Gemini = Struct.new(:api_key, :
|
|
10
|
-
OpenAI = Struct.new(:api_key)
|
|
11
|
-
OpenRouter = Struct.new(:api_key)
|
|
6
|
+
AmazonBedrock = Struct.new(:api_token, :region, :client)
|
|
7
|
+
Anthropic = Struct.new(:api_key, :client)
|
|
8
|
+
AzureOpenAI = Struct.new(:api_key, :endpoint, :client)
|
|
9
|
+
Gemini = Struct.new(:api_key, :client)
|
|
10
|
+
OpenAI = Struct.new(:api_key, :base_url, :client)
|
|
11
|
+
OpenRouter = Struct.new(:api_key, :client)
|
|
12
12
|
Evals = Struct.new(:judge_model)
|
|
13
13
|
Mcp = Struct.new(:credentials, :discovery_runner)
|
|
14
14
|
|
data/lib/riffer/evals/judge.rb
CHANGED
|
@@ -6,7 +6,6 @@ require "json"
|
|
|
6
6
|
# Executes LLM-as-judge evaluations, using tool calling internally to get
|
|
7
7
|
# structured output from the judge model.
|
|
8
8
|
class Riffer::Evals::Judge
|
|
9
|
-
# @rbs @provider_options: Hash[Symbol, untyped]
|
|
10
9
|
# @rbs @provider_instance: Riffer::Providers::Base?
|
|
11
10
|
# @rbs @provider_name: String?
|
|
12
11
|
# @rbs @model_name: String?
|
|
@@ -33,15 +32,14 @@ class Riffer::Evals::Judge
|
|
|
33
32
|
|
|
34
33
|
# Raises Riffer::ArgumentError unless +model+ is "provider/model" format.
|
|
35
34
|
#--
|
|
36
|
-
#: (model: String
|
|
37
|
-
def initialize(model
|
|
35
|
+
#: (model: String) -> void
|
|
36
|
+
def initialize(model:)
|
|
38
37
|
provider_name, model_name = model.split("/", 2)
|
|
39
38
|
unless [provider_name, model_name].all? { |part| part.is_a?(String) && !part.strip.empty? }
|
|
40
39
|
raise Riffer::ArgumentError, "Invalid model string: #{model}"
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
@model = model
|
|
44
|
-
@provider_options = provider_options
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
# Evaluates an input/output pair using the configured LLM.
|
|
@@ -92,7 +90,7 @@ class Riffer::Evals::Judge
|
|
|
92
90
|
provider_class = Riffer::Providers::Repository.find(provider_name)
|
|
93
91
|
raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
|
|
94
92
|
|
|
95
|
-
provider_class.new
|
|
93
|
+
provider_class.new
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
|
data/lib/riffer/guardrail.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Helper module for deriving snake_case identifiers from class names.
|
|
5
|
+
module Riffer::Helpers::Identifier
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Derives a snake_case identifier from a class name string.
|
|
9
|
+
#
|
|
10
|
+
#--
|
|
11
|
+
#: (String?) -> String
|
|
12
|
+
def derive(class_name)
|
|
13
|
+
class_name.
|
|
14
|
+
to_s.
|
|
15
|
+
gsub("::", "/").
|
|
16
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
|
|
17
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
|
18
|
+
downcase
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Derives and memoizes the identifier for a class or module. Anonymous
|
|
22
|
+
# classes return "" without caching, so a class named later still derives its
|
|
23
|
+
# real identifier — a guard that must travel with the cache, so callers never
|
|
24
|
+
# memoize their own.
|
|
25
|
+
#
|
|
26
|
+
#--
|
|
27
|
+
#: (Module) -> String
|
|
28
|
+
def for(klass)
|
|
29
|
+
cached = klass.instance_variable_get(:@derived_identifier) #: String?
|
|
30
|
+
return cached if cached
|
|
31
|
+
|
|
32
|
+
# Tool classes shadow Module#name with the identifier DSL, so the real
|
|
33
|
+
# class-path name must come from Module's own implementation.
|
|
34
|
+
real_name = Module.instance_method(:name).bind_call(klass) #: String?
|
|
35
|
+
return "" if real_name.nil?
|
|
36
|
+
|
|
37
|
+
derived = derive(real_name)
|
|
38
|
+
klass.instance_variable_set(:@derived_identifier, derived)
|
|
39
|
+
derived
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -37,28 +37,40 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
#--
|
|
40
|
-
#: (
|
|
41
|
-
def initialize
|
|
42
|
-
super
|
|
40
|
+
#: () -> void
|
|
41
|
+
def initialize
|
|
42
|
+
super
|
|
43
43
|
depends_on "aws-sdk-bedrockruntime"
|
|
44
|
-
|
|
45
|
-
api_token ||= Riffer.config.amazon_bedrock.api_token
|
|
46
|
-
region ||= Riffer.config.amazon_bedrock.region
|
|
47
|
-
|
|
48
|
-
@client = if api_token && !api_token.empty?
|
|
49
|
-
Aws::BedrockRuntime::Client.new(
|
|
50
|
-
region: region,
|
|
51
|
-
token_provider: Aws::StaticTokenProvider.new(api_token),
|
|
52
|
-
auth_scheme_preference: ["httpBearerAuth"],
|
|
53
|
-
**,
|
|
54
|
-
)
|
|
55
|
-
else
|
|
56
|
-
Aws::BedrockRuntime::Client.new(region: region, **)
|
|
57
|
-
end
|
|
58
44
|
end
|
|
59
45
|
|
|
60
46
|
private
|
|
61
47
|
|
|
48
|
+
#--
|
|
49
|
+
#: () -> untyped
|
|
50
|
+
def global_client
|
|
51
|
+
Riffer.config.amazon_bedrock.client
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Compacted so an unset region stays absent: the AWS SDK resolves +AWS_REGION+
|
|
55
|
+
# and the shared config only for a missing argument, and raises
|
|
56
|
+
# +Aws::Errors::MissingRegionError+ on an explicit nil.
|
|
57
|
+
#--
|
|
58
|
+
#: () -> untyped
|
|
59
|
+
def build_client
|
|
60
|
+
api_token = Riffer.config.amazon_bedrock.api_token
|
|
61
|
+
region = Riffer.config.amazon_bedrock.region
|
|
62
|
+
|
|
63
|
+
if api_token && !api_token.empty?
|
|
64
|
+
Aws::BedrockRuntime::Client.new(**{
|
|
65
|
+
region: region,
|
|
66
|
+
token_provider: Aws::StaticTokenProvider.new(api_token),
|
|
67
|
+
auth_scheme_preference: ["httpBearerAuth"],
|
|
68
|
+
}.compact)
|
|
69
|
+
else
|
|
70
|
+
Aws::BedrockRuntime::Client.new(**{ region: region }.compact)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
62
74
|
#--
|
|
63
75
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
64
76
|
def build_request_params(messages, model, options)
|
|
@@ -137,7 +149,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
137
149
|
#--
|
|
138
150
|
#: (Hash[Symbol, untyped]) -> untyped
|
|
139
151
|
def execute_generate(params)
|
|
140
|
-
|
|
152
|
+
client.converse(**params)
|
|
141
153
|
end
|
|
142
154
|
|
|
143
155
|
#--
|
|
@@ -227,7 +239,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
227
239
|
tool_call: nil,
|
|
228
240
|
} #: Hash[Symbol, untyped]
|
|
229
241
|
|
|
230
|
-
|
|
242
|
+
client.converse_stream(**params) do |stream|
|
|
231
243
|
stream.on_event do |event|
|
|
232
244
|
case event
|
|
233
245
|
when Aws::BedrockRuntime::Types::ContentBlockStartEvent
|