decision_agent 0.2.0 → 1.0.1

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -0,0 +1,287 @@
1
+ require_relative "errors"
2
+
3
+ module DecisionAgent
4
+ module Simulation
5
+ # Engine for shadow testing - comparing new rules against production without affecting outcomes
6
+ class ShadowTestEngine
7
+ attr_reader :production_agent, :version_manager
8
+
9
+ def initialize(production_agent:, version_manager: nil)
10
+ @production_agent = production_agent
11
+ @version_manager = version_manager || Versioning::VersionManager.new
12
+ end
13
+
14
+ # Execute shadow test - compare shadow version against production
15
+ # @param context [Hash, Context] Context for decision
16
+ # @param shadow_version [String, Integer, Hash] Shadow rule version to test
17
+ # @param options [Hash] Test options
18
+ # - :track_differences [Boolean] Track and return differences (default: true)
19
+ # - :record_results [Boolean] Record results for later analysis (default: false)
20
+ # @return [Hash] Shadow test result
21
+ def test(context:, shadow_version:, options: {})
22
+ options = {
23
+ track_differences: true,
24
+ record_results: false
25
+ }.merge(options)
26
+
27
+ ctx = normalize_context(context)
28
+ production_decision = execute_production_decision(ctx)
29
+ shadow_decision = execute_shadow_decision(ctx, shadow_version)
30
+
31
+ result = build_comparison_result(ctx, production_decision, shadow_decision)
32
+ add_differences(result, production_decision, shadow_decision) if options[:track_differences] && !result[:matches]
33
+ record_result(result, shadow_version) if options[:record_results]
34
+
35
+ result
36
+ end
37
+
38
+ def normalize_context(context)
39
+ context.is_a?(Context) ? context : Context.new(context)
40
+ end
41
+
42
+ def execute_production_decision(context)
43
+ @production_agent.decide(context: context)
44
+ rescue NoEvaluationsError
45
+ nil
46
+ end
47
+
48
+ def execute_shadow_decision(context, shadow_version)
49
+ shadow_agent = build_shadow_agent(shadow_version)
50
+ shadow_agent.decide(context: context)
51
+ rescue NoEvaluationsError
52
+ nil
53
+ end
54
+
55
+ def build_comparison_result(context, production_decision, shadow_decision)
56
+ {
57
+ context: context.to_h,
58
+ production_decision: production_decision&.decision,
59
+ production_confidence: production_decision&.confidence || 0.0,
60
+ shadow_decision: shadow_decision&.decision,
61
+ shadow_confidence: shadow_decision&.confidence || 0.0,
62
+ matches: production_decision&.decision == shadow_decision&.decision,
63
+ confidence_delta: (shadow_decision&.confidence || 0.0) - (production_decision&.confidence || 0.0),
64
+ timestamp: Time.now.utc.iso8601
65
+ }
66
+ end
67
+
68
+ def add_differences(result, production_decision, shadow_decision)
69
+ result[:differences] = {
70
+ decision_mismatch: true,
71
+ production_explanations: production_decision&.explanations || [],
72
+ shadow_explanations: shadow_decision&.explanations || []
73
+ }
74
+ end
75
+
76
+ # Batch shadow test multiple contexts
77
+ # @param contexts [Array<Hash>] Array of contexts to test
78
+ # @param shadow_version [String, Integer, Hash] Shadow rule version
79
+ # @param options [Hash] Test options
80
+ # - :parallel [Boolean] Use parallel execution (default: true)
81
+ # - :thread_count [Integer] Number of threads (default: 4)
82
+ # - :progress_callback [Proc] Progress callback
83
+ # @return [Hash] Batch shadow test results
84
+ def batch_test(contexts:, shadow_version:, options: {})
85
+ options = {
86
+ parallel: true,
87
+ thread_count: 4,
88
+ progress_callback: nil,
89
+ track_differences: true,
90
+ record_results: false
91
+ }.merge(options)
92
+
93
+ shadow_agent = build_shadow_agent(shadow_version)
94
+ results = execute_contexts_with_progress(contexts, shadow_version, shadow_agent, options)
95
+
96
+ build_batch_report(results)
97
+ end
98
+
99
+ def execute_contexts_with_progress(contexts, shadow_version, shadow_agent, options)
100
+ results = []
101
+ mutex = Mutex.new
102
+ progress_tracker = ProgressTracker.new(contexts.size, options[:progress_callback])
103
+
104
+ if options[:parallel] && contexts.size > 1
105
+ execute_parallel(contexts, shadow_agent, options, mutex) do |result|
106
+ mutex.synchronize do
107
+ results << result
108
+ progress_tracker.increment
109
+ end
110
+ end
111
+ else
112
+ execute_sequential_contexts(contexts, shadow_version, options, results, progress_tracker)
113
+ end
114
+
115
+ results
116
+ end
117
+
118
+ def execute_sequential_contexts(contexts, shadow_version, options, results, progress_tracker)
119
+ contexts.each_with_index do |context, _index|
120
+ result = test(context: context, shadow_version: shadow_version, options: options)
121
+ results << result
122
+ progress_tracker.increment
123
+ end
124
+ end
125
+
126
+ # Helper class for tracking progress
127
+ class ProgressTracker
128
+ def initialize(total, callback)
129
+ @total = total
130
+ @callback = callback
131
+ @completed = 0
132
+ end
133
+
134
+ def increment
135
+ @completed += 1
136
+ return unless @callback
137
+
138
+ @callback.call(
139
+ completed: @completed,
140
+ total: @total,
141
+ percentage: (@completed.to_f / @total * 100).round(2)
142
+ )
143
+ end
144
+ end
145
+
146
+ # Get shadow test summary statistics
147
+ # @param shadow_version [String, Integer, Hash] Shadow version ID
148
+ # @return [Hash] Summary statistics
149
+ def get_summary(_shadow_version)
150
+ # In a real implementation, this would query stored results
151
+ # For now, return empty summary
152
+ {
153
+ total_tests: 0,
154
+ matches: 0,
155
+ mismatches: 0,
156
+ match_rate: 0.0,
157
+ average_confidence_delta: 0.0
158
+ }
159
+ end
160
+
161
+ private
162
+
163
+ def build_shadow_agent(shadow_version)
164
+ version_hash = resolve_version(shadow_version)
165
+ evaluators = build_evaluators_from_version(version_hash)
166
+ Agent.new(
167
+ evaluators: evaluators,
168
+ scoring_strategy: @production_agent.scoring_strategy,
169
+ audit_adapter: Audit::NullAdapter.new
170
+ )
171
+ end
172
+
173
+ def resolve_version(version)
174
+ case version
175
+ when String, Integer
176
+ version_data = @version_manager.get_version(version_id: version)
177
+ raise InvalidShadowTestError, "Shadow version not found: #{version}" unless version_data
178
+
179
+ version_data
180
+ when Hash
181
+ version
182
+ else
183
+ raise InvalidShadowTestError, "Invalid shadow version format: #{version.class}"
184
+ end
185
+ end
186
+
187
+ def build_evaluators_from_version(version)
188
+ content = version[:content] || version["content"]
189
+ raise InvalidShadowTestError, "Shadow version has no content" unless content
190
+
191
+ if content.is_a?(Hash) && content[:evaluators]
192
+ build_evaluators_from_config(content[:evaluators])
193
+ elsif content.is_a?(Hash) && (content[:rules] || content["rules"])
194
+ [Evaluators::JsonRuleEvaluator.new(rules_json: content)]
195
+ else
196
+ raise InvalidShadowTestError, "Cannot build evaluators from shadow version"
197
+ end
198
+ end
199
+
200
+ def build_evaluators_from_config(configs)
201
+ Array(configs).map do |config|
202
+ case config[:type] || config["type"]
203
+ when "json_rule"
204
+ Evaluators::JsonRuleEvaluator.new(rules_json: config[:rules] || config["rules"])
205
+ when "dmn"
206
+ model = config[:model] || config["model"]
207
+ decision_id = config[:decision_id] || config["decision_id"]
208
+ Evaluators::DmnEvaluator.new(model: model, decision_id: decision_id)
209
+ else
210
+ raise InvalidShadowTestError, "Unknown evaluator type: #{config[:type]}"
211
+ end
212
+ end
213
+ end
214
+
215
+ def execute_parallel(contexts, shadow_agent, options, _mutex, &block)
216
+ thread_count = [options[:thread_count], contexts.size].min
217
+ queue = Queue.new
218
+ contexts.each { |c| queue << c }
219
+
220
+ threads = Array.new(thread_count) do
221
+ Thread.new do
222
+ process_contexts_from_queue(queue, shadow_agent, options, &block)
223
+ end
224
+ end
225
+
226
+ threads.each(&:join)
227
+ end
228
+
229
+ def process_contexts_from_queue(queue, shadow_agent, options)
230
+ loop do
231
+ context = dequeue_context(queue)
232
+ break unless context
233
+
234
+ ctx = normalize_context(context)
235
+ production_decision = execute_production_decision(ctx)
236
+ shadow_decision = execute_shadow_decision_in_parallel(ctx, shadow_agent)
237
+
238
+ result = build_comparison_result(ctx, production_decision, shadow_decision)
239
+ add_differences(result, production_decision, shadow_decision) if options[:track_differences] && !result[:matches]
240
+
241
+ yield result
242
+ end
243
+ end
244
+
245
+ def dequeue_context(queue)
246
+ queue.pop(true)
247
+ rescue StandardError
248
+ nil
249
+ end
250
+
251
+ def execute_shadow_decision_in_parallel(context, shadow_agent)
252
+ shadow_agent.decide(context: context)
253
+ rescue NoEvaluationsError
254
+ nil
255
+ end
256
+
257
+ def record_result(_result, shadow_version)
258
+ # In a real implementation, this would store results in a database or file
259
+ # For now, this is a placeholder
260
+ shadow_version.is_a?(Hash) ? shadow_version[:id] || shadow_version["id"] : shadow_version
261
+ # Store result for later analysis
262
+ end
263
+
264
+ def build_batch_report(results)
265
+ total = results.size
266
+ matches = results.count { |r| r[:matches] }
267
+ mismatches = total - matches
268
+ confidence_deltas = results.map { |r| r[:confidence_delta] }.compact
269
+
270
+ {
271
+ total_tests: total,
272
+ matches: matches,
273
+ mismatches: mismatches,
274
+ match_rate: total.positive? ? (matches.to_f / total) : 0,
275
+ average_confidence_delta: confidence_deltas.any? ? confidence_deltas.sum / confidence_deltas.size : 0,
276
+ max_confidence_delta: confidence_deltas.map(&:abs).max || 0,
277
+ decision_distribution: {
278
+ production: results.group_by { |r| r[:production_decision] }.transform_values(&:count),
279
+ shadow: results.group_by { |r| r[:shadow_decision] }.transform_values(&:count)
280
+ },
281
+ mismatched_results: results.reject { |r| r[:matches] },
282
+ results: results
283
+ }
284
+ end
285
+ end
286
+ end
287
+ end