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.
- checksums.yaml +4 -4
- data/README.md +28 -9
- data/lib/dspy/lm/adapter_factory.rb +1 -1
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +3 -2
- data/lib/dspy/lm/chat_strategy.rb +38 -0
- data/lib/dspy/lm/json_strategy.rb +222 -0
- data/lib/dspy/lm.rb +13 -16
- data/lib/dspy/re_act.rb +253 -68
- data/lib/dspy/signature.rb +2 -251
- data/lib/dspy/tools/base.rb +5 -7
- data/lib/dspy/type_system/sorbet_json_schema.rb +56 -18
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +0 -8
- metadata +4 -12
- data/lib/dspy/lm/retry_handler.rb +0 -132
- data/lib/dspy/lm/strategies/anthropic_extraction_strategy.rb +0 -78
- data/lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb +0 -192
- data/lib/dspy/lm/strategies/base_strategy.rb +0 -53
- data/lib/dspy/lm/strategies/enhanced_prompting_strategy.rb +0 -178
- data/lib/dspy/lm/strategies/gemini_structured_output_strategy.rb +0 -80
- data/lib/dspy/lm/strategies/openai_structured_output_strategy.rb +0 -65
- data/lib/dspy/lm/strategy_selector.rb +0 -144
- data/lib/dspy/lm/structured_output_strategy.rb +0 -17
- data/lib/dspy/strategy.rb +0 -18
data/lib/dspy/signature.rb
CHANGED
@@ -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
|
data/lib/dspy/tools/base.rb
CHANGED
@@ -145,11 +145,11 @@ module DSPy
|
|
145
145
|
when String
|
146
146
|
begin
|
147
147
|
JSON.parse(args_json)
|
148
|
-
rescue JSON::ParserError
|
149
|
-
|
148
|
+
rescue JSON::ParserError => e
|
149
|
+
raise ArgumentError, "Invalid JSON input: #{e.message}"
|
150
150
|
end
|
151
151
|
else
|
152
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
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.
|
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-
|
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
|