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 +4 -4
- data/lib/dspy/ext/struct_descriptions.rb +58 -0
- data/lib/dspy/schema/sorbet_json_schema.rb +82 -25
- data/lib/dspy/signature.rb +29 -0
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 100e82f4aeff8020a845aa80a63ad86278ead32f34b7846b0624db99dc060325
|
|
4
|
+
data.tar.gz: 43ed18798e67e829e2decdd8ef0519751d6f4fc2cf52571850f1acfd671f2780
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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" => "
|
|
59
|
-
description: "Recursive reference to #{type.name}"
|
|
83
|
+
"$ref" => "#/$defs/#{simple_name}"
|
|
60
84
|
}
|
|
61
85
|
else
|
|
62
|
-
|
|
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" => "
|
|
98
|
-
description: "Recursive reference to #{type.raw_type.name}"
|
|
123
|
+
"$ref" => "#/$defs/#{simple_name}"
|
|
99
124
|
}
|
|
100
125
|
else
|
|
101
|
-
|
|
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:
|
|
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 =
|
|
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] =
|
|
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 =
|
|
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|
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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|
|
|
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
|
-
|
|
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
|
data/lib/dspy/signature.rb
CHANGED
|
@@ -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
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.
|
|
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
|