dspy 0.17.0 → 0.18.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7d6061a3dd63f61bd9b5078694218b2a26b56da974da4c42b29f654efb3c08c
4
- data.tar.gz: 3d8277e907c953c3577d3da3de5182c47665a1d105a131753aa6d0f25481d0ff
3
+ metadata.gz: 63f9d02616c10429f0d409e08036fbf25e2557ba02d48785271ffd7b90aec464
4
+ data.tar.gz: 46eb23dd11cc7e81ec2c58ee13beca5d36cbfc8c221ec1ae1b70b59cf3e56f36
5
5
  SHA512:
6
- metadata.gz: ea762ce55ffcd24e3462c98ff14e5e29169889e73befd45abbb7bd84cce55b2ae0adf000e3af945a462538ef16fd78bd8a98bf941f3dc550c1188fda0eb266b4
7
- data.tar.gz: 4f4e0be0529e942fadf1edd7fc4b0132732384715dd7e7f68889139b6bd39fc9c5b1701bb2b1fd27b166314d9a164a07b43a78d899efadc0a518dcf2bc24da7d
6
+ metadata.gz: d53d4a72482146cf692304f82a0c273ebc06925dc66042d461e600e77fa99f79fda88f67282b5497a4625275d7311b1d10ea255814085883e15a323a7c4d56e9
7
+ data.tar.gz: b2f63c03a162101345589732a192267a55464c7ebed524274a175b66ee872501ec36386d4fd7995005d4726fa0181bb82a9d48b32b09ed722e0662e39fd6958b
data/README.md CHANGED
@@ -71,10 +71,10 @@ puts result.confidence # => 0.85
71
71
  - **Reliable JSON Extraction** - Native OpenAI structured outputs, Anthropic extraction patterns, and automatic strategy selection with fallback
72
72
  - **Type-Safe Configuration** - Strategy enums with automatic provider optimization (Strict/Compatible modes)
73
73
  - **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
74
+ - **Zero-Config Langfuse Integration** - Set env vars and get automatic OpenTelemetry traces in Langfuse
74
75
  - **Performance Caching** - Schema and capability caching for faster repeated operations
75
76
  - **File-based Storage** - Optimization result persistence with versioning
76
- - **Multi-Platform Observability** - OpenTelemetry, New Relic, and Langfuse integration
77
- - **Comprehensive Instrumentation** - Event tracking, performance monitoring, and detailed logging
77
+ - **Structured Logging** - JSON and key=value formats with span tracking
78
78
 
79
79
  **Developer Experience:**
80
80
  - LLM provider support using official Ruby clients:
@@ -163,7 +163,7 @@ For LLMs and AI assistants working with DSPy.rb:
163
163
 
164
164
  ### Production Features
165
165
  - **[Storage System](docs/src/production/storage.md)** - Basic file-based persistence
166
- - **[Observability](docs/src/production/observability.md)** - Multi-platform monitoring and metrics
166
+ - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration and structured logging
167
167
 
168
168
  ### Advanced Usage
169
169
  - **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
@@ -4,7 +4,6 @@
4
4
  require 'sorbet-runtime'
5
5
  require_relative 'predict'
6
6
  require_relative 'signature'
7
- require_relative 'instrumentation'
8
7
  require_relative 'mixins/struct_builder'
9
8
 
10
9
  module DSPy
@@ -82,18 +81,16 @@ module DSPy
82
81
  sig { returns(T.class_of(DSPy::Signature)) }
83
82
  attr_reader :original_signature
84
83
 
85
- # Override forward_untyped to add ChainOfThought-specific instrumentation
84
+ # Override forward_untyped to add ChainOfThought-specific analysis
86
85
  sig { override.params(input_values: T.untyped).returns(T.untyped) }
87
86
  def forward_untyped(**input_values)
88
- instrument_prediction('dspy.chain_of_thought', @original_signature, input_values) do
89
- # Call parent prediction logic
90
- prediction_result = super(**input_values)
91
-
92
- # Analyze reasoning if present
93
- analyze_reasoning(prediction_result)
94
-
95
- prediction_result
96
- end
87
+ # Call parent prediction logic
88
+ prediction_result = super(**input_values)
89
+
90
+ # Analyze reasoning if present
91
+ analyze_reasoning(prediction_result)
92
+
93
+ prediction_result
97
94
  end
98
95
 
99
96
  private
@@ -165,11 +162,11 @@ module DSPy
165
162
  # Emits reasoning analysis instrumentation event
166
163
  sig { params(reasoning_content: String).void }
167
164
  def emit_reasoning_analysis(reasoning_content)
168
- Instrumentation.emit('dspy.chain_of_thought.reasoning_complete', {
169
- signature_class: @original_signature.name,
170
- reasoning_steps: count_reasoning_steps(reasoning_content),
171
- reasoning_length: reasoning_content.length,
172
- has_reasoning: true
165
+ DSPy.log('chain_of_thought.reasoning_complete', **{
166
+ 'dspy.signature' => @original_signature.name,
167
+ 'cot.reasoning_steps' => count_reasoning_steps(reasoning_content),
168
+ 'cot.reasoning_length' => reasoning_content.length,
169
+ 'cot.has_reasoning' => true
173
170
  })
174
171
  end
175
172
 
data/lib/dspy/code_act.rb CHANGED
@@ -6,9 +6,7 @@ require_relative 'predict'
6
6
  require_relative 'signature'
7
7
  require 'json'
8
8
  require 'stringio'
9
- require_relative 'instrumentation'
10
9
  require_relative 'mixins/struct_builder'
11
- require_relative 'mixins/instrumentation_helpers'
12
10
  require_relative 'type_serializer'
13
11
 
14
12
  module DSPy
@@ -91,7 +89,6 @@ module DSPy
91
89
  class CodeAct < Predict
92
90
  extend T::Sig
93
91
  include Mixins::StructBuilder
94
- include Mixins::InstrumentationHelpers
95
92
 
96
93
  sig { returns(T.class_of(DSPy::Signature)) }
97
94
  attr_reader :original_signature_class
@@ -145,22 +142,15 @@ module DSPy
145
142
  def forward(**kwargs)
146
143
  lm = config.lm || DSPy.config.lm
147
144
 
148
- # Instrument the entire CodeAct agent lifecycle
149
- result = instrument_prediction('dspy.codeact', @original_signature_class, kwargs, {
150
- max_iterations: @max_iterations
151
- }) do
152
- # Validate input and serialize all fields as task context
153
- input_struct = @original_signature_class.input_struct_class.new(**kwargs)
154
- task = DSPy::TypeSerializer.serialize(input_struct).to_json
145
+ # Validate input and serialize all fields as task context
146
+ input_struct = @original_signature_class.input_struct_class.new(**kwargs)
147
+ task = DSPy::TypeSerializer.serialize(input_struct).to_json
155
148
 
156
- # Execute CodeAct reasoning loop
157
- reasoning_result = execute_codeact_reasoning_loop(task)
149
+ # Execute CodeAct reasoning loop
150
+ reasoning_result = execute_codeact_reasoning_loop(task)
158
151
 
159
- # Create enhanced output with all CodeAct data
160
- create_enhanced_result(kwargs, reasoning_result)
161
- end
162
-
163
- result
152
+ # Create enhanced output with all CodeAct data
153
+ create_enhanced_result(kwargs, reasoning_result)
164
154
  end
165
155
 
166
156
  private
@@ -202,11 +192,13 @@ module DSPy
202
192
  # Executes a single iteration of the Think-Code-Observe loop
203
193
  sig { params(task: String, history: T::Array[CodeActHistoryEntry], context: String, iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
204
194
  def execute_single_iteration(task, history, context, iteration)
205
- Instrumentation.instrument('dspy.codeact.iteration', {
206
- iteration: iteration,
207
- max_iterations: @max_iterations,
208
- history_length: history.length
209
- }) do
195
+ DSPy::Context.with_span(
196
+ operation: 'codeact.iteration',
197
+ 'dspy.module' => 'CodeAct',
198
+ 'codeact.iteration' => iteration,
199
+ 'codeact.max_iterations' => @max_iterations,
200
+ 'codeact.history_length' => history.length
201
+ ) do
210
202
  execution_state = execute_think_code_step(task, context, history, iteration)
211
203
 
212
204
  observation_decision = process_observation_and_decide_next_step(
@@ -305,10 +297,12 @@ module DSPy
305
297
 
306
298
  sig { params(ruby_code: String, iteration: Integer).returns([T.nilable(String), String]) }
307
299
  def execute_ruby_code_with_instrumentation(ruby_code, iteration)
308
- Instrumentation.instrument('dspy.codeact.code_execution', {
309
- iteration: iteration,
310
- code_length: ruby_code.length
311
- }) do
300
+ DSPy::Context.with_span(
301
+ operation: 'codeact.code_execution',
302
+ 'dspy.module' => 'CodeAct',
303
+ 'codeact.iteration' => iteration,
304
+ 'code.length' => ruby_code.length
305
+ ) do
312
306
  execute_ruby_code_safely(ruby_code)
313
307
  end
314
308
  end
@@ -355,23 +349,23 @@ module DSPy
355
349
 
356
350
  sig { params(iteration: Integer, thought: String, ruby_code: String, execution_result: T.nilable(String), error_message: T.nilable(String)).void }
357
351
  def emit_iteration_complete_event(iteration, thought, ruby_code, execution_result, error_message)
358
- Instrumentation.emit('dspy.codeact.iteration_complete', {
359
- iteration: iteration,
360
- thought: thought,
361
- ruby_code: ruby_code,
362
- execution_result: execution_result,
363
- error_message: error_message,
364
- success: error_message.nil?
352
+ DSPy.log('codeact.iteration_complete', **{
353
+ 'codeact.iteration' => iteration,
354
+ 'codeact.thought' => thought,
355
+ 'codeact.ruby_code' => ruby_code,
356
+ 'codeact.execution_result' => execution_result,
357
+ 'codeact.error_message' => error_message,
358
+ 'codeact.success' => error_message.nil?
365
359
  })
366
360
  end
367
361
 
368
362
  sig { params(iterations_count: Integer, final_answer: T.nilable(String), history: T::Array[CodeActHistoryEntry]).void }
369
363
  def handle_max_iterations_if_needed(iterations_count, final_answer, history)
370
364
  if iterations_count >= @max_iterations && final_answer.nil?
371
- Instrumentation.emit('dspy.codeact.max_iterations', {
372
- iteration_count: iterations_count,
373
- max_iterations: @max_iterations,
374
- final_history_length: history.length
365
+ DSPy.log('codeact.max_iterations', **{
366
+ 'codeact.iteration_count' => iterations_count,
367
+ 'codeact.max_iterations' => @max_iterations,
368
+ 'codeact.final_history_length' => history.length
375
369
  })
376
370
  end
377
371
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module DSPy
6
+ class Context
7
+ class << self
8
+ def current
9
+ Thread.current[:dspy_context] ||= {
10
+ trace_id: SecureRandom.uuid,
11
+ span_stack: []
12
+ }
13
+ end
14
+
15
+ def with_span(operation:, **attributes)
16
+ span_id = SecureRandom.uuid
17
+ parent_span_id = current[:span_stack].last
18
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+
20
+ # Prepare attributes with context information
21
+ span_attributes = {
22
+ trace_id: current[:trace_id],
23
+ span_id: span_id,
24
+ parent_span_id: parent_span_id,
25
+ operation: operation,
26
+ **attributes
27
+ }
28
+
29
+ # Log span start with proper hierarchy
30
+ DSPy.log('span.start', **span_attributes)
31
+
32
+ # Create OpenTelemetry span if observability is enabled
33
+ otel_span = nil
34
+ if DSPy::Observability.enabled?
35
+ otel_span = DSPy::Observability.start_span(operation, span_attributes)
36
+ end
37
+
38
+ # Push to stack for child spans
39
+ current[:span_stack].push(span_id)
40
+
41
+ begin
42
+ result = yield
43
+ ensure
44
+ # Pop from stack
45
+ current[:span_stack].pop
46
+
47
+ # Log span end with duration
48
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
49
+ DSPy.log('span.end',
50
+ trace_id: current[:trace_id],
51
+ span_id: span_id,
52
+ duration_ms: duration_ms
53
+ )
54
+
55
+ # Finish OpenTelemetry span
56
+ DSPy::Observability.finish_span(otel_span) if otel_span
57
+ end
58
+
59
+ result
60
+ end
61
+
62
+ def clear!
63
+ Thread.current[:dspy_context] = nil
64
+ end
65
+ end
66
+ end
67
+ end
data/lib/dspy/evaluate.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
- require_relative 'instrumentation'
5
4
  require_relative 'example'
6
5
 
7
6
  module DSPy
@@ -138,10 +137,12 @@ module DSPy
138
137
  # Evaluate program on a single example
139
138
  sig { params(example: T.untyped, trace: T.nilable(T.untyped)).returns(EvaluationResult) }
140
139
  def call(example, trace: nil)
141
- Instrumentation.instrument('dspy.evaluation.example', {
142
- program_class: @program.class.name,
143
- has_metric: !@metric.nil?
144
- }) do
140
+ DSPy::Context.with_span(
141
+ operation: 'evaluation.example',
142
+ 'dspy.module' => 'Evaluator',
143
+ 'evaluation.program' => @program.class.name,
144
+ 'evaluation.has_metric' => !@metric.nil?
145
+ ) do
145
146
  begin
146
147
  # Extract input from example - support both hash and object formats
147
148
  input_values = extract_input_values(example)
@@ -209,12 +210,14 @@ module DSPy
209
210
  ).returns(BatchEvaluationResult)
210
211
  end
211
212
  def evaluate(devset, display_progress: true, display_table: false, return_outputs: true)
212
- Instrumentation.instrument('dspy.evaluation.batch', {
213
- program_class: @program.class.name,
214
- num_examples: devset.length,
215
- has_metric: !@metric.nil?,
216
- num_threads: @num_threads
217
- }) do
213
+ DSPy::Context.with_span(
214
+ operation: 'evaluation.batch',
215
+ 'dspy.module' => 'Evaluator',
216
+ 'evaluation.program' => @program.class.name,
217
+ 'evaluation.num_examples' => devset.length,
218
+ 'evaluation.has_metric' => !@metric.nil?,
219
+ 'evaluation.num_threads' => @num_threads
220
+ ) do
218
221
  results = []
219
222
  errors = 0
220
223
 
@@ -266,12 +269,12 @@ module DSPy
266
269
  end
267
270
 
268
271
  # Emit batch completion event
269
- Instrumentation.emit('dspy.evaluation.batch_complete', {
270
- program_class: @program.class.name,
271
- total_examples: batch_result.total_examples,
272
- passed_examples: batch_result.passed_examples,
273
- pass_rate: batch_result.pass_rate,
274
- aggregated_metrics: aggregated_metrics
272
+ DSPy.log('evaluation.batch_complete', **{
273
+ 'evaluation.program_class' => @program.class.name,
274
+ 'evaluation.total_examples' => batch_result.total_examples,
275
+ 'evaluation.passed_examples' => batch_result.passed_examples,
276
+ 'evaluation.pass_rate' => batch_result.pass_rate,
277
+ 'evaluation.aggregated_metrics' => aggregated_metrics
275
278
  })
276
279
 
277
280
  if display_progress
data/lib/dspy/lm.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sorbet-runtime'
4
+
3
5
  # Load adapter infrastructure
4
6
  require_relative 'lm/errors'
5
7
  require_relative 'lm/response'
@@ -7,8 +9,6 @@ require_relative 'lm/adapter'
7
9
  require_relative 'lm/adapter_factory'
8
10
 
9
11
  # Load instrumentation
10
- require_relative 'instrumentation'
11
- require_relative 'instrumentation/token_tracker'
12
12
 
13
13
  # Load adapters
14
14
  require_relative 'lm/adapters/openai_adapter'
@@ -25,6 +25,7 @@ require_relative 'lm/message_builder'
25
25
 
26
26
  module DSPy
27
27
  class LM
28
+ extend T::Sig
28
29
  attr_reader :model_id, :api_key, :model, :provider, :adapter
29
30
 
30
31
  def initialize(model_id, api_key: nil, **options)
@@ -49,18 +50,8 @@ module DSPy
49
50
  chat_with_strategy(messages, signature_class, &block)
50
51
  end
51
52
 
52
- # Instrument response parsing
53
- if should_emit_lm_events?
54
- parsed_result = Instrumentation.instrument('dspy.lm.response.parsed', {
55
- signature_class: signature_class.name,
56
- provider: provider,
57
- response_length: response.content&.length || 0
58
- }) do
59
- parse_response(response, input_values, signature_class)
60
- end
61
- else
62
- parsed_result = parse_response(response, input_values, signature_class)
63
- end
53
+ # Parse response (no longer needs separate instrumentation)
54
+ parsed_result = parse_response(response, input_values, signature_class)
64
55
 
65
56
  parsed_result
66
57
  end
@@ -227,25 +218,28 @@ module DSPy
227
218
  end.join(' ')
228
219
  input_size = input_text.length
229
220
 
230
- response = nil
231
-
232
- if should_emit_lm_events?
233
- # Emit dspy.lm.request event
234
- response = Instrumentation.instrument('dspy.lm.request', {
235
- gen_ai_operation_name: 'chat',
236
- gen_ai_system: provider,
237
- gen_ai_request_model: model,
238
- signature_class: signature_class_name,
239
- provider: provider,
240
- adapter_class: adapter.class.name,
241
- input_size: input_size
242
- }, &execution_block)
221
+ # Wrap LLM call in span tracking
222
+ response = DSPy::Context.with_span(
223
+ operation: 'llm.generate',
224
+ 'gen_ai.system' => provider,
225
+ 'gen_ai.request.model' => model,
226
+ 'dspy.signature' => signature_class_name
227
+ ) do
228
+ result = execution_block.call
243
229
 
244
- # Extract and emit token usage
245
- emit_token_usage(response, signature_class_name)
246
- else
247
- # Consolidated mode: execute without instrumentation
248
- response = execution_block.call
230
+ # Add usage data if available
231
+ if result.respond_to?(:usage) && result.usage
232
+ usage = result.usage
233
+ DSPy.log('span.attributes',
234
+ span_id: DSPy::Context.current[:span_stack].last,
235
+ 'gen_ai.response.model' => result.respond_to?(:model) ? result.model : nil,
236
+ 'gen_ai.usage.prompt_tokens' => usage.respond_to?(:input_tokens) ? usage.input_tokens : nil,
237
+ 'gen_ai.usage.completion_tokens' => usage.respond_to?(:output_tokens) ? usage.output_tokens : nil,
238
+ 'gen_ai.usage.total_tokens' => usage.respond_to?(:total_tokens) ? usage.total_tokens : nil
239
+ )
240
+ end
241
+
242
+ result
249
243
  end
250
244
 
251
245
  response
@@ -253,19 +247,60 @@ module DSPy
253
247
 
254
248
  # Common method to emit token usage events
255
249
  def emit_token_usage(response, signature_class_name)
256
- token_usage = Instrumentation::TokenTracker.extract_token_usage(response, provider)
250
+ token_usage = extract_token_usage(response)
257
251
 
258
252
  if token_usage.any?
259
- Instrumentation.emit('dspy.lm.tokens', token_usage.merge({
260
- gen_ai_system: provider,
261
- gen_ai_request_model: model,
262
- signature_class: signature_class_name
253
+ DSPy.log('lm.tokens', **token_usage.merge({
254
+ 'gen_ai.system' => provider,
255
+ 'gen_ai.request.model' => model,
256
+ 'dspy.signature' => signature_class_name
263
257
  }))
264
258
  end
265
259
 
266
260
  token_usage
267
261
  end
268
262
 
263
+ private
264
+
265
+ # Extract token usage from API responses
266
+ sig { params(response: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
267
+ def extract_token_usage(response)
268
+ return {} unless response&.usage
269
+
270
+ # Handle Usage struct objects
271
+ if response.usage.respond_to?(:input_tokens)
272
+ return {
273
+ input_tokens: response.usage.input_tokens,
274
+ output_tokens: response.usage.output_tokens,
275
+ total_tokens: response.usage.total_tokens
276
+ }.compact
277
+ end
278
+
279
+ # Handle hash-based usage (for VCR compatibility)
280
+ usage = response.usage
281
+ return {} unless usage.is_a?(Hash)
282
+
283
+ case provider.to_s.downcase
284
+ when 'openai'
285
+ {
286
+ input_tokens: usage[:prompt_tokens] || usage['prompt_tokens'],
287
+ output_tokens: usage[:completion_tokens] || usage['completion_tokens'],
288
+ total_tokens: usage[:total_tokens] || usage['total_tokens']
289
+ }.compact
290
+ when 'anthropic'
291
+ {
292
+ input_tokens: usage[:input_tokens] || usage['input_tokens'],
293
+ output_tokens: usage[:output_tokens] || usage['output_tokens'],
294
+ total_tokens: (usage[:input_tokens] || usage['input_tokens'] || 0) +
295
+ (usage[:output_tokens] || usage['output_tokens'] || 0)
296
+ }.compact
297
+ else
298
+ {}
299
+ end
300
+ end
301
+
302
+ public
303
+
269
304
  def validate_messages!(messages)
270
305
  unless messages.is_a?(Array)
271
306
  raise ArgumentError, "messages must be an array"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
- require_relative '../instrumentation'
5
4
 
6
5
  module DSPy
7
6
  module Memory
@@ -51,7 +50,7 @@ module DSPy
51
50
  # Main compaction entry point - checks all triggers and compacts if needed
52
51
  sig { params(store: MemoryStore, embedding_engine: EmbeddingEngine, user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
53
52
  def compact_if_needed!(store, embedding_engine, user_id: nil)
54
- DSPy::Instrumentation.instrument('dspy.memory.compaction_check', { user_id: user_id }) do
53
+ DSPy::Context.with_span(operation: 'memory.compaction_check', 'memory.user_id' => user_id) do
55
54
  results = {}
56
55
 
57
56
  # Check triggers in order of impact
@@ -143,7 +142,7 @@ module DSPy
143
142
  # Remove oldest memories when over size limit
144
143
  sig { params(store: MemoryStore, user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
145
144
  def perform_size_compaction!(store, user_id)
146
- DSPy::Instrumentation.instrument('dspy.memory.size_compaction', { user_id: user_id }) do
145
+ DSPy::Context.with_span(operation: 'memory.size_compaction', 'memory.user_id' => user_id) do
147
146
  current_count = store.count(user_id: user_id)
148
147
  target_count = (@max_memories * 0.8).to_i # Remove to 80% of limit
149
148
  remove_count = current_count - target_count
@@ -182,7 +181,7 @@ module DSPy
182
181
  # Remove memories older than age limit
183
182
  sig { params(store: MemoryStore, user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
184
183
  def perform_age_compaction!(store, user_id)
185
- DSPy::Instrumentation.instrument('dspy.memory.age_compaction', { user_id: user_id }) do
184
+ DSPy::Context.with_span(operation: 'memory.age_compaction', 'memory.user_id' => user_id) do
186
185
  cutoff_time = Time.now - (@max_age_days * 24 * 60 * 60)
187
186
  all_memories = store.list(user_id: user_id)
188
187
  old_memories = all_memories.select { |m| m.created_at < cutoff_time }
@@ -206,7 +205,7 @@ module DSPy
206
205
  # Remove near-duplicate memories using embedding similarity
207
206
  sig { params(store: MemoryStore, embedding_engine: EmbeddingEngine, user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
208
207
  def perform_deduplication!(store, embedding_engine, user_id)
209
- DSPy::Instrumentation.instrument('dspy.memory.deduplication', { user_id: user_id }) do
208
+ DSPy::Context.with_span(operation: 'memory.deduplication', 'memory.user_id' => user_id) do
210
209
  memories = store.list(user_id: user_id)
211
210
  memories_with_embeddings = memories.select(&:embedding)
212
211
 
@@ -259,7 +258,7 @@ module DSPy
259
258
  # Remove memories with low relevance (low access patterns)
260
259
  sig { params(store: MemoryStore, user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
261
260
  def perform_relevance_pruning!(store, user_id)
262
- DSPy::Instrumentation.instrument('dspy.memory.relevance_pruning', { user_id: user_id }) do
261
+ DSPy::Context.with_span(operation: 'memory.relevance_pruning', 'memory.user_id' => user_id) do
263
262
  memories = store.list(user_id: user_id)
264
263
  total_access = memories.sum(&:access_count)
265
264
  return { removed_count: 0, trigger: 'no_access_data' } if total_access == 0
@@ -216,10 +216,11 @@ module DSPy
216
216
  # Force memory compaction (useful for testing or manual cleanup)
217
217
  sig { params(user_id: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
218
218
  def force_compact!(user_id = nil)
219
- DSPy::Instrumentation.instrument('dspy.memory.compaction_complete', {
220
- user_id: user_id,
221
- forced: true
222
- }) do
219
+ DSPy::Context.with_span(
220
+ operation: 'memory.compaction_complete',
221
+ 'memory.user_id' => user_id,
222
+ 'memory.forced' => true
223
+ ) do
223
224
  results = {}
224
225
 
225
226
  # Run all compaction strategies regardless of thresholds