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
|
@@ -28,6 +28,15 @@ require_relative "../auth/access_audit_logger"
|
|
|
28
28
|
require_relative "middleware/auth_middleware"
|
|
29
29
|
require_relative "middleware/permission_middleware"
|
|
30
30
|
|
|
31
|
+
# Simulation components
|
|
32
|
+
require_relative "../simulation/replay_engine"
|
|
33
|
+
require_relative "../simulation/what_if_analyzer"
|
|
34
|
+
require_relative "../simulation/impact_analyzer"
|
|
35
|
+
require_relative "../simulation/shadow_test_engine"
|
|
36
|
+
require_relative "../simulation/scenario_engine"
|
|
37
|
+
require_relative "../simulation/scenario_library"
|
|
38
|
+
require_relative "../versioning/version_manager"
|
|
39
|
+
|
|
31
40
|
module DecisionAgent
|
|
32
41
|
module Web
|
|
33
42
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -41,25 +50,27 @@ module DecisionAgent
|
|
|
41
50
|
@batch_test_storage = {}
|
|
42
51
|
@batch_test_storage_mutex = Mutex.new
|
|
43
52
|
|
|
53
|
+
# In-memory storage for simulation runs
|
|
54
|
+
@simulation_storage = {}
|
|
55
|
+
@simulation_storage_mutex = Mutex.new
|
|
56
|
+
|
|
44
57
|
# Auth components
|
|
45
58
|
@authenticator = nil
|
|
46
59
|
@permission_checker = nil
|
|
47
60
|
@access_audit_logger = nil
|
|
48
|
-
|
|
49
|
-
def self.batch_test_storage
|
|
50
|
-
@batch_test_storage ||= {}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.batch_test_storage_mutex
|
|
54
|
-
@batch_test_storage_mutex ||= Mutex.new
|
|
55
|
-
end
|
|
61
|
+
@auth_mutex = Mutex.new
|
|
56
62
|
|
|
57
63
|
class << self
|
|
64
|
+
attr_reader :batch_test_storage, :batch_test_storage_mutex, :simulation_storage, :simulation_storage_mutex
|
|
58
65
|
attr_writer :authenticator
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def self.authenticator
|
|
62
|
-
@authenticator
|
|
69
|
+
return @authenticator if @authenticator
|
|
70
|
+
|
|
71
|
+
@auth_mutex.synchronize do
|
|
72
|
+
@authenticator ||= Auth::Authenticator.new
|
|
73
|
+
end
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
class << self
|
|
@@ -67,7 +78,11 @@ module DecisionAgent
|
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
def self.permission_checker
|
|
70
|
-
@permission_checker
|
|
81
|
+
return @permission_checker if @permission_checker
|
|
82
|
+
|
|
83
|
+
@auth_mutex.synchronize do
|
|
84
|
+
@permission_checker ||= Auth::PermissionChecker.new(adapter: DecisionAgent.rbac_config.adapter)
|
|
85
|
+
end
|
|
71
86
|
end
|
|
72
87
|
|
|
73
88
|
class << self
|
|
@@ -75,7 +90,11 @@ module DecisionAgent
|
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
def self.access_audit_logger
|
|
78
|
-
@access_audit_logger
|
|
93
|
+
return @access_audit_logger if @access_audit_logger
|
|
94
|
+
|
|
95
|
+
@auth_mutex.synchronize do
|
|
96
|
+
@access_audit_logger ||= Auth::AccessAuditLogger.new
|
|
97
|
+
end
|
|
79
98
|
end
|
|
80
99
|
|
|
81
100
|
# Enable CORS for API calls
|
|
@@ -204,19 +223,55 @@ module DecisionAgent
|
|
|
204
223
|
result = evaluator.evaluate(DecisionAgent::Context.new(context))
|
|
205
224
|
|
|
206
225
|
if result
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
# Get explainability data from metadata if available
|
|
227
|
+
explainability = result.metadata[:explainability] if result.metadata.is_a?(Hash)
|
|
228
|
+
|
|
229
|
+
# Structure response as explainability by default
|
|
230
|
+
# This makes explainability the primary format for decision results
|
|
231
|
+
response = if explainability
|
|
232
|
+
{
|
|
233
|
+
success: true,
|
|
234
|
+
decision: explainability[:decision] || result.decision,
|
|
235
|
+
because: explainability[:because] || [],
|
|
236
|
+
failed_conditions: explainability[:failed_conditions] || [],
|
|
237
|
+
# Include additional metadata for completeness
|
|
238
|
+
confidence: result.weight,
|
|
239
|
+
reason: result.reason,
|
|
240
|
+
evaluator_name: result.evaluator_name,
|
|
241
|
+
# Full explainability data (includes rule_traces in verbose mode)
|
|
242
|
+
explainability: explainability
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
# Fallback if explainability is not available
|
|
246
|
+
{
|
|
247
|
+
success: true,
|
|
248
|
+
decision: result.decision,
|
|
249
|
+
because: [],
|
|
250
|
+
failed_conditions: [],
|
|
251
|
+
confidence: result.weight,
|
|
252
|
+
reason: result.reason,
|
|
253
|
+
evaluator_name: result.evaluator_name,
|
|
254
|
+
explainability: {
|
|
255
|
+
decision: result.decision,
|
|
256
|
+
because: [],
|
|
257
|
+
failed_conditions: []
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
response.to_json
|
|
215
263
|
else
|
|
216
264
|
{
|
|
217
265
|
success: true,
|
|
218
266
|
decision: nil,
|
|
219
|
-
|
|
267
|
+
because: [],
|
|
268
|
+
failed_conditions: [],
|
|
269
|
+
message: "No rules matched the given context",
|
|
270
|
+
explainability: {
|
|
271
|
+
decision: nil,
|
|
272
|
+
because: [],
|
|
273
|
+
failed_conditions: []
|
|
274
|
+
}
|
|
220
275
|
}.to_json
|
|
221
276
|
end
|
|
222
277
|
rescue StandardError => e
|
|
@@ -1145,6 +1200,516 @@ module DecisionAgent
|
|
|
1145
1200
|
"Batch testing page not found"
|
|
1146
1201
|
end
|
|
1147
1202
|
|
|
1203
|
+
# Simulation API Endpoints
|
|
1204
|
+
|
|
1205
|
+
# POST /api/simulation/replay - Historical replay/backtesting
|
|
1206
|
+
post "/api/simulation/replay" do
|
|
1207
|
+
content_type :json
|
|
1208
|
+
|
|
1209
|
+
begin
|
|
1210
|
+
request_body = request.body.read
|
|
1211
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1212
|
+
|
|
1213
|
+
historical_data = data["historical_data"]
|
|
1214
|
+
rule_version = data["rule_version"]
|
|
1215
|
+
compare_with = data["compare_with"]
|
|
1216
|
+
options = data["options"] || {}
|
|
1217
|
+
|
|
1218
|
+
unless historical_data
|
|
1219
|
+
status 400
|
|
1220
|
+
return { error: "historical_data is required" }.to_json
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1223
|
+
# Get rules for agent creation
|
|
1224
|
+
rules_json = data["rules"]
|
|
1225
|
+
unless rules_json
|
|
1226
|
+
status 400
|
|
1227
|
+
return { error: "rules JSON is required" }.to_json
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
# Create agent
|
|
1231
|
+
evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
|
|
1232
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
1233
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1234
|
+
|
|
1235
|
+
# Create replay engine
|
|
1236
|
+
replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
|
|
1237
|
+
agent: agent,
|
|
1238
|
+
version_manager: version_manager
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
# Convert historical data if it's a file path (for future file upload support)
|
|
1242
|
+
contexts = if historical_data.is_a?(Array)
|
|
1243
|
+
historical_data
|
|
1244
|
+
else
|
|
1245
|
+
# Assume it's a file path - load it
|
|
1246
|
+
raise ArgumentError, "File not found: #{historical_data}" unless File.exist?(historical_data)
|
|
1247
|
+
|
|
1248
|
+
if historical_data.end_with?(".json")
|
|
1249
|
+
JSON.parse(File.read(historical_data))
|
|
1250
|
+
elsif historical_data.end_with?(".csv")
|
|
1251
|
+
# Simple CSV parsing
|
|
1252
|
+
require "csv"
|
|
1253
|
+
csv_data = CSV.read(historical_data, headers: true)
|
|
1254
|
+
csv_data.map(&:to_h)
|
|
1255
|
+
else
|
|
1256
|
+
raise ArgumentError, "Unsupported file format"
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
# Execute replay
|
|
1262
|
+
results = if compare_with
|
|
1263
|
+
replay_engine.replay(
|
|
1264
|
+
historical_data: contexts,
|
|
1265
|
+
rule_version: rule_version,
|
|
1266
|
+
compare_with: compare_with
|
|
1267
|
+
)
|
|
1268
|
+
else
|
|
1269
|
+
replay_engine.replay(
|
|
1270
|
+
historical_data: contexts,
|
|
1271
|
+
rule_version: rule_version,
|
|
1272
|
+
options: options
|
|
1273
|
+
)
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
# Store results
|
|
1277
|
+
replay_id = SecureRandom.uuid
|
|
1278
|
+
self.class.simulation_storage_mutex.synchronize do
|
|
1279
|
+
self.class.simulation_storage[replay_id] = {
|
|
1280
|
+
id: replay_id,
|
|
1281
|
+
type: "replay",
|
|
1282
|
+
status: "completed",
|
|
1283
|
+
created_at: Time.now.utc.iso8601,
|
|
1284
|
+
results: results
|
|
1285
|
+
}
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
{
|
|
1289
|
+
replay_id: replay_id,
|
|
1290
|
+
results: results
|
|
1291
|
+
}.to_json
|
|
1292
|
+
rescue StandardError => e
|
|
1293
|
+
status 500
|
|
1294
|
+
{ error: "Replay failed: #{e.message}" }.to_json
|
|
1295
|
+
end
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
# POST /api/simulation/whatif - What-if analysis
|
|
1299
|
+
post "/api/simulation/whatif" do
|
|
1300
|
+
content_type :json
|
|
1301
|
+
|
|
1302
|
+
begin
|
|
1303
|
+
request_body = request.body.read
|
|
1304
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1305
|
+
|
|
1306
|
+
scenarios = data["scenarios"]
|
|
1307
|
+
rule_version = data["rule_version"]
|
|
1308
|
+
options = data["options"] || {}
|
|
1309
|
+
|
|
1310
|
+
unless scenarios.is_a?(Array)
|
|
1311
|
+
status 400
|
|
1312
|
+
return { error: "scenarios array is required" }.to_json
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# Get rules for agent creation
|
|
1316
|
+
rules_json = data["rules"]
|
|
1317
|
+
unless rules_json
|
|
1318
|
+
status 400
|
|
1319
|
+
return { error: "rules JSON is required" }.to_json
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
# Create agent
|
|
1323
|
+
evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
|
|
1324
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
1325
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1326
|
+
|
|
1327
|
+
# Create what-if analyzer
|
|
1328
|
+
analyzer = DecisionAgent::Simulation::WhatIfAnalyzer.new(
|
|
1329
|
+
agent: agent,
|
|
1330
|
+
version_manager: version_manager
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
# Execute analysis
|
|
1334
|
+
results = analyzer.analyze(
|
|
1335
|
+
scenarios: scenarios,
|
|
1336
|
+
rule_version: rule_version,
|
|
1337
|
+
options: options
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
# Store results
|
|
1341
|
+
analysis_id = SecureRandom.uuid
|
|
1342
|
+
self.class.simulation_storage_mutex.synchronize do
|
|
1343
|
+
self.class.simulation_storage[analysis_id] = {
|
|
1344
|
+
id: analysis_id,
|
|
1345
|
+
type: "whatif",
|
|
1346
|
+
status: "completed",
|
|
1347
|
+
created_at: Time.now.utc.iso8601,
|
|
1348
|
+
results: results
|
|
1349
|
+
}
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
{
|
|
1353
|
+
analysis_id: analysis_id,
|
|
1354
|
+
results: results
|
|
1355
|
+
}.to_json
|
|
1356
|
+
rescue StandardError => e
|
|
1357
|
+
status 500
|
|
1358
|
+
{ error: "What-if analysis failed: #{e.message}" }.to_json
|
|
1359
|
+
end
|
|
1360
|
+
end
|
|
1361
|
+
|
|
1362
|
+
# POST /api/simulation/whatif/sensitivity - Sensitivity analysis
|
|
1363
|
+
post "/api/simulation/whatif/sensitivity" do
|
|
1364
|
+
content_type :json
|
|
1365
|
+
|
|
1366
|
+
begin
|
|
1367
|
+
request_body = request.body.read
|
|
1368
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1369
|
+
|
|
1370
|
+
base_scenario = data["base_scenario"]
|
|
1371
|
+
variations = data["variations"]
|
|
1372
|
+
rule_version = data["rule_version"]
|
|
1373
|
+
|
|
1374
|
+
unless base_scenario && variations
|
|
1375
|
+
status 400
|
|
1376
|
+
return { error: "base_scenario and variations are required" }.to_json
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
# Get rules for agent creation
|
|
1380
|
+
rules_json = data["rules"]
|
|
1381
|
+
unless rules_json
|
|
1382
|
+
status 400
|
|
1383
|
+
return { error: "rules JSON is required" }.to_json
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
# Create agent
|
|
1387
|
+
evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules_json)
|
|
1388
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
1389
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1390
|
+
|
|
1391
|
+
# Create what-if analyzer
|
|
1392
|
+
analyzer = DecisionAgent::Simulation::WhatIfAnalyzer.new(
|
|
1393
|
+
agent: agent,
|
|
1394
|
+
version_manager: version_manager
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
# Execute sensitivity analysis
|
|
1398
|
+
results = analyzer.sensitivity_analysis(
|
|
1399
|
+
base_scenario: base_scenario,
|
|
1400
|
+
variations: variations,
|
|
1401
|
+
rule_version: rule_version
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
{
|
|
1405
|
+
results: results
|
|
1406
|
+
}.to_json
|
|
1407
|
+
rescue StandardError => e
|
|
1408
|
+
status 500
|
|
1409
|
+
{ error: "Sensitivity analysis failed: #{e.message}" }.to_json
|
|
1410
|
+
end
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1413
|
+
# POST /api/simulation/impact - Impact analysis
|
|
1414
|
+
post "/api/simulation/impact" do
|
|
1415
|
+
content_type :json
|
|
1416
|
+
|
|
1417
|
+
begin
|
|
1418
|
+
request_body = request.body.read
|
|
1419
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1420
|
+
|
|
1421
|
+
baseline_version = data["baseline_version"]
|
|
1422
|
+
proposed_version = data["proposed_version"]
|
|
1423
|
+
test_data = data["test_data"]
|
|
1424
|
+
options = data["options"] || {}
|
|
1425
|
+
|
|
1426
|
+
unless baseline_version && proposed_version && test_data
|
|
1427
|
+
status 400
|
|
1428
|
+
return { error: "baseline_version, proposed_version, and test_data are required" }.to_json
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1432
|
+
|
|
1433
|
+
# Create impact analyzer
|
|
1434
|
+
analyzer = DecisionAgent::Simulation::ImpactAnalyzer.new(
|
|
1435
|
+
version_manager: version_manager
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
# Execute impact analysis
|
|
1439
|
+
results = analyzer.analyze(
|
|
1440
|
+
baseline_version: baseline_version,
|
|
1441
|
+
proposed_version: proposed_version,
|
|
1442
|
+
test_data: test_data,
|
|
1443
|
+
options: options
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
# Store results
|
|
1447
|
+
impact_id = SecureRandom.uuid
|
|
1448
|
+
self.class.simulation_storage_mutex.synchronize do
|
|
1449
|
+
self.class.simulation_storage[impact_id] = {
|
|
1450
|
+
id: impact_id,
|
|
1451
|
+
type: "impact",
|
|
1452
|
+
status: "completed",
|
|
1453
|
+
created_at: Time.now.utc.iso8601,
|
|
1454
|
+
results: results
|
|
1455
|
+
}
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
{
|
|
1459
|
+
impact_id: impact_id,
|
|
1460
|
+
results: results
|
|
1461
|
+
}.to_json
|
|
1462
|
+
rescue StandardError => e
|
|
1463
|
+
status 500
|
|
1464
|
+
{ error: "Impact analysis failed: #{e.message}" }.to_json
|
|
1465
|
+
end
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
# POST /api/simulation/shadow - Shadow testing
|
|
1469
|
+
post "/api/simulation/shadow" do
|
|
1470
|
+
content_type :json
|
|
1471
|
+
|
|
1472
|
+
begin
|
|
1473
|
+
request_body = request.body.read
|
|
1474
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1475
|
+
|
|
1476
|
+
context = data["context"]
|
|
1477
|
+
shadow_version = data["shadow_version"]
|
|
1478
|
+
production_rules = data["production_rules"]
|
|
1479
|
+
shadow_rules = data["shadow_rules"]
|
|
1480
|
+
options = data["options"] || {}
|
|
1481
|
+
|
|
1482
|
+
unless context
|
|
1483
|
+
status 400
|
|
1484
|
+
return { error: "context is required" }.to_json
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
unless (production_rules && shadow_rules) || shadow_version
|
|
1488
|
+
status 400
|
|
1489
|
+
return { error: "Either (production_rules and shadow_rules) or shadow_version is required" }.to_json
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1492
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1493
|
+
|
|
1494
|
+
# Create production agent
|
|
1495
|
+
if production_rules
|
|
1496
|
+
prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: production_rules)
|
|
1497
|
+
production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
|
|
1498
|
+
else
|
|
1499
|
+
# Use active version
|
|
1500
|
+
active_version = version_manager.get_active_version
|
|
1501
|
+
if active_version
|
|
1502
|
+
prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: active_version[:content])
|
|
1503
|
+
production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
|
|
1504
|
+
else
|
|
1505
|
+
status 400
|
|
1506
|
+
return { error: "No active version found and production_rules not provided" }.to_json
|
|
1507
|
+
end
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
# Create shadow test engine
|
|
1511
|
+
shadow_engine = DecisionAgent::Simulation::ShadowTestEngine.new(
|
|
1512
|
+
production_agent: production_agent,
|
|
1513
|
+
version_manager: version_manager
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
# Execute shadow test
|
|
1517
|
+
if shadow_rules
|
|
1518
|
+
# Create a temporary version for shadow rules
|
|
1519
|
+
temp_version = {
|
|
1520
|
+
content: shadow_rules,
|
|
1521
|
+
rule_id: "shadow_temp",
|
|
1522
|
+
version_number: 1
|
|
1523
|
+
}
|
|
1524
|
+
result = shadow_engine.test(
|
|
1525
|
+
context: context,
|
|
1526
|
+
shadow_version: temp_version,
|
|
1527
|
+
options: options
|
|
1528
|
+
)
|
|
1529
|
+
else
|
|
1530
|
+
result = shadow_engine.test(
|
|
1531
|
+
context: context,
|
|
1532
|
+
shadow_version: shadow_version,
|
|
1533
|
+
options: options
|
|
1534
|
+
)
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
{
|
|
1538
|
+
result: result
|
|
1539
|
+
}.to_json
|
|
1540
|
+
rescue StandardError => e
|
|
1541
|
+
status 500
|
|
1542
|
+
{ error: "Shadow test failed: #{e.message}" }.to_json
|
|
1543
|
+
end
|
|
1544
|
+
end
|
|
1545
|
+
|
|
1546
|
+
# POST /api/simulation/shadow/batch - Batch shadow testing
|
|
1547
|
+
post "/api/simulation/shadow/batch" do
|
|
1548
|
+
content_type :json
|
|
1549
|
+
|
|
1550
|
+
begin
|
|
1551
|
+
request_body = request.body.read
|
|
1552
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
1553
|
+
|
|
1554
|
+
contexts = data["contexts"]
|
|
1555
|
+
shadow_version = data["shadow_version"]
|
|
1556
|
+
production_rules = data["production_rules"]
|
|
1557
|
+
shadow_rules = data["shadow_rules"]
|
|
1558
|
+
options = data["options"] || {}
|
|
1559
|
+
|
|
1560
|
+
unless contexts.is_a?(Array)
|
|
1561
|
+
status 400
|
|
1562
|
+
return { error: "contexts array is required" }.to_json
|
|
1563
|
+
end
|
|
1564
|
+
|
|
1565
|
+
unless (production_rules && shadow_rules) || shadow_version
|
|
1566
|
+
status 400
|
|
1567
|
+
return { error: "Either (production_rules and shadow_rules) or shadow_version is required" }.to_json
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1571
|
+
|
|
1572
|
+
# Create production agent
|
|
1573
|
+
if production_rules
|
|
1574
|
+
prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: production_rules)
|
|
1575
|
+
production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
|
|
1576
|
+
else
|
|
1577
|
+
# Use active version
|
|
1578
|
+
active_version = version_manager.get_active_version
|
|
1579
|
+
if active_version
|
|
1580
|
+
prod_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: active_version[:content])
|
|
1581
|
+
production_agent = DecisionAgent::Agent.new(evaluators: [prod_evaluator])
|
|
1582
|
+
else
|
|
1583
|
+
status 400
|
|
1584
|
+
return { error: "No active version found and production_rules not provided" }.to_json
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
# Create shadow test engine
|
|
1589
|
+
shadow_engine = DecisionAgent::Simulation::ShadowTestEngine.new(
|
|
1590
|
+
production_agent: production_agent,
|
|
1591
|
+
version_manager: version_manager
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
# Execute batch shadow test
|
|
1595
|
+
if shadow_rules
|
|
1596
|
+
# Create a temporary version for shadow rules
|
|
1597
|
+
temp_version = {
|
|
1598
|
+
content: shadow_rules,
|
|
1599
|
+
rule_id: "shadow_temp",
|
|
1600
|
+
version_number: 1
|
|
1601
|
+
}
|
|
1602
|
+
results = shadow_engine.batch_test(
|
|
1603
|
+
contexts: contexts,
|
|
1604
|
+
shadow_version: temp_version,
|
|
1605
|
+
options: options
|
|
1606
|
+
)
|
|
1607
|
+
else
|
|
1608
|
+
results = shadow_engine.batch_test(
|
|
1609
|
+
contexts: contexts,
|
|
1610
|
+
shadow_version: shadow_version,
|
|
1611
|
+
options: options
|
|
1612
|
+
)
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
{
|
|
1616
|
+
results: results
|
|
1617
|
+
}.to_json
|
|
1618
|
+
rescue StandardError => e
|
|
1619
|
+
status 500
|
|
1620
|
+
{ error: "Batch shadow test failed: #{e.message}" }.to_json
|
|
1621
|
+
end
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1624
|
+
# GET /api/simulation/:id - Get simulation results
|
|
1625
|
+
get "/api/simulation/:id" do
|
|
1626
|
+
content_type :json
|
|
1627
|
+
|
|
1628
|
+
begin
|
|
1629
|
+
sim_id = params[:id]
|
|
1630
|
+
|
|
1631
|
+
sim_data = nil
|
|
1632
|
+
self.class.simulation_storage_mutex.synchronize do
|
|
1633
|
+
sim_data = self.class.simulation_storage[sim_id]
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
unless sim_data
|
|
1637
|
+
status 404
|
|
1638
|
+
return { error: "Simulation not found" }.to_json
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
sim_data.to_json
|
|
1642
|
+
rescue StandardError => e
|
|
1643
|
+
status 500
|
|
1644
|
+
{ error: e.message }.to_json
|
|
1645
|
+
end
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
# GET /api/versions - List all versions (for simulation dropdowns)
|
|
1649
|
+
get "/api/versions" do
|
|
1650
|
+
content_type :json
|
|
1651
|
+
|
|
1652
|
+
begin
|
|
1653
|
+
version_manager = DecisionAgent::Versioning::VersionManager.new
|
|
1654
|
+
versions = version_manager.list_versions
|
|
1655
|
+
|
|
1656
|
+
{
|
|
1657
|
+
versions: versions.map do |v|
|
|
1658
|
+
{
|
|
1659
|
+
id: v[:id] || v["id"],
|
|
1660
|
+
rule_id: v[:rule_id] || v["rule_id"],
|
|
1661
|
+
version_number: v[:version_number] || v["version_number"],
|
|
1662
|
+
status: v[:status] || v["status"],
|
|
1663
|
+
created_at: v[:created_at] || v["created_at"]
|
|
1664
|
+
}
|
|
1665
|
+
end
|
|
1666
|
+
}.to_json
|
|
1667
|
+
rescue StandardError => e
|
|
1668
|
+
status 500
|
|
1669
|
+
{ error: e.message }.to_json
|
|
1670
|
+
end
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
# GET /simulation - Simulation dashboard UI page
|
|
1674
|
+
get "/simulation" do
|
|
1675
|
+
send_file File.join(settings.public_folder, "simulation.html")
|
|
1676
|
+
rescue StandardError
|
|
1677
|
+
status 404
|
|
1678
|
+
"Simulation page not found"
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
# GET /simulation/replay - Historical replay UI page
|
|
1682
|
+
get "/simulation/replay" do
|
|
1683
|
+
send_file File.join(settings.public_folder, "simulation_replay.html")
|
|
1684
|
+
rescue StandardError
|
|
1685
|
+
status 404
|
|
1686
|
+
"Historical replay page not found"
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
# GET /simulation/whatif - What-if analysis UI page
|
|
1690
|
+
get "/simulation/whatif" do
|
|
1691
|
+
send_file File.join(settings.public_folder, "simulation_whatif.html")
|
|
1692
|
+
rescue StandardError
|
|
1693
|
+
status 404
|
|
1694
|
+
"What-if analysis page not found"
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
# GET /simulation/impact - Impact analysis UI page
|
|
1698
|
+
get "/simulation/impact" do
|
|
1699
|
+
send_file File.join(settings.public_folder, "simulation_impact.html")
|
|
1700
|
+
rescue StandardError
|
|
1701
|
+
status 404
|
|
1702
|
+
"Impact analysis page not found"
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
# GET /simulation/shadow - Shadow testing UI page
|
|
1706
|
+
get "/simulation/shadow" do
|
|
1707
|
+
send_file File.join(settings.public_folder, "simulation_shadow.html")
|
|
1708
|
+
rescue StandardError
|
|
1709
|
+
status 404
|
|
1710
|
+
"Shadow testing page not found"
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1148
1713
|
# GET /auth/login - Login page
|
|
1149
1714
|
get "/auth/login" do
|
|
1150
1715
|
send_file File.join(settings.public_folder, "login.html")
|
|
@@ -1660,10 +2225,14 @@ module DecisionAgent
|
|
|
1660
2225
|
return true if permissions_disabled?
|
|
1661
2226
|
|
|
1662
2227
|
checker = self.class.permission_checker
|
|
1663
|
-
|
|
2228
|
+
granted = checker.can?(@current_user, permission, resource)
|
|
2229
|
+
|
|
2230
|
+
unless granted
|
|
2231
|
+
# Log the permission denial, but don't let logging failures prevent the denial
|
|
1664
2232
|
begin
|
|
2233
|
+
user_id = checker.user_id(@current_user)
|
|
1665
2234
|
self.class.access_audit_logger.log_permission_check(
|
|
1666
|
-
user_id:
|
|
2235
|
+
user_id: user_id,
|
|
1667
2236
|
permission: permission,
|
|
1668
2237
|
resource_type: resource&.class&.name,
|
|
1669
2238
|
resource_id: resource&.id,
|
|
@@ -1679,9 +2248,11 @@ module DecisionAgent
|
|
|
1679
2248
|
halt 403, { error: "Permission denied: #{permission}" }.to_json
|
|
1680
2249
|
end
|
|
1681
2250
|
|
|
2251
|
+
# Log successful permission check, but don't let logging failures prevent access
|
|
1682
2252
|
begin
|
|
2253
|
+
user_id = checker.user_id(@current_user)
|
|
1683
2254
|
self.class.access_audit_logger.log_permission_check(
|
|
1684
|
-
user_id:
|
|
2255
|
+
user_id: user_id,
|
|
1685
2256
|
permission: permission,
|
|
1686
2257
|
resource_type: resource&.class&.name,
|
|
1687
2258
|
resource_id: resource&.id,
|