decision_agent 0.3.0 → 1.1.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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -15,6 +15,11 @@ module DecisionAgent
15
15
  # DMN Editor Backend
16
16
  # Provides API endpoints for visual DMN modeling
17
17
  class DmnEditor
18
+ require_relative "dmn_editor/serialization"
19
+ require_relative "dmn_editor/xml_builder"
20
+ include DmnEditor::Serialization
21
+ include DmnEditor::XmlBuilder
22
+
18
23
  attr_reader :storage
19
24
 
20
25
  def initialize(storage: nil)
@@ -249,8 +254,9 @@ module DecisionAgent
249
254
  model = retrieve_model(model_id)
250
255
  return nil unless model
251
256
 
252
- exporter = Dmn::Exporter.new
253
- exporter.export(model)
257
+ # Convert model to DMN XML directly using the model's to_xml method
258
+ # or generate XML from the model's structure
259
+ generate_dmn_xml(model)
254
260
  end
255
261
 
256
262
  # Import DMN model from XML
@@ -356,71 +362,6 @@ module DecisionAgent
356
362
  table.instance_variable_set(:@outputs, logic[:outputs]) if logic[:outputs]
357
363
  table.instance_variable_set(:@rules, logic[:rules]) if logic[:rules]
358
364
  end
359
-
360
- def serialize_model(model)
361
- {
362
- id: model.id,
363
- name: model.name,
364
- namespace: model.namespace,
365
- decisions: model.decisions.map { |d| serialize_decision(d) }
366
- }
367
- end
368
-
369
- def serialize_decision(decision)
370
- result = {
371
- id: decision.id,
372
- name: decision.name
373
- }
374
-
375
- if decision.decision_table
376
- result[:decision_table] = serialize_decision_table(decision.decision_table)
377
- elsif decision.decision_tree
378
- result[:decision_tree] = decision.decision_tree.to_h
379
- elsif decision.instance_variable_get(:@literal_expression)
380
- result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
381
- end
382
-
383
- result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
384
-
385
- result
386
- end
387
-
388
- def serialize_decision_table(table)
389
- {
390
- id: table.id,
391
- hit_policy: table.hit_policy,
392
- inputs: table.inputs.map { |i| serialize_input(i) },
393
- outputs: table.outputs.map { |o| serialize_output(o) },
394
- rules: table.rules.map { |r| serialize_rule(r) }
395
- }
396
- end
397
-
398
- def serialize_input(input)
399
- {
400
- id: input.id,
401
- label: input.label,
402
- type_ref: input.type_ref,
403
- expression: input.expression
404
- }
405
- end
406
-
407
- def serialize_output(output)
408
- {
409
- id: output.id,
410
- label: output.label,
411
- type_ref: output.type_ref,
412
- name: output.name
413
- }
414
- end
415
-
416
- def serialize_rule(rule)
417
- {
418
- id: rule.id,
419
- input_entries: rule.input_entries,
420
- output_entries: rule.output_entries,
421
- description: rule.description
422
- }
423
- end
424
365
  end
425
366
  end
426
367
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack"
2
4
 
3
5
  module DecisionAgent
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rack"
2
4
  require "json"
3
5
 
@@ -58,7 +60,7 @@ module DecisionAgent
58
60
 
59
61
  def extract_resource_id(env)
60
62
  request = Rack::Request.new(env)
61
- # Try params first (for query parameters and Sinatra path params)
63
+ # Try params first (for query parameters and path params)
62
64
  resource_id = request.params["id"] || request.params["rule_id"] || request.params["version_id"]
63
65
 
64
66
  # If not in params, try to extract from path (e.g., /api/rules/123 -> 123)
@@ -84,6 +84,7 @@ class RuleBuilder {
84
84
 
85
85
  // Actions
86
86
  document.getElementById('validateBtn').addEventListener('click', () => this.validateRules());
87
+ document.getElementById('testRuleBtn').addEventListener('click', () => this.openTestRuleModal());
87
88
  document.getElementById('clearBtn').addEventListener('click', () => this.clearAll());
88
89
  document.getElementById('loadExampleBtn').addEventListener('click', () => this.loadExample());
89
90
 
@@ -101,6 +102,11 @@ class RuleBuilder {
101
102
  document.getElementById('closeCompareBtn').addEventListener('click', () => this.closeCompareModal());
102
103
  document.getElementById('closeCompareModalBtn').addEventListener('click', () => this.closeCompareModal());
103
104
 
105
+ // Test Rule
106
+ document.getElementById('runTestBtn').addEventListener('click', () => this.runTest());
107
+ document.getElementById('closeTestRuleBtn').addEventListener('click', () => this.closeTestRuleModal());
108
+ document.getElementById('closeTestRuleModalBtn').addEventListener('click', () => this.closeTestRuleModal());
109
+
104
110
  // Modal close on outside click
105
111
  document.getElementById('ruleModal').addEventListener('click', (e) => {
106
112
  if (e.target.id === 'ruleModal') {
@@ -307,20 +313,40 @@ class RuleBuilder {
307
313
  'between': '[min, max] or {"min": 0, "max": 100}',
308
314
  'modulo': '[divisor, remainder] or {"divisor": 2, "remainder": 0}',
309
315
 
310
- // Mathematical functions
311
- 'sin': 'expected result (e.g., 0.0)',
312
- 'cos': 'expected result (e.g., 1.0)',
313
- 'tan': 'expected result (e.g., 0.0)',
314
- 'sqrt': 'expected result (e.g., 3.0)',
316
+ // Mathematical functions - Trigonometric
317
+ 'sin': 'expected result (e.g., 0.0 for sin(0))',
318
+ 'cos': 'expected result (e.g., 1.0 for cos(0))',
319
+ 'tan': 'expected result (e.g., 0.0 for tan(0))',
320
+ 'asin': 'expected result (e.g., 1.571 for asin(1))',
321
+ 'acos': 'expected result (e.g., 0.0 for acos(1))',
322
+ 'atan': 'expected result (e.g., 0.785 for atan(1))',
323
+ 'atan2': '{"y": 1, "result": 0.785} or [1, 0.785]',
324
+ // Hyperbolic
325
+ 'sinh': 'expected result (e.g., 0.0 for sinh(0))',
326
+ 'cosh': 'expected result (e.g., 1.0 for cosh(0))',
327
+ 'tanh': 'expected result (e.g., 0.0 for tanh(0))',
328
+ // Power and roots
329
+ 'sqrt': 'expected result (e.g., 3.0 for sqrt(9))',
330
+ 'cbrt': 'expected result (e.g., 2.0 for cbrt(8))',
315
331
  'power': '[exponent, result] or {"exponent": 2, "result": 4}',
316
- 'exp': 'expected result (e.g., 2.718)',
317
- 'log': 'expected result (e.g., 0.0)',
318
- 'round': 'expected rounded value (e.g., 3)',
319
- 'floor': 'expected floor value (e.g., 3)',
320
- 'ceil': 'expected ceiling value (e.g., 4)',
321
- 'abs': 'expected absolute value (e.g., 5)',
322
- 'min': 'expected minimum value (e.g., 1)',
323
- 'max': 'expected maximum value (e.g., 10)',
332
+ 'exp': 'expected result (e.g., 2.718 for exp(1))',
333
+ // Logarithmic
334
+ 'log': 'expected result (e.g., 0.0 for log(1))',
335
+ 'log10': 'expected result (e.g., 2.0 for log10(100))',
336
+ 'log2': 'expected result (e.g., 3.0 for log2(8))',
337
+ // Rounding
338
+ 'round': 'expected rounded value (e.g., 3 for round(3.4))',
339
+ 'floor': 'expected floor value (e.g., 3 for floor(3.9))',
340
+ 'ceil': 'expected ceiling value (e.g., 4 for ceil(3.1))',
341
+ 'truncate': 'expected truncated value (e.g., 3 for truncate(3.9))',
342
+ 'abs': 'expected absolute value (e.g., 5 for abs(-5))',
343
+ // Advanced math
344
+ 'factorial': 'expected result (e.g., 120 for factorial(5))',
345
+ 'gcd': '{"other": 12, "result": 6} or [12, 6]',
346
+ 'lcm': '{"other": 12, "result": 36} or [12, 36]',
347
+ // Statistical (these are for arrays)
348
+ 'min': 'expected minimum value (e.g., 1 for min([3, 1, 5, 2]))',
349
+ 'max': 'expected maximum value (e.g., 5 for max([3, 1, 5, 2]))',
324
350
 
325
351
  // Statistical aggregations
326
352
  'sum': 'expected sum (e.g., 100) or {"min": 50, "max": 150}',
@@ -440,19 +466,40 @@ class RuleBuilder {
440
466
  'day_of_week': 'Day name (monday) or number (0=Sunday, 1=Monday, ...)',
441
467
  'within_radius': 'JSON: {"center": {"lat": y, "lon": x}, "radius": km}',
442
468
  'in_polygon': 'Array of coordinates: [{"lat": y, "lon": x}, ...]',
443
- 'sin': 'Expected result of sin(field_value). Example: 0.0 for sin(0)',
444
- 'cos': 'Expected result of cos(field_value). Example: 1.0 for cos(0)',
445
- 'tan': 'Expected result of tan(field_value). Example: 0.0 for tan(0)',
446
- 'sqrt': 'Expected result of sqrt(field_value). Example: 3.0 for sqrt(9)',
447
- 'power': 'Power: [exponent, result] or {"exponent": 2, "result": 4}. Checks if field^exponent == result',
448
- 'exp': 'Expected result of exp(field_value) = e^field_value. Example: 2.718 for exp(1)',
449
- 'log': 'Expected result of log(field_value) = natural logarithm. Example: 0.0 for log(1)',
450
- 'round': 'Expected rounded value. Example: 3 for round(3.4)',
451
- 'floor': 'Expected floor value. Example: 3 for floor(3.9)',
452
- 'ceil': 'Expected ceiling value. Example: 4 for ceil(3.1)',
453
- 'abs': 'Expected absolute value. Example: 5 for abs(-5) or abs(5)',
454
- 'min': 'Expected minimum value from array. Example: 1 for min([3, 1, 5, 2])',
455
- 'max': 'Expected maximum value from array. Example: 5 for max([3, 1, 5, 2])'
469
+ // Trigonometric functions
470
+ 'sin': 'Expected result of sin(field_value). Example: 0.0 for sin(0), 1.0 for sin(π/2)',
471
+ 'cos': 'Expected result of cos(field_value). Example: 1.0 for cos(0), 0.0 for cos(π/2)',
472
+ 'tan': 'Expected result of tan(field_value). Example: 0.0 for tan(0), 1.0 for tan(π/4)',
473
+ 'asin': 'Expected result of asin(field_value). Input must be [-1, 1]. Example: 1.571 for asin(1) = π/2',
474
+ 'acos': 'Expected result of acos(field_value). Input must be [-1, 1]. Example: 0.0 for acos(1)',
475
+ 'atan': 'Expected result of atan(field_value). Example: 0.785 for atan(1) = π/4',
476
+ 'atan2': 'atan2(field_value, y). Format: {"y": 1, "result": 0.785} or [1, 0.785]',
477
+ // Hyperbolic functions
478
+ 'sinh': 'Expected result of sinh(field_value). Example: 0.0 for sinh(0), 1.175 for sinh(1)',
479
+ 'cosh': 'Expected result of cosh(field_value). Example: 1.0 for cosh(0), 1.543 for cosh(1)',
480
+ 'tanh': 'Expected result of tanh(field_value). Example: 0.0 for tanh(0), 0.762 for tanh(1)',
481
+ // Power and roots
482
+ 'sqrt': 'Expected result of sqrt(field_value). Example: 3.0 for sqrt(9), 5.0 for sqrt(25)',
483
+ 'cbrt': 'Expected result of cbrt(field_value) = cube root. Example: 2.0 for cbrt(8), -2.0 for cbrt(-8)',
484
+ 'power': 'Power: Checks if field^exponent == result. Format: [exponent, result] or {"exponent": 2, "result": 4}. Example: [2, 4] means 2^2 == 4',
485
+ 'exp': 'Expected result of exp(field_value) = e^field_value. Example: 2.718 for exp(1), 7.389 for exp(2)',
486
+ // Logarithmic functions
487
+ 'log': 'Expected result of log(field_value) = natural logarithm (base e). Example: 0.0 for log(1), 2.303 for log(10)',
488
+ 'log10': 'Expected result of log10(field_value) = base 10 logarithm. Example: 2.0 for log10(100), 3.0 for log10(1000)',
489
+ 'log2': 'Expected result of log2(field_value) = base 2 logarithm. Example: 3.0 for log2(8), 4.0 for log2(16)',
490
+ // Rounding functions
491
+ 'round': 'Expected rounded value to nearest integer. Example: 3 for round(3.4), 4 for round(3.6)',
492
+ 'floor': 'Expected floor value (round down). Example: 3 for floor(3.9), -4 for floor(-3.1)',
493
+ 'ceil': 'Expected ceiling value (round up). Example: 4 for ceil(3.1), -3 for ceil(-3.9)',
494
+ 'truncate': 'Expected truncated value (remove decimal part). Example: 3 for truncate(3.9), -3 for truncate(-3.9)',
495
+ 'abs': 'Expected absolute value. Example: 5 for abs(-5) or abs(5), 3.14 for abs(-3.14)',
496
+ // Advanced mathematical functions
497
+ 'factorial': 'Expected result of factorial(field_value). Input must be non-negative integer. Example: 120 for factorial(5), 720 for factorial(6)',
498
+ 'gcd': 'Greatest Common Divisor: gcd(field_value, other) == result. Format: {"other": 12, "result": 6} or [12, 6]. Example: gcd(18, 12) == 6',
499
+ 'lcm': 'Least Common Multiple: lcm(field_value, other) == result. Format: {"other": 12, "result": 36} or [12, 36]. Example: lcm(9, 12) == 36',
500
+ // Statistical aggregations (for arrays)
501
+ 'min': 'Expected minimum value from array. Example: 1 for min([3, 1, 5, 2]), 0.5 for min([1.5, 2.0, 0.5])',
502
+ 'max': 'Expected maximum value from array. Example: 5 for max([3, 1, 5, 2]), 100.0 for max([10.5, 100.0, 50.0])'
456
503
  };
457
504
 
458
505
  if (hints[operator]) {
@@ -1134,6 +1181,119 @@ class RuleBuilder {
1134
1181
  document.getElementById('compareVersionsModal').classList.add('hidden');
1135
1182
  }
1136
1183
 
1184
+ getRulesJSON() {
1185
+ const version = document.getElementById('rulesetVersion').value || '1.0';
1186
+ const ruleset = document.getElementById('rulesetName').value || 'my_ruleset';
1187
+ const rules = this.rules.map(rule => ({
1188
+ id: rule.id,
1189
+ if: rule.if,
1190
+ then: rule.then
1191
+ }));
1192
+
1193
+ return {
1194
+ version: version,
1195
+ ruleset: ruleset,
1196
+ rules: rules
1197
+ };
1198
+ }
1199
+
1200
+ openTestRuleModal() {
1201
+ document.getElementById('testRuleModal').classList.remove('hidden');
1202
+ document.getElementById('testContext').value = '{}';
1203
+ document.getElementById('testResults').classList.add('hidden');
1204
+ }
1205
+
1206
+ closeTestRuleModal() {
1207
+ document.getElementById('testRuleModal').classList.add('hidden');
1208
+ }
1209
+
1210
+ async runTest() {
1211
+ const contextText = document.getElementById('testContext').value.trim();
1212
+
1213
+ // Get current rules
1214
+ const rules = this.getRulesJSON();
1215
+
1216
+ if (!rules || !rules.rules || rules.rules.length === 0) {
1217
+ alert('Please add at least one rule before testing');
1218
+ return;
1219
+ }
1220
+
1221
+ let context;
1222
+ try {
1223
+ context = contextText ? JSON.parse(contextText) : {};
1224
+ } catch (e) {
1225
+ alert('Invalid JSON in context field: ' + e.message);
1226
+ return;
1227
+ }
1228
+
1229
+ try {
1230
+ const response = await fetch(`${this.basePath}api/evaluate`, {
1231
+ method: 'POST',
1232
+ headers: this.getAuthHeaders(),
1233
+ body: JSON.stringify({
1234
+ rules: rules,
1235
+ context: context
1236
+ })
1237
+ });
1238
+
1239
+ const data = await response.json();
1240
+
1241
+ if (!response.ok || !data.success) {
1242
+ alert('Test failed: ' + (data.error || 'Unknown error'));
1243
+ return;
1244
+ }
1245
+
1246
+ // Display results
1247
+ const resultsDiv = document.getElementById('testResults');
1248
+ resultsDiv.classList.remove('hidden');
1249
+
1250
+ if (data.decision) {
1251
+ document.getElementById('testDecisionValue').textContent = data.decision;
1252
+ document.getElementById('testConfidenceValue').textContent = (data.confidence || 0).toFixed(3);
1253
+ document.getElementById('testReasonValue').textContent = data.reason || 'N/A';
1254
+
1255
+ // Display explainability
1256
+ const becauseList = document.getElementById('testBecauseList');
1257
+ const failedList = document.getElementById('testFailedList');
1258
+
1259
+ if (data.because && data.because.length > 0) {
1260
+ becauseList.innerHTML = '';
1261
+ data.because.forEach(condition => {
1262
+ const li = document.createElement('li');
1263
+ li.textContent = condition;
1264
+ li.style.color = '#28a745';
1265
+ becauseList.appendChild(li);
1266
+ });
1267
+ document.getElementById('testBecause').style.display = 'block';
1268
+ } else {
1269
+ document.getElementById('testBecause').style.display = 'none';
1270
+ }
1271
+
1272
+ if (data.failed_conditions && data.failed_conditions.length > 0) {
1273
+ failedList.innerHTML = '';
1274
+ data.failed_conditions.forEach(condition => {
1275
+ const li = document.createElement('li');
1276
+ li.textContent = condition;
1277
+ li.style.color = '#dc3545';
1278
+ failedList.appendChild(li);
1279
+ });
1280
+ document.getElementById('testFailedConditions').style.display = 'block';
1281
+ } else {
1282
+ document.getElementById('testFailedConditions').style.display = 'none';
1283
+ }
1284
+
1285
+ document.getElementById('testExplainability').style.display = 'block';
1286
+ } else {
1287
+ document.getElementById('testDecisionValue').textContent = 'No match';
1288
+ document.getElementById('testConfidenceValue').textContent = 'N/A';
1289
+ document.getElementById('testReasonValue').textContent = data.message || 'No rules matched';
1290
+ document.getElementById('testExplainability').style.display = 'none';
1291
+ }
1292
+ } catch (error) {
1293
+ alert('Error running test: ' + error.message);
1294
+ }
1295
+ }
1296
+
1137
1297
  async deleteVersion(versionId) {
1138
1298
  if (!confirm('Are you sure you want to delete this version? This action cannot be undone.')) {
1139
1299
  return;
@@ -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>DecisionAgent Batch Testing</title>
7
- <link rel="stylesheet" href="styles.css">
7
+ <link rel="stylesheet" href="/styles.css">
8
8
  <style>
9
9
  .batch-testing-container {
10
10
  max-width: 1400px;
@@ -180,7 +180,11 @@
180
180
  <h2>Upload Test Scenarios</h2>
181
181
  </div>
182
182
  <p>Upload a CSV or Excel file with your test scenarios. Required columns: <code>id</code>, context fields. Optional: <code>expected_decision</code>, <code>expected_confidence</code></p>
183
-
183
+ <p style="margin-top: 10px;">
184
+ <strong>Not sure about the format?</strong> Download a sample file:
185
+ <a href="/sample_batch.csv" download style="color: #007bff; text-decoration: none; margin-left: 5px;">📥 Sample CSV</a>
186
+ </p>
187
+
184
188
  <div class="file-upload-area" id="uploadArea">
185
189
  <p>📁 Drag and drop your CSV/Excel file here, or click to browse</p>
186
190
  <input type="file" id="fileInput" accept=".csv,.xlsx,.xls" style="display: none;">
@@ -333,6 +337,13 @@
333
337
  return dirPath || '/';
334
338
  }
335
339
 
340
+ function getAuthHeaders() {
341
+ const token = localStorage.getItem('auth_token');
342
+ const headers = { 'Content-Type': 'application/json' };
343
+ if (token) headers['Authorization'] = `Bearer ${token}`;
344
+ return headers;
345
+ }
346
+
336
347
  const basePath = getBasePath();
337
348
  let currentTestId = null;
338
349
  let currentScenarios = null;
@@ -376,8 +387,13 @@
376
387
  progressFill.style.width = '0%';
377
388
  progressText.textContent = 'Uploading...';
378
389
 
390
+ const uploadHeaders = {};
391
+ const token = localStorage.getItem('auth_token');
392
+ if (token) uploadHeaders['Authorization'] = `Bearer ${token}`;
393
+
379
394
  fetch(`${basePath}api/testing/batch/import`, {
380
395
  method: 'POST',
396
+ headers: uploadHeaders,
381
397
  body: formData
382
398
  })
383
399
  .then(response => response.json())
@@ -393,6 +409,7 @@
393
409
  uploadStatus.textContent = `Successfully imported ${data.scenarios_count} scenarios. Test ID: ${data.test_id}`;
394
410
  uploadProgress.classList.add('hidden');
395
411
  document.getElementById('runTestBtn').disabled = false;
412
+ document.getElementById('resumeTestBtn').disabled = false;
396
413
 
397
414
  if (data.errors && data.errors.length > 0) {
398
415
  uploadStatus.textContent += `\nWarnings: ${data.errors.join(', ')}`;
@@ -430,7 +447,7 @@
430
447
  JSON.parse(rulesJson);
431
448
  fetch(`${basePath}api/validate`, {
432
449
  method: 'POST',
433
- headers: { 'Content-Type': 'application/json' },
450
+ headers: getAuthHeaders(),
434
451
  body: JSON.stringify(JSON.parse(rulesJson))
435
452
  })
436
453
  .then(response => response.json())
@@ -483,7 +500,7 @@
483
500
 
484
501
  fetch(`${basePath}api/testing/batch/run`, {
485
502
  method: 'POST',
486
- headers: { 'Content-Type': 'application/json' },
503
+ headers: getAuthHeaders(),
487
504
  body: JSON.stringify({
488
505
  test_id: currentTestId,
489
506
  rules: JSON.parse(rulesJson),
@@ -510,9 +527,66 @@
510
527
  });
511
528
  });
512
529
 
530
+ // Resume test - re-run the previously imported test
531
+ document.getElementById('resumeTestBtn').addEventListener('click', () => {
532
+ if (!currentTestId) {
533
+ alert('No test to resume. Please import test data first.');
534
+ return;
535
+ }
536
+
537
+ const rulesJson = document.getElementById('rulesJson').value;
538
+ try {
539
+ JSON.parse(rulesJson);
540
+ } catch (e) {
541
+ alert('Invalid rules JSON: ' + e.message);
542
+ return;
543
+ }
544
+
545
+ const runStatus = document.getElementById('runStatus');
546
+ const runProgress = document.getElementById('runProgress');
547
+ const runProgressText = document.getElementById('runProgressText');
548
+
549
+ runStatus.classList.add('hidden');
550
+ runProgress.classList.remove('hidden');
551
+ runProgressText.textContent = 'Resuming...';
552
+
553
+ const options = {
554
+ parallel: document.getElementById('parallelExecution').checked,
555
+ thread_count: parseInt(document.getElementById('threadCount').value) || 4
556
+ };
557
+
558
+ fetch(`${basePath}api/testing/batch/run`, {
559
+ method: 'POST',
560
+ headers: getAuthHeaders(),
561
+ body: JSON.stringify({
562
+ test_id: currentTestId,
563
+ rules: JSON.parse(rulesJson),
564
+ options: options
565
+ })
566
+ })
567
+ .then(response => response.json())
568
+ .then(data => {
569
+ if (data.error) {
570
+ runStatus.className = 'error-message';
571
+ runStatus.textContent = `Error: ${data.error}`;
572
+ runProgress.classList.add('hidden');
573
+ } else {
574
+ runStatus.className = 'success-message';
575
+ runStatus.textContent = 'Test resumed and completed successfully!';
576
+ runProgress.classList.add('hidden');
577
+ loadResults(currentTestId);
578
+ }
579
+ })
580
+ .catch(error => {
581
+ runStatus.className = 'error-message';
582
+ runStatus.textContent = `Error: ${error.message}`;
583
+ runProgress.classList.add('hidden');
584
+ });
585
+ });
586
+
513
587
  // Load results
514
588
  function loadResults(testId) {
515
- fetch(`${basePath}api/testing/batch/${testId}/results`)
589
+ fetch(`${basePath}api/testing/batch/${testId}/results`, { headers: getAuthHeaders() })
516
590
  .then(response => response.json())
517
591
  .then(data => {
518
592
  if (data.error) {
@@ -600,7 +674,7 @@
600
674
 
601
675
  // Load coverage
602
676
  function loadCoverage(testId) {
603
- fetch(`${basePath}api/testing/batch/${testId}/coverage`)
677
+ fetch(`${basePath}api/testing/batch/${testId}/coverage`, { headers: getAuthHeaders() })
604
678
  .then(response => response.json())
605
679
  .then(data => {
606
680
  if (data.error) {
@@ -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>DMN Editor - DecisionAgent</title>
7
- <link rel="stylesheet" href="dmn-editor.css">
7
+ <link rel="stylesheet" href="/dmn-editor.css">
8
8
  </head>
9
9
  <body>
10
10
  <div class="container">
@@ -245,6 +245,6 @@
245
245
 
246
246
  <div id="notification" class="notification"></div>
247
247
 
248
- <script src="dmn-editor.js"></script>
248
+ <script src="/dmn-editor.js"></script>
249
249
  </body>
250
250
  </html>