dspy 0.27.5 → 0.28.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.
@@ -133,7 +133,7 @@ module DSPy
133
133
  required = []
134
134
 
135
135
  @input_field_descriptors&.each do |name, descriptor|
136
- schema = type_to_json_schema(descriptor.type)
136
+ schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(descriptor.type)
137
137
  schema[:description] = descriptor.description if descriptor.description
138
138
  properties[name] = schema
139
139
  required << name.to_s unless descriptor.has_default
@@ -160,7 +160,7 @@ module DSPy
160
160
  required = []
161
161
 
162
162
  @output_field_descriptors&.each do |name, descriptor|
163
- schema = type_to_json_schema(descriptor.type)
163
+ schema = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(descriptor.type)
164
164
  schema[:description] = descriptor.description if descriptor.description
165
165
  properties[name] = schema
166
166
  required << name.to_s unless descriptor.has_default
@@ -178,255 +178,6 @@ module DSPy
178
178
  def output_schema
179
179
  @output_struct_class
180
180
  end
181
-
182
- private
183
-
184
- sig { params(type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
185
- def type_to_json_schema(type)
186
- # Handle T::Boolean type alias first
187
- if type == T::Boolean
188
- return { type: "boolean" }
189
- end
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
-
196
- # Handle raw class types first
197
- if type.is_a?(Class)
198
- if type < T::Enum
199
- # Get all enum values
200
- values = type.values.map(&:serialize)
201
- { type: "string", enum: values }
202
- elsif type == String
203
- { type: "string" }
204
- elsif type == Integer
205
- { type: "integer" }
206
- elsif type == Float
207
- { type: "number" }
208
- elsif type == Numeric
209
- { type: "number" }
210
- elsif type == Date
211
- { type: "string", format: "date" }
212
- elsif type == DateTime
213
- { type: "string", format: "date-time" }
214
- elsif type == Time
215
- { type: "string", format: "date-time" }
216
- elsif [TrueClass, FalseClass].include?(type)
217
- { type: "boolean" }
218
- elsif type < T::Struct
219
- # Handle custom T::Struct classes by generating nested object schema
220
- generate_struct_schema(type)
221
- else
222
- { type: "string" } # Default fallback
223
- end
224
- elsif type.is_a?(T::Types::Simple)
225
- case type.raw_type.to_s
226
- when "String"
227
- { type: "string" }
228
- when "Integer"
229
- { type: "integer" }
230
- when "Float"
231
- { type: "number" }
232
- when "Numeric"
233
- { type: "number" }
234
- when "Date"
235
- { type: "string", format: "date" }
236
- when "DateTime"
237
- { type: "string", format: "date-time" }
238
- when "Time"
239
- { type: "string", format: "date-time" }
240
- when "TrueClass", "FalseClass"
241
- { type: "boolean" }
242
- when "T::Boolean"
243
- { type: "boolean" }
244
- else
245
- # Check if it's an enum
246
- if type.raw_type < T::Enum
247
- # Get all enum values
248
- values = type.raw_type.values.map(&:serialize)
249
- { type: "string", enum: values }
250
- elsif type.raw_type < T::Struct
251
- # Handle custom T::Struct classes
252
- generate_struct_schema(type.raw_type)
253
- else
254
- { type: "string" } # Default fallback
255
- end
256
- end
257
- elsif type.is_a?(T::Types::TypedArray)
258
- # Handle arrays properly with nested item type
259
- {
260
- type: "array",
261
- items: type_to_json_schema(type.type)
262
- }
263
- elsif type.is_a?(T::Types::TypedHash)
264
- # Handle hashes as objects with additionalProperties
265
- # TypedHash has keys and values methods to access its key and value types
266
- key_schema = type_to_json_schema(type.keys)
267
- value_schema = type_to_json_schema(type.values)
268
-
269
- # Create a more descriptive schema for nested structures
270
- {
271
- type: "object",
272
- propertyNames: key_schema, # Describe key constraints
273
- additionalProperties: value_schema,
274
- # Add a more explicit description of the expected structure
275
- description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
276
- }
277
- elsif type.is_a?(T::Types::FixedHash)
278
- # Handle fixed hashes (from type aliases like { "key" => Type })
279
- properties = {}
280
- required = []
281
-
282
- type.types.each do |key, value_type|
283
- properties[key] = type_to_json_schema(value_type)
284
- required << key
285
- end
286
-
287
- {
288
- type: "object",
289
- properties: properties,
290
- required: required,
291
- additionalProperties: false
292
- }
293
- elsif type.class.name == "T::Private::Types::SimplePairUnion"
294
- # Handle T.nilable types (T::Private::Types::SimplePairUnion)
295
- # This is the actual implementation of T.nilable(SomeType)
296
- has_nil = type.respond_to?(:types) && type.types.any? do |t|
297
- (t.respond_to?(:raw_type) && t.raw_type == NilClass) ||
298
- (t.respond_to?(:name) && t.name == "NilClass")
299
- end
300
-
301
- if has_nil
302
- # Find the non-nil type
303
- non_nil_type = type.types.find do |t|
304
- !(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
305
- !(t.respond_to?(:name) && t.name == "NilClass")
306
- end
307
-
308
- if non_nil_type
309
- base_schema = type_to_json_schema(non_nil_type)
310
- if base_schema[:type].is_a?(String)
311
- # Convert single type to array with null
312
- { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
313
- else
314
- # For complex schemas, use anyOf to allow null
315
- { anyOf: [base_schema, { type: "null" }] }
316
- end
317
- else
318
- { type: "string" } # Fallback
319
- end
320
- else
321
- # Not nilable SimplePairUnion - this is a regular T.any() union
322
- # Generate oneOf schema for all types
323
- if type.respond_to?(:types) && type.types.length > 1
324
- {
325
- oneOf: type.types.map { |t| type_to_json_schema(t) },
326
- description: "Union of multiple types"
327
- }
328
- else
329
- # Single type or fallback
330
- first_type = type.respond_to?(:types) ? type.types.first : type
331
- type_to_json_schema(first_type)
332
- end
333
- end
334
- elsif type.is_a?(T::Types::Union)
335
- # Check if this is a nilable type (contains NilClass)
336
- is_nilable = type.types.any? { |t| t == T::Utils.coerce(NilClass) }
337
- non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
338
-
339
- # Special case: check if we have TrueClass + FalseClass (T.nilable(T::Boolean))
340
- if non_nil_types.size == 2 && is_nilable
341
- true_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == TrueClass }
342
- false_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == FalseClass }
343
-
344
- if true_class_type && false_class_type
345
- # This is T.nilable(T::Boolean) - treat as nilable boolean
346
- return { type: ["boolean", "null"] }
347
- end
348
- end
349
-
350
- if non_nil_types.size == 1 && is_nilable
351
- # This is T.nilable(SomeType) - generate proper schema with null allowed
352
- base_schema = type_to_json_schema(non_nil_types.first)
353
- if base_schema[:type].is_a?(String)
354
- # Convert single type to array with null
355
- { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
356
- else
357
- # For complex schemas, use anyOf to allow null
358
- { anyOf: [base_schema, { type: "null" }] }
359
- end
360
- elsif non_nil_types.size == 1
361
- # Non-nilable single type union (shouldn't happen in practice)
362
- type_to_json_schema(non_nil_types.first)
363
- elsif non_nil_types.size > 1
364
- # Handle complex unions with oneOf for better JSON schema compliance
365
- base_schema = {
366
- oneOf: non_nil_types.map { |t| type_to_json_schema(t) },
367
- description: "Union of multiple types"
368
- }
369
- if is_nilable
370
- # Add null as an option for complex nilable unions
371
- base_schema[:oneOf] << { type: "null" }
372
- end
373
- base_schema
374
- else
375
- { type: "string" } # Fallback for complex unions
376
- end
377
- elsif type.is_a?(T::Types::ClassOf)
378
- # Handle T.class_of() types
379
- {
380
- type: "string",
381
- description: "Class name (T.class_of type)"
382
- }
383
- else
384
- { type: "string" } # Default fallback
385
- end
386
- end
387
-
388
- private
389
-
390
- # Generate JSON schema for custom T::Struct classes
391
- sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[Symbol, T.untyped]) }
392
- def generate_struct_schema(struct_class)
393
- return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
394
-
395
- properties = {}
396
- required = []
397
-
398
- # Check if struct already has a _type field
399
- if struct_class.props.key?(:_type)
400
- raise DSPy::ValidationError, "_type field conflict: #{struct_class.name} already has a _type field defined. " \
401
- "DSPy uses _type for automatic type detection in union types."
402
- end
403
-
404
- # Add automatic _type field for type detection
405
- properties[:_type] = {
406
- type: "string",
407
- const: struct_class.name.split('::').last # Use the simple class name
408
- }
409
- required << "_type"
410
-
411
- struct_class.props.each do |prop_name, prop_info|
412
- prop_type = prop_info[:type_object] || prop_info[:type]
413
- properties[prop_name] = type_to_json_schema(prop_type)
414
-
415
- # A field is required if it's not fully optional
416
- # fully_optional is true for nilable prop fields
417
- # immutable const fields are required unless nilable
418
- unless prop_info[:fully_optional]
419
- required << prop_name.to_s
420
- end
421
- end
422
-
423
- {
424
- type: "object",
425
- properties: properties,
426
- required: required,
427
- description: "#{struct_class.name} struct"
428
- }
429
- end
430
181
  end
431
182
  end
432
183
  end
@@ -145,11 +145,11 @@ module DSPy
145
145
  when String
146
146
  begin
147
147
  JSON.parse(args_json)
148
- rescue JSON::ParserError
149
- return "Error: Invalid JSON input"
148
+ rescue JSON::ParserError => e
149
+ raise ArgumentError, "Invalid JSON input: #{e.message}"
150
150
  end
151
151
  else
152
- return "Error: Expected Hash or JSON string"
152
+ raise ArgumentError, "Expected Hash or JSON string, got #{args_json.class}"
153
153
  end
154
154
 
155
155
  # Convert string keys to symbols and validate types
@@ -168,7 +168,7 @@ module DSPy
168
168
  if args.key?(key)
169
169
  kwargs[param_name] = coerce_value_to_type(args[key], param_type)
170
170
  elsif schema[:required].include?(key)
171
- return "Error: Missing required parameter: #{key}"
171
+ raise ArgumentError, "Missing required parameter: #{key}"
172
172
  end
173
173
  end
174
174
 
@@ -180,15 +180,13 @@ module DSPy
180
180
  if args.key?(key)
181
181
  kwargs[param_name] = coerce_value_to_type(args[key], param_type)
182
182
  elsif schema[:required].include?(key)
183
- return "Error: Missing required parameter: #{key}"
183
+ raise ArgumentError, "Missing required parameter: #{key}"
184
184
  end
185
185
  end
186
186
  end
187
187
 
188
188
  call(**kwargs)
189
189
  end
190
- rescue => e
191
- "Error: #{e.message}"
192
190
  end
193
191
 
194
192
  # Subclasses must implement their own call method with their own signature
@@ -12,8 +12,10 @@ module DSPy
12
12
  extend T::Helpers
13
13
 
14
14
  # Convert a Sorbet type to JSON Schema format
15
- sig { params(type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
16
- def self.type_to_json_schema(type)
15
+ sig { params(type: T.untyped, visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
16
+ def self.type_to_json_schema(type, visited = nil)
17
+ visited ||= Set.new
18
+
17
19
  # Handle T::Boolean type alias first
18
20
  if type == T::Boolean
19
21
  return { type: "boolean" }
@@ -21,7 +23,7 @@ module DSPy
21
23
 
22
24
  # Handle type aliases by resolving to their underlying type
23
25
  if type.is_a?(T::Private::Types::TypeAlias)
24
- return self.type_to_json_schema(type.aliased_type)
26
+ return self.type_to_json_schema(type.aliased_type, visited)
25
27
  end
26
28
 
27
29
  # Handle raw class types first
@@ -38,11 +40,26 @@ module DSPy
38
40
  { type: "number" }
39
41
  elsif type == Numeric
40
42
  { type: "number" }
43
+ elsif type == Date
44
+ { type: "string", format: "date" }
45
+ elsif type == DateTime
46
+ { type: "string", format: "date-time" }
47
+ elsif type == Time
48
+ { type: "string", format: "date-time" }
41
49
  elsif [TrueClass, FalseClass].include?(type)
42
50
  { type: "boolean" }
43
51
  elsif type < T::Struct
44
52
  # Handle custom T::Struct classes by generating nested object schema
45
- self.generate_struct_schema(type)
53
+ # Check for recursion
54
+ if visited.include?(type)
55
+ # Return a reference to avoid infinite recursion
56
+ {
57
+ "$ref" => "#/definitions/#{type.name.split('::').last}",
58
+ description: "Recursive reference to #{type.name}"
59
+ }
60
+ else
61
+ self.generate_struct_schema(type, visited)
62
+ end
46
63
  else
47
64
  { type: "string" } # Default fallback
48
65
  end
@@ -56,6 +73,12 @@ module DSPy
56
73
  { type: "number" }
57
74
  when "Numeric"
58
75
  { type: "number" }
76
+ when "Date"
77
+ { type: "string", format: "date" }
78
+ when "DateTime"
79
+ { type: "string", format: "date-time" }
80
+ when "Time"
81
+ { type: "string", format: "date-time" }
59
82
  when "TrueClass", "FalseClass"
60
83
  { type: "boolean" }
61
84
  when "T::Boolean"
@@ -68,7 +91,14 @@ module DSPy
68
91
  { type: "string", enum: values }
69
92
  elsif type.raw_type < T::Struct
70
93
  # Handle custom T::Struct classes
71
- generate_struct_schema(type.raw_type)
94
+ if visited.include?(type.raw_type)
95
+ {
96
+ "$ref" => "#/definitions/#{type.raw_type.name.split('::').last}",
97
+ description: "Recursive reference to #{type.raw_type.name}"
98
+ }
99
+ else
100
+ generate_struct_schema(type.raw_type, visited)
101
+ end
72
102
  else
73
103
  { type: "string" } # Default fallback
74
104
  end
@@ -77,13 +107,13 @@ module DSPy
77
107
  # Handle arrays properly with nested item type
78
108
  {
79
109
  type: "array",
80
- items: self.type_to_json_schema(type.type)
110
+ items: self.type_to_json_schema(type.type, visited)
81
111
  }
82
112
  elsif type.is_a?(T::Types::TypedHash)
83
113
  # Handle hashes as objects with additionalProperties
84
114
  # TypedHash has keys and values methods to access its key and value types
85
- key_schema = self.type_to_json_schema(type.keys)
86
- value_schema = self.type_to_json_schema(type.values)
115
+ key_schema = self.type_to_json_schema(type.keys, visited)
116
+ value_schema = self.type_to_json_schema(type.values, visited)
87
117
 
88
118
  # Create a more descriptive schema for nested structures
89
119
  {
@@ -99,7 +129,7 @@ module DSPy
99
129
  required = []
100
130
 
101
131
  type.types.each do |key, value_type|
102
- properties[key] = self.type_to_json_schema(value_type)
132
+ properties[key] = self.type_to_json_schema(value_type, visited)
103
133
  required << key
104
134
  end
105
135
 
@@ -125,7 +155,7 @@ module DSPy
125
155
  end
126
156
 
127
157
  if non_nil_type
128
- base_schema = self.type_to_json_schema(non_nil_type)
158
+ base_schema = self.type_to_json_schema(non_nil_type, visited)
129
159
  if base_schema[:type].is_a?(String)
130
160
  # Convert single type to array with null
131
161
  { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
@@ -141,13 +171,13 @@ module DSPy
141
171
  # Generate oneOf schema for all types
142
172
  if type.respond_to?(:types) && type.types.length > 1
143
173
  {
144
- oneOf: type.types.map { |t| self.type_to_json_schema(t) },
174
+ oneOf: type.types.map { |t| self.type_to_json_schema(t, visited) },
145
175
  description: "Union of multiple types"
146
176
  }
147
177
  else
148
178
  # Single type or fallback
149
179
  first_type = type.respond_to?(:types) ? type.types.first : type
150
- self.type_to_json_schema(first_type)
180
+ self.type_to_json_schema(first_type, visited)
151
181
  end
152
182
  end
153
183
  elsif type.is_a?(T::Types::Union)
@@ -168,7 +198,7 @@ module DSPy
168
198
 
169
199
  if non_nil_types.size == 1 && is_nilable
170
200
  # This is T.nilable(SomeType) - generate proper schema with null allowed
171
- base_schema = self.type_to_json_schema(non_nil_types.first)
201
+ base_schema = self.type_to_json_schema(non_nil_types.first, visited)
172
202
  if base_schema[:type].is_a?(String)
173
203
  # Convert single type to array with null
174
204
  { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
@@ -178,11 +208,11 @@ module DSPy
178
208
  end
179
209
  elsif non_nil_types.size == 1
180
210
  # Non-nilable single type union (shouldn't happen in practice)
181
- self.type_to_json_schema(non_nil_types.first)
211
+ self.type_to_json_schema(non_nil_types.first, visited)
182
212
  elsif non_nil_types.size > 1
183
213
  # Handle complex unions with oneOf for better JSON schema compliance
184
214
  base_schema = {
185
- oneOf: non_nil_types.map { |t| self.type_to_json_schema(t) },
215
+ oneOf: non_nil_types.map { |t| self.type_to_json_schema(t, visited) },
186
216
  description: "Union of multiple types"
187
217
  }
188
218
  if is_nilable
@@ -205,10 +235,15 @@ module DSPy
205
235
  end
206
236
 
207
237
  # Generate JSON schema for custom T::Struct classes
208
- sig { params(struct_class: T.class_of(T::Struct)).returns(T::Hash[Symbol, T.untyped]) }
209
- def self.generate_struct_schema(struct_class)
238
+ sig { params(struct_class: T.class_of(T::Struct), visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
239
+ def self.generate_struct_schema(struct_class, visited = nil)
240
+ visited ||= Set.new
241
+
210
242
  return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
211
243
 
244
+ # Add this struct to visited set to detect recursion
245
+ visited.add(struct_class)
246
+
212
247
  properties = {}
213
248
  required = []
214
249
 
@@ -227,7 +262,7 @@ module DSPy
227
262
 
228
263
  struct_class.props.each do |prop_name, prop_info|
229
264
  prop_type = prop_info[:type_object] || prop_info[:type]
230
- properties[prop_name] = self.type_to_json_schema(prop_type)
265
+ properties[prop_name] = self.type_to_json_schema(prop_type, visited)
231
266
 
232
267
  # A field is required if it's not fully optional
233
268
  # fully_optional is true for nilable prop fields
@@ -237,6 +272,9 @@ module DSPy
237
272
  end
238
273
  end
239
274
 
275
+ # Remove this struct from visited set after processing
276
+ visited.delete(struct_class)
277
+
240
278
  {
241
279
  type: "object",
242
280
  properties: properties,
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.27.5"
4
+ VERSION = "0.28.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -23,15 +23,8 @@ module DSPy
23
23
  setting :structured_outputs do
24
24
  setting :openai, default: false
25
25
  setting :anthropic, default: false # Reserved for future use
26
- setting :strategy, default: nil # Can be DSPy::Strategy::Strict, DSPy::Strategy::Compatible, or nil for auto
27
- setting :retry_enabled, default: true
28
- setting :max_retries, default: 3
29
- setting :fallback_enabled, default: true
30
26
  end
31
27
 
32
- # Test mode disables sleeps in retry logic
33
- setting :test_mode, default: false
34
-
35
28
  def self.logger
36
29
  @logger ||= create_logger
37
30
  end
@@ -206,7 +199,6 @@ require_relative 'dspy/prompt'
206
199
  require_relative 'dspy/example'
207
200
  require_relative 'dspy/lm'
208
201
  require_relative 'dspy/image'
209
- require_relative 'dspy/strategy'
210
202
  require_relative 'dspy/prediction'
211
203
  require_relative 'dspy/predict'
212
204
  require_relative 'dspy/events/subscribers'
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.27.5
4
+ version: 0.28.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-09-30 00:00:00.000000000 Z
10
+ date: 2025-10-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -213,19 +213,12 @@ files:
213
213
  - lib/dspy/lm/adapters/openai/schema_converter.rb
214
214
  - lib/dspy/lm/adapters/openai_adapter.rb
215
215
  - lib/dspy/lm/adapters/openrouter_adapter.rb
216
+ - lib/dspy/lm/chat_strategy.rb
216
217
  - lib/dspy/lm/errors.rb
218
+ - lib/dspy/lm/json_strategy.rb
217
219
  - lib/dspy/lm/message.rb
218
220
  - lib/dspy/lm/message_builder.rb
219
221
  - lib/dspy/lm/response.rb
220
- - lib/dspy/lm/retry_handler.rb
221
- - lib/dspy/lm/strategies/anthropic_extraction_strategy.rb
222
- - lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb
223
- - lib/dspy/lm/strategies/base_strategy.rb
224
- - lib/dspy/lm/strategies/enhanced_prompting_strategy.rb
225
- - lib/dspy/lm/strategies/gemini_structured_output_strategy.rb
226
- - lib/dspy/lm/strategies/openai_structured_output_strategy.rb
227
- - lib/dspy/lm/strategy_selector.rb
228
- - lib/dspy/lm/structured_output_strategy.rb
229
222
  - lib/dspy/lm/usage.rb
230
223
  - lib/dspy/lm/vision_models.rb
231
224
  - lib/dspy/memory.rb
@@ -254,7 +247,6 @@ files:
254
247
  - lib/dspy/signature.rb
255
248
  - lib/dspy/storage/program_storage.rb
256
249
  - lib/dspy/storage/storage_manager.rb
257
- - lib/dspy/strategy.rb
258
250
  - lib/dspy/teleprompt/data_handler.rb
259
251
  - lib/dspy/teleprompt/gepa.rb
260
252
  - lib/dspy/teleprompt/mipro_v2.rb