decision_agent 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +0 -0
- data/README.md +64 -108
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -16
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +49 -51
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +13 -0
- data/lib/decision_agent/decision.rb +11 -2
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +43 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +102 -112
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -6
- data/lib/decision_agent/dmn/validator.rb +8 -10
- data/lib/decision_agent/dmn/versioning.rb +41 -15
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +197 -1473
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +9 -24
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluation_validator.rb +0 -0
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +2 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +28 -41
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +2 -0
- data/lib/decision_agent/explainability/explainability_result.rb +2 -4
- data/lib/decision_agent/explainability/rule_trace.rb +2 -0
- data/lib/decision_agent/explainability/trace_collector.rb +2 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +2 -15
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +0 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +0 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +0 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +0 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +0 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +1 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +2 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +3 -3
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +11 -10
- data/lib/decision_agent/simulation/replay_engine.rb +3 -3
- data/lib/decision_agent/simulation/scenario_engine.rb +3 -1
- data/lib/decision_agent/simulation/scenario_library.rb +2 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +3 -16
- data/lib/decision_agent/simulation/what_if_analyzer.rb +17 -13
- data/lib/decision_agent/simulation.rb +2 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -6
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -4
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +56 -63
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +159 -47
- data/lib/decision_agent/versioning/adapter.rb +42 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -13
- data/lib/decision_agent/versioning/version_manager.rb +49 -2
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -73
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +67 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.css +0 -0
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +79 -8
- data/lib/decision_agent/web/public/index.html +20 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +23 -7
- data/lib/decision_agent/web/public/simulation_impact.html +37 -20
- data/lib/decision_agent/web/public/simulation_replay.html +19 -23
- data/lib/decision_agent/web/public/simulation_shadow.html +36 -21
- data/lib/decision_agent/web/public/simulation_whatif.html +38 -21
- data/lib/decision_agent/web/public/styles.css +0 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2038 -1851
- data/lib/decision_agent.rb +3 -43
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/README +0 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +0 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +0 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +0 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +16 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +0 -2
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +0 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version_tag.rb +23 -0
- data/lib/generators/decision_agent/install/templates/versioning_migration.rb +44 -0
- data/lib/generators/decision_agent/monitoring_migration/monitoring_migration_generator.rb +67 -0
- data/lib/generators/decision_agent/versioning_migration/versioning_migration_generator.rb +57 -0
- metadata +66 -25
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +0 -86
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +0 -49
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +0 -135
- data/lib/decision_agent/data_enrichment/client.rb +0 -220
- data/lib/decision_agent/data_enrichment/config.rb +0 -78
- data/lib/decision_agent/data_enrichment/errors.rb +0 -36
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Historical Replay / Backtesting</title>
|
|
7
|
-
<link rel="stylesheet" href="styles.css">
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
8
|
<style>
|
|
9
9
|
.simulation-container {
|
|
10
10
|
max-width: 1400px;
|
|
@@ -150,7 +150,12 @@
|
|
|
150
150
|
<h2>Upload Historical Data</h2>
|
|
151
151
|
</div>
|
|
152
152
|
<p>Upload a CSV or JSON file with historical decision contexts. Each row should contain the context fields used in your rules.</p>
|
|
153
|
-
|
|
153
|
+
<p style="margin-top: 10px;">
|
|
154
|
+
<strong>Need a template?</strong> Download sample files:
|
|
155
|
+
<a href="/sample_replay.csv" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample CSV</a> |
|
|
156
|
+
<a href="/sample_rules.json" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample Rules</a>
|
|
157
|
+
</p>
|
|
158
|
+
|
|
154
159
|
<div class="file-upload-area" id="uploadArea">
|
|
155
160
|
<p>📁 Drag and drop your CSV/JSON file here, or click to browse</p>
|
|
156
161
|
<input type="file" id="fileInput" accept=".csv,.json" style="display: none;">
|
|
@@ -255,39 +260,30 @@
|
|
|
255
260
|
try {
|
|
256
261
|
const baseUrl = new URL(baseTag.href, window.location.href);
|
|
257
262
|
let path = baseUrl.pathname;
|
|
258
|
-
if (path && !path.endsWith('/'))
|
|
259
|
-
path += '/';
|
|
260
|
-
}
|
|
263
|
+
if (path && !path.endsWith('/')) path += '/';
|
|
261
264
|
return path;
|
|
262
265
|
} catch (e) {
|
|
263
|
-
|
|
264
|
-
const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
|
|
265
|
-
if (match && match[2]) {
|
|
266
|
-
return match[2].endsWith('/') ? match[2] : match[2] + '/';
|
|
267
|
-
}
|
|
268
|
-
} else if (baseTag.href.startsWith('./')) {
|
|
269
|
-
const pathname = window.location.pathname;
|
|
270
|
-
return pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
271
|
-
}
|
|
266
|
+
// fallback
|
|
272
267
|
}
|
|
273
268
|
}
|
|
274
269
|
const pathname = window.location.pathname;
|
|
275
|
-
if (pathname.includes('/decision_agent')) {
|
|
276
|
-
const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
|
|
277
|
-
if (match) {
|
|
278
|
-
return match[1].endsWith('/') ? match[1] : match[1] + '/';
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
270
|
const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
282
271
|
return dirPath || '/';
|
|
283
272
|
}
|
|
284
273
|
|
|
274
|
+
function getAuthHeaders() {
|
|
275
|
+
const token = localStorage.getItem('auth_token');
|
|
276
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
277
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
278
|
+
return headers;
|
|
279
|
+
}
|
|
280
|
+
|
|
285
281
|
const basePath = getBasePath();
|
|
286
282
|
let historicalData = null;
|
|
287
283
|
|
|
288
284
|
// Load versions
|
|
289
285
|
function loadVersions() {
|
|
290
|
-
fetch(`${basePath}api/versions
|
|
286
|
+
fetch(`${basePath}api/versions`, { headers: getAuthHeaders() })
|
|
291
287
|
.then(response => response.json())
|
|
292
288
|
.then(data => {
|
|
293
289
|
const ruleVersionSelect = document.getElementById('ruleVersion');
|
|
@@ -399,7 +395,7 @@
|
|
|
399
395
|
JSON.parse(rulesJson);
|
|
400
396
|
fetch(`${basePath}api/validate`, {
|
|
401
397
|
method: 'POST',
|
|
402
|
-
headers:
|
|
398
|
+
headers: getAuthHeaders(),
|
|
403
399
|
body: JSON.stringify(JSON.parse(rulesJson))
|
|
404
400
|
})
|
|
405
401
|
.then(response => response.json())
|
|
@@ -457,7 +453,7 @@
|
|
|
457
453
|
|
|
458
454
|
fetch(`${basePath}api/simulation/replay`, {
|
|
459
455
|
method: 'POST',
|
|
460
|
-
headers:
|
|
456
|
+
headers: getAuthHeaders(),
|
|
461
457
|
body: JSON.stringify(requestData)
|
|
462
458
|
})
|
|
463
459
|
.then(response => response.json())
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Shadow Testing</title>
|
|
7
|
-
<link rel="stylesheet" href="styles.css">
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
8
|
<style>
|
|
9
9
|
.simulation-container {
|
|
10
10
|
max-width: 1400px;
|
|
@@ -155,7 +155,12 @@
|
|
|
155
155
|
<div class="step-section">
|
|
156
156
|
<h2>Production Rules</h2>
|
|
157
157
|
<p>Configure the production rules (or use active version)</p>
|
|
158
|
-
|
|
158
|
+
<p style="margin-top: 10px;">
|
|
159
|
+
<strong>Need a template?</strong> Download sample files:
|
|
160
|
+
<a href="/sample_shadow.csv" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample CSV</a> |
|
|
161
|
+
<a href="/sample_rules.json" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample Rules</a>
|
|
162
|
+
</p>
|
|
163
|
+
|
|
159
164
|
<div class="form-group">
|
|
160
165
|
<label for="productionRulesJson">Production Rules JSON:</label>
|
|
161
166
|
<textarea id="productionRulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
@@ -258,6 +263,7 @@
|
|
|
258
263
|
</div>
|
|
259
264
|
|
|
260
265
|
<script>
|
|
266
|
+
// Helper function to get the base path for API calls
|
|
261
267
|
function getBasePath() {
|
|
262
268
|
const baseTag = document.querySelector('base');
|
|
263
269
|
if (baseTag && baseTag.href) {
|
|
@@ -267,30 +273,26 @@
|
|
|
267
273
|
if (path && !path.endsWith('/')) path += '/';
|
|
268
274
|
return path;
|
|
269
275
|
} catch (e) {
|
|
270
|
-
|
|
271
|
-
const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
|
|
272
|
-
if (match && match[2]) {
|
|
273
|
-
return match[2].endsWith('/') ? match[2] : match[2] + '/';
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
+
// fallback
|
|
276
277
|
}
|
|
277
278
|
}
|
|
278
279
|
const pathname = window.location.pathname;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
281
|
+
return dirPath || '/';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getAuthHeaders() {
|
|
285
|
+
const token = localStorage.getItem('auth_token');
|
|
286
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
287
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
288
|
+
return headers;
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
const basePath = getBasePath();
|
|
289
|
-
let contextCount = 1;
|
|
290
292
|
|
|
291
293
|
// Load versions
|
|
292
294
|
function loadVersions() {
|
|
293
|
-
fetch(`${basePath}api/versions
|
|
295
|
+
fetch(`${basePath}api/versions`, { headers: getAuthHeaders() })
|
|
294
296
|
.then(response => response.json())
|
|
295
297
|
.then(data => {
|
|
296
298
|
const select = document.getElementById('shadowVersion');
|
|
@@ -311,16 +313,18 @@
|
|
|
311
313
|
|
|
312
314
|
// Add context
|
|
313
315
|
document.getElementById('addContextBtn').addEventListener('click', () => {
|
|
314
|
-
|
|
316
|
+
const existingContexts = document.querySelectorAll('.context-item');
|
|
317
|
+
const nextNumber = existingContexts.length + 1;
|
|
318
|
+
|
|
315
319
|
const container = document.getElementById('contextsContainer');
|
|
316
320
|
const context = document.createElement('div');
|
|
317
321
|
context.className = 'context-item';
|
|
318
322
|
context.innerHTML = `
|
|
319
323
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
320
|
-
<strong>Context ${
|
|
324
|
+
<strong class="context-label">Context ${nextNumber}</strong>
|
|
321
325
|
<button class="btn btn-secondary" onclick="removeContext(this)" style="padding: 5px 10px;">Remove</button>
|
|
322
326
|
</div>
|
|
323
|
-
<div class="context-fields"
|
|
327
|
+
<div class="context-fields">
|
|
324
328
|
<div class="field-input">
|
|
325
329
|
<label>Field Name</label>
|
|
326
330
|
<input type="text" class="input context-field-name" placeholder="e.g., credit_score">
|
|
@@ -337,6 +341,17 @@
|
|
|
337
341
|
|
|
338
342
|
function removeContext(btn) {
|
|
339
343
|
btn.closest('.context-item').remove();
|
|
344
|
+
updateContextLabels();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function updateContextLabels() {
|
|
348
|
+
const contexts = document.querySelectorAll('.context-item');
|
|
349
|
+
contexts.forEach((context, index) => {
|
|
350
|
+
const label = context.querySelector('.context-label');
|
|
351
|
+
if (label) {
|
|
352
|
+
label.textContent = `Context ${index + 1}`;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
340
355
|
}
|
|
341
356
|
|
|
342
357
|
function addField(btn) {
|
|
@@ -441,7 +456,7 @@
|
|
|
441
456
|
|
|
442
457
|
fetch(endpoint, {
|
|
443
458
|
method: 'POST',
|
|
444
|
-
headers:
|
|
459
|
+
headers: getAuthHeaders(),
|
|
445
460
|
body: JSON.stringify(requestData)
|
|
446
461
|
})
|
|
447
462
|
.then(response => response.json())
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>What-If Analysis</title>
|
|
7
|
-
<link rel="stylesheet" href="styles.css">
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
8
|
<style>
|
|
9
9
|
.simulation-container {
|
|
10
10
|
max-width: 1400px;
|
|
@@ -102,13 +102,27 @@
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
.header-links {
|
|
105
|
-
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: 1rem;
|
|
107
|
+
align-items: center;
|
|
108
|
+
margin-top: 1.5rem;
|
|
109
|
+
padding-top: 1rem;
|
|
110
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
.header-links a {
|
|
109
|
-
|
|
114
|
+
padding: 0.5rem 1rem;
|
|
115
|
+
border-radius: 6px;
|
|
110
116
|
text-decoration: none;
|
|
111
|
-
color: var(--primary
|
|
117
|
+
color: var(--text-primary);
|
|
118
|
+
background: rgba(255, 255, 255, 0.05);
|
|
119
|
+
transition: all 0.2s;
|
|
120
|
+
font-size: 0.9rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.header-links a:hover {
|
|
124
|
+
background: rgba(255, 255, 255, 0.1);
|
|
125
|
+
transform: translateY(-1px);
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
.results-table {
|
|
@@ -144,7 +158,12 @@
|
|
|
144
158
|
<div class="step-section">
|
|
145
159
|
<h2>Configure Rules</h2>
|
|
146
160
|
<p>Paste your rules JSON or select a version to use</p>
|
|
147
|
-
|
|
161
|
+
<p style="margin-top: 10px;">
|
|
162
|
+
<strong>Need a template?</strong> Download sample files:
|
|
163
|
+
<a href="/sample_whatif.csv" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample Scenarios CSV</a> |
|
|
164
|
+
<a href="/sample_rules.json" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample Rules</a>
|
|
165
|
+
</p>
|
|
166
|
+
|
|
148
167
|
<div class="form-group">
|
|
149
168
|
<label for="rulesJson">Rules JSON:</label>
|
|
150
169
|
<textarea id="rulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
@@ -248,6 +267,7 @@
|
|
|
248
267
|
</div>
|
|
249
268
|
|
|
250
269
|
<script>
|
|
270
|
+
// Helper function to get the base path for API calls
|
|
251
271
|
function getBasePath() {
|
|
252
272
|
const baseTag = document.querySelector('base');
|
|
253
273
|
if (baseTag && baseTag.href) {
|
|
@@ -257,22 +277,19 @@
|
|
|
257
277
|
if (path && !path.endsWith('/')) path += '/';
|
|
258
278
|
return path;
|
|
259
279
|
} catch (e) {
|
|
260
|
-
|
|
261
|
-
const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
|
|
262
|
-
if (match && match[2]) {
|
|
263
|
-
return match[2].endsWith('/') ? match[2] : match[2] + '/';
|
|
264
|
-
}
|
|
265
|
-
}
|
|
280
|
+
// fallback
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
283
|
const pathname = window.location.pathname;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
284
|
+
const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
285
|
+
return dirPath || '/';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getAuthHeaders() {
|
|
289
|
+
const token = localStorage.getItem('auth_token');
|
|
290
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
291
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
292
|
+
return headers;
|
|
276
293
|
}
|
|
277
294
|
|
|
278
295
|
const basePath = getBasePath();
|
|
@@ -280,7 +297,7 @@
|
|
|
280
297
|
|
|
281
298
|
// Load versions
|
|
282
299
|
function loadVersions() {
|
|
283
|
-
fetch(`${basePath}api/versions
|
|
300
|
+
fetch(`${basePath}api/versions`, { headers: getAuthHeaders() })
|
|
284
301
|
.then(response => response.json())
|
|
285
302
|
.then(data => {
|
|
286
303
|
const select = document.getElementById('ruleVersion');
|
|
@@ -366,7 +383,7 @@
|
|
|
366
383
|
JSON.parse(rulesJson);
|
|
367
384
|
fetch(`${basePath}api/validate`, {
|
|
368
385
|
method: 'POST',
|
|
369
|
-
headers:
|
|
386
|
+
headers: getAuthHeaders(),
|
|
370
387
|
body: JSON.stringify(JSON.parse(rulesJson))
|
|
371
388
|
})
|
|
372
389
|
.then(response => response.json())
|
|
@@ -442,7 +459,7 @@
|
|
|
442
459
|
|
|
443
460
|
fetch(`${basePath}api/simulation/whatif`, {
|
|
444
461
|
method: 'POST',
|
|
445
|
-
headers:
|
|
462
|
+
headers: getAuthHeaders(),
|
|
446
463
|
body: JSON.stringify(requestData)
|
|
447
464
|
})
|
|
448
465
|
.then(response => response.json())
|
|
File without changes
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>User Management - DecisionAgent</title>
|
|
7
|
-
<link rel="stylesheet" href="styles.css">
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
8
|
<style>
|
|
9
9
|
.users-container {
|
|
10
10
|
max-width: 1200px;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rack helpers for framework-agnostic web server
|
|
4
|
+
# These helpers provide convenience methods for pure Rack applications
|
|
5
|
+
|
|
6
|
+
require "rack"
|
|
7
|
+
require "rack/file"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module DecisionAgent
|
|
11
|
+
module Web
|
|
12
|
+
module RackHelpers
|
|
13
|
+
# Simple router for Rack applications
|
|
14
|
+
class Router
|
|
15
|
+
def initialize
|
|
16
|
+
@routes = []
|
|
17
|
+
@before_filters = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(path, &block)
|
|
21
|
+
add_route("GET", path, block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def post(path, &block)
|
|
25
|
+
add_route("POST", path, block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def put(path, &block)
|
|
29
|
+
add_route("PUT", path, block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(path, &block)
|
|
33
|
+
add_route("DELETE", path, block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def options(path, &block)
|
|
37
|
+
add_route("OPTIONS", path, block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def before(&block)
|
|
41
|
+
@before_filters << block if block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def match(env)
|
|
45
|
+
method = env["REQUEST_METHOD"]
|
|
46
|
+
path = env["PATH_INFO"] || "/"
|
|
47
|
+
script_name = env["SCRIPT_NAME"] || ""
|
|
48
|
+
|
|
49
|
+
# Remove script_name prefix if present
|
|
50
|
+
path = path[script_name.length..] || "/" if script_name && !script_name.empty? && path.start_with?(script_name)
|
|
51
|
+
|
|
52
|
+
route = find_route(method, path)
|
|
53
|
+
return nil unless route
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
handler: route[:handler],
|
|
57
|
+
params: route[:params],
|
|
58
|
+
before_filters: @before_filters
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def add_route(method, path_pattern, handler)
|
|
65
|
+
# Convert path patterns to regex
|
|
66
|
+
# Example: "/api/versions/:id" -> /^\/api\/versions\/(?<id>[^\/]+)$/
|
|
67
|
+
# Handle wildcard "*" for catch-all routes
|
|
68
|
+
regex_pattern = if path_pattern == "*"
|
|
69
|
+
".*"
|
|
70
|
+
else
|
|
71
|
+
path_pattern
|
|
72
|
+
.gsub(%r{:[^/]+}) { |match| "(?<#{match[1..]}>[^/]+)" }
|
|
73
|
+
.gsub("*", ".*")
|
|
74
|
+
end
|
|
75
|
+
regex = /^#{regex_pattern}$/
|
|
76
|
+
|
|
77
|
+
@routes << {
|
|
78
|
+
method: method,
|
|
79
|
+
pattern: regex,
|
|
80
|
+
handler: handler,
|
|
81
|
+
path_pattern: path_pattern
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def find_route(method, path)
|
|
86
|
+
# Try exact match first, then try routes in order
|
|
87
|
+
# More specific routes should be registered first
|
|
88
|
+
@routes.each do |route|
|
|
89
|
+
next unless [method, "*"].include?(route[:method])
|
|
90
|
+
|
|
91
|
+
match = route[:pattern].match(path)
|
|
92
|
+
next unless match
|
|
93
|
+
|
|
94
|
+
params = match.named_captures || {}
|
|
95
|
+
params.transform_keys!(&:to_sym) if params.any?
|
|
96
|
+
return {
|
|
97
|
+
handler: route[:handler],
|
|
98
|
+
params: params
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Request/Response helpers for Rack applications
|
|
4
|
+
# Provides convenience methods for handling requests and responses
|
|
5
|
+
|
|
6
|
+
require "rack"
|
|
7
|
+
require "rack/utils"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module DecisionAgent
|
|
11
|
+
module Web
|
|
12
|
+
module RackRequestHelpers
|
|
13
|
+
# Context object that provides convenience methods in route handlers
|
|
14
|
+
class RequestContext
|
|
15
|
+
attr_reader :env, :request, :response_status, :response_headers, :response_body, :halted_response
|
|
16
|
+
attr_accessor :current_user, :current_session
|
|
17
|
+
|
|
18
|
+
def initialize(env, route_params = {})
|
|
19
|
+
@env = env
|
|
20
|
+
@request = Rack::Request.new(env)
|
|
21
|
+
@route_params = route_params
|
|
22
|
+
@response_status = 200
|
|
23
|
+
@response_headers = { "Content-Type" => "text/html" }
|
|
24
|
+
@response_body = []
|
|
25
|
+
@halted = false
|
|
26
|
+
@halted_response = nil
|
|
27
|
+
@params_hybrid = nil
|
|
28
|
+
|
|
29
|
+
# Merge route params, query params, and body params
|
|
30
|
+
# Convert all keys to symbols for consistency
|
|
31
|
+
route_params_sym = route_params.transform_keys(&:to_sym)
|
|
32
|
+
query_params_sym = query_params.transform_keys(&:to_sym)
|
|
33
|
+
body_params_sym = body_params.transform_keys(&:to_sym)
|
|
34
|
+
@params = route_params_sym.merge(query_params_sym).merge(body_params_sym)
|
|
35
|
+
|
|
36
|
+
# Handle multipart form data (file uploads)
|
|
37
|
+
content_type_header = @env["CONTENT_TYPE"] || ""
|
|
38
|
+
return unless content_type_header.include?("multipart/form-data")
|
|
39
|
+
|
|
40
|
+
# Rack::Request handles multipart automatically
|
|
41
|
+
multipart_params = @request.params
|
|
42
|
+
multipart_params_sym = multipart_params.transform_keys(&:to_sym)
|
|
43
|
+
@params.merge!(multipart_params_sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def params
|
|
47
|
+
# Return params with support for both symbol and string key access
|
|
48
|
+
@params ||= begin
|
|
49
|
+
hash = @params.dup
|
|
50
|
+
# Add string-key versions for all symbol keys
|
|
51
|
+
@params.each do |k, v|
|
|
52
|
+
hash[k.to_s] = v if k.is_a?(Symbol)
|
|
53
|
+
end
|
|
54
|
+
# Add symbol-key versions for all string keys
|
|
55
|
+
hash.to_a.each do |k, v|
|
|
56
|
+
hash[k.to_sym] = v if k.is_a?(String) && !hash.key?(k.to_sym)
|
|
57
|
+
end
|
|
58
|
+
# Create accessor that checks both
|
|
59
|
+
def hash.[](key)
|
|
60
|
+
super(key.to_sym) || super(key.to_s) || super
|
|
61
|
+
end
|
|
62
|
+
hash
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def status(code)
|
|
67
|
+
@response_status = code
|
|
68
|
+
code
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def content_type(type)
|
|
72
|
+
@response_headers["Content-Type"] = type
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def headers
|
|
76
|
+
@response_headers
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def body(str = nil)
|
|
80
|
+
if str
|
|
81
|
+
@response_body = [str.to_s]
|
|
82
|
+
str
|
|
83
|
+
else
|
|
84
|
+
@response_body
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def json(obj)
|
|
89
|
+
content_type "application/json"
|
|
90
|
+
body(obj.to_json)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def halt(status_code, body = nil)
|
|
94
|
+
@halted = true
|
|
95
|
+
@response_status = status_code
|
|
96
|
+
if body
|
|
97
|
+
content_type "application/json" if @response_headers["Content-Type"] == "text/html"
|
|
98
|
+
@halted_response = [status_code, @response_headers.dup, [body.to_s]]
|
|
99
|
+
else
|
|
100
|
+
@halted_response = [status_code, @response_headers.dup, []]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def halted?
|
|
105
|
+
@halted
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def send_file(filepath)
|
|
109
|
+
return unless File.exist?(filepath)
|
|
110
|
+
|
|
111
|
+
content = File.read(filepath)
|
|
112
|
+
ext = File.extname(filepath).downcase
|
|
113
|
+
mime_types = {
|
|
114
|
+
".css" => "text/css",
|
|
115
|
+
".js" => "application/javascript",
|
|
116
|
+
".html" => "text/html",
|
|
117
|
+
".json" => "application/json",
|
|
118
|
+
".xml" => "application/xml",
|
|
119
|
+
".svg" => "image/svg+xml"
|
|
120
|
+
}
|
|
121
|
+
content_type(mime_types[ext] || "application/octet-stream")
|
|
122
|
+
body(content)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def script_name
|
|
126
|
+
@request.script_name
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def path_info
|
|
130
|
+
@request.path_info
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cookies
|
|
134
|
+
@request.cookies
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_rack_response
|
|
138
|
+
if @halted && @halted_response
|
|
139
|
+
@halted_response
|
|
140
|
+
else
|
|
141
|
+
# Ensure body is an array
|
|
142
|
+
body_array = @response_body.is_a?(Array) ? @response_body : [@response_body.to_s]
|
|
143
|
+
body_array = [""] if body_array.empty?
|
|
144
|
+
[@response_status, @response_headers.dup, body_array]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def query_params
|
|
151
|
+
@request.params
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def body_params
|
|
155
|
+
return {} unless @env["rack.input"]
|
|
156
|
+
|
|
157
|
+
# Read body if content type is JSON or form data
|
|
158
|
+
content_type_header = @env["CONTENT_TYPE"] || ""
|
|
159
|
+
body_input = @env["rack.input"].read
|
|
160
|
+
@env["rack.input"].rewind
|
|
161
|
+
|
|
162
|
+
return {} if body_input.nil? || body_input.empty?
|
|
163
|
+
|
|
164
|
+
if content_type_header.include?("application/json")
|
|
165
|
+
begin
|
|
166
|
+
parsed = JSON.parse(body_input)
|
|
167
|
+
# Convert string keys to symbol keys for consistency
|
|
168
|
+
parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : parsed
|
|
169
|
+
rescue JSON::ParserError
|
|
170
|
+
{}
|
|
171
|
+
end
|
|
172
|
+
elsif content_type_header.include?("application/x-www-form-urlencoded")
|
|
173
|
+
parsed = Rack::Utils.parse_nested_query(body_input)
|
|
174
|
+
parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : parsed
|
|
175
|
+
elsif content_type_header.include?("multipart/form-data")
|
|
176
|
+
# For multipart, use Rack::Request.params which handles it automatically
|
|
177
|
+
# This will be merged in initialize
|
|
178
|
+
{}
|
|
179
|
+
else
|
|
180
|
+
{}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Helper to read request body as string
|
|
186
|
+
def self.read_body(env)
|
|
187
|
+
input = env["rack.input"]
|
|
188
|
+
return "" unless input
|
|
189
|
+
|
|
190
|
+
body = input.read
|
|
191
|
+
input.rewind
|
|
192
|
+
body.to_s
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|