dspy 0.15.2 → 0.15.4
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 +5 -3
- data/lib/dspy/instrumentation/event_payload_factory.rb +282 -0
- data/lib/dspy/instrumentation/event_payloads.rb +476 -0
- data/lib/dspy/instrumentation.rb +36 -4
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +11 -6
- data/lib/dspy/lm/adapters/openai_adapter.rb +13 -8
- data/lib/dspy/lm/message.rb +99 -0
- data/lib/dspy/lm/message_builder.rb +26 -3
- data/lib/dspy/lm/response.rb +143 -14
- data/lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb +2 -2
- data/lib/dspy/lm.rb +95 -19
- data/lib/dspy/mixins/type_coercion.rb +64 -2
- data/lib/dspy/subscribers/otel_subscriber.rb +3 -4
- data/lib/dspy/version.rb +1 -1
- metadata +6 -6
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
class LM
|
7
|
+
# Type-safe representation of chat messages
|
8
|
+
class Message < T::Struct
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
# Role enum for type safety
|
12
|
+
class Role < T::Enum
|
13
|
+
enums do
|
14
|
+
System = new('system')
|
15
|
+
User = new('user')
|
16
|
+
Assistant = new('assistant')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
const :role, Role
|
21
|
+
const :content, String
|
22
|
+
const :name, T.nilable(String), default: nil
|
23
|
+
|
24
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
25
|
+
def to_h
|
26
|
+
base = {
|
27
|
+
role: role.serialize,
|
28
|
+
content: content
|
29
|
+
}
|
30
|
+
base[:name] = name if name
|
31
|
+
base
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { returns(String) }
|
35
|
+
def to_s
|
36
|
+
name ? "#{role.serialize}(#{name}): #{content}" : "#{role.serialize}: #{content}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Factory for creating Message objects from various formats
|
41
|
+
module MessageFactory
|
42
|
+
extend T::Sig
|
43
|
+
|
44
|
+
sig { params(message_data: T.untyped).returns(T.nilable(Message)) }
|
45
|
+
def self.create(message_data)
|
46
|
+
return nil if message_data.nil?
|
47
|
+
|
48
|
+
# Already a Message? Return as-is
|
49
|
+
return message_data if message_data.is_a?(Message)
|
50
|
+
|
51
|
+
# Convert to hash if needed
|
52
|
+
if message_data.respond_to?(:to_h)
|
53
|
+
message_data = message_data.to_h
|
54
|
+
end
|
55
|
+
|
56
|
+
return nil unless message_data.is_a?(Hash)
|
57
|
+
|
58
|
+
# Normalize keys to symbols
|
59
|
+
normalized = message_data.transform_keys(&:to_sym)
|
60
|
+
|
61
|
+
create_from_hash(normalized)
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(messages: T::Array[T.untyped]).returns(T::Array[Message]) }
|
65
|
+
def self.create_many(messages)
|
66
|
+
messages.compact.map { |m| create(m) }.compact
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Message)) }
|
72
|
+
def self.create_from_hash(data)
|
73
|
+
role_str = data[:role]&.to_s
|
74
|
+
content = data[:content]&.to_s
|
75
|
+
|
76
|
+
return nil if role_str.nil? || content.nil?
|
77
|
+
|
78
|
+
# Convert string role to enum
|
79
|
+
role = case role_str
|
80
|
+
when 'system' then Message::Role::System
|
81
|
+
when 'user' then Message::Role::User
|
82
|
+
when 'assistant' then Message::Role::Assistant
|
83
|
+
else
|
84
|
+
DSPy.logger.debug("Unknown message role: #{role_str}")
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
|
88
|
+
Message.new(
|
89
|
+
role: role,
|
90
|
+
content: content,
|
91
|
+
name: data[:name]&.to_s
|
92
|
+
)
|
93
|
+
rescue => e
|
94
|
+
DSPy.logger.debug("Failed to create Message: #{e.message}")
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -1,28 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'message'
|
4
|
+
|
3
5
|
module DSPy
|
4
6
|
class LM
|
5
7
|
class MessageBuilder
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(T::Array[Message]) }
|
6
11
|
attr_reader :messages
|
7
12
|
|
8
13
|
def initialize
|
9
14
|
@messages = []
|
10
15
|
end
|
11
16
|
|
17
|
+
sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
|
12
18
|
def system(content)
|
13
|
-
@messages <<
|
19
|
+
@messages << Message.new(
|
20
|
+
role: Message::Role::System,
|
21
|
+
content: content.to_s
|
22
|
+
)
|
14
23
|
self
|
15
24
|
end
|
16
25
|
|
26
|
+
sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
|
17
27
|
def user(content)
|
18
|
-
@messages <<
|
28
|
+
@messages << Message.new(
|
29
|
+
role: Message::Role::User,
|
30
|
+
content: content.to_s
|
31
|
+
)
|
19
32
|
self
|
20
33
|
end
|
21
34
|
|
35
|
+
sig { params(content: T.any(String, T.untyped)).returns(MessageBuilder) }
|
22
36
|
def assistant(content)
|
23
|
-
@messages <<
|
37
|
+
@messages << Message.new(
|
38
|
+
role: Message::Role::Assistant,
|
39
|
+
content: content.to_s
|
40
|
+
)
|
24
41
|
self
|
25
42
|
end
|
43
|
+
|
44
|
+
# For backward compatibility, allow conversion to hash array
|
45
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
46
|
+
def to_h
|
47
|
+
@messages.map(&:to_h)
|
48
|
+
end
|
26
49
|
end
|
27
50
|
end
|
28
51
|
end
|
data/lib/dspy/lm/response.rb
CHANGED
@@ -1,29 +1,158 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'sorbet-runtime'
|
3
4
|
require_relative 'usage'
|
4
5
|
|
5
6
|
module DSPy
|
6
7
|
class LM
|
7
|
-
#
|
8
|
-
class
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
# Base metadata struct for common fields across providers
|
9
|
+
class ResponseMetadata < T::Struct
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
const :provider, String
|
13
|
+
const :model, String
|
14
|
+
const :response_id, T.nilable(String), default: nil
|
15
|
+
const :created, T.nilable(Integer), default: nil
|
16
|
+
const :structured_output, T.nilable(T::Boolean), default: nil
|
17
|
+
|
18
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
19
|
+
def to_h
|
20
|
+
hash = {
|
21
|
+
provider: provider,
|
22
|
+
model: model
|
23
|
+
}
|
24
|
+
hash[:response_id] = response_id if response_id
|
25
|
+
hash[:created] = created if created
|
26
|
+
hash[:structured_output] = structured_output unless structured_output.nil?
|
27
|
+
hash
|
15
28
|
end
|
16
|
-
|
29
|
+
end
|
30
|
+
|
31
|
+
# OpenAI-specific metadata with additional fields
|
32
|
+
class OpenAIResponseMetadata < T::Struct
|
33
|
+
extend T::Sig
|
34
|
+
|
35
|
+
const :provider, String
|
36
|
+
const :model, String
|
37
|
+
const :response_id, T.nilable(String), default: nil
|
38
|
+
const :created, T.nilable(Integer), default: nil
|
39
|
+
const :structured_output, T.nilable(T::Boolean), default: nil
|
40
|
+
const :system_fingerprint, T.nilable(String), default: nil
|
41
|
+
const :finish_reason, T.nilable(String), default: nil
|
42
|
+
|
43
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
44
|
+
def to_h
|
45
|
+
hash = {
|
46
|
+
provider: provider,
|
47
|
+
model: model
|
48
|
+
}
|
49
|
+
hash[:response_id] = response_id if response_id
|
50
|
+
hash[:created] = created if created
|
51
|
+
hash[:structured_output] = structured_output unless structured_output.nil?
|
52
|
+
hash[:system_fingerprint] = system_fingerprint if system_fingerprint
|
53
|
+
hash[:finish_reason] = finish_reason if finish_reason
|
54
|
+
hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Anthropic-specific metadata with additional fields
|
59
|
+
class AnthropicResponseMetadata < T::Struct
|
60
|
+
extend T::Sig
|
61
|
+
|
62
|
+
const :provider, String
|
63
|
+
const :model, String
|
64
|
+
const :response_id, T.nilable(String), default: nil
|
65
|
+
const :created, T.nilable(Integer), default: nil
|
66
|
+
const :structured_output, T.nilable(T::Boolean), default: nil
|
67
|
+
const :stop_reason, T.nilable(String), default: nil
|
68
|
+
const :stop_sequence, T.nilable(String), default: nil
|
69
|
+
const :tool_calls, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]), default: nil
|
70
|
+
|
71
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
72
|
+
def to_h
|
73
|
+
hash = {
|
74
|
+
provider: provider,
|
75
|
+
model: model
|
76
|
+
}
|
77
|
+
hash[:response_id] = response_id if response_id
|
78
|
+
hash[:created] = created if created
|
79
|
+
hash[:structured_output] = structured_output unless structured_output.nil?
|
80
|
+
hash[:stop_reason] = stop_reason if stop_reason
|
81
|
+
hash[:stop_sequence] = stop_sequence if stop_sequence
|
82
|
+
hash[:tool_calls] = tool_calls if tool_calls
|
83
|
+
hash
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Normalized response format for all LM providers
|
88
|
+
class Response < T::Struct
|
89
|
+
extend T::Sig
|
90
|
+
|
91
|
+
const :content, String
|
92
|
+
const :usage, T.nilable(T.any(Usage, OpenAIUsage)), default: nil
|
93
|
+
const :metadata, T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, T::Hash[Symbol, T.untyped])
|
94
|
+
|
95
|
+
sig { returns(String) }
|
17
96
|
def to_s
|
18
97
|
content
|
19
98
|
end
|
20
|
-
|
99
|
+
|
100
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
21
101
|
def to_h
|
22
|
-
{
|
23
|
-
content: content
|
24
|
-
|
25
|
-
|
102
|
+
hash = {
|
103
|
+
content: content
|
104
|
+
}
|
105
|
+
hash[:usage] = usage.to_h if usage
|
106
|
+
hash[:metadata] = metadata.is_a?(Hash) ? metadata : metadata.to_h
|
107
|
+
hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Factory for creating response metadata objects
|
112
|
+
module ResponseMetadataFactory
|
113
|
+
extend T::Sig
|
114
|
+
|
115
|
+
sig { params(provider: String, metadata: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata)) }
|
116
|
+
def self.create(provider, metadata)
|
117
|
+
# Handle nil metadata
|
118
|
+
metadata ||= {}
|
119
|
+
|
120
|
+
# Normalize provider name
|
121
|
+
provider_name = provider.to_s.downcase
|
122
|
+
|
123
|
+
# Extract common fields
|
124
|
+
common_fields = {
|
125
|
+
provider: provider,
|
126
|
+
model: metadata[:model] || 'unknown',
|
127
|
+
response_id: metadata[:response_id] || metadata[:id],
|
128
|
+
created: metadata[:created],
|
129
|
+
structured_output: metadata[:structured_output]
|
26
130
|
}
|
131
|
+
|
132
|
+
case provider_name
|
133
|
+
when 'openai'
|
134
|
+
OpenAIResponseMetadata.new(
|
135
|
+
**common_fields,
|
136
|
+
system_fingerprint: metadata[:system_fingerprint],
|
137
|
+
finish_reason: metadata[:finish_reason]&.to_s
|
138
|
+
)
|
139
|
+
when 'anthropic'
|
140
|
+
AnthropicResponseMetadata.new(
|
141
|
+
**common_fields,
|
142
|
+
stop_reason: metadata[:stop_reason]&.to_s,
|
143
|
+
stop_sequence: metadata[:stop_sequence]&.to_s,
|
144
|
+
tool_calls: metadata[:tool_calls]
|
145
|
+
)
|
146
|
+
else
|
147
|
+
ResponseMetadata.new(**common_fields)
|
148
|
+
end
|
149
|
+
rescue => e
|
150
|
+
DSPy.logger.debug("Failed to create response metadata: #{e.message}")
|
151
|
+
# Fallback to basic metadata
|
152
|
+
ResponseMetadata.new(
|
153
|
+
provider: provider,
|
154
|
+
model: metadata[:model] || 'unknown'
|
155
|
+
)
|
27
156
|
end
|
28
157
|
end
|
29
158
|
end
|
@@ -50,8 +50,8 @@ module DSPy
|
|
50
50
|
# Extract JSON from tool use response
|
51
51
|
begin
|
52
52
|
# Check for tool calls in metadata first (this is the primary method)
|
53
|
-
if response.metadata && response.metadata
|
54
|
-
tool_calls = response.metadata
|
53
|
+
if response.metadata.respond_to?(:tool_calls) && response.metadata.tool_calls
|
54
|
+
tool_calls = response.metadata.tool_calls
|
55
55
|
if tool_calls.is_a?(Array) && !tool_calls.empty?
|
56
56
|
first_call = tool_calls.first
|
57
57
|
if first_call[:name] == "json_output" && first_call[:input]
|
data/lib/dspy/lm.rb
CHANGED
@@ -19,7 +19,8 @@ require_relative 'lm/adapters/ollama_adapter'
|
|
19
19
|
require_relative 'lm/strategy_selector'
|
20
20
|
require_relative 'lm/retry_handler'
|
21
21
|
|
22
|
-
# Load message builder
|
22
|
+
# Load message builder and message types
|
23
|
+
require_relative 'lm/message'
|
23
24
|
require_relative 'lm/message_builder'
|
24
25
|
|
25
26
|
module DSPy
|
@@ -78,8 +79,8 @@ module DSPy
|
|
78
79
|
streaming_block = block
|
79
80
|
end
|
80
81
|
|
81
|
-
#
|
82
|
-
|
82
|
+
# Normalize and validate messages
|
83
|
+
messages = normalize_messages(messages)
|
83
84
|
|
84
85
|
# Execute with instrumentation
|
85
86
|
execute_raw_chat(messages, &streaming_block)
|
@@ -106,16 +107,19 @@ module DSPy
|
|
106
107
|
end
|
107
108
|
|
108
109
|
def execute_chat_with_strategy(messages, signature_class, strategy, &block)
|
110
|
+
# Convert messages to hash format for strategy and adapter
|
111
|
+
hash_messages = messages_to_hash_array(messages)
|
112
|
+
|
109
113
|
# Prepare request with strategy-specific modifications
|
110
114
|
request_params = {}
|
111
|
-
strategy.prepare_request(
|
115
|
+
strategy.prepare_request(hash_messages.dup, request_params)
|
112
116
|
|
113
117
|
# Make the request
|
114
118
|
response = if request_params.any?
|
115
119
|
# Pass additional parameters if strategy added them
|
116
|
-
adapter.chat(messages:
|
120
|
+
adapter.chat(messages: hash_messages, signature: signature_class, **request_params, &block)
|
117
121
|
else
|
118
|
-
adapter.chat(messages:
|
122
|
+
adapter.chat(messages: hash_messages, signature: signature_class, &block)
|
119
123
|
end
|
120
124
|
|
121
125
|
# Let strategy handle JSON extraction if needed
|
@@ -171,11 +175,19 @@ module DSPy
|
|
171
175
|
|
172
176
|
# Add system message
|
173
177
|
system_prompt = inference_module.system_signature
|
174
|
-
|
178
|
+
if system_prompt
|
179
|
+
messages << Message.new(
|
180
|
+
role: Message::Role::System,
|
181
|
+
content: system_prompt
|
182
|
+
)
|
183
|
+
end
|
175
184
|
|
176
185
|
# Add user message
|
177
186
|
user_prompt = inference_module.user_signature(input_values)
|
178
|
-
messages <<
|
187
|
+
messages << Message.new(
|
188
|
+
role: Message::Role::User,
|
189
|
+
content: user_prompt
|
190
|
+
)
|
179
191
|
|
180
192
|
messages
|
181
193
|
end
|
@@ -205,7 +217,14 @@ module DSPy
|
|
205
217
|
|
206
218
|
# Common instrumentation method for LM requests
|
207
219
|
def instrument_lm_request(messages, signature_class_name, &execution_block)
|
208
|
-
|
220
|
+
# Handle both Message objects and hash format
|
221
|
+
input_text = messages.map do |m|
|
222
|
+
if m.is_a?(Message)
|
223
|
+
m.content
|
224
|
+
else
|
225
|
+
m[:content]
|
226
|
+
end
|
227
|
+
end.join(' ')
|
209
228
|
input_size = input_text.length
|
210
229
|
|
211
230
|
response = nil
|
@@ -252,27 +271,84 @@ module DSPy
|
|
252
271
|
raise ArgumentError, "messages must be an array"
|
253
272
|
end
|
254
273
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
274
|
+
messages.each_with_index do |message, index|
|
275
|
+
# Accept both Message objects and hash format for backward compatibility
|
276
|
+
if message.is_a?(Message)
|
277
|
+
# Already validated by type system
|
278
|
+
next
|
279
|
+
elsif message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
|
280
|
+
# Legacy hash format - validate role
|
281
|
+
valid_roles = %w[system user assistant]
|
282
|
+
unless valid_roles.include?(message[:role])
|
283
|
+
raise ArgumentError, "Invalid role at index #{index}: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
284
|
+
end
|
285
|
+
else
|
286
|
+
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
264
287
|
end
|
265
288
|
end
|
266
289
|
end
|
267
290
|
|
268
291
|
def execute_raw_chat(messages, &streaming_block)
|
269
292
|
response = instrument_lm_request(messages, 'RawPrompt') do
|
293
|
+
# Convert messages to hash format for adapter
|
294
|
+
hash_messages = messages_to_hash_array(messages)
|
270
295
|
# Direct adapter call, no strategies or JSON parsing
|
271
|
-
adapter.chat(messages:
|
296
|
+
adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
|
272
297
|
end
|
273
298
|
|
274
299
|
# Return raw response content, not parsed JSON
|
275
300
|
response.content
|
276
301
|
end
|
302
|
+
|
303
|
+
# Convert messages to normalized Message objects
|
304
|
+
def normalize_messages(messages)
|
305
|
+
# Validate array format first
|
306
|
+
unless messages.is_a?(Array)
|
307
|
+
raise ArgumentError, "messages must be an array"
|
308
|
+
end
|
309
|
+
|
310
|
+
return messages if messages.all? { |m| m.is_a?(Message) }
|
311
|
+
|
312
|
+
# Convert hash messages to Message objects
|
313
|
+
normalized = []
|
314
|
+
messages.each_with_index do |msg, index|
|
315
|
+
if msg.is_a?(Message)
|
316
|
+
normalized << msg
|
317
|
+
elsif msg.is_a?(Hash)
|
318
|
+
# Validate hash has required fields
|
319
|
+
unless msg.key?(:role) && msg.key?(:content)
|
320
|
+
raise ArgumentError, "Message at index #{index} must have :role and :content"
|
321
|
+
end
|
322
|
+
|
323
|
+
# Validate role
|
324
|
+
valid_roles = %w[system user assistant]
|
325
|
+
unless valid_roles.include?(msg[:role])
|
326
|
+
raise ArgumentError, "Invalid role at index #{index}: #{msg[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
327
|
+
end
|
328
|
+
|
329
|
+
# Create Message object
|
330
|
+
message = MessageFactory.create(msg)
|
331
|
+
if message.nil?
|
332
|
+
raise ArgumentError, "Failed to create Message from hash at index #{index}"
|
333
|
+
end
|
334
|
+
normalized << message
|
335
|
+
else
|
336
|
+
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
normalized
|
341
|
+
end
|
342
|
+
|
343
|
+
# Convert Message objects to hash array for adapters
|
344
|
+
def messages_to_hash_array(messages)
|
345
|
+
messages.map do |msg|
|
346
|
+
if msg.is_a?(Message)
|
347
|
+
msg.to_h
|
348
|
+
else
|
349
|
+
msg
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
277
353
|
end
|
278
354
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed: strict
|
1
|
+
54 # typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'sorbet-runtime'
|
@@ -30,6 +30,8 @@ module DSPy
|
|
30
30
|
return value if value.nil?
|
31
31
|
|
32
32
|
case prop_type
|
33
|
+
when ->(type) { union_type?(type) }
|
34
|
+
coerce_union_value(value, prop_type)
|
33
35
|
when ->(type) { array_type?(type) }
|
34
36
|
coerce_array_value(value, prop_type)
|
35
37
|
when ->(type) { enum_type?(type) }
|
@@ -100,6 +102,18 @@ module DSPy
|
|
100
102
|
false
|
101
103
|
end
|
102
104
|
|
105
|
+
# Checks if a type is a union type (T.any)
|
106
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
107
|
+
def union_type?(type)
|
108
|
+
type.is_a?(T::Types::Union) && !is_nilable_type?(type)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks if a type is nilable (contains NilClass)
|
112
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
113
|
+
def is_nilable_type?(type)
|
114
|
+
type.is_a?(T::Types::Union) && type.types.any? { |t| t == T::Utils.coerce(NilClass) }
|
115
|
+
end
|
116
|
+
|
103
117
|
# Coerces an array value, converting each element as needed
|
104
118
|
sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
|
105
119
|
def coerce_array_value(value, prop_type)
|
@@ -133,6 +147,54 @@ module DSPy
|
|
133
147
|
DSPy.logger.debug("Failed to coerce to struct #{struct_class}: #{e.message}")
|
134
148
|
value
|
135
149
|
end
|
150
|
+
|
151
|
+
# Coerces a union value by using _type discriminator
|
152
|
+
sig { params(value: T.untyped, union_type: T.untyped).returns(T.untyped) }
|
153
|
+
def coerce_union_value(value, union_type)
|
154
|
+
return value unless value.is_a?(Hash)
|
155
|
+
|
156
|
+
# Check for _type discriminator field
|
157
|
+
type_name = value[:_type] || value["_type"]
|
158
|
+
return value unless type_name
|
159
|
+
|
160
|
+
# Find matching struct type in the union
|
161
|
+
union_type.types.each do |type|
|
162
|
+
next if type == T::Utils.coerce(NilClass)
|
163
|
+
|
164
|
+
if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
|
165
|
+
struct_name = type.raw_type.name.split('::').last
|
166
|
+
if struct_name == type_name
|
167
|
+
# Convert string keys to symbols and remove _type
|
168
|
+
symbolized_hash = value.transform_keys(&:to_sym)
|
169
|
+
symbolized_hash.delete(:_type)
|
170
|
+
|
171
|
+
# Coerce struct field values based on their types
|
172
|
+
struct_class = type.raw_type
|
173
|
+
struct_props = struct_class.props
|
174
|
+
|
175
|
+
coerced_hash = {}
|
176
|
+
symbolized_hash.each do |key, val|
|
177
|
+
if struct_props[key]
|
178
|
+
prop_type = struct_props[key][:type_object] || struct_props[key][:type]
|
179
|
+
coerced_hash[key] = coerce_value_to_type(val, prop_type)
|
180
|
+
else
|
181
|
+
coerced_hash[key] = val
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create the struct instance with coerced values
|
186
|
+
return struct_class.new(**coerced_hash)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# If no matching type found, return original value
|
192
|
+
value
|
193
|
+
rescue ArgumentError => e
|
194
|
+
# If struct creation fails, return the original value
|
195
|
+
DSPy.logger.debug("Failed to coerce union type: #{e.message}")
|
196
|
+
value
|
197
|
+
end
|
136
198
|
end
|
137
199
|
end
|
138
|
-
end
|
200
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'sorbet-runtime'
|
4
|
+
require_relative '../instrumentation/event_payloads'
|
4
5
|
|
5
6
|
begin
|
6
7
|
require 'opentelemetry/api'
|
@@ -402,10 +403,8 @@ module DSPy
|
|
402
403
|
'dspy.lm.model' => payload[:gen_ai_request_model] || payload[:model],
|
403
404
|
'dspy.lm.status' => payload[:status],
|
404
405
|
'dspy.lm.duration_ms' => payload[:duration_ms],
|
405
|
-
'dspy.lm.
|
406
|
-
'dspy.lm.
|
407
|
-
'dspy.lm.tokens_output' => payload[:tokens_output],
|
408
|
-
'dspy.lm.cost' => payload[:cost]
|
406
|
+
'dspy.lm.adapter_class' => payload[:adapter_class],
|
407
|
+
'dspy.lm.input_size' => payload[:input_size]
|
409
408
|
}
|
410
409
|
) do |span|
|
411
410
|
if payload[:status] == 'error'
|
data/lib/dspy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dspy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.15.
|
4
|
+
version: 0.15.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vicente Reig Rincón de Arellano
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-08-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: dry-configurable
|
@@ -167,6 +166,8 @@ files:
|
|
167
166
|
- lib/dspy/few_shot_example.rb
|
168
167
|
- lib/dspy/field.rb
|
169
168
|
- lib/dspy/instrumentation.rb
|
169
|
+
- lib/dspy/instrumentation/event_payload_factory.rb
|
170
|
+
- lib/dspy/instrumentation/event_payloads.rb
|
170
171
|
- lib/dspy/instrumentation/token_tracker.rb
|
171
172
|
- lib/dspy/lm.rb
|
172
173
|
- lib/dspy/lm/adapter.rb
|
@@ -177,6 +178,7 @@ files:
|
|
177
178
|
- lib/dspy/lm/adapters/openai_adapter.rb
|
178
179
|
- lib/dspy/lm/cache_manager.rb
|
179
180
|
- lib/dspy/lm/errors.rb
|
181
|
+
- lib/dspy/lm/message.rb
|
180
182
|
- lib/dspy/lm/message_builder.rb
|
181
183
|
- lib/dspy/lm/response.rb
|
182
184
|
- lib/dspy/lm/retry_handler.rb
|
@@ -232,7 +234,6 @@ homepage: https://github.com/vicentereig/dspy.rb
|
|
232
234
|
licenses:
|
233
235
|
- MIT
|
234
236
|
metadata: {}
|
235
|
-
post_install_message:
|
236
237
|
rdoc_options: []
|
237
238
|
require_paths:
|
238
239
|
- lib
|
@@ -247,8 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
247
248
|
- !ruby/object:Gem::Version
|
248
249
|
version: '0'
|
249
250
|
requirements: []
|
250
|
-
rubygems_version: 3.5
|
251
|
-
signing_key:
|
251
|
+
rubygems_version: 3.6.5
|
252
252
|
specification_version: 4
|
253
253
|
summary: The Ruby framework for programming—rather than prompting—language models.
|
254
254
|
test_files: []
|