decision_agent 0.3.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Historical Replay / Backtesting</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
8
|
+
<style>
|
|
9
|
+
.simulation-container {
|
|
10
|
+
max-width: 1400px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.step-section {
|
|
16
|
+
background: var(--panel-bg);
|
|
17
|
+
border-radius: 10px;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
margin-bottom: 20px;
|
|
20
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.step-header {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
margin-bottom: 15px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.step-number {
|
|
30
|
+
background: var(--primary-color);
|
|
31
|
+
color: white;
|
|
32
|
+
width: 30px;
|
|
33
|
+
height: 30px;
|
|
34
|
+
border-radius: 50%;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
margin-right: 10px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.file-upload-area {
|
|
43
|
+
border: 2px dashed var(--border-color);
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
padding: 40px;
|
|
46
|
+
text-align: center;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: all 0.3s;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.file-upload-area:hover {
|
|
52
|
+
border-color: var(--primary-color);
|
|
53
|
+
background: var(--hover-bg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.file-upload-area.dragover {
|
|
57
|
+
border-color: var(--primary-color);
|
|
58
|
+
background: var(--hover-bg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.results-table {
|
|
62
|
+
width: 100%;
|
|
63
|
+
border-collapse: collapse;
|
|
64
|
+
margin-top: 15px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.results-table th,
|
|
68
|
+
.results-table td {
|
|
69
|
+
padding: 10px;
|
|
70
|
+
text-align: left;
|
|
71
|
+
border-bottom: 1px solid var(--border-color);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.results-table th {
|
|
75
|
+
background: var(--hover-bg);
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.metrics-grid {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
82
|
+
gap: 15px;
|
|
83
|
+
margin-top: 15px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.metric-card {
|
|
87
|
+
background: var(--hover-bg);
|
|
88
|
+
padding: 15px;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
text-align: center;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.metric-value {
|
|
94
|
+
font-size: 2rem;
|
|
95
|
+
font-weight: bold;
|
|
96
|
+
color: var(--primary-color);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.metric-label {
|
|
100
|
+
font-size: 0.9rem;
|
|
101
|
+
color: var(--text-secondary);
|
|
102
|
+
margin-top: 5px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.hidden {
|
|
106
|
+
display: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.error-message {
|
|
110
|
+
background: #fee2e2;
|
|
111
|
+
color: #991b1b;
|
|
112
|
+
padding: 12px;
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
margin-top: 10px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.success-message {
|
|
118
|
+
background: #d1fae5;
|
|
119
|
+
color: #065f46;
|
|
120
|
+
padding: 12px;
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
margin-top: 10px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.header-links {
|
|
126
|
+
margin-top: 20px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.header-links a {
|
|
130
|
+
margin-right: 15px;
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
color: var(--primary-color);
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
135
|
+
</head>
|
|
136
|
+
<body>
|
|
137
|
+
<div class="simulation-container">
|
|
138
|
+
<header>
|
|
139
|
+
<h1>🔄 Historical Replay / Backtesting</h1>
|
|
140
|
+
<p class="subtitle">Replay historical decisions with different rule versions</p>
|
|
141
|
+
<div class="header-links">
|
|
142
|
+
<a href="/simulation">← Back to Simulation Dashboard</a>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<!-- Step 1: Upload Historical Data -->
|
|
147
|
+
<div class="step-section">
|
|
148
|
+
<div class="step-header">
|
|
149
|
+
<div class="step-number">1</div>
|
|
150
|
+
<h2>Upload Historical Data</h2>
|
|
151
|
+
</div>
|
|
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
|
+
|
|
154
|
+
<div class="file-upload-area" id="uploadArea">
|
|
155
|
+
<p>📁 Drag and drop your CSV/JSON file here, or click to browse</p>
|
|
156
|
+
<input type="file" id="fileInput" accept=".csv,.json" style="display: none;">
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div id="uploadStatus" class="hidden"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Step 2: Configure Rules -->
|
|
163
|
+
<div class="step-section">
|
|
164
|
+
<div class="step-header">
|
|
165
|
+
<div class="step-number">2</div>
|
|
166
|
+
<h2>Configure Rules</h2>
|
|
167
|
+
</div>
|
|
168
|
+
<p>Paste your rules JSON or select a version to use</p>
|
|
169
|
+
|
|
170
|
+
<div class="form-group">
|
|
171
|
+
<label for="rulesJson">Rules JSON:</label>
|
|
172
|
+
<textarea id="rulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="form-group">
|
|
176
|
+
<label for="ruleVersion">Or select version:</label>
|
|
177
|
+
<select id="ruleVersion" class="input">
|
|
178
|
+
<option value="">-- Select Version --</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div class="form-group">
|
|
183
|
+
<label for="compareVersion">Compare with version (optional):</label>
|
|
184
|
+
<select id="compareVersion" class="input">
|
|
185
|
+
<option value="">-- None --</option>
|
|
186
|
+
</select>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="actions">
|
|
190
|
+
<label for="importRulesFile" class="btn btn-secondary">📁 Import Rules JSON
|
|
191
|
+
<input type="file" id="importRulesFile" accept=".json" style="display: none;">
|
|
192
|
+
</label>
|
|
193
|
+
<button class="btn btn-secondary" id="validateRulesBtn">✓ Validate Rules</button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Step 3: Run Replay -->
|
|
198
|
+
<div class="step-section">
|
|
199
|
+
<div class="step-header">
|
|
200
|
+
<div class="step-number">3</div>
|
|
201
|
+
<h2>Run Historical Replay</h2>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="form-group">
|
|
205
|
+
<label>
|
|
206
|
+
<input type="checkbox" id="parallelExecution" checked> Parallel execution
|
|
207
|
+
</label>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="form-group">
|
|
210
|
+
<label for="threadCount">Thread count:</label>
|
|
211
|
+
<input type="number" id="threadCount" value="4" min="1" max="16" class="input" style="width: 100px;">
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<button class="btn btn-primary" id="runReplayBtn" disabled>▶ Run Replay</button>
|
|
215
|
+
|
|
216
|
+
<div id="runStatus" class="hidden"></div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Step 4: Results -->
|
|
220
|
+
<div class="step-section" id="resultsSection" style="display: none;">
|
|
221
|
+
<div class="step-header">
|
|
222
|
+
<div class="step-number">4</div>
|
|
223
|
+
<h2>Replay Results</h2>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div id="replayInfo">
|
|
227
|
+
<p><strong>Replay ID:</strong> <span id="replayId"></span></p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<!-- Statistics -->
|
|
231
|
+
<div id="statisticsSection" class="hidden">
|
|
232
|
+
<h3>Statistics</h3>
|
|
233
|
+
<div class="metrics-grid" id="statisticsGrid"></div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- Decision Distribution -->
|
|
237
|
+
<div id="distributionSection" class="hidden">
|
|
238
|
+
<h3>Decision Distribution</h3>
|
|
239
|
+
<div id="distributionContent"></div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Comparison Results (if comparing) -->
|
|
243
|
+
<div id="comparisonSection" class="hidden">
|
|
244
|
+
<h3>Comparison Results</h3>
|
|
245
|
+
<div class="metrics-grid" id="comparisonGrid"></div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<script>
|
|
251
|
+
// Helper function to get the base path for API calls
|
|
252
|
+
function getBasePath() {
|
|
253
|
+
const baseTag = document.querySelector('base');
|
|
254
|
+
if (baseTag && baseTag.href) {
|
|
255
|
+
try {
|
|
256
|
+
const baseUrl = new URL(baseTag.href, window.location.href);
|
|
257
|
+
let path = baseUrl.pathname;
|
|
258
|
+
if (path && !path.endsWith('/')) {
|
|
259
|
+
path += '/';
|
|
260
|
+
}
|
|
261
|
+
return path;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
if (baseTag.href.startsWith('/')) {
|
|
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
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
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
|
+
const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
|
|
282
|
+
return dirPath || '/';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const basePath = getBasePath();
|
|
286
|
+
let historicalData = null;
|
|
287
|
+
|
|
288
|
+
// Load versions
|
|
289
|
+
function loadVersions() {
|
|
290
|
+
fetch(`${basePath}api/versions`)
|
|
291
|
+
.then(response => response.json())
|
|
292
|
+
.then(data => {
|
|
293
|
+
const ruleVersionSelect = document.getElementById('ruleVersion');
|
|
294
|
+
const compareVersionSelect = document.getElementById('compareVersion');
|
|
295
|
+
|
|
296
|
+
ruleVersionSelect.innerHTML = '<option value="">-- Select Version --</option>';
|
|
297
|
+
compareVersionSelect.innerHTML = '<option value="">-- None --</option>';
|
|
298
|
+
|
|
299
|
+
if (data.versions) {
|
|
300
|
+
data.versions.forEach(v => {
|
|
301
|
+
const option1 = document.createElement('option');
|
|
302
|
+
option1.value = v.id;
|
|
303
|
+
option1.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
|
|
304
|
+
ruleVersionSelect.appendChild(option1);
|
|
305
|
+
|
|
306
|
+
const option2 = document.createElement('option');
|
|
307
|
+
option2.value = v.id;
|
|
308
|
+
option2.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
|
|
309
|
+
compareVersionSelect.appendChild(option2);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
.catch(error => console.error('Error loading versions:', error));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
loadVersions();
|
|
317
|
+
|
|
318
|
+
// File upload
|
|
319
|
+
const uploadArea = document.getElementById('uploadArea');
|
|
320
|
+
const fileInput = document.getElementById('fileInput');
|
|
321
|
+
const uploadStatus = document.getElementById('uploadStatus');
|
|
322
|
+
|
|
323
|
+
uploadArea.addEventListener('click', () => fileInput.click());
|
|
324
|
+
uploadArea.addEventListener('dragover', (e) => {
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
uploadArea.classList.add('dragover');
|
|
327
|
+
});
|
|
328
|
+
uploadArea.addEventListener('dragleave', () => {
|
|
329
|
+
uploadArea.classList.remove('dragover');
|
|
330
|
+
});
|
|
331
|
+
uploadArea.addEventListener('drop', (e) => {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
uploadArea.classList.remove('dragover');
|
|
334
|
+
const files = e.dataTransfer.files;
|
|
335
|
+
if (files.length > 0) {
|
|
336
|
+
handleFileUpload(files[0]);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
fileInput.addEventListener('change', (e) => {
|
|
340
|
+
if (e.target.files.length > 0) {
|
|
341
|
+
handleFileUpload(e.target.files[0]);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
function handleFileUpload(file) {
|
|
346
|
+
const reader = new FileReader();
|
|
347
|
+
reader.onload = (e) => {
|
|
348
|
+
try {
|
|
349
|
+
if (file.name.endsWith('.json')) {
|
|
350
|
+
historicalData = JSON.parse(e.target.result);
|
|
351
|
+
} else if (file.name.endsWith('.csv')) {
|
|
352
|
+
// Simple CSV parsing
|
|
353
|
+
const lines = e.target.result.split('\n');
|
|
354
|
+
const headers = lines[0].split(',');
|
|
355
|
+
historicalData = lines.slice(1).filter(l => l.trim()).map(line => {
|
|
356
|
+
const values = line.split(',');
|
|
357
|
+
const obj = {};
|
|
358
|
+
headers.forEach((h, i) => {
|
|
359
|
+
obj[h.trim()] = values[i]?.trim() || '';
|
|
360
|
+
});
|
|
361
|
+
return obj;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
uploadStatus.className = 'success-message';
|
|
366
|
+
uploadStatus.textContent = `Successfully loaded ${historicalData.length} historical records`;
|
|
367
|
+
uploadStatus.classList.remove('hidden');
|
|
368
|
+
document.getElementById('runReplayBtn').disabled = false;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
uploadStatus.className = 'error-message';
|
|
371
|
+
uploadStatus.textContent = `Error: ${error.message}`;
|
|
372
|
+
uploadStatus.classList.remove('hidden');
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
reader.readAsText(file);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Import rules JSON
|
|
379
|
+
document.getElementById('importRulesFile').addEventListener('change', (e) => {
|
|
380
|
+
const file = e.target.files[0];
|
|
381
|
+
if (file) {
|
|
382
|
+
const reader = new FileReader();
|
|
383
|
+
reader.onload = (e) => {
|
|
384
|
+
try {
|
|
385
|
+
const json = JSON.parse(e.target.result);
|
|
386
|
+
document.getElementById('rulesJson').value = JSON.stringify(json, null, 2);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
alert('Invalid JSON file: ' + error.message);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
reader.readAsText(file);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Validate rules
|
|
396
|
+
document.getElementById('validateRulesBtn').addEventListener('click', () => {
|
|
397
|
+
const rulesJson = document.getElementById('rulesJson').value;
|
|
398
|
+
try {
|
|
399
|
+
JSON.parse(rulesJson);
|
|
400
|
+
fetch(`${basePath}api/validate`, {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
headers: { 'Content-Type': 'application/json' },
|
|
403
|
+
body: JSON.stringify(JSON.parse(rulesJson))
|
|
404
|
+
})
|
|
405
|
+
.then(response => response.json())
|
|
406
|
+
.then(data => {
|
|
407
|
+
if (data.valid) {
|
|
408
|
+
alert('Rules are valid!');
|
|
409
|
+
} else {
|
|
410
|
+
alert('Validation errors: ' + data.errors.join(', '));
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
} catch (error) {
|
|
414
|
+
alert('Invalid JSON: ' + error.message);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Run replay
|
|
419
|
+
document.getElementById('runReplayBtn').addEventListener('click', () => {
|
|
420
|
+
if (!historicalData) {
|
|
421
|
+
alert('Please upload historical data first');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const rulesJson = document.getElementById('rulesJson').value;
|
|
426
|
+
const ruleVersion = document.getElementById('ruleVersion').value;
|
|
427
|
+
const compareVersion = document.getElementById('compareVersion').value;
|
|
428
|
+
|
|
429
|
+
if (!rulesJson.trim() && !ruleVersion) {
|
|
430
|
+
alert('Please provide rules JSON or select a version');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const rules = rulesJson.trim() ? JSON.parse(rulesJson) : null;
|
|
436
|
+
|
|
437
|
+
const runStatus = document.getElementById('runStatus');
|
|
438
|
+
runStatus.className = 'hidden';
|
|
439
|
+
|
|
440
|
+
const requestData = {
|
|
441
|
+
historical_data: historicalData,
|
|
442
|
+
options: {
|
|
443
|
+
parallel: document.getElementById('parallelExecution').checked,
|
|
444
|
+
thread_count: parseInt(document.getElementById('threadCount').value) || 4
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
if (rules) {
|
|
449
|
+
requestData.rules = rules;
|
|
450
|
+
}
|
|
451
|
+
if (ruleVersion) {
|
|
452
|
+
requestData.rule_version = ruleVersion;
|
|
453
|
+
}
|
|
454
|
+
if (compareVersion) {
|
|
455
|
+
requestData.compare_with = compareVersion;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fetch(`${basePath}api/simulation/replay`, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers: { 'Content-Type': 'application/json' },
|
|
461
|
+
body: JSON.stringify(requestData)
|
|
462
|
+
})
|
|
463
|
+
.then(response => response.json())
|
|
464
|
+
.then(data => {
|
|
465
|
+
if (data.error) {
|
|
466
|
+
runStatus.className = 'error-message';
|
|
467
|
+
runStatus.textContent = `Error: ${data.error}`;
|
|
468
|
+
runStatus.classList.remove('hidden');
|
|
469
|
+
} else {
|
|
470
|
+
runStatus.className = 'success-message';
|
|
471
|
+
runStatus.textContent = 'Replay completed successfully!';
|
|
472
|
+
runStatus.classList.remove('hidden');
|
|
473
|
+
displayResults(data);
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
.catch(error => {
|
|
477
|
+
runStatus.className = 'error-message';
|
|
478
|
+
runStatus.textContent = `Error: ${error.message}`;
|
|
479
|
+
runStatus.classList.remove('hidden');
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
alert('Invalid JSON: ' + error.message);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
function displayResults(data) {
|
|
487
|
+
document.getElementById('resultsSection').style.display = 'block';
|
|
488
|
+
document.getElementById('replayId').textContent = data.replay_id;
|
|
489
|
+
|
|
490
|
+
const results = data.results;
|
|
491
|
+
|
|
492
|
+
// Statistics
|
|
493
|
+
if (results.total_decisions !== undefined) {
|
|
494
|
+
const statsGrid = document.getElementById('statisticsGrid');
|
|
495
|
+
statsGrid.innerHTML = `
|
|
496
|
+
<div class="metric-card">
|
|
497
|
+
<div class="metric-value">${results.total_decisions}</div>
|
|
498
|
+
<div class="metric-label">Total Decisions</div>
|
|
499
|
+
</div>
|
|
500
|
+
${results.changed_decisions !== undefined ? `
|
|
501
|
+
<div class="metric-card">
|
|
502
|
+
<div class="metric-value">${results.changed_decisions}</div>
|
|
503
|
+
<div class="metric-label">Changed Decisions</div>
|
|
504
|
+
</div>
|
|
505
|
+
<div class="metric-card">
|
|
506
|
+
<div class="metric-value">${(results.change_rate * 100).toFixed(2)}%</div>
|
|
507
|
+
<div class="metric-label">Change Rate</div>
|
|
508
|
+
</div>
|
|
509
|
+
` : ''}
|
|
510
|
+
${results.average_confidence_delta !== undefined ? `
|
|
511
|
+
<div class="metric-card">
|
|
512
|
+
<div class="metric-value">${results.average_confidence_delta.toFixed(3)}</div>
|
|
513
|
+
<div class="metric-label">Avg Confidence Delta</div>
|
|
514
|
+
</div>
|
|
515
|
+
` : ''}
|
|
516
|
+
`;
|
|
517
|
+
document.getElementById('statisticsSection').classList.remove('hidden');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Decision distribution
|
|
521
|
+
if (results.decision_distribution) {
|
|
522
|
+
const distContent = document.getElementById('distributionContent');
|
|
523
|
+
let html = '<table class="results-table"><thead><tr><th>Decision</th><th>Count</th></tr></thead><tbody>';
|
|
524
|
+
Object.entries(results.decision_distribution).forEach(([decision, count]) => {
|
|
525
|
+
html += `<tr><td>${decision}</td><td>${count}</td></tr>`;
|
|
526
|
+
});
|
|
527
|
+
html += '</tbody></table>';
|
|
528
|
+
distContent.innerHTML = html;
|
|
529
|
+
document.getElementById('distributionSection').classList.remove('hidden');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Comparison results
|
|
533
|
+
if (results.changed_decisions !== undefined) {
|
|
534
|
+
const compGrid = document.getElementById('comparisonGrid');
|
|
535
|
+
compGrid.innerHTML = `
|
|
536
|
+
<div class="metric-card">
|
|
537
|
+
<div class="metric-value">${results.changed_decisions}</div>
|
|
538
|
+
<div class="metric-label">Changed Decisions</div>
|
|
539
|
+
</div>
|
|
540
|
+
<div class="metric-card">
|
|
541
|
+
<div class="metric-value">${(results.change_rate * 100).toFixed(2)}%</div>
|
|
542
|
+
<div class="metric-label">Change Rate</div>
|
|
543
|
+
</div>
|
|
544
|
+
`;
|
|
545
|
+
document.getElementById('comparisonSection').classList.remove('hidden');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
</script>
|
|
549
|
+
</body>
|
|
550
|
+
</html>
|
|
551
|
+
|