dspy 0.3.1 → 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.
@@ -0,0 +1,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require_relative 'signature_registry'
5
+
6
+ module DSPy
7
+ module Registry
8
+ # High-level registry manager that integrates with the DSPy ecosystem
9
+ # Provides automatic version management and integration with optimization results
10
+ class RegistryManager
11
+ extend T::Sig
12
+
13
+ # Configuration for automatic registry integration
14
+ class RegistryIntegrationConfig
15
+ extend T::Sig
16
+
17
+ sig { returns(T::Boolean) }
18
+ attr_accessor :auto_register_optimizations
19
+
20
+ sig { returns(T::Boolean) }
21
+ attr_accessor :auto_deploy_best_versions
22
+
23
+ sig { returns(Float) }
24
+ attr_accessor :auto_deploy_threshold
25
+
26
+ sig { returns(T::Boolean) }
27
+ attr_accessor :rollback_on_performance_drop
28
+
29
+ sig { returns(Float) }
30
+ attr_accessor :rollback_threshold
31
+
32
+ sig { returns(String) }
33
+ attr_accessor :deployment_strategy
34
+
35
+ sig { void }
36
+ def initialize
37
+ @auto_register_optimizations = true
38
+ @auto_deploy_best_versions = false
39
+ @auto_deploy_threshold = 0.1 # 10% improvement
40
+ @rollback_on_performance_drop = true
41
+ @rollback_threshold = 0.05 # 5% drop
42
+ @deployment_strategy = "conservative" # conservative, aggressive, manual
43
+ end
44
+
45
+ sig { returns(T::Hash[Symbol, T.untyped]) }
46
+ def to_h
47
+ {
48
+ auto_register_optimizations: @auto_register_optimizations,
49
+ auto_deploy_best_versions: @auto_deploy_best_versions,
50
+ auto_deploy_threshold: @auto_deploy_threshold,
51
+ rollback_on_performance_drop: @rollback_on_performance_drop,
52
+ rollback_threshold: @rollback_threshold,
53
+ deployment_strategy: @deployment_strategy
54
+ }
55
+ end
56
+ end
57
+
58
+ sig { returns(SignatureRegistry) }
59
+ attr_reader :registry
60
+
61
+ sig { returns(RegistryIntegrationConfig) }
62
+ attr_reader :integration_config
63
+
64
+ sig do
65
+ params(
66
+ registry_config: T.nilable(SignatureRegistry::RegistryConfig),
67
+ integration_config: T.nilable(RegistryIntegrationConfig)
68
+ ).void
69
+ end
70
+ def initialize(registry_config: nil, integration_config: nil)
71
+ @registry = SignatureRegistry.new(config: registry_config)
72
+ @integration_config = integration_config || RegistryIntegrationConfig.new
73
+ end
74
+
75
+ # Register an optimization result automatically
76
+ sig do
77
+ params(
78
+ optimization_result: T.untyped,
79
+ signature_name: T.nilable(String),
80
+ metadata: T::Hash[Symbol, T.untyped]
81
+ ).returns(T.nilable(SignatureRegistry::SignatureVersion))
82
+ end
83
+ def register_optimization_result(optimization_result, signature_name: nil, metadata: {})
84
+ return nil unless @integration_config.auto_register_optimizations
85
+
86
+ # Extract signature name if not provided
87
+ signature_name ||= extract_signature_name(optimization_result)
88
+ return nil unless signature_name
89
+
90
+ # Extract configuration from optimization result
91
+ configuration = extract_configuration(optimization_result)
92
+
93
+ # Get performance score
94
+ performance_score = extract_performance_score(optimization_result)
95
+
96
+ # Enhanced metadata
97
+ enhanced_metadata = metadata.merge({
98
+ optimizer: extract_optimizer_name(optimization_result),
99
+ optimization_timestamp: extract_optimization_timestamp(optimization_result),
100
+ trials_count: extract_trials_count(optimization_result),
101
+ auto_registered: true
102
+ })
103
+
104
+ # Register the version
105
+ version = @registry.register_version(
106
+ signature_name,
107
+ configuration,
108
+ metadata: enhanced_metadata,
109
+ program_id: extract_program_id(optimization_result)
110
+ )
111
+
112
+ # Update performance score
113
+ if performance_score
114
+ @registry.update_performance_score(signature_name, version.version, performance_score)
115
+ version = version.with_performance_score(performance_score)
116
+ end
117
+
118
+ # Check for auto-deployment
119
+ check_auto_deployment(signature_name, version)
120
+
121
+ version
122
+ end
123
+
124
+ # Create a deployment strategy
125
+ sig { params(signature_name: String, strategy: String).returns(T.nilable(SignatureRegistry::SignatureVersion)) }
126
+ def deploy_with_strategy(signature_name, strategy: nil)
127
+ strategy ||= @integration_config.deployment_strategy
128
+
129
+ case strategy
130
+ when "conservative"
131
+ deploy_conservative(signature_name)
132
+ when "aggressive"
133
+ deploy_aggressive(signature_name)
134
+ when "best_score"
135
+ deploy_best_score(signature_name)
136
+ else
137
+ nil
138
+ end
139
+ end
140
+
141
+ # Monitor and rollback if needed
142
+ sig { params(signature_name: String, current_score: Float).returns(T::Boolean) }
143
+ def monitor_and_rollback(signature_name, current_score)
144
+ return false unless @integration_config.rollback_on_performance_drop
145
+
146
+ deployed_version = @registry.get_deployed_version(signature_name)
147
+ return false unless deployed_version&.performance_score
148
+
149
+ # Check if performance dropped significantly
150
+ performance_drop = deployed_version.performance_score - current_score
151
+ threshold_drop = deployed_version.performance_score * @integration_config.rollback_threshold
152
+
153
+ if performance_drop > threshold_drop
154
+ rollback_result = @registry.rollback(signature_name)
155
+ emit_automatic_rollback_event(signature_name, current_score, deployed_version.performance_score)
156
+ !rollback_result.nil?
157
+ else
158
+ false
159
+ end
160
+ end
161
+
162
+ # Get deployment status and recommendations
163
+ sig { params(signature_name: String).returns(T::Hash[Symbol, T.untyped]) }
164
+ def get_deployment_status(signature_name)
165
+ deployed_version = @registry.get_deployed_version(signature_name)
166
+ all_versions = @registry.list_versions(signature_name)
167
+ performance_history = @registry.get_performance_history(signature_name)
168
+
169
+ recommendations = generate_deployment_recommendations(signature_name, all_versions, deployed_version)
170
+
171
+ {
172
+ deployed_version: deployed_version&.to_h,
173
+ total_versions: all_versions.size,
174
+ performance_history: performance_history,
175
+ recommendations: recommendations,
176
+ last_updated: all_versions.max_by(&:created_at)&.created_at&.iso8601
177
+ }
178
+ end
179
+
180
+ # Create a safe deployment plan
181
+ sig { params(signature_name: String, target_version: String).returns(T::Hash[Symbol, T.untyped]) }
182
+ def create_deployment_plan(signature_name, target_version)
183
+ current_deployed = @registry.get_deployed_version(signature_name)
184
+ target = @registry.list_versions(signature_name).find { |v| v.version == target_version }
185
+
186
+ return { error: "Target version not found" } unless target
187
+
188
+ plan = {
189
+ signature_name: signature_name,
190
+ current_version: current_deployed&.version,
191
+ target_version: target_version,
192
+ deployment_safe: true,
193
+ checks: [],
194
+ recommendations: []
195
+ }
196
+
197
+ # Performance check
198
+ if current_deployed&.performance_score && target.performance_score
199
+ performance_change = target.performance_score - current_deployed.performance_score
200
+ plan[:performance_change] = performance_change
201
+
202
+ if performance_change < 0
203
+ plan[:checks] << "Performance regression detected"
204
+ plan[:deployment_safe] = false
205
+ else
206
+ plan[:checks] << "Performance improvement expected"
207
+ end
208
+ else
209
+ plan[:checks] << "No performance data available"
210
+ plan[:recommendations] << "Run evaluation before deployment"
211
+ end
212
+
213
+ # Version age check
214
+ if target.created_at < (Time.now - 7 * 24 * 60 * 60) # 7 days old
215
+ plan[:recommendations] << "Target version is more than 7 days old"
216
+ end
217
+
218
+ # Configuration complexity check
219
+ config_complexity = estimate_configuration_complexity(target.configuration)
220
+ if config_complexity > 0.8
221
+ plan[:recommendations] << "Complex configuration detected - consider gradual rollout"
222
+ end
223
+
224
+ plan
225
+ end
226
+
227
+ # Bulk operations for managing multiple signatures
228
+ sig { params(signature_names: T::Array[String]).returns(T::Hash[String, T.untyped]) }
229
+ def bulk_deployment_status(signature_names)
230
+ results = {}
231
+
232
+ signature_names.each do |name|
233
+ results[name] = get_deployment_status(name)
234
+ end
235
+
236
+ results
237
+ end
238
+
239
+ # Clean up old versions across all signatures
240
+ sig { returns(T::Hash[Symbol, Integer]) }
241
+ def cleanup_old_versions
242
+ cleaned_signatures = 0
243
+ cleaned_versions = 0
244
+
245
+ @registry.list_signatures.each do |signature_name|
246
+ versions = @registry.list_versions(signature_name)
247
+
248
+ # Keep deployed version and recent versions
249
+ deployed_version = versions.find(&:is_deployed)
250
+ recent_versions = versions.sort_by(&:created_at).last(5)
251
+
252
+ keep_versions = [deployed_version, *recent_versions].compact.uniq
253
+
254
+ if keep_versions.size < versions.size
255
+ @registry.send(:save_signature_versions, signature_name, keep_versions)
256
+ cleaned_signatures += 1
257
+ cleaned_versions += (versions.size - keep_versions.size)
258
+ end
259
+ end
260
+
261
+ {
262
+ cleaned_signatures: cleaned_signatures,
263
+ cleaned_versions: cleaned_versions
264
+ }
265
+ end
266
+
267
+ # Global registry instance
268
+ @@instance = T.let(nil, T.nilable(RegistryManager))
269
+
270
+ sig { returns(RegistryManager) }
271
+ def self.instance
272
+ @@instance ||= new
273
+ end
274
+
275
+ sig { params(registry_config: SignatureRegistry::RegistryConfig, integration_config: RegistryIntegrationConfig).void }
276
+ def self.configure(registry_config: nil, integration_config: nil)
277
+ @@instance = new(registry_config: registry_config, integration_config: integration_config)
278
+ end
279
+
280
+ private
281
+
282
+ sig { params(optimization_result: T.untyped).returns(T.nilable(String)) }
283
+ def extract_signature_name(optimization_result)
284
+ if optimization_result.respond_to?(:optimized_program)
285
+ program = optimization_result.optimized_program
286
+ if program.respond_to?(:signature_class)
287
+ program.signature_class&.name
288
+ end
289
+ end
290
+ end
291
+
292
+ sig { params(optimization_result: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
293
+ def extract_configuration(optimization_result)
294
+ config = {}
295
+
296
+ if optimization_result.respond_to?(:optimized_program)
297
+ program = optimization_result.optimized_program
298
+
299
+ # Extract instruction
300
+ if program.respond_to?(:prompt) && program.prompt.respond_to?(:instruction)
301
+ config[:instruction] = program.prompt.instruction
302
+ end
303
+
304
+ # Extract few-shot examples
305
+ if program.respond_to?(:few_shot_examples)
306
+ config[:few_shot_examples_count] = program.few_shot_examples.size
307
+ config[:few_shot_examples] = program.few_shot_examples.map do |example|
308
+ {
309
+ input: example.respond_to?(:input) ? example.input : nil,
310
+ output: example.respond_to?(:output) ? example.output : nil
311
+ }
312
+ end
313
+ end
314
+ end
315
+
316
+ # Extract optimization metadata
317
+ if optimization_result.respond_to?(:metadata)
318
+ config[:optimization_metadata] = optimization_result.metadata
319
+ end
320
+
321
+ config
322
+ end
323
+
324
+ sig { params(optimization_result: T.untyped).returns(T.nilable(Float)) }
325
+ def extract_performance_score(optimization_result)
326
+ if optimization_result.respond_to?(:best_score_value)
327
+ optimization_result.best_score_value
328
+ elsif optimization_result.respond_to?(:scores) && optimization_result.scores.is_a?(Hash)
329
+ optimization_result.scores.values.first
330
+ end
331
+ end
332
+
333
+ sig { params(optimization_result: T.untyped).returns(T.nilable(String)) }
334
+ def extract_optimizer_name(optimization_result)
335
+ if optimization_result.respond_to?(:metadata) && optimization_result.metadata[:optimizer]
336
+ optimization_result.metadata[:optimizer]
337
+ else
338
+ optimization_result.class.name
339
+ end
340
+ end
341
+
342
+ sig { params(optimization_result: T.untyped).returns(T.nilable(String)) }
343
+ def extract_optimization_timestamp(optimization_result)
344
+ if optimization_result.respond_to?(:metadata)
345
+ optimization_result.metadata[:optimization_timestamp]
346
+ end
347
+ end
348
+
349
+ sig { params(optimization_result: T.untyped).returns(T.nilable(Integer)) }
350
+ def extract_trials_count(optimization_result)
351
+ if optimization_result.respond_to?(:history) && optimization_result.history[:total_trials]
352
+ optimization_result.history[:total_trials]
353
+ end
354
+ end
355
+
356
+ sig { params(optimization_result: T.untyped).returns(T.nilable(String)) }
357
+ def extract_program_id(optimization_result)
358
+ # This would typically come from storage system
359
+ if optimization_result.respond_to?(:metadata) && optimization_result.metadata[:program_id]
360
+ optimization_result.metadata[:program_id]
361
+ end
362
+ end
363
+
364
+ sig { params(signature_name: String, version: SignatureRegistry::SignatureVersion).void }
365
+ def check_auto_deployment(signature_name, version)
366
+ return unless @integration_config.auto_deploy_best_versions
367
+ return unless version.performance_score
368
+
369
+ deployed_version = @registry.get_deployed_version(signature_name)
370
+
371
+ should_deploy = if deployed_version&.performance_score
372
+ improvement = version.performance_score - deployed_version.performance_score
373
+ improvement >= @integration_config.auto_deploy_threshold
374
+ else
375
+ true # No current deployment, deploy this one
376
+ end
377
+
378
+ if should_deploy
379
+ @registry.deploy_version(signature_name, version.version)
380
+ emit_auto_deployment_event(signature_name, version.version)
381
+ end
382
+ end
383
+
384
+ sig { params(signature_name: String).returns(T.nilable(SignatureRegistry::SignatureVersion)) }
385
+ def deploy_conservative(signature_name)
386
+ versions = @registry.list_versions(signature_name)
387
+ deployed = @registry.get_deployed_version(signature_name)
388
+
389
+ # Only deploy if significantly better than current
390
+ candidates = versions.select { |v| v.performance_score }
391
+ return nil if candidates.empty?
392
+
393
+ best_candidate = candidates.max_by(&:performance_score)
394
+
395
+ if deployed&.performance_score
396
+ improvement = best_candidate.performance_score - deployed.performance_score
397
+ threshold = deployed.performance_score * 0.1 # 10% improvement required
398
+
399
+ if improvement >= threshold
400
+ @registry.deploy_version(signature_name, best_candidate.version)
401
+ else
402
+ nil
403
+ end
404
+ else
405
+ @registry.deploy_version(signature_name, best_candidate.version)
406
+ end
407
+ end
408
+
409
+ sig { params(signature_name: String).returns(T.nilable(SignatureRegistry::SignatureVersion)) }
410
+ def deploy_aggressive(signature_name)
411
+ versions = @registry.list_versions(signature_name)
412
+ candidates = versions.select { |v| v.performance_score }
413
+ return nil if candidates.empty?
414
+
415
+ # Deploy the best version regardless of current deployment
416
+ best_candidate = candidates.max_by(&:performance_score)
417
+ @registry.deploy_version(signature_name, best_candidate.version)
418
+ end
419
+
420
+ sig { params(signature_name: String).returns(T.nilable(SignatureRegistry::SignatureVersion)) }
421
+ def deploy_best_score(signature_name)
422
+ deploy_aggressive(signature_name) # Same as aggressive for now
423
+ end
424
+
425
+ sig do
426
+ params(
427
+ signature_name: String,
428
+ versions: T::Array[SignatureRegistry::SignatureVersion],
429
+ deployed: T.nilable(SignatureRegistry::SignatureVersion)
430
+ ).returns(T::Array[String])
431
+ end
432
+ def generate_deployment_recommendations(signature_name, versions, deployed)
433
+ recommendations = []
434
+
435
+ if deployed.nil?
436
+ if versions.any? { |v| v.performance_score }
437
+ recommendations << "No version deployed - consider deploying best performing version"
438
+ else
439
+ recommendations << "No version deployed - run evaluation on versions before deployment"
440
+ end
441
+ else
442
+ # Check for better versions
443
+ better_versions = versions.select do |v|
444
+ v.performance_score &&
445
+ deployed.performance_score &&
446
+ v.performance_score > deployed.performance_score
447
+ end
448
+
449
+ if better_versions.any?
450
+ best = better_versions.max_by(&:performance_score)
451
+ improvement = ((best.performance_score - deployed.performance_score) / deployed.performance_score * 100).round(1)
452
+ recommendations << "Version #{best.version} shows #{improvement}% improvement"
453
+ end
454
+
455
+ # Check for old deployment
456
+ if deployed.created_at < (Time.now - 30 * 24 * 60 * 60) # 30 days
457
+ recommendations << "Current deployment is over 30 days old - consider updating"
458
+ end
459
+ end
460
+
461
+ recommendations
462
+ end
463
+
464
+ sig { params(configuration: T::Hash[Symbol, T.untyped]).returns(Float) }
465
+ def estimate_configuration_complexity(configuration)
466
+ complexity = 0.0
467
+
468
+ # Instruction complexity
469
+ if configuration[:instruction]
470
+ instruction_length = configuration[:instruction].length
471
+ complexity += [instruction_length / 1000.0, 0.5].min
472
+ end
473
+
474
+ # Few-shot examples complexity
475
+ if configuration[:few_shot_examples_count]
476
+ examples_complexity = [configuration[:few_shot_examples_count] / 10.0, 0.5].min
477
+ complexity += examples_complexity
478
+ end
479
+
480
+ [complexity, 1.0].min
481
+ end
482
+
483
+ sig { params(signature_name: String, version: String).void }
484
+ def emit_auto_deployment_event(signature_name, version)
485
+ DSPy::Instrumentation.emit('dspy.registry.auto_deployment', {
486
+ signature_name: signature_name,
487
+ version: version,
488
+ timestamp: Time.now.iso8601
489
+ })
490
+ end
491
+
492
+ sig { params(signature_name: String, current_score: Float, previous_score: Float).void }
493
+ def emit_automatic_rollback_event(signature_name, current_score, previous_score)
494
+ DSPy::Instrumentation.emit('dspy.registry.automatic_rollback', {
495
+ signature_name: signature_name,
496
+ current_score: current_score,
497
+ previous_score: previous_score,
498
+ performance_drop: previous_score - current_score,
499
+ timestamp: Time.now.iso8601
500
+ })
501
+ end
502
+ end
503
+ end
504
+ end