dspy 0.29.0 → 0.30.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 +4 -4
- data/LICENSE +45 -0
- data/README.md +121 -101
- data/lib/dspy/callbacks.rb +74 -19
- data/lib/dspy/context.rb +49 -4
- data/lib/dspy/errors.rb +19 -1
- data/lib/dspy/{datasets.rb → evals/version.rb} +2 -3
- data/lib/dspy/{evaluate.rb → evals.rb} +373 -110
- data/lib/dspy/mixins/instruction_updatable.rb +22 -0
- data/lib/dspy/observability.rb +40 -182
- data/lib/dspy/predict.rb +10 -2
- data/lib/dspy/propose/dataset_summary_generator.rb +28 -18
- data/lib/dspy/re_act.rb +21 -0
- data/lib/dspy/schema/sorbet_json_schema.rb +302 -0
- data/lib/dspy/schema/version.rb +7 -0
- data/lib/dspy/schema.rb +4 -0
- data/lib/dspy/structured_outputs_prompt.rb +48 -0
- data/lib/dspy/support/warning_filters.rb +27 -0
- data/lib/dspy/teleprompt/gepa.rb +9 -588
- data/lib/dspy/teleprompt/instruction_updates.rb +94 -0
- data/lib/dspy/teleprompt/teleprompter.rb +6 -6
- data/lib/dspy/teleprompt/utils.rb +5 -65
- data/lib/dspy/type_system/sorbet_json_schema.rb +2 -299
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +33 -7
- metadata +14 -60
- data/lib/dspy/code_act.rb +0 -477
- data/lib/dspy/datasets/ade.rb +0 -90
- data/lib/dspy/observability/async_span_processor.rb +0 -250
- data/lib/dspy/observability/observation_type.rb +0 -65
- data/lib/dspy/optimizers/gaussian_process.rb +0 -141
- data/lib/dspy/teleprompt/mipro_v2.rb +0 -1423
- data/lib/gepa/api.rb +0 -61
- data/lib/gepa/core/engine.rb +0 -226
- data/lib/gepa/core/evaluation_batch.rb +0 -26
- data/lib/gepa/core/result.rb +0 -92
- data/lib/gepa/core/state.rb +0 -231
- data/lib/gepa/logging/experiment_tracker.rb +0 -54
- data/lib/gepa/logging/logger.rb +0 -57
- data/lib/gepa/logging.rb +0 -9
- data/lib/gepa/proposer/base.rb +0 -27
- data/lib/gepa/proposer/merge_proposer.rb +0 -424
- data/lib/gepa/proposer/reflective_mutation/base.rb +0 -48
- data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +0 -188
- data/lib/gepa/strategies/batch_sampler.rb +0 -91
- data/lib/gepa/strategies/candidate_selector.rb +0 -97
- data/lib/gepa/strategies/component_selector.rb +0 -57
- data/lib/gepa/strategies/instruction_proposal.rb +0 -120
- data/lib/gepa/telemetry.rb +0 -122
- data/lib/gepa/utils/pareto.rb +0 -119
- data/lib/gepa.rb +0 -21
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
|
|
7
|
+
module DSPy
|
|
8
|
+
module TypeSystem
|
|
9
|
+
# Unified module for converting Sorbet types to JSON Schema
|
|
10
|
+
# Extracted from Signature class to ensure consistency across Tools, Toolsets, and Signatures
|
|
11
|
+
module SorbetJsonSchema
|
|
12
|
+
extend T::Sig
|
|
13
|
+
extend T::Helpers
|
|
14
|
+
|
|
15
|
+
# Convert a Sorbet type to JSON Schema format
|
|
16
|
+
sig { params(type: T.untyped, visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
|
|
17
|
+
def self.type_to_json_schema(type, visited = nil)
|
|
18
|
+
visited ||= Set.new
|
|
19
|
+
|
|
20
|
+
# Handle T::Boolean type alias first
|
|
21
|
+
if type == T::Boolean
|
|
22
|
+
return { type: "boolean" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Handle type aliases by resolving to their underlying type
|
|
26
|
+
if type.is_a?(T::Private::Types::TypeAlias)
|
|
27
|
+
return self.type_to_json_schema(type.aliased_type, visited)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Handle raw class types first
|
|
31
|
+
if type.is_a?(Class)
|
|
32
|
+
if type < T::Enum
|
|
33
|
+
# Get all enum values
|
|
34
|
+
values = type.values.map(&:serialize)
|
|
35
|
+
{ type: "string", enum: values }
|
|
36
|
+
elsif type == String
|
|
37
|
+
{ type: "string" }
|
|
38
|
+
elsif type == Integer
|
|
39
|
+
{ type: "integer" }
|
|
40
|
+
elsif type == Float
|
|
41
|
+
{ type: "number" }
|
|
42
|
+
elsif type == Numeric
|
|
43
|
+
{ type: "number" }
|
|
44
|
+
elsif type == Date
|
|
45
|
+
{ type: "string", format: "date" }
|
|
46
|
+
elsif type == DateTime
|
|
47
|
+
{ type: "string", format: "date-time" }
|
|
48
|
+
elsif type == Time
|
|
49
|
+
{ type: "string", format: "date-time" }
|
|
50
|
+
elsif [TrueClass, FalseClass].include?(type)
|
|
51
|
+
{ type: "boolean" }
|
|
52
|
+
elsif type < T::Struct
|
|
53
|
+
# Handle custom T::Struct classes by generating nested object schema
|
|
54
|
+
# Check for recursion
|
|
55
|
+
if visited.include?(type)
|
|
56
|
+
# Return a reference to avoid infinite recursion
|
|
57
|
+
{
|
|
58
|
+
"$ref" => "#/definitions/#{type.name.split('::').last}",
|
|
59
|
+
description: "Recursive reference to #{type.name}"
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
self.generate_struct_schema(type, visited)
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
{ type: "string" } # Default fallback
|
|
66
|
+
end
|
|
67
|
+
elsif type.is_a?(T::Types::Simple)
|
|
68
|
+
case type.raw_type.to_s
|
|
69
|
+
when "String"
|
|
70
|
+
{ type: "string" }
|
|
71
|
+
when "Integer"
|
|
72
|
+
{ type: "integer" }
|
|
73
|
+
when "Float"
|
|
74
|
+
{ type: "number" }
|
|
75
|
+
when "Numeric"
|
|
76
|
+
{ type: "number" }
|
|
77
|
+
when "Date"
|
|
78
|
+
{ type: "string", format: "date" }
|
|
79
|
+
when "DateTime"
|
|
80
|
+
{ type: "string", format: "date-time" }
|
|
81
|
+
when "Time"
|
|
82
|
+
{ type: "string", format: "date-time" }
|
|
83
|
+
when "TrueClass", "FalseClass"
|
|
84
|
+
{ type: "boolean" }
|
|
85
|
+
when "T::Boolean"
|
|
86
|
+
{ type: "boolean" }
|
|
87
|
+
else
|
|
88
|
+
# Check if it's an enum
|
|
89
|
+
if type.raw_type < T::Enum
|
|
90
|
+
# Get all enum values
|
|
91
|
+
values = type.raw_type.values.map(&:serialize)
|
|
92
|
+
{ type: "string", enum: values }
|
|
93
|
+
elsif type.raw_type < T::Struct
|
|
94
|
+
# Handle custom T::Struct classes
|
|
95
|
+
if visited.include?(type.raw_type)
|
|
96
|
+
{
|
|
97
|
+
"$ref" => "#/definitions/#{type.raw_type.name.split('::').last}",
|
|
98
|
+
description: "Recursive reference to #{type.raw_type.name}"
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
generate_struct_schema(type.raw_type, visited)
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
{ type: "string" } # Default fallback
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
elsif type.is_a?(T::Types::TypedArray)
|
|
108
|
+
# Handle arrays properly with nested item type
|
|
109
|
+
{
|
|
110
|
+
type: "array",
|
|
111
|
+
items: self.type_to_json_schema(type.type, visited)
|
|
112
|
+
}
|
|
113
|
+
elsif type.is_a?(T::Types::TypedHash)
|
|
114
|
+
# Handle hashes as objects with additionalProperties
|
|
115
|
+
# TypedHash has keys and values methods to access its key and value types
|
|
116
|
+
key_schema = self.type_to_json_schema(type.keys, visited)
|
|
117
|
+
value_schema = self.type_to_json_schema(type.values, visited)
|
|
118
|
+
|
|
119
|
+
# Create a more descriptive schema for nested structures
|
|
120
|
+
{
|
|
121
|
+
type: "object",
|
|
122
|
+
propertyNames: key_schema, # Describe key constraints
|
|
123
|
+
additionalProperties: value_schema,
|
|
124
|
+
# Add a more explicit description of the expected structure
|
|
125
|
+
description: "A mapping where keys are #{key_schema[:type]}s and values are #{value_schema[:description] || value_schema[:type]}s"
|
|
126
|
+
}
|
|
127
|
+
elsif type.is_a?(T::Types::FixedHash)
|
|
128
|
+
# Handle fixed hashes (from type aliases like { "key" => Type })
|
|
129
|
+
properties = {}
|
|
130
|
+
required = []
|
|
131
|
+
|
|
132
|
+
type.types.each do |key, value_type|
|
|
133
|
+
properties[key] = self.type_to_json_schema(value_type, visited)
|
|
134
|
+
required << key
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: properties,
|
|
140
|
+
required: required,
|
|
141
|
+
additionalProperties: false
|
|
142
|
+
}
|
|
143
|
+
elsif type.class.name == "T::Private::Types::SimplePairUnion"
|
|
144
|
+
# Handle T.nilable types (T::Private::Types::SimplePairUnion)
|
|
145
|
+
# This is the actual implementation of T.nilable(SomeType)
|
|
146
|
+
has_nil = type.respond_to?(:types) && type.types.any? do |t|
|
|
147
|
+
(t.respond_to?(:raw_type) && t.raw_type == NilClass) ||
|
|
148
|
+
(t.respond_to?(:name) && t.name == "NilClass")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if has_nil
|
|
152
|
+
# Find the non-nil type
|
|
153
|
+
non_nil_type = type.types.find do |t|
|
|
154
|
+
!(t.respond_to?(:raw_type) && t.raw_type == NilClass) &&
|
|
155
|
+
!(t.respond_to?(:name) && t.name == "NilClass")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if non_nil_type
|
|
159
|
+
base_schema = self.type_to_json_schema(non_nil_type, visited)
|
|
160
|
+
if base_schema[:type].is_a?(String)
|
|
161
|
+
# Convert single type to array with null
|
|
162
|
+
{ type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
|
|
163
|
+
else
|
|
164
|
+
# For complex schemas, use anyOf to allow null
|
|
165
|
+
{ anyOf: [base_schema, { type: "null" }] }
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
{ type: "string" } # Fallback
|
|
169
|
+
end
|
|
170
|
+
else
|
|
171
|
+
# Not nilable SimplePairUnion - this is a regular T.any() union
|
|
172
|
+
# Generate oneOf schema for all types
|
|
173
|
+
if type.respond_to?(:types) && type.types.length > 1
|
|
174
|
+
{
|
|
175
|
+
oneOf: type.types.map { |t| self.type_to_json_schema(t, visited) },
|
|
176
|
+
description: "Union of multiple types"
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
# Single type or fallback
|
|
180
|
+
first_type = type.respond_to?(:types) ? type.types.first : type
|
|
181
|
+
self.type_to_json_schema(first_type, visited)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
elsif type.is_a?(T::Types::Union)
|
|
185
|
+
# Check if this is a nilable type (contains NilClass)
|
|
186
|
+
is_nilable = type.types.any? { |t| t == T::Utils.coerce(NilClass) }
|
|
187
|
+
non_nil_types = type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
|
188
|
+
|
|
189
|
+
# Special case: check if we have TrueClass + FalseClass (T.nilable(T::Boolean))
|
|
190
|
+
if non_nil_types.size == 2 && is_nilable
|
|
191
|
+
true_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == TrueClass }
|
|
192
|
+
false_class_type = non_nil_types.find { |t| t.respond_to?(:raw_type) && t.raw_type == FalseClass }
|
|
193
|
+
|
|
194
|
+
if true_class_type && false_class_type
|
|
195
|
+
# This is T.nilable(T::Boolean) - treat as nilable boolean
|
|
196
|
+
return { type: ["boolean", "null"] }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if non_nil_types.size == 1 && is_nilable
|
|
201
|
+
# This is T.nilable(SomeType) - generate proper schema with null allowed
|
|
202
|
+
base_schema = self.type_to_json_schema(non_nil_types.first, visited)
|
|
203
|
+
if base_schema[:type].is_a?(String)
|
|
204
|
+
# Convert single type to array with null
|
|
205
|
+
{ type: [base_schema[:type], "null"] }.merge(base_schema.except(:type))
|
|
206
|
+
else
|
|
207
|
+
# For complex schemas, use anyOf to allow null
|
|
208
|
+
{ anyOf: [base_schema, { type: "null" }] }
|
|
209
|
+
end
|
|
210
|
+
elsif non_nil_types.size == 1
|
|
211
|
+
# Non-nilable single type union (shouldn't happen in practice)
|
|
212
|
+
self.type_to_json_schema(non_nil_types.first, visited)
|
|
213
|
+
elsif non_nil_types.size > 1
|
|
214
|
+
# Handle complex unions with oneOf for better JSON schema compliance
|
|
215
|
+
base_schema = {
|
|
216
|
+
oneOf: non_nil_types.map { |t| self.type_to_json_schema(t, visited) },
|
|
217
|
+
description: "Union of multiple types"
|
|
218
|
+
}
|
|
219
|
+
if is_nilable
|
|
220
|
+
# Add null as an option for complex nilable unions
|
|
221
|
+
base_schema[:oneOf] << { type: "null" }
|
|
222
|
+
end
|
|
223
|
+
base_schema
|
|
224
|
+
else
|
|
225
|
+
{ type: "string" } # Fallback for complex unions
|
|
226
|
+
end
|
|
227
|
+
elsif type.is_a?(T::Types::ClassOf)
|
|
228
|
+
# Handle T.class_of() types
|
|
229
|
+
{
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "Class name (T.class_of type)"
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{ type: "string" } # Default fallback
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Generate JSON schema for custom T::Struct classes
|
|
239
|
+
sig { params(struct_class: T.class_of(T::Struct), visited: T.nilable(T::Set[T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
|
|
240
|
+
def self.generate_struct_schema(struct_class, visited = nil)
|
|
241
|
+
visited ||= Set.new
|
|
242
|
+
|
|
243
|
+
return { type: "string", description: "Struct (schema introspection not available)" } unless struct_class.respond_to?(:props)
|
|
244
|
+
|
|
245
|
+
# Add this struct to visited set to detect recursion
|
|
246
|
+
visited.add(struct_class)
|
|
247
|
+
|
|
248
|
+
properties = {}
|
|
249
|
+
required = []
|
|
250
|
+
|
|
251
|
+
# Check if struct already has a _type field
|
|
252
|
+
if struct_class.props.key?(:_type)
|
|
253
|
+
raise DSPy::ValidationError, "_type field conflict: #{struct_class.name} already has a _type field defined. " \
|
|
254
|
+
"DSPy uses _type for automatic type detection in union types."
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Add automatic _type field for type detection
|
|
258
|
+
properties[:_type] = {
|
|
259
|
+
type: "string",
|
|
260
|
+
const: struct_class.name.split('::').last # Use the simple class name
|
|
261
|
+
}
|
|
262
|
+
required << "_type"
|
|
263
|
+
|
|
264
|
+
struct_class.props.each do |prop_name, prop_info|
|
|
265
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
|
266
|
+
properties[prop_name] = self.type_to_json_schema(prop_type, visited)
|
|
267
|
+
|
|
268
|
+
# A field is required if it's not fully optional
|
|
269
|
+
# fully_optional is true for nilable prop fields
|
|
270
|
+
# immutable const fields are required unless nilable
|
|
271
|
+
unless prop_info[:fully_optional]
|
|
272
|
+
required << prop_name.to_s
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Remove this struct from visited set after processing
|
|
277
|
+
visited.delete(struct_class)
|
|
278
|
+
|
|
279
|
+
{
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: properties,
|
|
282
|
+
required: required,
|
|
283
|
+
description: "#{struct_class.name} struct"
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
# Extensions to Hash for Rails-like except method if not available
|
|
290
|
+
# This ensures compatibility with the original code
|
|
291
|
+
unless Hash.method_defined?(:except)
|
|
292
|
+
Hash.class_eval do
|
|
293
|
+
def except(*keys)
|
|
294
|
+
dup.tap do |hash|
|
|
295
|
+
keys.each { |key| hash.delete(key) }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
data/lib/dspy/schema.rb
ADDED
|
@@ -9,6 +9,40 @@ module DSPy
|
|
|
9
9
|
class StructuredOutputsPrompt < Prompt
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
instruction: String,
|
|
15
|
+
input_schema: T::Hash[Symbol, T.untyped],
|
|
16
|
+
output_schema: T::Hash[Symbol, T.untyped],
|
|
17
|
+
few_shot_examples: T::Array[T.untyped],
|
|
18
|
+
signature_class_name: T.nilable(String),
|
|
19
|
+
schema_format: Symbol,
|
|
20
|
+
signature_class: T.nilable(T.class_of(Signature))
|
|
21
|
+
).void
|
|
22
|
+
end
|
|
23
|
+
def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil)
|
|
24
|
+
normalized_examples = few_shot_examples.map do |example|
|
|
25
|
+
case example
|
|
26
|
+
when FewShotExample
|
|
27
|
+
example
|
|
28
|
+
when Hash
|
|
29
|
+
FewShotExample.from_h(symbolize_keys(example))
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, "Unsupported few-shot example type: #{example.class}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
super(
|
|
36
|
+
instruction: instruction,
|
|
37
|
+
input_schema: input_schema,
|
|
38
|
+
output_schema: output_schema,
|
|
39
|
+
few_shot_examples: normalized_examples,
|
|
40
|
+
signature_class_name: signature_class_name,
|
|
41
|
+
schema_format: schema_format,
|
|
42
|
+
signature_class: signature_class
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
12
46
|
# Render minimal system prompt without output schema or JSON formatting instructions
|
|
13
47
|
sig { returns(String) }
|
|
14
48
|
def render_system_prompt
|
|
@@ -49,5 +83,19 @@ module DSPy
|
|
|
49
83
|
|
|
50
84
|
sections.join("\n")
|
|
51
85
|
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
90
|
+
def symbolize_keys(hash)
|
|
91
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
92
|
+
value = symbolize_keys(value) if value.is_a?(Hash)
|
|
93
|
+
if value.is_a?(Array)
|
|
94
|
+
value = value.map { |item| item.is_a?(Hash) ? symbolize_keys(item) : item }
|
|
95
|
+
end
|
|
96
|
+
key_sym = key.is_a?(Symbol) ? key : key.to_sym
|
|
97
|
+
result[key_sym] = value
|
|
98
|
+
end
|
|
99
|
+
end
|
|
52
100
|
end
|
|
53
101
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DSPy
|
|
4
|
+
module Support
|
|
5
|
+
module WarningFilters
|
|
6
|
+
RUBY2_KEYWORDS_MESSAGE = 'Skipping set of ruby2_keywords flag for forward'
|
|
7
|
+
|
|
8
|
+
module WarningSilencer
|
|
9
|
+
def warn(message = nil, category: nil, **kwargs)
|
|
10
|
+
msg = message.to_s
|
|
11
|
+
return if msg.include?(RUBY2_KEYWORDS_MESSAGE)
|
|
12
|
+
|
|
13
|
+
super(message, category: category, **kwargs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.install!
|
|
18
|
+
return if @installed
|
|
19
|
+
|
|
20
|
+
Warning.singleton_class.prepend(WarningSilencer)
|
|
21
|
+
@installed = true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
DSPy::Support::WarningFilters.install!
|