dspy 0.15.2 → 0.15.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.
@@ -0,0 +1,476 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ module DSPy
6
+ module Instrumentation
7
+ # Type-safe event payload structures for DSPy instrumentation
8
+ # Each event is a complete T::Struct (no inheritance due to T::Struct limitations)
9
+
10
+ # LM Request event payload
11
+ class LMRequestEvent < T::Struct
12
+ extend T::Sig
13
+
14
+ # Common fields
15
+ const :timestamp, String
16
+ const :duration_ms, Float
17
+ const :cpu_time_ms, Float
18
+ const :status, String
19
+
20
+ # LM-specific fields
21
+ const :gen_ai_operation_name, String
22
+ const :gen_ai_system, String
23
+ const :gen_ai_request_model, String
24
+ const :signature_class, T.nilable(String), default: nil
25
+ const :provider, String
26
+ const :adapter_class, String
27
+ const :input_size, Integer
28
+
29
+ # Error fields (optional)
30
+ const :error_type, T.nilable(String), default: nil
31
+ const :error_message, T.nilable(String), default: nil
32
+
33
+ sig { returns(T::Hash[Symbol, T.untyped]) }
34
+ def to_h
35
+ hash = {
36
+ timestamp: timestamp,
37
+ duration_ms: duration_ms,
38
+ cpu_time_ms: cpu_time_ms,
39
+ status: status,
40
+ gen_ai_operation_name: gen_ai_operation_name,
41
+ gen_ai_system: gen_ai_system,
42
+ gen_ai_request_model: gen_ai_request_model,
43
+ provider: provider,
44
+ adapter_class: adapter_class,
45
+ input_size: input_size
46
+ }
47
+ hash[:signature_class] = signature_class if signature_class
48
+ hash[:error_type] = error_type if error_type
49
+ hash[:error_message] = error_message if error_message
50
+ hash
51
+ end
52
+ end
53
+
54
+ # Token usage event payload
55
+ class LMTokensEvent < T::Struct
56
+ extend T::Sig
57
+
58
+ # Common fields
59
+ const :timestamp, String
60
+ const :status, String
61
+
62
+ # Token-specific fields
63
+ const :input_tokens, Integer
64
+ const :output_tokens, Integer
65
+ const :total_tokens, Integer
66
+ const :gen_ai_system, String
67
+ const :gen_ai_request_model, String
68
+ const :signature_class, T.nilable(String), default: nil
69
+
70
+ sig { returns(T::Hash[Symbol, T.untyped]) }
71
+ def to_h
72
+ hash = {
73
+ timestamp: timestamp,
74
+ status: status,
75
+ input_tokens: input_tokens,
76
+ output_tokens: output_tokens,
77
+ total_tokens: total_tokens,
78
+ gen_ai_system: gen_ai_system,
79
+ gen_ai_request_model: gen_ai_request_model
80
+ }
81
+ hash[:signature_class] = signature_class if signature_class
82
+ hash
83
+ end
84
+ end
85
+
86
+ # LM Response parsed event payload
87
+ class LMResponseParsedEvent < T::Struct
88
+ extend T::Sig
89
+
90
+ # Common fields
91
+ const :timestamp, String
92
+ const :duration_ms, Float
93
+ const :cpu_time_ms, Float
94
+ const :status, String
95
+
96
+ # Response parsing fields
97
+ const :signature_class, String
98
+ const :provider, String
99
+ const :success, T::Boolean
100
+ const :response_length, Integer
101
+ const :parse_type, T.nilable(String), default: nil
102
+
103
+ # Error fields (optional)
104
+ const :error_type, T.nilable(String), default: nil
105
+ const :error_message, T.nilable(String), default: nil
106
+
107
+ sig { returns(T::Hash[Symbol, T.untyped]) }
108
+ def to_h
109
+ hash = {
110
+ timestamp: timestamp,
111
+ duration_ms: duration_ms,
112
+ cpu_time_ms: cpu_time_ms,
113
+ status: status,
114
+ signature_class: signature_class,
115
+ provider: provider,
116
+ success: success,
117
+ response_length: response_length
118
+ }
119
+ hash[:parse_type] = parse_type if parse_type
120
+ hash[:error_type] = error_type if error_type
121
+ hash[:error_message] = error_message if error_message
122
+ hash
123
+ end
124
+ end
125
+
126
+ # Predict event payload
127
+ class PredictEvent < T::Struct
128
+ extend T::Sig
129
+
130
+ # Common fields
131
+ const :timestamp, String
132
+ const :duration_ms, Float
133
+ const :cpu_time_ms, Float
134
+ const :status, String
135
+
136
+ # Predict-specific fields
137
+ const :signature_class, String
138
+ const :module_name, String
139
+ const :model, String
140
+ const :provider, String
141
+ const :input_fields, T::Array[String]
142
+ const :input_size, T.nilable(Integer), default: nil
143
+ const :output_size, T.nilable(Integer), default: nil
144
+
145
+ # Error fields (optional)
146
+ const :error_type, T.nilable(String), default: nil
147
+ const :error_message, T.nilable(String), default: nil
148
+
149
+ sig { returns(T::Hash[Symbol, T.untyped]) }
150
+ def to_h
151
+ hash = {
152
+ timestamp: timestamp,
153
+ duration_ms: duration_ms,
154
+ cpu_time_ms: cpu_time_ms,
155
+ status: status,
156
+ signature_class: signature_class,
157
+ module_name: module_name,
158
+ model: model,
159
+ provider: provider,
160
+ input_fields: input_fields
161
+ }
162
+ hash[:input_size] = input_size if input_size
163
+ hash[:output_size] = output_size if output_size
164
+ hash[:error_type] = error_type if error_type
165
+ hash[:error_message] = error_message if error_message
166
+ hash
167
+ end
168
+ end
169
+
170
+ # Chain of Thought event payload
171
+ class ChainOfThoughtEvent < T::Struct
172
+ extend T::Sig
173
+
174
+ # Common fields
175
+ const :timestamp, String
176
+ const :duration_ms, Float
177
+ const :cpu_time_ms, Float
178
+ const :status, String
179
+
180
+ # CoT-specific fields
181
+ const :signature_class, String
182
+ const :module_name, String
183
+ const :model, String
184
+ const :provider, String
185
+ const :reasoning_length, T.nilable(Integer), default: nil
186
+ const :answer_length, T.nilable(Integer), default: nil
187
+
188
+ # Error fields (optional)
189
+ const :error_type, T.nilable(String), default: nil
190
+ const :error_message, T.nilable(String), default: nil
191
+
192
+ sig { returns(T::Hash[Symbol, T.untyped]) }
193
+ def to_h
194
+ hash = {
195
+ timestamp: timestamp,
196
+ duration_ms: duration_ms,
197
+ cpu_time_ms: cpu_time_ms,
198
+ status: status,
199
+ signature_class: signature_class,
200
+ module_name: module_name,
201
+ model: model,
202
+ provider: provider
203
+ }
204
+ hash[:reasoning_length] = reasoning_length if reasoning_length
205
+ hash[:answer_length] = answer_length if answer_length
206
+ hash[:error_type] = error_type if error_type
207
+ hash[:error_message] = error_message if error_message
208
+ hash
209
+ end
210
+ end
211
+
212
+ # ReAct iteration event payload
213
+ class ReactIterationEvent < T::Struct
214
+ extend T::Sig
215
+
216
+ # Common fields
217
+ const :timestamp, String
218
+ const :duration_ms, Float
219
+ const :cpu_time_ms, Float
220
+ const :status, String
221
+
222
+ # ReAct-specific fields
223
+ const :iteration, Integer
224
+ const :max_iterations, Integer
225
+ const :history_length, Integer
226
+ const :tools_used_so_far, T::Array[String]
227
+
228
+ # Error fields (optional)
229
+ const :error_type, T.nilable(String), default: nil
230
+ const :error_message, T.nilable(String), default: nil
231
+
232
+ sig { returns(T::Hash[Symbol, T.untyped]) }
233
+ def to_h
234
+ hash = {
235
+ timestamp: timestamp,
236
+ duration_ms: duration_ms,
237
+ cpu_time_ms: cpu_time_ms,
238
+ status: status,
239
+ iteration: iteration,
240
+ max_iterations: max_iterations,
241
+ history_length: history_length,
242
+ tools_used_so_far: tools_used_so_far
243
+ }
244
+ hash[:error_type] = error_type if error_type
245
+ hash[:error_message] = error_message if error_message
246
+ hash
247
+ end
248
+ end
249
+
250
+ # ReAct tool call event payload
251
+ class ReactToolCallEvent < T::Struct
252
+ extend T::Sig
253
+
254
+ # Common fields
255
+ const :timestamp, String
256
+ const :duration_ms, Float
257
+ const :cpu_time_ms, Float
258
+ const :status, String
259
+
260
+ # Tool call fields
261
+ const :iteration, Integer
262
+ const :tool_name, String
263
+ const :tool_input, T.untyped
264
+
265
+ # Error fields (optional)
266
+ const :error_type, T.nilable(String), default: nil
267
+ const :error_message, T.nilable(String), default: nil
268
+
269
+ sig { returns(T::Hash[Symbol, T.untyped]) }
270
+ def to_h
271
+ hash = {
272
+ timestamp: timestamp,
273
+ duration_ms: duration_ms,
274
+ cpu_time_ms: cpu_time_ms,
275
+ status: status,
276
+ iteration: iteration,
277
+ tool_name: tool_name,
278
+ tool_input: tool_input
279
+ }
280
+ hash[:error_type] = error_type if error_type
281
+ hash[:error_message] = error_message if error_message
282
+ hash
283
+ end
284
+ end
285
+
286
+ # ReAct iteration complete event (emit, not instrument)
287
+ class ReactIterationCompleteEvent < T::Struct
288
+ extend T::Sig
289
+
290
+ # Common fields
291
+ const :timestamp, String
292
+ const :status, String
293
+
294
+ # Iteration complete fields
295
+ const :iteration, Integer
296
+ const :thought, String
297
+ const :action, String
298
+ const :action_input, T.untyped
299
+ const :observation, String
300
+ const :tools_used, T::Array[String]
301
+
302
+ sig { returns(T::Hash[Symbol, T.untyped]) }
303
+ def to_h
304
+ {
305
+ timestamp: timestamp,
306
+ status: status,
307
+ iteration: iteration,
308
+ thought: thought,
309
+ action: action,
310
+ action_input: action_input,
311
+ observation: observation,
312
+ tools_used: tools_used
313
+ }
314
+ end
315
+ end
316
+
317
+ # ReAct max iterations event (emit, not instrument)
318
+ class ReactMaxIterationsEvent < T::Struct
319
+ extend T::Sig
320
+
321
+ # Common fields
322
+ const :timestamp, String
323
+ const :status, String
324
+
325
+ # Max iterations fields
326
+ const :iteration_count, Integer
327
+ const :max_iterations, Integer
328
+ const :tools_used, T::Array[String]
329
+ const :final_history_length, Integer
330
+
331
+ sig { returns(T::Hash[Symbol, T.untyped]) }
332
+ def to_h
333
+ {
334
+ timestamp: timestamp,
335
+ status: status,
336
+ iteration_count: iteration_count,
337
+ max_iterations: max_iterations,
338
+ tools_used: tools_used,
339
+ final_history_length: final_history_length
340
+ }
341
+ end
342
+ end
343
+
344
+ # CodeAct iteration event payload
345
+ class CodeActIterationEvent < T::Struct
346
+ extend T::Sig
347
+
348
+ # Common fields
349
+ const :timestamp, String
350
+ const :duration_ms, Float
351
+ const :cpu_time_ms, Float
352
+ const :status, String
353
+
354
+ # CodeAct-specific fields
355
+ const :iteration, Integer
356
+ const :max_iterations, Integer
357
+ const :history_length, Integer
358
+ const :code_blocks_executed, Integer
359
+
360
+ # Error fields (optional)
361
+ const :error_type, T.nilable(String), default: nil
362
+ const :error_message, T.nilable(String), default: nil
363
+
364
+ sig { returns(T::Hash[Symbol, T.untyped]) }
365
+ def to_h
366
+ hash = {
367
+ timestamp: timestamp,
368
+ duration_ms: duration_ms,
369
+ cpu_time_ms: cpu_time_ms,
370
+ status: status,
371
+ iteration: iteration,
372
+ max_iterations: max_iterations,
373
+ history_length: history_length,
374
+ code_blocks_executed: code_blocks_executed
375
+ }
376
+ hash[:error_type] = error_type if error_type
377
+ hash[:error_message] = error_message if error_message
378
+ hash
379
+ end
380
+ end
381
+
382
+ # CodeAct code execution event payload
383
+ class CodeActCodeExecutionEvent < T::Struct
384
+ extend T::Sig
385
+
386
+ # Common fields
387
+ const :timestamp, String
388
+ const :duration_ms, Float
389
+ const :cpu_time_ms, Float
390
+ const :status, String
391
+
392
+ # Code execution fields
393
+ const :iteration, Integer
394
+ const :code_type, String
395
+ const :code_length, Integer
396
+ const :execution_success, T::Boolean
397
+
398
+ # Error fields (optional)
399
+ const :error_type, T.nilable(String), default: nil
400
+ const :error_message, T.nilable(String), default: nil
401
+
402
+ sig { returns(T::Hash[Symbol, T.untyped]) }
403
+ def to_h
404
+ hash = {
405
+ timestamp: timestamp,
406
+ duration_ms: duration_ms,
407
+ cpu_time_ms: cpu_time_ms,
408
+ status: status,
409
+ iteration: iteration,
410
+ code_type: code_type,
411
+ code_length: code_length,
412
+ execution_success: execution_success
413
+ }
414
+ hash[:error_type] = error_type if error_type
415
+ hash[:error_message] = error_message if error_message
416
+ hash
417
+ end
418
+ end
419
+
420
+ # Chain of thought reasoning complete event (emit, not instrument)
421
+ class ChainOfThoughtReasoningCompleteEvent < T::Struct
422
+ extend T::Sig
423
+
424
+ # Common fields
425
+ const :timestamp, String
426
+ const :status, String
427
+
428
+ # Reasoning complete fields
429
+ const :signature_class, String
430
+ const :module_name, String
431
+ const :reasoning_length, Integer
432
+ const :answer_present, T::Boolean
433
+
434
+ sig { returns(T::Hash[Symbol, T.untyped]) }
435
+ def to_h
436
+ {
437
+ timestamp: timestamp,
438
+ status: status,
439
+ signature_class: signature_class,
440
+ module_name: module_name,
441
+ reasoning_length: reasoning_length,
442
+ answer_present: answer_present
443
+ }
444
+ end
445
+ end
446
+
447
+ # Validation error event (emit, not instrument)
448
+ class PredictValidationErrorEvent < T::Struct
449
+ extend T::Sig
450
+
451
+ # Common fields
452
+ const :timestamp, String
453
+ const :status, String
454
+
455
+ # Validation error fields
456
+ const :signature_class, String
457
+ const :module_name, String
458
+ const :field_name, String
459
+ const :error_message, String
460
+ const :retry_count, Integer
461
+
462
+ sig { returns(T::Hash[Symbol, T.untyped]) }
463
+ def to_h
464
+ {
465
+ timestamp: timestamp,
466
+ status: status,
467
+ signature_class: signature_class,
468
+ module_name: module_name,
469
+ field_name: field_name,
470
+ error_message: error_message,
471
+ retry_count: retry_count
472
+ }
473
+ end
474
+ end
475
+ end
476
+ end
@@ -3,6 +3,7 @@
3
3
  require 'dry-monitor'
4
4
  require 'dry-configurable'
5
5
  require 'time'
6
+ require_relative 'instrumentation/event_payload_factory'
6
7
 
7
8
  module DSPy
8
9
  # Core instrumentation module using dry-monitor for event emission
@@ -133,7 +134,9 @@ module DSPy
133
134
  status: 'success'
134
135
  ).merge(generate_timestamp)
135
136
 
136
- self.emit_event(event_name, enhanced_payload)
137
+ # Create typed event struct
138
+ event_struct = EventPayloadFactory.create_event(event_name, enhanced_payload)
139
+ self.emit_event(event_name, event_struct)
137
140
  result
138
141
  rescue => error
139
142
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -147,7 +150,9 @@ module DSPy
147
150
  error_message: error.message
148
151
  ).merge(generate_timestamp)
149
152
 
150
- self.emit_event(event_name, error_payload)
153
+ # Create typed event struct
154
+ event_struct = EventPayloadFactory.create_event(event_name, error_payload)
155
+ self.emit_event(event_name, event_struct)
151
156
  raise
152
157
  end
153
158
  end
@@ -161,7 +166,9 @@ module DSPy
161
166
  status: payload[:status] || 'success'
162
167
  ).merge(generate_timestamp)
163
168
 
164
- self.emit_event(event_name, enhanced_payload)
169
+ # Create typed event struct
170
+ event_struct = EventPayloadFactory.create_event(event_name, enhanced_payload)
171
+ self.emit_event(event_name, event_struct)
165
172
  end
166
173
 
167
174
  # Register additional events dynamically (useful for testing)
@@ -183,7 +190,32 @@ module DSPy
183
190
 
184
191
  def self.emit_event(event_name, payload)
185
192
  # Only emit events - subscribers self-register when explicitly created
186
- notifications.instrument(event_name, payload)
193
+ # Convert struct to hash if needed (dry-monitor expects hash)
194
+ if payload.respond_to?(:to_h)
195
+ payload_hash = payload.to_h
196
+ # Restore original timestamp format if needed
197
+ restore_timestamp_format(payload_hash)
198
+ else
199
+ payload_hash = payload
200
+ end
201
+ notifications.instrument(event_name, payload_hash)
202
+ end
203
+
204
+ # Restore timestamp to original format based on configuration
205
+ def self.restore_timestamp_format(payload_hash)
206
+ return unless payload_hash[:timestamp]
207
+
208
+ case DSPy.config.instrumentation.timestamp_format
209
+ when DSPy::TimestampFormat::UNIX_NANO
210
+ # Convert ISO8601 back to nanoseconds
211
+ timestamp = Time.parse(payload_hash[:timestamp])
212
+ payload_hash.delete(:timestamp)
213
+ payload_hash[:timestamp_ns] = (timestamp.to_f * 1_000_000_000).to_i
214
+ when DSPy::TimestampFormat::RFC3339_NANO
215
+ # Convert to RFC3339 with nanoseconds
216
+ timestamp = Time.parse(payload_hash[:timestamp])
217
+ payload_hash[:timestamp] = timestamp.strftime('%Y-%m-%dT%H:%M:%S.%9N%z')
218
+ end
187
219
  end
188
220
 
189
221
  def self.setup_subscribers
@@ -49,14 +49,16 @@ module DSPy
49
49
  end
50
50
  end
51
51
 
52
+ # Create typed metadata for streaming response
53
+ metadata = ResponseMetadataFactory.create('anthropic', {
54
+ model: model,
55
+ streaming: true
56
+ })
57
+
52
58
  Response.new(
53
59
  content: content,
54
60
  usage: nil, # Usage not available in streaming
55
- metadata: {
56
- provider: 'anthropic',
57
- model: model,
58
- streaming: true
59
- }
61
+ metadata: metadata
60
62
  )
61
63
  else
62
64
  response = @client.messages.create(**request_params)
@@ -99,10 +101,13 @@ module DSPy
99
101
  # Add tool calls to metadata if present
100
102
  metadata[:tool_calls] = tool_calls unless tool_calls.empty?
101
103
 
104
+ # Create typed metadata
105
+ typed_metadata = ResponseMetadataFactory.create('anthropic', metadata)
106
+
102
107
  Response.new(
103
108
  content: content,
104
109
  usage: usage_struct,
105
- metadata: metadata
110
+ metadata: typed_metadata
106
111
  )
107
112
  end
108
113
  rescue => e
@@ -43,7 +43,8 @@ module DSPy
43
43
  raise AdapterError, "OpenAI API error: #{response.error}"
44
44
  end
45
45
 
46
- message = response.choices.first.message
46
+ choice = response.choices.first
47
+ message = choice.message
47
48
  content = message.content
48
49
  usage = response.usage
49
50
 
@@ -55,16 +56,20 @@ module DSPy
55
56
  # Convert usage data to typed struct
56
57
  usage_struct = UsageFactory.create('openai', usage)
57
58
 
59
+ # Create typed metadata
60
+ metadata = ResponseMetadataFactory.create('openai', {
61
+ model: model,
62
+ response_id: response.id,
63
+ created: response.created,
64
+ structured_output: @structured_outputs_enabled && signature && supports_structured_outputs?,
65
+ system_fingerprint: response.system_fingerprint,
66
+ finish_reason: choice.finish_reason
67
+ })
68
+
58
69
  Response.new(
59
70
  content: content,
60
71
  usage: usage_struct,
61
- metadata: {
62
- provider: 'openai',
63
- model: model,
64
- response_id: response.id,
65
- created: response.created,
66
- structured_output: @structured_outputs_enabled && signature && supports_structured_outputs?
67
- }
72
+ metadata: metadata
68
73
  )
69
74
  rescue => e
70
75
  raise AdapterError, "OpenAI adapter error: #{e.message}"