dspy 0.19.1 → 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 +4 -4
- data/README.md +93 -69
- data/lib/dspy/chain_of_thought.rb +1 -0
- data/lib/dspy/code_act.rb +9 -3
- data/lib/dspy/evaluate.rb +1 -1
- data/lib/dspy/image.rb +33 -0
- data/lib/dspy/lm/adapter_factory.rb +2 -1
- data/lib/dspy/lm/adapters/gemini_adapter.rb +189 -0
- data/lib/dspy/lm/response.rb +40 -4
- data/lib/dspy/lm/usage.rb +19 -0
- data/lib/dspy/lm/vision_models.rb +20 -0
- data/lib/dspy/lm.rb +5 -4
- data/lib/dspy/mixins/struct_builder.rb +14 -2
- data/lib/dspy/module.rb +2 -2
- data/lib/dspy/predict.rb +72 -1
- data/lib/dspy/prediction.rb +59 -10
- data/lib/dspy/propose/grounded_proposer.rb +38 -3
- data/lib/dspy/re_act.rb +9 -4
- data/lib/dspy/signature.rb +73 -3
- data/lib/dspy/storage/program_storage.rb +45 -10
- data/lib/dspy/teleprompt/mipro_v2.rb +58 -17
- data/lib/dspy/teleprompt/utils.rb +30 -5
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +14 -0
- metadata +17 -2
@@ -26,12 +26,30 @@ module DSPy
|
|
26
26
|
'claude-3-5-haiku-20241022'
|
27
27
|
].freeze
|
28
28
|
|
29
|
+
# Gemini vision-capable models (all Gemini models support vision)
|
30
|
+
# Based on official Google AI API documentation (March 2025)
|
31
|
+
GEMINI_VISION_MODELS = [
|
32
|
+
# Gemini 2.5 series (2025)
|
33
|
+
'gemini-2.5-pro',
|
34
|
+
'gemini-2.5-flash',
|
35
|
+
'gemini-2.5-flash-lite',
|
36
|
+
# Gemini 2.0 series (2024-2025)
|
37
|
+
'gemini-2.0-flash',
|
38
|
+
'gemini-2.0-flash-lite',
|
39
|
+
# Gemini 1.5 series
|
40
|
+
'gemini-1.5-pro',
|
41
|
+
'gemini-1.5-flash',
|
42
|
+
'gemini-1.5-flash-8b'
|
43
|
+
].freeze
|
44
|
+
|
29
45
|
def self.supports_vision?(provider, model)
|
30
46
|
case provider.to_s.downcase
|
31
47
|
when 'openai'
|
32
48
|
OPENAI_VISION_MODELS.any? { |m| model.include?(m) }
|
33
49
|
when 'anthropic'
|
34
50
|
ANTHROPIC_VISION_MODELS.any? { |m| model.include?(m) }
|
51
|
+
when 'gemini'
|
52
|
+
GEMINI_VISION_MODELS.any? { |m| model.include?(m) }
|
35
53
|
else
|
36
54
|
false
|
37
55
|
end
|
@@ -49,6 +67,8 @@ module DSPy
|
|
49
67
|
OPENAI_VISION_MODELS
|
50
68
|
when 'anthropic'
|
51
69
|
ANTHROPIC_VISION_MODELS
|
70
|
+
when 'gemini'
|
71
|
+
GEMINI_VISION_MODELS
|
52
72
|
else
|
53
73
|
[]
|
54
74
|
end
|
data/lib/dspy/lm.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative 'lm/adapter_factory'
|
|
14
14
|
require_relative 'lm/adapters/openai_adapter'
|
15
15
|
require_relative 'lm/adapters/anthropic_adapter'
|
16
16
|
require_relative 'lm/adapters/ollama_adapter'
|
17
|
+
require_relative 'lm/adapters/gemini_adapter'
|
17
18
|
|
18
19
|
# Load strategy system
|
19
20
|
require_relative 'lm/strategy_selector'
|
@@ -232,10 +233,10 @@ module DSPy
|
|
232
233
|
usage = result.usage
|
233
234
|
DSPy.log('span.attributes',
|
234
235
|
span_id: DSPy::Context.current[:span_stack].last,
|
235
|
-
'gen_ai.response.model' => result.
|
236
|
-
'gen_ai.usage.prompt_tokens' => usage.
|
237
|
-
'gen_ai.usage.completion_tokens' => usage.
|
238
|
-
'gen_ai.usage.total_tokens' => usage.
|
236
|
+
'gen_ai.response.model' => result.metadata.model,
|
237
|
+
'gen_ai.usage.prompt_tokens' => usage.input_tokens,
|
238
|
+
'gen_ai.usage.completion_tokens' => usage.output_tokens,
|
239
|
+
'gen_ai.usage.total_tokens' => usage.total_tokens
|
239
240
|
)
|
240
241
|
end
|
241
242
|
|
@@ -80,7 +80,8 @@ module DSPy
|
|
80
80
|
def extract_type_from_prop(prop)
|
81
81
|
case prop
|
82
82
|
when Hash
|
83
|
-
|
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
|
-
|
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/module.rb
CHANGED
@@ -49,10 +49,10 @@ module DSPy
|
|
49
49
|
forward_untyped(**input_values)
|
50
50
|
end
|
51
51
|
|
52
|
-
# Get the configured LM for this instance,
|
52
|
+
# Get the configured LM for this instance, checking fiber-local context first
|
53
53
|
sig { returns(T.untyped) }
|
54
54
|
def lm
|
55
|
-
config.lm || DSPy.
|
55
|
+
config.lm || DSPy.current_lm
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
data/lib/dspy/predict.rb
CHANGED
@@ -59,6 +59,42 @@ module DSPy
|
|
59
59
|
@prompt = Prompt.from_signature(signature_class)
|
60
60
|
end
|
61
61
|
|
62
|
+
# Reconstruct program from serialized hash
|
63
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
|
64
|
+
def self.from_h(data)
|
65
|
+
state = data[:state]
|
66
|
+
raise ArgumentError, "Missing state in serialized data" unless state
|
67
|
+
|
68
|
+
signature_class_name = state[:signature_class]
|
69
|
+
signature_class = Object.const_get(signature_class_name)
|
70
|
+
program = new(signature_class)
|
71
|
+
|
72
|
+
# Restore instruction if available
|
73
|
+
if state[:instruction]
|
74
|
+
program = program.with_instruction(state[:instruction])
|
75
|
+
end
|
76
|
+
|
77
|
+
# Restore examples if available
|
78
|
+
few_shot_examples = state[:few_shot_examples]
|
79
|
+
if few_shot_examples && !few_shot_examples.empty?
|
80
|
+
# Convert hash examples back to FewShotExample objects
|
81
|
+
examples = few_shot_examples.map do |ex|
|
82
|
+
if ex.is_a?(Hash)
|
83
|
+
DSPy::FewShotExample.new(
|
84
|
+
input: ex[:input],
|
85
|
+
output: ex[:output],
|
86
|
+
reasoning: ex[:reasoning]
|
87
|
+
)
|
88
|
+
else
|
89
|
+
ex
|
90
|
+
end
|
91
|
+
end
|
92
|
+
program = program.with_examples(examples)
|
93
|
+
end
|
94
|
+
|
95
|
+
program
|
96
|
+
end
|
97
|
+
|
62
98
|
# Backward compatibility methods - delegate to prompt object
|
63
99
|
sig { returns(String) }
|
64
100
|
def system_signature
|
@@ -159,7 +195,11 @@ module DSPy
|
|
159
195
|
begin
|
160
196
|
combined_struct = create_combined_struct_class
|
161
197
|
all_attributes = input_values.merge(output_attributes)
|
162
|
-
|
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)
|
163
203
|
rescue ArgumentError => e
|
164
204
|
raise PredictionInvalidError.new({ output: e.message })
|
165
205
|
rescue TypeError => e
|
@@ -195,5 +235,36 @@ module DSPy
|
|
195
235
|
|
196
236
|
output_attributes
|
197
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
|
198
269
|
end
|
199
270
|
end
|
data/lib/dspy/prediction.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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
|
404
|
+
rescue
|
356
405
|
# Return original value if conversion fails
|
357
406
|
value
|
358
407
|
end
|
@@ -172,15 +172,35 @@ module DSPy
|
|
172
172
|
sig { params(struct_class: T.class_of(T::Struct)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
173
173
|
def extract_field_info(struct_class)
|
174
174
|
struct_class.props.map do |name, prop_info|
|
175
|
-
{
|
175
|
+
field_info = {
|
176
176
|
name: name,
|
177
177
|
type: prop_info[:type].to_s,
|
178
178
|
description: prop_info[:description] || "",
|
179
179
|
required: !prop_info[:rules]&.any? { |rule| rule.is_a?(T::Props::NilableRules) }
|
180
180
|
}
|
181
|
+
|
182
|
+
# Extract enum values if this is an enum type
|
183
|
+
if enum_values = extract_enum_values(prop_info[:type])
|
184
|
+
field_info[:enum_values] = enum_values
|
185
|
+
field_info[:is_enum] = true
|
186
|
+
end
|
187
|
+
|
188
|
+
field_info
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Extract enum values from a type if it's an enum
|
193
|
+
sig { params(type: T.untyped).returns(T.nilable(T::Array[String])) }
|
194
|
+
def extract_enum_values(type)
|
195
|
+
# Handle T::Enum types
|
196
|
+
if type.is_a?(Class) && type < T::Enum
|
197
|
+
type.values.map(&:serialize)
|
198
|
+
else
|
199
|
+
nil
|
181
200
|
end
|
182
201
|
end
|
183
202
|
|
203
|
+
|
184
204
|
# Analyze patterns in training examples
|
185
205
|
sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
186
206
|
def analyze_example_patterns(examples)
|
@@ -364,8 +384,12 @@ module DSPy
|
|
364
384
|
context_parts << "Task: #{signature_class.description}" if @config.use_task_description
|
365
385
|
|
366
386
|
if @config.use_input_output_analysis
|
367
|
-
|
368
|
-
|
387
|
+
# Build detailed field descriptions including enum values
|
388
|
+
input_descriptions = analysis[:input_fields].map { |f| format_field_description(f) }
|
389
|
+
output_descriptions = analysis[:output_fields].map { |f| format_field_description(f) }
|
390
|
+
|
391
|
+
context_parts << "Input fields: #{input_descriptions.join(', ')}"
|
392
|
+
context_parts << "Output fields: #{output_descriptions.join(', ')}"
|
369
393
|
end
|
370
394
|
|
371
395
|
if analysis[:common_themes] && analysis[:common_themes].any?
|
@@ -379,6 +403,17 @@ module DSPy
|
|
379
403
|
context_parts.join("\n")
|
380
404
|
end
|
381
405
|
|
406
|
+
# Format field description with enum values if applicable
|
407
|
+
sig { params(field: T::Hash[Symbol, T.untyped]).returns(String) }
|
408
|
+
def format_field_description(field)
|
409
|
+
base = "#{field[:name]} (#{field[:type]})"
|
410
|
+
if field[:is_enum] && field[:enum_values] && !field[:enum_values].empty?
|
411
|
+
"#{base} [values: #{field[:enum_values].join(', ')}]"
|
412
|
+
else
|
413
|
+
base
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
382
417
|
# Build requirements text for instruction generation
|
383
418
|
sig { params(analysis: T::Hash[Symbol, T.untyped]).returns(String) }
|
384
419
|
def build_requirements_text(analysis)
|
data/lib/dspy/re_act.rb
CHANGED
@@ -112,8 +112,16 @@ module DSPy
|
|
112
112
|
# Use the enhanced output struct with ReAct fields
|
113
113
|
@output_struct_class = enhanced_output_struct
|
114
114
|
|
115
|
+
# Store original signature name
|
116
|
+
@original_signature_name = signature_class.name
|
117
|
+
|
115
118
|
class << self
|
116
|
-
attr_reader :input_struct_class, :output_struct_class
|
119
|
+
attr_reader :input_struct_class, :output_struct_class, :original_signature_name
|
120
|
+
|
121
|
+
# Override name to return the original signature name
|
122
|
+
def name
|
123
|
+
@original_signature_name || super
|
124
|
+
end
|
117
125
|
end
|
118
126
|
end
|
119
127
|
|
@@ -123,9 +131,6 @@ module DSPy
|
|
123
131
|
|
124
132
|
sig { params(kwargs: T.untyped).returns(T.untyped).override }
|
125
133
|
def forward(**kwargs)
|
126
|
-
lm = config.lm || DSPy.config.lm
|
127
|
-
available_tools = @tools.keys
|
128
|
-
|
129
134
|
# Validate input
|
130
135
|
input_struct = @original_signature_class.input_struct_class.new(**kwargs)
|
131
136
|
|
data/lib/dspy/signature.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
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
|
@@ -90,18 +90,39 @@ module DSPy
|
|
90
90
|
|
91
91
|
sig { params(program: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
92
92
|
def serialize_program(program)
|
93
|
-
# Basic serialization
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
93
|
+
# Basic serialization
|
94
|
+
if program.is_a?(Hash)
|
95
|
+
# Already serialized - return as-is to preserve state
|
96
|
+
program
|
97
|
+
else
|
98
|
+
# Real program object - serialize it
|
99
|
+
{
|
100
|
+
class_name: program.class.name,
|
101
|
+
state: extract_program_state(program)
|
102
|
+
}
|
103
|
+
end
|
98
104
|
end
|
99
105
|
|
100
|
-
sig { params(data: T
|
106
|
+
sig { params(data: T.untyped).returns(T.untyped) }
|
101
107
|
def self.deserialize_program(data)
|
102
|
-
#
|
103
|
-
|
104
|
-
|
108
|
+
# Ensure data is a Hash
|
109
|
+
unless data.is_a?(Hash)
|
110
|
+
raise ArgumentError, "Expected Hash for program data, got #{data.class.name}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get class name from the serialized data
|
114
|
+
class_name = data[:class_name]
|
115
|
+
raise ArgumentError, "Missing class_name in serialized data" unless class_name
|
116
|
+
|
117
|
+
# Get the class constant
|
118
|
+
program_class = Object.const_get(class_name)
|
119
|
+
|
120
|
+
# Use the class's from_h method
|
121
|
+
unless program_class.respond_to?(:from_h)
|
122
|
+
raise ArgumentError, "Class #{class_name} does not support deserialization (missing from_h method)"
|
123
|
+
end
|
124
|
+
|
125
|
+
program_class.from_h(data)
|
105
126
|
end
|
106
127
|
|
107
128
|
sig { params(program: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
@@ -310,6 +331,20 @@ module DSPy
|
|
310
331
|
{ programs: [], summary: { total_programs: 0, avg_score: 0.0 } }
|
311
332
|
end
|
312
333
|
|
334
|
+
# Extract signature class name from program object
|
335
|
+
unless saved_program.program.respond_to?(:signature_class)
|
336
|
+
raise ArgumentError, "Program #{saved_program.program.class.name} does not respond to signature_class method"
|
337
|
+
end
|
338
|
+
|
339
|
+
signature_class_name = saved_program.program.signature_class.name
|
340
|
+
|
341
|
+
if signature_class_name.nil? || signature_class_name.empty?
|
342
|
+
raise(
|
343
|
+
"Program #{saved_program.program.class.name} has a signature class that does not provide a name.\n" \
|
344
|
+
"Ensure the signature class responds to #name or that signature_class_name is stored in program state."
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
313
348
|
# Add or update program entry
|
314
349
|
program_entry = {
|
315
350
|
program_id: saved_program.program_id,
|
@@ -317,7 +352,7 @@ module DSPy
|
|
317
352
|
best_score: saved_program.optimization_result[:best_score_value],
|
318
353
|
score_name: saved_program.optimization_result[:best_score_name],
|
319
354
|
optimizer: saved_program.optimization_result[:metadata]&.dig(:optimizer),
|
320
|
-
signature_class:
|
355
|
+
signature_class: signature_class_name,
|
321
356
|
metadata: saved_program.metadata
|
322
357
|
}
|
323
358
|
|