dspy 0.9.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 306fa376ea024edd2fc75d7bc1af6a42e60770e9566b4dd114414a40c685f2a7
4
- data.tar.gz: 74f321fdd09a5761d17481af846dcd85dc3a372f573f79659c4fcfc761e92a18
3
+ metadata.gz: 7efcb1d5900858477fe538790a6d132a8e289260bb326cee5f30316358fa80f0
4
+ data.tar.gz: c4116d5f2e01e34e23b6bd3baefd5f7480af214284410c77255d4f37fc099743
5
5
  SHA512:
6
- metadata.gz: 5efb12df9c365114696857067bb4f869ce2329c0977666331846a5f070aca050312992c176fddb60ab701b5d31b017d23182d7a4e813e80cba152d155fc2091c
7
- data.tar.gz: 2fd7f831b6fab1624d2de7702d54d6377a4a96fff4cb0230eb810285828b764033288bc3180e2aa2cf92b1a1026fa6552cdbf851174216a3fc1a651419accc59
6
+ metadata.gz: 487725527cfbe3b91d5694999aecdd6fe916940bb9ed42c2a222346b0f1b09ec7b9cb0bdf0f6509a542d72a07bfd4dab66dc879b6bf76d2dc1ebb7e61064dfa7
7
+ data.tar.gz: d3674ea0fe89d498d31b5be92e1a24458bfec0c79f55aef061869cf3a7014f2b5ab67ffac24226c6783f3ea0e485e16bdc16a1dafeef906f602241d8fd7077b7
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/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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -123,6 +123,7 @@ require_relative 'dspy/prompt'
123
123
  require_relative 'dspy/example'
124
124
  require_relative 'dspy/lm'
125
125
  require_relative 'dspy/strategy'
126
+ require_relative 'dspy/prediction'
126
127
  require_relative 'dspy/predict'
127
128
  require_relative 'dspy/chain_of_thought'
128
129
  require_relative 'dspy/re_act'
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.9.1
4
+ version: 0.10.0
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-16 00:00:00.000000000 Z
11
+ date: 2025-07-20 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: dry-configurable
@@ -196,6 +197,7 @@ files:
196
197
  - lib/dspy/mixins/type_coercion.rb
197
198
  - lib/dspy/module.rb
198
199
  - lib/dspy/predict.rb
200
+ - lib/dspy/prediction.rb
199
201
  - lib/dspy/prompt.rb
200
202
  - lib/dspy/propose/grounded_proposer.rb
201
203
  - lib/dspy/re_act.rb
@@ -225,6 +227,7 @@ homepage: https://github.com/vicentereig/dspy.rb
225
227
  licenses:
226
228
  - MIT
227
229
  metadata: {}
230
+ post_install_message:
228
231
  rdoc_options: []
229
232
  require_paths:
230
233
  - lib
@@ -239,7 +242,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
239
242
  - !ruby/object:Gem::Version
240
243
  version: '0'
241
244
  requirements: []
242
- rubygems_version: 3.6.5
245
+ rubygems_version: 3.5.22
246
+ signing_key:
243
247
  specification_version: 4
244
248
  summary: Ruby port of DSPy 2.6
245
249
  test_files: []