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 +4 -4
- data/README.md +3 -3
- data/lib/dspy/chain_of_thought.rb +13 -16
- data/lib/dspy/code_act.rb +31 -37
- data/lib/dspy/context.rb +67 -0
- data/lib/dspy/evaluate.rb +20 -17
- data/lib/dspy/lm.rb +72 -37
- data/lib/dspy/memory/memory_compactor.rb +5 -6
- data/lib/dspy/memory/memory_manager.rb +5 -4
- data/lib/dspy/observability.rb +109 -0
- data/lib/dspy/predict.rb +18 -6
- data/lib/dspy/propose/grounded_proposer.rb +13 -12
- data/lib/dspy/re_act.rb +34 -41
- data/lib/dspy/registry/registry_manager.rb +8 -10
- data/lib/dspy/registry/signature_registry.rb +40 -52
- data/lib/dspy/storage/program_storage.rb +28 -37
- data/lib/dspy/storage/storage_manager.rb +3 -4
- data/lib/dspy/teleprompt/teleprompter.rb +11 -12
- data/lib/dspy/teleprompt/utils.rb +24 -22
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +42 -82
- metadata +31 -24
- data/lib/dspy/instrumentation/event_payload_factory.rb +0 -282
- data/lib/dspy/instrumentation/event_payloads.rb +0 -476
- data/lib/dspy/instrumentation/token_tracker.rb +0 -70
- data/lib/dspy/instrumentation.rb +0 -341
- data/lib/dspy/mixins/instrumentation_helpers.rb +0 -120
- data/lib/dspy/subscribers/langfuse_subscriber.rb +0 -669
- data/lib/dspy/subscribers/logger_subscriber.rb +0 -480
- data/lib/dspy/subscribers/newrelic_subscriber.rb +0 -686
- data/lib/dspy/subscribers/otel_subscriber.rb +0 -537
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63f9d02616c10429f0d409e08036fbf25e2557ba02d48785271ffd7b90aec464
|
4
|
+
data.tar.gz: 46eb23dd11cc7e81ec2c58ee13beca5d36cbfc8c221ec1ae1b70b59cf3e56f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- **
|
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)** -
|
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
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
169
|
-
|
170
|
-
reasoning_steps
|
171
|
-
reasoning_length
|
172
|
-
has_reasoning
|
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
|
-
#
|
149
|
-
|
150
|
-
|
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
|
-
|
157
|
-
|
149
|
+
# Execute CodeAct reasoning loop
|
150
|
+
reasoning_result = execute_codeact_reasoning_loop(task)
|
158
151
|
|
159
|
-
|
160
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
359
|
-
iteration
|
360
|
-
thought
|
361
|
-
ruby_code
|
362
|
-
execution_result
|
363
|
-
error_message
|
364
|
-
success
|
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
|
-
|
372
|
-
iteration_count
|
373
|
-
max_iterations
|
374
|
-
final_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
|
data/lib/dspy/context.rb
ADDED
@@ -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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
|
270
|
-
program_class
|
271
|
-
total_examples
|
272
|
-
passed_examples
|
273
|
-
pass_rate
|
274
|
-
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
|
-
#
|
53
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
#
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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 =
|
250
|
+
token_usage = extract_token_usage(response)
|
257
251
|
|
258
252
|
if token_usage.any?
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|