dspy 0.34.0 → 0.34.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: 3bf98e1e8f5f939799d7e14717d8859b10830144a9c23f1d4818e6fa021fb46a
4
- data.tar.gz: 154e27f97ed2c3ae5b8a04f2d3941a93e79a0e88bae8459fd73a85a9d03ed186
3
+ metadata.gz: 100e82f4aeff8020a845aa80a63ad86278ead32f34b7846b0624db99dc060325
4
+ data.tar.gz: 43ed18798e67e829e2decdd8ef0519751d6f4fc2cf52571850f1acfd671f2780
5
5
  SHA512:
6
- metadata.gz: 298305a05b5a38806d67989c01ed2a476f291d13ec6c8a228c8d465fa925d56b178b6c0865d4e9282b4e6ab29aa9655e9b7fc64a228478524d2ce94d3015758f
7
- data.tar.gz: 207ff7188ff0bcb16bfd6b45893672085363683464889a4d3e3ba666d2989f2daa0260660da00f7e29ba0df360ca85a430536f38fae319a2e2106405e99e0af9
6
+ metadata.gz: 3081d9fada92dcf1f7f5b003212fd6d5b94787b6982c6828bd95704e84f197be3017d08eff81c4c1271e4a1d442e6a235973f4c2ab69c47493e025b2d155b1ca
7
+ data.tar.gz: 32454139be26918608d4c63f58a706d762caa457ca3df9addb238eb9c9bfd5e97f749054923573838b6e983db129732de290387076423e39293bcf02e2747bc4
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ module Ext
7
+ # Extends T::Struct to support field descriptions via the :description kwarg.
8
+ #
9
+ # This module is prepended to T::Struct to intercept const/prop definitions
10
+ # and capture descriptions before they reach Sorbet (which doesn't support them).
11
+ #
12
+ # @example
13
+ # class ASTNode < T::Struct
14
+ # const :node_type, String, description: 'The type of AST node'
15
+ # const :text, String, default: "", description: 'Text content of the node'
16
+ # const :children, T::Array[ASTNode], default: []
17
+ # end
18
+ #
19
+ # ASTNode.field_descriptions[:node_type] # => "The type of AST node"
20
+ # ASTNode.field_descriptions[:text] # => "Text content of the node"
21
+ # ASTNode.field_descriptions[:children] # => nil (no description)
22
+ #
23
+ module StructDescriptions
24
+ def self.prepended(base)
25
+ base.singleton_class.prepend(ClassMethods)
26
+ end
27
+
28
+ module ClassMethods
29
+ # Returns a hash of field names to their descriptions.
30
+ # Only fields with explicit :description kwargs are included.
31
+ #
32
+ # @return [Hash{Symbol => String}]
33
+ def field_descriptions
34
+ @field_descriptions ||= {}
35
+ end
36
+
37
+ # Intercepts const definitions to capture :description before Sorbet sees it.
38
+ def const(name, type, **kwargs)
39
+ if kwargs.key?(:description)
40
+ field_descriptions[name] = kwargs.delete(:description)
41
+ end
42
+ super(name, type, **kwargs)
43
+ end
44
+
45
+ # Intercepts prop definitions to capture :description before Sorbet sees it.
46
+ def prop(name, type, **kwargs)
47
+ if kwargs.key?(:description)
48
+ field_descriptions[name] = kwargs.delete(:description)
49
+ end
50
+ super(name, type, **kwargs)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # Apply the extension to T::Struct globally
58
+ T::Struct.prepend(DSPy::Ext::StructDescriptions)
@@ -12,10 +12,33 @@ module DSPy
12
12
  extend T::Sig
13
13
  extend T::Helpers
14
14
 
15
+ # Result type that includes both schema and any accumulated definitions
16
+ class SchemaResult < T::Struct
17
+ const :schema, T::Hash[Symbol, T.untyped]
18
+ const :definitions, T::Hash[String, T::Hash[Symbol, T.untyped]], default: {}
19
+ end
20
+
21
+ # Convert a Sorbet type to JSON Schema format with definitions tracking
22
+ # Returns a SchemaResult with the schema and any $defs needed
23
+ sig { params(type: T.untyped, visited: T.nilable(T::Set[T.untyped]), definitions: T.nilable(T::Hash[String, T::Hash[Symbol, T.untyped]])).returns(SchemaResult) }
24
+ def self.type_to_json_schema_with_defs(type, visited = nil, definitions = nil)
25
+ visited ||= Set.new
26
+ definitions ||= {}
27
+ schema = type_to_json_schema_internal(type, visited, definitions)
28
+ SchemaResult.new(schema: schema, definitions: definitions)
29
+ end
30
+
15
31
  # Convert a Sorbet type to JSON Schema format
32
+ # For backward compatibility, this method returns just the schema hash
16
33
  sig { params(type: T.untyped, visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
17
34
  def self.type_to_json_schema(type, visited = nil)
18
35
  visited ||= Set.new
36
+ type_to_json_schema_internal(type, visited, {})
37
+ end
38
+
39
+ # Internal implementation that tracks definitions
40
+ sig { params(type: T.untyped, visited: T::Set[T.untyped], definitions: T::Hash[String, T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
41
+ def self.type_to_json_schema_internal(type, visited, definitions)
19
42
 
20
43
  # Handle T::Boolean type alias first
21
44
  if type == T::Boolean
@@ -24,7 +47,7 @@ module DSPy
24
47
 
25
48
  # Handle type aliases by resolving to their underlying type
26
49
  if type.is_a?(T::Private::Types::TypeAlias)
27
- return self.type_to_json_schema(type.aliased_type, visited)
50
+ return type_to_json_schema_internal(type.aliased_type, visited, definitions)
28
51
  end
29
52
 
30
53
  # Handle raw class types first
@@ -54,12 +77,13 @@ module DSPy
54
77
  # Check for recursion
55
78
  if visited.include?(type)
56
79
  # Return a reference to avoid infinite recursion
80
+ # Use #/$defs/ format for OpenAI/Gemini compatibility
81
+ simple_name = type.name.split('::').last
57
82
  {
58
- "$ref" => "#/definitions/#{type.name.split('::').last}",
59
- description: "Recursive reference to #{type.name}"
83
+ "$ref" => "#/$defs/#{simple_name}"
60
84
  }
61
85
  else
62
- self.generate_struct_schema(type, visited)
86
+ generate_struct_schema_internal(type, visited, definitions)
63
87
  end
64
88
  else
65
89
  { type: "string" } # Default fallback
@@ -93,12 +117,13 @@ module DSPy
93
117
  elsif type.raw_type < T::Struct
94
118
  # Handle custom T::Struct classes
95
119
  if visited.include?(type.raw_type)
120
+ # Use #/$defs/ format for OpenAI/Gemini compatibility
121
+ simple_name = type.raw_type.name.split('::').last
96
122
  {
97
- "$ref" => "#/definitions/#{type.raw_type.name.split('::').last}",
98
- description: "Recursive reference to #{type.raw_type.name}"
123
+ "$ref" => "#/$defs/#{simple_name}"
99
124
  }
100
125
  else
101
- generate_struct_schema(type.raw_type, visited)
126
+ generate_struct_schema_internal(type.raw_type, visited, definitions)
102
127
  end
103
128
  else
104
129
  { type: "string" } # Default fallback
@@ -108,13 +133,13 @@ module DSPy
108
133
  # Handle arrays properly with nested item type
109
134
  {
110
135
  type: "array",
111
- items: self.type_to_json_schema(type.type, visited)
136
+ items: type_to_json_schema_internal(type.type, visited, definitions)
112
137
  }
113
138
  elsif type.is_a?(T::Types::TypedHash)
114
139
  # Handle hashes as objects with additionalProperties
115
140
  # TypedHash has keys and values methods to access its key and value types
116
141
  # Note: propertyNames is NOT supported by OpenAI structured outputs, so we omit it
117
- value_schema = self.type_to_json_schema(type.values, visited)
142
+ value_schema = type_to_json_schema_internal(type.values, visited, definitions)
118
143
  key_type_desc = type.keys.respond_to?(:raw_type) ? type.keys.raw_type.to_s : "string"
119
144
  value_type_desc = value_schema[:description] || value_schema[:type].to_s
120
145
 
@@ -129,9 +154,9 @@ module DSPy
129
154
  # Handle fixed hashes (from type aliases like { "key" => Type })
130
155
  properties = {}
131
156
  required = []
132
-
157
+
133
158
  type.types.each do |key, value_type|
134
- properties[key] = self.type_to_json_schema(value_type, visited)
159
+ properties[key] = type_to_json_schema_internal(value_type, visited, definitions)
135
160
  required << key
136
161
  end
137
162
 
@@ -155,9 +180,9 @@ module DSPy
155
180
  !(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
156
181
  !(t.respond_to?(:name) && t.name == "NilClass")
157
182
  end
158
-
183
+
159
184
  if non_nil_type
160
- base_schema = self.type_to_json_schema(non_nil_type, visited)
185
+ base_schema = type_to_json_schema_internal(non_nil_type, visited, definitions)
161
186
  if base_schema[:type].is_a?(String)
162
187
  # Convert single type to array with null
163
188
  { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
@@ -173,13 +198,13 @@ module DSPy
173
198
  # Generate oneOf schema for all types
174
199
  if type.respond_to?(:types) && type.types.length > 1
175
200
  {
176
- oneOf: type.types.map { |t| self.type_to_json_schema(t, visited) },
201
+ oneOf: type.types.map { |t| type_to_json_schema_internal(t, visited, definitions) },
177
202
  description: "Union of multiple types"
178
203
  }
179
204
  else
180
205
  # Single type or fallback
181
206
  first_type = type.respond_to?(:types) ? type.types.first : type
182
- self.type_to_json_schema(first_type, visited)
207
+ type_to_json_schema_internal(first_type, visited, definitions)
183
208
  end
184
209
  end
185
210
  elsif type.is_a?(T::Types::Union)
@@ -200,7 +225,7 @@ module DSPy
200
225
 
201
226
  if non_nil_types.size == 1 && is_nilable
202
227
  # This is T.nilable(SomeType) - generate proper schema with null allowed
203
- base_schema = self.type_to_json_schema(non_nil_types.first, visited)
228
+ base_schema = type_to_json_schema_internal(non_nil_types.first, visited, definitions)
204
229
  if base_schema[:type].is_a?(String)
205
230
  # Convert single type to array with null
206
231
  { type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
@@ -210,11 +235,11 @@ module DSPy
210
235
  end
211
236
  elsif non_nil_types.size == 1
212
237
  # Non-nilable single type union (shouldn't happen in practice)
213
- self.type_to_json_schema(non_nil_types.first, visited)
238
+ type_to_json_schema_internal(non_nil_types.first, visited, definitions)
214
239
  elsif non_nil_types.size > 1
215
240
  # Handle complex unions with oneOf for better JSON schema compliance
216
241
  base_schema = {
217
- oneOf: non_nil_types.map { |t| self.type_to_json_schema(t, visited) },
242
+ oneOf: non_nil_types.map { |t| type_to_json_schema_internal(t, visited, definitions) },
218
243
  description: "Union of multiple types"
219
244
  }
220
245
  if is_nilable
@@ -237,12 +262,31 @@ module DSPy
237
262
  end
238
263
 
239
264
  # Generate JSON schema for custom T::Struct classes
265
+ # For backward compatibility, this returns just the schema hash
240
266
  sig { params(struct_class: T.class_of(T::Struct), visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
241
267
  def self.generate_struct_schema(struct_class, visited = nil)
242
268
  visited ||= Set.new
243
-
269
+ generate_struct_schema_internal(struct_class, visited, {})
270
+ end
271
+
272
+ # Generate JSON schema with $defs tracking
273
+ # Returns a SchemaResult with schema and accumulated definitions
274
+ sig { params(struct_class: T.class_of(T::Struct), visited: T.nilable(T::Set[T.untyped]), definitions: T.nilable(T::Hash[String, T::Hash[Symbol, T.untyped]])).returns(SchemaResult) }
275
+ def self.generate_struct_schema_with_defs(struct_class, visited = nil, definitions = nil)
276
+ visited ||= Set.new
277
+ definitions ||= {}
278
+ schema = generate_struct_schema_internal(struct_class, visited, definitions)
279
+ SchemaResult.new(schema: schema, definitions: definitions)
280
+ end
281
+
282
+ # Internal implementation that tracks definitions for $defs
283
+ sig { params(struct_class: T.class_of(T::Struct), visited: T::Set[T.untyped], definitions: T::Hash[String, T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
284
+ def self.generate_struct_schema_internal(struct_class, visited, definitions)
244
285
  return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
245
286
 
287
+ struct_name = struct_class.name || "Struct#{format('%x', struct_class.object_id)}"
288
+ simple_name = struct_name.split('::').last || struct_name
289
+
246
290
  # Add this struct to visited set to detect recursion
247
291
  visited.add(struct_class)
248
292
 
@@ -255,9 +299,6 @@ module DSPy
255
299
  "DSPy uses _type for automatic type detection in union types."
256
300
  end
257
301
 
258
- struct_name = struct_class.name || "Struct#{format('%x', struct_class.object_id)}"
259
- simple_name = struct_name.split('::').last || struct_name
260
-
261
302
  # Add automatic _type field for type detection
262
303
  properties[:_type] = {
263
304
  type: "string",
@@ -265,10 +306,20 @@ module DSPy
265
306
  }
266
307
  required << "_type"
267
308
 
309
+ # Get field descriptions if the struct supports them (via DSPy::Ext::StructDescriptions)
310
+ field_descs = struct_class.respond_to?(:field_descriptions) ? struct_class.field_descriptions : {}
311
+
268
312
  struct_class.props.each do |prop_name, prop_info|
269
313
  prop_type = prop_info[:type_object] || prop_info[:type]
270
- properties[prop_name] = self.type_to_json_schema(prop_type, visited)
271
-
314
+ prop_schema = type_to_json_schema_internal(prop_type, visited, definitions)
315
+
316
+ # Add field description if available
317
+ if field_descs[prop_name]
318
+ prop_schema[:description] = field_descs[prop_name]
319
+ end
320
+
321
+ properties[prop_name] = prop_schema
322
+
272
323
  # A field is required if it's not fully optional
273
324
  # fully_optional is true for nilable prop fields
274
325
  # immutable const fields are required unless nilable
@@ -280,12 +331,18 @@ module DSPy
280
331
  # Remove this struct from visited set after processing
281
332
  visited.delete(struct_class)
282
333
 
283
- {
334
+ schema = {
284
335
  type: "object",
285
336
  properties: properties,
286
337
  required: required,
287
338
  description: "#{struct_name} struct"
288
339
  }
340
+
341
+ # Add this struct's schema to definitions for $defs
342
+ # This allows recursive references to be resolved
343
+ definitions[simple_name] = schema
344
+
345
+ schema
289
346
  end
290
347
 
291
348
  private
@@ -174,6 +174,35 @@ module DSPy
174
174
  }
175
175
  end
176
176
 
177
+ # Returns output JSON schema with accumulated $defs for recursive types
178
+ # This is needed for providers like OpenAI and Gemini that require $defs at the root
179
+ sig { returns(DSPy::TypeSystem::SorbetJsonSchema::SchemaResult) }
180
+ def output_json_schema_with_defs
181
+ properties = {}
182
+ required = []
183
+ all_definitions = {}
184
+
185
+ @output_field_descriptors&.each do |name, descriptor|
186
+ result = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema_with_defs(descriptor.type, nil, all_definitions)
187
+ schema = result.schema
188
+ schema[:description] = descriptor.description if descriptor.description
189
+ properties[name] = schema
190
+ required << name.to_s unless descriptor.has_default
191
+ end
192
+
193
+ final_schema = {
194
+ "$schema": "http://json-schema.org/draft-06/schema#",
195
+ type: "object",
196
+ properties: properties,
197
+ required: required
198
+ }
199
+
200
+ DSPy::TypeSystem::SorbetJsonSchema::SchemaResult.new(
201
+ schema: final_schema,
202
+ definitions: all_definitions
203
+ )
204
+ end
205
+
177
206
  sig { returns(T.nilable(T.class_of(T::Struct))) }
178
207
  def output_schema
179
208
  @output_struct_class
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.34.0"
4
+ VERSION = "0.34.1"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -5,6 +5,9 @@ require 'dry-configurable'
5
5
  require 'dry/logger'
6
6
  require 'securerandom'
7
7
 
8
+ # Extensions to core classes (must be loaded early)
9
+ require_relative 'dspy/ext/struct_descriptions'
10
+
8
11
  require_relative 'dspy/version'
9
12
  require_relative 'dspy/errors'
10
13
  require_relative 'dspy/type_serializer'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.0
4
+ version: 0.34.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
@@ -174,6 +174,7 @@ files:
174
174
  - lib/dspy/events/subscribers.rb
175
175
  - lib/dspy/events/types.rb
176
176
  - lib/dspy/example.rb
177
+ - lib/dspy/ext/struct_descriptions.rb
177
178
  - lib/dspy/few_shot_example.rb
178
179
  - lib/dspy/field.rb
179
180
  - lib/dspy/image.rb