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
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_adapter"
4
+ require "monitor"
5
+
6
+ module DecisionAgent
7
+ module Monitoring
8
+ module Storage
9
+ # In-memory adapter for metrics storage (default, no dependencies)
10
+ class MemoryAdapter < BaseAdapter
11
+ include MonitorMixin
12
+
13
+ def initialize(window_size: 3600)
14
+ super()
15
+ @window_size = window_size
16
+ @metrics = {
17
+ decisions: [],
18
+ evaluations: [],
19
+ performance: [],
20
+ errors: []
21
+ }
22
+ end
23
+
24
+ def record_decision(decision, context, confidence: nil, evaluations_count: 0, duration_ms: nil, status: nil)
25
+ synchronize do
26
+ @metrics[:decisions] << {
27
+ decision: decision,
28
+ context: context,
29
+ confidence: confidence,
30
+ evaluations_count: evaluations_count,
31
+ duration_ms: duration_ms,
32
+ status: status,
33
+ timestamp: Time.now
34
+ }
35
+ cleanup_old_metrics
36
+ end
37
+ end
38
+
39
+ def record_evaluation(evaluator_name, score: nil, success: nil, duration_ms: nil, details: {})
40
+ synchronize do
41
+ @metrics[:evaluations] << {
42
+ evaluator_name: evaluator_name,
43
+ score: score,
44
+ success: success,
45
+ duration_ms: duration_ms,
46
+ details: details,
47
+ timestamp: Time.now
48
+ }
49
+ cleanup_old_metrics
50
+ end
51
+ end
52
+
53
+ def record_performance(operation, duration_ms: nil, status: nil, metadata: {})
54
+ synchronize do
55
+ @metrics[:performance] << {
56
+ operation: operation,
57
+ duration_ms: duration_ms,
58
+ status: status,
59
+ metadata: metadata,
60
+ timestamp: Time.now
61
+ }
62
+ cleanup_old_metrics
63
+ end
64
+ end
65
+
66
+ def record_error(error_type, message: nil, stack_trace: nil, severity: nil, context: {})
67
+ synchronize do
68
+ @metrics[:errors] << {
69
+ error_type: error_type,
70
+ message: message,
71
+ stack_trace: stack_trace,
72
+ severity: severity,
73
+ context: context,
74
+ timestamp: Time.now
75
+ }
76
+ cleanup_old_metrics
77
+ end
78
+ end
79
+
80
+ def statistics(time_range: 3600)
81
+ synchronize do
82
+ cutoff = Time.now - time_range
83
+ recent_decisions = @metrics[:decisions].select { |m| m[:timestamp] >= cutoff }
84
+ recent_evaluations = @metrics[:evaluations].select { |m| m[:timestamp] >= cutoff }
85
+ recent_performance = @metrics[:performance].select { |m| m[:timestamp] >= cutoff }
86
+ recent_errors = @metrics[:errors].select { |m| m[:timestamp] >= cutoff }
87
+
88
+ {
89
+ decisions: decision_statistics(recent_decisions),
90
+ evaluations: evaluation_statistics(recent_evaluations),
91
+ performance: performance_statistics(recent_performance),
92
+ errors: error_statistics(recent_errors)
93
+ }
94
+ end
95
+ end
96
+
97
+ def time_series(metric_type, bucket_size: 60, time_range: 3600)
98
+ synchronize do
99
+ cutoff = Time.now - time_range
100
+ metrics = @metrics[metric_type].select { |m| m[:timestamp] >= cutoff }
101
+
102
+ buckets = Hash.new(0)
103
+ metrics.each do |metric|
104
+ bucket = (metric[:timestamp].to_i / bucket_size) * bucket_size
105
+ buckets[bucket] += 1
106
+ end
107
+
108
+ timestamps = buckets.keys.sort
109
+ {
110
+ timestamps: timestamps.map { |ts| Time.at(ts).iso8601 },
111
+ data: timestamps.map { |ts| buckets[ts] }
112
+ }
113
+ end
114
+ end
115
+
116
+ def metrics_count
117
+ synchronize do
118
+ {
119
+ decisions: @metrics[:decisions].size,
120
+ evaluations: @metrics[:evaluations].size,
121
+ performance: @metrics[:performance].size,
122
+ errors: @metrics[:errors].size
123
+ }
124
+ end
125
+ end
126
+
127
+ def cleanup(older_than:)
128
+ synchronize do
129
+ cutoff = Time.now - older_than
130
+ count = 0
131
+
132
+ @metrics.each_value do |metric_array|
133
+ before_size = metric_array.size
134
+ metric_array.reject! { |m| m[:timestamp] < cutoff }
135
+ count += before_size - metric_array.size
136
+ end
137
+
138
+ count
139
+ end
140
+ end
141
+
142
+ def self.available?
143
+ true # Always available, no dependencies
144
+ end
145
+
146
+ private
147
+
148
+ def cleanup_old_metrics
149
+ cutoff = Time.now - @window_size
150
+ @metrics.each_value do |metric_array|
151
+ metric_array.reject! { |m| m[:timestamp] < cutoff }
152
+ end
153
+ end
154
+
155
+ def decision_statistics(decisions)
156
+ total = decisions.size
157
+ confidences = decisions.map { |d| d[:confidence] }.compact
158
+ statuses = decisions.map { |d| d[:status] }.compact
159
+
160
+ {
161
+ total: total,
162
+ by_decision: decisions.group_by { |d| d[:decision] }.transform_values(&:count),
163
+ average_confidence: confidences.empty? ? 0.0 : confidences.sum / confidences.size.to_f,
164
+ success_rate: calculate_success_rate(statuses)
165
+ }
166
+ end
167
+
168
+ def evaluation_statistics(evaluations)
169
+ total = evaluations.size
170
+ scores = evaluations.map { |e| e[:score] }.compact
171
+
172
+ {
173
+ total: total,
174
+ by_evaluator: evaluations.group_by { |e| e[:evaluator_name] }.transform_values(&:count),
175
+ average_score: scores.empty? ? 0.0 : scores.sum / scores.size.to_f,
176
+ success_rate_by_evaluator: evaluations.select { |e| e[:success] }
177
+ .group_by { |e| e[:evaluator_name] }
178
+ .transform_values(&:count)
179
+ }
180
+ end
181
+
182
+ def performance_statistics(performance_metrics)
183
+ total = performance_metrics.size
184
+ durations = performance_metrics.map { |p| p[:duration_ms] }.compact.sort
185
+ statuses = performance_metrics.map { |p| p[:status] }.compact
186
+
187
+ {
188
+ total: total,
189
+ average_duration_ms: durations.empty? ? 0.0 : durations.sum / durations.size.to_f,
190
+ p50: percentile(durations, 0.50),
191
+ p95: percentile(durations, 0.95),
192
+ p99: percentile(durations, 0.99),
193
+ success_rate: calculate_success_rate(statuses)
194
+ }
195
+ end
196
+
197
+ def error_statistics(errors)
198
+ {
199
+ total: errors.size,
200
+ by_type: errors.group_by { |e| e[:error_type] }.transform_values(&:count),
201
+ by_severity: errors.group_by { |e| e[:severity] }.transform_values(&:count),
202
+ critical_count: errors.count { |e| e[:severity] == "critical" }
203
+ }
204
+ end
205
+
206
+ def percentile(sorted_array, pct)
207
+ return 0.0 if sorted_array.empty?
208
+
209
+ index = ((sorted_array.length - 1) * pct).ceil
210
+ sorted_array[index].to_f
211
+ end
212
+
213
+ def calculate_success_rate(statuses)
214
+ return 0.0 if statuses.empty?
215
+
216
+ successful = statuses.count { |s| s == "success" }
217
+ successful.to_f / statuses.size
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -1,3 +1,3 @@
1
1
  module DecisionAgent
2
- VERSION = "0.1.3".freeze
2
+ VERSION = "0.1.4".freeze
3
3
  end
@@ -36,5 +36,12 @@ require_relative "decision_agent/monitoring/alert_manager"
36
36
  require_relative "decision_agent/monitoring/monitored_agent"
37
37
  # dashboard_server has additional dependencies (faye/websocket) - require it explicitly when needed
38
38
 
39
+ require_relative "decision_agent/ab_testing/ab_test"
40
+ require_relative "decision_agent/ab_testing/ab_test_assignment"
41
+ require_relative "decision_agent/ab_testing/ab_test_manager"
42
+ require_relative "decision_agent/ab_testing/ab_testing_agent"
43
+ require_relative "decision_agent/ab_testing/storage/adapter"
44
+ require_relative "decision_agent/ab_testing/storage/memory_adapter"
45
+
39
46
  module DecisionAgent
40
47
  end
@@ -10,6 +10,14 @@ module DecisionAgent
10
10
 
11
11
  desc "Installs DecisionAgent models and migrations for Rails"
12
12
 
13
+ class_option :monitoring, type: :boolean,
14
+ default: false,
15
+ desc: "Install monitoring tables and models for persistent metrics storage"
16
+
17
+ class_option :ab_testing, type: :boolean,
18
+ default: false,
19
+ desc: "Install A/B testing tables and models for variant testing"
20
+
13
21
  def self.next_migration_number(dirname)
14
22
  next_migration_number = current_migration_number(dirname) + 1
15
23
  ActiveRecord::Migration.next_migration_number(next_migration_number)
@@ -19,11 +27,40 @@ module DecisionAgent
19
27
  migration_template "migration.rb",
20
28
  "db/migrate/create_decision_agent_tables.rb",
21
29
  migration_version: migration_version
30
+
31
+ if options[:monitoring]
32
+ migration_template "monitoring_migration.rb",
33
+ "db/migrate/create_decision_agent_monitoring_tables.rb",
34
+ migration_version: migration_version
35
+ end
36
+
37
+ return unless options[:ab_testing]
38
+
39
+ migration_template "ab_testing_migration.rb",
40
+ "db/migrate/create_decision_agent_ab_testing_tables.rb",
41
+ migration_version: migration_version
22
42
  end
23
43
 
24
44
  def copy_models
25
45
  copy_file "rule.rb", "app/models/rule.rb"
26
46
  copy_file "rule_version.rb", "app/models/rule_version.rb"
47
+
48
+ if options[:monitoring]
49
+ copy_file "decision_log.rb", "app/models/decision_log.rb"
50
+ copy_file "evaluation_metric.rb", "app/models/evaluation_metric.rb"
51
+ copy_file "performance_metric.rb", "app/models/performance_metric.rb"
52
+ copy_file "error_metric.rb", "app/models/error_metric.rb"
53
+ end
54
+
55
+ return unless options[:ab_testing]
56
+
57
+ copy_file "ab_test_model.rb", "app/models/ab_test_model.rb"
58
+ copy_file "ab_test_assignment_model.rb", "app/models/ab_test_assignment_model.rb"
59
+ end
60
+
61
+ def copy_rake_tasks
62
+ copy_file "decision_agent_tasks.rake", "lib/tasks/decision_agent.rake" if options[:monitoring]
63
+ copy_file "ab_testing_tasks.rake", "lib/tasks/ab_testing.rake" if options[:ab_testing]
27
64
  end
28
65
 
29
66
  def show_readme
@@ -0,0 +1,45 @@
1
+ # A/B Test Assignment model for decision_agent gem
2
+ # Tracks individual variant assignments and their results
3
+ class ABTestAssignmentModel < ActiveRecord::Base
4
+ belongs_to :ab_test_model
5
+
6
+ validates :variant, presence: true, inclusion: { in: %w[champion challenger] }
7
+ validates :version_id, presence: true
8
+ validates :timestamp, presence: true
9
+
10
+ before_validation :set_defaults
11
+
12
+ # Scopes
13
+ scope :champion, -> { where(variant: "champion") }
14
+ scope :challenger, -> { where(variant: "challenger") }
15
+ scope :with_decisions, -> { where.not(decision_result: nil) }
16
+ scope :recent, -> { order(timestamp: :desc) }
17
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
18
+
19
+ serialize :context, JSON
20
+
21
+ # Check if decision has been recorded
22
+ def decision_recorded?
23
+ decision_result.present?
24
+ end
25
+
26
+ # Get the test this assignment belongs to
27
+ def test
28
+ ab_test_model
29
+ end
30
+
31
+ # Record decision result
32
+ def record_decision!(decision, confidence_score)
33
+ update!(
34
+ decision_result: decision,
35
+ confidence: confidence_score
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def set_defaults
42
+ self.timestamp ||= Time.current
43
+ self.context ||= {}
44
+ end
45
+ end
@@ -0,0 +1,54 @@
1
+ # A/B Test model for decision_agent gem
2
+ # Stores A/B test configurations
3
+ class ABTestModel < ActiveRecord::Base
4
+ has_many :ab_test_assignment_models, dependent: :destroy
5
+
6
+ validates :name, presence: true
7
+ validates :champion_version_id, presence: true
8
+ validates :challenger_version_id, presence: true
9
+ validates :status, presence: true, inclusion: { in: %w[scheduled running completed cancelled] }
10
+
11
+ serialize :traffic_split, JSON
12
+
13
+ before_validation :set_defaults
14
+
15
+ # Scopes
16
+ scope :active, -> { where(status: "running") }
17
+ scope :scheduled, -> { where(status: "scheduled") }
18
+ scope :completed, -> { where(status: "completed") }
19
+ scope :running_or_scheduled, -> { where(status: %w[running scheduled]) }
20
+
21
+ # Check if test is currently running
22
+ def running?
23
+ status == "running" &&
24
+ (start_date.nil? || start_date <= Time.current) &&
25
+ (end_date.nil? || end_date > Time.current)
26
+ end
27
+
28
+ # Get statistics for this test
29
+ def statistics
30
+ {
31
+ total_assignments: ab_test_assignment_models.count,
32
+ champion_count: ab_test_assignment_models.where(variant: "champion").count,
33
+ challenger_count: ab_test_assignment_models.where(variant: "challenger").count,
34
+ with_decisions: ab_test_assignment_models.where.not(decision_result: nil).count,
35
+ avg_confidence: ab_test_assignment_models.where.not(confidence: nil).average(:confidence)&.to_f
36
+ }
37
+ end
38
+
39
+ # Get assignments by variant
40
+ def champion_assignments
41
+ ab_test_assignment_models.where(variant: "champion")
42
+ end
43
+
44
+ def challenger_assignments
45
+ ab_test_assignment_models.where(variant: "challenger")
46
+ end
47
+
48
+ private
49
+
50
+ def set_defaults
51
+ self.traffic_split ||= { champion: 90, challenger: 10 }
52
+ self.status ||= "scheduled"
53
+ end
54
+ end
@@ -0,0 +1,43 @@
1
+ class CreateDecisionAgentABTestingTables < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # A/B Tests table
4
+ create_table :ab_test_models do |t|
5
+ t.string :name, null: false
6
+ t.string :champion_version_id, null: false
7
+ t.string :challenger_version_id, null: false
8
+ t.text :traffic_split, null: false # JSON: { champion: 90, challenger: 10 }
9
+ t.datetime :start_date
10
+ t.datetime :end_date
11
+ t.string :status, null: false, default: "scheduled" # scheduled, running, completed, cancelled
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :ab_test_models, :status
16
+ add_index :ab_test_models, :start_date
17
+ add_index :ab_test_models, %i[status start_date], name: "index_ab_tests_on_status_and_start_date"
18
+
19
+ # A/B Test Assignments table
20
+ create_table :ab_test_assignment_models do |t|
21
+ t.references :ab_test_model, null: false, foreign_key: true, index: true
22
+ t.string :user_id # Optional: for consistent assignment to same users
23
+ t.string :variant, null: false # "champion" or "challenger"
24
+ t.string :version_id, null: false
25
+ t.datetime :timestamp, null: false, default: -> { "CURRENT_TIMESTAMP" }
26
+
27
+ # Decision results (populated after decision is made)
28
+ t.string :decision_result
29
+ t.float :confidence
30
+ t.text :context # JSON: additional context
31
+
32
+ t.timestamps
33
+ end
34
+
35
+ add_index :ab_test_assignment_models, :user_id
36
+ add_index :ab_test_assignment_models, :variant
37
+ add_index :ab_test_assignment_models, :timestamp
38
+ add_index :ab_test_assignment_models, %i[ab_test_model_id variant], name: "index_assignments_on_test_and_variant"
39
+
40
+ # Optional: Index for querying assignments with decisions
41
+ add_index :ab_test_assignment_models, :decision_result, where: "decision_result IS NOT NULL", name: "index_assignments_with_decisions"
42
+ end
43
+ end
@@ -0,0 +1,189 @@
1
+ # rubocop:disable Metrics/BlockLength
2
+ namespace :decision_agent do
3
+ namespace :ab_testing do
4
+ desc "List all A/B tests"
5
+ task list: :environment do
6
+ require "decision_agent/ab_testing/ab_test_manager"
7
+
8
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
9
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
10
+ )
11
+
12
+ tests = manager.list_tests
13
+ puts "\nšŸ“Š A/B Tests (Total: #{tests.size})\n"
14
+ puts "=" * 80
15
+
16
+ if tests.empty?
17
+ puts "No A/B tests found."
18
+ else
19
+ tests.each do |test|
20
+ puts "\nID: #{test.id}"
21
+ puts "Name: #{test.name}"
22
+ puts "Status: #{test.status}"
23
+ puts "Champion Version: #{test.champion_version_id}"
24
+ puts "Challenger Version: #{test.challenger_version_id}"
25
+ puts "Traffic Split: #{test.traffic_split[:champion]}% / #{test.traffic_split[:challenger]}%"
26
+ puts "Start Date: #{test.start_date}"
27
+ puts "End Date: #{test.end_date || 'N/A'}"
28
+ puts "-" * 80
29
+ end
30
+ end
31
+ end
32
+
33
+ desc "Show A/B test results - Usage: rake decision_agent:ab_testing:results[test_id]"
34
+ task :results, [:test_id] => :environment do |_t, args|
35
+ require "decision_agent/ab_testing/ab_test_manager"
36
+
37
+ test_id = args[:test_id]
38
+ raise "Test ID is required. Usage: rake decision_agent:ab_testing:results[123]" unless test_id
39
+
40
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
41
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
42
+ )
43
+
44
+ results = manager.get_results(test_id)
45
+
46
+ puts "\nšŸ“Š A/B Test Results"
47
+ puts "=" * 80
48
+ puts "\nTest: #{results[:test][:name]}"
49
+ puts "Status: #{results[:test][:status]}"
50
+ puts "Total Assignments: #{results[:total_assignments]}"
51
+ puts ""
52
+
53
+ puts "šŸ† Champion (Version #{results[:test][:champion_version_id]})"
54
+ puts " Assignments: #{results[:champion][:total_assignments]}"
55
+ puts " Decisions Recorded: #{results[:champion][:decisions_recorded]}"
56
+ if results[:champion][:avg_confidence]
57
+ puts " Avg Confidence: #{results[:champion][:avg_confidence]}"
58
+ puts " Min/Max Confidence: #{results[:champion][:min_confidence]} / #{results[:champion][:max_confidence]}"
59
+ end
60
+ puts ""
61
+
62
+ puts "šŸ†• Challenger (Version #{results[:test][:challenger_version_id]})"
63
+ puts " Assignments: #{results[:challenger][:total_assignments]}"
64
+ puts " Decisions Recorded: #{results[:challenger][:decisions_recorded]}"
65
+ if results[:challenger][:avg_confidence]
66
+ puts " Avg Confidence: #{results[:challenger][:avg_confidence]}"
67
+ puts " Min/Max Confidence: #{results[:challenger][:min_confidence]} / #{results[:challenger][:max_confidence]}"
68
+ end
69
+ puts ""
70
+
71
+ if results[:comparison][:statistical_significance] == "insufficient_data"
72
+ puts "āš ļø Insufficient data for statistical comparison"
73
+ else
74
+ puts "šŸ“ˆ Statistical Comparison"
75
+ puts " Improvement: #{results[:comparison][:improvement_percentage]}%"
76
+ puts " Winner: #{results[:comparison][:winner]}"
77
+ puts " Statistical Significance: #{results[:comparison][:statistical_significance]}"
78
+ puts " Confidence Level: #{(results[:comparison][:confidence_level] * 100).round(0)}%"
79
+ puts ""
80
+ puts "šŸ’” Recommendation: #{results[:comparison][:recommendation]}"
81
+ end
82
+
83
+ puts "=" * 80
84
+ end
85
+
86
+ desc "Start an A/B test - Usage: rake decision_agent:ab_testing:start[test_id]"
87
+ task :start, [:test_id] => :environment do |_t, args|
88
+ require "decision_agent/ab_testing/ab_test_manager"
89
+
90
+ test_id = args[:test_id]
91
+ raise "Test ID is required. Usage: rake decision_agent:ab_testing:start[123]" unless test_id
92
+
93
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
94
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
95
+ )
96
+
97
+ manager.start_test(test_id)
98
+ puts "āœ… A/B test #{test_id} started successfully!"
99
+ end
100
+
101
+ desc "Complete an A/B test - Usage: rake decision_agent:ab_testing:complete[test_id]"
102
+ task :complete, [:test_id] => :environment do |_t, args|
103
+ require "decision_agent/ab_testing/ab_test_manager"
104
+
105
+ test_id = args[:test_id]
106
+ raise "Test ID is required. Usage: rake decision_agent:ab_testing:complete[123]" unless test_id
107
+
108
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
109
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
110
+ )
111
+
112
+ manager.complete_test(test_id)
113
+ puts "āœ… A/B test #{test_id} completed successfully!"
114
+ end
115
+
116
+ desc "Cancel an A/B test - Usage: rake decision_agent:ab_testing:cancel[test_id]"
117
+ task :cancel, [:test_id] => :environment do |_t, args|
118
+ require "decision_agent/ab_testing/ab_test_manager"
119
+
120
+ test_id = args[:test_id]
121
+ raise "Test ID is required. Usage: rake decision_agent:ab_testing:cancel[123]" unless test_id
122
+
123
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
124
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
125
+ )
126
+
127
+ manager.cancel_test(test_id)
128
+ puts "āœ… A/B test #{test_id} cancelled successfully!"
129
+ end
130
+
131
+ desc "Create a new A/B test - Usage: rake decision_agent:ab_testing:create[name,champion_id,challenger_id,split]"
132
+ task :create, %i[name champion_id challenger_id split] => :environment do |_t, args|
133
+ require "decision_agent/ab_testing/ab_test_manager"
134
+
135
+ name = args[:name]
136
+ champion_id = args[:champion_id]
137
+ challenger_id = args[:challenger_id]
138
+ split = args[:split] || "90,10"
139
+
140
+ unless name && champion_id && challenger_id
141
+ raise "Missing arguments. Usage: rake decision_agent:ab_testing:create[name,champion_id,challenger_id,split]"
142
+ end
143
+
144
+ champion_pct, challenger_pct = split.split(",").map(&:to_i)
145
+
146
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
147
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
148
+ )
149
+
150
+ test = manager.create_test(
151
+ name: name,
152
+ champion_version_id: champion_id,
153
+ challenger_version_id: challenger_id,
154
+ traffic_split: { champion: champion_pct, challenger: challenger_pct }
155
+ )
156
+
157
+ puts "āœ… A/B test created successfully!"
158
+ puts " ID: #{test.id}"
159
+ puts " Name: #{test.name}"
160
+ puts " Status: #{test.status}"
161
+ end
162
+
163
+ desc "Show active A/B tests"
164
+ task active: :environment do
165
+ require "decision_agent/ab_testing/ab_test_manager"
166
+
167
+ manager = DecisionAgent::ABTesting::ABTestManager.new(
168
+ storage_adapter: DecisionAgent::ABTesting::Storage::ActiveRecordAdapter.new
169
+ )
170
+
171
+ tests = manager.active_tests
172
+ puts "\nšŸ”„ Active A/B Tests (Total: #{tests.size})\n"
173
+ puts "=" * 80
174
+
175
+ if tests.empty?
176
+ puts "No active A/B tests found."
177
+ else
178
+ tests.each do |test|
179
+ puts "\nID: #{test.id}"
180
+ puts "Name: #{test.name}"
181
+ puts "Champion vs Challenger: #{test.champion_version_id} vs #{test.challenger_version_id}"
182
+ puts "Traffic Split: #{test.traffic_split[:champion]}% / #{test.traffic_split[:challenger]}%"
183
+ puts "-" * 80
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ # rubocop:enable Metrics/BlockLength