riffer 0.50.1 → 0.52.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/CHANGELOG.md +19 -0
- data/docs/AGENTS.md +26 -1
- data/docs/AGENT_LIFECYCLE.md +1 -1
- data/docs/AGENT_LOOP.md +2 -2
- data/docs/SERIALIZATION.md +3 -0
- data/lib/riffer/agent/config.rb +16 -0
- data/lib/riffer/agent/run.rb +1 -1
- data/lib/riffer/agent/serializer.rb +2 -0
- data/lib/riffer/agent/session.rb +7 -0
- data/lib/riffer/agent/structured_output/parser.rb +35 -0
- data/lib/riffer/agent/structured_output.rb +22 -5
- data/lib/riffer/agent.rb +4 -3
- data/lib/riffer/providers/base.rb +25 -7
- data/lib/riffer/version.rb +1 -1
- data/rbs_collection.lock.yaml +2 -2
- data/sig/generated/riffer/agent/config.rbs +10 -1
- data/sig/generated/riffer/agent/session.rbs +4 -0
- data/sig/generated/riffer/agent/structured_output/parser.rbs +17 -0
- data/sig/generated/riffer/agent/structured_output.rbs +13 -2
- data/sig/generated/riffer/agent.rbs +2 -2
- data/sig/generated/riffer/providers/base.rbs +8 -0
- data/sig/manual/riffer/agent/structured_output/parser.rbs +5 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a69720c69304a2b2ac26a5b03a3fa0866a58434687fc5745563a6a4dd0afc7c
|
|
4
|
+
data.tar.gz: aa9b4937c096c1e152b941f426b7f6b119d6b90d45935a57a42e9fd0f19ac0bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 169697ebceedccc7be95b4f98ddee84a93f48b330b1fe71a1207ad98d79822298e0432e7bb9ab4696ffa2e2709c8df4f795fbd2e6fd33a1643147be2f2cf8870
|
|
7
|
+
data.tar.gz: eb1eab78a9d50a63ea156b53c951e500944d5ef8ec622811dead87c0427198de9d979a31ec60b0de006d8c6409424d73b605ff7ddf997a88de55bd37eea3d3b1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.52.0](https://github.com/janeapp/riffer/compare/riffer/v0.51.0...riffer/v0.52.0) (2026-10-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **structured_output:** add experimental prompted strategy ([#477](https://github.com/janeapp/riffer/issues/477)) ([f517fe1](https://github.com/janeapp/riffer/commit/f517fe19788fe34167515998a7ac2870605b2228))
|
|
14
|
+
|
|
15
|
+
## [0.51.0](https://github.com/janeapp/riffer/compare/riffer/v0.50.1...riffer/v0.51.0) (2026-10-07)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **structured_output:** recover objects wrapped in code fences or prose ([#473](https://github.com/janeapp/riffer/issues/473)) ([59a6d40](https://github.com/janeapp/riffer/commit/59a6d40b08b9f18fcb4ebac24598925f94a9acea))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **agent:** apply the max_steps cap per run, not per conversation ([#478](https://github.com/janeapp/riffer/issues/478)) ([4948e6b](https://github.com/janeapp/riffer/commit/4948e6b9769df084bed8c8a29862067560f800bc))
|
|
26
|
+
|
|
8
27
|
## [0.50.1](https://github.com/janeapp/riffer/compare/riffer/v0.50.0...riffer/v0.50.1) (2026-10-02)
|
|
9
28
|
|
|
10
29
|
|
data/docs/AGENTS.md
CHANGED
|
@@ -168,7 +168,9 @@ end
|
|
|
168
168
|
|
|
169
169
|
The LLM response is automatically parsed and validated against the schema. Access the result via `response.structured_output`.
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
If the response is not valid JSON on its own, Riffer recovers a JSON object wrapped in a code fence (```` ```json ````), surrounded by prose (`Here is the data: {...}`), or wrapped in formatting (`**{...}**`). It parses the span from the first `{` to the last `}`, so a reply whose surrounding text contains other braces, or that holds more than one object, is still rejected; `response.content` keeps the raw text.
|
|
172
|
+
|
|
173
|
+
When the response contains no well-formed JSON object or does not satisfy the schema, `response.structured_output` is `nil`, `response.outcome.reason` is `:invalid_structured_output`, and `response.outcome.detail` carries the parse or validation message:
|
|
172
174
|
|
|
173
175
|
```ruby
|
|
174
176
|
response = SentimentAgent.generate('Analyze: "I love this!"')
|
|
@@ -181,6 +183,29 @@ else
|
|
|
181
183
|
end
|
|
182
184
|
```
|
|
183
185
|
|
|
186
|
+
#### Structured output strategy (experimental)
|
|
187
|
+
|
|
188
|
+
By default Riffer sends the schema through each provider's native structured-output field. Some models reject that field, accept it without enforcing it, or behave differently with it enabled (for example, skipping tool calls). For those, opt the agent into the **experimental** prompted strategy:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class SentimentAgent < Riffer::Agent
|
|
192
|
+
model 'amazon_bedrock/my-model'
|
|
193
|
+
instructions 'Analyze the sentiment of the given text.'
|
|
194
|
+
structured_output strategy: :prompted do
|
|
195
|
+
required :sentiment, String, description: "positive, negative, or neutral"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
With `strategy: :prompted`, the provider never receives its native structured-output field. Instead, an instruction carrying the JSON Schema is appended to the system instructions on every request of the run (it is not stored in `session.messages`), and the reply is parsed and validated exactly as above: `response.content` is the reply text, `response.structured_output` holds the validated object, and a reply that fails parsing or validation ends with `:invalid_structured_output`. Because nothing forces the model to comply, expect more `:invalid_structured_output` outcomes than with the native strategy.
|
|
201
|
+
|
|
202
|
+
| Strategy | Behavior |
|
|
203
|
+
| ------------------- | --------------------------------------------------------------- |
|
|
204
|
+
| `:native` (default) | Schema sent through the provider's structured-output field |
|
|
205
|
+
| `:prompted` | Schema sent in the system prompt; experimental, may change |
|
|
206
|
+
|
|
207
|
+
Riffer does not pick a strategy per model — the choice is always yours. The strategy is copied to subclasses and survives [serialization](SERIALIZATION.md). An unknown value raises `Riffer::ArgumentError`. `strategy:` can also be passed with a `Riffer::Params` instance: `structured_output params, strategy: :prompted`.
|
|
208
|
+
|
|
184
209
|
#### Nested Objects
|
|
185
210
|
|
|
186
211
|
Use `Hash` with a block to define nested object schemas:
|
data/docs/AGENT_LIFECYCLE.md
CHANGED
|
@@ -351,7 +351,7 @@ end
|
|
|
351
351
|
|
|
352
352
|
### response.structured_output
|
|
353
353
|
|
|
354
|
-
When structured output is configured, the LLM response is parsed as JSON and validated against the schema. The validated result is available as `response.structured_output`:
|
|
354
|
+
When structured output is configured, the LLM response is parsed as JSON and validated against the schema. A JSON object wrapped in a code fence, prose, or formatting is recovered (see [Agents — structured_output](AGENTS.md#structured_output)). The validated result is available as `response.structured_output`:
|
|
355
355
|
|
|
356
356
|
```ruby
|
|
357
357
|
response = SentimentAgent.generate('Analyze: "I love this!"')
|
data/docs/AGENT_LOOP.md
CHANGED
|
@@ -72,12 +72,12 @@ response = agent.generate('Approved, continue') # continues where it left off
|
|
|
72
72
|
|
|
73
73
|
### Max Steps Limit
|
|
74
74
|
|
|
75
|
-
The `max_steps` class method caps the number of LLM call steps in the tool-use loop. When the step count reaches the limit, the loop interrupts automatically with reason `:max_steps`.
|
|
75
|
+
The `max_steps` class method caps the number of LLM call steps in the tool-use loop of a single run. Steps are counted from the last user message in the session, so each new user message starts a fresh budget — prior turns' assistant messages in the history do not count against it. When the step count reaches the limit, the loop interrupts automatically with reason `:max_steps`.
|
|
76
76
|
|
|
77
77
|
- **When to use:** Safety net to prevent runaway tool-use loops — useful when agents have access to many tools or operate autonomously.
|
|
78
78
|
- **Response:** `response.outcome.reason` is `:max_steps`.
|
|
79
79
|
- **Streaming:** Yields an `Interrupt` event with `reason: :max_steps`.
|
|
80
|
-
- **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume. For cross-process resume, pass persisted messages as an array to a new agent. Pending tool calls are automatically executed before the LLM loop resumes.
|
|
80
|
+
- **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume with a fresh budget, or call `generate` / `stream` with no prompt to resume against the remaining budget. For cross-process resume, pass persisted messages as an array to a new agent; the same rule applies. Pending tool calls are automatically executed before the LLM loop resumes.
|
|
81
81
|
|
|
82
82
|
```ruby
|
|
83
83
|
class MyAgent < Riffer::Agent
|
data/docs/SERIALIZATION.md
CHANGED
|
@@ -45,6 +45,7 @@ The session is used **as-is**: the rebuilt agent does not prepend anything to it
|
|
|
45
45
|
model_options: { temperature: 0.2 },
|
|
46
46
|
max_steps: 8, # integer; -1 = unlimited (see below)
|
|
47
47
|
structured_output: { type: "object", … }, # JSON Schema, or null
|
|
48
|
+
structured_output_strategy: :native, # :native or :prompted ("native"/"prompted" over JSON)
|
|
48
49
|
tools: [ { name:, description:, parameters_schema:, timeout: }, … ]
|
|
49
50
|
}
|
|
50
51
|
```
|
|
@@ -57,6 +58,8 @@ The session is used **as-is**: the rebuilt agent does not prepend anything to it
|
|
|
57
58
|
|
|
58
59
|
Structured output crosses as **provider-neutral JSON Schema** (`Riffer::Params#to_json_schema`), never a provider-rendered schema. `from_h` rebuilds a validating `Riffer::Params` via `Riffer::Params.from_json_schema`, so the rebuilt agent both constrains the model and can `parse_and_validate` responses — rendering provider-correct bytes at call time, the same way an in-code agent does.
|
|
59
60
|
|
|
61
|
+
`structured_output_strategy` carries the [strategy](AGENTS.md#structured-output-strategy-experimental) the agent was configured with, so a `:prompted` agent rebuilds as `:prompted`. A hash without the key decodes as `:native`.
|
|
62
|
+
|
|
60
63
|
## Reconstructing tools
|
|
61
64
|
|
|
62
65
|
Tools cross as `{name, description, parameters_schema, timeout}` descriptors — never code. How a descriptor becomes a runnable tool is controlled by the `tool_resolver:` you pass to `from_h`. The two common shapes:
|
data/lib/riffer/agent/config.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
class Riffer::Agent::Config
|
|
5
5
|
DEFAULT_MAX_STEPS = 16 #: Integer
|
|
6
|
+
STRUCTURED_OUTPUT_STRATEGIES = %i[native prompted].freeze #: Array[Symbol]
|
|
6
7
|
|
|
7
8
|
# @rbs @tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ class Riffer::Agent::Config
|
|
|
11
12
|
attr_reader :instructions #: (String | Proc)? # @dynamic instructions
|
|
12
13
|
attr_accessor :model_options #: Hash[Symbol, untyped] # @dynamic model_options, model_options=
|
|
13
14
|
attr_reader :structured_output #: Riffer::Params? # @dynamic structured_output
|
|
15
|
+
attr_reader :structured_output_strategy #: Symbol # @dynamic structured_output_strategy
|
|
14
16
|
attr_accessor :max_steps #: Numeric? # @dynamic max_steps, max_steps=
|
|
15
17
|
attr_accessor :tools_config #: (Array[singleton(Riffer::Tool)] | Proc)? # @dynamic tools_config, tools_config=
|
|
16
18
|
attr_reader :mcp_configs #: Array[Hash[Symbol, untyped]] # @dynamic mcp_configs
|
|
@@ -24,6 +26,7 @@ class Riffer::Agent::Config
|
|
|
24
26
|
# ?instructions: (String | Proc)?,
|
|
25
27
|
# ?model_options: Hash[Symbol, untyped],
|
|
26
28
|
# ?structured_output: Riffer::Params?,
|
|
29
|
+
# ?structured_output_strategy: Symbol,
|
|
27
30
|
# ?max_steps: Numeric?,
|
|
28
31
|
# ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?,
|
|
29
32
|
# ?mcp_configs: Array[Hash[Symbol, untyped]],
|
|
@@ -37,6 +40,7 @@ class Riffer::Agent::Config
|
|
|
37
40
|
instructions: nil,
|
|
38
41
|
model_options: {},
|
|
39
42
|
structured_output: nil,
|
|
43
|
+
structured_output_strategy: :native,
|
|
40
44
|
max_steps: DEFAULT_MAX_STEPS,
|
|
41
45
|
tools_config: nil,
|
|
42
46
|
mcp_configs: [],
|
|
@@ -54,6 +58,7 @@ class Riffer::Agent::Config
|
|
|
54
58
|
self.model = model
|
|
55
59
|
self.instructions = instructions
|
|
56
60
|
self.structured_output = structured_output
|
|
61
|
+
self.structured_output_strategy = structured_output_strategy
|
|
57
62
|
self.tool_runtime = tool_runtime if tool_runtime
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -73,6 +78,17 @@ class Riffer::Agent::Config
|
|
|
73
78
|
@structured_output = value
|
|
74
79
|
end
|
|
75
80
|
|
|
81
|
+
#--
|
|
82
|
+
#: (Symbol) -> Symbol
|
|
83
|
+
def structured_output_strategy=(value)
|
|
84
|
+
unless STRUCTURED_OUTPUT_STRATEGIES.include?(value)
|
|
85
|
+
raise Riffer::ArgumentError,
|
|
86
|
+
"structured_output_strategy must be one of #{STRUCTURED_OUTPUT_STRATEGIES.inspect}, got #{value.inspect}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@structured_output_strategy = value
|
|
90
|
+
end
|
|
91
|
+
|
|
76
92
|
#--
|
|
77
93
|
#: () -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
78
94
|
def tool_runtime
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Riffer::Agent::Serializer
|
|
|
27
27
|
model_options: config.model_options,
|
|
28
28
|
max_steps: encode_max_steps(config.max_steps),
|
|
29
29
|
structured_output: config.structured_output&.to_json_schema(strict: false),
|
|
30
|
+
structured_output_strategy: config.structured_output_strategy,
|
|
30
31
|
tools: agent.tools.map { |tool_class| tool_descriptor(tool_class) },
|
|
31
32
|
}
|
|
32
33
|
end
|
|
@@ -76,6 +77,7 @@ module Riffer::Agent::Serializer
|
|
|
76
77
|
instructions: hash[:instructions],
|
|
77
78
|
model_options: hash[:model_options] || {},
|
|
78
79
|
structured_output: decode_structured_output(hash[:structured_output]),
|
|
80
|
+
structured_output_strategy: (hash[:structured_output_strategy] || :native).to_sym,
|
|
79
81
|
max_steps: decode_max_steps(hash),
|
|
80
82
|
tools_config: tools,
|
|
81
83
|
} #: Hash[Symbol, untyped]
|
data/lib/riffer/agent/session.rb
CHANGED
|
@@ -143,6 +143,13 @@ class Riffer::Agent::Session
|
|
|
143
143
|
@messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
+
#--
|
|
147
|
+
#: () -> Integer
|
|
148
|
+
def steps_since_last_user_message
|
|
149
|
+
since_last_user = @messages.reverse_each.take_while { |m| !m.is_a?(Riffer::Messages::User) }
|
|
150
|
+
since_last_user.count { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
151
|
+
end
|
|
152
|
+
|
|
146
153
|
#--
|
|
147
154
|
#: () -> Riffer::Messages::Assistant?
|
|
148
155
|
def final_assistant_message
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Riffer::Agent::StructuredOutput::Parser
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
#--
|
|
10
|
+
#: (String) -> Hash[Symbol, untyped]?
|
|
11
|
+
def parse(content)
|
|
12
|
+
parse_object(content) || outermost_object(content)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
#--
|
|
18
|
+
#: (String) -> Hash[Symbol, untyped]?
|
|
19
|
+
def outermost_object(content)
|
|
20
|
+
start = content.index("{")
|
|
21
|
+
stop = content.rindex("}")
|
|
22
|
+
return unless start && stop
|
|
23
|
+
|
|
24
|
+
parse_object(content[start..stop].to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#--
|
|
28
|
+
#: (String) -> Hash[Symbol, untyped]?
|
|
29
|
+
def parse_object(json)
|
|
30
|
+
parsed = JSON.parse(json, symbolize_names: true)
|
|
31
|
+
parsed if parsed.is_a?(Hash)
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -5,11 +5,19 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
class Riffer::Agent::StructuredOutput
|
|
7
7
|
attr_reader :params #: Riffer::Params # @dynamic params
|
|
8
|
+
attr_reader :strategy #: Symbol # @dynamic strategy
|
|
8
9
|
|
|
9
10
|
#--
|
|
10
|
-
#: (Riffer::Params) -> void
|
|
11
|
-
def initialize(params)
|
|
11
|
+
#: (Riffer::Params, ?strategy: Symbol) -> void
|
|
12
|
+
def initialize(params, strategy: :native)
|
|
12
13
|
@params = params
|
|
14
|
+
@strategy = strategy
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#--
|
|
18
|
+
#: () -> bool
|
|
19
|
+
def prompted?
|
|
20
|
+
@strategy == :prompted
|
|
13
21
|
end
|
|
14
22
|
|
|
15
23
|
#--
|
|
@@ -18,14 +26,23 @@ class Riffer::Agent::StructuredOutput
|
|
|
18
26
|
@params.to_json_schema(strict: strict)
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
# Worded for the final answer only, so the model can still call tools first.
|
|
30
|
+
#--
|
|
31
|
+
#: () -> String
|
|
32
|
+
def prompt_instructions
|
|
33
|
+
"When you give your final answer, respond with only a JSON object that conforms to " \
|
|
34
|
+
"the following JSON Schema, with no other text before or after it:\n\n" \
|
|
35
|
+
"#{JSON.generate(json_schema)}"
|
|
36
|
+
end
|
|
37
|
+
|
|
21
38
|
#--
|
|
22
39
|
#: (String) -> Riffer::Agent::StructuredOutput::Result
|
|
23
40
|
def parse_and_validate(json_string)
|
|
24
|
-
parsed =
|
|
41
|
+
parsed = Riffer::Agent::StructuredOutput::Parser.parse(json_string)
|
|
42
|
+
return Result.new(error: "JSON parse error: no JSON object found in the response") if parsed.nil?
|
|
43
|
+
|
|
25
44
|
validated = @params.validate(parsed)
|
|
26
45
|
Result.new(object: validated)
|
|
27
|
-
rescue JSON::ParserError => e
|
|
28
|
-
Result.new(error: "JSON parse error: #{e.message}")
|
|
29
46
|
rescue Riffer::ValidationError => e
|
|
30
47
|
Result.new(error: "Validation error: #{e.message}")
|
|
31
48
|
end
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -53,13 +53,14 @@ class Riffer::Agent
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
#--
|
|
56
|
-
#: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
57
|
-
def self.structured_output(params = nil, &block)
|
|
56
|
+
#: (?Riffer::Params?, ?strategy: Symbol?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
57
|
+
def self.structured_output(params = nil, strategy: nil, &block)
|
|
58
58
|
if block
|
|
59
59
|
params = Riffer::Params.new
|
|
60
60
|
params.instance_eval(&block)
|
|
61
61
|
end
|
|
62
62
|
config.structured_output = params if params
|
|
63
|
+
config.structured_output_strategy = strategy if strategy
|
|
63
64
|
config.structured_output
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -328,7 +329,7 @@ class Riffer::Agent
|
|
|
328
329
|
#: () -> Riffer::Agent::StructuredOutput?
|
|
329
330
|
def resolve_structured_output
|
|
330
331
|
params = @config.structured_output
|
|
331
|
-
params ? Riffer::Agent::StructuredOutput.new(params) : nil
|
|
332
|
+
params ? Riffer::Agent::StructuredOutput.new(params, strategy: @config.structured_output_strategy) : nil
|
|
332
333
|
end
|
|
333
334
|
|
|
334
335
|
#--
|
|
@@ -38,8 +38,8 @@ class Riffer::Providers::Base
|
|
|
38
38
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
39
39
|
validate_normalized_messages!(messages)
|
|
40
40
|
Riffer::Files::Resolver.new(provider: self).resolve!(messages)
|
|
41
|
-
messages = merge_consecutive_messages(messages)
|
|
42
|
-
params = build_request_params(messages, model, options)
|
|
41
|
+
messages = merge_consecutive_messages(with_structured_output_prompt(messages, options[:structured_output]))
|
|
42
|
+
params = build_request_params(messages, model, request_options(options))
|
|
43
43
|
|
|
44
44
|
in_chat_span(model, messages, options) do |span|
|
|
45
45
|
response = execute_generate(params)
|
|
@@ -76,8 +76,8 @@ class Riffer::Providers::Base
|
|
|
76
76
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
77
77
|
validate_normalized_messages!(messages)
|
|
78
78
|
Riffer::Files::Resolver.new(provider: self).resolve!(messages)
|
|
79
|
-
messages = merge_consecutive_messages(messages)
|
|
80
|
-
params = build_request_params(messages, model, options)
|
|
79
|
+
messages = merge_consecutive_messages(with_structured_output_prompt(messages, options[:structured_output]))
|
|
80
|
+
params = build_request_params(messages, model, request_options(options))
|
|
81
81
|
|
|
82
82
|
# The enumerator body runs in its own fiber, where the fiber-local OTEL
|
|
83
83
|
# context is empty — capture here so the chat span parents to the caller's
|
|
@@ -360,9 +360,7 @@ class Riffer::Providers::Base
|
|
|
360
360
|
#--
|
|
361
361
|
#: (String) -> Hash[Symbol, untyped]?
|
|
362
362
|
def parse_structured_output(content)
|
|
363
|
-
|
|
364
|
-
rescue JSON::ParserError
|
|
365
|
-
nil
|
|
363
|
+
Riffer::Agent::StructuredOutput::Parser.parse(content)
|
|
366
364
|
end
|
|
367
365
|
|
|
368
366
|
#--
|
|
@@ -373,6 +371,26 @@ class Riffer::Providers::Base
|
|
|
373
371
|
JSON.parse(arguments)
|
|
374
372
|
end
|
|
375
373
|
|
|
374
|
+
#--
|
|
375
|
+
#: (Array[Riffer::Messages::Base], Riffer::Agent::StructuredOutput?) -> Array[Riffer::Messages::Base]
|
|
376
|
+
def with_structured_output_prompt(messages, structured_output)
|
|
377
|
+
return messages unless structured_output&.prompted?
|
|
378
|
+
|
|
379
|
+
# After the leading system messages, so merge_consecutive_messages folds it into the system prompt.
|
|
380
|
+
leading_system = messages.take_while { |m| m.is_a?(Riffer::Messages::System) }
|
|
381
|
+
[
|
|
382
|
+
*leading_system,
|
|
383
|
+
Riffer::Messages::System.new(structured_output.prompt_instructions),
|
|
384
|
+
*messages.drop(leading_system.length),
|
|
385
|
+
]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
#--
|
|
389
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
390
|
+
def request_options(options)
|
|
391
|
+
options[:structured_output]&.prompted? ? options.except(:structured_output) : options
|
|
392
|
+
end
|
|
393
|
+
|
|
376
394
|
#--
|
|
377
395
|
#: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
|
|
378
396
|
def merge_consecutive_messages(messages)
|
data/lib/riffer/version.rb
CHANGED
data/rbs_collection.lock.yaml
CHANGED
|
@@ -10,7 +10,7 @@ gems:
|
|
|
10
10
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
11
11
|
repo_dir: gems
|
|
12
12
|
- name: anthropic
|
|
13
|
-
version: 1.
|
|
13
|
+
version: 1.76.0
|
|
14
14
|
source:
|
|
15
15
|
type: rubygems
|
|
16
16
|
- name: ast
|
|
@@ -170,7 +170,7 @@ gems:
|
|
|
170
170
|
source:
|
|
171
171
|
type: stdlib
|
|
172
172
|
- name: openai
|
|
173
|
-
version: 0.
|
|
173
|
+
version: 0.97.0
|
|
174
174
|
source:
|
|
175
175
|
type: rubygems
|
|
176
176
|
- name: openssl
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
class Riffer::Agent::Config
|
|
4
4
|
DEFAULT_MAX_STEPS: Integer
|
|
5
5
|
|
|
6
|
+
STRUCTURED_OUTPUT_STRATEGIES: Array[Symbol]
|
|
7
|
+
|
|
6
8
|
@tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?
|
|
7
9
|
|
|
8
10
|
attr_reader identifier: String?
|
|
@@ -15,6 +17,8 @@ class Riffer::Agent::Config
|
|
|
15
17
|
|
|
16
18
|
attr_reader structured_output: Riffer::Params?
|
|
17
19
|
|
|
20
|
+
attr_reader structured_output_strategy: Symbol
|
|
21
|
+
|
|
18
22
|
attr_accessor max_steps: Numeric?
|
|
19
23
|
|
|
20
24
|
attr_accessor tools_config: (Array[singleton(Riffer::Tool)] | Proc)?
|
|
@@ -32,6 +36,7 @@ class Riffer::Agent::Config
|
|
|
32
36
|
# ?instructions: (String | Proc)?,
|
|
33
37
|
# ?model_options: Hash[Symbol, untyped],
|
|
34
38
|
# ?structured_output: Riffer::Params?,
|
|
39
|
+
# ?structured_output_strategy: Symbol,
|
|
35
40
|
# ?max_steps: Numeric?,
|
|
36
41
|
# ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?,
|
|
37
42
|
# ?mcp_configs: Array[Hash[Symbol, untyped]],
|
|
@@ -39,7 +44,7 @@ class Riffer::Agent::Config
|
|
|
39
44
|
# ?skills_config: Riffer::Skills::Config?,
|
|
40
45
|
# ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]
|
|
41
46
|
# ) -> void
|
|
42
|
-
def initialize: (?identifier: String?, ?model: (String | Proc)?, ?instructions: (String | Proc)?, ?model_options: Hash[Symbol, untyped], ?structured_output: Riffer::Params?, ?max_steps: Numeric?, ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?, ?mcp_configs: Array[Hash[Symbol, untyped]], ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?, ?skills_config: Riffer::Skills::Config?, ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]) -> void
|
|
47
|
+
def initialize: (?identifier: String?, ?model: (String | Proc)?, ?instructions: (String | Proc)?, ?model_options: Hash[Symbol, untyped], ?structured_output: Riffer::Params?, ?structured_output_strategy: Symbol, ?max_steps: Numeric?, ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?, ?mcp_configs: Array[Hash[Symbol, untyped]], ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?, ?skills_config: Riffer::Skills::Config?, ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]) -> void
|
|
43
48
|
|
|
44
49
|
# --
|
|
45
50
|
# : (untyped) -> String?
|
|
@@ -49,6 +54,10 @@ class Riffer::Agent::Config
|
|
|
49
54
|
# : (Riffer::Params?) -> Riffer::Params?
|
|
50
55
|
def structured_output=: (Riffer::Params?) -> Riffer::Params?
|
|
51
56
|
|
|
57
|
+
# --
|
|
58
|
+
# : (Symbol) -> Symbol
|
|
59
|
+
def structured_output_strategy=: (Symbol) -> Symbol
|
|
60
|
+
|
|
52
61
|
# --
|
|
53
62
|
# : () -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
54
63
|
def tool_runtime: () -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
@@ -57,6 +57,10 @@ class Riffer::Agent::Session
|
|
|
57
57
|
# : () -> Integer
|
|
58
58
|
def steps: () -> Integer
|
|
59
59
|
|
|
60
|
+
# --
|
|
61
|
+
# : () -> Integer
|
|
62
|
+
def steps_since_last_user_message: () -> Integer
|
|
63
|
+
|
|
60
64
|
# --
|
|
61
65
|
# : () -> Riffer::Messages::Assistant?
|
|
62
66
|
def final_assistant_message: () -> Riffer::Messages::Assistant?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/riffer/agent/structured_output/parser.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Riffer::Agent::StructuredOutput::Parser
|
|
4
|
+
# --
|
|
5
|
+
# : (String) -> Hash[Symbol, untyped]?
|
|
6
|
+
def parse: (String) -> Hash[Symbol, untyped]?
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# --
|
|
11
|
+
# : (String) -> Hash[Symbol, untyped]?
|
|
12
|
+
def outermost_object: (String) -> Hash[Symbol, untyped]?
|
|
13
|
+
|
|
14
|
+
# --
|
|
15
|
+
# : (String) -> Hash[Symbol, untyped]?
|
|
16
|
+
def parse_object: (String) -> Hash[Symbol, untyped]?
|
|
17
|
+
end
|
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
class Riffer::Agent::StructuredOutput
|
|
4
4
|
attr_reader params: Riffer::Params
|
|
5
5
|
|
|
6
|
+
attr_reader strategy: Symbol
|
|
7
|
+
|
|
8
|
+
# --
|
|
9
|
+
# : (Riffer::Params, ?strategy: Symbol) -> void
|
|
10
|
+
def initialize: (Riffer::Params, ?strategy: Symbol) -> void
|
|
11
|
+
|
|
6
12
|
# --
|
|
7
|
-
# : (
|
|
8
|
-
def
|
|
13
|
+
# : () -> bool
|
|
14
|
+
def prompted?: () -> bool
|
|
9
15
|
|
|
10
16
|
# --
|
|
11
17
|
# : (?strict: bool) -> Hash[Symbol, untyped]
|
|
12
18
|
def json_schema: (?strict: bool) -> Hash[Symbol, untyped]
|
|
13
19
|
|
|
20
|
+
# Worded for the final answer only, so the model can still call tools first.
|
|
21
|
+
# --
|
|
22
|
+
# : () -> String
|
|
23
|
+
def prompt_instructions: () -> String
|
|
24
|
+
|
|
14
25
|
# --
|
|
15
26
|
# : (String) -> Riffer::Agent::StructuredOutput::Result
|
|
16
27
|
def parse_and_validate: (String) -> Riffer::Agent::StructuredOutput::Result
|
|
@@ -32,8 +32,8 @@ class Riffer::Agent
|
|
|
32
32
|
def self.model_options: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
|
|
33
33
|
|
|
34
34
|
# --
|
|
35
|
-
# : (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
36
|
-
def self.structured_output: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
35
|
+
# : (?Riffer::Params?, ?strategy: Symbol?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
36
|
+
def self.structured_output: (?Riffer::Params?, ?strategy: Symbol?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
37
37
|
|
|
38
38
|
# --
|
|
39
39
|
# : (*Numeric?) -> Numeric?
|
|
@@ -146,6 +146,14 @@ class Riffer::Providers::Base
|
|
|
146
146
|
# : (String) -> Hash[String, untyped]
|
|
147
147
|
def parse_tool_arguments: (String) -> Hash[String, untyped]
|
|
148
148
|
|
|
149
|
+
# --
|
|
150
|
+
# : (Array[Riffer::Messages::Base], Riffer::Agent::StructuredOutput?) -> Array[Riffer::Messages::Base]
|
|
151
|
+
def with_structured_output_prompt: (Array[Riffer::Messages::Base], Riffer::Agent::StructuredOutput?) -> Array[Riffer::Messages::Base]
|
|
152
|
+
|
|
153
|
+
# --
|
|
154
|
+
# : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
155
|
+
def request_options: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
156
|
+
|
|
149
157
|
# --
|
|
150
158
|
# : (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
|
|
151
159
|
def merge_consecutive_messages: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# `Riffer::Agent::StructuredOutput::Parser` uses `extend self`; rbs-inline doesn't emit that,
|
|
2
|
+
# so re-extend here to expose its instance methods as singleton methods.
|
|
3
|
+
module Riffer::Agent::StructuredOutput::Parser
|
|
4
|
+
extend ::Riffer::Agent::StructuredOutput::Parser
|
|
5
|
+
end
|
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.52.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -117,6 +117,7 @@ files:
|
|
|
117
117
|
- lib/riffer/agent/session.rb
|
|
118
118
|
- lib/riffer/agent/session/repair.rb
|
|
119
119
|
- lib/riffer/agent/structured_output.rb
|
|
120
|
+
- lib/riffer/agent/structured_output/parser.rb
|
|
120
121
|
- lib/riffer/agent/structured_output/result.rb
|
|
121
122
|
- lib/riffer/config.rb
|
|
122
123
|
- lib/riffer/config/amazon_bedrock.rb
|
|
@@ -265,6 +266,7 @@ files:
|
|
|
265
266
|
- sig/generated/riffer/agent/session.rbs
|
|
266
267
|
- sig/generated/riffer/agent/session/repair.rbs
|
|
267
268
|
- sig/generated/riffer/agent/structured_output.rbs
|
|
269
|
+
- sig/generated/riffer/agent/structured_output/parser.rbs
|
|
268
270
|
- sig/generated/riffer/agent/structured_output/result.rbs
|
|
269
271
|
- sig/generated/riffer/config.rbs
|
|
270
272
|
- sig/generated/riffer/config/amazon_bedrock.rbs
|
|
@@ -390,6 +392,7 @@ files:
|
|
|
390
392
|
- sig/manual/riffer/agent/run.rbs
|
|
391
393
|
- sig/manual/riffer/agent/serializer.rbs
|
|
392
394
|
- sig/manual/riffer/agent/session/repair.rbs
|
|
395
|
+
- sig/manual/riffer/agent/structured_output/parser.rbs
|
|
393
396
|
- sig/manual/riffer/evals/evaluator_runner.rbs
|
|
394
397
|
- sig/manual/riffer/helpers/boolean.rbs
|
|
395
398
|
- sig/manual/riffer/helpers/call_or_value.rbs
|