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.
- checksums.yaml +4 -4
- data/README.md +69 -382
- data/lib/dspy/chain_of_thought.rb +57 -0
- data/lib/dspy/evaluate.rb +554 -0
- data/lib/dspy/example.rb +203 -0
- data/lib/dspy/few_shot_example.rb +81 -0
- data/lib/dspy/instrumentation.rb +97 -8
- data/lib/dspy/lm/adapter_factory.rb +6 -8
- data/lib/dspy/lm.rb +5 -7
- data/lib/dspy/predict.rb +32 -34
- data/lib/dspy/prompt.rb +222 -0
- data/lib/dspy/propose/grounded_proposer.rb +560 -0
- data/lib/dspy/registry/registry_manager.rb +504 -0
- data/lib/dspy/registry/signature_registry.rb +725 -0
- data/lib/dspy/storage/program_storage.rb +442 -0
- data/lib/dspy/storage/storage_manager.rb +331 -0
- data/lib/dspy/subscribers/langfuse_subscriber.rb +669 -0
- data/lib/dspy/subscribers/logger_subscriber.rb +120 -0
- data/lib/dspy/subscribers/newrelic_subscriber.rb +686 -0
- data/lib/dspy/subscribers/otel_subscriber.rb +538 -0
- data/lib/dspy/teleprompt/data_handler.rb +107 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +790 -0
- data/lib/dspy/teleprompt/simple_optimizer.rb +497 -0
- data/lib/dspy/teleprompt/teleprompter.rb +336 -0
- data/lib/dspy/teleprompt/utils.rb +380 -0
- data/lib/dspy/version.rb +5 -0
- data/lib/dspy.rb +16 -0
- metadata +29 -12
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +0 -81
@@ -0,0 +1,725 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require 'yaml'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'digest'
|
7
|
+
|
8
|
+
module DSPy
|
9
|
+
module Registry
|
10
|
+
# Registry for managing signature versions and deployments
|
11
|
+
# Provides version control, rollback capabilities, and deployment tracking
|
12
|
+
class SignatureRegistry
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
# Represents a versioned signature with deployment information
|
16
|
+
class SignatureVersion
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
sig { returns(String) }
|
20
|
+
attr_reader :signature_name
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
attr_reader :version
|
24
|
+
|
25
|
+
sig { returns(String) }
|
26
|
+
attr_reader :version_hash
|
27
|
+
|
28
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
29
|
+
attr_reader :configuration
|
30
|
+
|
31
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
32
|
+
attr_reader :metadata
|
33
|
+
|
34
|
+
sig { returns(Time) }
|
35
|
+
attr_reader :created_at
|
36
|
+
|
37
|
+
sig { returns(T.nilable(String)) }
|
38
|
+
attr_reader :program_id
|
39
|
+
|
40
|
+
sig { returns(T::Boolean) }
|
41
|
+
attr_reader :is_deployed
|
42
|
+
|
43
|
+
sig { returns(T.nilable(Float)) }
|
44
|
+
attr_reader :performance_score
|
45
|
+
|
46
|
+
sig do
|
47
|
+
params(
|
48
|
+
signature_name: String,
|
49
|
+
version: String,
|
50
|
+
configuration: T::Hash[Symbol, T.untyped],
|
51
|
+
metadata: T::Hash[Symbol, T.untyped],
|
52
|
+
program_id: T.nilable(String),
|
53
|
+
is_deployed: T::Boolean,
|
54
|
+
performance_score: T.nilable(Float)
|
55
|
+
).void
|
56
|
+
end
|
57
|
+
def initialize(signature_name:, version:, configuration:, metadata: {}, program_id: nil, is_deployed: false, performance_score: nil)
|
58
|
+
@signature_name = signature_name
|
59
|
+
@version = version
|
60
|
+
@configuration = configuration.freeze
|
61
|
+
@metadata = metadata.merge({
|
62
|
+
created_at: Time.now.iso8601,
|
63
|
+
registry_version: "1.0"
|
64
|
+
}).freeze
|
65
|
+
@created_at = Time.now
|
66
|
+
@program_id = program_id
|
67
|
+
@is_deployed = is_deployed
|
68
|
+
@performance_score = performance_score
|
69
|
+
@version_hash = generate_version_hash
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
73
|
+
def to_h
|
74
|
+
{
|
75
|
+
signature_name: @signature_name,
|
76
|
+
version: @version,
|
77
|
+
version_hash: @version_hash,
|
78
|
+
configuration: @configuration,
|
79
|
+
metadata: @metadata,
|
80
|
+
created_at: @created_at.iso8601,
|
81
|
+
program_id: @program_id,
|
82
|
+
is_deployed: @is_deployed,
|
83
|
+
performance_score: @performance_score
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(SignatureVersion) }
|
88
|
+
def self.from_h(data)
|
89
|
+
version = new(
|
90
|
+
signature_name: data[:signature_name],
|
91
|
+
version: data[:version],
|
92
|
+
configuration: data[:configuration] || {},
|
93
|
+
metadata: data[:metadata] || {},
|
94
|
+
program_id: data[:program_id],
|
95
|
+
is_deployed: data[:is_deployed] || false,
|
96
|
+
performance_score: data[:performance_score]
|
97
|
+
)
|
98
|
+
version.instance_variable_set(:@created_at, Time.parse(data[:created_at])) if data[:created_at]
|
99
|
+
version.instance_variable_set(:@version_hash, data[:version_hash]) if data[:version_hash]
|
100
|
+
version
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { params(score: Float).returns(SignatureVersion) }
|
104
|
+
def with_performance_score(score)
|
105
|
+
self.class.new(
|
106
|
+
signature_name: @signature_name,
|
107
|
+
version: @version,
|
108
|
+
configuration: @configuration,
|
109
|
+
metadata: @metadata,
|
110
|
+
program_id: @program_id,
|
111
|
+
is_deployed: @is_deployed,
|
112
|
+
performance_score: score
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { returns(SignatureVersion) }
|
117
|
+
def deploy
|
118
|
+
self.class.new(
|
119
|
+
signature_name: @signature_name,
|
120
|
+
version: @version,
|
121
|
+
configuration: @configuration,
|
122
|
+
metadata: @metadata,
|
123
|
+
program_id: @program_id,
|
124
|
+
is_deployed: true,
|
125
|
+
performance_score: @performance_score
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
sig { returns(SignatureVersion) }
|
130
|
+
def undeploy
|
131
|
+
self.class.new(
|
132
|
+
signature_name: @signature_name,
|
133
|
+
version: @version,
|
134
|
+
configuration: @configuration,
|
135
|
+
metadata: @metadata,
|
136
|
+
program_id: @program_id,
|
137
|
+
is_deployed: false,
|
138
|
+
performance_score: @performance_score
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
sig { returns(String) }
|
145
|
+
def generate_version_hash
|
146
|
+
content = "#{@signature_name}_#{@version}_#{@configuration.hash}_#{@created_at.to_f}"
|
147
|
+
Digest::SHA256.hexdigest(content)[0, 12]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Configuration for the registry
|
152
|
+
class RegistryConfig
|
153
|
+
extend T::Sig
|
154
|
+
|
155
|
+
sig { returns(String) }
|
156
|
+
attr_accessor :registry_path
|
157
|
+
|
158
|
+
sig { returns(String) }
|
159
|
+
attr_accessor :config_file
|
160
|
+
|
161
|
+
sig { returns(T::Boolean) }
|
162
|
+
attr_accessor :auto_version
|
163
|
+
|
164
|
+
sig { returns(Integer) }
|
165
|
+
attr_accessor :max_versions_per_signature
|
166
|
+
|
167
|
+
sig { returns(T::Boolean) }
|
168
|
+
attr_accessor :backup_on_deploy
|
169
|
+
|
170
|
+
sig { returns(String) }
|
171
|
+
attr_accessor :version_format
|
172
|
+
|
173
|
+
sig { void }
|
174
|
+
def initialize
|
175
|
+
@registry_path = "./dspy_registry"
|
176
|
+
@config_file = "registry.yml"
|
177
|
+
@auto_version = true
|
178
|
+
@max_versions_per_signature = 10
|
179
|
+
@backup_on_deploy = true
|
180
|
+
@version_format = "v%Y%m%d_%H%M%S" # timestamp-based versions
|
181
|
+
end
|
182
|
+
|
183
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
184
|
+
def to_h
|
185
|
+
{
|
186
|
+
registry_path: @registry_path,
|
187
|
+
config_file: @config_file,
|
188
|
+
auto_version: @auto_version,
|
189
|
+
max_versions_per_signature: @max_versions_per_signature,
|
190
|
+
backup_on_deploy: @backup_on_deploy,
|
191
|
+
version_format: @version_format
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).void }
|
196
|
+
def from_h(data)
|
197
|
+
@registry_path = data[:registry_path] if data[:registry_path]
|
198
|
+
@config_file = data[:config_file] if data[:config_file]
|
199
|
+
@auto_version = data[:auto_version] if data.key?(:auto_version)
|
200
|
+
@max_versions_per_signature = data[:max_versions_per_signature] if data[:max_versions_per_signature]
|
201
|
+
@backup_on_deploy = data[:backup_on_deploy] if data.key?(:backup_on_deploy)
|
202
|
+
@version_format = data[:version_format] if data[:version_format]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
sig { returns(RegistryConfig) }
|
207
|
+
attr_reader :config
|
208
|
+
|
209
|
+
sig { params(config: T.nilable(RegistryConfig)).void }
|
210
|
+
def initialize(config: nil)
|
211
|
+
@config = config || RegistryConfig.new
|
212
|
+
setup_registry_directory
|
213
|
+
load_or_create_config
|
214
|
+
end
|
215
|
+
|
216
|
+
# Register a new signature version
|
217
|
+
sig do
|
218
|
+
params(
|
219
|
+
signature_name: String,
|
220
|
+
configuration: T::Hash[Symbol, T.untyped],
|
221
|
+
metadata: T::Hash[Symbol, T.untyped],
|
222
|
+
program_id: T.nilable(String),
|
223
|
+
version: T.nilable(String)
|
224
|
+
).returns(SignatureVersion)
|
225
|
+
end
|
226
|
+
def register_version(signature_name, configuration, metadata: {}, program_id: nil, version: nil)
|
227
|
+
emit_register_start_event(signature_name, version)
|
228
|
+
|
229
|
+
begin
|
230
|
+
version ||= generate_version_name if @config.auto_version
|
231
|
+
|
232
|
+
signature_version = SignatureVersion.new(
|
233
|
+
signature_name: signature_name,
|
234
|
+
version: version,
|
235
|
+
configuration: configuration,
|
236
|
+
metadata: metadata,
|
237
|
+
program_id: program_id
|
238
|
+
)
|
239
|
+
|
240
|
+
# Load existing versions
|
241
|
+
versions = load_signature_versions(signature_name)
|
242
|
+
|
243
|
+
# Check if version already exists
|
244
|
+
if versions.any? { |v| v.version == version }
|
245
|
+
raise ArgumentError, "Version #{version} already exists for signature #{signature_name}"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Add new version
|
249
|
+
versions << signature_version
|
250
|
+
|
251
|
+
# Cleanup old versions if needed
|
252
|
+
if @config.max_versions_per_signature > 0 && versions.size > @config.max_versions_per_signature
|
253
|
+
versions = versions.sort_by(&:created_at).last(@config.max_versions_per_signature)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Save versions
|
257
|
+
save_signature_versions(signature_name, versions)
|
258
|
+
|
259
|
+
emit_register_complete_event(signature_version)
|
260
|
+
signature_version
|
261
|
+
|
262
|
+
rescue => error
|
263
|
+
emit_register_error_event(signature_name, version, error)
|
264
|
+
raise
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Deploy a specific version
|
269
|
+
sig { params(signature_name: String, version: String).returns(T.nilable(SignatureVersion)) }
|
270
|
+
def deploy_version(signature_name, version)
|
271
|
+
emit_deploy_start_event(signature_name, version)
|
272
|
+
|
273
|
+
begin
|
274
|
+
versions = load_signature_versions(signature_name)
|
275
|
+
target_version = versions.find { |v| v.version == version }
|
276
|
+
|
277
|
+
return nil unless target_version
|
278
|
+
|
279
|
+
# Backup current deployment if configured
|
280
|
+
if @config.backup_on_deploy
|
281
|
+
current_deployed = get_deployed_version(signature_name)
|
282
|
+
if current_deployed
|
283
|
+
create_deployment_backup(current_deployed)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Mark currently deployed version as previously deployed and undeploy all
|
288
|
+
versions = versions.map do |v|
|
289
|
+
if v.is_deployed
|
290
|
+
# Add deployment history metadata
|
291
|
+
updated_metadata = v.metadata.merge(was_deployed: true, last_deployed_at: Time.now.iso8601)
|
292
|
+
SignatureVersion.new(
|
293
|
+
signature_name: v.signature_name,
|
294
|
+
version: v.version,
|
295
|
+
configuration: v.configuration,
|
296
|
+
metadata: updated_metadata,
|
297
|
+
program_id: v.program_id,
|
298
|
+
is_deployed: false,
|
299
|
+
performance_score: v.performance_score
|
300
|
+
)
|
301
|
+
else
|
302
|
+
v.undeploy
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Deploy target version
|
307
|
+
target_index = versions.index { |v| v.version == version }
|
308
|
+
versions[target_index] = target_version.deploy
|
309
|
+
|
310
|
+
save_signature_versions(signature_name, versions)
|
311
|
+
|
312
|
+
deployed_version = versions[target_index]
|
313
|
+
emit_deploy_complete_event(deployed_version)
|
314
|
+
deployed_version
|
315
|
+
|
316
|
+
rescue => error
|
317
|
+
emit_deploy_error_event(signature_name, version, error)
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Rollback to previous deployed version
|
323
|
+
sig { params(signature_name: String).returns(T.nilable(SignatureVersion)) }
|
324
|
+
def rollback(signature_name)
|
325
|
+
emit_rollback_start_event(signature_name)
|
326
|
+
|
327
|
+
begin
|
328
|
+
versions = load_signature_versions(signature_name)
|
329
|
+
|
330
|
+
# Find versions that have deployment history (previously deployed)
|
331
|
+
# Look for versions with deployment metadata or that were deployed
|
332
|
+
deployed_history = versions.select do |v|
|
333
|
+
v.metadata[:was_deployed] || v.is_deployed
|
334
|
+
end.sort_by(&:created_at)
|
335
|
+
|
336
|
+
# If we don't have deployment history, check if any versions exist
|
337
|
+
if deployed_history.empty?
|
338
|
+
# Look for the second newest version as fallback
|
339
|
+
all_versions = versions.sort_by(&:created_at)
|
340
|
+
if all_versions.size >= 2
|
341
|
+
previous_version = all_versions[-2]
|
342
|
+
result = deploy_version(signature_name, previous_version.version)
|
343
|
+
if result
|
344
|
+
emit_rollback_complete_event(result)
|
345
|
+
end
|
346
|
+
return result
|
347
|
+
end
|
348
|
+
elsif deployed_history.size >= 2
|
349
|
+
# Get the previous deployed version (excluding currently deployed)
|
350
|
+
current_deployed = versions.find(&:is_deployed)
|
351
|
+
previous_versions = deployed_history.reject { |v| v.version == current_deployed&.version }
|
352
|
+
|
353
|
+
if previous_versions.any?
|
354
|
+
previous_version = previous_versions.last
|
355
|
+
result = deploy_version(signature_name, previous_version.version)
|
356
|
+
if result
|
357
|
+
emit_rollback_complete_event(result)
|
358
|
+
end
|
359
|
+
return result
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
emit_rollback_error_event(signature_name, "No previous version to rollback to")
|
364
|
+
nil
|
365
|
+
|
366
|
+
rescue => error
|
367
|
+
emit_rollback_error_event(signature_name, error.message)
|
368
|
+
nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Get currently deployed version
|
373
|
+
sig { params(signature_name: String).returns(T.nilable(SignatureVersion)) }
|
374
|
+
def get_deployed_version(signature_name)
|
375
|
+
versions = load_signature_versions(signature_name)
|
376
|
+
versions.find(&:is_deployed)
|
377
|
+
end
|
378
|
+
|
379
|
+
# List all versions for a signature
|
380
|
+
sig { params(signature_name: String).returns(T::Array[SignatureVersion]) }
|
381
|
+
def list_versions(signature_name)
|
382
|
+
load_signature_versions(signature_name)
|
383
|
+
end
|
384
|
+
|
385
|
+
# List all signatures in registry
|
386
|
+
sig { returns(T::Array[String]) }
|
387
|
+
def list_signatures
|
388
|
+
Dir.glob(File.join(@config.registry_path, "signatures", "*.yml")).map do |file|
|
389
|
+
File.basename(file, ".yml")
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Update performance score for a version
|
394
|
+
sig { params(signature_name: String, version: String, score: Float).returns(T.nilable(SignatureVersion)) }
|
395
|
+
def update_performance_score(signature_name, version, score)
|
396
|
+
versions = load_signature_versions(signature_name)
|
397
|
+
target_index = versions.index { |v| v.version == version }
|
398
|
+
|
399
|
+
return nil unless target_index
|
400
|
+
|
401
|
+
versions[target_index] = versions[target_index].with_performance_score(score)
|
402
|
+
save_signature_versions(signature_name, versions)
|
403
|
+
|
404
|
+
emit_performance_update_event(versions[target_index])
|
405
|
+
versions[target_index]
|
406
|
+
end
|
407
|
+
|
408
|
+
# Get performance history for a signature
|
409
|
+
sig { params(signature_name: String).returns(T::Hash[Symbol, T.untyped]) }
|
410
|
+
def get_performance_history(signature_name)
|
411
|
+
versions = load_signature_versions(signature_name)
|
412
|
+
versions_with_scores = versions.select { |v| v.performance_score }
|
413
|
+
|
414
|
+
return { versions: [], trends: {} } if versions_with_scores.empty?
|
415
|
+
|
416
|
+
sorted_versions = versions_with_scores.sort_by(&:created_at)
|
417
|
+
|
418
|
+
{
|
419
|
+
versions: sorted_versions.map do |v|
|
420
|
+
{
|
421
|
+
version: v.version,
|
422
|
+
score: v.performance_score,
|
423
|
+
created_at: v.created_at.iso8601,
|
424
|
+
is_deployed: v.is_deployed
|
425
|
+
}
|
426
|
+
end,
|
427
|
+
trends: {
|
428
|
+
latest_score: sorted_versions.last.performance_score,
|
429
|
+
best_score: versions_with_scores.map(&:performance_score).compact.max,
|
430
|
+
worst_score: versions_with_scores.map(&:performance_score).compact.min,
|
431
|
+
improvement_trend: calculate_improvement_trend(sorted_versions)
|
432
|
+
}
|
433
|
+
}
|
434
|
+
end
|
435
|
+
|
436
|
+
# Compare two versions
|
437
|
+
sig do
|
438
|
+
params(
|
439
|
+
signature_name: String,
|
440
|
+
version1: String,
|
441
|
+
version2: String
|
442
|
+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
443
|
+
end
|
444
|
+
def compare_versions(signature_name, version1, version2)
|
445
|
+
versions = load_signature_versions(signature_name)
|
446
|
+
v1 = versions.find { |v| v.version == version1 }
|
447
|
+
v2 = versions.find { |v| v.version == version2 }
|
448
|
+
|
449
|
+
return nil unless v1 && v2
|
450
|
+
|
451
|
+
{
|
452
|
+
version_1: {
|
453
|
+
version: v1.version,
|
454
|
+
created_at: v1.created_at.iso8601,
|
455
|
+
performance_score: v1.performance_score,
|
456
|
+
is_deployed: v1.is_deployed,
|
457
|
+
configuration: v1.configuration
|
458
|
+
},
|
459
|
+
version_2: {
|
460
|
+
version: v2.version,
|
461
|
+
created_at: v2.created_at.iso8601,
|
462
|
+
performance_score: v2.performance_score,
|
463
|
+
is_deployed: v2.is_deployed,
|
464
|
+
configuration: v2.configuration
|
465
|
+
},
|
466
|
+
comparison: {
|
467
|
+
age_difference_hours: ((v1.created_at - v2.created_at) / 3600).round(2),
|
468
|
+
performance_difference: (v1.performance_score || 0) - (v2.performance_score || 0),
|
469
|
+
configuration_changes: compare_configurations(v1.configuration, v2.configuration)
|
470
|
+
}
|
471
|
+
}
|
472
|
+
end
|
473
|
+
|
474
|
+
# Export registry state
|
475
|
+
sig { params(export_path: String).void }
|
476
|
+
def export_registry(export_path)
|
477
|
+
registry_data = {
|
478
|
+
exported_at: Time.now.iso8601,
|
479
|
+
config: @config.to_h,
|
480
|
+
signatures: {}
|
481
|
+
}
|
482
|
+
|
483
|
+
list_signatures.each do |signature_name|
|
484
|
+
registry_data[:signatures][signature_name] = load_signature_versions(signature_name).map(&:to_h)
|
485
|
+
end
|
486
|
+
|
487
|
+
File.write(export_path, YAML.dump(registry_data))
|
488
|
+
emit_export_event(export_path, list_signatures.size)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Import registry state
|
492
|
+
sig { params(import_path: String).void }
|
493
|
+
def import_registry(import_path)
|
494
|
+
data = YAML.load_file(import_path, symbolize_names: true)
|
495
|
+
imported_count = 0
|
496
|
+
|
497
|
+
data[:signatures].each do |signature_name, versions_data|
|
498
|
+
versions = versions_data.map { |v| SignatureVersion.from_h(v) }
|
499
|
+
save_signature_versions(signature_name.to_s, versions)
|
500
|
+
imported_count += 1
|
501
|
+
end
|
502
|
+
|
503
|
+
emit_import_event(import_path, imported_count)
|
504
|
+
end
|
505
|
+
|
506
|
+
private
|
507
|
+
|
508
|
+
sig { void }
|
509
|
+
def setup_registry_directory
|
510
|
+
FileUtils.mkdir_p(@config.registry_path) unless Dir.exist?(@config.registry_path)
|
511
|
+
|
512
|
+
signatures_dir = File.join(@config.registry_path, "signatures")
|
513
|
+
FileUtils.mkdir_p(signatures_dir) unless Dir.exist?(signatures_dir)
|
514
|
+
|
515
|
+
backups_dir = File.join(@config.registry_path, "backups")
|
516
|
+
FileUtils.mkdir_p(backups_dir) unless Dir.exist?(backups_dir)
|
517
|
+
end
|
518
|
+
|
519
|
+
sig { void }
|
520
|
+
def load_or_create_config
|
521
|
+
config_path = File.join(@config.registry_path, @config.config_file)
|
522
|
+
|
523
|
+
if File.exist?(config_path)
|
524
|
+
config_data = YAML.load_file(config_path, symbolize_names: true)
|
525
|
+
@config.from_h(config_data)
|
526
|
+
else
|
527
|
+
save_config
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
sig { void }
|
532
|
+
def save_config
|
533
|
+
config_path = File.join(@config.registry_path, @config.config_file)
|
534
|
+
File.write(config_path, YAML.dump(@config.to_h))
|
535
|
+
end
|
536
|
+
|
537
|
+
sig { returns(String) }
|
538
|
+
def generate_version_name
|
539
|
+
Time.now.strftime(@config.version_format)
|
540
|
+
end
|
541
|
+
|
542
|
+
sig { params(signature_name: String).returns(T::Array[SignatureVersion]) }
|
543
|
+
def load_signature_versions(signature_name)
|
544
|
+
file_path = signature_file_path(signature_name)
|
545
|
+
|
546
|
+
return [] unless File.exist?(file_path)
|
547
|
+
|
548
|
+
versions_data = YAML.load_file(file_path, symbolize_names: true)
|
549
|
+
versions_data.map { |data| SignatureVersion.from_h(data) }
|
550
|
+
end
|
551
|
+
|
552
|
+
sig { params(signature_name: String, versions: T::Array[SignatureVersion]).void }
|
553
|
+
def save_signature_versions(signature_name, versions)
|
554
|
+
file_path = signature_file_path(signature_name)
|
555
|
+
versions_data = versions.map(&:to_h)
|
556
|
+
File.write(file_path, YAML.dump(versions_data))
|
557
|
+
end
|
558
|
+
|
559
|
+
sig { params(signature_name: String).returns(String) }
|
560
|
+
def signature_file_path(signature_name)
|
561
|
+
File.join(@config.registry_path, "signatures", "#{signature_name}.yml")
|
562
|
+
end
|
563
|
+
|
564
|
+
sig { params(version: SignatureVersion).void }
|
565
|
+
def create_deployment_backup(version)
|
566
|
+
backup_dir = File.join(@config.registry_path, "backups", version.signature_name)
|
567
|
+
FileUtils.mkdir_p(backup_dir) unless Dir.exist?(backup_dir)
|
568
|
+
|
569
|
+
backup_file = File.join(backup_dir, "#{version.version}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.yml")
|
570
|
+
File.write(backup_file, YAML.dump(version.to_h))
|
571
|
+
end
|
572
|
+
|
573
|
+
sig { params(versions: T::Array[SignatureVersion]).returns(Float) }
|
574
|
+
def calculate_improvement_trend(versions)
|
575
|
+
return 0.0 if versions.size < 2
|
576
|
+
|
577
|
+
scores = versions.map(&:performance_score).compact
|
578
|
+
return 0.0 if scores.size < 2
|
579
|
+
|
580
|
+
# Simple linear trend calculation
|
581
|
+
recent_scores = scores.last([scores.size / 2, 2].max)
|
582
|
+
older_scores = scores.first([scores.size / 2, 2].max)
|
583
|
+
|
584
|
+
recent_avg = recent_scores.sum.to_f / recent_scores.size
|
585
|
+
older_avg = older_scores.sum.to_f / older_scores.size
|
586
|
+
|
587
|
+
return 0.0 if older_avg == 0.0
|
588
|
+
|
589
|
+
((recent_avg - older_avg) / older_avg * 100).round(2)
|
590
|
+
end
|
591
|
+
|
592
|
+
sig { params(config1: T::Hash[Symbol, T.untyped], config2: T::Hash[Symbol, T.untyped]).returns(T::Array[String]) }
|
593
|
+
def compare_configurations(config1, config2)
|
594
|
+
changes = []
|
595
|
+
|
596
|
+
all_keys = (config1.keys + config2.keys).uniq
|
597
|
+
|
598
|
+
all_keys.each do |key|
|
599
|
+
if !config1.key?(key)
|
600
|
+
changes << "Added #{key}: #{config2[key]}"
|
601
|
+
elsif !config2.key?(key)
|
602
|
+
changes << "Removed #{key}: #{config1[key]}"
|
603
|
+
elsif config1[key] != config2[key]
|
604
|
+
changes << "Changed #{key}: #{config1[key]} → #{config2[key]}"
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
changes
|
609
|
+
end
|
610
|
+
|
611
|
+
# Event emission methods
|
612
|
+
sig { params(signature_name: String, version: T.nilable(String)).void }
|
613
|
+
def emit_register_start_event(signature_name, version)
|
614
|
+
DSPy::Instrumentation.emit('dspy.registry.register_start', {
|
615
|
+
signature_name: signature_name,
|
616
|
+
version: version,
|
617
|
+
timestamp: Time.now.iso8601
|
618
|
+
})
|
619
|
+
end
|
620
|
+
|
621
|
+
sig { params(version: SignatureVersion).void }
|
622
|
+
def emit_register_complete_event(version)
|
623
|
+
DSPy::Instrumentation.emit('dspy.registry.register_complete', {
|
624
|
+
signature_name: version.signature_name,
|
625
|
+
version: version.version,
|
626
|
+
version_hash: version.version_hash,
|
627
|
+
timestamp: Time.now.iso8601
|
628
|
+
})
|
629
|
+
end
|
630
|
+
|
631
|
+
sig { params(signature_name: String, version: T.nilable(String), error: Exception).void }
|
632
|
+
def emit_register_error_event(signature_name, version, error)
|
633
|
+
DSPy::Instrumentation.emit('dspy.registry.register_error', {
|
634
|
+
signature_name: signature_name,
|
635
|
+
version: version,
|
636
|
+
error: error.message,
|
637
|
+
timestamp: Time.now.iso8601
|
638
|
+
})
|
639
|
+
end
|
640
|
+
|
641
|
+
sig { params(signature_name: String, version: String).void }
|
642
|
+
def emit_deploy_start_event(signature_name, version)
|
643
|
+
DSPy::Instrumentation.emit('dspy.registry.deploy_start', {
|
644
|
+
signature_name: signature_name,
|
645
|
+
version: version,
|
646
|
+
timestamp: Time.now.iso8601
|
647
|
+
})
|
648
|
+
end
|
649
|
+
|
650
|
+
sig { params(version: SignatureVersion).void }
|
651
|
+
def emit_deploy_complete_event(version)
|
652
|
+
DSPy::Instrumentation.emit('dspy.registry.deploy_complete', {
|
653
|
+
signature_name: version.signature_name,
|
654
|
+
version: version.version,
|
655
|
+
performance_score: version.performance_score,
|
656
|
+
timestamp: Time.now.iso8601
|
657
|
+
})
|
658
|
+
end
|
659
|
+
|
660
|
+
sig { params(signature_name: String, version: String, error: Exception).void }
|
661
|
+
def emit_deploy_error_event(signature_name, version, error)
|
662
|
+
DSPy::Instrumentation.emit('dspy.registry.deploy_error', {
|
663
|
+
signature_name: signature_name,
|
664
|
+
version: version,
|
665
|
+
error: error.message,
|
666
|
+
timestamp: Time.now.iso8601
|
667
|
+
})
|
668
|
+
end
|
669
|
+
|
670
|
+
sig { params(signature_name: String).void }
|
671
|
+
def emit_rollback_start_event(signature_name)
|
672
|
+
DSPy::Instrumentation.emit('dspy.registry.rollback_start', {
|
673
|
+
signature_name: signature_name,
|
674
|
+
timestamp: Time.now.iso8601
|
675
|
+
})
|
676
|
+
end
|
677
|
+
|
678
|
+
sig { params(version: SignatureVersion).void }
|
679
|
+
def emit_rollback_complete_event(version)
|
680
|
+
DSPy::Instrumentation.emit('dspy.registry.rollback_complete', {
|
681
|
+
signature_name: version.signature_name,
|
682
|
+
version: version.version,
|
683
|
+
timestamp: Time.now.iso8601
|
684
|
+
})
|
685
|
+
end
|
686
|
+
|
687
|
+
sig { params(signature_name: String, error_message: String).void }
|
688
|
+
def emit_rollback_error_event(signature_name, error_message)
|
689
|
+
DSPy::Instrumentation.emit('dspy.registry.rollback_error', {
|
690
|
+
signature_name: signature_name,
|
691
|
+
error: error_message,
|
692
|
+
timestamp: Time.now.iso8601
|
693
|
+
})
|
694
|
+
end
|
695
|
+
|
696
|
+
sig { params(version: SignatureVersion).void }
|
697
|
+
def emit_performance_update_event(version)
|
698
|
+
DSPy::Instrumentation.emit('dspy.registry.performance_update', {
|
699
|
+
signature_name: version.signature_name,
|
700
|
+
version: version.version,
|
701
|
+
performance_score: version.performance_score,
|
702
|
+
timestamp: Time.now.iso8601
|
703
|
+
})
|
704
|
+
end
|
705
|
+
|
706
|
+
sig { params(export_path: String, signature_count: Integer).void }
|
707
|
+
def emit_export_event(export_path, signature_count)
|
708
|
+
DSPy::Instrumentation.emit('dspy.registry.export', {
|
709
|
+
export_path: export_path,
|
710
|
+
signature_count: signature_count,
|
711
|
+
timestamp: Time.now.iso8601
|
712
|
+
})
|
713
|
+
end
|
714
|
+
|
715
|
+
sig { params(import_path: String, signature_count: Integer).void }
|
716
|
+
def emit_import_event(import_path, signature_count)
|
717
|
+
DSPy::Instrumentation.emit('dspy.registry.import', {
|
718
|
+
import_path: import_path,
|
719
|
+
signature_count: signature_count,
|
720
|
+
timestamp: Time.now.iso8601
|
721
|
+
})
|
722
|
+
end
|
723
|
+
end
|
724
|
+
end
|
725
|
+
end
|