dspy 0.3.0 → 0.4.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.
@@ -7,14 +7,20 @@ module DSPy
7
7
  class LoggerSubscriber
8
8
  extend T::Sig
9
9
 
10
- sig { params(logger: T.nilable(Logger)).void }
10
+ sig { params(logger: T.nilable(T.any(Logger, Dry::Logger::Dispatcher))).void }
11
11
  def initialize(logger: nil)
12
- @logger = T.let(logger || DSPy.config.logger, Logger)
12
+ @explicit_logger = T.let(logger, T.nilable(T.any(Logger, Dry::Logger::Dispatcher)))
13
13
  setup_event_subscriptions
14
14
  end
15
15
 
16
16
  private
17
17
 
18
+ # Always use the current configured logger or the explicit one
19
+ sig { returns(T.any(Logger, Dry::Logger::Dispatcher)) }
20
+ def logger
21
+ @explicit_logger || DSPy.config.logger
22
+ end
23
+
18
24
  sig { void }
19
25
  def setup_event_subscriptions
20
26
  # Subscribe to DSPy instrumentation events
@@ -41,6 +47,27 @@ module DSPy
41
47
  DSPy::Instrumentation.subscribe('dspy.react.tool_call') do |event|
42
48
  log_react_tool_call(event)
43
49
  end
50
+
51
+ # Subscribe to optimization events
52
+ DSPy::Instrumentation.subscribe('dspy.optimization.start') do |event|
53
+ log_optimization_start(event)
54
+ end
55
+
56
+ DSPy::Instrumentation.subscribe('dspy.optimization.complete') do |event|
57
+ log_optimization_complete(event)
58
+ end
59
+
60
+ DSPy::Instrumentation.subscribe('dspy.optimization.trial_start') do |event|
61
+ log_optimization_trial_start(event)
62
+ end
63
+
64
+ DSPy::Instrumentation.subscribe('dspy.optimization.trial_complete') do |event|
65
+ log_optimization_trial_complete(event)
66
+ end
67
+
68
+ DSPy::Instrumentation.subscribe('dspy.optimization.error') do |event|
69
+ log_optimization_error(event)
70
+ end
44
71
  end
45
72
 
46
73
  # Callback methods for different event types
@@ -82,18 +109,19 @@ module DSPy
82
109
  model = payload[:gen_ai_request_model] || payload[:model]
83
110
  duration = payload[:duration_ms]&.round(2)
84
111
  status = payload[:status]
85
- tokens = if payload[:tokens_total]
86
- " (#{payload[:tokens_total]} tokens)"
87
- else
88
- ""
89
- end
90
-
91
- status_emoji = status == 'success' ? '✅' : '❌'
92
- @logger.info("#{status_emoji} LM Request [#{provider}/#{model}] - #{status} (#{duration}ms)#{tokens}")
93
-
94
- if status == 'error' && payload[:error_message]
95
- @logger.error(" Error: #{payload[:error_message]}")
96
- end
112
+ tokens = payload[:tokens_total]
113
+
114
+ log_parts = [
115
+ "event=lm_request",
116
+ "provider=#{provider}",
117
+ "model=#{model}",
118
+ "status=#{status}",
119
+ "duration_ms=#{duration}"
120
+ ]
121
+ log_parts << "tokens=#{tokens}" if tokens
122
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
123
+
124
+ logger.info(log_parts.join(' '))
97
125
  end
98
126
 
99
127
  sig { params(event: T.untyped).void }
@@ -104,13 +132,16 @@ module DSPy
104
132
  status = payload[:status]
105
133
  input_size = payload[:input_size]
106
134
 
107
- status_emoji = status == 'success' ? '🔮' : '❌'
108
- @logger.info("#{status_emoji} Prediction [#{signature}] - #{status} (#{duration}ms)")
109
- @logger.info(" Input size: #{input_size} chars") if input_size
110
-
111
- if status == 'error' && payload[:error_message]
112
- @logger.error(" Error: #{payload[:error_message]}")
113
- end
135
+ log_parts = [
136
+ "event=prediction",
137
+ "signature=#{signature}",
138
+ "status=#{status}",
139
+ "duration_ms=#{duration}"
140
+ ]
141
+ log_parts << "input_size=#{input_size}" if input_size
142
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
143
+
144
+ logger.info(log_parts.join(' '))
114
145
  end
115
146
 
116
147
  sig { params(event: T.untyped).void }
@@ -122,14 +153,17 @@ module DSPy
122
153
  reasoning_steps = payload[:reasoning_steps]
123
154
  reasoning_length = payload[:reasoning_length]
124
155
 
125
- status_emoji = status == 'success' ? '🧠' : '❌'
126
- @logger.info("#{status_emoji} Chain of Thought [#{signature}] - #{status} (#{duration}ms)")
127
- @logger.info(" Reasoning steps: #{reasoning_steps}") if reasoning_steps
128
- @logger.info(" Reasoning length: #{reasoning_length} chars") if reasoning_length
129
-
130
- if status == 'error' && payload[:error_message]
131
- @logger.error(" Error: #{payload[:error_message]}")
132
- end
156
+ log_parts = [
157
+ "event=chain_of_thought",
158
+ "signature=#{signature}",
159
+ "status=#{status}",
160
+ "duration_ms=#{duration}"
161
+ ]
162
+ log_parts << "reasoning_steps=#{reasoning_steps}" if reasoning_steps
163
+ log_parts << "reasoning_length=#{reasoning_length}" if reasoning_length
164
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
165
+
166
+ logger.info(log_parts.join(' '))
133
167
  end
134
168
 
135
169
  sig { params(event: T.untyped).void }
@@ -142,20 +176,18 @@ module DSPy
142
176
  tools_used = payload[:tools_used]
143
177
  final_answer = payload[:final_answer]
144
178
 
145
- status_emoji = case status
146
- when 'success' then '🤖'
147
- when 'max_iterations' then '⏰'
148
- else '❌'
149
- end
150
-
151
- @logger.info("#{status_emoji} ReAct Agent [#{signature}] - #{status} (#{duration}ms)")
152
- @logger.info(" Iterations: #{iteration_count}") if iteration_count
153
- @logger.info(" Tools used: #{tools_used.join(', ')}") if tools_used&.any?
154
- @logger.info(" Final answer: #{final_answer}") if final_answer
155
-
156
- if status == 'error' && payload[:error_message]
157
- @logger.error(" Error: #{payload[:error_message]}")
158
- end
179
+ log_parts = [
180
+ "event=react",
181
+ "signature=#{signature}",
182
+ "status=#{status}",
183
+ "duration_ms=#{duration}"
184
+ ]
185
+ log_parts << "iterations=#{iteration_count}" if iteration_count
186
+ log_parts << "tools_used=\"#{tools_used.join(',')}\"" if tools_used&.any?
187
+ log_parts << "final_answer=\"#{final_answer&.truncate(100)}\"" if final_answer
188
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
189
+
190
+ logger.info(log_parts.join(' '))
159
191
  end
160
192
 
161
193
  sig { params(event: T.untyped).void }
@@ -167,14 +199,17 @@ module DSPy
167
199
  duration = payload[:duration_ms]&.round(2)
168
200
  status = payload[:status]
169
201
 
170
- status_emoji = status == 'success' ? '🔄' : '❌'
171
- @logger.info("#{status_emoji} ReAct Iteration #{iteration} - #{status} (#{duration}ms)")
172
- @logger.info(" Thought: #{thought.truncate(100)}") if thought
173
- @logger.info(" Action: #{action}") if action
174
-
175
- if status == 'error' && payload[:error_message]
176
- @logger.error(" Error: #{payload[:error_message]}")
177
- end
202
+ log_parts = [
203
+ "event=react_iteration",
204
+ "iteration=#{iteration}",
205
+ "status=#{status}",
206
+ "duration_ms=#{duration}"
207
+ ]
208
+ log_parts << "thought=\"#{thought&.truncate(100)}\"" if thought
209
+ log_parts << "action=\"#{action}\"" if action
210
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
211
+
212
+ logger.info(log_parts.join(' '))
178
213
  end
179
214
 
180
215
  sig { params(event: T.untyped).void }
@@ -185,12 +220,115 @@ module DSPy
185
220
  duration = payload[:duration_ms]&.round(2)
186
221
  status = payload[:status]
187
222
 
188
- status_emoji = status == 'success' ? '🔧' : '❌'
189
- @logger.info("#{status_emoji} Tool Call [#{tool_name}] (Iteration #{iteration}) - #{status} (#{duration}ms)")
190
-
191
- if status == 'error' && payload[:error_message]
192
- @logger.error(" Error: #{payload[:error_message]}")
193
- end
223
+ log_parts = [
224
+ "event=tool_call",
225
+ "tool=#{tool_name}",
226
+ "iteration=#{iteration}",
227
+ "status=#{status}",
228
+ "duration_ms=#{duration}"
229
+ ]
230
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
231
+
232
+ logger.info(log_parts.join(' '))
233
+ end
234
+
235
+ # Optimization event logging methods
236
+ sig { params(event: T.untyped).void }
237
+ def log_optimization_start(event)
238
+ payload = event.payload
239
+ optimization_id = payload[:optimization_id]
240
+ optimizer = payload[:optimizer]
241
+ trainset_size = payload[:trainset_size]
242
+ valset_size = payload[:valset_size]
243
+
244
+ log_parts = [
245
+ "event=optimization_start",
246
+ "optimization_id=#{optimization_id}",
247
+ "optimizer=#{optimizer}",
248
+ "trainset_size=#{trainset_size}"
249
+ ]
250
+ log_parts << "valset_size=#{valset_size}" if valset_size
251
+
252
+ logger.info(log_parts.join(' '))
253
+ end
254
+
255
+ sig { params(event: T.untyped).void }
256
+ def log_optimization_complete(event)
257
+ payload = event.payload
258
+ optimization_id = payload[:optimization_id]
259
+ optimizer = payload[:optimizer]
260
+ duration = payload[:duration_ms]&.round(2)
261
+ best_score = payload[:best_score]
262
+ trials_count = payload[:trials_count]
263
+
264
+ log_parts = [
265
+ "event=optimization_complete",
266
+ "optimization_id=#{optimization_id}",
267
+ "optimizer=#{optimizer}",
268
+ "duration_ms=#{duration}"
269
+ ]
270
+ log_parts << "best_score=#{best_score}" if best_score
271
+ log_parts << "trials_count=#{trials_count}" if trials_count
272
+
273
+ logger.info(log_parts.join(' '))
274
+ end
275
+
276
+ sig { params(event: T.untyped).void }
277
+ def log_optimization_trial_start(event)
278
+ payload = event.payload
279
+ optimization_id = payload[:optimization_id]
280
+ trial_number = payload[:trial_number]
281
+ instruction = payload[:instruction]
282
+
283
+ log_parts = [
284
+ "event=optimization_trial_start",
285
+ "optimization_id=#{optimization_id}",
286
+ "trial_number=#{trial_number}"
287
+ ]
288
+ log_parts << "instruction=\"#{instruction&.slice(0, 100)}\"" if instruction
289
+
290
+ logger.info(log_parts.join(' '))
291
+ end
292
+
293
+ sig { params(event: T.untyped).void }
294
+ def log_optimization_trial_complete(event)
295
+ payload = event.payload
296
+ optimization_id = payload[:optimization_id]
297
+ trial_number = payload[:trial_number]
298
+ duration = payload[:duration_ms]&.round(2)
299
+ score = payload[:score]
300
+ status = payload[:status]
301
+
302
+ log_parts = [
303
+ "event=optimization_trial_complete",
304
+ "optimization_id=#{optimization_id}",
305
+ "trial_number=#{trial_number}",
306
+ "status=#{status}",
307
+ "duration_ms=#{duration}"
308
+ ]
309
+ log_parts << "score=#{score}" if score
310
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
311
+
312
+ logger.info(log_parts.join(' '))
313
+ end
314
+
315
+ sig { params(event: T.untyped).void }
316
+ def log_optimization_error(event)
317
+ payload = event.payload
318
+ optimization_id = payload[:optimization_id]
319
+ optimizer = payload[:optimizer]
320
+ error_message = payload[:error_message]
321
+ error_type = payload[:error_type]
322
+
323
+ log_parts = [
324
+ "event=optimization_error",
325
+ "optimization_id=#{optimization_id}",
326
+ "optimizer=#{optimizer}",
327
+ "error_type=#{error_type}"
328
+ ]
329
+ log_parts << "error=\"#{error_message}\"" if error_message
330
+
331
+ logger.info(log_parts.join(' '))
194
332
  end
195
333
  end
196
334
  end