decision_agent 0.1.3 → 0.1.6
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 +84 -233
- data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +59 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +221 -3
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- metadata +123 -6
|
@@ -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,16 @@ 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, cleanup_threshold: 100)
|
|
13
20
|
super()
|
|
14
21
|
@window_size = window_size # Default: 1 hour window
|
|
22
|
+
@cleanup_threshold = cleanup_threshold # Cleanup every N records
|
|
23
|
+
@cleanup_counter = 0
|
|
24
|
+
@storage_adapter = initialize_storage_adapter(storage, window_size)
|
|
25
|
+
|
|
26
|
+
# Legacy in-memory metrics for backward compatibility with observers
|
|
15
27
|
@metrics = {
|
|
16
28
|
decisions: [],
|
|
17
29
|
evaluations: [],
|
|
@@ -35,8 +47,20 @@ module DecisionAgent
|
|
|
35
47
|
evaluator_names: decision.evaluations.map(&:evaluator_name).uniq
|
|
36
48
|
}
|
|
37
49
|
|
|
50
|
+
# Store in-memory for observers (backward compatibility)
|
|
38
51
|
@metrics[:decisions] << metric
|
|
39
|
-
|
|
52
|
+
maybe_cleanup_old_metrics!
|
|
53
|
+
|
|
54
|
+
# Persist to storage adapter
|
|
55
|
+
@storage_adapter.record_decision(
|
|
56
|
+
decision.decision,
|
|
57
|
+
context.to_h,
|
|
58
|
+
confidence: decision.confidence,
|
|
59
|
+
evaluations_count: decision.evaluations.size,
|
|
60
|
+
duration_ms: duration_ms,
|
|
61
|
+
status: determine_decision_status(decision)
|
|
62
|
+
)
|
|
63
|
+
|
|
40
64
|
notify_observers(:decision, metric)
|
|
41
65
|
metric
|
|
42
66
|
end
|
|
@@ -52,8 +76,18 @@ module DecisionAgent
|
|
|
52
76
|
evaluator_name: evaluation.evaluator_name
|
|
53
77
|
}
|
|
54
78
|
|
|
79
|
+
# Store in-memory for observers (backward compatibility)
|
|
55
80
|
@metrics[:evaluations] << metric
|
|
56
|
-
|
|
81
|
+
maybe_cleanup_old_metrics!
|
|
82
|
+
|
|
83
|
+
# Persist to storage adapter
|
|
84
|
+
@storage_adapter.record_evaluation(
|
|
85
|
+
evaluation.evaluator_name,
|
|
86
|
+
score: evaluation.weight,
|
|
87
|
+
success: evaluation.weight.positive?,
|
|
88
|
+
details: { decision: evaluation.decision }
|
|
89
|
+
)
|
|
90
|
+
|
|
57
91
|
notify_observers(:evaluation, metric)
|
|
58
92
|
metric
|
|
59
93
|
end
|
|
@@ -70,8 +104,18 @@ module DecisionAgent
|
|
|
70
104
|
metadata: metadata
|
|
71
105
|
}
|
|
72
106
|
|
|
107
|
+
# Store in-memory for observers (backward compatibility)
|
|
73
108
|
@metrics[:performance] << metric
|
|
74
|
-
|
|
109
|
+
maybe_cleanup_old_metrics!
|
|
110
|
+
|
|
111
|
+
# Persist to storage adapter
|
|
112
|
+
@storage_adapter.record_performance(
|
|
113
|
+
operation,
|
|
114
|
+
duration_ms: duration_ms,
|
|
115
|
+
status: success ? "success" : "failure",
|
|
116
|
+
metadata: metadata
|
|
117
|
+
)
|
|
118
|
+
|
|
75
119
|
notify_observers(:performance, metric)
|
|
76
120
|
metric
|
|
77
121
|
end
|
|
@@ -87,8 +131,19 @@ module DecisionAgent
|
|
|
87
131
|
context: context
|
|
88
132
|
}
|
|
89
133
|
|
|
134
|
+
# Store in-memory for observers (backward compatibility)
|
|
90
135
|
@metrics[:errors] << metric
|
|
91
|
-
|
|
136
|
+
maybe_cleanup_old_metrics!
|
|
137
|
+
|
|
138
|
+
# Persist to storage adapter
|
|
139
|
+
@storage_adapter.record_error(
|
|
140
|
+
error.class.name,
|
|
141
|
+
message: error.message,
|
|
142
|
+
stack_trace: error.backtrace,
|
|
143
|
+
severity: determine_error_severity(error),
|
|
144
|
+
context: context
|
|
145
|
+
)
|
|
146
|
+
|
|
92
147
|
notify_observers(:error, metric)
|
|
93
148
|
metric
|
|
94
149
|
end
|
|
@@ -97,6 +152,18 @@ module DecisionAgent
|
|
|
97
152
|
# Get aggregated statistics
|
|
98
153
|
def statistics(time_range: nil)
|
|
99
154
|
synchronize do
|
|
155
|
+
# Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
|
|
156
|
+
# Only delegate to ActiveRecordAdapter for persistent storage
|
|
157
|
+
use_storage = time_range &&
|
|
158
|
+
@storage_adapter.respond_to?(:statistics) &&
|
|
159
|
+
!@storage_adapter.is_a?(Storage::MemoryAdapter)
|
|
160
|
+
|
|
161
|
+
if use_storage
|
|
162
|
+
stats = @storage_adapter.statistics(time_range: time_range)
|
|
163
|
+
return stats.merge(timestamp: Time.now.utc, storage: @storage_adapter.class.name) if stats
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Use in-memory metrics
|
|
100
167
|
range_start = time_range ? Time.now.utc - time_range : nil
|
|
101
168
|
|
|
102
169
|
decisions = filter_by_time(@metrics[:decisions], range_start)
|
|
@@ -115,7 +182,8 @@ module DecisionAgent
|
|
|
115
182
|
evaluations: compute_evaluation_stats(evaluations),
|
|
116
183
|
performance: compute_performance_stats(performance),
|
|
117
184
|
errors: compute_error_stats(errors),
|
|
118
|
-
timestamp: Time.now.utc
|
|
185
|
+
timestamp: Time.now.utc,
|
|
186
|
+
storage: "memory (fallback)"
|
|
119
187
|
}
|
|
120
188
|
end
|
|
121
189
|
end
|
|
@@ -123,6 +191,17 @@ module DecisionAgent
|
|
|
123
191
|
# Get time-series data for graphing
|
|
124
192
|
def time_series(metric_type:, bucket_size: 60, time_range: 3600)
|
|
125
193
|
synchronize do
|
|
194
|
+
# Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
|
|
195
|
+
# Only delegate to ActiveRecordAdapter for persistent storage
|
|
196
|
+
use_storage = @storage_adapter.respond_to?(:time_series) &&
|
|
197
|
+
!@storage_adapter.is_a?(Storage::MemoryAdapter)
|
|
198
|
+
|
|
199
|
+
if use_storage
|
|
200
|
+
series = @storage_adapter.time_series(metric_type, bucket_size: bucket_size, time_range: time_range)
|
|
201
|
+
return series if series && series[:timestamps]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Use in-memory metrics
|
|
126
205
|
data = @metrics[metric_type] || []
|
|
127
206
|
range_start = Time.now.utc - time_range
|
|
128
207
|
|
|
@@ -156,22 +235,100 @@ module DecisionAgent
|
|
|
156
235
|
def clear!
|
|
157
236
|
synchronize do
|
|
158
237
|
@metrics.each_value(&:clear)
|
|
238
|
+
# Also clear storage adapter if using MemoryAdapter
|
|
239
|
+
if @storage_adapter.is_a?(Storage::MemoryAdapter)
|
|
240
|
+
# Clear all by using a very large time period (100 years in seconds)
|
|
241
|
+
@storage_adapter.cleanup(older_than: 100 * 365 * 24 * 60 * 60)
|
|
242
|
+
end
|
|
159
243
|
end
|
|
160
244
|
end
|
|
161
245
|
|
|
162
246
|
# Get current metrics count
|
|
163
247
|
def metrics_count
|
|
164
248
|
synchronize do
|
|
249
|
+
# Use in-memory metrics for MemoryAdapter (to maintain backward compatibility)
|
|
250
|
+
# Only delegate to ActiveRecordAdapter for persistent storage
|
|
251
|
+
use_storage = @storage_adapter.respond_to?(:metrics_count) &&
|
|
252
|
+
!@storage_adapter.is_a?(Storage::MemoryAdapter)
|
|
253
|
+
|
|
254
|
+
return @storage_adapter.metrics_count if use_storage
|
|
255
|
+
|
|
256
|
+
# Use in-memory
|
|
165
257
|
@metrics.transform_values(&:size)
|
|
166
258
|
end
|
|
167
259
|
end
|
|
168
260
|
|
|
261
|
+
# Cleanup old metrics from persistent storage
|
|
262
|
+
def cleanup_old_metrics_from_storage(older_than:)
|
|
263
|
+
synchronize do
|
|
264
|
+
return 0 unless @storage_adapter.respond_to?(:cleanup)
|
|
265
|
+
|
|
266
|
+
@storage_adapter.cleanup(older_than: older_than)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
169
270
|
private
|
|
170
271
|
|
|
171
272
|
def freeze_config
|
|
172
273
|
@window_size.freeze
|
|
173
274
|
end
|
|
174
275
|
|
|
276
|
+
def initialize_storage_adapter(storage_option, window_size)
|
|
277
|
+
case storage_option
|
|
278
|
+
when :auto
|
|
279
|
+
# Auto-detect: prefer ActiveRecord if available
|
|
280
|
+
if defined?(DecisionAgent::Monitoring::Storage::ActiveRecordAdapter) &&
|
|
281
|
+
DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.available?
|
|
282
|
+
DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.new
|
|
283
|
+
else
|
|
284
|
+
DecisionAgent::Monitoring::Storage::MemoryAdapter.new(window_size: window_size)
|
|
285
|
+
end
|
|
286
|
+
when :activerecord, :database
|
|
287
|
+
unless defined?(DecisionAgent::Monitoring::Storage::ActiveRecordAdapter)
|
|
288
|
+
raise "ActiveRecord adapter not available. Install models or use :memory storage."
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
DecisionAgent::Monitoring::Storage::ActiveRecordAdapter.new
|
|
292
|
+
when :memory
|
|
293
|
+
DecisionAgent::Monitoring::Storage::MemoryAdapter.new(window_size: window_size)
|
|
294
|
+
when Symbol
|
|
295
|
+
raise ArgumentError, "Unknown storage option: #{storage_option}. Use :auto, :activerecord, or :memory"
|
|
296
|
+
else
|
|
297
|
+
# Custom adapter instance provided
|
|
298
|
+
storage_option
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def determine_decision_status(decision)
|
|
303
|
+
return "success" if decision.confidence >= 0.7
|
|
304
|
+
return "failure" if decision.confidence < 0.3
|
|
305
|
+
|
|
306
|
+
"success" # Default for medium confidence
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def determine_error_severity(error)
|
|
310
|
+
case error
|
|
311
|
+
when ArgumentError, TypeError
|
|
312
|
+
"medium"
|
|
313
|
+
when StandardError
|
|
314
|
+
"low"
|
|
315
|
+
when Exception
|
|
316
|
+
"critical"
|
|
317
|
+
else
|
|
318
|
+
"low"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Conditionally cleanup old metrics based on counter
|
|
323
|
+
# This reduces O(n) array scans from every record to every N records
|
|
324
|
+
def maybe_cleanup_old_metrics!
|
|
325
|
+
@cleanup_counter += 1
|
|
326
|
+
return unless @cleanup_counter >= @cleanup_threshold
|
|
327
|
+
|
|
328
|
+
@cleanup_counter = 0
|
|
329
|
+
cleanup_old_metrics!
|
|
330
|
+
end
|
|
331
|
+
|
|
175
332
|
def cleanup_old_metrics!
|
|
176
333
|
cutoff_time = Time.now.utc - @window_size
|
|
177
334
|
|
|
@@ -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
|