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