dspy 0.18.1 → 0.19.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/error_formatter.rb +209 -0
- data/lib/dspy/predict.rb +23 -3
- data/lib/dspy/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d76e6cfecee3a85c7d8cd389b39eb0401e2051644e0f3ada98ab03645414f35e
|
4
|
+
data.tar.gz: fe078c869c9ee45237916dd10bd12a5d9bb80ea629c1bb6b0a29ae6ff5249b73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 56565be338d06d517daa931d7ebfb32f259b92d9b8a55140783bcf34f635ace57213909d74003ad33b6e8761d5c1fae950f1f6332ee55131d8a7ffd5c4a58183
|
7
|
+
data.tar.gz: 3e36f838fbba8e06428397611f1f2e2476f789c7f4714a98ce2198f38bf5632a0f4e12cf5be56282c48d31071c9b784959f0095f27bc8f42c44472ad43111d41
|
@@ -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
|
-
|
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
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.
|
4
|
+
version: 0.19.1
|
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-
|
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
|