dspy 0.15.2 → 0.15.3

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.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ class LM
7
+ # Type-safe representation of chat messages
8
+ class Message < T::Struct
9
+ extend T::Sig
10
+
11
+ # Role enum for type safety
12
+ class Role < T::Enum
13
+ enums do
14
+ System = new('system')
15
+ User = new('user')
16
+ Assistant = new('assistant')
17
+ end
18
+ end
19
+
20
+ const :role, Role
21
+ const :content, String
22
+ const :name, T.nilable(String), default: nil
23
+
24
+ sig { returns(T::Hash[Symbol, T.untyped]) }
25
+ def to_h
26
+ base = {
27
+ role: role.serialize,
28
+ content: content
29
+ }
30
+ base[:name] = name if name
31
+ base
32
+ end
33
+
34
+ sig { returns(String) }
35
+ def to_s
36
+ name ? "#{role.serialize}(#{name}): #{content}" : "#{role.serialize}: #{content}"
37
+ end
38
+ end
39
+
40
+ # Factory for creating Message objects from various formats
41
+ module MessageFactory
42
+ extend T::Sig
43
+
44
+ sig { params(message_data: T.untyped).returns(T.nilable(Message)) }
45
+ def self.create(message_data)
46
+ return nil if message_data.nil?
47
+
48
+ # Already a Message? Return as-is
49
+ return message_data if message_data.is_a?(Message)
50
+
51
+ # Convert to hash if needed
52
+ if message_data.respond_to?(:to_h)
53
+ message_data = message_data.to_h
54
+ end
55
+
56
+ return nil unless message_data.is_a?(Hash)
57
+
58
+ # Normalize keys to symbols
59
+ normalized = message_data.transform_keys(&:to_sym)
60
+
61
+ create_from_hash(normalized)
62
+ end
63
+
64
+ sig { params(messages: T::Array[T.untyped]).returns(T::Array[Message]) }
65
+ def self.create_many(messages)
66
+ messages.compact.map { |m| create(m) }.compact
67
+ end
68
+
69
+ private
70
+
71
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Message)) }
72
+ def self.create_from_hash(data)
73
+ role_str = data[:role]&.to_s
74
+ content = data[:content]&.to_s
75
+
76
+ return nil if role_str.nil? || content.nil?
77
+
78
+ # Convert string role to enum
79
+ role = case role_str
80
+ when 'system' then Message::Role::System
81
+ when 'user' then Message::Role::User
82
+ when 'assistant' then Message::Role::Assistant
83
+ else
84
+ DSPy.logger.debug("Unknown message role: #{role_str}")
85
+ return nil
86
+ end
87
+
88
+ Message.new(
89
+ role: role,
90
+ content: content,
91
+ name: data[:name]&.to_s
92
+ )
93
+ rescue => e
94
+ DSPy.logger.debug("Failed to create Message: #{e.message}")
95
+ nil
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,28 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'message'
4
+
3
5
  module DSPy
4
6
  class LM
5
7
  class MessageBuilder
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Array[Message]) }
6
11
  attr_reader :messages
7
12
 
8
13
  def initialize
9
14
  @messages = []
10
15
  end
11
16
 
17
+ sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
12
18
  def system(content)
13
- @messages << { role: 'system', content: content.to_s }
19
+ @messages << Message.new(
20
+ role: Message::Role::System,
21
+ content: content.to_s
22
+ )
14
23
  self
15
24
  end
16
25
 
26
+ sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
17
27
  def user(content)
18
- @messages << { role: 'user', content: content.to_s }
28
+ @messages << Message.new(
29
+ role: Message::Role::User,
30
+ content: content.to_s
31
+ )
19
32
  self
20
33
  end
21
34
 
35
+ sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
22
36
  def assistant(content)
23
- @messages << { role: 'assistant', content: content.to_s }
37
+ @messages << Message.new(
38
+ role: Message::Role::Assistant,
39
+ content: content.to_s
40
+ )
24
41
  self
25
42
  end
43
+
44
+ # For backward compatibility, allow conversion to hash array
45
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
46
+ def to_h
47
+ @messages.map(&:to_h)
48
+ end
26
49
  end
27
50
  end
28
51
  end
@@ -1,29 +1,158 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sorbet-runtime'
3
4
  require_relative 'usage'
4
5
 
5
6
  module DSPy
6
7
  class LM
7
- # Normalized response format for all LM providers
8
- class Response
9
- attr_reader :content, :usage, :metadata
10
-
11
- def initialize(content:, usage: nil, metadata: {})
12
- @content = content
13
- @usage = usage
14
- @metadata = metadata
8
+ # Base metadata struct for common fields across providers
9
+ class ResponseMetadata < T::Struct
10
+ extend T::Sig
11
+
12
+ const :provider, String
13
+ const :model, String
14
+ const :response_id, T.nilable(String), default: nil
15
+ const :created, T.nilable(Integer), default: nil
16
+ const :structured_output, T.nilable(T::Boolean), default: nil
17
+
18
+ sig { returns(T::Hash[Symbol, T.untyped]) }
19
+ def to_h
20
+ hash = {
21
+ provider: provider,
22
+ model: model
23
+ }
24
+ hash[:response_id] = response_id if response_id
25
+ hash[:created] = created if created
26
+ hash[:structured_output] = structured_output unless structured_output.nil?
27
+ hash
15
28
  end
16
-
29
+ end
30
+
31
+ # OpenAI-specific metadata with additional fields
32
+ class OpenAIResponseMetadata < T::Struct
33
+ extend T::Sig
34
+
35
+ const :provider, String
36
+ const :model, String
37
+ const :response_id, T.nilable(String), default: nil
38
+ const :created, T.nilable(Integer), default: nil
39
+ const :structured_output, T.nilable(T::Boolean), default: nil
40
+ const :system_fingerprint, T.nilable(String), default: nil
41
+ const :finish_reason, T.nilable(String), default: nil
42
+
43
+ sig { returns(T::Hash[Symbol, T.untyped]) }
44
+ def to_h
45
+ hash = {
46
+ provider: provider,
47
+ model: model
48
+ }
49
+ hash[:response_id] = response_id if response_id
50
+ hash[:created] = created if created
51
+ hash[:structured_output] = structured_output unless structured_output.nil?
52
+ hash[:system_fingerprint] = system_fingerprint if system_fingerprint
53
+ hash[:finish_reason] = finish_reason if finish_reason
54
+ hash
55
+ end
56
+ end
57
+
58
+ # Anthropic-specific metadata with additional fields
59
+ class AnthropicResponseMetadata < T::Struct
60
+ extend T::Sig
61
+
62
+ const :provider, String
63
+ const :model, String
64
+ const :response_id, T.nilable(String), default: nil
65
+ const :created, T.nilable(Integer), default: nil
66
+ const :structured_output, T.nilable(T::Boolean), default: nil
67
+ const :stop_reason, T.nilable(String), default: nil
68
+ const :stop_sequence, T.nilable(String), default: nil
69
+ const :tool_calls, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]), default: nil
70
+
71
+ sig { returns(T::Hash[Symbol, T.untyped]) }
72
+ def to_h
73
+ hash = {
74
+ provider: provider,
75
+ model: model
76
+ }
77
+ hash[:response_id] = response_id if response_id
78
+ hash[:created] = created if created
79
+ hash[:structured_output] = structured_output unless structured_output.nil?
80
+ hash[:stop_reason] = stop_reason if stop_reason
81
+ hash[:stop_sequence] = stop_sequence if stop_sequence
82
+ hash[:tool_calls] = tool_calls if tool_calls
83
+ hash
84
+ end
85
+ end
86
+
87
+ # Normalized response format for all LM providers
88
+ class Response < T::Struct
89
+ extend T::Sig
90
+
91
+ const :content, String
92
+ const :usage, T.nilable(T.any(Usage, OpenAIUsage)), default: nil
93
+ const :metadata, T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, T::Hash[Symbol, T.untyped])
94
+
95
+ sig { returns(String) }
17
96
  def to_s
18
97
  content
19
98
  end
20
-
99
+
100
+ sig { returns(T::Hash[Symbol, T.untyped]) }
21
101
  def to_h
22
- {
23
- content: content,
24
- usage: usage,
25
- metadata: metadata
102
+ hash = {
103
+ content: content
104
+ }
105
+ hash[:usage] = usage.to_h if usage
106
+ hash[:metadata] = metadata.is_a?(Hash) ? metadata : metadata.to_h
107
+ hash
108
+ end
109
+ end
110
+
111
+ # Factory for creating response metadata objects
112
+ module ResponseMetadataFactory
113
+ extend T::Sig
114
+
115
+ sig { params(provider: String, metadata: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata)) }
116
+ def self.create(provider, metadata)
117
+ # Handle nil metadata
118
+ metadata ||= {}
119
+
120
+ # Normalize provider name
121
+ provider_name = provider.to_s.downcase
122
+
123
+ # Extract common fields
124
+ common_fields = {
125
+ provider: provider,
126
+ model: metadata[:model] || 'unknown',
127
+ response_id: metadata[:response_id] || metadata[:id],
128
+ created: metadata[:created],
129
+ structured_output: metadata[:structured_output]
26
130
  }
131
+
132
+ case provider_name
133
+ when 'openai'
134
+ OpenAIResponseMetadata.new(
135
+ **common_fields,
136
+ system_fingerprint: metadata[:system_fingerprint],
137
+ finish_reason: metadata[:finish_reason]&.to_s
138
+ )
139
+ when 'anthropic'
140
+ AnthropicResponseMetadata.new(
141
+ **common_fields,
142
+ stop_reason: metadata[:stop_reason]&.to_s,
143
+ stop_sequence: metadata[:stop_sequence]&.to_s,
144
+ tool_calls: metadata[:tool_calls]
145
+ )
146
+ else
147
+ ResponseMetadata.new(**common_fields)
148
+ end
149
+ rescue => e
150
+ DSPy.logger.debug("Failed to create response metadata: #{e.message}")
151
+ # Fallback to basic metadata
152
+ ResponseMetadata.new(
153
+ provider: provider,
154
+ model: metadata[:model] || 'unknown'
155
+ )
27
156
  end
28
157
  end
29
158
  end
@@ -50,8 +50,8 @@ module DSPy
50
50
  # Extract JSON from tool use response
51
51
  begin
52
52
  # Check for tool calls in metadata first (this is the primary method)
53
- if response.metadata && response.metadata[:tool_calls]
54
- tool_calls = response.metadata[:tool_calls]
53
+ if response.metadata.respond_to?(:tool_calls) && response.metadata.tool_calls
54
+ tool_calls = response.metadata.tool_calls
55
55
  if tool_calls.is_a?(Array) && !tool_calls.empty?
56
56
  first_call = tool_calls.first
57
57
  if first_call[:name] == "json_output" && first_call[:input]
data/lib/dspy/lm.rb CHANGED
@@ -19,7 +19,8 @@ require_relative 'lm/adapters/ollama_adapter'
19
19
  require_relative 'lm/strategy_selector'
20
20
  require_relative 'lm/retry_handler'
21
21
 
22
- # Load message builder
22
+ # Load message builder and message types
23
+ require_relative 'lm/message'
23
24
  require_relative 'lm/message_builder'
24
25
 
25
26
  module DSPy
@@ -78,8 +79,8 @@ module DSPy
78
79
  streaming_block = block
79
80
  end
80
81
 
81
- # Validate messages format
82
- validate_messages!(messages)
82
+ # Normalize and validate messages
83
+ messages = normalize_messages(messages)
83
84
 
84
85
  # Execute with instrumentation
85
86
  execute_raw_chat(messages, &streaming_block)
@@ -106,16 +107,19 @@ module DSPy
106
107
  end
107
108
 
108
109
  def execute_chat_with_strategy(messages, signature_class, strategy, &block)
110
+ # Convert messages to hash format for strategy and adapter
111
+ hash_messages = messages_to_hash_array(messages)
112
+
109
113
  # Prepare request with strategy-specific modifications
110
114
  request_params = {}
111
- strategy.prepare_request(messages.dup, request_params)
115
+ strategy.prepare_request(hash_messages.dup, request_params)
112
116
 
113
117
  # Make the request
114
118
  response = if request_params.any?
115
119
  # Pass additional parameters if strategy added them
116
- adapter.chat(messages: messages, signature: signature_class, **request_params, &block)
120
+ adapter.chat(messages: hash_messages, signature: signature_class, **request_params, &block)
117
121
  else
118
- adapter.chat(messages: messages, signature: signature_class, &block)
122
+ adapter.chat(messages: hash_messages, signature: signature_class, &block)
119
123
  end
120
124
 
121
125
  # Let strategy handle JSON extraction if needed
@@ -171,11 +175,19 @@ module DSPy
171
175
 
172
176
  # Add system message
173
177
  system_prompt = inference_module.system_signature
174
- messages << { role: 'system', content: system_prompt } if system_prompt
178
+ if system_prompt
179
+ messages << Message.new(
180
+ role: Message::Role::System,
181
+ content: system_prompt
182
+ )
183
+ end
175
184
 
176
185
  # Add user message
177
186
  user_prompt = inference_module.user_signature(input_values)
178
- messages << { role: 'user', content: user_prompt }
187
+ messages << Message.new(
188
+ role: Message::Role::User,
189
+ content: user_prompt
190
+ )
179
191
 
180
192
  messages
181
193
  end
@@ -205,7 +217,14 @@ module DSPy
205
217
 
206
218
  # Common instrumentation method for LM requests
207
219
  def instrument_lm_request(messages, signature_class_name, &execution_block)
208
- input_text = messages.map { |m| m[:content] }.join(' ')
220
+ # Handle both Message objects and hash format
221
+ input_text = messages.map do |m|
222
+ if m.is_a?(Message)
223
+ m.content
224
+ else
225
+ m[:content]
226
+ end
227
+ end.join(' ')
209
228
  input_size = input_text.length
210
229
 
211
230
  response = nil
@@ -252,27 +271,84 @@ module DSPy
252
271
  raise ArgumentError, "messages must be an array"
253
272
  end
254
273
 
255
- valid_roles = %w[system user assistant]
256
-
257
- messages.each do |message|
258
- unless message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
259
- raise ArgumentError, "Each message must have :role and :content"
260
- end
261
-
262
- unless valid_roles.include?(message[:role])
263
- raise ArgumentError, "Invalid role: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
274
+ messages.each_with_index do |message, index|
275
+ # Accept both Message objects and hash format for backward compatibility
276
+ if message.is_a?(Message)
277
+ # Already validated by type system
278
+ next
279
+ elsif message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
280
+ # Legacy hash format - validate role
281
+ valid_roles = %w[system user assistant]
282
+ unless valid_roles.include?(message[:role])
283
+ raise ArgumentError, "Invalid role at index #{index}: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
284
+ end
285
+ else
286
+ raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
264
287
  end
265
288
  end
266
289
  end
267
290
 
268
291
  def execute_raw_chat(messages, &streaming_block)
269
292
  response = instrument_lm_request(messages, 'RawPrompt') do
293
+ # Convert messages to hash format for adapter
294
+ hash_messages = messages_to_hash_array(messages)
270
295
  # Direct adapter call, no strategies or JSON parsing
271
- adapter.chat(messages: messages, signature: nil, &streaming_block)
296
+ adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
272
297
  end
273
298
 
274
299
  # Return raw response content, not parsed JSON
275
300
  response.content
276
301
  end
302
+
303
+ # Convert messages to normalized Message objects
304
+ def normalize_messages(messages)
305
+ # Validate array format first
306
+ unless messages.is_a?(Array)
307
+ raise ArgumentError, "messages must be an array"
308
+ end
309
+
310
+ return messages if messages.all? { |m| m.is_a?(Message) }
311
+
312
+ # Convert hash messages to Message objects
313
+ normalized = []
314
+ messages.each_with_index do |msg, index|
315
+ if msg.is_a?(Message)
316
+ normalized << msg
317
+ elsif msg.is_a?(Hash)
318
+ # Validate hash has required fields
319
+ unless msg.key?(:role) && msg.key?(:content)
320
+ raise ArgumentError, "Message at index #{index} must have :role and :content"
321
+ end
322
+
323
+ # Validate role
324
+ valid_roles = %w[system user assistant]
325
+ unless valid_roles.include?(msg[:role])
326
+ raise ArgumentError, "Invalid role at index #{index}: #{msg[:role]}. Must be one of: #{valid_roles.join(', ')}"
327
+ end
328
+
329
+ # Create Message object
330
+ message = MessageFactory.create(msg)
331
+ if message.nil?
332
+ raise ArgumentError, "Failed to create Message from hash at index #{index}"
333
+ end
334
+ normalized << message
335
+ else
336
+ raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
337
+ end
338
+ end
339
+
340
+ normalized
341
+ end
342
+
343
+ # Convert Message objects to hash array for adapters
344
+ def messages_to_hash_array(messages)
345
+ messages.map do |msg|
346
+ if msg.is_a?(Message)
347
+ msg.to_h
348
+ else
349
+ msg
350
+ end
351
+ end
352
+ end
277
353
  end
278
354
  end
@@ -30,6 +30,8 @@ module DSPy
30
30
  return value if value.nil?
31
31
 
32
32
  case prop_type
33
+ when ->(type) { union_type?(type) }
34
+ coerce_union_value(value, prop_type)
33
35
  when ->(type) { array_type?(type) }
34
36
  coerce_array_value(value, prop_type)
35
37
  when ->(type) { enum_type?(type) }
@@ -100,6 +102,18 @@ module DSPy
100
102
  false
101
103
  end
102
104
 
105
+ # Checks if a type is a union type (T.any)
106
+ sig { params(type: T.untyped).returns(T::Boolean) }
107
+ def union_type?(type)
108
+ type.is_a?(T::Types::Union) && !is_nilable_type?(type)
109
+ end
110
+
111
+ # Checks if a type is nilable (contains NilClass)
112
+ sig { params(type: T.untyped).returns(T::Boolean) }
113
+ def is_nilable_type?(type)
114
+ type.is_a?(T::Types::Union) && type.types.any? { |t| t == T::Utils.coerce(NilClass) }
115
+ end
116
+
103
117
  # Coerces an array value, converting each element as needed
104
118
  sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
105
119
  def coerce_array_value(value, prop_type)
@@ -133,6 +147,40 @@ module DSPy
133
147
  DSPy.logger.debug("Failed to coerce to struct #{struct_class}: #{e.message}")
134
148
  value
135
149
  end
150
+
151
+ # Coerces a union value by using _type discriminator
152
+ sig { params(value: T.untyped, union_type: T.untyped).returns(T.untyped) }
153
+ def coerce_union_value(value, union_type)
154
+ return value unless value.is_a?(Hash)
155
+
156
+ # Check for _type discriminator field
157
+ type_name = value[:_type] || value["_type"]
158
+ return value unless type_name
159
+
160
+ # Find matching struct type in the union
161
+ union_type.types.each do |type|
162
+ next if type == T::Utils.coerce(NilClass)
163
+
164
+ if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
165
+ struct_name = type.raw_type.name.split('::').last
166
+ if struct_name == type_name
167
+ # Convert string keys to symbols and remove _type
168
+ symbolized_hash = value.transform_keys(&:to_sym)
169
+ symbolized_hash.delete(:_type)
170
+
171
+ # Create the struct instance
172
+ return type.raw_type.new(**symbolized_hash)
173
+ end
174
+ end
175
+ end
176
+
177
+ # If no matching type found, return original value
178
+ value
179
+ rescue ArgumentError => e
180
+ # If struct creation fails, return the original value
181
+ DSPy.logger.debug("Failed to coerce union type: #{e.message}")
182
+ value
183
+ end
136
184
  end
137
185
  end
138
186
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
+ require_relative '../instrumentation/event_payloads'
4
5
 
5
6
  begin
6
7
  require 'opentelemetry/api'
@@ -402,10 +403,8 @@ module DSPy
402
403
  'dspy.lm.model' => payload[:gen_ai_request_model] || payload[:model],
403
404
  'dspy.lm.status' => payload[:status],
404
405
  'dspy.lm.duration_ms' => payload[:duration_ms],
405
- 'dspy.lm.tokens_total' => payload[:tokens_total],
406
- 'dspy.lm.tokens_input' => payload[:tokens_input],
407
- 'dspy.lm.tokens_output' => payload[:tokens_output],
408
- 'dspy.lm.cost' => payload[:cost]
406
+ 'dspy.lm.adapter_class' => payload[:adapter_class],
407
+ 'dspy.lm.input_size' => payload[:input_size]
409
408
  }
410
409
  ) do |span|
411
410
  if payload[:status] == 'error'
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.15.2"
4
+ VERSION = "0.15.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.2
4
+ version: 0.15.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-28 00:00:00.000000000 Z
10
+ date: 2025-08-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-configurable
@@ -167,6 +166,8 @@ files:
167
166
  - lib/dspy/few_shot_example.rb
168
167
  - lib/dspy/field.rb
169
168
  - lib/dspy/instrumentation.rb
169
+ - lib/dspy/instrumentation/event_payload_factory.rb
170
+ - lib/dspy/instrumentation/event_payloads.rb
170
171
  - lib/dspy/instrumentation/token_tracker.rb
171
172
  - lib/dspy/lm.rb
172
173
  - lib/dspy/lm/adapter.rb
@@ -177,6 +178,7 @@ files:
177
178
  - lib/dspy/lm/adapters/openai_adapter.rb
178
179
  - lib/dspy/lm/cache_manager.rb
179
180
  - lib/dspy/lm/errors.rb
181
+ - lib/dspy/lm/message.rb
180
182
  - lib/dspy/lm/message_builder.rb
181
183
  - lib/dspy/lm/response.rb
182
184
  - lib/dspy/lm/retry_handler.rb
@@ -232,7 +234,6 @@ homepage: https://github.com/vicentereig/dspy.rb
232
234
  licenses:
233
235
  - MIT
234
236
  metadata: {}
235
- post_install_message:
236
237
  rdoc_options: []
237
238
  require_paths:
238
239
  - lib
@@ -247,8 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
248
  - !ruby/object:Gem::Version
248
249
  version: '0'
249
250
  requirements: []
250
- rubygems_version: 3.5.22
251
- signing_key:
251
+ rubygems_version: 3.6.5
252
252
  specification_version: 4
253
253
  summary: The Ruby framework for programming—rather than prompting—language models.
254
254
  test_files: []