dspy 0.34.1 → 0.34.3
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/README.md +139 -216
- data/lib/dspy/chain_of_thought.rb +3 -2
- data/lib/dspy/context.rb +57 -30
- data/lib/dspy/evals/version.rb +1 -1
- data/lib/dspy/evals.rb +42 -31
- data/lib/dspy/events.rb +2 -3
- data/lib/dspy/example.rb +1 -1
- data/lib/dspy/lm/adapter.rb +39 -0
- data/lib/dspy/lm/json_strategy.rb +37 -2
- data/lib/dspy/lm/message.rb +1 -1
- data/lib/dspy/lm/response.rb +1 -1
- data/lib/dspy/lm/usage.rb +4 -4
- data/lib/dspy/lm.rb +27 -79
- data/lib/dspy/mixins/type_coercion.rb +189 -30
- data/lib/dspy/module.rb +70 -25
- data/lib/dspy/predict.rb +32 -5
- data/lib/dspy/prediction.rb +15 -57
- data/lib/dspy/prompt.rb +50 -30
- data/lib/dspy/propose/dataset_summary_generator.rb +1 -1
- data/lib/dspy/propose/grounded_proposer.rb +3 -3
- data/lib/dspy/re_act.rb +0 -162
- data/lib/dspy/registry/signature_registry.rb +3 -3
- data/lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb +1 -27
- data/lib/dspy/schema/sorbet_json_schema.rb +7 -6
- data/lib/dspy/schema/version.rb +1 -1
- data/lib/dspy/schema_adapters.rb +1 -1
- data/lib/dspy/storage/program_storage.rb +2 -2
- data/lib/dspy/structured_outputs_prompt.rb +3 -3
- data/lib/dspy/teleprompt/utils.rb +2 -2
- data/lib/dspy/tools/github_cli_toolset.rb +7 -7
- data/lib/dspy/tools/text_processing_toolset.rb +2 -2
- data/lib/dspy/tools/toolset.rb +1 -1
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +1 -4
- metadata +1 -26
- data/lib/dspy/events/subscriber_mixin.rb +0 -79
- data/lib/dspy/events/subscribers.rb +0 -43
- data/lib/dspy/memory/embedding_engine.rb +0 -68
- data/lib/dspy/memory/in_memory_store.rb +0 -216
- data/lib/dspy/memory/local_embedding_engine.rb +0 -244
- data/lib/dspy/memory/memory_compactor.rb +0 -298
- data/lib/dspy/memory/memory_manager.rb +0 -266
- data/lib/dspy/memory/memory_record.rb +0 -163
- data/lib/dspy/memory/memory_store.rb +0 -90
- data/lib/dspy/memory.rb +0 -30
- data/lib/dspy/tools/memory_toolset.rb +0 -117
|
@@ -9,6 +9,62 @@ module DSPy
|
|
|
9
9
|
module TypeCoercion
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
+
# Centralized enum deserialization with case-insensitive fallback.
|
|
13
|
+
# Uses try_deserialize for O(1) exact match, then a lazily-built
|
|
14
|
+
# case-insensitive lookup hash as fallback for LLM casing variations.
|
|
15
|
+
#
|
|
16
|
+
# Returns the enum instance on match, or nil if no match found.
|
|
17
|
+
sig { params(enum_class: T.untyped, value: T.untyped).returns(T.nilable(T::Enum)) }
|
|
18
|
+
def self.deserialize_enum(enum_class, value)
|
|
19
|
+
return value if value.is_a?(enum_class)
|
|
20
|
+
|
|
21
|
+
str = value.to_s
|
|
22
|
+
result = enum_class.try_deserialize(str)
|
|
23
|
+
return result if result
|
|
24
|
+
|
|
25
|
+
@ci_enum_cache ||= {}
|
|
26
|
+
ci_map = (@ci_enum_cache[enum_class] ||=
|
|
27
|
+
enum_class.values.each_with_object({}) { |v, h| h[v.serialize.downcase.freeze] = v }.freeze)
|
|
28
|
+
|
|
29
|
+
ci_map[str.downcase]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Module-level enum type detection (delegates to instance method)
|
|
33
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
|
34
|
+
def self.enum_type?(type)
|
|
35
|
+
return false unless type
|
|
36
|
+
|
|
37
|
+
case type
|
|
38
|
+
when Class
|
|
39
|
+
!!(type < T::Enum)
|
|
40
|
+
when T::Types::Simple
|
|
41
|
+
type.raw_type.is_a?(Class) && !!(type.raw_type < T::Enum)
|
|
42
|
+
when T::Types::Union
|
|
43
|
+
non_nil = type.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
44
|
+
non_nil.size == 1 && enum_type?(non_nil.first)
|
|
45
|
+
else
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Module-level enum class extraction (delegates to instance method)
|
|
53
|
+
sig { params(prop_type: T.untyped).returns(T.class_of(T::Enum)) }
|
|
54
|
+
def self.extract_enum_class(prop_type)
|
|
55
|
+
case prop_type
|
|
56
|
+
when Class
|
|
57
|
+
return prop_type if prop_type < T::Enum
|
|
58
|
+
when T::Types::Simple
|
|
59
|
+
return prop_type.raw_type if prop_type.raw_type.is_a?(Class) && prop_type.raw_type < T::Enum
|
|
60
|
+
when T::Types::Union
|
|
61
|
+
non_nil = prop_type.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
62
|
+
return extract_enum_class(non_nil.first) if non_nil.size == 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, "Not an enum type: #{prop_type.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
12
68
|
private
|
|
13
69
|
|
|
14
70
|
# Coerces output attributes to match their expected types
|
|
@@ -57,32 +113,16 @@ module DSPy
|
|
|
57
113
|
end
|
|
58
114
|
end
|
|
59
115
|
|
|
60
|
-
# Checks if a type is an enum type
|
|
116
|
+
# Checks if a type is an enum type (handles Class, Simple, and nilable unions)
|
|
61
117
|
sig { params(type: T.untyped).returns(T::Boolean) }
|
|
62
118
|
def enum_type?(type)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if type.is_a?(Class)
|
|
66
|
-
!!(type < T::Enum)
|
|
67
|
-
elsif type.is_a?(T::Types::Simple)
|
|
68
|
-
!!(type.raw_type < T::Enum)
|
|
69
|
-
else
|
|
70
|
-
false
|
|
71
|
-
end
|
|
72
|
-
rescue StandardError
|
|
73
|
-
false
|
|
119
|
+
DSPy::Mixins::TypeCoercion.enum_type?(type)
|
|
74
120
|
end
|
|
75
121
|
|
|
76
|
-
# Extracts the enum class from a type
|
|
122
|
+
# Extracts the enum class from a type (handles Class, Simple, and nilable unions)
|
|
77
123
|
sig { params(prop_type: T.untyped).returns(T.class_of(T::Enum)) }
|
|
78
124
|
def extract_enum_class(prop_type)
|
|
79
|
-
|
|
80
|
-
prop_type
|
|
81
|
-
elsif prop_type.is_a?(T::Types::Simple) && prop_type.raw_type < T::Enum
|
|
82
|
-
prop_type.raw_type
|
|
83
|
-
else
|
|
84
|
-
T.cast(prop_type, T.class_of(T::Enum))
|
|
85
|
-
end
|
|
125
|
+
DSPy::Mixins::TypeCoercion.extract_enum_class(prop_type)
|
|
86
126
|
end
|
|
87
127
|
|
|
88
128
|
# Checks if a type matches a simple type (like Float, Integer)
|
|
@@ -130,6 +170,106 @@ module DSPy
|
|
|
130
170
|
type.is_a?(T::Types::Union) && type.types.any? { |t| t == T::Utils.coerce(NilClass) }
|
|
131
171
|
end
|
|
132
172
|
|
|
173
|
+
# Checks if a type is a scalar (primitives that don't need special serialization)
|
|
174
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
175
|
+
def scalar_type?(type_object)
|
|
176
|
+
case type_object
|
|
177
|
+
when T::Types::Simple
|
|
178
|
+
scalar_classes = [String, Integer, Float, Numeric, TrueClass, FalseClass]
|
|
179
|
+
scalar_classes.any? { |klass| type_object.raw_type == klass || type_object.raw_type <= klass }
|
|
180
|
+
when T::Types::Union
|
|
181
|
+
# Union is scalar if all its types are scalars
|
|
182
|
+
type_object.types.all? { |t| scalar_type?(t) }
|
|
183
|
+
else
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Checks if a type is structured (arrays, hashes, structs that need type preservation)
|
|
189
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
190
|
+
def structured_type?(type_object)
|
|
191
|
+
return true if type_object.is_a?(T::Types::TypedArray)
|
|
192
|
+
return true if type_object.is_a?(T::Types::TypedHash)
|
|
193
|
+
|
|
194
|
+
if type_object.is_a?(T::Types::Simple)
|
|
195
|
+
raw_type = type_object.raw_type
|
|
196
|
+
return true if raw_type.respond_to?(:<=) && raw_type <= T::Struct
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# For union types (like T.nilable(T::Array[...])), check if any non-nil type is structured
|
|
200
|
+
if type_object.is_a?(T::Types::Union)
|
|
201
|
+
non_nil_types = type_object.types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
202
|
+
return non_nil_types.any? { |t| structured_type?(t) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Checks if a type is String or compatible with String (e.g., T.any(String, ...) or T.nilable(String))
|
|
209
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
210
|
+
def string_type?(type_object)
|
|
211
|
+
case type_object
|
|
212
|
+
when T::Types::Simple
|
|
213
|
+
type_object.raw_type == String
|
|
214
|
+
when T::Types::Union
|
|
215
|
+
# Check if any of the union types is String
|
|
216
|
+
type_object.types.any? { |t| t.is_a?(T::Types::Simple) && t.raw_type == String }
|
|
217
|
+
else
|
|
218
|
+
false
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get a readable type name from a Sorbet type object
|
|
223
|
+
sig { params(type_object: T.untyped).returns(String) }
|
|
224
|
+
def type_name(type_object)
|
|
225
|
+
case type_object
|
|
226
|
+
when T::Types::TypedArray
|
|
227
|
+
element_type = type_object.type
|
|
228
|
+
"T::Array[#{type_name(element_type)}]"
|
|
229
|
+
when T::Types::TypedHash
|
|
230
|
+
"T::Hash"
|
|
231
|
+
when T::Types::Simple
|
|
232
|
+
type_object.raw_type.to_s
|
|
233
|
+
when T::Types::Union
|
|
234
|
+
types_str = type_object.types.map { |t| type_name(t) }.join(', ')
|
|
235
|
+
"T.any(#{types_str})"
|
|
236
|
+
else
|
|
237
|
+
type_object.to_s
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns an appropriate default value for a given Sorbet type
|
|
242
|
+
# This is used when max iterations is reached without a successful completion
|
|
243
|
+
sig { params(type_object: T.untyped).returns(T.untyped) }
|
|
244
|
+
def default_value_for_type(type_object)
|
|
245
|
+
# Handle TypedArray (T::Array[...])
|
|
246
|
+
return [] if type_object.is_a?(T::Types::TypedArray)
|
|
247
|
+
|
|
248
|
+
# Handle TypedHash (T::Hash[...])
|
|
249
|
+
return {} if type_object.is_a?(T::Types::TypedHash)
|
|
250
|
+
|
|
251
|
+
# Handle simple types
|
|
252
|
+
case type_object
|
|
253
|
+
when T::Types::Simple
|
|
254
|
+
raw_type = type_object.raw_type
|
|
255
|
+
case raw_type.to_s
|
|
256
|
+
when 'String' then ''
|
|
257
|
+
when 'Integer' then 0
|
|
258
|
+
when 'Float' then 0.0
|
|
259
|
+
when 'TrueClass', 'FalseClass' then false
|
|
260
|
+
else
|
|
261
|
+
# For T::Struct types, return nil as fallback
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
when T::Types::Union
|
|
265
|
+
# For unions, return nil (assuming it's nilable) or first non-nil default
|
|
266
|
+
nil
|
|
267
|
+
else
|
|
268
|
+
# Default fallback for unknown types
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
133
273
|
# Coerces an array value, converting each element as needed
|
|
134
274
|
sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
|
|
135
275
|
def coerce_array_value(value, prop_type)
|
|
@@ -152,7 +292,7 @@ module DSPy
|
|
|
152
292
|
# Convert string keys to enum instances if key_type is an enum
|
|
153
293
|
result = if enum_type?(key_type)
|
|
154
294
|
enum_class = extract_enum_class(key_type)
|
|
155
|
-
value.transform_keys { |k|
|
|
295
|
+
value.transform_keys { |k| DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, k) || k }
|
|
156
296
|
else
|
|
157
297
|
# For non-enum keys, coerce them to the expected type
|
|
158
298
|
value.transform_keys { |k| coerce_value_to_type(k, key_type) }
|
|
@@ -197,7 +337,19 @@ module DSPy
|
|
|
197
337
|
[key, val]
|
|
198
338
|
end
|
|
199
339
|
end.to_h
|
|
200
|
-
|
|
340
|
+
|
|
341
|
+
# Strip nil values for non-nilable fields that have defaults.
|
|
342
|
+
# LLMs in advisory mode may return null for unused fields.
|
|
343
|
+
# Removing the key lets Sorbet use the field's default value.
|
|
344
|
+
coerced_hash.reject! do |key, val|
|
|
345
|
+
next false unless val.nil?
|
|
346
|
+
prop_info = struct_props[key]
|
|
347
|
+
next false unless prop_info
|
|
348
|
+
prop_type = prop_info[:type_object] || prop_info[:type]
|
|
349
|
+
has_default = prop_info.key?(:default) || prop_info[:fully_optional]
|
|
350
|
+
!is_nilable_type?(prop_type) && has_default
|
|
351
|
+
end
|
|
352
|
+
|
|
201
353
|
# Create the struct instance
|
|
202
354
|
struct_class.new(**coerced_hash)
|
|
203
355
|
rescue ArgumentError => e
|
|
@@ -209,6 +361,17 @@ module DSPy
|
|
|
209
361
|
# Coerces a union value by using _type discriminator
|
|
210
362
|
sig { params(value: T.untyped, union_type: T.untyped).returns(T.untyped) }
|
|
211
363
|
def coerce_union_value(value, union_type)
|
|
364
|
+
# Anthropic tool use may return complex oneOf union fields as JSON strings
|
|
365
|
+
# instead of nested objects. Parse them back into Hashes for coercion.
|
|
366
|
+
if value.is_a?(String)
|
|
367
|
+
begin
|
|
368
|
+
parsed = JSON.parse(value)
|
|
369
|
+
value = parsed if parsed.is_a?(Hash)
|
|
370
|
+
rescue JSON::ParserError
|
|
371
|
+
# Not JSON — fall through
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
212
375
|
return value unless value.is_a?(Hash)
|
|
213
376
|
|
|
214
377
|
# Check for _type discriminator field
|
|
@@ -311,14 +474,10 @@ module DSPy
|
|
|
311
474
|
sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
|
|
312
475
|
def coerce_enum_value(value, prop_type)
|
|
313
476
|
enum_class = extract_enum_class(prop_type)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
# Otherwise, try to deserialize from string
|
|
319
|
-
enum_class.deserialize(value.to_s)
|
|
320
|
-
rescue ArgumentError, KeyError => e
|
|
321
|
-
DSPy.logger.debug("Failed to coerce to enum #{enum_class}: #{e.message}")
|
|
477
|
+
result = DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, value)
|
|
478
|
+
return result if result
|
|
479
|
+
|
|
480
|
+
DSPy.logger.debug("Failed to coerce to enum #{enum_class}: no match for #{value.inspect}")
|
|
322
481
|
value
|
|
323
482
|
end
|
|
324
483
|
end
|
data/lib/dspy/module.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'sorbet-runtime'
|
|
4
4
|
require 'dry-configurable'
|
|
5
5
|
require 'securerandom'
|
|
6
|
+
require 'weakref'
|
|
6
7
|
require_relative 'context'
|
|
7
8
|
require_relative 'callbacks'
|
|
8
9
|
require_relative 'type_serializer'
|
|
@@ -24,15 +25,20 @@ module DSPy
|
|
|
24
25
|
|
|
25
26
|
DEFAULT_MODULE_SUBSCRIPTION_SCOPE = SubcriptionScope::Descendants
|
|
26
27
|
|
|
28
|
+
# Hook to wrap forward methods with instrumentation.
|
|
29
|
+
# Uses a Set-based guard (not boolean) to prevent re-wrapping when
|
|
30
|
+
# other hooks (like Callbacks) also use define_method.
|
|
27
31
|
module ForwardOverrideHooks
|
|
28
32
|
def method_added(method_name)
|
|
29
33
|
super
|
|
30
34
|
|
|
31
35
|
return unless method_name == :forward
|
|
32
36
|
return if self == DSPy::Module
|
|
33
|
-
return if @_wrapping_forward
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
# Use Set-based guard - persists across hook invocations
|
|
39
|
+
@_forward_instrumented ||= Set.new
|
|
40
|
+
return if @_forward_instrumented.include?(object_id)
|
|
41
|
+
@_forward_instrumented << object_id
|
|
36
42
|
|
|
37
43
|
original = instance_method(:forward)
|
|
38
44
|
define_method(:forward) do |*args, **kwargs, &block|
|
|
@@ -40,8 +46,6 @@ module DSPy
|
|
|
40
46
|
original.bind(self).call(*args, **kwargs, &block)
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
|
-
ensure
|
|
44
|
-
@_wrapping_forward = false
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
|
|
@@ -71,6 +75,35 @@ module DSPy
|
|
|
71
75
|
|
|
72
76
|
private
|
|
73
77
|
|
|
78
|
+
def build_subscription_callback(weakref, subscription_id_ref, spec)
|
|
79
|
+
scope = spec[:scope] || DEFAULT_MODULE_SUBSCRIPTION_SCOPE
|
|
80
|
+
handler = spec[:handler]
|
|
81
|
+
block = spec[:block]
|
|
82
|
+
|
|
83
|
+
->(event_name, attributes) do
|
|
84
|
+
target = begin
|
|
85
|
+
weakref.__getobj__
|
|
86
|
+
rescue WeakRef::RefError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
unless target
|
|
91
|
+
subscription_id = subscription_id_ref[:id]
|
|
92
|
+
DSPy.events.unsubscribe(subscription_id) if subscription_id
|
|
93
|
+
DSPy.logger&.debug(event: 'module.subscription.auto_unsubscribe', subscription_id: subscription_id)
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return unless target.send(:module_event_within_scope?, attributes, scope)
|
|
98
|
+
|
|
99
|
+
if handler
|
|
100
|
+
target.send(handler, event_name, attributes)
|
|
101
|
+
else
|
|
102
|
+
target.instance_exec(event_name, attributes, &block)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
74
107
|
def validate_subscription_scope!(scope)
|
|
75
108
|
T.must(scope)
|
|
76
109
|
end
|
|
@@ -97,7 +130,8 @@ module DSPy
|
|
|
97
130
|
create_after_callback :forward
|
|
98
131
|
create_around_callback :forward
|
|
99
132
|
|
|
100
|
-
# The main forward method that users will call is generic and type parameterized
|
|
133
|
+
# The main forward method that users will call is generic and type parameterized.
|
|
134
|
+
# Instrument here only when subclasses don't override forward.
|
|
101
135
|
sig do
|
|
102
136
|
type_parameters(:I, :O)
|
|
103
137
|
.params(
|
|
@@ -106,10 +140,14 @@ module DSPy
|
|
|
106
140
|
.returns(T.type_parameter(:O))
|
|
107
141
|
end
|
|
108
142
|
def forward(**input_values)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
result = if self.class.instance_method(:forward).owner == DSPy::Module
|
|
144
|
+
instrument_forward_call([], input_values) do
|
|
145
|
+
forward_untyped(**input_values)
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
forward_untyped(**input_values)
|
|
112
149
|
end
|
|
150
|
+
T.cast(result, T.type_parameter(:O))
|
|
113
151
|
end
|
|
114
152
|
|
|
115
153
|
# The implementation method that subclasses must override
|
|
@@ -294,8 +332,28 @@ module DSPy
|
|
|
294
332
|
@module_subscriptions_registered = false
|
|
295
333
|
end
|
|
296
334
|
|
|
335
|
+
sig { returns(T.self_type) }
|
|
336
|
+
def dup_for_thread
|
|
337
|
+
cloned = dup
|
|
338
|
+
cloned.instance_variable_set(:@module_subscription_ids, [])
|
|
339
|
+
cloned.instance_variable_set(:@module_subscriptions_registered, false)
|
|
340
|
+
cloned.instance_variable_set(:@module_scope_id, SecureRandom.uuid)
|
|
341
|
+
cloned.send(:reset_thread_state)
|
|
342
|
+
cloned
|
|
343
|
+
end
|
|
344
|
+
|
|
297
345
|
private
|
|
298
346
|
|
|
347
|
+
def reset_thread_state
|
|
348
|
+
instance_variables.each do |ivar|
|
|
349
|
+
value = instance_variable_get(ivar)
|
|
350
|
+
case value
|
|
351
|
+
when Array, Hash, Set
|
|
352
|
+
instance_variable_set(ivar, value.dup)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
299
357
|
# Propagate LM configuration to child predictors recursively
|
|
300
358
|
# Skips children that already have an explicit LM configured
|
|
301
359
|
sig { params(lm: T.untyped).void }
|
|
@@ -322,30 +380,17 @@ module DSPy
|
|
|
322
380
|
|
|
323
381
|
@module_subscription_ids ||= []
|
|
324
382
|
specs.each do |spec|
|
|
325
|
-
|
|
383
|
+
weakref = WeakRef.new(self)
|
|
384
|
+
subscription_id_ref = { id: nil }
|
|
385
|
+
callback = self.class.send(:build_subscription_callback, weakref, subscription_id_ref, spec)
|
|
326
386
|
subscription_id = DSPy.events.subscribe(spec[:pattern], &callback)
|
|
387
|
+
subscription_id_ref[:id] = subscription_id
|
|
327
388
|
@module_subscription_ids << subscription_id
|
|
328
389
|
end
|
|
329
390
|
|
|
330
391
|
@module_subscriptions_registered = true
|
|
331
392
|
end
|
|
332
393
|
|
|
333
|
-
def build_subscription_callback(spec)
|
|
334
|
-
scope = spec[:scope] || DEFAULT_MODULE_SUBSCRIPTION_SCOPE
|
|
335
|
-
handler = spec[:handler]
|
|
336
|
-
block = spec[:block]
|
|
337
|
-
|
|
338
|
-
proc do |event_name, attributes|
|
|
339
|
-
next unless module_event_within_scope?(attributes, scope)
|
|
340
|
-
|
|
341
|
-
if handler
|
|
342
|
-
send(handler, event_name, attributes)
|
|
343
|
-
else
|
|
344
|
-
instance_exec(event_name, attributes, &block)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
394
|
def module_event_within_scope?(attributes, scope)
|
|
350
395
|
metadata = extract_module_metadata(attributes)
|
|
351
396
|
return false unless metadata
|
data/lib/dspy/predict.rb
CHANGED
|
@@ -64,8 +64,7 @@ module DSPy
|
|
|
64
64
|
super()
|
|
65
65
|
@signature_class = signature_class
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
@prompt = Prompt.from_signature(signature_class)
|
|
67
|
+
@prompt = build_prompt_from_signature
|
|
69
68
|
@demos = nil
|
|
70
69
|
end
|
|
71
70
|
|
|
@@ -146,6 +145,13 @@ module DSPy
|
|
|
146
145
|
instance
|
|
147
146
|
end
|
|
148
147
|
|
|
148
|
+
sig { override.params(block: T.proc.params(config: T.untyped).void).returns(T.self_type) }
|
|
149
|
+
def configure(&block)
|
|
150
|
+
super(&block)
|
|
151
|
+
sync_prompt_formats_from_lm(config.lm) if config.lm
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
149
155
|
sig { override.returns(T::Array[[String, DSPy::Module]]) }
|
|
150
156
|
def named_predictors
|
|
151
157
|
[["self", self]]
|
|
@@ -166,9 +172,6 @@ module DSPy
|
|
|
166
172
|
input_props = @signature_class.input_struct_class.props
|
|
167
173
|
coerced_input_values = coerce_output_attributes(input_values, input_props)
|
|
168
174
|
|
|
169
|
-
# Store coerced input values for optimization
|
|
170
|
-
@last_input_values = coerced_input_values.clone
|
|
171
|
-
|
|
172
175
|
# Validate input with coerced values
|
|
173
176
|
validate_input_struct(coerced_input_values)
|
|
174
177
|
|
|
@@ -190,6 +193,30 @@ module DSPy
|
|
|
190
193
|
|
|
191
194
|
private
|
|
192
195
|
|
|
196
|
+
def reset_thread_state
|
|
197
|
+
super
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def build_prompt_from_signature
|
|
201
|
+
lm_source = lm
|
|
202
|
+
schema_format = lm_source&.schema_format
|
|
203
|
+
data_format = lm_source&.respond_to?(:data_format) ? lm_source.data_format : nil
|
|
204
|
+
|
|
205
|
+
Prompt.from_signature(@signature_class, schema_format: schema_format, data_format: data_format)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def sync_prompt_formats_from_lm(lm_source)
|
|
209
|
+
return unless lm_source
|
|
210
|
+
|
|
211
|
+
schema_format = lm_source&.schema_format
|
|
212
|
+
data_format = lm_source&.respond_to?(:data_format) ? lm_source.data_format : nil
|
|
213
|
+
|
|
214
|
+
prompt = @prompt
|
|
215
|
+
prompt = prompt.with_schema_format(schema_format) if schema_format
|
|
216
|
+
prompt = prompt.with_data_format(data_format) if data_format
|
|
217
|
+
@prompt = prompt
|
|
218
|
+
end
|
|
219
|
+
|
|
193
220
|
# Validates input using signature struct (assumes input is already coerced)
|
|
194
221
|
sig { params(input_values: T::Hash[Symbol, T.untyped]).void }
|
|
195
222
|
def validate_input_struct(input_values)
|
data/lib/dspy/prediction.rb
CHANGED
|
@@ -122,9 +122,10 @@ module DSPy
|
|
|
122
122
|
converted[key] = nil
|
|
123
123
|
end
|
|
124
124
|
elsif is_enum_type?(prop_type) && value.is_a?(String)
|
|
125
|
-
# Convert string to enum
|
|
125
|
+
# Convert string to enum (case-insensitive for structured_outputs: false)
|
|
126
126
|
enum_class = extract_enum_class(prop_type)
|
|
127
|
-
|
|
127
|
+
result = DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, value)
|
|
128
|
+
converted[key] = result || value
|
|
128
129
|
elsif value.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
|
129
130
|
# Regular struct field that needs conversion
|
|
130
131
|
converted[key] = convert_to_struct(value, prop_type)
|
|
@@ -188,60 +189,12 @@ module DSPy
|
|
|
188
189
|
|
|
189
190
|
sig { params(type: T.untyped).returns(T::Boolean) }
|
|
190
191
|
def is_enum_type?(type)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
case type
|
|
194
|
-
when T::Types::Simple
|
|
195
|
-
# Handle regular enum types
|
|
196
|
-
begin
|
|
197
|
-
raw_type = type.raw_type
|
|
198
|
-
return false unless raw_type.is_a?(Class)
|
|
199
|
-
result = raw_type < T::Enum
|
|
200
|
-
return result == true # Force conversion to boolean
|
|
201
|
-
rescue StandardError
|
|
202
|
-
return false
|
|
203
|
-
end
|
|
204
|
-
when T::Private::Types::SimplePairUnion, T::Types::Union
|
|
205
|
-
# Handle T.nilable enum types
|
|
206
|
-
# Find the non-nil type and check if it's an enum
|
|
207
|
-
non_nil_types = if type.respond_to?(:types)
|
|
208
|
-
type.types.reject { |t| t.respond_to?(:raw_type) && t.raw_type == NilClass }
|
|
209
|
-
else
|
|
210
|
-
[]
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# For nilable types, we expect exactly one non-nil type
|
|
214
|
-
return false unless non_nil_types.size == 1
|
|
215
|
-
|
|
216
|
-
non_nil_type = non_nil_types.first
|
|
217
|
-
return is_enum_type?(non_nil_type) # Recursively check
|
|
218
|
-
else
|
|
219
|
-
return false
|
|
220
|
-
end
|
|
192
|
+
DSPy::Mixins::TypeCoercion.enum_type?(type)
|
|
221
193
|
end
|
|
222
194
|
|
|
223
195
|
sig { params(type: T.untyped).returns(T.untyped) }
|
|
224
196
|
def extract_enum_class(type)
|
|
225
|
-
|
|
226
|
-
when T::Types::Simple
|
|
227
|
-
# Regular enum type
|
|
228
|
-
type.raw_type
|
|
229
|
-
when T::Private::Types::SimplePairUnion, T::Types::Union
|
|
230
|
-
# Nilable enum type - find the non-nil type
|
|
231
|
-
non_nil_types = if type.respond_to?(:types)
|
|
232
|
-
type.types.reject { |t| t.respond_to?(:raw_type) && t.raw_type == NilClass }
|
|
233
|
-
else
|
|
234
|
-
[]
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
if non_nil_types.size == 1
|
|
238
|
-
extract_enum_class(non_nil_types.first)
|
|
239
|
-
else
|
|
240
|
-
raise ArgumentError, "Unable to extract enum class from complex union type: #{type.inspect}"
|
|
241
|
-
end
|
|
242
|
-
else
|
|
243
|
-
raise ArgumentError, "Not an enum type: #{type.inspect}"
|
|
244
|
-
end
|
|
197
|
+
DSPy::Mixins::TypeCoercion.extract_enum_class(type)
|
|
245
198
|
end
|
|
246
199
|
|
|
247
200
|
sig { params(union_type: T::Types::Union, discriminator_type: T.untyped).returns(T::Hash[String, T.untyped]) }
|
|
@@ -387,8 +340,10 @@ module DSPy
|
|
|
387
340
|
if prop_info
|
|
388
341
|
prop_type = prop_info[:type_object] || prop_info[:type]
|
|
389
342
|
if v.is_a?(String) && is_enum_type?(prop_type)
|
|
390
|
-
# Convert string to enum
|
|
391
|
-
|
|
343
|
+
# Convert string to enum (case-insensitive for structured_outputs: false)
|
|
344
|
+
enum_class = extract_enum_class(prop_type)
|
|
345
|
+
result = DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, v)
|
|
346
|
+
converted_hash[k] = result || v
|
|
392
347
|
elsif v.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
|
393
348
|
converted_hash[k] = convert_to_struct(v, prop_type)
|
|
394
349
|
elsif v.is_a?(Array) && needs_array_conversion?(prop_type)
|
|
@@ -488,8 +443,9 @@ module DSPy
|
|
|
488
443
|
convert_to_struct(element, element_type)
|
|
489
444
|
end
|
|
490
445
|
elsif element.is_a?(String) && is_enum_type?(element_type)
|
|
491
|
-
# Convert string to enum
|
|
492
|
-
element_type
|
|
446
|
+
# Convert string to enum (case-insensitive for structured_outputs: false)
|
|
447
|
+
enum_class = extract_enum_class(element_type)
|
|
448
|
+
DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, element) || element
|
|
493
449
|
else
|
|
494
450
|
element
|
|
495
451
|
end
|
|
@@ -539,7 +495,9 @@ module DSPy
|
|
|
539
495
|
if prop_info
|
|
540
496
|
prop_type = prop_info[:type_object] || prop_info[:type]
|
|
541
497
|
if v.is_a?(String) && is_enum_type?(prop_type)
|
|
542
|
-
|
|
498
|
+
enum_class = extract_enum_class(prop_type)
|
|
499
|
+
result = DSPy::Mixins::TypeCoercion.deserialize_enum(enum_class, v)
|
|
500
|
+
converted_hash[k] = result || v
|
|
543
501
|
elsif v.is_a?(Hash) && needs_struct_conversion?(prop_type)
|
|
544
502
|
converted_hash[k] = convert_to_struct(v, prop_type)
|
|
545
503
|
elsif v.is_a?(Array) && needs_array_conversion?(prop_type)
|