riffer 0.15.1 → 0.16.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/providers.md +15 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +22 -0
- data/docs/01_OVERVIEW.md +6 -2
- data/docs/03_AGENTS.md +68 -0
- data/docs/04_TOOLS.md +31 -0
- data/lib/riffer/boolean.rb +12 -0
- data/lib/riffer/param.rb +31 -7
- data/lib/riffer/params.rb +117 -10
- data/lib/riffer/providers/amazon_bedrock.rb +5 -2
- data/lib/riffer/providers/anthropic.rb +5 -2
- data/lib/riffer/providers/open_ai.rb +4 -2
- data/lib/riffer/structured_output.rb +7 -2
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/boolean.rbs +10 -0
- data/sig/generated/riffer/param.rbs +16 -5
- data/sig/generated/riffer/params.rbs +30 -6
- data/sig/generated/riffer/structured_output.rbs +5 -2
- data/sig/generated/riffer/tool.rbs +2 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fff2cbd2ae809651bb2a3b54cc79e591d9ba295fe8d096769e74ee75159e32b
|
|
4
|
+
data.tar.gz: 18ea73a692e864fbb2bd1c4c880ec293348cd4012a5218364b9207d51686b9bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01eeb93e3253a1061d884e19470f880eebfdd354e3ae1a32673364533411e67f9e0ccfb48a1a90d7be88e12d0bf0d5b3027ea399e517b2d18783ec8599b0ee4b
|
|
7
|
+
data.tar.gz: fd8ca068330ab1858055dc3e5a0fc75a69db606189cfe7af5feac049e8b634919fb78e3501a8bb513bc542d1883babfe627d8b6a355927eb1df4707dc05943a6
|
data/.agents/providers.md
CHANGED
|
@@ -116,9 +116,19 @@ def build_request_params(messages, model, options)
|
|
|
116
116
|
end
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Strict schema
|
|
120
|
+
|
|
121
|
+
All providers apply `strict_schema` (defined in `Providers::Base`) to schemas sent to LLMs — both structured output and tool parameters. This transformation:
|
|
122
|
+
|
|
123
|
+
- Moves all properties into `required`
|
|
124
|
+
- Makes originally-optional properties nullable (`[type, "null"]`)
|
|
125
|
+
- Recurses into nested objects and array items
|
|
126
|
+
|
|
127
|
+
This ensures all providers return proper `null` for optional fields instead of empty strings or garbage.
|
|
128
|
+
|
|
119
129
|
### Provider-specific formats
|
|
120
130
|
|
|
121
|
-
**OpenAI** — uses `params[:text][:format]`:
|
|
131
|
+
**OpenAI** — uses `params[:text][:format]` with `strict: true`:
|
|
122
132
|
|
|
123
133
|
```ruby
|
|
124
134
|
if structured_output
|
|
@@ -126,7 +136,7 @@ if structured_output
|
|
|
126
136
|
format: {
|
|
127
137
|
type: "json_schema",
|
|
128
138
|
name: "response",
|
|
129
|
-
schema: structured_output.json_schema,
|
|
139
|
+
schema: strict_schema(structured_output.json_schema),
|
|
130
140
|
strict: true
|
|
131
141
|
}
|
|
132
142
|
}
|
|
@@ -140,7 +150,7 @@ if structured_output
|
|
|
140
150
|
params[:output_config] = {
|
|
141
151
|
format: {
|
|
142
152
|
type: "json_schema",
|
|
143
|
-
schema: structured_output.json_schema
|
|
153
|
+
schema: strict_schema(structured_output.json_schema)
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
156
|
end
|
|
@@ -155,7 +165,7 @@ if structured_output
|
|
|
155
165
|
type: "json_schema",
|
|
156
166
|
structure: {
|
|
157
167
|
json_schema: {
|
|
158
|
-
schema: structured_output.json_schema.to_json,
|
|
168
|
+
schema: strict_schema(structured_output.json_schema).to_json,
|
|
159
169
|
name: "response"
|
|
160
170
|
}
|
|
161
171
|
}
|
|
@@ -167,6 +177,7 @@ end
|
|
|
167
177
|
### Key details
|
|
168
178
|
|
|
169
179
|
- `structured_output.json_schema` returns a Hash with `type`, `properties`, `required`, and `additionalProperties` keys
|
|
180
|
+
- All providers wrap schemas with `strict_schema` to ensure proper null handling
|
|
170
181
|
- Bedrock requires the schema as a JSON string (`.to_json`), others use the Hash directly
|
|
171
182
|
- The agent handles parsing and validation of the response — providers only need to pass the schema to the SDK
|
|
172
183
|
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ 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.16.0](https://github.com/janeapp/riffer/compare/riffer/v0.15.1...riffer/v0.16.0) (2026-02-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add Riffer::Boolean sentinel type for boolean params ([#147](https://github.com/janeapp/riffer/issues/147)) ([5337cf3](https://github.com/janeapp/riffer/commit/5337cf3820d54cf9e03a630ca32b8a7f59347221))
|
|
14
|
+
* nested params DSL and strict schema for all providers ([#144](https://github.com/janeapp/riffer/issues/144)) ([2984855](https://github.com/janeapp/riffer/commit/2984855c64a1d0b421497aa81c5e0a393d443e46))
|
|
15
|
+
|
|
8
16
|
## [0.15.1](https://github.com/janeapp/riffer/compare/riffer/v0.15.0...riffer/v0.15.1) (2026-02-25)
|
|
9
17
|
|
|
10
18
|
|
data/README.md
CHANGED
|
@@ -93,6 +93,28 @@ Run the interactive console:
|
|
|
93
93
|
bin/console
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### Recording VCR Cassettes
|
|
97
|
+
|
|
98
|
+
Integration tests use [VCR](https://github.com/vcr/vcr) to record and replay HTTP interactions. When adding new tests that hit provider APIs, you need to record cassettes with real API keys.
|
|
99
|
+
|
|
100
|
+
Create a `.env` file in the project root (it is gitignored):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
OPENAI_API_KEY=sk-...
|
|
104
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
105
|
+
AWS_BEDROCK_API_TOKEN=...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The test helper loads this file automatically via `dotenv`. Then run the specific tests that need new cassettes:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bundle exec ruby -Itest test/riffer/providers/open_ai_test.rb
|
|
112
|
+
bundle exec ruby -Itest test/riffer/providers/anthropic_test.rb
|
|
113
|
+
bundle exec ruby -Itest test/riffer/providers/amazon_bedrock_test.rb
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
VCR records the HTTP interactions to `test/fixtures/vcr_cassettes/` on the first run. Subsequent runs replay from the cassettes without hitting the API. API keys are automatically filtered from recorded cassettes.
|
|
117
|
+
|
|
96
118
|
## Contributing
|
|
97
119
|
|
|
98
120
|
1. Fork the repository and create your branch: `git checkout -b feature/foo`
|
data/docs/01_OVERVIEW.md
CHANGED
|
@@ -39,7 +39,7 @@ See [Tools](04_TOOLS.md) for details.
|
|
|
39
39
|
|
|
40
40
|
### Structured Output
|
|
41
41
|
|
|
42
|
-
Agents can return structured JSON responses that conform to a schema. The response is automatically parsed and validated:
|
|
42
|
+
Agents can return structured JSON responses that conform to a schema. The response is automatically parsed and validated. Schemas support nested objects (`Hash`), typed arrays (`Array, of:`), and arrays of objects (`Array` with block):
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
class SentimentAgent < Riffer::Agent
|
|
@@ -47,11 +47,15 @@ class SentimentAgent < Riffer::Agent
|
|
|
47
47
|
structured_output do
|
|
48
48
|
required :sentiment, String
|
|
49
49
|
required :score, Float
|
|
50
|
+
required :entities, Array, description: "Named entities" do
|
|
51
|
+
required :name, String
|
|
52
|
+
required :type, String, enum: ["person", "place", "org"]
|
|
53
|
+
end
|
|
50
54
|
end
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
response = SentimentAgent.generate('Analyze: "I love this!"')
|
|
54
|
-
response.structured_output # => {sentiment: "positive", score: 0.95}
|
|
58
|
+
response.structured_output # => {sentiment: "positive", score: 0.95, entities: [...]}
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
See the [structured output section in Agents](03_AGENTS.md#structured_output) for details.
|
data/docs/03_AGENTS.md
CHANGED
|
@@ -149,6 +149,74 @@ end
|
|
|
149
149
|
|
|
150
150
|
The LLM response is automatically parsed and validated against the schema. Access the result via `response.structured_output`.
|
|
151
151
|
|
|
152
|
+
#### Nested Objects
|
|
153
|
+
|
|
154
|
+
Use `Hash` with a block to define nested object schemas:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
structured_output do
|
|
158
|
+
required :name, String, description: "Person name"
|
|
159
|
+
required :address, Hash, description: "Mailing address" do
|
|
160
|
+
required :street, String, description: "Street address"
|
|
161
|
+
required :city, String, description: "City"
|
|
162
|
+
optional :postal_code, String, description: "Postal or zip code"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Validation errors use dot-path notation: `address.city is required`.
|
|
168
|
+
|
|
169
|
+
#### Typed Arrays
|
|
170
|
+
|
|
171
|
+
Use `Array` with the `of:` keyword for arrays of primitive types:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
structured_output do
|
|
175
|
+
required :tags, Array, of: String, description: "Tags"
|
|
176
|
+
required :scores, Array, of: Float, description: "Scores"
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Only primitive types are allowed with `of:`: `String`, `Integer`, `Float`, `TrueClass`, `FalseClass`.
|
|
181
|
+
|
|
182
|
+
#### Arrays of Objects
|
|
183
|
+
|
|
184
|
+
Use `Array` with a block to define arrays of objects:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
structured_output do
|
|
188
|
+
required :items, Array, description: "Line items" do
|
|
189
|
+
required :name, String, description: "Product name"
|
|
190
|
+
required :price, Float, description: "Price"
|
|
191
|
+
optional :quantity, Integer, description: "Quantity"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Validation errors include the array index: `items[1].price is required`.
|
|
197
|
+
|
|
198
|
+
#### Deep Nesting
|
|
199
|
+
|
|
200
|
+
Blocks can be nested arbitrarily deep:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
structured_output do
|
|
204
|
+
required :orders, Array, description: "Orders" do
|
|
205
|
+
required :id, String, description: "Order ID"
|
|
206
|
+
required :shipping, Hash, description: "Shipping info" do
|
|
207
|
+
required :address, Hash, description: "Address" do
|
|
208
|
+
required :street, String
|
|
209
|
+
required :city, String
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Limitations
|
|
217
|
+
|
|
218
|
+
Using both `of:` and a block raises `Riffer::ArgumentError`. Using `of:` with a non-primitive type (e.g. `of: Hash`) also raises `Riffer::ArgumentError`.
|
|
219
|
+
|
|
152
220
|
Structured output is not compatible with streaming — calling `stream` on an agent with structured output configured raises `Riffer::ArgumentError`.
|
|
153
221
|
|
|
154
222
|
### guardrail
|
data/docs/04_TOOLS.md
CHANGED
|
@@ -104,10 +104,41 @@ Options:
|
|
|
104
104
|
| `String` | `string` |
|
|
105
105
|
| `Integer` | `integer` |
|
|
106
106
|
| `Float` | `number` |
|
|
107
|
+
| `Riffer::Boolean` | `boolean` |
|
|
107
108
|
| `TrueClass` / `FalseClass` | `boolean` |
|
|
108
109
|
| `Array` | `array` |
|
|
109
110
|
| `Hash` | `object` |
|
|
110
111
|
|
|
112
|
+
`Riffer::Boolean` is the preferred way to declare boolean parameters. `TrueClass` and `FalseClass` continue to work for backwards compatibility.
|
|
113
|
+
|
|
114
|
+
### Nested Parameters
|
|
115
|
+
|
|
116
|
+
Tool params support the same nested DSL as structured output — nested objects (`Hash` with block), typed arrays (`Array, of:`), and arrays of objects (`Array` with block). See the [structured output section in Agents](03_AGENTS.md#nested-objects) for full syntax.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class CreateOrderTool < Riffer::Tool
|
|
120
|
+
description "Creates an order"
|
|
121
|
+
|
|
122
|
+
params do
|
|
123
|
+
required :items, Array, description: "Line items" do
|
|
124
|
+
required :product_id, Integer
|
|
125
|
+
required :quantity, Integer
|
|
126
|
+
optional :notes, String
|
|
127
|
+
end
|
|
128
|
+
required :shipping, Hash, description: "Shipping address" do
|
|
129
|
+
required :street, String
|
|
130
|
+
required :city, String
|
|
131
|
+
optional :zip, String
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def call(context:, items:, shipping:)
|
|
136
|
+
# items is an Array of Hashes with symbolized keys
|
|
137
|
+
# shipping is a Hash with symbolized keys
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
111
142
|
## The call Method
|
|
112
143
|
|
|
113
144
|
Every tool must implement the `call` method and return a `Riffer::Tools::Response`:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Riffer::Boolean is a sentinel type for declaring boolean parameters.
|
|
5
|
+
#
|
|
6
|
+
# Ruby has no +Boolean+ class (+true+ is +TrueClass+, +false+ is +FalseClass+).
|
|
7
|
+
# Use this module wherever you need a single type that means "boolean":
|
|
8
|
+
#
|
|
9
|
+
# required :verbose, Riffer::Boolean
|
|
10
|
+
#
|
|
11
|
+
module Riffer::Boolean
|
|
12
|
+
end
|
data/lib/riffer/param.rb
CHANGED
|
@@ -10,11 +10,15 @@ class Riffer::Param
|
|
|
10
10
|
String => "string",
|
|
11
11
|
Integer => "integer",
|
|
12
12
|
Float => "number",
|
|
13
|
+
Riffer::Boolean => "boolean",
|
|
13
14
|
TrueClass => "boolean",
|
|
14
15
|
FalseClass => "boolean",
|
|
15
16
|
Array => "array",
|
|
16
17
|
Hash => "object"
|
|
17
|
-
}.freeze #: Hash[
|
|
18
|
+
}.freeze #: Hash[Module, String]
|
|
19
|
+
|
|
20
|
+
# Primitive types allowed for the +of:+ keyword on Array params
|
|
21
|
+
PRIMITIVE_TYPES = (TYPE_MAPPINGS.keys - [Array, Hash]).freeze #: Array[Class]
|
|
18
22
|
|
|
19
23
|
attr_reader :name #: Symbol
|
|
20
24
|
attr_reader :type #: Class
|
|
@@ -22,15 +26,19 @@ class Riffer::Param
|
|
|
22
26
|
attr_reader :description #: String?
|
|
23
27
|
attr_reader :enum #: Array[untyped]?
|
|
24
28
|
attr_reader :default #: untyped
|
|
29
|
+
attr_reader :item_type #: Class?
|
|
30
|
+
attr_reader :nested_params #: Riffer::Params?
|
|
25
31
|
|
|
26
|
-
#: (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped) -> void
|
|
27
|
-
def initialize(name:, type:, required:, description: nil, enum: nil, default: nil)
|
|
32
|
+
#: (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
|
|
33
|
+
def initialize(name:, type:, required:, description: nil, enum: nil, default: nil, item_type: nil, nested_params: nil)
|
|
28
34
|
@name = name.to_sym
|
|
29
35
|
@type = type
|
|
30
36
|
@required = required
|
|
31
37
|
@description = description
|
|
32
38
|
@enum = enum
|
|
33
39
|
@default = default
|
|
40
|
+
@item_type = item_type
|
|
41
|
+
@nested_params = nested_params
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
# Validates that a value matches the expected type.
|
|
@@ -39,7 +47,7 @@ class Riffer::Param
|
|
|
39
47
|
def valid_type?(value)
|
|
40
48
|
return true if value.nil? && !required
|
|
41
49
|
|
|
42
|
-
if type == TrueClass || type == FalseClass
|
|
50
|
+
if type == Riffer::Boolean || type == TrueClass || type == FalseClass
|
|
43
51
|
value == true || value == false
|
|
44
52
|
else
|
|
45
53
|
value.is_a?(type)
|
|
@@ -55,11 +63,27 @@ class Riffer::Param
|
|
|
55
63
|
|
|
56
64
|
# Converts this parameter to JSON Schema format.
|
|
57
65
|
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
# When +strict+ is true, optional parameters are made nullable
|
|
67
|
+
# (+["type", "null"]+) so that strict mode providers can distinguish
|
|
68
|
+
# "absent" from "present" without rejecting the schema.
|
|
69
|
+
#
|
|
70
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
71
|
+
def to_json_schema(strict: false)
|
|
72
|
+
type = type_name
|
|
73
|
+
type = [type, "null"] if strict && !required
|
|
74
|
+
|
|
75
|
+
schema = {type: type}
|
|
61
76
|
schema[:description] = description if description
|
|
62
77
|
schema[:enum] = enum if enum
|
|
78
|
+
|
|
79
|
+
if self.type == Array && nested_params
|
|
80
|
+
schema[:items] = nested_params.to_json_schema(strict: strict)
|
|
81
|
+
elsif self.type == Array && item_type
|
|
82
|
+
schema[:items] = {type: TYPE_MAPPINGS[item_type]}
|
|
83
|
+
elsif self.type == Hash && nested_params
|
|
84
|
+
schema.merge!(nested_params.to_json_schema(strict: strict))
|
|
85
|
+
end
|
|
86
|
+
|
|
63
87
|
schema
|
|
64
88
|
end
|
|
65
89
|
end
|
data/lib/riffer/params.rb
CHANGED
|
@@ -21,28 +21,34 @@ class Riffer::Params
|
|
|
21
21
|
|
|
22
22
|
# Defines a required parameter.
|
|
23
23
|
#
|
|
24
|
-
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?) -> void
|
|
25
|
-
def required(name, type, description: nil, enum: nil)
|
|
24
|
+
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ () -> void } -> void
|
|
25
|
+
def required(name, type, description: nil, enum: nil, of: nil, &block)
|
|
26
|
+
nested = build_nested(type, of, &block)
|
|
26
27
|
@parameters << Riffer::Param.new(
|
|
27
28
|
name: name,
|
|
28
29
|
type: type,
|
|
29
30
|
required: true,
|
|
30
31
|
description: description,
|
|
31
|
-
enum: enum
|
|
32
|
+
enum: enum,
|
|
33
|
+
item_type: of,
|
|
34
|
+
nested_params: nested
|
|
32
35
|
)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Defines an optional parameter.
|
|
36
39
|
#
|
|
37
|
-
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped) -> void
|
|
38
|
-
def optional(name, type, description: nil, enum: nil, default: nil)
|
|
40
|
+
#: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ () -> void } -> void
|
|
41
|
+
def optional(name, type, description: nil, enum: nil, default: nil, of: nil, &block)
|
|
42
|
+
nested = build_nested(type, of, &block)
|
|
39
43
|
@parameters << Riffer::Param.new(
|
|
40
44
|
name: name,
|
|
41
45
|
type: type,
|
|
42
46
|
required: false,
|
|
43
47
|
description: description,
|
|
44
48
|
enum: enum,
|
|
45
|
-
default: default
|
|
49
|
+
default: default,
|
|
50
|
+
item_type: of,
|
|
51
|
+
nested_params: nested
|
|
46
52
|
)
|
|
47
53
|
end
|
|
48
54
|
|
|
@@ -78,6 +84,8 @@ class Riffer::Params
|
|
|
78
84
|
next
|
|
79
85
|
end
|
|
80
86
|
|
|
87
|
+
value = validate_nested(param, value, errors)
|
|
88
|
+
|
|
81
89
|
validated[param.name] = value
|
|
82
90
|
end
|
|
83
91
|
|
|
@@ -88,14 +96,18 @@ class Riffer::Params
|
|
|
88
96
|
|
|
89
97
|
# Converts all parameters to JSON Schema format.
|
|
90
98
|
#
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
# When +strict+ is true, every property appears in +required+ and
|
|
100
|
+
# optional properties are made nullable instead. This satisfies
|
|
101
|
+
# providers that enforce strict structured output schemas.
|
|
102
|
+
#
|
|
103
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
104
|
+
def to_json_schema(strict: false)
|
|
93
105
|
properties = {}
|
|
94
106
|
required_params = []
|
|
95
107
|
|
|
96
108
|
@parameters.each do |param|
|
|
97
|
-
properties[param.name.to_s] = param.to_json_schema
|
|
98
|
-
required_params << param.name.to_s if param.required
|
|
109
|
+
properties[param.name.to_s] = param.to_json_schema(strict: strict)
|
|
110
|
+
required_params << param.name.to_s if strict || param.required
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
{
|
|
@@ -105,4 +117,99 @@ class Riffer::Params
|
|
|
105
117
|
additionalProperties: false
|
|
106
118
|
}
|
|
107
119
|
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
#: (Class, Class?) ?{ () -> void } -> Riffer::Params?
|
|
124
|
+
def build_nested(type, of, &block)
|
|
125
|
+
if of && block
|
|
126
|
+
raise Riffer::ArgumentError, "cannot use both of: and a block"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if of
|
|
130
|
+
unless type == Array
|
|
131
|
+
raise Riffer::ArgumentError, "of: can only be used with Array type, got #{type}"
|
|
132
|
+
end
|
|
133
|
+
unless Riffer::Param::PRIMITIVE_TYPES.include?(of)
|
|
134
|
+
raise Riffer::ArgumentError,
|
|
135
|
+
"of: must be a primitive type (#{Riffer::Param::PRIMITIVE_TYPES.map(&:name).join(", ")}), got #{of}"
|
|
136
|
+
end
|
|
137
|
+
return nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if block
|
|
141
|
+
unless type == Hash || type == Array
|
|
142
|
+
raise Riffer::ArgumentError, "block can only be used with Hash or Array type, got #{type}"
|
|
143
|
+
end
|
|
144
|
+
nested = Riffer::Params.new
|
|
145
|
+
nested.instance_eval(&block)
|
|
146
|
+
nested
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#: (Riffer::Param, untyped, Array[String]) -> untyped
|
|
151
|
+
def validate_nested(param, value, errors)
|
|
152
|
+
if param.type == Hash && param.nested_params
|
|
153
|
+
validate_nested_hash(param, value, errors)
|
|
154
|
+
elsif param.type == Array && param.nested_params
|
|
155
|
+
validate_nested_array_of_objects(param, value, errors)
|
|
156
|
+
elsif param.type == Array && param.item_type
|
|
157
|
+
validate_typed_array(param, value, errors)
|
|
158
|
+
value
|
|
159
|
+
else
|
|
160
|
+
value
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
#: (Riffer::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
165
|
+
def validate_nested_hash(param, value, errors)
|
|
166
|
+
sym_value = deep_symbolize_keys(value)
|
|
167
|
+
param.nested_params.validate(sym_value)
|
|
168
|
+
rescue Riffer::ValidationError => e
|
|
169
|
+
e.message.split("; ").each do |msg|
|
|
170
|
+
errors << "#{param.name}.#{msg}"
|
|
171
|
+
end
|
|
172
|
+
sym_value
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
#: (Riffer::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
176
|
+
def validate_nested_array_of_objects(param, value, errors)
|
|
177
|
+
value.map.with_index do |item, i|
|
|
178
|
+
unless item.is_a?(Hash)
|
|
179
|
+
errors << "#{param.name}[#{i}] must be an object"
|
|
180
|
+
next item
|
|
181
|
+
end
|
|
182
|
+
sym_item = deep_symbolize_keys(item)
|
|
183
|
+
param.nested_params.validate(sym_item)
|
|
184
|
+
rescue Riffer::ValidationError => e
|
|
185
|
+
e.message.split("; ").each do |msg|
|
|
186
|
+
errors << "#{param.name}[#{i}].#{msg}"
|
|
187
|
+
end
|
|
188
|
+
sym_item
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
#: (Riffer::Param, Array[untyped], Array[String]) -> void
|
|
193
|
+
def validate_typed_array(param, value, errors)
|
|
194
|
+
type_name = Riffer::Param::TYPE_MAPPINGS[param.item_type]
|
|
195
|
+
value.each_with_index do |item, i|
|
|
196
|
+
valid = if param.item_type == Riffer::Boolean || param.item_type == TrueClass || param.item_type == FalseClass
|
|
197
|
+
item == true || item == false
|
|
198
|
+
else
|
|
199
|
+
item.is_a?(param.item_type)
|
|
200
|
+
end
|
|
201
|
+
errors << "#{param.name}[#{i}] must be a #{type_name}" unless valid
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
|
|
206
|
+
def deep_symbolize_keys(hash)
|
|
207
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
208
|
+
result[key.to_sym] = case value
|
|
209
|
+
when Hash then deep_symbolize_keys(value)
|
|
210
|
+
when Array then value.map { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
|
|
211
|
+
else value
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
108
215
|
end
|
|
@@ -50,12 +50,15 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
if structured_output
|
|
53
|
+
# Use strict schema to make optional fields nullable. Without this,
|
|
54
|
+
# Bedrock may return string literals like ": null," instead of actual
|
|
55
|
+
# null values for optional fields that the model has no value for.
|
|
53
56
|
params[:output_config] = {
|
|
54
57
|
text_format: {
|
|
55
58
|
type: "json_schema",
|
|
56
59
|
structure: {
|
|
57
60
|
json_schema: {
|
|
58
|
-
schema: structured_output.json_schema.to_json,
|
|
61
|
+
schema: structured_output.json_schema(strict: true).to_json,
|
|
59
62
|
name: "response"
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -302,7 +305,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
302
305
|
name: tool.name,
|
|
303
306
|
description: tool.description,
|
|
304
307
|
input_schema: {
|
|
305
|
-
json: tool.parameters_schema
|
|
308
|
+
json: tool.parameters_schema(strict: true)
|
|
306
309
|
}
|
|
307
310
|
}
|
|
308
311
|
}
|
|
@@ -50,10 +50,13 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
if structured_output
|
|
53
|
+
# Use strict schema to make optional fields nullable. Without this,
|
|
54
|
+
# Anthropic may return empty strings or whitespace instead of null
|
|
55
|
+
# for optional fields that the model has no value for.
|
|
53
56
|
params[:output_config] = {
|
|
54
57
|
format: {
|
|
55
58
|
type: "json_schema",
|
|
56
|
-
schema: structured_output.json_schema
|
|
59
|
+
schema: structured_output.json_schema(strict: true)
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
end
|
|
@@ -335,7 +338,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
335
338
|
{
|
|
336
339
|
name: tool.name,
|
|
337
340
|
description: tool.description,
|
|
338
|
-
input_schema: tool.parameters_schema
|
|
341
|
+
input_schema: tool.parameters_schema(strict: true)
|
|
339
342
|
}
|
|
340
343
|
end
|
|
341
344
|
end
|
|
@@ -46,11 +46,13 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
if structured_output
|
|
49
|
+
# OpenAI requires strict mode schemas.
|
|
50
|
+
# https://platform.openai.com/docs/guides/structured-outputs#all-fields-must-be-required
|
|
49
51
|
params[:text] = {
|
|
50
52
|
format: {
|
|
51
53
|
type: "json_schema",
|
|
52
54
|
name: "response",
|
|
53
|
-
schema: structured_output.json_schema,
|
|
55
|
+
schema: structured_output.json_schema(strict: true),
|
|
54
56
|
strict: true
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -292,7 +294,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
292
294
|
type: "function",
|
|
293
295
|
name: tool.name,
|
|
294
296
|
description: tool.description,
|
|
295
|
-
parameters: tool.parameters_schema,
|
|
297
|
+
parameters: tool.parameters_schema(strict: true),
|
|
296
298
|
strict: true
|
|
297
299
|
}
|
|
298
300
|
end
|
|
@@ -14,12 +14,17 @@ require "json"
|
|
|
14
14
|
#
|
|
15
15
|
class Riffer::StructuredOutput
|
|
16
16
|
attr_reader :params #: Riffer::Params
|
|
17
|
-
attr_reader :json_schema #: Hash[Symbol, untyped]
|
|
18
17
|
|
|
19
18
|
#: (Riffer::Params) -> void
|
|
20
19
|
def initialize(params)
|
|
21
20
|
@params = params
|
|
22
|
-
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns the JSON Schema for this structured output.
|
|
24
|
+
#
|
|
25
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
26
|
+
def json_schema(strict: false)
|
|
27
|
+
@params.to_json_schema(strict: strict)
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
# Parses a JSON string and validates it against the schema.
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -74,9 +74,9 @@ class Riffer::Tool
|
|
|
74
74
|
|
|
75
75
|
# Returns the JSON Schema for the tool's parameters.
|
|
76
76
|
#
|
|
77
|
-
#: () -> Hash[Symbol, untyped]
|
|
78
|
-
def self.parameters_schema
|
|
79
|
-
@params_builder&.to_json_schema || empty_schema
|
|
77
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
78
|
+
def self.parameters_schema(strict: false)
|
|
79
|
+
@params_builder&.to_json_schema(strict: strict) || empty_schema
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self.empty_schema # :nodoc:
|
data/lib/riffer/version.rb
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Generated from lib/riffer/boolean.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Riffer::Boolean is a sentinel type for declaring boolean parameters.
|
|
4
|
+
#
|
|
5
|
+
# Ruby has no +Boolean+ class (+true+ is +TrueClass+, +false+ is +FalseClass+).
|
|
6
|
+
# Use this module wherever you need a single type that means "boolean":
|
|
7
|
+
#
|
|
8
|
+
# required :verbose, Riffer::Boolean
|
|
9
|
+
module Riffer::Boolean
|
|
10
|
+
end
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
# Handles type validation and JSON Schema generation for individual parameters.
|
|
6
6
|
class Riffer::Param
|
|
7
7
|
# Maps Ruby types to JSON Schema type strings
|
|
8
|
-
TYPE_MAPPINGS: Hash[
|
|
8
|
+
TYPE_MAPPINGS: Hash[Module, String]
|
|
9
|
+
|
|
10
|
+
# Primitive types allowed for the +of:+ keyword on Array params
|
|
11
|
+
PRIMITIVE_TYPES: Array[Class]
|
|
9
12
|
|
|
10
13
|
attr_reader name: Symbol
|
|
11
14
|
|
|
@@ -19,8 +22,12 @@ class Riffer::Param
|
|
|
19
22
|
|
|
20
23
|
attr_reader default: untyped
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
attr_reader item_type: Class?
|
|
26
|
+
|
|
27
|
+
attr_reader nested_params: Riffer::Params?
|
|
28
|
+
|
|
29
|
+
# : (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
|
|
30
|
+
def initialize: (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
|
|
24
31
|
|
|
25
32
|
# Validates that a value matches the expected type.
|
|
26
33
|
#
|
|
@@ -34,6 +41,10 @@ class Riffer::Param
|
|
|
34
41
|
|
|
35
42
|
# Converts this parameter to JSON Schema format.
|
|
36
43
|
#
|
|
37
|
-
#
|
|
38
|
-
|
|
44
|
+
# When +strict+ is true, optional parameters are made nullable
|
|
45
|
+
# (+["type", "null"]+) so that strict mode providers can distinguish
|
|
46
|
+
# "absent" from "present" without rejecting the schema.
|
|
47
|
+
#
|
|
48
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
49
|
+
def to_json_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
39
50
|
end
|
|
@@ -17,13 +17,13 @@ class Riffer::Params
|
|
|
17
17
|
|
|
18
18
|
# Defines a required parameter.
|
|
19
19
|
#
|
|
20
|
-
# : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?) -> void
|
|
21
|
-
def required: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?) -> void
|
|
20
|
+
# : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ () -> void } -> void
|
|
21
|
+
def required: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ () -> void } -> void
|
|
22
22
|
|
|
23
23
|
# Defines an optional parameter.
|
|
24
24
|
#
|
|
25
|
-
# : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped) -> void
|
|
26
|
-
def optional: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped) -> void
|
|
25
|
+
# : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ () -> void } -> void
|
|
26
|
+
def optional: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ () -> void } -> void
|
|
27
27
|
|
|
28
28
|
# Validates arguments against parameter definitions.
|
|
29
29
|
#
|
|
@@ -34,6 +34,30 @@ class Riffer::Params
|
|
|
34
34
|
|
|
35
35
|
# Converts all parameters to JSON Schema format.
|
|
36
36
|
#
|
|
37
|
-
#
|
|
38
|
-
|
|
37
|
+
# When +strict+ is true, every property appears in +required+ and
|
|
38
|
+
# optional properties are made nullable instead. This satisfies
|
|
39
|
+
# providers that enforce strict structured output schemas.
|
|
40
|
+
#
|
|
41
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
42
|
+
def to_json_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# : (Class, Class?) ?{ () -> void } -> Riffer::Params?
|
|
47
|
+
def build_nested: (Class, Class?) ?{ () -> void } -> Riffer::Params?
|
|
48
|
+
|
|
49
|
+
# : (Riffer::Param, untyped, Array[String]) -> untyped
|
|
50
|
+
def validate_nested: (Riffer::Param, untyped, Array[String]) -> untyped
|
|
51
|
+
|
|
52
|
+
# : (Riffer::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
53
|
+
def validate_nested_hash: (Riffer::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
54
|
+
|
|
55
|
+
# : (Riffer::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
56
|
+
def validate_nested_array_of_objects: (Riffer::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
57
|
+
|
|
58
|
+
# : (Riffer::Param, Array[untyped], Array[String]) -> void
|
|
59
|
+
def validate_typed_array: (Riffer::Param, Array[untyped], Array[String]) -> void
|
|
60
|
+
|
|
61
|
+
# : (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
|
|
62
|
+
def deep_symbolize_keys: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
|
|
39
63
|
end
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
class Riffer::StructuredOutput
|
|
12
12
|
attr_reader params: Riffer::Params
|
|
13
13
|
|
|
14
|
-
attr_reader json_schema: Hash[Symbol, untyped]
|
|
15
|
-
|
|
16
14
|
# : (Riffer::Params) -> void
|
|
17
15
|
def initialize: (Riffer::Params) -> void
|
|
18
16
|
|
|
17
|
+
# Returns the JSON Schema for this structured output.
|
|
18
|
+
#
|
|
19
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
20
|
+
def json_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
21
|
+
|
|
19
22
|
# Parses a JSON string and validates it against the schema.
|
|
20
23
|
#
|
|
21
24
|
# Returns a Result with the validated object on success, or an error message on failure.
|
|
@@ -54,8 +54,8 @@ class Riffer::Tool
|
|
|
54
54
|
|
|
55
55
|
# Returns the JSON Schema for the tool's parameters.
|
|
56
56
|
#
|
|
57
|
-
# : () -> Hash[Symbol, untyped]
|
|
58
|
-
def self.parameters_schema: () -> Hash[Symbol, untyped]
|
|
57
|
+
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
58
|
+
def self.parameters_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
59
59
|
|
|
60
60
|
def self.empty_schema: () -> untyped
|
|
61
61
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -218,6 +218,7 @@ files:
|
|
|
218
218
|
- lib/riffer.rb
|
|
219
219
|
- lib/riffer/agent.rb
|
|
220
220
|
- lib/riffer/agent/response.rb
|
|
221
|
+
- lib/riffer/boolean.rb
|
|
221
222
|
- lib/riffer/config.rb
|
|
222
223
|
- lib/riffer/core.rb
|
|
223
224
|
- lib/riffer/evals.rb
|
|
@@ -278,6 +279,7 @@ files:
|
|
|
278
279
|
- sig/generated/riffer.rbs
|
|
279
280
|
- sig/generated/riffer/agent.rbs
|
|
280
281
|
- sig/generated/riffer/agent/response.rbs
|
|
282
|
+
- sig/generated/riffer/boolean.rbs
|
|
281
283
|
- sig/generated/riffer/config.rbs
|
|
282
284
|
- sig/generated/riffer/core.rbs
|
|
283
285
|
- sig/generated/riffer/evals.rbs
|