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.
- checksums.yaml +4 -4
- 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 +152 -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/monitoring/metrics_collector.rb +148 -3
- 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/version.rb +1 -1
- data/lib/decision_agent.rb +7 -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/spec/ab_testing/ab_test_manager_spec.rb +330 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/examples.txt +612 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +2 -2
- 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 +346 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- metadata +26 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "decision_agent/monitoring/storage/memory_adapter"
|
|
5
|
+
|
|
6
|
+
RSpec.describe DecisionAgent::Monitoring::Storage::MemoryAdapter do
|
|
7
|
+
let(:adapter) { described_class.new(window_size: 3600) }
|
|
8
|
+
|
|
9
|
+
describe ".available?" do
|
|
10
|
+
it "is always available" do
|
|
11
|
+
expect(described_class.available?).to be true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#record_decision" do
|
|
16
|
+
it "stores decision in memory" do
|
|
17
|
+
expect do
|
|
18
|
+
adapter.record_decision(
|
|
19
|
+
"approve_loan",
|
|
20
|
+
{ user_id: 123, amount: 10_000 },
|
|
21
|
+
confidence: 0.85,
|
|
22
|
+
evaluations_count: 3,
|
|
23
|
+
duration_ms: 45.5,
|
|
24
|
+
status: "success"
|
|
25
|
+
)
|
|
26
|
+
end.to change { adapter.metrics_count[:decisions] }.by(1)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "#record_evaluation" do
|
|
31
|
+
it "stores evaluation in memory" do
|
|
32
|
+
expect do
|
|
33
|
+
adapter.record_evaluation(
|
|
34
|
+
"CreditScoreEvaluator",
|
|
35
|
+
score: 0.92,
|
|
36
|
+
success: true,
|
|
37
|
+
duration_ms: 12.3,
|
|
38
|
+
details: { credit_score: 750 }
|
|
39
|
+
)
|
|
40
|
+
end.to change { adapter.metrics_count[:evaluations] }.by(1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "#record_performance" do
|
|
45
|
+
it "stores performance metric in memory" do
|
|
46
|
+
expect do
|
|
47
|
+
adapter.record_performance(
|
|
48
|
+
"database_query",
|
|
49
|
+
duration_ms: 150.5,
|
|
50
|
+
status: "success",
|
|
51
|
+
metadata: { query: "SELECT * FROM users" }
|
|
52
|
+
)
|
|
53
|
+
end.to change { adapter.metrics_count[:performance] }.by(1)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#record_error" do
|
|
58
|
+
it "stores error in memory" do
|
|
59
|
+
expect do
|
|
60
|
+
adapter.record_error(
|
|
61
|
+
"ArgumentError",
|
|
62
|
+
message: "Invalid input",
|
|
63
|
+
stack_trace: ["line 1", "line 2"],
|
|
64
|
+
severity: "medium",
|
|
65
|
+
context: { input: "bad_value" }
|
|
66
|
+
)
|
|
67
|
+
end.to change { adapter.metrics_count[:errors] }.by(1)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "#statistics" do
|
|
72
|
+
before do
|
|
73
|
+
# Create test data
|
|
74
|
+
5.times do |i|
|
|
75
|
+
adapter.record_decision(
|
|
76
|
+
"decision_#{i}",
|
|
77
|
+
{ index: i },
|
|
78
|
+
confidence: 0.5 + (i * 0.05),
|
|
79
|
+
evaluations_count: 2,
|
|
80
|
+
duration_ms: 100,
|
|
81
|
+
status: i.even? ? "success" : "failure"
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
3.times do |i|
|
|
86
|
+
adapter.record_evaluation(
|
|
87
|
+
"Evaluator#{i}",
|
|
88
|
+
score: 0.8 + (i * 0.05),
|
|
89
|
+
success: true
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
6.times do |i|
|
|
94
|
+
adapter.record_performance(
|
|
95
|
+
"operation",
|
|
96
|
+
duration_ms: 100 + (i * 20),
|
|
97
|
+
status: "success"
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
2.times do
|
|
102
|
+
adapter.record_error("RuntimeError", severity: "critical")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "returns comprehensive statistics" do
|
|
107
|
+
stats = adapter.statistics(time_range: 3600)
|
|
108
|
+
|
|
109
|
+
expect(stats[:decisions][:total]).to eq(5)
|
|
110
|
+
expect(stats[:decisions][:average_confidence]).to be_within(0.01).of(0.6)
|
|
111
|
+
expect(stats[:decisions][:success_rate]).to eq(0.6) # 3 out of 5
|
|
112
|
+
|
|
113
|
+
expect(stats[:evaluations][:total]).to eq(3)
|
|
114
|
+
expect(stats[:evaluations][:average_score]).to be_within(0.01).of(0.85)
|
|
115
|
+
|
|
116
|
+
expect(stats[:performance][:total]).to eq(6)
|
|
117
|
+
expect(stats[:performance][:average_duration_ms]).to eq(150.0)
|
|
118
|
+
expect(stats[:performance][:success_rate]).to eq(1.0)
|
|
119
|
+
|
|
120
|
+
expect(stats[:errors][:total]).to eq(2)
|
|
121
|
+
expect(stats[:errors][:critical_count]).to eq(2)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "filters by time range" do
|
|
125
|
+
# Record an old metric that should be filtered out
|
|
126
|
+
adapter.instance_variable_get(:@metrics)[:decisions] << {
|
|
127
|
+
decision: "old_decision",
|
|
128
|
+
confidence: 0.5,
|
|
129
|
+
timestamp: Time.now - 7200 # 2 hours ago
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stats = adapter.statistics(time_range: 3600) # Last hour only
|
|
133
|
+
|
|
134
|
+
expect(stats[:decisions][:total]).to eq(5) # Doesn't include the old one
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "#time_series" do
|
|
139
|
+
before do
|
|
140
|
+
# Create metrics at different times
|
|
141
|
+
now = Time.now
|
|
142
|
+
adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 120 }
|
|
143
|
+
adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 70 }
|
|
144
|
+
adapter.instance_variable_get(:@metrics)[:decisions] << { timestamp: now - 10 }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "groups metrics into time buckets" do
|
|
148
|
+
series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
|
|
149
|
+
|
|
150
|
+
expect(series[:timestamps]).to be_an(Array)
|
|
151
|
+
expect(series[:data]).to be_an(Array)
|
|
152
|
+
expect(series[:data].sum).to eq(3) # All 3 metrics
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "uses correct bucket size" do
|
|
156
|
+
series = adapter.time_series(:decisions, bucket_size: 60, time_range: 200)
|
|
157
|
+
|
|
158
|
+
# Metrics should be grouped into 60-second buckets
|
|
159
|
+
expect(series[:data].max).to be <= 2 # No bucket should have more than 2
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "#metrics_count" do
|
|
164
|
+
before do
|
|
165
|
+
adapter.record_decision("test", {}, confidence: 0.8)
|
|
166
|
+
adapter.record_decision("test2", {}, confidence: 0.9)
|
|
167
|
+
adapter.record_evaluation("eval1", score: 0.85)
|
|
168
|
+
adapter.record_performance("perf1", duration_ms: 100)
|
|
169
|
+
adapter.record_error("Error1")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "returns count for each metric type" do
|
|
173
|
+
counts = adapter.metrics_count
|
|
174
|
+
|
|
175
|
+
expect(counts[:decisions]).to eq(2)
|
|
176
|
+
expect(counts[:evaluations]).to eq(1)
|
|
177
|
+
expect(counts[:performance]).to eq(1)
|
|
178
|
+
expect(counts[:errors]).to eq(1)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe "#cleanup" do
|
|
183
|
+
let(:long_window_adapter) { described_class.new(window_size: 30 * 24 * 3_600) } # 30 day window
|
|
184
|
+
|
|
185
|
+
before do
|
|
186
|
+
now = Time.now
|
|
187
|
+
|
|
188
|
+
# Add old metrics (8 days ago) to adapter with long window
|
|
189
|
+
long_window_adapter.instance_variable_get(:@metrics)[:decisions] << {
|
|
190
|
+
decision: "old",
|
|
191
|
+
timestamp: now - (8 * 24 * 3600)
|
|
192
|
+
}
|
|
193
|
+
long_window_adapter.instance_variable_get(:@metrics)[:evaluations] << {
|
|
194
|
+
evaluator_name: "old",
|
|
195
|
+
timestamp: now - (8 * 24 * 3600)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Add recent metrics
|
|
199
|
+
long_window_adapter.record_decision("recent", {}, confidence: 0.8)
|
|
200
|
+
long_window_adapter.record_evaluation("recent", score: 0.9)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "removes old metrics and returns count" do
|
|
204
|
+
count = long_window_adapter.cleanup(older_than: 7 * 24 * 3600) # 7 days
|
|
205
|
+
|
|
206
|
+
expect(count).to eq(2) # 2 old metrics removed
|
|
207
|
+
expect(long_window_adapter.metrics_count[:decisions]).to eq(1) # Only recent one
|
|
208
|
+
expect(long_window_adapter.metrics_count[:evaluations]).to eq(1)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
describe "window-based cleanup" do
|
|
213
|
+
let(:short_window_adapter) { described_class.new(window_size: 60) } # 1 minute window
|
|
214
|
+
|
|
215
|
+
it "automatically removes metrics older than window_size" do
|
|
216
|
+
now = Time.now
|
|
217
|
+
|
|
218
|
+
# Add old metric
|
|
219
|
+
short_window_adapter.instance_variable_get(:@metrics)[:decisions] << {
|
|
220
|
+
decision: "old",
|
|
221
|
+
timestamp: now - 120 # 2 minutes ago
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Add new metric (this should trigger cleanup)
|
|
225
|
+
short_window_adapter.record_decision("new", {}, confidence: 0.8)
|
|
226
|
+
|
|
227
|
+
# Only the new metric should remain
|
|
228
|
+
expect(short_window_adapter.metrics_count[:decisions]).to eq(1)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
describe "thread safety" do
|
|
233
|
+
it "handles concurrent writes" do
|
|
234
|
+
threads = 10.times.map do
|
|
235
|
+
Thread.new do
|
|
236
|
+
100.times do |i|
|
|
237
|
+
adapter.record_decision("concurrent_#{i}", {}, confidence: 0.8)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
threads.each(&:join)
|
|
243
|
+
|
|
244
|
+
expect(adapter.metrics_count[:decisions]).to eq(1000)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: decision_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Aswin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json-canonicalization
|
|
@@ -108,6 +108,13 @@ files:
|
|
|
108
108
|
- README.md
|
|
109
109
|
- bin/decision_agent
|
|
110
110
|
- lib/decision_agent.rb
|
|
111
|
+
- lib/decision_agent/ab_testing/ab_test.rb
|
|
112
|
+
- lib/decision_agent/ab_testing/ab_test_assignment.rb
|
|
113
|
+
- lib/decision_agent/ab_testing/ab_test_manager.rb
|
|
114
|
+
- lib/decision_agent/ab_testing/ab_testing_agent.rb
|
|
115
|
+
- lib/decision_agent/ab_testing/storage/activerecord_adapter.rb
|
|
116
|
+
- lib/decision_agent/ab_testing/storage/adapter.rb
|
|
117
|
+
- lib/decision_agent/ab_testing/storage/memory_adapter.rb
|
|
111
118
|
- lib/decision_agent/agent.rb
|
|
112
119
|
- lib/decision_agent/audit/adapter.rb
|
|
113
120
|
- lib/decision_agent/audit/logger_adapter.rb
|
|
@@ -131,6 +138,9 @@ files:
|
|
|
131
138
|
- lib/decision_agent/monitoring/metrics_collector.rb
|
|
132
139
|
- lib/decision_agent/monitoring/monitored_agent.rb
|
|
133
140
|
- lib/decision_agent/monitoring/prometheus_exporter.rb
|
|
141
|
+
- lib/decision_agent/monitoring/storage/activerecord_adapter.rb
|
|
142
|
+
- lib/decision_agent/monitoring/storage/base_adapter.rb
|
|
143
|
+
- lib/decision_agent/monitoring/storage/memory_adapter.rb
|
|
134
144
|
- lib/decision_agent/replay/replay.rb
|
|
135
145
|
- lib/decision_agent/scoring/base.rb
|
|
136
146
|
- lib/decision_agent/scoring/consensus.rb
|
|
@@ -148,9 +158,21 @@ files:
|
|
|
148
158
|
- lib/decision_agent/web/server.rb
|
|
149
159
|
- lib/generators/decision_agent/install/install_generator.rb
|
|
150
160
|
- lib/generators/decision_agent/install/templates/README
|
|
161
|
+
- lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb
|
|
162
|
+
- lib/generators/decision_agent/install/templates/ab_test_model.rb
|
|
163
|
+
- lib/generators/decision_agent/install/templates/ab_testing_migration.rb
|
|
164
|
+
- lib/generators/decision_agent/install/templates/ab_testing_tasks.rake
|
|
165
|
+
- lib/generators/decision_agent/install/templates/decision_agent_tasks.rake
|
|
166
|
+
- lib/generators/decision_agent/install/templates/decision_log.rb
|
|
167
|
+
- lib/generators/decision_agent/install/templates/error_metric.rb
|
|
168
|
+
- lib/generators/decision_agent/install/templates/evaluation_metric.rb
|
|
151
169
|
- lib/generators/decision_agent/install/templates/migration.rb
|
|
170
|
+
- lib/generators/decision_agent/install/templates/monitoring_migration.rb
|
|
171
|
+
- lib/generators/decision_agent/install/templates/performance_metric.rb
|
|
152
172
|
- lib/generators/decision_agent/install/templates/rule.rb
|
|
153
173
|
- lib/generators/decision_agent/install/templates/rule_version.rb
|
|
174
|
+
- spec/ab_testing/ab_test_manager_spec.rb
|
|
175
|
+
- spec/ab_testing/ab_test_spec.rb
|
|
154
176
|
- spec/activerecord_thread_safety_spec.rb
|
|
155
177
|
- spec/agent_spec.rb
|
|
156
178
|
- spec/api_contract_spec.rb
|
|
@@ -167,6 +189,8 @@ files:
|
|
|
167
189
|
- spec/monitoring/metrics_collector_spec.rb
|
|
168
190
|
- spec/monitoring/monitored_agent_spec.rb
|
|
169
191
|
- spec/monitoring/prometheus_exporter_spec.rb
|
|
192
|
+
- spec/monitoring/storage/activerecord_adapter_spec.rb
|
|
193
|
+
- spec/monitoring/storage/memory_adapter_spec.rb
|
|
170
194
|
- spec/replay_edge_cases_spec.rb
|
|
171
195
|
- spec/replay_spec.rb
|
|
172
196
|
- spec/rfc8785_canonicalization_spec.rb
|