decision_agent 0.1.3 → 0.1.4

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  3. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  4. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  5. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +152 -0
  6. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  7. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  8. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  9. data/lib/decision_agent/monitoring/metrics_collector.rb +148 -3
  10. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  11. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  12. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  13. data/lib/decision_agent/version.rb +1 -1
  14. data/lib/decision_agent.rb +7 -0
  15. data/lib/generators/decision_agent/install/install_generator.rb +37 -0
  16. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  17. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  18. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  19. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  20. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  21. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  22. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  23. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  24. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  25. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  26. data/spec/ab_testing/ab_test_manager_spec.rb +330 -0
  27. data/spec/ab_testing/ab_test_spec.rb +270 -0
  28. data/spec/examples.txt +612 -548
  29. data/spec/issue_verification_spec.rb +95 -21
  30. data/spec/monitoring/metrics_collector_spec.rb +2 -2
  31. data/spec/monitoring/monitored_agent_spec.rb +1 -1
  32. data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
  33. data/spec/monitoring/storage/activerecord_adapter_spec.rb +346 -0
  34. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  35. metadata +26 -2
@@ -1,5 +1,12 @@
1
1
  require "monitor"
2
2
  require "time"
3
+ require_relative "storage/memory_adapter"
4
+
5
+ begin
6
+ require_relative "storage/activerecord_adapter"
7
+ rescue LoadError, NameError
8
+ # ActiveRecord adapter not available
9
+ end
3
10
 
4
11
  module DecisionAgent
5
12
  module Monitoring
@@ -7,11 +14,14 @@ module DecisionAgent
7
14
  class MetricsCollector
8
15
  include MonitorMixin
9
16
 
10
- attr_reader :metrics, :window_size
17
+ attr_reader :metrics, :window_size, :storage_adapter
11
18
 
12
- def initialize(window_size: 3600)
19
+ def initialize(window_size: 3600, storage: :auto)
13
20
  super()
14
21
  @window_size = window_size # Default: 1 hour window
22
+ @storage_adapter = initialize_storage_adapter(storage, window_size)
23
+
24
+ # Legacy in-memory metrics for backward compatibility with observers
15
25
  @metrics = {
16
26
  decisions: [],
17
27
  evaluations: [],
@@ -35,8 +45,20 @@ module DecisionAgent
35
45
  evaluator_names: decision.evaluations.map(&:evaluator_name).uniq
36
46
  }
37
47
 
48
+ # Store in-memory for observers (backward compatibility)
38
49
  @metrics[:decisions] << metric
39
50
  cleanup_old_metrics!
51
+
52
+ # Persist to storage adapter
53
+ @storage_adapter.record_decision(
54
+ decision.decision,
55
+ context.to_h,
56
+ confidence: decision.confidence,
57
+ evaluations_count: decision.evaluations.size,
58
+ duration_ms: duration_ms,
59
+ status: determine_decision_status(decision)
60
+ )
61
+
40
62
  notify_observers(:decision, metric)
41
63
  metric
42
64
  end
@@ -52,8 +74,18 @@ module DecisionAgent
52
74
  evaluator_name: evaluation.evaluator_name
53
75
  }
54
76
 
77
+ # Store in-memory for observers (backward compatibility)
55
78
  @metrics[:evaluations] << metric
56
79
  cleanup_old_metrics!
80
+
81
+ # Persist to storage adapter
82
+ @storage_adapter.record_evaluation(
83
+ evaluation.evaluator_name,
84
+ score: evaluation.weight,
85
+ success: evaluation.weight.positive?,
86
+ details: { decision: evaluation.decision }
87
+ )
88
+
57
89
  notify_observers(:evaluation, metric)
58
90
  metric
59
91
  end
@@ -70,8 +102,18 @@ module DecisionAgent
70
102
  metadata: metadata
71
103
  }
72
104
 
105
+ # Store in-memory for observers (backward compatibility)
73
106
  @metrics[:performance] << metric
74
107
  cleanup_old_metrics!
108
+
109
+ # Persist to storage adapter
110
+ @storage_adapter.record_performance(
111
+ operation,
112
+ duration_ms: duration_ms,
113
+ status: success ? "success" : "failure",
114
+ metadata: metadata
115
+ )
116
+
75
117
  notify_observers(:performance, metric)
76
118
  metric
77
119
  end
@@ -87,8 +129,19 @@ module DecisionAgent
87
129
  context: context
88
130
  }
89
131
 
132
+ # Store in-memory for observers (backward compatibility)
90
133
  @metrics[:errors] << metric
91
134
  cleanup_old_metrics!
135
+
136
+ # Persist to storage adapter
137
+ @storage_adapter.record_error(
138
+ error.class.name,
139
+ message: error.message,
140
+ stack_trace: error.backtrace,
141
+ severity: determine_error_severity(error),
142
+ context: context
143
+ )
144
+
92
145
  notify_observers(:error, metric)
93
146
  metric
94
147
  end
@@ -97,6 +150,18 @@ module DecisionAgent
97
150
  # Get aggregated statistics
98
151
  def statistics(time_range: nil)
99
152
  synchronize do
153
+ # Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
154
+ # Only delegate to ActiveRecordAdapter for persistent storage
155
+ use_storage = time_range &&
156
+ @storage_adapter.respond_to?(:statistics) &&
157
+ !@storage_adapter.is_a?(Storage::MemoryAdapter)
158
+
159
+ if use_storage
160
+ stats = @storage_adapter.statistics(time_range: time_range)
161
+ return stats.merge(timestamp: Time.now.utc, storage: @storage_adapter.class.name) if stats
162
+ end
163
+
164
+ # Use in-memory metrics
100
165
  range_start = time_range ? Time.now.utc - time_range : nil
101
166
 
102
167
  decisions = filter_by_time(@metrics[:decisions], range_start)
@@ -115,7 +180,8 @@ module DecisionAgent
115
180
  evaluations: compute_evaluation_stats(evaluations),
116
181
  performance: compute_performance_stats(performance),
117
182
  errors: compute_error_stats(errors),
118
- timestamp: Time.now.utc
183
+ timestamp: Time.now.utc,
184
+ storage: "memory (fallback)"
119
185
  }
120
186
  end
121
187
  end
@@ -123,6 +189,17 @@ module DecisionAgent
123
189
  # Get time-series data for graphing
124
190
  def time_series(metric_type:, bucket_size: 60, time_range: 3600)
125
191
  synchronize do
192
+ # Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
193
+ # Only delegate to ActiveRecordAdapter for persistent storage
194
+ use_storage = @storage_adapter.respond_to?(:time_series) &&
195
+ !@storage_adapter.is_a?(Storage::MemoryAdapter)
196
+
197
+ if use_storage
198
+ series = @storage_adapter.time_series(metric_type, bucket_size: bucket_size, time_range: time_range)
199
+ return series if series && series[:timestamps]
200
+ end
201
+
202
+ # Use in-memory metrics
126
203
  data = @metrics[metric_type] || []
127
204
  range_start = Time.now.utc - time_range
128
205
 
@@ -156,22 +233,90 @@ module DecisionAgent
156
233
  def clear!
157
234
  synchronize do
158
235
  @metrics.each_value(&:clear)
236
+ # Also clear storage adapter if using MemoryAdapter
237
+ if @storage_adapter.is_a?(Storage::MemoryAdapter)
238
+ # Clear all by using a very large time period (100 years in seconds)
239
+ @storage_adapter.cleanup(older_than: 100 * 365 * 24 * 60 * 60)
240
+ end
159
241
  end
160
242
  end
161
243
 
162
244
  # Get current metrics count
163
245
  def metrics_count
164
246
  synchronize do
247
+ # Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
248
+ # Only delegate to ActiveRecordAdapter for persistent storage
249
+ use_storage = @storage_adapter.respond_to?(:metrics_count) &&
250
+ !@storage_adapter.is_a?(Storage::MemoryAdapter)
251
+
252
+ return @storage_adapter.metrics_count if use_storage
253
+
254
+ # Use in-memory
165
255
  @metrics.transform_values(&:size)
166
256
  end
167
257
  end
168
258
 
259
+ # Cleanup old metrics from persistent storage
260
+ def cleanup_old_metrics_from_storage(older_than:)
261
+ synchronize do
262
+ return 0 unless @storage_adapter.respond_to?(:cleanup)
263
+
264
+ @storage_adapter.cleanup(older_than: older_than)
265
+ end
266
+ end
267
+
169
268
  private
170
269
 
171
270
  def freeze_config
172
271
  @window_size.freeze
173
272
  end
174
273
 
274
+ def initialize_storage_adapter(storage_option, window_size)
275
+ case storage_option
276
+ when :auto
277
+ # Auto-detect: prefer ActiveRecord if available
278
+ if defined?(DecisionAgent::Monitoring::Storage::ActiveRecordAdapter) &&
279
+ DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.available?
280
+ DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.new
281
+ else
282
+ DecisionAgent::Monitoring::Storage::MemoryAdapter.new(window_size: window_size)
283
+ end
284
+ when :activerecord, :database
285
+ unless defined?(DecisionAgent::Monitoring::Storage::ActiveRecordAdapter)
286
+ raise "ActiveRecord adapter not available. Install models or use :memory storage."
287
+ end
288
+
289
+ DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.new
290
+ when :memory
291
+ DecisionAgent::Monitoring::Storage::MemoryAdapter.new(window_size: window_size)
292
+ when Symbol
293
+ raise ArgumentError, "Unknown storage option: #{storage_option}. Use :auto, :activerecord, or :memory"
294
+ else
295
+ # Custom adapter instance provided
296
+ storage_option
297
+ end
298
+ end
299
+
300
+ def determine_decision_status(decision)
301
+ return "success" if decision.confidence >= 0.7
302
+ return "failure" if decision.confidence < 0.3
303
+
304
+ "success" # Default for medium confidence
305
+ end
306
+
307
+ def determine_error_severity(error)
308
+ case error
309
+ when ArgumentError, TypeError
310
+ "medium"
311
+ when StandardError
312
+ "low"
313
+ when Exception
314
+ "critical"
315
+ else
316
+ "low"
317
+ end
318
+ end
319
+
175
320
  def cleanup_old_metrics!
176
321
  cutoff_time = Time.now.utc - @window_size
177
322
 
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_adapter"
4
+
5
+ module DecisionAgent
6
+ module Monitoring
7
+ module Storage
8
+ # ActiveRecord adapter for persistent database storage
9
+ class ActiveRecordAdapter < BaseAdapter
10
+ def initialize
11
+ super
12
+ validate_models!
13
+ end
14
+
15
+ def record_decision(decision, context, confidence: nil, evaluations_count: 0, duration_ms: nil, status: nil)
16
+ ::DecisionLog.create!(
17
+ decision: decision,
18
+ context: context.to_json,
19
+ confidence: confidence,
20
+ evaluations_count: evaluations_count,
21
+ duration_ms: duration_ms,
22
+ status: status
23
+ )
24
+ rescue StandardError => e
25
+ warn "Failed to record decision to database: #{e.message}"
26
+ end
27
+
28
+ def record_evaluation(evaluator_name, score: nil, success: nil, duration_ms: nil, details: {})
29
+ ::EvaluationMetric.create!(
30
+ evaluator_name: evaluator_name,
31
+ score: score,
32
+ success: success,
33
+ duration_ms: duration_ms,
34
+ details: details.to_json
35
+ )
36
+ rescue StandardError => e
37
+ warn "Failed to record evaluation to database: #{e.message}"
38
+ end
39
+
40
+ def record_performance(operation, duration_ms: nil, status: nil, metadata: {})
41
+ ::PerformanceMetric.create!(
42
+ operation: operation,
43
+ duration_ms: duration_ms,
44
+ status: status,
45
+ metadata: metadata.to_json
46
+ )
47
+ rescue StandardError => e
48
+ warn "Failed to record performance to database: #{e.message}"
49
+ end
50
+
51
+ def record_error(error_type, message: nil, stack_trace: nil, severity: nil, context: {})
52
+ ::ErrorMetric.create!(
53
+ error_type: error_type,
54
+ message: message,
55
+ stack_trace: stack_trace&.to_json,
56
+ severity: severity,
57
+ context: context.to_json
58
+ )
59
+ rescue StandardError => e
60
+ warn "Failed to record error to database: #{e.message}"
61
+ end
62
+
63
+ def statistics(time_range: 3600)
64
+ decisions = ::DecisionLog.recent(time_range)
65
+ evaluations = ::EvaluationMetric.recent(time_range)
66
+ performance = ::PerformanceMetric.recent(time_range)
67
+ errors = ::ErrorMetric.recent(time_range)
68
+
69
+ {
70
+ decisions: decision_statistics(decisions, time_range),
71
+ evaluations: evaluation_statistics(evaluations),
72
+ performance: performance_statistics(performance, time_range),
73
+ errors: error_statistics(errors)
74
+ }
75
+ rescue StandardError => e
76
+ warn "Failed to retrieve statistics from database: #{e.message}"
77
+ default_statistics
78
+ end
79
+
80
+ def time_series(metric_type, bucket_size: 60, time_range: 3600)
81
+ case metric_type
82
+ when :decisions
83
+ decisions_time_series(bucket_size, time_range)
84
+ when :evaluations
85
+ evaluations_time_series(bucket_size, time_range)
86
+ when :performance
87
+ performance_time_series(bucket_size, time_range)
88
+ when :errors
89
+ errors_time_series(bucket_size, time_range)
90
+ else
91
+ { data: [], timestamps: [] }
92
+ end
93
+ rescue StandardError => e
94
+ warn "Failed to retrieve time series from database: #{e.message}"
95
+ { data: [], timestamps: [] }
96
+ end
97
+
98
+ def metrics_count
99
+ {
100
+ decisions: ::DecisionLog.count,
101
+ evaluations: ::EvaluationMetric.count,
102
+ performance: ::PerformanceMetric.count,
103
+ errors: ::ErrorMetric.count
104
+ }
105
+ rescue StandardError => e
106
+ warn "Failed to get metrics count from database: #{e.message}"
107
+ { decisions: 0, evaluations: 0, performance: 0, errors: 0 }
108
+ end
109
+
110
+ def cleanup(older_than:)
111
+ cutoff_time = Time.now - older_than
112
+ count = 0
113
+
114
+ count += ::DecisionLog.where("created_at < ?", cutoff_time).delete_all
115
+ count += ::EvaluationMetric.where("created_at < ?", cutoff_time).delete_all
116
+ count += ::PerformanceMetric.where("created_at < ?", cutoff_time).delete_all
117
+ count += ::ErrorMetric.where("created_at < ?", cutoff_time).delete_all
118
+
119
+ count
120
+ rescue StandardError => e
121
+ warn "Failed to cleanup old metrics from database: #{e.message}"
122
+ 0
123
+ end
124
+
125
+ def self.available?
126
+ defined?(ActiveRecord) &&
127
+ defined?(::DecisionLog) &&
128
+ defined?(::EvaluationMetric) &&
129
+ defined?(::PerformanceMetric) &&
130
+ defined?(::ErrorMetric)
131
+ end
132
+
133
+ private
134
+
135
+ def decision_statistics(decisions, time_range)
136
+ {
137
+ total: decisions.count,
138
+ by_decision: decisions.group(:decision).count,
139
+ average_confidence: decisions.where.not(confidence: nil).average(:confidence).to_f,
140
+ success_rate: ::DecisionLog.success_rate(time_range: time_range)
141
+ }
142
+ end
143
+
144
+ def evaluation_statistics(evaluations)
145
+ {
146
+ total: evaluations.count,
147
+ by_evaluator: evaluations.group(:evaluator_name).count,
148
+ average_score: evaluations.where.not(score: nil).average(:score).to_f,
149
+ success_rate_by_evaluator: evaluations.successful.group(:evaluator_name).count
150
+ }
151
+ end
152
+
153
+ def performance_statistics(performance, time_range)
154
+ {
155
+ total: performance.count,
156
+ average_duration_ms: performance.average_duration(time_range: time_range),
157
+ p50: performance.p50(time_range: time_range),
158
+ p95: performance.p95(time_range: time_range),
159
+ p99: performance.p99(time_range: time_range),
160
+ success_rate: performance.success_rate(time_range: time_range)
161
+ }
162
+ end
163
+
164
+ def error_statistics(errors)
165
+ {
166
+ total: errors.count,
167
+ by_type: errors.group(:error_type).count,
168
+ by_severity: errors.group(:severity).count,
169
+ critical_count: errors.critical.count
170
+ }
171
+ end
172
+
173
+ def validate_models!
174
+ required_models = %w[DecisionLog EvaluationMetric PerformanceMetric ErrorMetric]
175
+ missing_models = required_models.reject { |model| Object.const_defined?(model) }
176
+
177
+ return if missing_models.empty?
178
+
179
+ raise "Missing required models: #{missing_models.join(', ')}. " \
180
+ "Run 'rails generate decision_agent:install --monitoring' to create them."
181
+ end
182
+
183
+ def decisions_time_series(bucket_size, time_range)
184
+ counts = ::DecisionLog.recent(time_range)
185
+ .group(time_bucket_sql(:created_at, bucket_size))
186
+ .count
187
+
188
+ format_time_series(counts)
189
+ end
190
+
191
+ def evaluations_time_series(bucket_size, time_range)
192
+ counts = ::EvaluationMetric.recent(time_range)
193
+ .group(time_bucket_sql(:created_at, bucket_size))
194
+ .count
195
+
196
+ format_time_series(counts)
197
+ end
198
+
199
+ def performance_time_series(bucket_size, time_range)
200
+ durations = ::PerformanceMetric.recent(time_range)
201
+ .where.not(duration_ms: nil)
202
+ .group(time_bucket_sql(:created_at, bucket_size))
203
+ .average(:duration_ms)
204
+
205
+ format_time_series(durations)
206
+ end
207
+
208
+ def errors_time_series(bucket_size, time_range)
209
+ counts = ::ErrorMetric.recent(time_range)
210
+ .group(time_bucket_sql(:created_at, bucket_size))
211
+ .count
212
+
213
+ format_time_series(counts)
214
+ end
215
+
216
+ def time_bucket_sql(column, bucket_size)
217
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
218
+
219
+ case adapter
220
+ when /postgres/
221
+ "(EXTRACT(EPOCH FROM #{column})::bigint / #{bucket_size}) * #{bucket_size}"
222
+ when /mysql/
223
+ "(UNIX_TIMESTAMP(#{column}) DIV #{bucket_size}) * #{bucket_size}"
224
+ when /sqlite/
225
+ "(CAST(strftime('%s', #{column}) AS INTEGER) / #{bucket_size}) * #{bucket_size}"
226
+ else
227
+ # Fallback: use group by timestamp truncated to bucket
228
+ column.to_s
229
+ end
230
+ end
231
+
232
+ def format_time_series(data)
233
+ timestamps = data.keys.sort
234
+ values = timestamps.map { |ts| data[ts] }
235
+
236
+ {
237
+ timestamps: timestamps.map { |ts| Time.at(ts).iso8601 },
238
+ data: values.map(&:to_f)
239
+ }
240
+ end
241
+
242
+ def default_statistics
243
+ {
244
+ decisions: { total: 0, by_decision: {}, average_confidence: 0.0, success_rate: 0.0 },
245
+ evaluations: { total: 0, by_evaluator: {}, average_score: 0.0, success_rate_by_evaluator: {} },
246
+ performance: { total: 0, average_duration_ms: 0.0, p50: 0.0, p95: 0.0, p99: 0.0, success_rate: 0.0 },
247
+ errors: { total: 0, by_type: {}, by_severity: {}, critical_count: 0 }
248
+ }
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Monitoring
5
+ module Storage
6
+ # Base adapter interface for metrics storage
7
+ # Subclasses must implement all abstract methods
8
+ class BaseAdapter
9
+ # Record a decision
10
+ # @param decision [String] The decision made
11
+ # @param context [Hash] Decision context
12
+ # @param confidence [Float, nil] Confidence score (0-1)
13
+ # @param evaluations_count [Integer] Number of evaluations
14
+ # @param duration_ms [Float, nil] Decision duration in milliseconds
15
+ # @param status [String, nil] Decision status (success, failure, error)
16
+ # @return [void]
17
+ def record_decision(decision, context, confidence: nil, evaluations_count: 0, duration_ms: nil, status: nil)
18
+ raise NotImplementedError, "#{self.class} must implement #record_decision"
19
+ end
20
+
21
+ # Record an evaluation
22
+ # @param evaluator_name [String] Name of the evaluator
23
+ # @param score [Float, nil] Evaluation score
24
+ # @param success [Boolean, nil] Whether evaluation succeeded
25
+ # @param duration_ms [Float, nil] Evaluation duration
26
+ # @param details [Hash] Additional details
27
+ # @return [void]
28
+ def record_evaluation(evaluator_name, score: nil, success: nil, duration_ms: nil, details: {})
29
+ raise NotImplementedError, "#{self.class} must implement #record_evaluation"
30
+ end
31
+
32
+ # Record a performance metric
33
+ # @param operation [String] Operation name
34
+ # @param duration_ms [Float, nil] Duration in milliseconds
35
+ # @param status [String, nil] Status (success, failure, error)
36
+ # @param metadata [Hash] Additional metadata
37
+ # @return [void]
38
+ def record_performance(operation, duration_ms: nil, status: nil, metadata: {})
39
+ raise NotImplementedError, "#{self.class} must implement #record_performance"
40
+ end
41
+
42
+ # Record an error
43
+ # @param error_type [String] Type of error
44
+ # @param message [String, nil] Error message
45
+ # @param stack_trace [Array, nil] Stack trace
46
+ # @param severity [String, nil] Error severity (low, medium, high, critical)
47
+ # @param context [Hash] Error context
48
+ # @return [void]
49
+ def record_error(error_type, message: nil, stack_trace: nil, severity: nil, context: {})
50
+ raise NotImplementedError, "#{self.class} must implement #record_error"
51
+ end
52
+
53
+ # Get statistics for a time range
54
+ # @param time_range [Integer] Time range in seconds
55
+ # @return [Hash] Statistics summary
56
+ def statistics(time_range: 3600)
57
+ raise NotImplementedError, "#{self.class} must implement #statistics"
58
+ end
59
+
60
+ # Get time series data
61
+ # @param metric_type [Symbol] Type of metric (:decisions, :evaluations, :performance, :errors)
62
+ # @param bucket_size [Integer] Bucket size in seconds
63
+ # @param time_range [Integer] Time range in seconds
64
+ # @return [Hash] Time series data
65
+ def time_series(metric_type, bucket_size: 60, time_range: 3600)
66
+ raise NotImplementedError, "#{self.class} must implement #time_series"
67
+ end
68
+
69
+ # Get count of metrics stored
70
+ # @return [Hash] Count by metric type
71
+ def metrics_count
72
+ raise NotImplementedError, "#{self.class} must implement #metrics_count"
73
+ end
74
+
75
+ # Clean up old metrics
76
+ # @param older_than [Integer] Remove metrics older than this many seconds
77
+ # @return [Integer] Number of metrics removed
78
+ def cleanup(older_than:)
79
+ raise NotImplementedError, "#{self.class} must implement #cleanup"
80
+ end
81
+
82
+ # Check if adapter is available (dependencies installed)
83
+ # @return [Boolean]
84
+ def self.available?
85
+ raise NotImplementedError, "#{self} must implement .available?"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end