dspy 0.18.0 → 0.19.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: 63f9d02616c10429f0d409e08036fbf25e2557ba02d48785271ffd7b90aec464
4
- data.tar.gz: 46eb23dd11cc7e81ec2c58ee13beca5d36cbfc8c221ec1ae1b70b59cf3e56f36
3
+ metadata.gz: '080e190ff2991ff97869bad95117f30a9e17a79b264285e1f122b1de6df4bbaf'
4
+ data.tar.gz: d3b47ff7e835a22111466e9c3e9a3be445e0a39c77a18c109482814c2804104e
5
5
  SHA512:
6
- metadata.gz: d53d4a72482146cf692304f82a0c273ebc06925dc66042d461e600e77fa99f79fda88f67282b5497a4625275d7311b1d10ea255814085883e15a323a7c4d56e9
7
- data.tar.gz: b2f63c03a162101345589732a192267a55464c7ebed524274a175b66ee872501ec36386d4fd7995005d4726fa0181bb82a9d48b32b09ed722e0662e39fd6958b
6
+ metadata.gz: 2aa3f3222b7e8c2039003e7acf724f32c9c5b5ad45d226fd93e74d2ca484a1be5332be96058b30ff1a5509e702ad4321dd9e1194b273820c897492277356efcc
7
+ data.tar.gz: 592266d28de856aad7da0481b62bf3ad9a6ce7ac06013ea1c8c980b1dc964267a3d85a930db8945a516f1265a0c0a38dbbf27206d1243eda7cf4a978468f0754
@@ -105,6 +105,8 @@ module DSPy
105
105
  # Creates signature class with enhanced description and reasoning field
106
106
  sig { params(signature_class: T.class_of(DSPy::Signature), enhanced_output_struct: T.class_of(T::Struct)).returns(T.class_of(DSPy::Signature)) }
107
107
  def create_signature_class(signature_class, enhanced_output_struct)
108
+ original_name = signature_class.name
109
+
108
110
  Class.new(DSPy::Signature) do
109
111
  description "#{signature_class.description} Think step by step."
110
112
 
@@ -125,8 +127,16 @@ module DSPy
125
127
  # Add reasoning field descriptor (ChainOfThought always provides this)
126
128
  @output_field_descriptors[:reasoning] = FieldDescriptor.new(String, "Step by step reasoning process")
127
129
 
130
+ # Store the original signature name for tracking/logging
131
+ @original_signature_name = original_name
132
+
128
133
  class << self
129
- attr_reader :input_struct_class, :output_struct_class
134
+ attr_reader :input_struct_class, :output_struct_class, :original_signature_name
135
+
136
+ # Override name to return the original signature name for tracking
137
+ def name
138
+ @original_signature_name || super
139
+ end
130
140
  end
131
141
  end
132
142
  end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ # Utility class for formatting complex error messages into human-readable format
7
+ class ErrorFormatter
8
+ extend T::Sig
9
+
10
+ # Main entry point for formatting errors
11
+ sig { params(error_message: String, context: T.nilable(String)).returns(String) }
12
+ def self.format_error(error_message, context = nil)
13
+ # Try different error patterns in order of specificity
14
+ if sorbet_type_error?(error_message)
15
+ format_sorbet_type_error(error_message)
16
+ elsif argument_error?(error_message)
17
+ format_argument_error(error_message)
18
+ else
19
+ # Fallback to original message with minor cleanup
20
+ clean_error_message(error_message)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # Check if this is a Sorbet runtime type validation error
27
+ sig { params(message: String).returns(T::Boolean) }
28
+ def self.sorbet_type_error?(message)
29
+ message.match?(/Can't set \.(\w+) to .* \(instance of (\w+)\) - need a (.+)/)
30
+ end
31
+
32
+ # Check if this is an ArgumentError (missing required fields, etc.)
33
+ sig { params(message: String).returns(T::Boolean) }
34
+ def self.argument_error?(message)
35
+ message.match?(/missing keyword/i) || message.match?(/unknown keyword/i)
36
+ end
37
+
38
+ # Format Sorbet type validation errors
39
+ sig { params(message: String).returns(String) }
40
+ def self.format_sorbet_type_error(message)
41
+ # Parse the Sorbet error pattern
42
+ match = message.match(/Can't set \.(\w+) to (.+?) \(instance of (\w+)\) - need a (.+?)(?:\n|$)/)
43
+ return clean_error_message(message) unless match
44
+
45
+ field_name = match[1]
46
+ raw_value = match[2]
47
+ actual_type = match[3]
48
+ expected_type = match[4]
49
+
50
+ # Extract sample data for display (truncate if too long)
51
+ sample_data = if raw_value.length > 100
52
+ "#{raw_value[0..100]}..."
53
+ else
54
+ raw_value
55
+ end
56
+
57
+ <<~ERROR.strip
58
+ Type Mismatch in '#{field_name}'
59
+
60
+ Expected: #{expected_type}
61
+ Received: #{actual_type} (plain Ruby #{actual_type.downcase})
62
+
63
+ #{generate_type_specific_explanation(actual_type, expected_type)}
64
+
65
+ Suggestions:
66
+ #{generate_suggestions(field_name, actual_type, expected_type)}
67
+
68
+ Sample data received: #{sample_data}
69
+ ERROR
70
+ end
71
+
72
+ # Format ArgumentError messages (missing/unknown keywords)
73
+ sig { params(message: String).returns(String) }
74
+ def self.format_argument_error(message)
75
+ if message.match(/missing keyword: (.+)/i)
76
+ missing_fields = $1.split(', ')
77
+ format_missing_fields_error(missing_fields)
78
+ elsif message.match(/unknown keyword: (.+)/i)
79
+ unknown_fields = $1.split(', ')
80
+ format_unknown_fields_error(unknown_fields)
81
+ else
82
+ clean_error_message(message)
83
+ end
84
+ end
85
+
86
+ # Format missing required fields error
87
+ sig { params(fields: T::Array[String]).returns(String) }
88
+ def self.format_missing_fields_error(fields)
89
+ field_list = fields.map { |f| "• #{f}" }.join("\n")
90
+
91
+ <<~ERROR.strip
92
+ Missing Required Fields
93
+
94
+ The following required fields were not provided:
95
+ #{field_list}
96
+
97
+ Suggestions:
98
+ • Check your signature definition - these fields should be marked as optional if they're not always provided
99
+ • Ensure your LLM prompt asks for all required information
100
+ • Consider providing default values in your signature
101
+
102
+ This usually happens when the LLM response doesn't include all expected fields.
103
+ ERROR
104
+ end
105
+
106
+ # Format unknown fields error
107
+ sig { params(fields: T::Array[String]).returns(String) }
108
+ def self.format_unknown_fields_error(fields)
109
+ field_list = fields.map { |f| "• #{f}" }.join("\n")
110
+
111
+ <<~ERROR.strip
112
+ Unknown Fields in Response
113
+
114
+ The LLM response included unexpected fields:
115
+ #{field_list}
116
+
117
+ Suggestions:
118
+ • Check if these fields should be added to your signature definition
119
+ • Review your prompt to ensure it only asks for expected fields
120
+ • Consider if the LLM is hallucinating extra information
121
+
122
+ Extra fields are ignored, but this might indicate a prompt or signature mismatch.
123
+ ERROR
124
+ end
125
+
126
+ # Generate type-specific explanations
127
+ sig { params(actual_type: String, expected_type: String).returns(String) }
128
+ def self.generate_type_specific_explanation(actual_type, expected_type)
129
+ case actual_type
130
+ when 'Array'
131
+ if expected_type.match(/T::Array\[(.+)\]/)
132
+ struct_type = $1
133
+ "The LLM returned a plain Ruby array with hash elements, but your signature requires an array of #{struct_type} struct objects."
134
+ else
135
+ "The LLM returned a #{actual_type}, but your signature requires #{expected_type}."
136
+ end
137
+ when 'Hash'
138
+ if expected_type != 'Hash' && !expected_type.include?('T::Hash')
139
+ "The LLM returned a plain Ruby hash, but your signature requires a #{expected_type} struct object."
140
+ else
141
+ "The LLM returned a #{actual_type}, but your signature requires #{expected_type}."
142
+ end
143
+ when 'String'
144
+ if expected_type.include?('T::Enum')
145
+ "The LLM returned a string, but your signature requires an enum value."
146
+ else
147
+ "The LLM returned a #{actual_type}, but your signature requires #{expected_type}."
148
+ end
149
+ else
150
+ "The LLM returned a #{actual_type}, but your signature requires #{expected_type}."
151
+ end
152
+ end
153
+
154
+ # Generate field and type-specific suggestions
155
+ sig { params(field_name: String, actual_type: String, expected_type: String).returns(String) }
156
+ def self.generate_suggestions(field_name, actual_type, expected_type)
157
+ suggestions = []
158
+
159
+ # Type-specific suggestions
160
+ case actual_type
161
+ when 'Array'
162
+ if expected_type.match(/T::Array\[(.+)\]/)
163
+ struct_type = $1
164
+ suggestions << "Check your signature uses proper T::Array[#{struct_type}] typing"
165
+ suggestions << "Verify the LLM response format matches your expected structure"
166
+ suggestions << "Ensure your struct definitions are correct and accessible"
167
+ else
168
+ suggestions << "Check your signature definition for the '#{field_name}' field"
169
+ suggestions << "Verify the LLM prompt asks for the correct data type"
170
+ end
171
+ when 'Hash'
172
+ if expected_type != 'Hash' && !expected_type.include?('T::Hash')
173
+ suggestions << "Check your signature uses proper #{expected_type} typing"
174
+ suggestions << "Verify the LLM response contains the expected fields"
175
+ else
176
+ suggestions << "Check your signature definition for the '#{field_name}' field"
177
+ suggestions << "Verify the LLM prompt asks for the correct data type"
178
+ end
179
+ when 'String'
180
+ if expected_type.include?('T::Enum')
181
+ suggestions << "Check your enum definition and available values"
182
+ suggestions << "Ensure your prompt specifies valid enum options"
183
+ else
184
+ suggestions << "Check your signature definition for the '#{field_name}' field"
185
+ suggestions << "Verify the LLM prompt asks for the correct data type"
186
+ end
187
+ else
188
+ suggestions << "Check your signature definition for the '#{field_name}' field"
189
+ suggestions << "Verify the LLM prompt asks for the correct data type"
190
+ end
191
+
192
+ # General suggestions
193
+ suggestions << "Consider if your prompt needs clearer type instructions"
194
+ suggestions << "Check if the LLM model supports structured output for complex types"
195
+
196
+ suggestions.map { |s| "• #{s}" }.join("\n")
197
+ end
198
+
199
+ # Clean up error messages by removing internal stack traces and formatting
200
+ sig { params(message: String).returns(String) }
201
+ def self.clean_error_message(message)
202
+ # Remove caller information that's not useful to end users
203
+ cleaned = message.gsub(/\nCaller:.*$/m, '')
204
+
205
+ # Remove excessive newlines and clean up formatting
206
+ cleaned.gsub(/\n+/, "\n").strip
207
+ end
208
+ end
209
+ end
data/lib/dspy/predict.rb CHANGED
@@ -5,20 +5,40 @@ require_relative 'module'
5
5
  require_relative 'prompt'
6
6
  require_relative 'mixins/struct_builder'
7
7
  require_relative 'mixins/type_coercion'
8
+ require_relative 'error_formatter'
8
9
 
9
10
  module DSPy
10
11
  # Exception raised when prediction fails validation
11
12
  class PredictionInvalidError < StandardError
12
13
  extend T::Sig
13
14
 
14
- sig { params(errors: T::Hash[T.untyped, T.untyped]).void }
15
- def initialize(errors)
15
+ sig { params(errors: T::Hash[T.untyped, T.untyped], context: T.nilable(String)).void }
16
+ def initialize(errors, context: nil)
16
17
  @errors = errors
17
- super("Prediction validation failed: #{errors}")
18
+ @context = context
19
+
20
+ # Format the error message using ErrorFormatter for better readability
21
+ formatted_message = if errors.key?(:output) && errors[:output].is_a?(String)
22
+ # This is likely a type validation error from Sorbet
23
+ formatted = DSPy::ErrorFormatter.format_error(errors[:output], context)
24
+ "Prediction validation failed:\n\n#{formatted}"
25
+ elsif errors.key?(:input) && errors[:input].is_a?(String)
26
+ # This is an input validation error
27
+ formatted = DSPy::ErrorFormatter.format_error(errors[:input], context)
28
+ "Input validation failed:\n\n#{formatted}"
29
+ else
30
+ # Fallback to original format for any other error structure
31
+ "Prediction validation failed: #{errors}"
32
+ end
33
+
34
+ super(formatted_message)
18
35
  end
19
36
 
20
37
  sig { returns(T::Hash[T.untyped, T.untyped]) }
21
38
  attr_reader :errors
39
+
40
+ sig { returns(T.nilable(String)) }
41
+ attr_reader :context
22
42
  end
23
43
 
24
44
  class Predict < DSPy::Module
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.18.0"
4
+ VERSION = "0.19.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-08 00:00:00.000000000 Z
10
+ date: 2025-08-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -175,6 +175,7 @@ files:
175
175
  - lib/dspy/chain_of_thought.rb
176
176
  - lib/dspy/code_act.rb
177
177
  - lib/dspy/context.rb
178
+ - lib/dspy/error_formatter.rb
178
179
  - lib/dspy/errors.rb
179
180
  - lib/dspy/evaluate.rb
180
181
  - lib/dspy/example.rb