riffer 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e6c1902fc4a3a1ad047491befa79ae44aa7aea3e160dda3303fe18c3670e477
4
- data.tar.gz: d4407ecd6a3af8900e7a2a456870e5e4a4211df016259980bf8f5b559ad8df2f
3
+ metadata.gz: dd2f811bba1934e2285de7f4ec122259039dcb61f6d6141e3c26992d38ba2e07
4
+ data.tar.gz: 877941ede9824184db9e7eb99ee901096995298937033f65c87562afeec7d208
5
5
  SHA512:
6
- metadata.gz: 5845e5336564993a8e23ff6a5a1c903fb28925991443d4beb2673a49f7d2d49587a2e799330ea1e30614f7c63343b35a9828d0a1ee1744d6c96e19c7aa476cc2
7
- data.tar.gz: b0c71cf0f09e04982b4942b75d7dee3cee757a007a6d7279ca036fa5f81ae2216690d630f3e0a34e5ba1ea07d680f034b0d85568c5111c37805a3ff72f4d6eb5
6
+ metadata.gz: 2477d9fcba22f212f7a83b6e7a400476aa7e4cc7c842d631d9044d7771827fcf903355f4015d9fa357e752ceb41a26fcfd62afd3711134c61a9745c3005dfe3c
7
+ data.tar.gz: d06abbd9da19f04cfc97449e72f283297ae07ab5dface13119c92cdf2121515c1de83ed6992daa1ab49ec460e15c5afaf4883b4224c9ed7c838f64b7f9ca816a
@@ -112,6 +112,7 @@ On resume, `execute_pending_tool_calls` detects tool calls from the last assista
112
112
  Concurrency primitive for batch execution. Subclasses implement `#map(items, context: nil, &block)` to control how items are processed. The `context` keyword carries the agent's context hash, enabling runners that need it for job serialization or routing.
113
113
 
114
114
  Built-in runners:
115
+
115
116
  - `Sequential` — processes items in the current thread via `Array#map`
116
117
  - `Threaded` — processes items concurrently using a thread pool with configurable `max_concurrency`
117
118
 
@@ -125,6 +126,7 @@ runner.map(items, context: ctx) { |item| process(item) }
125
126
  Composes with a Runner to execute tool calls. Provides `#execute` as the public entry point and `#around_tool_call` as a hook for instrumentation. Passes the agent context through to the runner.
126
127
 
127
128
  Built-in runtimes:
129
+
128
130
  - `Inline` — uses `Runner::Sequential` (default)
129
131
  - `Threaded` — uses `Runner::Threaded`
130
132
 
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.22.0"
2
+ ".": "0.24.0"
3
3
  }
data/AGENTS.md CHANGED
@@ -8,6 +8,7 @@ Ruby gem framework for building AI-powered agents with LLM provider adapters.
8
8
  - **Lint + Test**: `bundle exec rake`
9
9
  - **Autoloading**: Zeitwerk (file paths must match module/class names)
10
10
  - **Model format**: `provider/model` (e.g., `openai/gpt-4`)
11
+ - **Docs**: when adding a public config option or message attribute, update the matching page in `docs/` (e.g., `docs/10_CONFIGURATION.md`, `docs/08_MESSAGES.md`). RDoc ≠ user docs.
11
12
 
12
13
  ## Topic Guides
13
14
 
@@ -20,12 +21,12 @@ Ruby gem framework for building AI-powered agents with LLM provider adapters.
20
21
 
21
22
  ## Commands
22
23
 
23
- | Command | Description |
24
- |---------|-------------|
25
- | `bundle exec rake` | Run tests + lint (default) |
26
- | `bundle exec rake test` | Run tests only |
27
- | `bundle exec rake standard` | Check code style |
28
- | `bundle exec rake standard:fix` | Auto-fix style issues |
29
- | `bundle exec rake rbs:generate` | Generate RBS type signatures |
30
- | `bundle exec rake rbs:watch` | Watch and regenerate RBS files |
31
- | `bin/console` | Interactive console |
24
+ | Command | Description |
25
+ | ------------------------------- | ------------------------------ |
26
+ | `bundle exec rake` | Run tests + lint (default) |
27
+ | `bundle exec rake test` | Run tests only |
28
+ | `bundle exec rake standard` | Check code style |
29
+ | `bundle exec rake standard:fix` | Auto-fix style issues |
30
+ | `bundle exec rake rbs:generate` | Generate RBS type signatures |
31
+ | `bundle exec rake rbs:watch` | Watch and regenerate RBS files |
32
+ | `bin/console` | Interactive console |
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ 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.24.0](https://github.com/janeapp/riffer/compare/riffer/v0.23.0...riffer/v0.24.0) (2026-04-20)
9
+
10
+
11
+ ### Features
12
+
13
+ * add optional message id generation ([#210](https://github.com/janeapp/riffer/issues/210)) ([0f9e20d](https://github.com/janeapp/riffer/commit/0f9e20dcd2d69a49fe5da71be3f0b9deb0a046a4))
14
+
15
+ ## [0.23.0](https://github.com/janeapp/riffer/compare/riffer/v0.22.0...riffer/v0.23.0) (2026-04-15)
16
+
17
+
18
+ ### Features
19
+
20
+ * add Gemini provider ([#199](https://github.com/janeapp/riffer/issues/199)) ([d0f0823](https://github.com/janeapp/riffer/commit/d0f08237052258be64a8fb63e0d1c23508258176))
21
+
8
22
  ## [0.22.0](https://github.com/janeapp/riffer/compare/riffer/v0.21.0...riffer/v0.22.0) (2026-04-09)
9
23
 
10
24
 
data/Steepfile CHANGED
@@ -5,10 +5,13 @@ target :lib do
5
5
 
6
6
  check "lib"
7
7
 
8
- library "logger"
9
8
  library "anthropic"
10
- library "openai"
11
9
  library "aws-sdk-bedrockruntime"
10
+ library "aws-sdk-core"
11
+ library "logger"
12
+ library "net-http"
13
+ library "openai"
14
+ library "uri"
12
15
 
13
16
  configure_code_diagnostics(D::Ruby.lenient)
14
17
  end
data/docs/08_MESSAGES.md CHANGED
@@ -252,12 +252,12 @@ Without this step, the same model can receive different input depending on the p
252
252
 
253
253
  ### Merge rules
254
254
 
255
- | Message type | Content | Auxiliary data | Merged? |
256
- |--------------|---------|----------------|---------|
257
- | `System` | Joined with `"\n\n"` | — | Yes |
258
- | `User` | Joined with `"\n\n"` | `files` arrays concatenated | Yes |
259
- | `Assistant` | Joined with `"\n\n"` | `tool_calls` arrays concatenated | Yes |
260
- | `Tool` | — | — | Never (each has a unique `tool_call_id`) |
255
+ | Message type | Content | Auxiliary data | Merged? |
256
+ | ------------ | -------------------- | -------------------------------- | ---------------------------------------- |
257
+ | `System` | Joined with `"\n\n"` | — | Yes |
258
+ | `User` | Joined with `"\n\n"` | `files` arrays concatenated | Yes |
259
+ | `Assistant` | Joined with `"\n\n"` | `tool_calls` arrays concatenated | Yes |
260
+ | `Tool` | — | — | Never (each has a unique `tool_call_id`) |
261
261
 
262
262
  ### Example
263
263
 
@@ -278,20 +278,50 @@ agent.generate(messages)
278
278
 
279
279
  Merging happens at serialization time only. The agent's `messages` array still contains the original separate messages for logging, evals, and debugging.
280
280
 
281
+ ## IDs
282
+
283
+ Every message carries an optional `id` attribute. By default ids are disabled (`message.id` returns `nil` and `:id` is omitted from `to_h`). Enable them globally by setting `Riffer.config.message_id_strategy`:
284
+
285
+ ```ruby
286
+ Riffer.configure { |c| c.message_id_strategy = :uuidv7 }
287
+
288
+ msg = Riffer::Messages::User.new("Hello")
289
+ msg.id # => "0195a2e1-..." (auto-generated UUIDv7)
290
+ msg.to_h # => {role: :user, content: "Hello", id: "0195a2e1-..."}
291
+ ```
292
+
293
+ Supported strategies: `:none` (default), `:uuid`, `:uuidv7`. See [Configuration — Message ID Strategy](10_CONFIGURATION.md#message-id-strategy) for the full reference.
294
+
295
+ Ids pass through to subclass constructors via an `id:` kwarg and are preserved when set explicitly:
296
+
297
+ ```ruby
298
+ msg = Riffer::Messages::Assistant.new("Done.", id: "reply-42")
299
+ msg.id # => "reply-42"
300
+ ```
301
+
302
+ When seeding an agent with existing conversation history and the strategy is enabled, every seeded message must include an id — Riffer raises `Riffer::ArgumentError` on missing ids rather than fabricating them.
303
+
281
304
  ## Base Class
282
305
 
283
306
  All messages inherit from `Riffer::Messages::Base`:
284
307
 
285
308
  ```ruby
286
309
  class Riffer::Messages::Base
287
- attr_reader :content
310
+ attr_reader :content, :id
311
+
312
+ def initialize(content, id: nil)
313
+ @content = content
314
+ @id = id || generate_id # uses Riffer.config.message_id_strategy
315
+ end
288
316
 
289
317
  def role
290
318
  raise NotImplementedError
291
319
  end
292
320
 
293
321
  def to_h
294
- {role: role, content: content}
322
+ hash = {role: role, content: content}
323
+ hash[:id] = id unless id.nil?
324
+ hash
295
325
  end
296
326
  end
297
327
  ```
@@ -54,6 +54,39 @@ end
54
54
 
55
55
  Per-agent configuration overrides this global default. See [Advanced Tool Configuration — Tool Runtime](07_TOOL_ADVANCED.md#tool-runtime-experimental) for details.
56
56
 
57
+ ### Message ID Strategy
58
+
59
+ Opt in to stable identifiers on every message for logging, persistence, or replay:
60
+
61
+ ```ruby
62
+ Riffer.configure do |config|
63
+ config.message_id_strategy = :uuidv7
64
+ end
65
+ ```
66
+
67
+ | Value | Description |
68
+ | ----------------- | -------------------------------------------------------------------------------- |
69
+ | `:none` (default) | No id is generated; `message.id` returns `nil` and `:id` is omitted from `to_h`. |
70
+ | `:uuid` | UUIDv4 via `SecureRandom.uuid`. |
71
+ | `:uuidv7` | Time-ordered UUIDv7 via `SecureRandom.uuid_v7` (Ruby 3.3+). |
72
+
73
+ When the strategy is not `:none`, every `Riffer::Messages::Base` instance — user prompts, system instructions, assistant responses, and tool results — gets an auto-generated `id` at construction time. IDs are included in `message.to_h` when present and omitted when `nil`. Provider API payloads are unaffected; the `id` stays on the Ruby side.
74
+
75
+ Seeded messages passed to `agent.generate([...])` must carry their own `:id` when the strategy is enabled — Riffer never fabricates identifiers for pre-existing history:
76
+
77
+ ```ruby
78
+ Riffer.configure { |c| c.message_id_strategy = :uuidv7 }
79
+
80
+ agent.generate([
81
+ {role: :user, content: "Hi", id: "msg-001"},
82
+ {role: :assistant, content: "Hello!", id: "msg-002"}
83
+ ])
84
+ ```
85
+
86
+ Missing ids raise `Riffer::ArgumentError` with the offending index.
87
+
88
+ See [Messages — IDs](08_MESSAGES.md#ids) for more details.
89
+
57
90
  ## Agent-Level Configuration
58
91
 
59
92
  Override global configuration at the agent level:
@@ -10,6 +10,7 @@ Providers are adapters that connect Riffer to LLM services. They implement a com
10
10
  | Azure OpenAI | `azure_openai` | `openai` |
11
11
  | Amazon Bedrock | `amazon_bedrock` | `aws-sdk-bedrockruntime` |
12
12
  | Anthropic | `anthropic` | `anthropic` |
13
+ | Gemini | `gemini` | None |
13
14
  | Mock | `mock` | None |
14
15
 
15
16
  ## Model String Format
@@ -22,6 +23,7 @@ class MyAgent < Riffer::Agent
22
23
  model 'azure_openai/gpt-5-mini' # Azure OpenAI
23
24
  model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0' # Bedrock
24
25
  model 'anthropic/claude-haiku-4-5-20251001' # Anthropic
26
+ model 'gemini/gemini-2.5-flash-lite' # Gemini
25
27
  model 'mock/any' # Mock provider
26
28
  end
27
29
  ```
@@ -160,6 +162,9 @@ Riffer::Providers::Repository.find(:amazon_bedrock)
160
162
  Riffer::Providers::Repository.find(:anthropic)
161
163
  # => Riffer::Providers::Anthropic
162
164
 
165
+ Riffer::Providers::Repository.find(:gemini)
166
+ # => Riffer::Providers::Gemini
167
+
163
168
  Riffer::Providers::Repository.find(:mock)
164
169
  # => Riffer::Providers::Mock
165
170
  ```
@@ -172,3 +177,4 @@ Riffer::Providers::Repository.find(:mock)
172
177
  - [Azure OpenAI](05_AZURE_OPENAI.md) - GPT models via Azure
173
178
  - [Mock](06_MOCK_PROVIDER.md) - Mock provider for testing
174
179
  - [Custom Providers](07_CUSTOM_PROVIDERS.md) - Creating your own provider
180
+ - [Gemini](08_GEMINI.md) - Gemini models via Google GenAI API
@@ -0,0 +1,142 @@
1
+ # Gemini Provider
2
+
3
+ The Gemini provider connects to Google's Gemini models via the Gemini REST API.
4
+
5
+ ## Configuration
6
+
7
+ Configure your Gemini API key:
8
+
9
+ ```ruby
10
+ Riffer.configure do |config|
11
+ config.gemini.api_key = ENV['GEMINI_API_KEY']
12
+ end
13
+ ```
14
+
15
+ Or per-agent:
16
+
17
+ ```ruby
18
+ class MyAgent < Riffer::Agent
19
+ model 'gemini/gemini-2.5-flash-lite'
20
+ provider_options api_key: ENV['GEMINI_API_KEY']
21
+ end
22
+ ```
23
+
24
+ ## Supported Models
25
+
26
+ Use Gemini model IDs in the `gemini/model` format:
27
+
28
+ ```ruby
29
+ model 'gemini/gemini-2.5-flash-lite'
30
+ model 'gemini/gemini-2.5-pro'
31
+ model 'gemini/gemini-2.5-flash'
32
+ ```
33
+
34
+ ## Model Options
35
+
36
+ ### temperature
37
+
38
+ Controls randomness:
39
+
40
+ ```ruby
41
+ model_options temperature: 0.7
42
+ ```
43
+
44
+ ### maxOutputTokens
45
+
46
+ Maximum tokens in response:
47
+
48
+ ```ruby
49
+ model_options maxOutputTokens: 4096
50
+ ```
51
+
52
+ ### topP
53
+
54
+ Nucleus sampling:
55
+
56
+ ```ruby
57
+ model_options topP: 0.9
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Basic Generation
63
+
64
+ ```ruby
65
+ provider = Riffer::Providers::Gemini.new(api_key: ENV['GEMINI_API_KEY'])
66
+
67
+ response = provider.generate_text(
68
+ prompt: "Hello!",
69
+ model: "gemini-2.5-flash-lite"
70
+ )
71
+ puts response.content
72
+ ```
73
+
74
+ ### Streaming
75
+
76
+ ```ruby
77
+ provider.stream_text(prompt: "Tell me a story", model: "gemini-2.5-flash-lite").each do |event|
78
+ case event
79
+ when Riffer::StreamEvents::TextDelta
80
+ print event.content
81
+ when Riffer::StreamEvents::TextDone
82
+ puts "\n---"
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### Structured Output
88
+
89
+ ```ruby
90
+ params = Riffer::Params.new
91
+ params.required(:sentiment, String)
92
+ params.required(:score, Float)
93
+ structured_output = Riffer::StructuredOutput.new(params)
94
+
95
+ response = provider.generate_text(
96
+ prompt: "Analyze: 'This is great!'",
97
+ model: "gemini-2.5-flash-lite",
98
+ structured_output: structured_output
99
+ )
100
+ puts response.structured_output
101
+ ```
102
+
103
+ ### Tool Calling
104
+
105
+ ```ruby
106
+ class WeatherTool < Riffer::Tool
107
+ description "Gets weather"
108
+ params do
109
+ required :city, String
110
+ end
111
+ def call(context:, city:)
112
+ text("Sunny in #{city}")
113
+ end
114
+ end
115
+
116
+ response = provider.generate_text(
117
+ prompt: "What's the weather in Tokyo?",
118
+ model: "gemini-2.5-flash-lite",
119
+ tools: [WeatherTool]
120
+ )
121
+ ```
122
+
123
+ ### File Support
124
+
125
+ Gemini supports inline base64-encoded files (images and documents):
126
+
127
+ ```ruby
128
+ file = Riffer::FilePart.new(data: base64_data, media_type: "image/png")
129
+ response = provider.generate_text(
130
+ prompt: "Describe this image",
131
+ model: "gemini-2.5-flash-lite",
132
+ files: [file]
133
+ )
134
+ ```
135
+
136
+ **Note:** URL-based file references are not supported. Provide base64-encoded data instead.
137
+
138
+ ## Limitations
139
+
140
+ - **No web search** - Gemini's standard API does not include a web search tool
141
+ - **No URL files** - Only base64 inline data is supported for file attachments
142
+ - **Tool call IDs** - Gemini does not return unique call IDs for tool invocations; IDs are generated client-side
data/lib/riffer/agent.rb CHANGED
@@ -176,7 +176,7 @@ class Riffer::Agent
176
176
  #--
177
177
  #: (String) -> singleton(Riffer::Agent)?
178
178
  def self.find(identifier)
179
- subclasses.find { |agent_class| agent_class.identifier == identifier.to_s }
179
+ all.find { |agent_class| agent_class.identifier == identifier.to_s }
180
180
  end
181
181
 
182
182
  # Returns all agent subclasses.
@@ -184,7 +184,7 @@ class Riffer::Agent
184
184
  #--
185
185
  #: () -> Array[singleton(Riffer::Agent)]
186
186
  def self.all
187
- subclasses
187
+ subclasses #: Array[singleton(Riffer::Agent)]
188
188
  end
189
189
 
190
190
  # Generates a response using a new agent instance.
@@ -433,6 +433,7 @@ class Riffer::Agent
433
433
  if prompt_or_messages.is_a?(Array)
434
434
  raise Riffer::ArgumentError, "cannot pass an array of messages on an agent with existing messages; use a string to continue the conversation or a new agent instance to start fresh" if @messages.any?
435
435
  raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead" if files && !files.empty?
436
+ validate_seed_ids!(prompt_or_messages)
436
437
  @messages = prompt_or_messages.map { |item| convert_to_message_object(item) }
437
438
  elsif @messages.any?
438
439
  file_parts = (files || []).map { |f| convert_to_file_part(f) }
@@ -448,6 +449,24 @@ class Riffer::Agent
448
449
  end
449
450
  end
450
451
 
452
+ #--
453
+ #: (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
454
+ def validate_seed_ids!(items)
455
+ strategy = Riffer.config.message_id_strategy
456
+ return if strategy == :none
457
+
458
+ items.each_with_index do |item, idx|
459
+ raw_id = case item
460
+ when Hash then item[:id]
461
+ when Riffer::Messages::Base then item.id
462
+ else next # type errors surface later via convert_to_message_object
463
+ end
464
+ next unless raw_id.nil?
465
+ raise Riffer::ArgumentError,
466
+ "seeded message at index #{idx} is missing :id (required when Riffer.config.message_id_strategy = #{strategy.inspect})"
467
+ end
468
+ end
469
+
451
470
  #--
452
471
  #: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
453
472
  def build_instruction_message(context = @context)
@@ -783,7 +802,7 @@ class Riffer::Agent
783
802
  #--
784
803
  #: () -> Riffer::Messages::Assistant?
785
804
  def extract_final_response
786
- @messages.reverse.find { |msg| msg.is_a?(Riffer::Messages::Assistant) }
805
+ @messages.reverse.find { |msg| msg.is_a?(Riffer::Messages::Assistant) } #: Riffer::Messages::Assistant?
787
806
  end
788
807
 
789
808
  #--
data/lib/riffer/config.rb CHANGED
@@ -18,9 +18,12 @@ class Riffer::Config
18
18
  AmazonBedrock = Struct.new(:api_token, :region, keyword_init: true)
19
19
  Anthropic = Struct.new(:api_key, keyword_init: true)
20
20
  AzureOpenAI = Struct.new(:api_key, :endpoint, keyword_init: true)
21
+ Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
21
22
  OpenAI = Struct.new(:api_key, keyword_init: true)
22
23
  Evals = Struct.new(:judge_model, keyword_init: true)
23
24
 
25
+ VALID_MESSAGE_ID_STRATEGIES = %i[none uuid uuidv7].freeze
26
+
24
27
  # Amazon Bedrock configuration (Struct with +api_token+ and +region+).
25
28
  attr_reader :amazon_bedrock #: Riffer::Config::AmazonBedrock
26
29
 
@@ -30,6 +33,9 @@ class Riffer::Config
30
33
  # Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
31
34
  attr_reader :azure_openai #: Riffer::Config::AzureOpenAI
32
35
 
36
+ # Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
37
+ attr_reader :gemini #: Riffer::Config::Gemini
38
+
33
39
  # OpenAI configuration (Struct with +api_key+).
34
40
  attr_reader :openai #: Riffer::Config::OpenAI
35
41
 
@@ -55,14 +61,39 @@ class Riffer::Config
55
61
  @tool_runtime = value
56
62
  end
57
63
 
64
+ # Strategy for auto-generating message ids. One of +:none+ (default, no id),
65
+ # +:uuid+ (UUIDv4), or +:uuidv7+ (time-ordered UUIDv7).
66
+ #
67
+ # When set to anything other than +:none+, each +Riffer::Messages::Base+
68
+ # instance gets an +id+ populated at construction time, and seeded messages
69
+ # passed to +Riffer::Agent#generate+ must carry their own +:id+.
70
+ attr_reader :message_id_strategy #: Symbol
71
+
72
+ # Sets the message id strategy.
73
+ #
74
+ # Raises +Riffer::ArgumentError+ if the value is not one of
75
+ # +:none+, +:uuid+, or +:uuidv7+.
76
+ #
77
+ #--
78
+ #: (Symbol) -> void
79
+ def message_id_strategy=(value)
80
+ unless VALID_MESSAGE_ID_STRATEGIES.include?(value)
81
+ raise Riffer::ArgumentError,
82
+ "message_id_strategy must be one of #{VALID_MESSAGE_ID_STRATEGIES.inspect}, got #{value.inspect}"
83
+ end
84
+ @message_id_strategy = value
85
+ end
86
+
58
87
  #--
59
88
  #: () -> void
60
89
  def initialize
61
90
  @amazon_bedrock = AmazonBedrock.new
62
91
  @anthropic = Anthropic.new
63
92
  @azure_openai = AzureOpenAI.new
93
+ @gemini = Gemini.new
64
94
  @openai = OpenAI.new
65
95
  @evals = Evals.new
66
96
  @tool_runtime = Riffer::ToolRuntime::Inline.new
97
+ @message_id_strategy = :none
67
98
  end
68
99
  end
@@ -23,9 +23,9 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
23
23
  attr_reader :structured_output #: Hash[Symbol, untyped]?
24
24
 
25
25
  #--
26
- #: (String, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
27
- def initialize(content, tool_calls: [], token_usage: nil, structured_output: nil)
28
- super(content)
26
+ #: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
27
+ def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil)
28
+ super(content, id: id)
29
29
  @tool_calls = tool_calls
30
30
  @token_usage = token_usage
31
31
  @structured_output = structured_output
@@ -55,6 +55,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
55
55
  #: () -> Hash[Symbol, untyped]
56
56
  def to_h
57
57
  hash = {role: role, content: content}
58
+ hash[:id] = id unless id.nil?
58
59
  hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
59
60
  hash[:token_usage] = token_usage.to_h if token_usage
60
61
  hash[:structured_output] = structured_output if structured_output?
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
+ require "securerandom"
5
+
4
6
  # Base class for all message types in the Riffer framework.
5
7
  #
6
8
  # Subclasses must implement the +role+ method.
@@ -8,10 +10,14 @@ class Riffer::Messages::Base
8
10
  # The message content.
9
11
  attr_reader :content #: String
10
12
 
13
+ # The message id, or nil when +Riffer.config.message_id_strategy+ is +:none+.
14
+ attr_reader :id #: String?
15
+
11
16
  #--
12
- #: (String) -> void
13
- def initialize(content)
17
+ #: (String, ?id: String?) -> void
18
+ def initialize(content, id: nil)
14
19
  @content = content
20
+ @id = id || generate_id
15
21
  end
16
22
 
17
23
  # Converts the message to a hash.
@@ -19,7 +25,9 @@ class Riffer::Messages::Base
19
25
  #--
20
26
  #: () -> Hash[Symbol, untyped]
21
27
  def to_h
22
- {role: role, content: content}
28
+ hash = {role: role, content: content}
29
+ hash[:id] = id unless id.nil?
30
+ hash
23
31
  end
24
32
 
25
33
  # Returns the message role.
@@ -31,4 +39,14 @@ class Riffer::Messages::Base
31
39
  def role
32
40
  raise NotImplementedError, "Subclasses must implement #role"
33
41
  end
42
+
43
+ private
44
+
45
+ #: () -> String?
46
+ def generate_id
47
+ case Riffer.config.message_id_strategy
48
+ when :uuid then SecureRandom.uuid
49
+ when :uuidv7 then SecureRandom.uuid_v7
50
+ end
51
+ end
34
52
  end
@@ -65,22 +65,24 @@ module Riffer::Messages::Converter
65
65
  raise Riffer::ArgumentError, "Message hash must include a 'role' key"
66
66
  end
67
67
 
68
+ id = hash[:id]
69
+
68
70
  case role.to_sym
69
71
  when :user
70
72
  files = (hash[:files] || []).map { |f| convert_to_file_part(f) }
71
- Riffer::Messages::User.new(content, files: files)
73
+ Riffer::Messages::User.new(content, id: id, files: files)
72
74
  when :assistant
73
75
  tool_calls = (hash[:tool_calls] || []).map { |tc|
74
76
  tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
75
77
  }
76
78
  structured_output = hash[:structured_output]
77
- Riffer::Messages::Assistant.new(content, tool_calls: tool_calls, structured_output: structured_output)
79
+ Riffer::Messages::Assistant.new(content, id: id, tool_calls: tool_calls, structured_output: structured_output)
78
80
  when :system
79
- Riffer::Messages::System.new(content)
81
+ Riffer::Messages::System.new(content, id: id)
80
82
  when :tool
81
83
  tool_call_id = hash[:tool_call_id]
82
84
  name = hash[:name]
83
- Riffer::Messages::Tool.new(content, tool_call_id: tool_call_id, name: name)
85
+ Riffer::Messages::Tool.new(content, id: id, tool_call_id: tool_call_id, name: name)
84
86
  else
85
87
  raise Riffer::ArgumentError, "Unknown message role: #{role}"
86
88
  end
@@ -26,9 +26,9 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
26
26
  attr_reader :error_type #: Symbol?
27
27
 
28
28
  #--
29
- #: (String, tool_call_id: String, name: String, ?error: String?, ?error_type: Symbol?) -> void
30
- def initialize(content, tool_call_id:, name:, error: nil, error_type: nil)
31
- super(content)
29
+ #: (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
30
+ def initialize(content, tool_call_id:, name:, id: nil, error: nil, error_type: nil)
31
+ super(content, id: id)
32
32
  @tool_call_id = tool_call_id
33
33
  @name = name
34
34
  @error = error
@@ -55,6 +55,7 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
55
55
  #: () -> Hash[Symbol, untyped]
56
56
  def to_h
57
57
  hash = {role: role, content: content, tool_call_id: tool_call_id, name: name}
58
+ hash[:id] = id unless id.nil?
58
59
  if error?
59
60
  hash[:error] = error
60
61
  hash[:error_type] = error_type
@@ -17,9 +17,9 @@ class Riffer::Messages::User < Riffer::Messages::Base
17
17
  # Initializes a user message.
18
18
  #
19
19
  #--
20
- #: (String, ?files: Array[Riffer::FilePart]) -> void
21
- def initialize(content, files: [])
22
- super(content)
20
+ #: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
21
+ def initialize(content, id: nil, files: [])
22
+ super(content, id: id)
23
23
  @files = files
24
24
  end
25
25
 
@@ -39,6 +39,7 @@ class Riffer::Messages::User < Riffer::Messages::Base
39
39
  #: () -> Hash[Symbol, untyped]
40
40
  def to_h
41
41
  hash = {role: role, content: content}
42
+ hash[:id] = id unless id.nil?
42
43
  hash[:files] = files.map(&:to_h) unless files.empty?
43
44
  hash
44
45
  end