dspy 0.10.0 → 0.11.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: 7efcb1d5900858477fe538790a6d132a8e289260bb326cee5f30316358fa80f0
4
- data.tar.gz: c4116d5f2e01e34e23b6bd3baefd5f7480af214284410c77255d4f37fc099743
3
+ metadata.gz: de6126089636bd0d5fdaf9cd2bba791157186683a7984174bac980be8e32a483
4
+ data.tar.gz: 1e78c20f7e1a37cf025158be0f4014ee2abe9ba95b84682361ff9ea544fecd2c
5
5
  SHA512:
6
- metadata.gz: 487725527cfbe3b91d5694999aecdd6fe916940bb9ed42c2a222346b0f1b09ec7b9cb0bdf0f6509a542d72a07bfd4dab66dc879b6bf76d2dc1ebb7e61064dfa7
7
- data.tar.gz: d3674ea0fe89d498d31b5be92e1a24458bfec0c79f55aef061869cf3a7014f2b5ab67ffac24226c6783f3ea0e485e16bdc16a1dafeef906f602241d8fd7077b7
6
+ metadata.gz: 7b3dbde7e5040dc1b0562142bdc560f9fc393bbb7cf20bef4d767541be079408dc1c6e196257f07193266713585bdfb21ac77bd08df3c6ab69607354393987c2
7
+ data.tar.gz: 412cf071635f85bb3fb08f339a1dce23d4a9f00144104fc03fc25cc706d87c86c707221bc8a98f6847a7e157e2fa86de8bee4ee7c44d2bd1b9b0ab41934dd411
data/README.md CHANGED
@@ -43,7 +43,7 @@ The result? LLM applications that actually scale and don't break when you sneeze
43
43
 
44
44
  ## Development Status
45
45
 
46
- DSPy.rb is actively developed and approaching stability at **v0.9.0**. The core framework is production-ready with comprehensive documentation, but I'm battle-testing features through the 0.x series before committing to a stable v1.0 API.
46
+ DSPy.rb is actively developed and approaching stability at **v0.10.1**. The core framework is production-ready with comprehensive documentation, but I'm battle-testing features through the 0.x series before committing to a stable v1.0 API.
47
47
 
48
48
  Real-world usage feedback is invaluable - if you encounter issues or have suggestions, please open a GitHub issue!
49
49
 
@@ -174,7 +174,7 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
174
174
 
175
175
  ## Roadmap - Battle-Testing Toward v1.0
176
176
 
177
- DSPy.rb is currently at **v0.9.0** and approaching stability. I'm focusing on real-world usage and refinement through the 0.10, 0.11, 0.12+ series before committing to a stable v1.0 API.
177
+ DSPy.rb is currently at **v0.10.1** and approaching stability. I'm focusing on real-world usage and refinement through the 0.11, 0.12+ series before committing to a stable v1.0 API.
178
178
 
179
179
  **Current Focus Areas:**
180
180
  - 🚧 **Ollama Support** - Local model integration
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSPy
4
+ class Error < StandardError; end
5
+
6
+ class ValidationError < Error; end
7
+
8
+ class DeserializationError < Error; end
9
+
10
+ class ConfigurationError < Error
11
+ def self.missing_lm(module_name)
12
+ new(<<~MESSAGE)
13
+ No language model configured for #{module_name} module.
14
+
15
+ To fix this, configure a language model either globally:
16
+
17
+ DSPy.configure do |config|
18
+ config.lm = DSPy::LM.new("openai/gpt-4", api_key: ENV["OPENAI_API_KEY"])
19
+ end
20
+
21
+ Or on the module instance:
22
+
23
+ module_instance.configure do |config|
24
+ config.lm = DSPy::LM.new("anthropic/claude-3", api_key: ENV["ANTHROPIC_API_KEY"])
25
+ end
26
+ MESSAGE
27
+ end
28
+ end
29
+ end
data/lib/dspy/example.rb CHANGED
@@ -77,6 +77,17 @@ module DSPy
77
77
  expected_hash
78
78
  end
79
79
 
80
+ # Custom equality comparison
81
+ sig { params(other: T.untyped).returns(T::Boolean) }
82
+ def ==(other)
83
+ return false unless other.is_a?(Example)
84
+
85
+ @signature_class == other.signature_class &&
86
+ input_values == other.input_values &&
87
+ expected_values == other.expected_values
88
+ end
89
+
90
+
80
91
  # Check if prediction matches expected output using struct comparison
81
92
  sig { params(prediction: T.untyped).returns(T::Boolean) }
82
93
  def matches_prediction?(prediction)
@@ -179,15 +190,6 @@ module DSPy
179
190
  examples
180
191
  end
181
192
 
182
- # Equality comparison
183
- sig { params(other: T.untyped).returns(T::Boolean) }
184
- def ==(other)
185
- return false unless other.is_a?(Example)
186
-
187
- @signature_class == other.signature_class &&
188
- input_values == other.input_values &&
189
- expected_values == other.expected_values
190
- end
191
193
 
192
194
  # String representation for debugging
193
195
  sig { returns(String) }
@@ -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,
@@ -319,6 +319,9 @@ module DSPy
319
319
  end
320
320
 
321
321
  value.each do |k, v|
322
+ # Skip _type field from being added to the struct (it's not a real field)
323
+ next if k == :_type || k == "_type"
324
+
322
325
  prop_info = struct_class.props[k]
323
326
  if prop_info
324
327
  prop_type = prop_info[:type_object] || prop_info[:type]
@@ -343,7 +346,27 @@ module DSPy
343
346
  value
344
347
  end
345
348
  when T::Types::Union
346
- # For unions without discriminator, try each type
349
+ # Check if value has a _type field for automatic type detection
350
+ type_name = value[:_type] || value["_type"]
351
+
352
+ if type_name
353
+ # Use _type field to determine which struct to instantiate
354
+ type.types.each do |t|
355
+ next if t == T::Utils.coerce(NilClass)
356
+
357
+ if t.is_a?(T::Types::Simple) && t.raw_type < T::Struct
358
+ struct_name = t.raw_type.name.split('::').last
359
+ if struct_name == type_name
360
+ return convert_to_struct(value, t)
361
+ end
362
+ end
363
+ end
364
+
365
+ # If no matching type found, raise an error
366
+ raise DSPy::DeserializationError, "Unknown type: #{type_name}. Expected one of: #{type.types.map { |t| t.is_a?(T::Types::Simple) && t.raw_type < T::Struct ? t.raw_type.name.split('::').last : nil }.compact.join(', ')}"
367
+ end
368
+
369
+ # Fallback to trying each type if no _type field
347
370
  type.types.each do |t|
348
371
  next if t == T::Utils.coerce(NilClass)
349
372
 
@@ -398,7 +421,27 @@ module DSPy
398
421
 
399
422
  sig { params(hash: T::Hash[Symbol, T.untyped], union_type: T::Types::Union).returns(T.untyped) }
400
423
  def convert_hash_to_union_struct(hash, union_type)
401
- # Try to match the hash structure to one of the union types
424
+ # First check if hash has a _type field for automatic type detection
425
+ type_name = hash[:_type] || hash["_type"]
426
+
427
+ if type_name
428
+ # Use _type field to determine which struct to instantiate
429
+ union_type.types.each do |type|
430
+ next if type == T::Utils.coerce(NilClass)
431
+
432
+ if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
433
+ struct_name = type.raw_type.name.split('::').last
434
+ if struct_name == type_name
435
+ return convert_to_struct(hash, type)
436
+ end
437
+ end
438
+ end
439
+
440
+ # If no matching type found, raise an error
441
+ raise DSPy::DeserializationError, "Unknown type: #{type_name}. Expected one of: #{union_type.types.map { |t| t.is_a?(T::Types::Simple) && t.raw_type < T::Struct ? t.raw_type.name.split('::').last : nil }.compact.join(', ')}"
442
+ end
443
+
444
+ # Fallback: Try to match the hash structure to one of the union types
402
445
  union_type.types.each do |type|
403
446
  next if type == T::Utils.coerce(NilClass)
404
447
 
@@ -412,6 +455,9 @@ module DSPy
412
455
  # Need to convert nested values too
413
456
  converted_hash = {}
414
457
  hash.each do |k, v|
458
+ # Skip _type field
459
+ next if k == :_type || k == "_type"
460
+
415
461
  prop_info = struct_class.props[k]
416
462
  if prop_info
417
463
  prop_type = prop_info[:type_object] || prop_info[:type]
data/lib/dspy/re_act.rb CHANGED
@@ -256,7 +256,7 @@ module DSPy
256
256
  }) do
257
257
  # Generate thought and action
258
258
  thought_obj = @thought_generator.forward(
259
- input_context: input_struct.serialize.to_json,
259
+ input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
260
260
  history: history,
261
261
  available_tools: available_tools_desc
262
262
  )
@@ -385,7 +385,7 @@ module DSPy
385
385
  return { should_finish: false } if observation.include?("Unknown action")
386
386
 
387
387
  observation_result = @observation_processor.forward(
388
- input_context: input_struct.serialize.to_json,
388
+ input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
389
389
  history: history,
390
390
  observation: observation
391
391
  )
@@ -402,7 +402,7 @@ module DSPy
402
402
  sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], observation_result: T.untyped, iteration: Integer).returns(String) }
403
403
  def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
404
404
  final_thought = @thought_generator.forward(
405
- input_context: input_struct.serialize.to_json,
405
+ input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
406
406
  history: history,
407
407
  available_tools: available_tools_desc
408
408
  )
@@ -292,6 +292,19 @@ module DSPy
292
292
  properties = {}
293
293
  required = []
294
294
 
295
+ # Check if struct already has a _type field
296
+ if struct_class.props.key?(:_type)
297
+ raise DSPy::ValidationError, "_type field conflict: #{struct_class.name} already has a _type field defined. " \
298
+ "DSPy uses _type for automatic type detection in union types."
299
+ end
300
+
301
+ # Add automatic _type field for type detection
302
+ properties[:_type] = {
303
+ type: "string",
304
+ const: struct_class.name.split('::').last # Use the simple class name
305
+ }
306
+ required << "_type"
307
+
295
308
  struct_class.props.each do |prop_name, prop_info|
296
309
  prop_type = prop_info[:type_object] || prop_info[:type]
297
310
  properties[prop_name] = type_to_json_schema(prop_type)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+
5
+ module DSPy
6
+ class TypeSerializer
7
+ extend T::Sig
8
+
9
+ # Serialize a value, injecting _type fields for T::Struct instances
10
+ sig { params(value: T.untyped).returns(T.untyped) }
11
+ def self.serialize(value)
12
+ case value
13
+ when T::Struct
14
+ serialize_struct(value)
15
+ when Array
16
+ value.map { |item| serialize(item) }
17
+ when Hash
18
+ value.transform_values { |v| serialize(v) }
19
+ else
20
+ value
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ sig { params(struct: T::Struct).returns(T::Hash[String, T.untyped]) }
27
+ def self.serialize_struct(struct)
28
+ # Handle anonymous structs that don't have a name
29
+ class_name = struct.class.name
30
+ type_name = if class_name.nil? || class_name.empty?
31
+ # For anonymous structs, use a generic identifier
32
+ "AnonymousStruct"
33
+ else
34
+ class_name.split('::').last
35
+ end
36
+
37
+ result = {
38
+ "_type" => type_name
39
+ }
40
+
41
+ # Get all props and serialize their values
42
+ struct.class.props.each do |prop_name, prop_info|
43
+ prop_value = struct.send(prop_name)
44
+
45
+ # Skip nil values for optional fields
46
+ next if prop_value.nil? && prop_info[:fully_optional]
47
+
48
+ # Recursively serialize nested values
49
+ result[prop_name.to_s] = serialize(prop_value)
50
+ end
51
+
52
+ result
53
+ end
54
+ end
55
+ end
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.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -5,6 +5,8 @@ require 'dry/logger'
5
5
  require 'securerandom'
6
6
 
7
7
  require_relative 'dspy/version'
8
+ require_relative 'dspy/errors'
9
+ require_relative 'dspy/type_serializer'
8
10
 
9
11
  module DSPy
10
12
  extend Dry::Configurable
@@ -145,3 +147,20 @@ require_relative 'dspy/registry/signature_registry'
145
147
  require_relative 'dspy/registry/registry_manager'
146
148
 
147
149
  # LoggerSubscriber will be lazy-initialized when first accessed
150
+
151
+ # Detect potential gem conflicts and warn users
152
+ # DSPy uses the official openai gem, warn if ruby-openai (community version) is detected
153
+ if defined?(OpenAI) && defined?(OpenAI::Client) && !defined?(OpenAI::Internal)
154
+ warn <<~WARNING
155
+ WARNING: ruby-openai gem detected. This may cause conflicts with DSPy's OpenAI integration.
156
+
157
+ DSPy uses the official 'openai' gem. The community 'ruby-openai' gem uses the same
158
+ OpenAI namespace and will cause conflicts.
159
+
160
+ To fix this, remove 'ruby-openai' from your Gemfile and use the official gem instead:
161
+ - Remove: gem 'ruby-openai'
162
+ - Keep: gem 'openai' (official SDK that DSPy uses)
163
+
164
+ The official gem provides better compatibility and is actively maintained by OpenAI.
165
+ WARNING
166
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-20 00:00:00.000000000 Z
11
+ date: 2025-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-configurable
@@ -162,6 +162,7 @@ files:
162
162
  - lib/dspy.rb
163
163
  - lib/dspy/chain_of_thought.rb
164
164
  - lib/dspy/code_act.rb
165
+ - lib/dspy/errors.rb
165
166
  - lib/dspy/evaluate.rb
166
167
  - lib/dspy/example.rb
167
168
  - lib/dspy/few_shot_example.rb
@@ -222,6 +223,7 @@ files:
222
223
  - lib/dspy/tools/memory_toolset.rb
223
224
  - lib/dspy/tools/text_processing_toolset.rb
224
225
  - lib/dspy/tools/toolset.rb
226
+ - lib/dspy/type_serializer.rb
225
227
  - lib/dspy/version.rb
226
228
  homepage: https://github.com/vicentereig/dspy.rb
227
229
  licenses: