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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// DMN Editor JavaScript
|
|
2
|
+
|
|
3
|
+
// State
|
|
4
|
+
const state = {
|
|
5
|
+
currentModel: null,
|
|
6
|
+
currentDecision: null,
|
|
7
|
+
models: [],
|
|
8
|
+
baseUrl: window.location.origin
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Initialize
|
|
12
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
13
|
+
initializeEventListeners();
|
|
14
|
+
loadModels();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Event Listeners
|
|
18
|
+
function initializeEventListeners() {
|
|
19
|
+
// Header actions
|
|
20
|
+
document.getElementById('new-model-btn').addEventListener('click', () => {
|
|
21
|
+
openModal('new-model-modal');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
document.getElementById('import-btn').addEventListener('click', () => {
|
|
25
|
+
openModal('import-modal');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
document.getElementById('export-btn').addEventListener('click', exportModel);
|
|
29
|
+
document.getElementById('validate-btn').addEventListener('click', validateModel);
|
|
30
|
+
|
|
31
|
+
// Forms
|
|
32
|
+
document.getElementById('new-model-form').addEventListener('submit', createNewModel);
|
|
33
|
+
document.getElementById('add-decision-form').addEventListener('submit', addDecision);
|
|
34
|
+
document.getElementById('add-column-form').addEventListener('submit', addColumn);
|
|
35
|
+
document.getElementById('import-form').addEventListener('submit', importModel);
|
|
36
|
+
|
|
37
|
+
// Decision actions
|
|
38
|
+
document.getElementById('add-decision-btn').addEventListener('click', () => {
|
|
39
|
+
openModal('add-decision-modal');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
document.getElementById('decision-select').addEventListener('change', (e) => {
|
|
43
|
+
loadDecision(e.target.value);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Table controls
|
|
47
|
+
document.getElementById('add-input-btn').addEventListener('click', () => {
|
|
48
|
+
document.getElementById('column-type').value = 'input';
|
|
49
|
+
document.getElementById('column-modal-title').textContent = 'Add Input Column';
|
|
50
|
+
document.getElementById('expression-group').style.display = 'block';
|
|
51
|
+
openModal('add-column-modal');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
document.getElementById('add-output-btn').addEventListener('click', () => {
|
|
55
|
+
document.getElementById('column-type').value = 'output';
|
|
56
|
+
document.getElementById('column-modal-title').textContent = 'Add Output Column';
|
|
57
|
+
document.getElementById('expression-group').style.display = 'none';
|
|
58
|
+
openModal('add-column-modal');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
document.getElementById('add-rule-btn').addEventListener('click', addRule);
|
|
62
|
+
document.getElementById('hit-policy-select').addEventListener('change', updateHitPolicy);
|
|
63
|
+
|
|
64
|
+
// Tabs
|
|
65
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
66
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Visualization
|
|
70
|
+
document.getElementById('visualize-graph-btn')?.addEventListener('click', visualizeGraph);
|
|
71
|
+
document.getElementById('visualize-tree-btn')?.addEventListener('click', visualizeTree);
|
|
72
|
+
|
|
73
|
+
// XML actions
|
|
74
|
+
document.getElementById('copy-xml-btn')?.addEventListener('click', copyXml);
|
|
75
|
+
document.getElementById('download-xml-btn')?.addEventListener('click', downloadXml);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// API Functions
|
|
79
|
+
async function apiCall(endpoint, method = 'GET', data = null) {
|
|
80
|
+
const options = {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (data) {
|
|
88
|
+
options.body = JSON.stringify(data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const response = await fetch(`${state.baseUrl}/api/dmn${endpoint}`, options);
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const error = await response.json();
|
|
95
|
+
throw new Error(error.error || 'API request failed');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Model Management
|
|
102
|
+
async function loadModels() {
|
|
103
|
+
try {
|
|
104
|
+
const models = await apiCall('/models');
|
|
105
|
+
state.models = models;
|
|
106
|
+
renderModelList();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
showNotification('Failed to load models: ' + error.message, 'error');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderModelList() {
|
|
113
|
+
const list = document.getElementById('model-list');
|
|
114
|
+
|
|
115
|
+
if (state.models.length === 0) {
|
|
116
|
+
list.innerHTML = '<p class="empty-state">No models yet. Create one to get started.</p>';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
list.innerHTML = state.models.map(model => `
|
|
121
|
+
<div class="model-item ${model.id === state.currentModel?.id ? 'active' : ''}"
|
|
122
|
+
onclick="loadModel('${model.id}')">
|
|
123
|
+
<h4>${escapeHtml(model.name)}</h4>
|
|
124
|
+
<p>${model.decision_count} decision(s)</p>
|
|
125
|
+
</div>
|
|
126
|
+
`).join('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function loadModel(modelId) {
|
|
130
|
+
try {
|
|
131
|
+
const model = await apiCall(`/models/${modelId}`);
|
|
132
|
+
state.currentModel = model;
|
|
133
|
+
renderModelEditor();
|
|
134
|
+
renderModelList();
|
|
135
|
+
|
|
136
|
+
// Hide welcome screen, show editor
|
|
137
|
+
document.getElementById('welcome-screen').style.display = 'none';
|
|
138
|
+
document.getElementById('editor-container').style.display = 'block';
|
|
139
|
+
|
|
140
|
+
// Load first decision if available
|
|
141
|
+
if (model.decisions && model.decisions.length > 0) {
|
|
142
|
+
loadDecision(model.decisions[0].id);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
showNotification('Failed to load model: ' + error.message, 'error');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function createNewModel(e) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
|
|
152
|
+
const name = document.getElementById('model-name-input').value;
|
|
153
|
+
const namespace = document.getElementById('model-namespace-input').value;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const model = await apiCall('/models', 'POST', { name, namespace });
|
|
157
|
+
state.models.push(model);
|
|
158
|
+
await loadModel(model.id);
|
|
159
|
+
closeModal('new-model-modal');
|
|
160
|
+
document.getElementById('new-model-form').reset();
|
|
161
|
+
showNotification('Model created successfully', 'success');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
showNotification('Failed to create model: ' + error.message, 'error');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderModelEditor() {
|
|
168
|
+
if (!state.currentModel) return;
|
|
169
|
+
|
|
170
|
+
document.getElementById('model-name').textContent = state.currentModel.name;
|
|
171
|
+
document.getElementById('model-id').textContent = `ID: ${state.currentModel.id}`;
|
|
172
|
+
|
|
173
|
+
// Populate decision selector
|
|
174
|
+
const select = document.getElementById('decision-select');
|
|
175
|
+
if (state.currentModel.decisions && state.currentModel.decisions.length > 0) {
|
|
176
|
+
select.innerHTML = state.currentModel.decisions.map(d =>
|
|
177
|
+
`<option value="${d.id}">${escapeHtml(d.name)}</option>`
|
|
178
|
+
).join('');
|
|
179
|
+
} else {
|
|
180
|
+
select.innerHTML = '<option value="">No decisions yet</option>';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Decision Management
|
|
185
|
+
async function addDecision(e) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
|
|
188
|
+
const decisionId = document.getElementById('decision-id-input').value;
|
|
189
|
+
const name = document.getElementById('decision-name-input').value;
|
|
190
|
+
const type = document.getElementById('decision-type-select').value;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const decision = await apiCall(`/models/${state.currentModel.id}/decisions`, 'POST', {
|
|
194
|
+
decision_id: decisionId,
|
|
195
|
+
name,
|
|
196
|
+
type
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
state.currentModel.decisions.push(decision);
|
|
200
|
+
renderModelEditor();
|
|
201
|
+
loadDecision(decision.id);
|
|
202
|
+
closeModal('add-decision-modal');
|
|
203
|
+
document.getElementById('add-decision-form').reset();
|
|
204
|
+
showNotification('Decision added successfully', 'success');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
showNotification('Failed to add decision: ' + error.message, 'error');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function loadDecision(decisionId) {
|
|
211
|
+
const decision = state.currentModel.decisions.find(d => d.id === decisionId);
|
|
212
|
+
if (!decision) return;
|
|
213
|
+
|
|
214
|
+
state.currentDecision = decision;
|
|
215
|
+
document.getElementById('decision-select').value = decisionId;
|
|
216
|
+
|
|
217
|
+
if (decision.decision_table) {
|
|
218
|
+
renderDecisionTable(decision.decision_table);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Decision Table
|
|
223
|
+
function renderDecisionTable(table) {
|
|
224
|
+
const headers = document.getElementById('table-headers');
|
|
225
|
+
const types = document.getElementById('table-types');
|
|
226
|
+
const body = document.getElementById('table-body');
|
|
227
|
+
|
|
228
|
+
// Set hit policy
|
|
229
|
+
document.getElementById('hit-policy-select').value = table.hit_policy;
|
|
230
|
+
|
|
231
|
+
// Render headers
|
|
232
|
+
let headerHtml = '<th class="rule-number">#</th>';
|
|
233
|
+
let typeHtml = '<th></th>';
|
|
234
|
+
|
|
235
|
+
table.inputs.forEach(input => {
|
|
236
|
+
headerHtml += `<th class="input-col">${escapeHtml(input.label)} <span class="col-type">(Input)</span></th>`;
|
|
237
|
+
typeHtml += `<th class="type-annotation">${input.type_ref || 'string'}</th>`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
table.outputs.forEach(output => {
|
|
241
|
+
headerHtml += `<th class="output-col">${escapeHtml(output.label)} <span class="col-type">(Output)</span></th>`;
|
|
242
|
+
typeHtml += `<th class="type-annotation">${output.type_ref || 'string'}</th>`;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
headerHtml += '<th class="actions-col">Actions</th>';
|
|
246
|
+
typeHtml += '<th></th>';
|
|
247
|
+
|
|
248
|
+
headers.innerHTML = headerHtml;
|
|
249
|
+
types.innerHTML = typeHtml;
|
|
250
|
+
|
|
251
|
+
// Render rules
|
|
252
|
+
if (table.rules.length === 0) {
|
|
253
|
+
body.innerHTML = `<tr class="empty-row"><td colspan="${table.inputs.length + table.outputs.length + 2}">
|
|
254
|
+
No rules defined. Click "Add Rule" to create one.</td></tr>`;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
body.innerHTML = table.rules.map((rule, index) => {
|
|
259
|
+
let rowHtml = `<tr data-rule-id="${rule.id}">`;
|
|
260
|
+
rowHtml += `<td class="rule-number">${index + 1}</td>`;
|
|
261
|
+
|
|
262
|
+
rule.input_entries.forEach((entry, i) => {
|
|
263
|
+
rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
|
|
264
|
+
onchange="updateRuleEntry('${rule.id}', 'input', ${i}, this.value)"></td>`;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
rule.output_entries.forEach((entry, i) => {
|
|
268
|
+
rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
|
|
269
|
+
onchange="updateRuleEntry('${rule.id}', 'output', ${i}, this.value)"></td>`;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
rowHtml += `<td class="actions-col">
|
|
273
|
+
<button class="btn btn-sm btn-danger" onclick="deleteRule('${rule.id}')">Delete</button>
|
|
274
|
+
</td>`;
|
|
275
|
+
rowHtml += '</tr>';
|
|
276
|
+
|
|
277
|
+
return rowHtml;
|
|
278
|
+
}).join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function addColumn(e) {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
|
|
284
|
+
const columnType = document.getElementById('column-type').value;
|
|
285
|
+
const id = document.getElementById('column-id-input').value;
|
|
286
|
+
const label = document.getElementById('column-label-input').value;
|
|
287
|
+
const typeRef = document.getElementById('column-type-input').value;
|
|
288
|
+
const expression = document.getElementById('column-expression-input').value;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const endpoint = `/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/${columnType}s`;
|
|
292
|
+
|
|
293
|
+
const data = {
|
|
294
|
+
[`${columnType}_id`]: id,
|
|
295
|
+
label,
|
|
296
|
+
type_ref: typeRef
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (columnType === 'input' && expression) {
|
|
300
|
+
data.expression = expression;
|
|
301
|
+
} else if (columnType === 'output') {
|
|
302
|
+
data.name = id;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await apiCall(endpoint, 'POST', data);
|
|
306
|
+
|
|
307
|
+
// Reload model to get updated state
|
|
308
|
+
await loadModel(state.currentModel.id);
|
|
309
|
+
loadDecision(state.currentDecision.id);
|
|
310
|
+
|
|
311
|
+
closeModal('add-column-modal');
|
|
312
|
+
document.getElementById('add-column-form').reset();
|
|
313
|
+
showNotification(`${columnType === 'input' ? 'Input' : 'Output'} column added successfully`, 'success');
|
|
314
|
+
} catch (error) {
|
|
315
|
+
showNotification('Failed to add column: ' + error.message, 'error');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function addRule() {
|
|
320
|
+
if (!state.currentDecision || !state.currentDecision.decision_table) return;
|
|
321
|
+
|
|
322
|
+
const table = state.currentDecision.decision_table;
|
|
323
|
+
const ruleId = `rule_${Date.now()}`;
|
|
324
|
+
const inputEntries = table.inputs.map(() => '-');
|
|
325
|
+
const outputEntries = table.outputs.map(() => '');
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules`, 'POST', {
|
|
329
|
+
rule_id: ruleId,
|
|
330
|
+
input_entries: inputEntries,
|
|
331
|
+
output_entries: outputEntries
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Reload model
|
|
335
|
+
await loadModel(state.currentModel.id);
|
|
336
|
+
loadDecision(state.currentDecision.id);
|
|
337
|
+
showNotification('Rule added successfully', 'success');
|
|
338
|
+
} catch (error) {
|
|
339
|
+
showNotification('Failed to add rule: ' + error.message, 'error');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function updateRuleEntry(ruleId, type, index, value) {
|
|
344
|
+
try {
|
|
345
|
+
const rule = state.currentDecision.decision_table.rules.find(r => r.id === ruleId);
|
|
346
|
+
if (!rule) return;
|
|
347
|
+
|
|
348
|
+
if (type === 'input') {
|
|
349
|
+
rule.input_entries[index] = value;
|
|
350
|
+
} else {
|
|
351
|
+
rule.output_entries[index] = value;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'PUT', {
|
|
355
|
+
input_entries: rule.input_entries,
|
|
356
|
+
output_entries: rule.output_entries
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
showNotification('Failed to update rule: ' + error.message, 'error');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function deleteRule(ruleId) {
|
|
364
|
+
if (!confirm('Are you sure you want to delete this rule?')) return;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'DELETE');
|
|
368
|
+
|
|
369
|
+
// Reload model
|
|
370
|
+
await loadModel(state.currentModel.id);
|
|
371
|
+
loadDecision(state.currentDecision.id);
|
|
372
|
+
showNotification('Rule deleted successfully', 'success');
|
|
373
|
+
} catch (error) {
|
|
374
|
+
showNotification('Failed to delete rule: ' + error.message, 'error');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function updateHitPolicy(e) {
|
|
379
|
+
if (!state.currentDecision || !state.currentDecision.decision_table) return;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}`, 'PUT', {
|
|
383
|
+
logic: { hit_policy: e.target.value }
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
showNotification('Hit policy updated successfully', 'success');
|
|
387
|
+
} catch (error) {
|
|
388
|
+
showNotification('Failed to update hit policy: ' + error.message, 'error');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Import/Export
|
|
393
|
+
async function exportModel() {
|
|
394
|
+
if (!state.currentModel) {
|
|
395
|
+
showNotification('No model selected', 'warning');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const xml = await apiCall(`/models/${state.currentModel.id}/export`);
|
|
401
|
+
|
|
402
|
+
// Update XML tab
|
|
403
|
+
document.getElementById('xml-content').innerHTML = `<code>${escapeHtml(xml)}</code>`;
|
|
404
|
+
|
|
405
|
+
showNotification('Model exported successfully', 'success');
|
|
406
|
+
} catch (error) {
|
|
407
|
+
showNotification('Failed to export model: ' + error.message, 'error');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function importModel(e) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
|
|
414
|
+
const fileInput = document.getElementById('import-file-input');
|
|
415
|
+
const file = fileInput.files[0];
|
|
416
|
+
|
|
417
|
+
if (!file) {
|
|
418
|
+
showNotification('Please select a file', 'warning');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const xmlContent = await file.text();
|
|
424
|
+
const model = await apiCall('/models/import', 'POST', { xml: xmlContent });
|
|
425
|
+
|
|
426
|
+
state.models.push(model);
|
|
427
|
+
await loadModel(model.id);
|
|
428
|
+
closeModal('import-modal');
|
|
429
|
+
document.getElementById('import-form').reset();
|
|
430
|
+
showNotification('Model imported successfully', 'success');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
showNotification('Failed to import model: ' + error.message, 'error');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function validateModel() {
|
|
437
|
+
if (!state.currentModel) {
|
|
438
|
+
showNotification('No model selected', 'warning');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const result = await apiCall(`/models/${state.currentModel.id}/validate`);
|
|
444
|
+
|
|
445
|
+
if (result.valid) {
|
|
446
|
+
showNotification('Model is valid!', 'success');
|
|
447
|
+
} else {
|
|
448
|
+
showNotification(`Validation failed: ${result.errors.join(', ')}`, 'error');
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
showNotification('Failed to validate model: ' + error.message, 'error');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Visualization
|
|
456
|
+
async function visualizeGraph() {
|
|
457
|
+
if (!state.currentModel) return;
|
|
458
|
+
|
|
459
|
+
const format = document.getElementById('graph-format-select').value;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const visualization = await apiCall(`/models/${state.currentModel.id}/visualize/graph?format=${format}`);
|
|
463
|
+
const container = document.getElementById('graph-visualization');
|
|
464
|
+
|
|
465
|
+
if (format === 'svg') {
|
|
466
|
+
container.innerHTML = visualization;
|
|
467
|
+
} else {
|
|
468
|
+
container.innerHTML = `<pre><code>${escapeHtml(visualization)}</code></pre>`;
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
showNotification('Failed to generate visualization: ' + error.message, 'error');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function visualizeTree() {
|
|
476
|
+
if (!state.currentDecision) return;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
const svg = await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/visualize/tree`);
|
|
480
|
+
document.getElementById('tree-visualization').innerHTML = svg;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
showNotification('Failed to generate tree visualization: ' + error.message, 'error');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function copyXml() {
|
|
487
|
+
const xmlContent = document.getElementById('xml-content').textContent;
|
|
488
|
+
navigator.clipboard.writeText(xmlContent);
|
|
489
|
+
showNotification('XML copied to clipboard', 'success');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function downloadXml() {
|
|
493
|
+
const xmlContent = document.getElementById('xml-content').textContent;
|
|
494
|
+
const blob = new Blob([xmlContent], { type: 'text/xml' });
|
|
495
|
+
const url = URL.createObjectURL(blob);
|
|
496
|
+
const a = document.createElement('a');
|
|
497
|
+
a.href = url;
|
|
498
|
+
a.download = `${state.currentModel.name}.dmn`;
|
|
499
|
+
a.click();
|
|
500
|
+
URL.revokeObjectURL(url);
|
|
501
|
+
showNotification('DMN file downloaded', 'success');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// UI Helpers
|
|
505
|
+
function switchTab(tabName) {
|
|
506
|
+
// Update tab buttons
|
|
507
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
508
|
+
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Update tab panes
|
|
512
|
+
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
513
|
+
pane.classList.toggle('active', pane.id === `${tabName}-tab`);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Load content based on tab
|
|
517
|
+
if (tabName === 'xml') {
|
|
518
|
+
exportModel();
|
|
519
|
+
} else if (tabName === 'graph') {
|
|
520
|
+
visualizeGraph();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function openModal(modalId) {
|
|
525
|
+
document.getElementById(modalId).classList.add('active');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function closeModal(modalId) {
|
|
529
|
+
document.getElementById(modalId).classList.remove('active');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function showNotification(message, type = 'info') {
|
|
533
|
+
const notification = document.getElementById('notification');
|
|
534
|
+
notification.textContent = message;
|
|
535
|
+
notification.className = `notification ${type} show`;
|
|
536
|
+
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
notification.classList.remove('show');
|
|
539
|
+
}, 3000);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function escapeHtml(text) {
|
|
543
|
+
const div = document.createElement('div');
|
|
544
|
+
div.textContent = text;
|
|
545
|
+
return div.innerHTML;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Close modals when clicking outside
|
|
549
|
+
window.addEventListener('click', (e) => {
|
|
550
|
+
if (e.target.classList.contains('modal')) {
|
|
551
|
+
e.target.classList.remove('active');
|
|
552
|
+
}
|
|
553
|
+
});
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
<header>
|
|
12
12
|
<h1>๐ฏ DecisionAgent Rule Builder</h1>
|
|
13
13
|
<p class="subtitle">Visual rule editor for non-technical users</p>
|
|
14
|
+
<div class="header-links">
|
|
15
|
+
<a href="/dmn/editor" class="btn btn-link">๐ DMN Editor</a>
|
|
16
|
+
<a href="/simulation" class="btn btn-link">๐งช Simulation</a>
|
|
17
|
+
<a href="/testing/batch" class="btn btn-link">๐ Batch Testing</a>
|
|
18
|
+
</div>
|
|
14
19
|
</header>
|
|
15
20
|
|
|
16
21
|
<div class="main-layout">
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
<!-- Action Buttons -->
|
|
49
54
|
<div class="actions">
|
|
50
55
|
<button class="btn btn-success" id="validateBtn">โ Validate Rules</button>
|
|
56
|
+
<button class="btn btn-info" id="testRuleBtn">๐งช Test Rule</button>
|
|
51
57
|
<button class="btn btn-primary" id="saveVersionBtn">๐พ Save Version</button>
|
|
52
58
|
<button class="btn btn-secondary" id="clearBtn">Clear All</button>
|
|
53
59
|
</div>
|
|
@@ -330,6 +336,52 @@
|
|
|
330
336
|
</div>
|
|
331
337
|
</div>
|
|
332
338
|
</div>
|
|
339
|
+
|
|
340
|
+
<!-- Test Rule Modal -->
|
|
341
|
+
<div id="testRuleModal" class="modal hidden">
|
|
342
|
+
<div class="modal-content modal-large">
|
|
343
|
+
<div class="modal-header">
|
|
344
|
+
<h2>๐งช Test Rule</h2>
|
|
345
|
+
<button class="close-btn" id="closeTestRuleBtn">×</button>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="modal-body">
|
|
348
|
+
<div class="form-group">
|
|
349
|
+
<label for="testContext">Test Context (JSON):</label>
|
|
350
|
+
<textarea id="testContext" class="input" rows="8" placeholder='{"field1": "value1", "field2": 123}'></textarea>
|
|
351
|
+
<small class="form-hint">Enter context data as JSON to test your rules</small>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="actions" style="margin-top: 1rem;">
|
|
354
|
+
<button class="btn btn-primary" id="runTestBtn">Run Test</button>
|
|
355
|
+
</div>
|
|
356
|
+
<div id="testResults" class="test-results hidden" style="margin-top: 2rem;">
|
|
357
|
+
<h3>Test Results</h3>
|
|
358
|
+
<div id="testDecision" class="test-result-item">
|
|
359
|
+
<strong>Decision:</strong> <span id="testDecisionValue">-</span>
|
|
360
|
+
</div>
|
|
361
|
+
<div id="testConfidence" class="test-result-item">
|
|
362
|
+
<strong>Confidence:</strong> <span id="testConfidenceValue">-</span>
|
|
363
|
+
</div>
|
|
364
|
+
<div id="testReason" class="test-result-item">
|
|
365
|
+
<strong>Reason:</strong> <span id="testReasonValue">-</span>
|
|
366
|
+
</div>
|
|
367
|
+
<div id="testExplainability" class="test-explainability" style="margin-top: 1.5rem;">
|
|
368
|
+
<h4>Explainability</h4>
|
|
369
|
+
<div id="testBecause" class="test-because">
|
|
370
|
+
<strong>Because (conditions that led to decision):</strong>
|
|
371
|
+
<ul id="testBecauseList"></ul>
|
|
372
|
+
</div>
|
|
373
|
+
<div id="testFailedConditions" class="test-failed" style="margin-top: 1rem;">
|
|
374
|
+
<strong>Failed Conditions:</strong>
|
|
375
|
+
<ul id="testFailedList"></ul>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="modal-footer">
|
|
381
|
+
<button class="btn btn-secondary" id="closeTestRuleModalBtn">Close</button>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
333
385
|
</div>
|
|
334
386
|
|
|
335
387
|
<footer class="footer">
|