decision_agent 0.3.0 → 1.1.0

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module DecisionAgent
6
+ module Simulation
7
+ # Engine for managing and executing test scenarios
8
+ class ScenarioEngine
9
+ attr_reader :agent, :version_manager
10
+
11
+ def initialize(agent:, version_manager: nil)
12
+ @agent = agent
13
+ @version_manager = version_manager || Versioning::VersionManager.new
14
+ end
15
+
16
+ # Execute a single scenario
17
+ # @param scenario [Hash] Scenario definition with context and optional metadata
18
+ # @param rule_version [String, Integer, Hash, nil] Optional rule version to use
19
+ # @return [Hash] Scenario execution result
20
+ def execute(scenario:, rule_version: nil)
21
+ context = scenario[:context] || scenario["context"] || scenario
22
+ metadata = scenario[:metadata] || scenario["metadata"] || {}
23
+
24
+ analysis_agent = build_agent_from_version(rule_version) if rule_version
25
+ analysis_agent ||= @agent
26
+
27
+ ctx = context.is_a?(Context) ? context : Context.new(context)
28
+ decision = analysis_agent.decide(context: ctx)
29
+
30
+ {
31
+ scenario_id: scenario[:id] || scenario["id"] || generate_scenario_id,
32
+ context: ctx.to_h,
33
+ decision: decision.decision,
34
+ confidence: decision.confidence,
35
+ explanations: decision.explanations,
36
+ metadata: metadata,
37
+ executed_at: Time.now.utc.iso8601
38
+ }
39
+ end
40
+
41
+ # Execute multiple scenarios
42
+ # @param scenarios [Array<Hash>] Array of scenario definitions
43
+ # @param rule_version [String, Integer, Hash, nil] Optional rule version
44
+ # @param options [Hash] Execution options
45
+ # - :parallel [Boolean] Use parallel execution (default: true)
46
+ # - :thread_count [Integer] Number of threads (default: 4)
47
+ # - :progress_callback [Proc] Progress callback
48
+ # @return [Hash] Batch execution results
49
+ def execute_batch(scenarios:, rule_version: nil, options: {})
50
+ options = {
51
+ parallel: true,
52
+ thread_count: 4,
53
+ progress_callback: nil
54
+ }.merge(options)
55
+
56
+ analysis_agent = build_agent_from_version(rule_version) if rule_version
57
+ analysis_agent ||= @agent
58
+
59
+ results = execute_scenarios_with_progress(
60
+ scenarios, analysis_agent, rule_version, options
61
+ )
62
+
63
+ build_batch_report(results)
64
+ end
65
+
66
+ def execute_scenarios_with_progress(scenarios, analysis_agent, rule_version, options)
67
+ results = []
68
+ mutex = Mutex.new
69
+ progress_tracker = ProgressTracker.new(scenarios.size, options[:progress_callback])
70
+
71
+ if options[:parallel] && scenarios.size > 1
72
+ execute_parallel(scenarios, analysis_agent, options, mutex) do |result|
73
+ mutex.synchronize do
74
+ results << result
75
+ progress_tracker.increment
76
+ end
77
+ end
78
+ else
79
+ execute_sequential(scenarios, rule_version, results, progress_tracker)
80
+ end
81
+
82
+ results
83
+ end
84
+
85
+ def execute_sequential(scenarios, rule_version, results, progress_tracker)
86
+ scenarios.each_with_index do |scenario, _index|
87
+ result = execute(scenario: scenario, rule_version: rule_version)
88
+ results << result
89
+ progress_tracker.increment
90
+ end
91
+ end
92
+
93
+ # Helper class for tracking progress
94
+ class ProgressTracker
95
+ def initialize(total, callback)
96
+ @total = total
97
+ @callback = callback
98
+ @completed = 0
99
+ end
100
+
101
+ def increment
102
+ @completed += 1
103
+ return unless @callback
104
+
105
+ @callback.call(
106
+ completed: @completed,
107
+ total: @total,
108
+ percentage: (@completed.to_f / @total * 100).round(2)
109
+ )
110
+ end
111
+ end
112
+
113
+ # Compare scenarios across different rule versions
114
+ # @param scenarios [Array<Hash>] Scenarios to test
115
+ # @param versions [Array<String, Integer, Hash>] Rule versions to compare
116
+ # @param options [Hash] Execution options
117
+ # @return [Hash] Comparison results
118
+ def compare_versions(scenarios:, versions:, options: {})
119
+ options = {
120
+ parallel: true,
121
+ thread_count: 4
122
+ }.merge(options)
123
+
124
+ version_results = {}
125
+ versions.each do |version|
126
+ results = execute_batch(scenarios: scenarios, rule_version: version, options: options)
127
+ version_id = version.is_a?(Hash) ? (version[:id] || version["id"]) : version
128
+ version_results[version_id.to_s] = results
129
+ end
130
+
131
+ {
132
+ scenarios: scenarios,
133
+ versions: versions.map { |v| v.is_a?(Hash) ? (v[:id] || v["id"]) : v },
134
+ results_by_version: version_results,
135
+ comparison: build_version_comparison(version_results)
136
+ }
137
+ end
138
+
139
+ private
140
+
141
+ def build_agent_from_version(version)
142
+ version_hash = resolve_version(version)
143
+ evaluators = build_evaluators_from_version(version_hash)
144
+ Agent.new(
145
+ evaluators: evaluators,
146
+ scoring_strategy: @agent.scoring_strategy,
147
+ audit_adapter: Audit::NullAdapter.new
148
+ )
149
+ end
150
+
151
+ def resolve_version(version)
152
+ case version
153
+ when String, Integer
154
+ version_data = @version_manager.get_version(version_id: version)
155
+ raise VersionComparisonError, "Version not found: #{version}" unless version_data
156
+
157
+ version_data
158
+ when Hash
159
+ version
160
+ else
161
+ raise VersionComparisonError, "Invalid version format: #{version.class}"
162
+ end
163
+ end
164
+
165
+ def build_evaluators_from_version(version)
166
+ content = version[:content] || version["content"]
167
+ return @agent.evaluators unless content
168
+
169
+ if content.is_a?(Hash) && content[:evaluators]
170
+ build_evaluators_from_config(content[:evaluators])
171
+ elsif content.is_a?(Hash) && (content[:rules] || content["rules"])
172
+ [Evaluators::JsonRuleEvaluator.new(rules_json: content)]
173
+ else
174
+ @agent.evaluators
175
+ end
176
+ end
177
+
178
+ def build_evaluators_from_config(configs)
179
+ Array(configs).map do |config|
180
+ case config[:type] || config["type"]
181
+ when "json_rule"
182
+ Evaluators::JsonRuleEvaluator.new(rules_json: config[:rules] || config["rules"])
183
+ when "dmn"
184
+ model = config[:model] || config["model"]
185
+ decision_id = config[:decision_id] || config["decision_id"]
186
+ Evaluators::DmnEvaluator.new(model: model, decision_id: decision_id)
187
+ else
188
+ raise VersionComparisonError, "Unknown evaluator type: #{config[:type]}"
189
+ end
190
+ end
191
+ end
192
+
193
+ def execute_parallel(scenarios, analysis_agent, options, _mutex, &block)
194
+ thread_count = [options[:thread_count], scenarios.size].min
195
+ queue = Queue.new
196
+ scenarios.each { |s| queue << s }
197
+
198
+ threads = Array.new(thread_count) do
199
+ Thread.new do
200
+ process_scenarios_from_queue(queue, analysis_agent, &block)
201
+ end
202
+ end
203
+
204
+ threads.each(&:join)
205
+ end
206
+
207
+ def process_scenarios_from_queue(queue, analysis_agent)
208
+ loop do
209
+ scenario = dequeue_scenario(queue)
210
+ break unless scenario
211
+
212
+ result = process_scenario(scenario, analysis_agent)
213
+ yield result
214
+ end
215
+ end
216
+
217
+ def dequeue_scenario(queue)
218
+ queue.pop(true)
219
+ rescue ThreadError
220
+ nil
221
+ end
222
+
223
+ def process_scenario(scenario, analysis_agent)
224
+ context = extract_scenario_context(scenario)
225
+ metadata = extract_scenario_metadata(scenario)
226
+ ctx = context.is_a?(Context) ? context : Context.new(context)
227
+
228
+ begin
229
+ decision = analysis_agent.decide(context: ctx)
230
+ build_scenario_result(scenario, ctx, decision, metadata)
231
+ rescue NoEvaluationsError
232
+ build_error_result(scenario, ctx, metadata)
233
+ end
234
+ end
235
+
236
+ def extract_scenario_context(scenario)
237
+ scenario[:context] || scenario["context"] || scenario
238
+ end
239
+
240
+ def extract_scenario_metadata(scenario)
241
+ scenario[:metadata] || scenario["metadata"] || {}
242
+ end
243
+
244
+ def build_scenario_result(scenario, context, decision, metadata)
245
+ {
246
+ scenario_id: extract_scenario_id(scenario),
247
+ context: context.to_h,
248
+ decision: decision.decision,
249
+ confidence: decision.confidence,
250
+ explanations: decision.explanations,
251
+ metadata: metadata,
252
+ executed_at: Time.now.utc.iso8601
253
+ }
254
+ end
255
+
256
+ def build_error_result(scenario, context, metadata)
257
+ {
258
+ scenario_id: extract_scenario_id(scenario),
259
+ context: context.to_h,
260
+ decision: nil,
261
+ confidence: 0.0,
262
+ explanations: [],
263
+ metadata: metadata,
264
+ executed_at: Time.now.utc.iso8601,
265
+ error: "No evaluators returned a decision"
266
+ }
267
+ end
268
+
269
+ def extract_scenario_id(scenario)
270
+ scenario[:id] || scenario["id"] || generate_scenario_id
271
+ end
272
+
273
+ def build_batch_report(results)
274
+ {
275
+ total_scenarios: results.size,
276
+ decision_distribution: results.group_by { |r| r[:decision] }.transform_values(&:count),
277
+ average_confidence: calculate_average_confidence(results),
278
+ min_confidence: results.map { |r| r[:confidence] }.min || 0,
279
+ max_confidence: results.map { |r| r[:confidence] }.max || 0,
280
+ results: results
281
+ }
282
+ end
283
+
284
+ def build_version_comparison(version_results)
285
+ comparison = {}
286
+ version_ids = version_results.keys
287
+
288
+ # Compare decision distributions
289
+ decision_comparison = {}
290
+ version_ids.each do |version_id|
291
+ results = version_results[version_id][:results] || []
292
+ decision_comparison[version_id] = results.group_by { |r| r[:decision] }.transform_values(&:count)
293
+ end
294
+
295
+ comparison[:decision_distributions] = decision_comparison
296
+
297
+ # Compare average confidence
298
+ confidence_comparison = {}
299
+ version_ids.each do |version_id|
300
+ results = version_results[version_id][:results] || []
301
+ confidences = results.map { |r| r[:confidence] }.compact
302
+ confidence_comparison[version_id] = confidences.any? ? confidences.sum / confidences.size : 0
303
+ end
304
+
305
+ comparison[:average_confidence] = confidence_comparison
306
+
307
+ comparison
308
+ end
309
+
310
+ def calculate_average_confidence(results)
311
+ confidences = results.map { |r| r[:confidence] }.compact
312
+ confidences.any? ? confidences.sum / confidences.size : 0
313
+ end
314
+
315
+ def generate_scenario_id
316
+ "scenario_#{Time.now.to_f}_#{rand(1000)}"
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecisionAgent
4
+ module Simulation
5
+ # Library of pre-defined test scenario templates
6
+ class ScenarioLibrary
7
+ # Get scenario template by name
8
+ # @param template_name [String, Symbol] Template name
9
+ # @return [Hash, nil] Scenario template or nil if not found
10
+ def self.get_template(template_name)
11
+ templates[template_name.to_sym] || templates[template_name.to_s]
12
+ end
13
+
14
+ # List all available templates
15
+ # @return [Array<String>] Array of template names
16
+ def self.list_templates
17
+ templates.keys.map(&:to_s)
18
+ end
19
+
20
+ # Create scenario from template
21
+ # @param template_name [String, Symbol] Template name
22
+ # @param overrides [Hash] Values to override in template
23
+ # @return [Hash] Scenario definition
24
+ def self.create_scenario(template_name, overrides: {})
25
+ template = get_template(template_name)
26
+ raise DecisionAgent::Simulation::ScenarioExecutionError, "Template not found: #{template_name}" unless template
27
+
28
+ scenario = template.dup
29
+ merge_overrides(scenario, overrides)
30
+ scenario
31
+ end
32
+
33
+ # Get edge case scenarios for a given context structure
34
+ # @param base_context [Hash] Base context to generate edge cases from
35
+ # @return [Array<Hash>] Array of edge case scenarios
36
+ def self.generate_edge_cases(base_context)
37
+ scenarios = []
38
+
39
+ # Generate scenarios with nil values
40
+ base_context.each_key do |key|
41
+ edge_scenario = base_context.dup
42
+ edge_scenario[key] = nil
43
+ scenarios << { context: edge_scenario, metadata: { type: "edge_case", field: key, value: "nil" } }
44
+ end
45
+
46
+ # Generate scenarios with extreme numeric values and empty strings
47
+ base_context.each do |key, value|
48
+ if value.is_a?(Numeric)
49
+ # Zero value
50
+ zero_scenario = base_context.dup
51
+ zero_scenario[key] = 0
52
+ scenarios << { context: zero_scenario, metadata: { type: "edge_case", field: key, value: "zero" } }
53
+
54
+ # Negative value (if positive)
55
+ if value.positive?
56
+ neg_scenario = base_context.dup
57
+ neg_scenario[key] = -value
58
+ scenarios << { context: neg_scenario, metadata: { type: "edge_case", field: key, value: "negative" } }
59
+ end
60
+
61
+ # Very large value
62
+ large_scenario = base_context.dup
63
+ large_scenario[key] = value * 1000
64
+ scenarios << { context: large_scenario, metadata: { type: "edge_case", field: key, value: "large" } }
65
+ elsif value.is_a?(String)
66
+ # Empty string
67
+ empty_scenario = base_context.dup
68
+ empty_scenario[key] = ""
69
+ scenarios << { context: empty_scenario, metadata: { type: "edge_case", field: key, value: "empty_string" } }
70
+ end
71
+ end
72
+
73
+ scenarios
74
+ end
75
+
76
+ def self.templates
77
+ loan_approval_templates
78
+ .merge(fraud_detection_templates)
79
+ .merge(pricing_templates)
80
+ end
81
+
82
+ def self.loan_approval_templates
83
+ {
84
+ loan_approval_high_risk: build_loan_scenario(100_000, 550, 30_000, "unemployed", "high_risk"),
85
+ loan_approval_low_risk: build_loan_scenario(50_000, 800, 100_000, "employed", "low_risk"),
86
+ loan_approval_medium_risk: build_loan_scenario(75_000, 650, 60_000, "employed", "medium_risk")
87
+ }
88
+ end
89
+
90
+ def self.build_loan_scenario(amount, credit_score, income, employment_status, risk_level)
91
+ {
92
+ context: {
93
+ amount: amount,
94
+ credit_score: credit_score,
95
+ income: income,
96
+ employment_status: employment_status
97
+ },
98
+ metadata: {
99
+ type: "loan_approval",
100
+ category: risk_level,
101
+ description: "#{risk_level.capitalize.tr('_', ' ')} loan application scenario"
102
+ }
103
+ }
104
+ end
105
+
106
+ def self.fraud_detection_templates
107
+ {
108
+ fraud_detection_suspicious: build_fraud_scenario(10_000, 5, 50, "unusual", "suspicious"),
109
+ fraud_detection_normal: build_fraud_scenario(100, 365, 3, "usual", "normal")
110
+ }
111
+ end
112
+
113
+ def self.build_fraud_scenario(amount, account_age, transaction_count, location, category)
114
+ {
115
+ context: {
116
+ transaction_amount: amount,
117
+ account_age_days: account_age,
118
+ transaction_count_24h: transaction_count,
119
+ location: location
120
+ },
121
+ metadata: {
122
+ type: "fraud_detection",
123
+ category: category,
124
+ description: "#{category.capitalize} transaction scenario"
125
+ }
126
+ }
127
+ end
128
+
129
+ def self.pricing_templates
130
+ {
131
+ pricing_high_value: build_pricing_scenario("premium", 5_000, 10_000, "high", "high_value"),
132
+ pricing_standard: build_pricing_scenario("standard", 100, 500, "medium", "standard")
133
+ }
134
+ end
135
+
136
+ def self.build_pricing_scenario(tier, order_value, loyalty_points, frequency, category)
137
+ {
138
+ context: {
139
+ customer_tier: tier,
140
+ order_value: order_value,
141
+ loyalty_points: loyalty_points,
142
+ purchase_frequency: frequency
143
+ },
144
+ metadata: {
145
+ type: "pricing",
146
+ category: category,
147
+ description: "#{category.capitalize.tr('_', ' ')} customer pricing scenario"
148
+ }
149
+ }
150
+ end
151
+
152
+ def self.merge_overrides(scenario, overrides)
153
+ scenario[:context] = scenario[:context].merge(overrides[:context]) if overrides[:context]
154
+
155
+ scenario[:metadata] = (scenario[:metadata] || {}).merge(overrides[:metadata]) if overrides[:metadata]
156
+
157
+ overrides.each do |key, value|
158
+ next if %i[context metadata].include?(key)
159
+
160
+ scenario[key] = value
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end