dspy 0.9.1 → 0.10.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 +24 -0
- data/lib/dspy/errors.rb +25 -0
- data/lib/dspy/example.rb +21 -1
- data/lib/dspy/mixins/instrumentation_helpers.rb +3 -0
- data/lib/dspy/prediction.rb +464 -0
- data/lib/dspy/signature.rb +10 -0
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +19 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b721c6c31be4c7a18cb488a7646ff356d658618a5a64ff36f50be9f731d9755a
|
4
|
+
data.tar.gz: 2e13ea6bf39e6a89872d604f22c7b197bf50924058824ff2a3483d859f1ba744
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6966d789e0ebad1e6ee6c65deaef2dec1d67d483908e7c0847df84998b4f167ad346d20de974af7194db890fcccabe38f3b38ab33dde95b430fdcd3b86a6045
|
7
|
+
data.tar.gz: 373e489777b036968ad5152aabc01141212c0409c849d89337075f1b2000de7504b8e2eaccca9755c4e52195b1d05d0667f9e4a8fe54c621cd90ba37b18837b0
|
data/README.md
CHANGED
@@ -67,6 +67,30 @@ Then run:
|
|
67
67
|
bundle install
|
68
68
|
```
|
69
69
|
|
70
|
+
#### System Dependencies for Ubuntu/Pop!_OS
|
71
|
+
|
72
|
+
If you need to compile the `polars-df` dependency from source (used for data processing in evaluations), install these system packages:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
# Update package list
|
76
|
+
sudo apt-get update
|
77
|
+
|
78
|
+
# Install Ruby development files (if not already installed)
|
79
|
+
sudo apt-get install ruby-full ruby-dev
|
80
|
+
|
81
|
+
# Install essential build tools
|
82
|
+
sudo apt-get install build-essential
|
83
|
+
|
84
|
+
# Install Rust and Cargo (required for polars-df compilation)
|
85
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
86
|
+
source $HOME/.cargo/env
|
87
|
+
|
88
|
+
# Install CMake (often needed for Rust projects)
|
89
|
+
sudo apt-get install cmake
|
90
|
+
```
|
91
|
+
|
92
|
+
**Note**: The `polars-df` gem compilation can take 15-20 minutes. Pre-built binaries are available for most platforms, so compilation is only needed if a pre-built binary isn't available for your system.
|
93
|
+
|
70
94
|
### Your First DSPy Program
|
71
95
|
|
72
96
|
```ruby
|
data/lib/dspy/errors.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DSPy
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ConfigurationError < Error
|
7
|
+
def self.missing_lm(module_name)
|
8
|
+
new(<<~MESSAGE)
|
9
|
+
No language model configured for #{module_name} module.
|
10
|
+
|
11
|
+
To fix this, configure a language model either globally:
|
12
|
+
|
13
|
+
DSPy.configure do |config|
|
14
|
+
config.lm = DSPy::LM.new("openai/gpt-4", api_key: ENV["OPENAI_API_KEY"])
|
15
|
+
end
|
16
|
+
|
17
|
+
Or on the module instance:
|
18
|
+
|
19
|
+
module_instance.configure do |config|
|
20
|
+
config.lm = DSPy::LM.new("anthropic/claude-3", api_key: ENV["ANTHROPIC_API_KEY"])
|
21
|
+
end
|
22
|
+
MESSAGE
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/dspy/example.rb
CHANGED
@@ -192,8 +192,28 @@ module DSPy
|
|
192
192
|
# String representation for debugging
|
193
193
|
sig { returns(String) }
|
194
194
|
def to_s
|
195
|
-
"DSPy::Example(#{@signature_class.name}) input=#{input_values} expected=#{expected_values}"
|
195
|
+
"DSPy::Example(#{@signature_class.name}) input=#{format_hash(input_values)} expected=#{format_hash(expected_values)}"
|
196
196
|
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
# Format hash without escaping Unicode characters
|
201
|
+
sig { params(hash: T::Hash[Symbol, T.untyped]).returns(String) }
|
202
|
+
def format_hash(hash)
|
203
|
+
pairs = hash.map do |k, v|
|
204
|
+
value_str = case v
|
205
|
+
when String
|
206
|
+
# Don't escape Unicode characters
|
207
|
+
"\"#{v}\""
|
208
|
+
else
|
209
|
+
v.inspect
|
210
|
+
end
|
211
|
+
":#{k} => #{value_str}"
|
212
|
+
end
|
213
|
+
"{#{pairs.join(", ")}}"
|
214
|
+
end
|
215
|
+
|
216
|
+
public
|
197
217
|
|
198
218
|
sig { returns(String) }
|
199
219
|
def inspect
|
@@ -15,6 +15,9 @@ module DSPy
|
|
15
15
|
# Prepares base instrumentation payload for prediction-based modules
|
16
16
|
sig { params(signature_class: T.class_of(DSPy::Signature), input_values: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
17
17
|
def prepare_base_instrumentation_payload(signature_class, input_values)
|
18
|
+
# Validate LM is configured before accessing its properties
|
19
|
+
raise DSPy::ConfigurationError.missing_lm(self.class.name) if lm.nil?
|
20
|
+
|
18
21
|
{
|
19
22
|
signature_class: signature_class.name,
|
20
23
|
model: lm.model,
|
@@ -0,0 +1,464 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module DSPy
|
5
|
+
class Prediction
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Generic
|
8
|
+
include T::Props
|
9
|
+
include T::Props::Serializable
|
10
|
+
|
11
|
+
# The underlying struct that holds the actual data
|
12
|
+
sig { returns(T.untyped) }
|
13
|
+
attr_reader :_struct
|
14
|
+
|
15
|
+
# Schema information for type conversion
|
16
|
+
sig { returns(T.nilable(T::Class[T::Struct])) }
|
17
|
+
attr_reader :_schema
|
18
|
+
|
19
|
+
sig do
|
20
|
+
params(
|
21
|
+
schema: T.nilable(T.any(T::Class[T::Struct], T::Types::Base)),
|
22
|
+
attributes: T.untyped
|
23
|
+
).void
|
24
|
+
end
|
25
|
+
def initialize(schema = nil, **attributes)
|
26
|
+
@_schema = extract_struct_class(schema)
|
27
|
+
|
28
|
+
# Convert attributes based on schema if provided
|
29
|
+
converted_attributes = if @_schema
|
30
|
+
convert_attributes_with_schema(attributes)
|
31
|
+
else
|
32
|
+
attributes
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create a dynamic struct to hold the data
|
36
|
+
struct_class = create_dynamic_struct(converted_attributes)
|
37
|
+
@_struct = struct_class.new(**converted_attributes)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Delegate all method calls to the underlying struct
|
41
|
+
sig { params(method: Symbol, args: T.untyped, block: T.untyped).returns(T.untyped) }
|
42
|
+
def method_missing(method, *args, &block)
|
43
|
+
if @_struct.respond_to?(method)
|
44
|
+
@_struct.send(method, *args, &block)
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(method: Symbol, include_all: T::Boolean).returns(T::Boolean) }
|
51
|
+
def respond_to_missing?(method, include_all = false)
|
52
|
+
@_struct.respond_to?(method, include_all) || super
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
56
|
+
def to_h
|
57
|
+
@_struct.to_h
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
sig { params(schema: T.untyped).returns(T.nilable(T::Class[T::Struct])) }
|
63
|
+
def extract_struct_class(schema)
|
64
|
+
case schema
|
65
|
+
when Class
|
66
|
+
schema if schema < T::Struct
|
67
|
+
when T::Types::Simple
|
68
|
+
schema.raw_type if schema.raw_type < T::Struct
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
75
|
+
def convert_attributes_with_schema(attributes)
|
76
|
+
return attributes unless @_schema
|
77
|
+
|
78
|
+
converted = {}
|
79
|
+
|
80
|
+
# Get discriminator mappings for T.any() fields
|
81
|
+
discriminator_mappings = detect_discriminator_fields(@_schema)
|
82
|
+
|
83
|
+
# First, add all the fields from the schema with defaults if not provided
|
84
|
+
@_schema.props.each do |field_name, prop_info|
|
85
|
+
# Skip if attribute was provided
|
86
|
+
next if attributes.key?(field_name)
|
87
|
+
|
88
|
+
# Apply default value if available
|
89
|
+
default_value = prop_info[:default]
|
90
|
+
if !default_value.nil?
|
91
|
+
if default_value.is_a?(Proc)
|
92
|
+
converted[field_name] = default_value.call
|
93
|
+
else
|
94
|
+
converted[field_name] = default_value
|
95
|
+
end
|
96
|
+
elsif prop_info[:fully_optional]
|
97
|
+
# For optional fields without defaults, set to nil
|
98
|
+
converted[field_name] = nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
attributes.each do |key, value|
|
103
|
+
prop_info = @_schema.props[key]
|
104
|
+
|
105
|
+
if prop_info && discriminator_mappings[key]
|
106
|
+
# This is a T.any() field with a discriminator
|
107
|
+
discriminator_field, type_mapping = discriminator_mappings[key]
|
108
|
+
discriminator_value = attributes[discriminator_field]
|
109
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
110
|
+
|
111
|
+
converted[key] = convert_union_type(value, discriminator_value, type_mapping, prop_type)
|
112
|
+
elsif prop_info
|
113
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
114
|
+
|
115
|
+
# Handle nil values explicitly
|
116
|
+
if value.nil?
|
117
|
+
# Check if there's a default value
|
118
|
+
default_value = prop_info[:default]
|
119
|
+
if !default_value.nil?
|
120
|
+
converted[key] = default_value.is_a?(Proc) ? default_value.call : default_value
|
121
|
+
else
|
122
|
+
converted[key] = nil
|
123
|
+
end
|
124
|
+
elsif is_enum_type?(prop_type) && value.is_a?(String)
|
125
|
+
# Convert string to enum
|
126
|
+
converted[key] = prop_type.raw_type.deserialize(value)
|
127
|
+
elsif value.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
128
|
+
# Regular struct field that needs conversion
|
129
|
+
converted[key] = convert_to_struct(value, prop_type)
|
130
|
+
elsif value.is_a?(Array) && needs_array_conversion?(prop_type)
|
131
|
+
# Array field that might contain structs
|
132
|
+
converted[key] = convert_array_elements(value, prop_type)
|
133
|
+
else
|
134
|
+
converted[key] = value
|
135
|
+
end
|
136
|
+
else
|
137
|
+
converted[key] = value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
converted
|
142
|
+
end
|
143
|
+
|
144
|
+
sig { params(schema: T::Class[T::Struct]).returns(T::Hash[Symbol, [Symbol, T::Hash[String, T.untyped]]]) }
|
145
|
+
def detect_discriminator_fields(schema)
|
146
|
+
discriminator_mappings = {}
|
147
|
+
props = schema.props.to_a
|
148
|
+
|
149
|
+
props.each_with_index do |(prop_name, prop_info), index|
|
150
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
151
|
+
next unless is_union_type?(prop_type)
|
152
|
+
|
153
|
+
# Look for preceding String or Enum field as potential discriminator
|
154
|
+
if index > 0
|
155
|
+
prev_prop_name, prev_prop_info = props[index - 1]
|
156
|
+
prev_prop_type = prev_prop_info[:type_object] || prev_prop_info[:type]
|
157
|
+
if prev_prop_type && (is_string_type?(prev_prop_type) || is_enum_type?(prev_prop_type))
|
158
|
+
# This String/Enum field might be a discriminator
|
159
|
+
type_mapping = build_type_mapping_from_union(prop_type, prev_prop_type)
|
160
|
+
discriminator_mappings[prop_name] = [prev_prop_name, type_mapping]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
discriminator_mappings
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
169
|
+
def is_union_type?(type)
|
170
|
+
type.is_a?(T::Types::Union) && !is_nilable_type?(type)
|
171
|
+
end
|
172
|
+
|
173
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
174
|
+
def is_nilable_type?(type)
|
175
|
+
type.is_a?(T::Types::Union) && type.types.any? { |t| t == T::Utils.coerce(NilClass) }
|
176
|
+
end
|
177
|
+
|
178
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
179
|
+
def is_string_type?(type)
|
180
|
+
case type
|
181
|
+
when T::Types::Simple
|
182
|
+
type.raw_type == String
|
183
|
+
else
|
184
|
+
false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
189
|
+
def is_enum_type?(type)
|
190
|
+
return false if type.nil?
|
191
|
+
return false unless type.is_a?(T::Types::Simple)
|
192
|
+
|
193
|
+
begin
|
194
|
+
raw_type = type.raw_type
|
195
|
+
return false unless raw_type.is_a?(Class)
|
196
|
+
result = raw_type < T::Enum
|
197
|
+
return result == true # Force conversion to boolean
|
198
|
+
rescue StandardError
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
sig { params(union_type: T::Types::Union, discriminator_type: T.untyped).returns(T::Hash[String, T.untyped]) }
|
204
|
+
def build_type_mapping_from_union(union_type, discriminator_type)
|
205
|
+
mapping = {}
|
206
|
+
|
207
|
+
if is_enum_type?(discriminator_type)
|
208
|
+
# For enum discriminators, try to map enum values to struct types
|
209
|
+
enum_class = discriminator_type.raw_type
|
210
|
+
union_type.types.each do |type|
|
211
|
+
next if type == T::Utils.coerce(NilClass)
|
212
|
+
|
213
|
+
if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
|
214
|
+
struct_class = type.raw_type
|
215
|
+
struct_name = struct_class.name.split("::").last
|
216
|
+
|
217
|
+
# Try to find matching enum value by name
|
218
|
+
enum_class.values.each do |enum_value|
|
219
|
+
enum_name = enum_value.instance_variable_get(:@const_name).to_s
|
220
|
+
if enum_name == struct_name
|
221
|
+
# Exact match
|
222
|
+
mapping[enum_value.serialize] = struct_class
|
223
|
+
elsif enum_name.downcase == struct_name.downcase
|
224
|
+
# Case-insensitive match
|
225
|
+
mapping[enum_value.serialize] = struct_class
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Also add snake_case mapping as fallback
|
230
|
+
discriminator_value = struct_name
|
231
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
232
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
233
|
+
.downcase
|
234
|
+
mapping[discriminator_value] = struct_class
|
235
|
+
end
|
236
|
+
end
|
237
|
+
else
|
238
|
+
# String discriminators use snake_case convention
|
239
|
+
union_type.types.each do |type|
|
240
|
+
next if type == T::Utils.coerce(NilClass)
|
241
|
+
|
242
|
+
if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
|
243
|
+
struct_class = type.raw_type
|
244
|
+
# Convert class name to snake_case for discriminator value
|
245
|
+
discriminator_value = struct_class.name
|
246
|
+
.split("::").last
|
247
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
248
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
249
|
+
.downcase
|
250
|
+
|
251
|
+
mapping[discriminator_value] = struct_class
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
mapping
|
257
|
+
end
|
258
|
+
|
259
|
+
sig do
|
260
|
+
params(
|
261
|
+
value: T.untyped,
|
262
|
+
discriminator_value: T.untyped,
|
263
|
+
type_mapping: T::Hash[String, T.untyped],
|
264
|
+
union_type: T.untyped
|
265
|
+
).returns(T.untyped)
|
266
|
+
end
|
267
|
+
def convert_union_type(value, discriminator_value, type_mapping, union_type)
|
268
|
+
return value unless value.is_a?(Hash)
|
269
|
+
|
270
|
+
# Handle enum discriminators
|
271
|
+
discriminator_str = case discriminator_value
|
272
|
+
when T::Enum
|
273
|
+
discriminator_value.serialize
|
274
|
+
when String
|
275
|
+
discriminator_value
|
276
|
+
else
|
277
|
+
return value
|
278
|
+
end
|
279
|
+
|
280
|
+
struct_class = type_mapping[discriminator_str]
|
281
|
+
return value unless struct_class
|
282
|
+
|
283
|
+
# Convert the Hash to the appropriate struct type
|
284
|
+
struct_class.new(**value)
|
285
|
+
rescue TypeError, ArgumentError
|
286
|
+
# If conversion fails, return the original value
|
287
|
+
value
|
288
|
+
end
|
289
|
+
|
290
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
291
|
+
def needs_struct_conversion?(type)
|
292
|
+
case type
|
293
|
+
when T::Types::Simple
|
294
|
+
type.raw_type < T::Struct
|
295
|
+
when T::Types::Union
|
296
|
+
# Check if any type in the union is a struct
|
297
|
+
type.types.any? { |t| needs_struct_conversion?(t) }
|
298
|
+
else
|
299
|
+
false
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
sig { params(value: T::Hash[Symbol, T.untyped], type: T.untyped).returns(T.untyped) }
|
304
|
+
def convert_to_struct(value, type)
|
305
|
+
case type
|
306
|
+
when T::Types::Simple
|
307
|
+
struct_class = type.raw_type
|
308
|
+
# Convert nested hash values to structs if needed
|
309
|
+
converted_hash = {}
|
310
|
+
|
311
|
+
# First, apply defaults for missing fields
|
312
|
+
struct_class.props.each do |field_name, prop_info|
|
313
|
+
next if value.key?(field_name)
|
314
|
+
|
315
|
+
default_value = prop_info[:default]
|
316
|
+
if !default_value.nil?
|
317
|
+
converted_hash[field_name] = default_value.is_a?(Proc) ? default_value.call : default_value
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
value.each do |k, v|
|
322
|
+
prop_info = struct_class.props[k]
|
323
|
+
if prop_info
|
324
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
325
|
+
if v.is_a?(String) && is_enum_type?(prop_type)
|
326
|
+
# Convert string to enum
|
327
|
+
converted_hash[k] = prop_type.raw_type.deserialize(v)
|
328
|
+
elsif v.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
329
|
+
converted_hash[k] = convert_to_struct(v, prop_type)
|
330
|
+
elsif v.is_a?(Array) && needs_array_conversion?(prop_type)
|
331
|
+
converted_hash[k] = convert_array_elements(v, prop_type)
|
332
|
+
else
|
333
|
+
converted_hash[k] = v
|
334
|
+
end
|
335
|
+
else
|
336
|
+
converted_hash[k] = v
|
337
|
+
end
|
338
|
+
end
|
339
|
+
begin
|
340
|
+
struct_class.new(**converted_hash)
|
341
|
+
rescue => e
|
342
|
+
# Return original value if conversion fails
|
343
|
+
value
|
344
|
+
end
|
345
|
+
when T::Types::Union
|
346
|
+
# For unions without discriminator, try each type
|
347
|
+
type.types.each do |t|
|
348
|
+
next if t == T::Utils.coerce(NilClass)
|
349
|
+
|
350
|
+
begin
|
351
|
+
return convert_to_struct(value, t) if needs_struct_conversion?(t)
|
352
|
+
rescue TypeError, ArgumentError
|
353
|
+
# Try next type
|
354
|
+
end
|
355
|
+
end
|
356
|
+
value
|
357
|
+
else
|
358
|
+
value
|
359
|
+
end
|
360
|
+
rescue TypeError, ArgumentError
|
361
|
+
value
|
362
|
+
end
|
363
|
+
|
364
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
365
|
+
def needs_array_conversion?(type)
|
366
|
+
case type
|
367
|
+
when T::Types::TypedArray
|
368
|
+
needs_struct_conversion?(type.type)
|
369
|
+
else
|
370
|
+
false
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
sig { params(array: T::Array[T.untyped], type: T.untyped).returns(T::Array[T.untyped]) }
|
375
|
+
def convert_array_elements(array, type)
|
376
|
+
return array unless type.is_a?(T::Types::TypedArray)
|
377
|
+
|
378
|
+
element_type = type.type
|
379
|
+
# Check if elements need any conversion (structs or enums)
|
380
|
+
return array unless needs_struct_conversion?(element_type) || is_enum_type?(element_type)
|
381
|
+
|
382
|
+
array.map do |element|
|
383
|
+
if element.is_a?(Hash)
|
384
|
+
# For union types, we need to infer which struct type based on the hash structure
|
385
|
+
if is_union_type?(element_type) && !is_nilable_type?(element_type)
|
386
|
+
convert_hash_to_union_struct(element, element_type)
|
387
|
+
else
|
388
|
+
convert_to_struct(element, element_type)
|
389
|
+
end
|
390
|
+
elsif element.is_a?(String) && is_enum_type?(element_type)
|
391
|
+
# Convert string to enum
|
392
|
+
element_type.raw_type.deserialize(element)
|
393
|
+
else
|
394
|
+
element
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
sig { params(hash: T::Hash[Symbol, T.untyped], union_type: T::Types::Union).returns(T.untyped) }
|
400
|
+
def convert_hash_to_union_struct(hash, union_type)
|
401
|
+
# Try to match the hash structure to one of the union types
|
402
|
+
union_type.types.each do |type|
|
403
|
+
next if type == T::Utils.coerce(NilClass)
|
404
|
+
|
405
|
+
if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
|
406
|
+
struct_class = type.raw_type
|
407
|
+
|
408
|
+
# Check if all required fields of this struct are present in the hash
|
409
|
+
required_fields = struct_class.props.reject { |_, info| info[:fully_optional] }.keys
|
410
|
+
if required_fields.all? { |field| hash.key?(field) }
|
411
|
+
begin
|
412
|
+
# Need to convert nested values too
|
413
|
+
converted_hash = {}
|
414
|
+
hash.each do |k, v|
|
415
|
+
prop_info = struct_class.props[k]
|
416
|
+
if prop_info
|
417
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
418
|
+
if v.is_a?(String) && is_enum_type?(prop_type)
|
419
|
+
converted_hash[k] = prop_type.raw_type.deserialize(v)
|
420
|
+
elsif v.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
421
|
+
converted_hash[k] = convert_to_struct(v, prop_type)
|
422
|
+
elsif v.is_a?(Array) && needs_array_conversion?(prop_type)
|
423
|
+
converted_hash[k] = convert_array_elements(v, prop_type)
|
424
|
+
else
|
425
|
+
converted_hash[k] = v
|
426
|
+
end
|
427
|
+
else
|
428
|
+
converted_hash[k] = v
|
429
|
+
end
|
430
|
+
end
|
431
|
+
return struct_class.new(**converted_hash)
|
432
|
+
rescue TypeError, ArgumentError
|
433
|
+
# This struct didn't match, try the next one
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# If no struct matched, return the original hash
|
440
|
+
hash
|
441
|
+
end
|
442
|
+
|
443
|
+
sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T::Class[T::Struct]) }
|
444
|
+
def create_dynamic_struct(attributes)
|
445
|
+
# If we have a schema, include all fields from it in the dynamic struct
|
446
|
+
all_fields = if @_schema
|
447
|
+
# Merge schema fields with provided attributes
|
448
|
+
schema_fields = @_schema.props.keys.to_h { |k| [k, nil] }
|
449
|
+
schema_fields.merge(attributes)
|
450
|
+
else
|
451
|
+
attributes
|
452
|
+
end
|
453
|
+
|
454
|
+
Class.new(T::Struct) do
|
455
|
+
const :_prediction_marker, T::Boolean, default: true
|
456
|
+
|
457
|
+
all_fields.each do |key, value|
|
458
|
+
# Use T.untyped for dynamic properties
|
459
|
+
const key, T.untyped
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
data/lib/dspy/signature.rb
CHANGED
@@ -147,6 +147,11 @@ module DSPy
|
|
147
147
|
}
|
148
148
|
end
|
149
149
|
|
150
|
+
sig { returns(T.nilable(T.class_of(T::Struct))) }
|
151
|
+
def input_schema
|
152
|
+
@input_struct_class
|
153
|
+
end
|
154
|
+
|
150
155
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
151
156
|
def output_json_schema
|
152
157
|
return {} unless @output_struct_class
|
@@ -169,6 +174,11 @@ module DSPy
|
|
169
174
|
}
|
170
175
|
end
|
171
176
|
|
177
|
+
sig { returns(T.nilable(T.class_of(T::Struct))) }
|
178
|
+
def output_schema
|
179
|
+
@output_struct_class
|
180
|
+
end
|
181
|
+
|
172
182
|
private
|
173
183
|
|
174
184
|
sig { params(type: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
data/lib/dspy/version.rb
CHANGED
data/lib/dspy.rb
CHANGED
@@ -5,6 +5,7 @@ require 'dry/logger'
|
|
5
5
|
require 'securerandom'
|
6
6
|
|
7
7
|
require_relative 'dspy/version'
|
8
|
+
require_relative 'dspy/errors'
|
8
9
|
|
9
10
|
module DSPy
|
10
11
|
extend Dry::Configurable
|
@@ -123,6 +124,7 @@ require_relative 'dspy/prompt'
|
|
123
124
|
require_relative 'dspy/example'
|
124
125
|
require_relative 'dspy/lm'
|
125
126
|
require_relative 'dspy/strategy'
|
127
|
+
require_relative 'dspy/prediction'
|
126
128
|
require_relative 'dspy/predict'
|
127
129
|
require_relative 'dspy/chain_of_thought'
|
128
130
|
require_relative 'dspy/re_act'
|
@@ -144,3 +146,20 @@ require_relative 'dspy/registry/signature_registry'
|
|
144
146
|
require_relative 'dspy/registry/registry_manager'
|
145
147
|
|
146
148
|
# LoggerSubscriber will be lazy-initialized when first accessed
|
149
|
+
|
150
|
+
# Detect potential gem conflicts and warn users
|
151
|
+
# DSPy uses the official openai gem, warn if ruby-openai (community version) is detected
|
152
|
+
if defined?(OpenAI) && defined?(OpenAI::Client) && !defined?(OpenAI::Internal)
|
153
|
+
warn <<~WARNING
|
154
|
+
WARNING: ruby-openai gem detected. This may cause conflicts with DSPy's OpenAI integration.
|
155
|
+
|
156
|
+
DSPy uses the official 'openai' gem. The community 'ruby-openai' gem uses the same
|
157
|
+
OpenAI namespace and will cause conflicts.
|
158
|
+
|
159
|
+
To fix this, remove 'ruby-openai' from your Gemfile and use the official gem instead:
|
160
|
+
- Remove: gem 'ruby-openai'
|
161
|
+
- Keep: gem 'openai' (official SDK that DSPy uses)
|
162
|
+
|
163
|
+
The official gem provides better compatibility and is actively maintained by OpenAI.
|
164
|
+
WARNING
|
165
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dspy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vicente Reig Rincón de Arellano
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-20 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: dry-configurable
|
@@ -161,6 +162,7 @@ files:
|
|
161
162
|
- lib/dspy.rb
|
162
163
|
- lib/dspy/chain_of_thought.rb
|
163
164
|
- lib/dspy/code_act.rb
|
165
|
+
- lib/dspy/errors.rb
|
164
166
|
- lib/dspy/evaluate.rb
|
165
167
|
- lib/dspy/example.rb
|
166
168
|
- lib/dspy/few_shot_example.rb
|
@@ -196,6 +198,7 @@ files:
|
|
196
198
|
- lib/dspy/mixins/type_coercion.rb
|
197
199
|
- lib/dspy/module.rb
|
198
200
|
- lib/dspy/predict.rb
|
201
|
+
- lib/dspy/prediction.rb
|
199
202
|
- lib/dspy/prompt.rb
|
200
203
|
- lib/dspy/propose/grounded_proposer.rb
|
201
204
|
- lib/dspy/re_act.rb
|
@@ -225,6 +228,7 @@ homepage: https://github.com/vicentereig/dspy.rb
|
|
225
228
|
licenses:
|
226
229
|
- MIT
|
227
230
|
metadata: {}
|
231
|
+
post_install_message:
|
228
232
|
rdoc_options: []
|
229
233
|
require_paths:
|
230
234
|
- lib
|
@@ -239,7 +243,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
239
243
|
- !ruby/object:Gem::Version
|
240
244
|
version: '0'
|
241
245
|
requirements: []
|
242
|
-
rubygems_version: 3.
|
246
|
+
rubygems_version: 3.5.22
|
247
|
+
signing_key:
|
243
248
|
specification_version: 4
|
244
249
|
summary: Ruby port of DSPy 2.6
|
245
250
|
test_files: []
|