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.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -0
  3. data/README.md +64 -108
  4. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  5. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  7. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  8. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -16
  9. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  10. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  11. data/lib/decision_agent/agent.rb +49 -51
  12. data/lib/decision_agent/audit/adapter.rb +2 -0
  13. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  14. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  15. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  16. data/lib/decision_agent/auth/authenticator.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  18. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  19. data/lib/decision_agent/auth/permission.rb +2 -0
  20. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  22. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  23. data/lib/decision_agent/auth/role.rb +2 -0
  24. data/lib/decision_agent/auth/session.rb +2 -0
  25. data/lib/decision_agent/auth/session_manager.rb +2 -0
  26. data/lib/decision_agent/auth/user.rb +2 -0
  27. data/lib/decision_agent/context.rb +13 -0
  28. data/lib/decision_agent/decision.rb +11 -2
  29. data/lib/decision_agent/dmn/adapter.rb +2 -0
  30. data/lib/decision_agent/dmn/cache.rb +2 -2
  31. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  32. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  33. data/lib/decision_agent/dmn/errors.rb +2 -0
  34. data/lib/decision_agent/dmn/exporter.rb +43 -2
  35. data/lib/decision_agent/dmn/feel/evaluator.rb +102 -112
  36. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  38. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  39. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  40. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  41. data/lib/decision_agent/dmn/importer.rb +2 -0
  42. data/lib/decision_agent/dmn/model.rb +2 -4
  43. data/lib/decision_agent/dmn/parser.rb +2 -0
  44. data/lib/decision_agent/dmn/testing.rb +3 -6
  45. data/lib/decision_agent/dmn/validator.rb +8 -10
  46. data/lib/decision_agent/dmn/versioning.rb +41 -15
  47. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  48. data/lib/decision_agent/dsl/condition_evaluator.rb +197 -1473
  49. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  50. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  51. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  52. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  53. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  54. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  55. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  56. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  57. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  58. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  59. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  60. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  61. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  62. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  63. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  64. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  65. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  66. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  67. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  68. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  69. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  70. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  71. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  72. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  73. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  74. data/lib/decision_agent/dsl/schema_validator.rb +9 -24
  75. data/lib/decision_agent/errors.rb +2 -0
  76. data/lib/decision_agent/evaluation.rb +14 -2
  77. data/lib/decision_agent/evaluation_validator.rb +0 -0
  78. data/lib/decision_agent/evaluators/base.rb +2 -0
  79. data/lib/decision_agent/evaluators/dmn_evaluator.rb +2 -0
  80. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +28 -41
  81. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  82. data/lib/decision_agent/explainability/condition_trace.rb +2 -0
  83. data/lib/decision_agent/explainability/explainability_result.rb +2 -4
  84. data/lib/decision_agent/explainability/rule_trace.rb +2 -0
  85. data/lib/decision_agent/explainability/trace_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/alert_manager.rb +2 -15
  87. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +0 -0
  88. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +0 -0
  89. data/lib/decision_agent/monitoring/dashboard/public/index.html +0 -0
  90. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  91. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  92. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  93. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  94. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +0 -0
  95. data/lib/decision_agent/monitoring/storage/base_adapter.rb +0 -0
  96. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +1 -1
  97. data/lib/decision_agent/replay/replay.rb +4 -1
  98. data/lib/decision_agent/scoring/base.rb +2 -0
  99. data/lib/decision_agent/scoring/consensus.rb +2 -0
  100. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  101. data/lib/decision_agent/scoring/threshold.rb +2 -0
  102. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  103. data/lib/decision_agent/simulation/errors.rb +2 -0
  104. data/lib/decision_agent/simulation/impact_analyzer.rb +3 -3
  105. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +11 -10
  106. data/lib/decision_agent/simulation/replay_engine.rb +3 -3
  107. data/lib/decision_agent/simulation/scenario_engine.rb +3 -1
  108. data/lib/decision_agent/simulation/scenario_library.rb +2 -0
  109. data/lib/decision_agent/simulation/shadow_test_engine.rb +3 -16
  110. data/lib/decision_agent/simulation/what_if_analyzer.rb +17 -13
  111. data/lib/decision_agent/simulation.rb +2 -0
  112. data/lib/decision_agent/testing/batch_test_importer.rb +6 -6
  113. data/lib/decision_agent/testing/batch_test_runner.rb +5 -4
  114. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  115. data/lib/decision_agent/testing/test_result_comparator.rb +56 -63
  116. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  117. data/lib/decision_agent/version.rb +3 -1
  118. data/lib/decision_agent/versioning/activerecord_adapter.rb +159 -47
  119. data/lib/decision_agent/versioning/adapter.rb +42 -0
  120. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -13
  121. data/lib/decision_agent/versioning/version_manager.rb +49 -2
  122. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  123. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  124. data/lib/decision_agent/web/dmn_editor.rb +8 -73
  125. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  126. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  127. data/lib/decision_agent/web/public/app.js +67 -26
  128. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  129. data/lib/decision_agent/web/public/dmn-editor.css +0 -0
  130. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  131. data/lib/decision_agent/web/public/dmn-editor.js +79 -8
  132. data/lib/decision_agent/web/public/index.html +20 -3
  133. data/lib/decision_agent/web/public/login.html +1 -1
  134. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  135. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  136. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  137. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  138. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  139. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  140. data/lib/decision_agent/web/public/simulation.html +23 -7
  141. data/lib/decision_agent/web/public/simulation_impact.html +37 -20
  142. data/lib/decision_agent/web/public/simulation_replay.html +19 -23
  143. data/lib/decision_agent/web/public/simulation_shadow.html +36 -21
  144. data/lib/decision_agent/web/public/simulation_whatif.html +38 -21
  145. data/lib/decision_agent/web/public/styles.css +0 -0
  146. data/lib/decision_agent/web/public/users.html +1 -1
  147. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  148. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  149. data/lib/decision_agent/web/server.rb +2038 -1851
  150. data/lib/decision_agent.rb +3 -43
  151. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  152. data/lib/generators/decision_agent/install/templates/README +0 -0
  153. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  154. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  155. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  156. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +0 -0
  157. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +0 -0
  158. data/lib/generators/decision_agent/install/templates/decision_log.rb +0 -0
  159. data/lib/generators/decision_agent/install/templates/error_metric.rb +0 -0
  160. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +0 -0
  161. data/lib/generators/decision_agent/install/templates/migration.rb +16 -0
  162. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +0 -2
  163. data/lib/generators/decision_agent/install/templates/performance_metric.rb +0 -0
  164. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  165. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  166. data/lib/generators/decision_agent/install/templates/rule_version_tag.rb +23 -0
  167. data/lib/generators/decision_agent/install/templates/versioning_migration.rb +44 -0
  168. data/lib/generators/decision_agent/monitoring_migration/monitoring_migration_generator.rb +67 -0
  169. data/lib/generators/decision_agent/versioning_migration/versioning_migration_generator.rb +57 -0
  170. metadata +66 -25
  171. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +0 -86
  172. data/lib/decision_agent/data_enrichment/cache_adapter.rb +0 -49
  173. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +0 -135
  174. data/lib/decision_agent/data_enrichment/client.rb +0 -220
  175. data/lib/decision_agent/data_enrichment/config.rb +0 -78
  176. 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
- 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
- }
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: { 'Content-Type': 'application/json' },
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: { 'Content-Type': 'application/json' },
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
- if (baseTag.href.startsWith('/')) {
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
- if (pathname.includes('/decision_agent')) {
280
- const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
281
- if (match) {
282
- return match[1].endsWith('/') ? match[1] : match[1] + '/';
283
- }
284
- }
285
- return pathname.substring(0, pathname.lastIndexOf('/') + 1) || '/';
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
- contextCount++;
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 ${contextCount}</strong>
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" id="contextFields${contextCount}">
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: { 'Content-Type': 'application/json' },
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
- margin-top: 20px;
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
- margin-right: 15px;
114
+ padding: 0.5rem 1rem;
115
+ border-radius: 6px;
110
116
  text-decoration: none;
111
- color: var(--primary-color);
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
- if (baseTag.href.startsWith('/')) {
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
- if (pathname.includes('/decision_agent')) {
270
- const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
271
- if (match) {
272
- return match[1].endsWith('/') ? match[1] : match[1] + '/';
273
- }
274
- }
275
- return pathname.substring(0, pathname.lastIndexOf('/') + 1) || '/';
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: { 'Content-Type': 'application/json' },
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: { 'Content-Type': 'application/json' },
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