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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +45 -0
  3. data/README.md +121 -101
  4. data/lib/dspy/callbacks.rb +74 -19
  5. data/lib/dspy/context.rb +49 -4
  6. data/lib/dspy/errors.rb +19 -1
  7. data/lib/dspy/{datasets.rb → evals/version.rb} +2 -3
  8. data/lib/dspy/{evaluate.rb → evals.rb} +373 -110
  9. data/lib/dspy/mixins/instruction_updatable.rb +22 -0
  10. data/lib/dspy/observability.rb +40 -182
  11. data/lib/dspy/predict.rb +10 -2
  12. data/lib/dspy/propose/dataset_summary_generator.rb +28 -18
  13. data/lib/dspy/re_act.rb +21 -0
  14. data/lib/dspy/schema/sorbet_json_schema.rb +302 -0
  15. data/lib/dspy/schema/version.rb +7 -0
  16. data/lib/dspy/schema.rb +4 -0
  17. data/lib/dspy/structured_outputs_prompt.rb +48 -0
  18. data/lib/dspy/support/warning_filters.rb +27 -0
  19. data/lib/dspy/teleprompt/gepa.rb +9 -588
  20. data/lib/dspy/teleprompt/instruction_updates.rb +94 -0
  21. data/lib/dspy/teleprompt/teleprompter.rb +6 -6
  22. data/lib/dspy/teleprompt/utils.rb +5 -65
  23. data/lib/dspy/type_system/sorbet_json_schema.rb +2 -299
  24. data/lib/dspy/version.rb +1 -1
  25. data/lib/dspy.rb +33 -7
  26. metadata +14 -60
  27. data/lib/dspy/code_act.rb +0 -477
  28. data/lib/dspy/datasets/ade.rb +0 -90
  29. data/lib/dspy/observability/async_span_processor.rb +0 -250
  30. data/lib/dspy/observability/observation_type.rb +0 -65
  31. data/lib/dspy/optimizers/gaussian_process.rb +0 -141
  32. data/lib/dspy/teleprompt/mipro_v2.rb +0 -1423
  33. data/lib/gepa/api.rb +0 -61
  34. data/lib/gepa/core/engine.rb +0 -226
  35. data/lib/gepa/core/evaluation_batch.rb +0 -26
  36. data/lib/gepa/core/result.rb +0 -92
  37. data/lib/gepa/core/state.rb +0 -231
  38. data/lib/gepa/logging/experiment_tracker.rb +0 -54
  39. data/lib/gepa/logging/logger.rb +0 -57
  40. data/lib/gepa/logging.rb +0 -9
  41. data/lib/gepa/proposer/base.rb +0 -27
  42. data/lib/gepa/proposer/merge_proposer.rb +0 -424
  43. data/lib/gepa/proposer/reflective_mutation/base.rb +0 -48
  44. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +0 -188
  45. data/lib/gepa/strategies/batch_sampler.rb +0 -91
  46. data/lib/gepa/strategies/candidate_selector.rb +0 -97
  47. data/lib/gepa/strategies/component_selector.rb +0 -57
  48. data/lib/gepa/strategies/instruction_proposal.rb +0 -120
  49. data/lib/gepa/telemetry.rb +0 -122
  50. data/lib/gepa/utils/pareto.rb +0 -119
  51. 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSPy
4
+ module Schema
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema/version"
4
+ require_relative "schema/sorbet_json_schema"
@@ -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!