dspy 0.20.0 → 0.21.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: 44cf35be07e90187237ccdc79533d0ca76dbe2cb1040f0e841bff69afd7e71fc
4
- data.tar.gz: 443a4d5dafe1fc7c90335e2b6294a4b1e1dd77b77cb24c19c7f8237f1824b2d0
3
+ metadata.gz: 78e01258a3b9b5a1bccddad913a1d0aa45ecb145a65adb3e691986cadbea6e23
4
+ data.tar.gz: 9a778ea689150002e0766357dd4aa4af526be81979378b9996ba9067c2cdbd42
5
5
  SHA512:
6
- metadata.gz: 6e2e2e6098773c599190e0ab5e43ecd883e04359268557f28b0350219b98995c8fbe6633db2a5d1a34be1d250ae10ce05cd4aaaebefd6c13f5e855fcf587b5ef
7
- data.tar.gz: b14760bb9cf7075991fdfc274bab60e1d55a4547a8b0da2b78c37bb76c69c4963a899f8ab26f26612210957f6184999b769adc590f90bd60714f992ab1c20230
6
+ metadata.gz: 7ba1376b844e5c5e61215b961142a70f82d758db41e061bf2e7404f4ffdbae867197b0c38254a92b25892beb51922fb3c3b963ab2474494c9d490b83814bba5d
7
+ data.tar.gz: 4a543e0b954469f316f003f36c5a55b97b0794ef1cbf395180ac5197acf5d4091bfe3ea87e342473c190d96adf5761baff59532502a20778edea23a83a9e5253
data/README.md CHANGED
@@ -14,6 +14,10 @@ Traditional prompting is like writing code with string concatenation: it works u
14
14
  the programming approach pioneered by [dspy.ai](https://dspy.ai/): instead of crafting fragile prompts, you define modular
15
15
  signatures and let the framework handle the messy details.
16
16
 
17
+ DSPy.rb is an idiomatic Ruby port of Stanford's [DSPy framework](https://github.com/stanfordnlp/dspy). While implementing
18
+ the core concepts of signatures, predictors, and optimization from the original Python library, DSPy.rb embraces Ruby
19
+ conventions and adds Ruby-specific innovations like CodeAct agents and enhanced production instrumentation.
20
+
17
21
  The result? LLM applications that actually scale and don't break when you sneeze.
18
22
 
19
23
  ## Your First DSPy Program
@@ -210,7 +214,7 @@ and ecosystem integration.
210
214
 
211
215
  ### Ecosystem Expansion
212
216
  - 🚧 **Model Context Protocol (MCP)** - Integration with MCP ecosystem
213
- - 🚧 **Additional Provider Support** - Google Gemini, Azure OpenAI, local models beyond Ollama
217
+ - 🚧 **Additional Provider Support** - Azure OpenAI, local models beyond Ollama
214
218
  - 🚧 **Tool Ecosystem** - Expanded tool integrations for ReAct agents
215
219
 
216
220
  ### Community & Adoption
data/lib/dspy/evaluate.rb CHANGED
@@ -49,7 +49,7 @@ module DSPy
49
49
  def to_h
50
50
  {
51
51
  example: @example,
52
- prediction: @prediction,
52
+ prediction: @prediction.respond_to?(:to_h) ? @prediction.to_h : @prediction,
53
53
  trace: @trace,
54
54
  metrics: @metrics,
55
55
  passed: @passed
@@ -114,24 +114,55 @@ module DSPy
114
114
 
115
115
  example = {}
116
116
  schema[:properties].each do |field_name, field_schema|
117
- example[field_name.to_s] = case field_schema[:type]
118
- when "string"
119
- field_schema[:description] || "example string"
120
- when "integer"
121
- 42
122
- when "number"
123
- 3.14
124
- when "boolean"
125
- true
126
- when "array"
117
+ example[field_name.to_s] = generate_example_value(field_schema)
118
+ end
119
+ example
120
+ end
121
+
122
+ sig { params(field_schema: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
123
+ def generate_example_value(field_schema)
124
+ case field_schema[:type]
125
+ when "string"
126
+ field_schema[:description] || "example string"
127
+ when "integer"
128
+ 42
129
+ when "number"
130
+ 3.14
131
+ when "boolean"
132
+ true
133
+ when "array"
134
+ if field_schema[:items]
135
+ [generate_example_value(field_schema[:items])]
136
+ else
127
137
  ["example item"]
128
- when "object"
138
+ end
139
+ when "object"
140
+ if field_schema[:properties]
141
+ # Generate proper nested object example
142
+ nested_example = {}
143
+ field_schema[:properties].each do |prop_name, prop_schema|
144
+ nested_example[prop_name.to_s] = generate_example_value(prop_schema)
145
+ end
146
+ nested_example
147
+ else
129
148
  { "nested" => "object" }
149
+ end
150
+ when Array
151
+ # Handle union types like ["object", "null"]
152
+ if field_schema[:type].include?("object") && field_schema[:properties]
153
+ nested_example = {}
154
+ field_schema[:properties].each do |prop_name, prop_schema|
155
+ nested_example[prop_name.to_s] = generate_example_value(prop_schema)
156
+ end
157
+ nested_example
158
+ elsif field_schema[:type].include?("string")
159
+ "example string"
130
160
  else
131
161
  "example value"
132
162
  end
163
+ else
164
+ "example value"
133
165
  end
134
- example
135
166
  end
136
167
 
137
168
  sig { params(content: String).returns(T::Boolean) }
@@ -27,6 +27,7 @@ module DSPy
27
27
  ].freeze
28
28
 
29
29
  # Gemini vision-capable models (all Gemini models support vision)
30
+ # Based on official Google AI API documentation (March 2025)
30
31
  GEMINI_VISION_MODELS = [
31
32
  # Gemini 2.5 series (2025)
32
33
  'gemini-2.5-pro',
@@ -34,17 +35,11 @@ module DSPy
34
35
  'gemini-2.5-flash-lite',
35
36
  # Gemini 2.0 series (2024-2025)
36
37
  'gemini-2.0-flash',
37
- 'gemini-2.0-flash-experimental',
38
- 'gemini-2.0-flash-lite',
39
- 'gemini-2.0-pro-experimental',
38
+ 'gemini-2.0-flash-lite',
40
39
  # Gemini 1.5 series
41
40
  'gemini-1.5-pro',
42
41
  'gemini-1.5-flash',
43
- 'gemini-1.5-pro-latest',
44
- 'gemini-1.5-flash-latest',
45
- # Legacy models
46
- 'gemini-pro-vision',
47
- 'gemini-1.0-pro-vision'
42
+ 'gemini-1.5-flash-8b'
48
43
  ].freeze
49
44
 
50
45
  def self.supports_vision?(provider, model)
@@ -80,7 +80,8 @@ module DSPy
80
80
  def extract_type_from_prop(prop)
81
81
  case prop
82
82
  when Hash
83
- prop[:type]
83
+ # Prefer type_object for nilable types, fallback to type
84
+ prop[:type_object] || prop[:type]
84
85
  when Array
85
86
  # Handle [Type, description] format
86
87
  prop.first
@@ -94,7 +95,18 @@ module DSPy
94
95
  def extract_options_from_prop(prop)
95
96
  case prop
96
97
  when Hash
97
- prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction)
98
+ # Preserve important flags like fully_optional for nilable types
99
+ extracted = prop.except(:type, :type_object, :accessor_key, :sensitivity, :redaction, :setter_proc, :value_validate_proc, :serialized_form, :need_nil_read_check, :immutable, :pii, :extra)
100
+
101
+ # Handle default values properly
102
+ if prop[:default]
103
+ extracted[:default] = prop[:default]
104
+ elsif prop[:fully_optional]
105
+ # For fully optional fields (nilable), set default to nil
106
+ extracted[:default] = nil
107
+ end
108
+
109
+ extracted
98
110
  else
99
111
  {}
100
112
  end
data/lib/dspy/predict.rb CHANGED
@@ -195,7 +195,11 @@ module DSPy
195
195
  begin
196
196
  combined_struct = create_combined_struct_class
197
197
  all_attributes = input_values.merge(output_attributes)
198
- combined_struct.new(**all_attributes)
198
+
199
+ # Preprocess nilable attributes before struct instantiation
200
+ processed_attributes = preprocess_nilable_attributes(all_attributes, combined_struct)
201
+
202
+ combined_struct.new(**processed_attributes)
199
203
  rescue ArgumentError => e
200
204
  raise PredictionInvalidError.new({ output: e.message })
201
205
  rescue TypeError => e
@@ -231,5 +235,36 @@ module DSPy
231
235
 
232
236
  output_attributes
233
237
  end
238
+
239
+ # Preprocesses attributes to handle nilable fields properly before struct instantiation
240
+ sig { params(attributes: T::Hash[Symbol, T.untyped], struct_class: T.class_of(T::Struct)).returns(T::Hash[Symbol, T.untyped]) }
241
+ def preprocess_nilable_attributes(attributes, struct_class)
242
+ processed = attributes.dup
243
+ struct_props = struct_class.props
244
+
245
+ # Process each attribute based on its type in the struct
246
+ processed.each do |key, value|
247
+ prop_info = struct_props[key]
248
+ next unless prop_info
249
+
250
+ prop_type = prop_info[:type_object] || prop_info[:type]
251
+ next unless prop_type
252
+
253
+ # For nilable fields with nil values, ensure proper handling
254
+ if value.nil? && is_nilable_type?(prop_type)
255
+ # For nilable fields, nil is valid - keep it as is
256
+ next
257
+ elsif value.nil? && prop_info[:fully_optional]
258
+ # For fully optional fields, nil is valid - keep it as is
259
+ next
260
+ elsif value.nil? && prop_info[:default]
261
+ # Use default value if available
262
+ default_value = prop_info[:default]
263
+ processed[key] = default_value.is_a?(Proc) ? default_value.call : default_value
264
+ end
265
+ end
266
+
267
+ processed
268
+ end
234
269
  end
235
270
  end
@@ -123,7 +123,8 @@ module DSPy
123
123
  end
124
124
  elsif is_enum_type?(prop_type) && value.is_a?(String)
125
125
  # Convert string to enum
126
- converted[key] = prop_type.raw_type.deserialize(value)
126
+ enum_class = extract_enum_class(prop_type)
127
+ converted[key] = enum_class.deserialize(value)
127
128
  elsif value.is_a?(Hash) && needs_struct_conversion?(prop_type)
128
129
  # Regular struct field that needs conversion
129
130
  converted[key] = convert_to_struct(value, prop_type)
@@ -188,18 +189,61 @@ module DSPy
188
189
  sig { params(type: T.untyped).returns(T::Boolean) }
189
190
  def is_enum_type?(type)
190
191
  return false if type.nil?
191
- return false unless type.is_a?(T::Types::Simple)
192
192
 
193
- begin
194
- raw_type = type.raw_type
195
- return false unless raw_type.is_a?(Class)
196
- result = raw_type < T::Enum
197
- return result == true # Force conversion to boolean
198
- rescue StandardError
193
+ case type
194
+ when T::Types::Simple
195
+ # Handle regular enum types
196
+ begin
197
+ raw_type = type.raw_type
198
+ return false unless raw_type.is_a?(Class)
199
+ result = raw_type < T::Enum
200
+ return result == true # Force conversion to boolean
201
+ rescue StandardError
202
+ return false
203
+ end
204
+ when T::Private::Types::SimplePairUnion, T::Types::Union
205
+ # Handle T.nilable enum types
206
+ # Find the non-nil type and check if it's an enum
207
+ non_nil_types = if type.respond_to?(:types)
208
+ type.types.reject { |t| t.respond_to?(:raw_type) && t.raw_type == NilClass }
209
+ else
210
+ []
211
+ end
212
+
213
+ # For nilable types, we expect exactly one non-nil type
214
+ return false unless non_nil_types.size == 1
215
+
216
+ non_nil_type = non_nil_types.first
217
+ return is_enum_type?(non_nil_type) # Recursively check
218
+ else
199
219
  return false
200
220
  end
201
221
  end
202
222
 
223
+ sig { params(type: T.untyped).returns(T.untyped) }
224
+ def extract_enum_class(type)
225
+ case type
226
+ when T::Types::Simple
227
+ # Regular enum type
228
+ type.raw_type
229
+ when T::Private::Types::SimplePairUnion, T::Types::Union
230
+ # Nilable enum type - find the non-nil type
231
+ non_nil_types = if type.respond_to?(:types)
232
+ type.types.reject { |t| t.respond_to?(:raw_type) && t.raw_type == NilClass }
233
+ else
234
+ []
235
+ end
236
+
237
+ if non_nil_types.size == 1
238
+ extract_enum_class(non_nil_types.first)
239
+ else
240
+ raise ArgumentError, "Unable to extract enum class from complex union type: #{type.inspect}"
241
+ end
242
+ else
243
+ raise ArgumentError, "Not an enum type: #{type.inspect}"
244
+ end
245
+ end
246
+
203
247
  sig { params(union_type: T::Types::Union, discriminator_type: T.untyped).returns(T::Hash[String, T.untyped]) }
204
248
  def build_type_mapping_from_union(union_type, discriminator_type)
205
249
  mapping = {}
@@ -303,7 +347,12 @@ module DSPy
303
347
  def needs_struct_conversion?(type)
304
348
  case type
305
349
  when T::Types::Simple
306
- type.raw_type < T::Struct
350
+ # Use !! to convert nil result of < comparison to false
351
+ begin
352
+ !!(type.raw_type < T::Struct)
353
+ rescue
354
+ false
355
+ end
307
356
  when T::Types::Union
308
357
  # Check if any type in the union is a struct
309
358
  type.types.any? { |t| needs_struct_conversion?(t) }
@@ -352,7 +401,7 @@ module DSPy
352
401
  end
353
402
  begin
354
403
  struct_class.new(**converted_hash)
355
- rescue => e
404
+ rescue
356
405
  # Return original value if conversion fails
357
406
  value
358
407
  end
@@ -188,6 +188,11 @@ module DSPy
188
188
  return { type: "boolean" }
189
189
  end
190
190
 
191
+ # Handle type aliases by resolving to their underlying type
192
+ if type.is_a?(T::Private::Types::TypeAlias)
193
+ return type_to_json_schema(type.aliased_type)
194
+ end
195
+
191
196
  # Handle raw class types first
192
197
  if type.is_a?(Class)
193
198
  if type < T::Enum
@@ -257,17 +262,103 @@ module DSPy
257
262
  # Add a more explicit description of the expected structure
258
263
  description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
259
264
  }
265
+ elsif type.is_a?(T::Types::FixedHash)
266
+ # Handle fixed hashes (from type aliases like { "key" => Type })
267
+ properties = {}
268
+ required = []
269
+
270
+ type.types.each do |key, value_type|
271
+ properties[key] = type_to_json_schema(value_type)
272
+ required << key
273
+ end
274
+
275
+ {
276
+ type: "object",
277
+ properties: properties,
278
+ required: required,
279
+ additionalProperties: false
280
+ }
281
+ elsif type.class.name == "T::Private::Types::SimplePairUnion"
282
+ # Handle T.nilable types (T::Private::Types::SimplePairUnion)
283
+ # This is the actual implementation of T.nilable(SomeType)
284
+ has_nil = type.respond_to?(:types) && type.types.any? do |t|
285
+ (t.respond_to?(:raw_type) && t.raw_type == NilClass) ||
286
+ (t.respond_to?(:name) && t.name == "NilClass")
287
+ end
288
+
289
+ if has_nil
290
+ # Find the non-nil type
291
+ non_nil_type = type.types.find do |t|
292
+ !(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
293
+ !(t.respond_to?(:name) && t.name == "NilClass")
294
+ end
295
+
296
+ if non_nil_type
297
+ base_schema = type_to_json_schema(non_nil_type)
298
+ if base_schema[:type].is_a?(String)
299
+ # Convert single type to array with null
300
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
301
+ else
302
+ # For complex schemas, use anyOf to allow null
303
+ { anyOf: [base_schema, { type: "null" }] }
304
+ end
305
+ else
306
+ { type: "string" } # Fallback
307
+ end
308
+ else
309
+ # Not nilable SimplePairUnion - this is a regular T.any() union
310
+ # Generate oneOf schema for all types
311
+ if type.respond_to?(:types) && type.types.length > 1
312
+ {
313
+ oneOf: type.types.map { |t| type_to_json_schema(t) },
314
+ description: "Union of multiple types"
315
+ }
316
+ else
317
+ # Single type or fallback
318
+ first_type = type.respond_to?(:types) ? type.types.first : type
319
+ type_to_json_schema(first_type)
320
+ end
321
+ end
260
322
  elsif type.is_a?(T::Types::Union)
261
- # For optional types (T.nilable), just use the non-nil type
323
+ # Check if this is a nilable type (contains NilClass)
324
+ is_nilable = type.types.any? { |t| t == T::Utils.coerce(NilClass) }
262
325
  non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
263
- if non_nil_types.size == 1
326
+
327
+ # Special case: check if we have TrueClass + FalseClass (T.nilable(T::Boolean))
328
+ if non_nil_types.size == 2 && is_nilable
329
+ true_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == TrueClass }
330
+ false_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == FalseClass }
331
+
332
+ if true_class_type && false_class_type
333
+ # This is T.nilable(T::Boolean) - treat as nilable boolean
334
+ return { type: ["boolean", "null"] }
335
+ end
336
+ end
337
+
338
+ if non_nil_types.size == 1 && is_nilable
339
+ # This is T.nilable(SomeType) - generate proper schema with null allowed
340
+ base_schema = type_to_json_schema(non_nil_types.first)
341
+ if base_schema[:type].is_a?(String)
342
+ # Convert single type to array with null
343
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
344
+ else
345
+ # For complex schemas, use anyOf to allow null
346
+ { anyOf: [base_schema, { type: "null" }] }
347
+ end
348
+ elsif non_nil_types.size == 1
349
+ # Non-nilable single type union (shouldn't happen in practice)
264
350
  type_to_json_schema(non_nil_types.first)
265
351
  elsif non_nil_types.size > 1
266
352
  # Handle complex unions with oneOf for better JSON schema compliance
267
- {
353
+ base_schema = {
268
354
  oneOf: non_nil_types.map { |t| type_to_json_schema(t) },
269
355
  description: "Union of multiple types"
270
356
  }
357
+ if is_nilable
358
+ # Add null as an option for complex nilable unions
359
+ base_schema[:oneOf] << { type: "null" }
360
+ end
361
+ base_schema
271
362
  else
272
363
  { type: "string" } # Fallback for complex unions
273
364
  end
@@ -203,6 +203,9 @@ module DSPy
203
203
  sig { returns(T::Hash[Symbol, T.untyped]) }
204
204
  attr_reader :proposal_statistics
205
205
 
206
+ sig { returns(T.nilable(DSPy::Evaluate::BatchEvaluationResult)) }
207
+ attr_reader :best_evaluation_result
208
+
206
209
  sig do
207
210
  params(
208
211
  optimized_program: T.untyped,
@@ -214,10 +217,11 @@ module DSPy
214
217
  proposal_statistics: T::Hash[Symbol, T.untyped],
215
218
  best_score_name: T.nilable(String),
216
219
  best_score_value: T.nilable(Float),
217
- metadata: T::Hash[Symbol, T.untyped]
220
+ metadata: T::Hash[Symbol, T.untyped],
221
+ best_evaluation_result: T.nilable(DSPy::Evaluate::BatchEvaluationResult)
218
222
  ).void
219
223
  end
220
- def initialize(optimized_program:, scores:, history:, evaluated_candidates:, optimization_trace:, bootstrap_statistics:, proposal_statistics:, best_score_name: nil, best_score_value: nil, metadata: {})
224
+ def initialize(optimized_program:, scores:, history:, evaluated_candidates:, optimization_trace:, bootstrap_statistics:, proposal_statistics:, best_score_name: nil, best_score_value: nil, metadata: {}, best_evaluation_result: nil)
221
225
  super(
222
226
  optimized_program: optimized_program,
223
227
  scores: scores,
@@ -230,6 +234,7 @@ module DSPy
230
234
  @optimization_trace = optimization_trace.freeze
231
235
  @bootstrap_statistics = bootstrap_statistics.freeze
232
236
  @proposal_statistics = proposal_statistics.freeze
237
+ @best_evaluation_result = best_evaluation_result&.freeze
233
238
  end
234
239
 
235
240
  sig { returns(T::Hash[Symbol, T.untyped]) }
@@ -238,7 +243,8 @@ module DSPy
238
243
  evaluated_candidates: @evaluated_candidates.map(&:to_h),
239
244
  optimization_trace: @optimization_trace,
240
245
  bootstrap_statistics: @bootstrap_statistics,
241
- proposal_statistics: @proposal_statistics
246
+ proposal_statistics: @proposal_statistics,
247
+ best_evaluation_result: @best_evaluation_result&.to_h
242
248
  })
243
249
  end
244
250
  end
@@ -399,6 +405,7 @@ module DSPy
399
405
  best_score = 0.0
400
406
  best_candidate = nil
401
407
  best_program = nil
408
+ best_evaluation_result = nil
402
409
 
403
410
  @mipro_config.num_trials.times do |trial_idx|
404
411
  trials_completed = trial_idx + 1
@@ -415,7 +422,7 @@ module DSPy
415
422
 
416
423
  begin
417
424
  # Evaluate candidate
418
- score, modified_program = evaluate_candidate(program, candidate, evaluation_set)
425
+ score, modified_program, evaluation_result = evaluate_candidate(program, candidate, evaluation_set)
419
426
 
420
427
  # Update optimization state
421
428
  update_optimization_state(optimization_state, candidate, score)
@@ -426,6 +433,7 @@ module DSPy
426
433
  best_score = score
427
434
  best_candidate = candidate
428
435
  best_program = modified_program
436
+ best_evaluation_result = evaluation_result
429
437
  end
430
438
 
431
439
  emit_event('trial_complete', {
@@ -456,6 +464,7 @@ module DSPy
456
464
  best_score: best_score,
457
465
  best_candidate: best_candidate,
458
466
  best_program: best_program,
467
+ best_evaluation_result: best_evaluation_result,
459
468
  trials_completed: trials_completed,
460
469
  optimization_state: optimization_state,
461
470
  evaluated_candidates: @evaluated_candidates
@@ -626,7 +635,7 @@ module DSPy
626
635
  program: T.untyped,
627
636
  candidate: CandidateConfig,
628
637
  evaluation_set: T::Array[DSPy::Example]
629
- ).returns([Float, T.untyped])
638
+ ).returns([Float, T.untyped, DSPy::Evaluate::BatchEvaluationResult])
630
639
  end
631
640
  def evaluate_candidate(program, candidate, evaluation_set)
632
641
  # Apply candidate configuration to program
@@ -638,7 +647,7 @@ module DSPy
638
647
  # Store evaluation details
639
648
  @evaluated_candidates << candidate
640
649
 
641
- [evaluation_result.pass_rate, modified_program]
650
+ [evaluation_result.pass_rate, modified_program, evaluation_result]
642
651
  end
643
652
 
644
653
  # Apply candidate configuration to program
@@ -724,6 +733,7 @@ module DSPy
724
733
  best_candidate = optimization_result[:best_candidate]
725
734
  best_program = optimization_result[:best_program]
726
735
  best_score = optimization_result[:best_score]
736
+ best_evaluation_result = optimization_result[:best_evaluation_result]
727
737
 
728
738
  scores = { pass_rate: best_score }
729
739
 
@@ -753,7 +763,8 @@ module DSPy
753
763
  evaluated_candidates: @evaluated_candidates,
754
764
  optimization_trace: serialize_optimization_trace(optimization_result[:optimization_state]),
755
765
  bootstrap_statistics: bootstrap_result.statistics,
756
- proposal_statistics: proposal_result.analysis
766
+ proposal_statistics: proposal_result.analysis,
767
+ best_evaluation_result: best_evaluation_result
757
768
  )
758
769
  end
759
770
 
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.20.0"
4
+ VERSION = "0.21.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-26 00:00:00.000000000 Z
10
+ date: 2025-09-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable