dspy 0.20.0 → 0.20.1

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: 3324f833b373e826df1dcdf3f2c3f46011e192e9a30fdf38d7db1b9cc51a7950
4
+ data.tar.gz: f6159081bc6429d0c57e6275111d2672bf984f8500be7756afbf8bb17599dd19
5
5
  SHA512:
6
- metadata.gz: 6e2e2e6098773c599190e0ab5e43ecd883e04359268557f28b0350219b98995c8fbe6633db2a5d1a34be1d250ae10ce05cd4aaaebefd6c13f5e855fcf587b5ef
7
- data.tar.gz: b14760bb9cf7075991fdfc274bab60e1d55a4547a8b0da2b78c37bb76c69c4963a899f8ab26f26612210957f6184999b769adc590f90bd60714f992ab1c20230
6
+ metadata.gz: cd81596af2ea0d734550b0827e18b71d4abdd1e3a4c92efb014af665d17bb2a37fb36b747757eb3ad4377d89a435b81bbaa4477ce0fd22b8ed88319ebc8b1f5b
7
+ data.tar.gz: d65e68af35f7ced5e97524dd85f1c590900f33b720038f25bbc22995031d10cc5115bdb1b160b153e5fbaa5bf2cb3809dde0520f6bd99a7ac348c0231bf88afa
data/README.md CHANGED
@@ -210,7 +210,7 @@ and ecosystem integration.
210
210
 
211
211
  ### Ecosystem Expansion
212
212
  - 🚧 **Model Context Protocol (MCP)** - Integration with MCP ecosystem
213
- - 🚧 **Additional Provider Support** - Google Gemini, Azure OpenAI, local models beyond Ollama
213
+ - 🚧 **Additional Provider Support** - Azure OpenAI, local models beyond Ollama
214
214
  - 🚧 **Tool Ecosystem** - Expanded tool integrations for ReAct agents
215
215
 
216
216
  ### 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
@@ -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
@@ -257,17 +257,87 @@ module DSPy
257
257
  # Add a more explicit description of the expected structure
258
258
  description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
259
259
  }
260
+ elsif type.class.name == "T::Private::Types::SimplePairUnion"
261
+ # Handle T.nilable types (T::Private::Types::SimplePairUnion)
262
+ # This is the actual implementation of T.nilable(SomeType)
263
+ has_nil = type.respond_to?(:types) && type.types.any? do |t|
264
+ (t.respond_to?(:raw_type) && t.raw_type == NilClass) ||
265
+ (t.respond_to?(:name) && t.name == "NilClass")
266
+ end
267
+
268
+ if has_nil
269
+ # Find the non-nil type
270
+ non_nil_type = type.types.find do |t|
271
+ !(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
272
+ !(t.respond_to?(:name) && t.name == "NilClass")
273
+ end
274
+
275
+ if non_nil_type
276
+ base_schema = type_to_json_schema(non_nil_type)
277
+ if base_schema[:type].is_a?(String)
278
+ # Convert single type to array with null
279
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
280
+ else
281
+ # For complex schemas, use anyOf to allow null
282
+ { anyOf: [base_schema, { type: "null" }] }
283
+ end
284
+ else
285
+ { type: "string" } # Fallback
286
+ end
287
+ else
288
+ # Not nilable SimplePairUnion - this is a regular T.any() union
289
+ # Generate oneOf schema for all types
290
+ if type.respond_to?(:types) && type.types.length > 1
291
+ {
292
+ oneOf: type.types.map { |t| type_to_json_schema(t) },
293
+ description: "Union of multiple types"
294
+ }
295
+ else
296
+ # Single type or fallback
297
+ first_type = type.respond_to?(:types) ? type.types.first : type
298
+ type_to_json_schema(first_type)
299
+ end
300
+ end
260
301
  elsif type.is_a?(T::Types::Union)
261
- # For optional types (T.nilable), just use the non-nil type
302
+ # Check if this is a nilable type (contains NilClass)
303
+ is_nilable = type.types.any? { |t| t == T::Utils.coerce(NilClass) }
262
304
  non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
263
- if non_nil_types.size == 1
305
+
306
+ # Special case: check if we have TrueClass + FalseClass (T.nilable(T::Boolean))
307
+ if non_nil_types.size == 2 && is_nilable
308
+ true_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == TrueClass }
309
+ false_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == FalseClass }
310
+
311
+ if true_class_type && false_class_type
312
+ # This is T.nilable(T::Boolean) - treat as nilable boolean
313
+ return { type: ["boolean", "null"] }
314
+ end
315
+ end
316
+
317
+ if non_nil_types.size == 1 && is_nilable
318
+ # This is T.nilable(SomeType) - generate proper schema with null allowed
319
+ base_schema = type_to_json_schema(non_nil_types.first)
320
+ if base_schema[:type].is_a?(String)
321
+ # Convert single type to array with null
322
+ { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
323
+ else
324
+ # For complex schemas, use anyOf to allow null
325
+ { anyOf: [base_schema, { type: "null" }] }
326
+ end
327
+ elsif non_nil_types.size == 1
328
+ # Non-nilable single type union (shouldn't happen in practice)
264
329
  type_to_json_schema(non_nil_types.first)
265
330
  elsif non_nil_types.size > 1
266
331
  # Handle complex unions with oneOf for better JSON schema compliance
267
- {
332
+ base_schema = {
268
333
  oneOf: non_nil_types.map { |t| type_to_json_schema(t) },
269
334
  description: "Union of multiple types"
270
335
  }
336
+ if is_nilable
337
+ # Add null as an option for complex nilable unions
338
+ base_schema[:oneOf] << { type: "null" }
339
+ end
340
+ base_schema
271
341
  else
272
342
  { type: "string" } # Fallback for complex unions
273
343
  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.20.1"
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.20.1
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-08-27 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable