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.
- checksums.yaml +4 -4
- data/README.md +313 -8
- data/bin/decision_agent +104 -0
- 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/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
- data/lib/decision_agent/dsl/schema_validator.rb +53 -14
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
- 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/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +52 -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 +86 -0
- data/lib/decision_agent/web/server.rb +1059 -23
- data/lib/decision_agent.rb +60 -2
- metadata +105 -61
- 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 -481
- 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 -550
- 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/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 -1633
- 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 -499
- data/spec/monitoring/monitored_agent_spec.rb +0 -222
- 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 -486
- 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 -482
- 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 -1840
|
@@ -12,6 +12,10 @@ require_relative "../testing/test_coverage_analyzer"
|
|
|
12
12
|
require_relative "../evaluators/json_rule_evaluator"
|
|
13
13
|
require_relative "../agent"
|
|
14
14
|
|
|
15
|
+
# DMN components
|
|
16
|
+
require_relative "../dmn/importer"
|
|
17
|
+
require_relative "../dmn/exporter"
|
|
18
|
+
|
|
15
19
|
# Auth components
|
|
16
20
|
require_relative "../auth/user"
|
|
17
21
|
require_relative "../auth/role"
|
|
@@ -24,6 +28,15 @@ require_relative "../auth/access_audit_logger"
|
|
|
24
28
|
require_relative "middleware/auth_middleware"
|
|
25
29
|
require_relative "middleware/permission_middleware"
|
|
26
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
|
+
|
|
27
40
|
module DecisionAgent
|
|
28
41
|
module Web
|
|
29
42
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -37,25 +50,27 @@ module DecisionAgent
|
|
|
37
50
|
@batch_test_storage = {}
|
|
38
51
|
@batch_test_storage_mutex = Mutex.new
|
|
39
52
|
|
|
53
|
+
# In-memory storage for simulation runs
|
|
54
|
+
@simulation_storage = {}
|
|
55
|
+
@simulation_storage_mutex = Mutex.new
|
|
56
|
+
|
|
40
57
|
# Auth components
|
|
41
58
|
@authenticator = nil
|
|
42
59
|
@permission_checker = nil
|
|
43
60
|
@access_audit_logger = nil
|
|
44
|
-
|
|
45
|
-
def self.batch_test_storage
|
|
46
|
-
@batch_test_storage ||= {}
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.batch_test_storage_mutex
|
|
50
|
-
@batch_test_storage_mutex ||= Mutex.new
|
|
51
|
-
end
|
|
61
|
+
@auth_mutex = Mutex.new
|
|
52
62
|
|
|
53
63
|
class << self
|
|
64
|
+
attr_reader :batch_test_storage, :batch_test_storage_mutex, :simulation_storage, :simulation_storage_mutex
|
|
54
65
|
attr_writer :authenticator
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
def self.authenticator
|
|
58
|
-
@authenticator
|
|
69
|
+
return @authenticator if @authenticator
|
|
70
|
+
|
|
71
|
+
@auth_mutex.synchronize do
|
|
72
|
+
@authenticator ||= Auth::Authenticator.new
|
|
73
|
+
end
|
|
59
74
|
end
|
|
60
75
|
|
|
61
76
|
class << self
|
|
@@ -63,7 +78,11 @@ module DecisionAgent
|
|
|
63
78
|
end
|
|
64
79
|
|
|
65
80
|
def self.permission_checker
|
|
66
|
-
@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
|
|
67
86
|
end
|
|
68
87
|
|
|
69
88
|
class << self
|
|
@@ -71,7 +90,11 @@ module DecisionAgent
|
|
|
71
90
|
end
|
|
72
91
|
|
|
73
92
|
def self.access_audit_logger
|
|
74
|
-
@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
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
# Enable CORS for API calls
|
|
@@ -200,19 +223,55 @@ module DecisionAgent
|
|
|
200
223
|
result = evaluator.evaluate(DecisionAgent::Context.new(context))
|
|
201
224
|
|
|
202
225
|
if result
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
211
263
|
else
|
|
212
264
|
{
|
|
213
265
|
success: true,
|
|
214
266
|
decision: nil,
|
|
215
|
-
|
|
267
|
+
because: [],
|
|
268
|
+
failed_conditions: [],
|
|
269
|
+
message: "No rules matched the given context",
|
|
270
|
+
explainability: {
|
|
271
|
+
decision: nil,
|
|
272
|
+
because: [],
|
|
273
|
+
failed_conditions: []
|
|
274
|
+
}
|
|
216
275
|
}.to_json
|
|
217
276
|
end
|
|
218
277
|
rescue StandardError => e
|
|
@@ -1141,6 +1200,516 @@ module DecisionAgent
|
|
|
1141
1200
|
"Batch testing page not found"
|
|
1142
1201
|
end
|
|
1143
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
|
+
|
|
1144
1713
|
# GET /auth/login - Login page
|
|
1145
1714
|
get "/auth/login" do
|
|
1146
1715
|
send_file File.join(settings.public_folder, "login.html")
|
|
@@ -1157,8 +1726,469 @@ module DecisionAgent
|
|
|
1157
1726
|
"User management page not found"
|
|
1158
1727
|
end
|
|
1159
1728
|
|
|
1729
|
+
# DMN Editor Routes
|
|
1730
|
+
|
|
1731
|
+
# GET /dmn/editor - DMN Editor UI page
|
|
1732
|
+
get "/dmn/editor" do
|
|
1733
|
+
send_file File.join(settings.public_folder, "dmn-editor.html")
|
|
1734
|
+
rescue StandardError
|
|
1735
|
+
status 404
|
|
1736
|
+
"DMN Editor page not found"
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
# API: List all DMN models
|
|
1740
|
+
get "/api/dmn/models" do
|
|
1741
|
+
content_type :json
|
|
1742
|
+
dmn_editor.list_models.to_json
|
|
1743
|
+
end
|
|
1744
|
+
|
|
1745
|
+
# API: Create new DMN model
|
|
1746
|
+
post "/api/dmn/models" do
|
|
1747
|
+
content_type :json
|
|
1748
|
+
|
|
1749
|
+
begin
|
|
1750
|
+
request_body = request.body.read
|
|
1751
|
+
data = JSON.parse(request_body)
|
|
1752
|
+
|
|
1753
|
+
model = dmn_editor.create_model(
|
|
1754
|
+
name: data["name"],
|
|
1755
|
+
namespace: data["namespace"]
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
status 201
|
|
1759
|
+
model.to_json
|
|
1760
|
+
rescue StandardError => e
|
|
1761
|
+
status 500
|
|
1762
|
+
{ error: e.message }.to_json
|
|
1763
|
+
end
|
|
1764
|
+
end
|
|
1765
|
+
|
|
1766
|
+
# API: Get DMN model
|
|
1767
|
+
get "/api/dmn/models/:id" do
|
|
1768
|
+
content_type :json
|
|
1769
|
+
|
|
1770
|
+
model = dmn_editor.get_model(params[:id])
|
|
1771
|
+
if model
|
|
1772
|
+
model.to_json
|
|
1773
|
+
else
|
|
1774
|
+
status 404
|
|
1775
|
+
{ error: "Model not found" }.to_json
|
|
1776
|
+
end
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
# API: Update DMN model
|
|
1780
|
+
put "/api/dmn/models/:id" do
|
|
1781
|
+
content_type :json
|
|
1782
|
+
|
|
1783
|
+
begin
|
|
1784
|
+
request_body = request.body.read
|
|
1785
|
+
data = JSON.parse(request_body)
|
|
1786
|
+
|
|
1787
|
+
model = dmn_editor.update_model(
|
|
1788
|
+
params[:id],
|
|
1789
|
+
name: data["name"],
|
|
1790
|
+
namespace: data["namespace"]
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
if model
|
|
1794
|
+
model.to_json
|
|
1795
|
+
else
|
|
1796
|
+
status 404
|
|
1797
|
+
{ error: "Model not found" }.to_json
|
|
1798
|
+
end
|
|
1799
|
+
rescue StandardError => e
|
|
1800
|
+
status 500
|
|
1801
|
+
{ error: e.message }.to_json
|
|
1802
|
+
end
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
# API: Delete DMN model
|
|
1806
|
+
delete "/api/dmn/models/:id" do
|
|
1807
|
+
content_type :json
|
|
1808
|
+
|
|
1809
|
+
result = dmn_editor.delete_model(params[:id])
|
|
1810
|
+
{ success: result }.to_json
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
# API: Add decision to model
|
|
1814
|
+
post "/api/dmn/models/:model_id/decisions" do
|
|
1815
|
+
content_type :json
|
|
1816
|
+
|
|
1817
|
+
begin
|
|
1818
|
+
request_body = request.body.read
|
|
1819
|
+
data = JSON.parse(request_body)
|
|
1820
|
+
|
|
1821
|
+
decision = dmn_editor.add_decision(
|
|
1822
|
+
model_id: params[:model_id],
|
|
1823
|
+
decision_id: data["decision_id"],
|
|
1824
|
+
name: data["name"],
|
|
1825
|
+
type: data["type"] || "decision_table"
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
if decision
|
|
1829
|
+
status 201
|
|
1830
|
+
decision.to_json
|
|
1831
|
+
else
|
|
1832
|
+
status 404
|
|
1833
|
+
{ error: "Model not found" }.to_json
|
|
1834
|
+
end
|
|
1835
|
+
rescue StandardError => e
|
|
1836
|
+
status 500
|
|
1837
|
+
{ error: e.message }.to_json
|
|
1838
|
+
end
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1841
|
+
# API: Update decision
|
|
1842
|
+
put "/api/dmn/models/:model_id/decisions/:decision_id" do
|
|
1843
|
+
content_type :json
|
|
1844
|
+
|
|
1845
|
+
begin
|
|
1846
|
+
request_body = request.body.read
|
|
1847
|
+
data = JSON.parse(request_body)
|
|
1848
|
+
|
|
1849
|
+
decision = dmn_editor.update_decision(
|
|
1850
|
+
model_id: params[:model_id],
|
|
1851
|
+
decision_id: params[:decision_id],
|
|
1852
|
+
name: data["name"],
|
|
1853
|
+
logic: data["logic"]
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
if decision
|
|
1857
|
+
decision.to_json
|
|
1858
|
+
else
|
|
1859
|
+
status 404
|
|
1860
|
+
{ error: "Decision not found" }.to_json
|
|
1861
|
+
end
|
|
1862
|
+
rescue StandardError => e
|
|
1863
|
+
status 500
|
|
1864
|
+
{ error: e.message }.to_json
|
|
1865
|
+
end
|
|
1866
|
+
end
|
|
1867
|
+
|
|
1868
|
+
# API: Delete decision
|
|
1869
|
+
delete "/api/dmn/models/:model_id/decisions/:decision_id" do
|
|
1870
|
+
content_type :json
|
|
1871
|
+
|
|
1872
|
+
result = dmn_editor.delete_decision(
|
|
1873
|
+
model_id: params[:model_id],
|
|
1874
|
+
decision_id: params[:decision_id]
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
{ success: result }.to_json
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
# API: Add input column
|
|
1881
|
+
post "/api/dmn/models/:model_id/decisions/:decision_id/inputs" do
|
|
1882
|
+
content_type :json
|
|
1883
|
+
|
|
1884
|
+
begin
|
|
1885
|
+
request_body = request.body.read
|
|
1886
|
+
data = JSON.parse(request_body)
|
|
1887
|
+
|
|
1888
|
+
input = dmn_editor.add_input(
|
|
1889
|
+
model_id: params[:model_id],
|
|
1890
|
+
decision_id: params[:decision_id],
|
|
1891
|
+
input_id: data["input_id"],
|
|
1892
|
+
label: data["label"],
|
|
1893
|
+
type_ref: data["type_ref"],
|
|
1894
|
+
expression: data["expression"]
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
if input
|
|
1898
|
+
status 201
|
|
1899
|
+
input.to_json
|
|
1900
|
+
else
|
|
1901
|
+
status 404
|
|
1902
|
+
{ error: "Decision not found" }.to_json
|
|
1903
|
+
end
|
|
1904
|
+
rescue StandardError => e
|
|
1905
|
+
status 500
|
|
1906
|
+
{ error: e.message }.to_json
|
|
1907
|
+
end
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# API: Add output column
|
|
1911
|
+
post "/api/dmn/models/:model_id/decisions/:decision_id/outputs" do
|
|
1912
|
+
content_type :json
|
|
1913
|
+
|
|
1914
|
+
begin
|
|
1915
|
+
request_body = request.body.read
|
|
1916
|
+
data = JSON.parse(request_body)
|
|
1917
|
+
|
|
1918
|
+
output = dmn_editor.add_output(
|
|
1919
|
+
model_id: params[:model_id],
|
|
1920
|
+
decision_id: params[:decision_id],
|
|
1921
|
+
output_id: data["output_id"],
|
|
1922
|
+
label: data["label"],
|
|
1923
|
+
type_ref: data["type_ref"],
|
|
1924
|
+
name: data["name"]
|
|
1925
|
+
)
|
|
1926
|
+
|
|
1927
|
+
if output
|
|
1928
|
+
status 201
|
|
1929
|
+
output.to_json
|
|
1930
|
+
else
|
|
1931
|
+
status 404
|
|
1932
|
+
{ error: "Decision not found" }.to_json
|
|
1933
|
+
end
|
|
1934
|
+
rescue StandardError => e
|
|
1935
|
+
status 500
|
|
1936
|
+
{ error: e.message }.to_json
|
|
1937
|
+
end
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
# API: Add rule
|
|
1941
|
+
post "/api/dmn/models/:model_id/decisions/:decision_id/rules" do
|
|
1942
|
+
content_type :json
|
|
1943
|
+
|
|
1944
|
+
begin
|
|
1945
|
+
request_body = request.body.read
|
|
1946
|
+
data = JSON.parse(request_body)
|
|
1947
|
+
|
|
1948
|
+
rule = dmn_editor.add_rule(
|
|
1949
|
+
model_id: params[:model_id],
|
|
1950
|
+
decision_id: params[:decision_id],
|
|
1951
|
+
rule_id: data["rule_id"],
|
|
1952
|
+
input_entries: data["input_entries"],
|
|
1953
|
+
output_entries: data["output_entries"],
|
|
1954
|
+
description: data["description"]
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
if rule
|
|
1958
|
+
status 201
|
|
1959
|
+
rule.to_json
|
|
1960
|
+
else
|
|
1961
|
+
status 404
|
|
1962
|
+
{ error: "Decision not found" }.to_json
|
|
1963
|
+
end
|
|
1964
|
+
rescue StandardError => e
|
|
1965
|
+
status 500
|
|
1966
|
+
{ error: e.message }.to_json
|
|
1967
|
+
end
|
|
1968
|
+
end
|
|
1969
|
+
|
|
1970
|
+
# API: Update rule
|
|
1971
|
+
put "/api/dmn/models/:model_id/decisions/:decision_id/rules/:rule_id" do
|
|
1972
|
+
content_type :json
|
|
1973
|
+
|
|
1974
|
+
begin
|
|
1975
|
+
request_body = request.body.read
|
|
1976
|
+
data = JSON.parse(request_body)
|
|
1977
|
+
|
|
1978
|
+
rule = dmn_editor.update_rule(
|
|
1979
|
+
model_id: params[:model_id],
|
|
1980
|
+
decision_id: params[:decision_id],
|
|
1981
|
+
rule_id: params[:rule_id],
|
|
1982
|
+
input_entries: data["input_entries"],
|
|
1983
|
+
output_entries: data["output_entries"],
|
|
1984
|
+
description: data["description"]
|
|
1985
|
+
)
|
|
1986
|
+
|
|
1987
|
+
if rule
|
|
1988
|
+
rule.to_json
|
|
1989
|
+
else
|
|
1990
|
+
status 404
|
|
1991
|
+
{ error: "Rule not found" }.to_json
|
|
1992
|
+
end
|
|
1993
|
+
rescue StandardError => e
|
|
1994
|
+
status 500
|
|
1995
|
+
{ error: e.message }.to_json
|
|
1996
|
+
end
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1999
|
+
# API: Delete rule
|
|
2000
|
+
delete "/api/dmn/models/:model_id/decisions/:decision_id/rules/:rule_id" do
|
|
2001
|
+
content_type :json
|
|
2002
|
+
|
|
2003
|
+
result = dmn_editor.delete_rule(
|
|
2004
|
+
model_id: params[:model_id],
|
|
2005
|
+
decision_id: params[:decision_id],
|
|
2006
|
+
rule_id: params[:rule_id]
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
{ success: result }.to_json
|
|
2010
|
+
end
|
|
2011
|
+
|
|
2012
|
+
# API: Validate DMN model
|
|
2013
|
+
get "/api/dmn/models/:id/validate" do
|
|
2014
|
+
content_type :json
|
|
2015
|
+
dmn_editor.validate_model(params[:id]).to_json
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
# API: Export DMN model to XML
|
|
2019
|
+
get "/api/dmn/models/:id/export" do
|
|
2020
|
+
content_type :xml
|
|
2021
|
+
|
|
2022
|
+
xml = dmn_editor.export_to_xml(params[:id])
|
|
2023
|
+
if xml
|
|
2024
|
+
xml
|
|
2025
|
+
else
|
|
2026
|
+
status 404
|
|
2027
|
+
"Model not found"
|
|
2028
|
+
end
|
|
2029
|
+
end
|
|
2030
|
+
|
|
2031
|
+
# API: Import DMN model from XML
|
|
2032
|
+
post "/api/dmn/models/import" do
|
|
2033
|
+
content_type :json
|
|
2034
|
+
|
|
2035
|
+
begin
|
|
2036
|
+
request_body = request.body.read
|
|
2037
|
+
data = JSON.parse(request_body)
|
|
2038
|
+
|
|
2039
|
+
model = dmn_editor.import_from_xml(data["xml"], name: data["name"])
|
|
2040
|
+
|
|
2041
|
+
if model
|
|
2042
|
+
status 201
|
|
2043
|
+
model.to_json
|
|
2044
|
+
else
|
|
2045
|
+
status 400
|
|
2046
|
+
{ error: "Failed to import model" }.to_json
|
|
2047
|
+
end
|
|
2048
|
+
rescue StandardError => e
|
|
2049
|
+
status 500
|
|
2050
|
+
{ error: e.message }.to_json
|
|
2051
|
+
end
|
|
2052
|
+
end
|
|
2053
|
+
|
|
2054
|
+
# API: Visualize decision tree
|
|
2055
|
+
get "/api/dmn/models/:model_id/decisions/:decision_id/visualize/tree" do
|
|
2056
|
+
format = params[:format] || "svg"
|
|
2057
|
+
|
|
2058
|
+
visualization = dmn_editor.visualize_tree(
|
|
2059
|
+
model_id: params[:model_id],
|
|
2060
|
+
decision_id: params[:decision_id],
|
|
2061
|
+
format: format
|
|
2062
|
+
)
|
|
2063
|
+
|
|
2064
|
+
if visualization
|
|
2065
|
+
content_type format == "svg" ? "image/svg+xml" : :text
|
|
2066
|
+
visualization
|
|
2067
|
+
else
|
|
2068
|
+
status 404
|
|
2069
|
+
"Decision not found or not a tree"
|
|
2070
|
+
end
|
|
2071
|
+
end
|
|
2072
|
+
|
|
2073
|
+
# API: Visualize decision graph
|
|
2074
|
+
get "/api/dmn/models/:id/visualize/graph" do
|
|
2075
|
+
format = params[:format] || "svg"
|
|
2076
|
+
|
|
2077
|
+
visualization = dmn_editor.visualize_graph(
|
|
2078
|
+
model_id: params[:id],
|
|
2079
|
+
format: format
|
|
2080
|
+
)
|
|
2081
|
+
|
|
2082
|
+
if visualization
|
|
2083
|
+
content_type format == "svg" ? "image/svg+xml" : :text
|
|
2084
|
+
visualization
|
|
2085
|
+
else
|
|
2086
|
+
status 404
|
|
2087
|
+
"Model not found"
|
|
2088
|
+
end
|
|
2089
|
+
end
|
|
2090
|
+
|
|
2091
|
+
# API: Import DMN file (uploads and imports to versioning system)
|
|
2092
|
+
post "/api/dmn/import" do
|
|
2093
|
+
content_type :json
|
|
2094
|
+
|
|
2095
|
+
begin
|
|
2096
|
+
# Check if request has multipart form data (file upload)
|
|
2097
|
+
if params[:file] && params[:file][:tempfile]
|
|
2098
|
+
# File upload
|
|
2099
|
+
file = params[:file][:tempfile]
|
|
2100
|
+
xml_content = file.read
|
|
2101
|
+
ruleset_name = params[:ruleset_name] || params[:file][:filename]&.gsub(/\.dmn$/i, "")
|
|
2102
|
+
created_by = @current_user ? @current_user.id.to_s : params[:created_by] || "system"
|
|
2103
|
+
elsif request.content_type&.include?("application/json")
|
|
2104
|
+
# JSON body with XML content
|
|
2105
|
+
request_body = request.body.read
|
|
2106
|
+
data = JSON.parse(request_body)
|
|
2107
|
+
xml_content = data["xml"] || data["content"]
|
|
2108
|
+
ruleset_name = data["ruleset_name"] || data["name"]
|
|
2109
|
+
created_by = @current_user ? @current_user.id.to_s : data["created_by"] || "system"
|
|
2110
|
+
elsif request.content_type&.include?("application/xml") || request.content_type&.include?("text/xml")
|
|
2111
|
+
# Direct XML upload
|
|
2112
|
+
xml_content = request.body.read
|
|
2113
|
+
ruleset_name = params[:ruleset_name] || "imported_dmn"
|
|
2114
|
+
created_by = @current_user ? @current_user.id.to_s : params[:created_by] || "system"
|
|
2115
|
+
else
|
|
2116
|
+
status 400
|
|
2117
|
+
return { error: "Invalid request. Expected file upload, JSON with 'xml' field, or XML content." }.to_json
|
|
2118
|
+
end
|
|
2119
|
+
|
|
2120
|
+
raise ArgumentError, "DMN XML content is required" if xml_content.nil? || xml_content.strip.empty?
|
|
2121
|
+
|
|
2122
|
+
# Import using DMN Importer
|
|
2123
|
+
importer = Dmn::Importer.new(version_manager: version_manager)
|
|
2124
|
+
result = importer.import_from_xml(
|
|
2125
|
+
xml_content,
|
|
2126
|
+
ruleset_name: ruleset_name,
|
|
2127
|
+
created_by: created_by
|
|
2128
|
+
)
|
|
2129
|
+
|
|
2130
|
+
status 201
|
|
2131
|
+
{
|
|
2132
|
+
success: true,
|
|
2133
|
+
ruleset_name: ruleset_name,
|
|
2134
|
+
decisions_imported: result[:decisions_imported],
|
|
2135
|
+
model: {
|
|
2136
|
+
id: result[:model].id,
|
|
2137
|
+
name: result[:model].name,
|
|
2138
|
+
namespace: result[:model].namespace,
|
|
2139
|
+
decisions: result[:model].decisions.map do |d|
|
|
2140
|
+
{
|
|
2141
|
+
id: d.id,
|
|
2142
|
+
name: d.name
|
|
2143
|
+
}
|
|
2144
|
+
end
|
|
2145
|
+
},
|
|
2146
|
+
versions: result[:versions].map do |v|
|
|
2147
|
+
{
|
|
2148
|
+
version: v[:version],
|
|
2149
|
+
rule_id: v[:rule_id],
|
|
2150
|
+
created_by: v[:created_by],
|
|
2151
|
+
created_at: v[:created_at]
|
|
2152
|
+
}
|
|
2153
|
+
end
|
|
2154
|
+
}.to_json
|
|
2155
|
+
rescue Dmn::InvalidDmnModelError, Dmn::DmnParseError => e
|
|
2156
|
+
status 400
|
|
2157
|
+
{ error: "DMN validation error", message: e.message }.to_json
|
|
2158
|
+
rescue StandardError => e
|
|
2159
|
+
status 500
|
|
2160
|
+
{ error: "Import failed", message: e.message }.to_json
|
|
2161
|
+
end
|
|
2162
|
+
end
|
|
2163
|
+
|
|
2164
|
+
# API: Export ruleset as DMN XML
|
|
2165
|
+
get "/api/dmn/export/:ruleset_id" do
|
|
2166
|
+
content_type :xml
|
|
2167
|
+
|
|
2168
|
+
begin
|
|
2169
|
+
ruleset_id = params[:ruleset_id]
|
|
2170
|
+
exporter = Dmn::Exporter.new(version_manager: version_manager)
|
|
2171
|
+
dmn_xml = exporter.export(ruleset_id)
|
|
2172
|
+
|
|
2173
|
+
headers["Content-Disposition"] = "attachment; filename=\"#{ruleset_id}.dmn\""
|
|
2174
|
+
dmn_xml
|
|
2175
|
+
rescue Dmn::InvalidDmnModelError => e
|
|
2176
|
+
status 404
|
|
2177
|
+
content_type :json
|
|
2178
|
+
{ error: "Ruleset not found", message: e.message }.to_json
|
|
2179
|
+
rescue StandardError => e
|
|
2180
|
+
status 500
|
|
2181
|
+
content_type :json
|
|
2182
|
+
{ error: "Export failed", message: e.message }.to_json
|
|
2183
|
+
end
|
|
2184
|
+
end
|
|
2185
|
+
|
|
1160
2186
|
private
|
|
1161
2187
|
|
|
2188
|
+
def dmn_editor
|
|
2189
|
+
@dmn_editor ||= DmnEditor.new
|
|
2190
|
+
end
|
|
2191
|
+
|
|
1162
2192
|
def version_manager
|
|
1163
2193
|
@version_manager ||= DecisionAgent::Versioning::VersionManager.new
|
|
1164
2194
|
end
|
|
@@ -1195,10 +2225,14 @@ module DecisionAgent
|
|
|
1195
2225
|
return true if permissions_disabled?
|
|
1196
2226
|
|
|
1197
2227
|
checker = self.class.permission_checker
|
|
1198
|
-
|
|
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
|
|
1199
2232
|
begin
|
|
2233
|
+
user_id = checker.user_id(@current_user)
|
|
1200
2234
|
self.class.access_audit_logger.log_permission_check(
|
|
1201
|
-
user_id:
|
|
2235
|
+
user_id: user_id,
|
|
1202
2236
|
permission: permission,
|
|
1203
2237
|
resource_type: resource&.class&.name,
|
|
1204
2238
|
resource_id: resource&.id,
|
|
@@ -1214,9 +2248,11 @@ module DecisionAgent
|
|
|
1214
2248
|
halt 403, { error: "Permission denied: #{permission}" }.to_json
|
|
1215
2249
|
end
|
|
1216
2250
|
|
|
2251
|
+
# Log successful permission check, but don't let logging failures prevent access
|
|
1217
2252
|
begin
|
|
2253
|
+
user_id = checker.user_id(@current_user)
|
|
1218
2254
|
self.class.access_audit_logger.log_permission_check(
|
|
1219
|
-
user_id:
|
|
2255
|
+
user_id: user_id,
|
|
1220
2256
|
permission: permission,
|
|
1221
2257
|
resource_type: resource&.class&.name,
|
|
1222
2258
|
resource_id: resource&.id,
|