decision_agent 0.1.4 → 0.1.6

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
  4. data/lib/decision_agent/agent.rb +5 -3
  5. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  6. data/lib/decision_agent/auth/authenticator.rb +127 -0
  7. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  8. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  9. data/lib/decision_agent/auth/permission.rb +29 -0
  10. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  11. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  12. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  13. data/lib/decision_agent/auth/role.rb +56 -0
  14. data/lib/decision_agent/auth/session.rb +33 -0
  15. data/lib/decision_agent/auth/session_manager.rb +57 -0
  16. data/lib/decision_agent/auth/user.rb +70 -0
  17. data/lib/decision_agent/context.rb +24 -4
  18. data/lib/decision_agent/decision.rb +10 -3
  19. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  20. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  21. data/lib/decision_agent/errors.rb +38 -0
  22. data/lib/decision_agent/evaluation.rb +10 -3
  23. data/lib/decision_agent/evaluation_validator.rb +8 -13
  24. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  25. data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
  26. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  27. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  28. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  29. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  30. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  31. data/lib/decision_agent/version.rb +10 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  33. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  34. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  35. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  36. data/lib/decision_agent/web/public/app.js +184 -29
  37. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  38. data/lib/decision_agent/web/public/index.html +37 -9
  39. data/lib/decision_agent/web/public/login.html +298 -0
  40. data/lib/decision_agent/web/public/users.html +679 -0
  41. data/lib/decision_agent/web/server.rb +873 -7
  42. data/lib/decision_agent.rb +52 -0
  43. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  45. data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
  46. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  47. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  48. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  49. data/spec/advanced_operators_spec.rb +1003 -0
  50. data/spec/agent_spec.rb +40 -0
  51. data/spec/audit_adapters_spec.rb +18 -0
  52. data/spec/auth/access_audit_logger_spec.rb +394 -0
  53. data/spec/auth/authenticator_spec.rb +112 -0
  54. data/spec/auth/password_reset_spec.rb +294 -0
  55. data/spec/auth/permission_checker_spec.rb +207 -0
  56. data/spec/auth/permission_spec.rb +73 -0
  57. data/spec/auth/rbac_adapter_spec.rb +550 -0
  58. data/spec/auth/rbac_config_spec.rb +82 -0
  59. data/spec/auth/role_spec.rb +51 -0
  60. data/spec/auth/session_manager_spec.rb +172 -0
  61. data/spec/auth/session_spec.rb +112 -0
  62. data/spec/auth/user_spec.rb +130 -0
  63. data/spec/context_spec.rb +43 -0
  64. data/spec/decision_agent_spec.rb +96 -0
  65. data/spec/decision_spec.rb +423 -0
  66. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  67. data/spec/evaluation_spec.rb +364 -0
  68. data/spec/evaluation_validator_spec.rb +165 -0
  69. data/spec/examples.txt +1542 -612
  70. data/spec/monitoring/metrics_collector_spec.rb +220 -2
  71. data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
  72. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  73. data/spec/performance_optimizations_spec.rb +486 -0
  74. data/spec/spec_helper.rb +23 -0
  75. data/spec/testing/batch_test_importer_spec.rb +693 -0
  76. data/spec/testing/batch_test_runner_spec.rb +307 -0
  77. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  78. data/spec/testing/test_result_comparator_spec.rb +392 -0
  79. data/spec/testing/test_scenario_spec.rb +113 -0
  80. data/spec/versioning/adapter_spec.rb +156 -0
  81. data/spec/versioning_spec.rb +253 -0
  82. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  83. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  84. data/spec/web_ui_rack_spec.rb +1705 -0
  85. metadata +99 -6
@@ -1,10 +1,63 @@
1
1
  // DecisionAgent Rule Builder - Main Application
2
+
3
+ // Helper function to get the base path for API calls
4
+ // This handles both standalone and Rails-mounted scenarios
5
+ function getBasePath() {
6
+ // Try to get from <base> tag first
7
+ const baseTag = document.querySelector('base');
8
+ if (baseTag && baseTag.href) {
9
+ try {
10
+ // Parse the base URL
11
+ const baseUrl = new URL(baseTag.href, window.location.href);
12
+ let path = baseUrl.pathname;
13
+
14
+ // Ensure it ends with / for proper path joining
15
+ if (path && !path.endsWith('/')) {
16
+ path += '/';
17
+ }
18
+
19
+ return path;
20
+ } catch (e) {
21
+ // If URL parsing fails, try simple string matching
22
+ // Check if it's an absolute path
23
+ if (baseTag.href.startsWith('/')) {
24
+ // Extract path from absolute URL
25
+ const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
26
+ if (match && match[2]) {
27
+ return match[2].endsWith('/') ? match[2] : match[2] + '/';
28
+ }
29
+ } else if (baseTag.href.startsWith('./')) {
30
+ // Relative path - use current location's directory
31
+ const pathname = window.location.pathname;
32
+ // Get directory path (remove filename if present)
33
+ const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
34
+ return dirPath;
35
+ }
36
+ }
37
+ }
38
+
39
+ // Fallback: detect from current location
40
+ // If we're at /decision_agent, use that as base
41
+ const pathname = window.location.pathname;
42
+ if (pathname.includes('/decision_agent')) {
43
+ const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
44
+ if (match) {
45
+ return match[1].endsWith('/') ? match[1] : match[1] + '/';
46
+ }
47
+ }
48
+
49
+ // Default: use current directory
50
+ const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
51
+ return dirPath || '/';
52
+ }
53
+
2
54
  class RuleBuilder {
3
55
  constructor() {
4
56
  this.rules = [];
5
57
  this.currentRule = null;
6
58
  this.currentRuleIndex = null;
7
59
  this.currentCondition = null;
60
+ this.basePath = getBasePath();
8
61
  this.init();
9
62
  }
10
63
 
@@ -13,6 +66,15 @@ class RuleBuilder {
13
66
  this.updateJSONPreview();
14
67
  }
15
68
 
69
+ getAuthHeaders() {
70
+ const token = localStorage.getItem('auth_token');
71
+ const headers = { 'Content-Type': 'application/json' };
72
+ if (token) {
73
+ headers['Authorization'] = `Bearer ${token}`;
74
+ }
75
+ return headers;
76
+ }
77
+
16
78
  bindEvents() {
17
79
  // Rule management
18
80
  document.getElementById('addRuleBtn').addEventListener('click', () => this.openRuleModal());
@@ -215,11 +277,70 @@ class RuleBuilder {
215
277
  const valueInput = selectElement.parentElement.querySelector('.field-value');
216
278
  const operator = selectElement.value;
217
279
 
280
+ // Operators that don't need a value
218
281
  if (operator === 'present' || operator === 'blank') {
219
282
  valueInput.style.display = 'none';
220
283
  valueInput.value = '';
284
+ return;
285
+ }
286
+
287
+ valueInput.style.display = 'block';
288
+
289
+ // Set helpful placeholders based on operator
290
+ const placeholders = {
291
+ // Basic operators
292
+ 'eq': 'value',
293
+ 'neq': 'value',
294
+ 'gt': '100',
295
+ 'gte': '100',
296
+ 'lt': '100',
297
+ 'lte': '100',
298
+ 'in': '["value1", "value2"]',
299
+
300
+ // String operators
301
+ 'contains': 'substring',
302
+ 'starts_with': 'prefix',
303
+ 'ends_with': 'suffix',
304
+ 'matches': '^pattern.*$',
305
+
306
+ // Numeric operators
307
+ 'between': '[min, max] or {"min": 0, "max": 100}',
308
+ 'modulo': '[divisor, remainder] or {"divisor": 2, "remainder": 0}',
309
+
310
+ // Date/time operators
311
+ 'before_date': '2025-12-31',
312
+ 'after_date': '2024-01-01',
313
+ 'within_days': '7',
314
+ 'day_of_week': 'monday or 1',
315
+
316
+ // Collection operators
317
+ 'contains_all': '["item1", "item2"]',
318
+ 'contains_any': '["item1", "item2"]',
319
+ 'intersects': '["item1", "item2"]',
320
+ 'subset_of': '["valid1", "valid2", "valid3"]',
321
+
322
+ // Geospatial operators
323
+ 'within_radius': '{"center": {"lat": 40.7128, "lon": -74.0060}, "radius": 10}',
324
+ 'in_polygon': '[{"lat": 40, "lon": -74}, {"lat": 41, "lon": -74}, ...]'
325
+ };
326
+
327
+ valueInput.placeholder = placeholders[operator] || 'value';
328
+
329
+ // Add title attribute with helpful hint
330
+ const hints = {
331
+ 'between': 'Range: [min, max] or {"min": 0, "max": 100}',
332
+ 'modulo': 'Modulo: [divisor, remainder] or {"divisor": 2, "remainder": 0}',
333
+ 'matches': 'Regular expression pattern (e.g., ^user@company\\.com$)',
334
+ 'within_days': 'Number of days from now (e.g., 7 for within a week)',
335
+ 'day_of_week': 'Day name (monday) or number (0=Sunday, 1=Monday, ...)',
336
+ 'within_radius': 'JSON: {"center": {"lat": y, "lon": x}, "radius": km}',
337
+ 'in_polygon': 'Array of coordinates: [{"lat": y, "lon": x}, ...]'
338
+ };
339
+
340
+ if (hints[operator]) {
341
+ valueInput.title = hints[operator];
221
342
  } else {
222
- valueInput.style.display = 'block';
343
+ valueInput.title = '';
223
344
  }
224
345
  }
225
346
 
@@ -404,7 +525,7 @@ class RuleBuilder {
404
525
  };
405
526
 
406
527
  try {
407
- const response = await fetch('/api/validate', {
528
+ const response = await fetch(`${this.basePath}api/validate`, {
408
529
  method: 'POST',
409
530
  headers: {
410
531
  'Content-Type': 'application/json'
@@ -526,48 +647,75 @@ class RuleBuilder {
526
647
  loadExample() {
527
648
  const example = {
528
649
  version: '1.0',
529
- ruleset: 'example_approval_rules',
650
+ ruleset: 'example_advanced_rules',
530
651
  rules: [
531
652
  {
532
- id: 'high_priority_auto_approve',
653
+ id: 'corporate_email_approval',
533
654
  if: {
534
655
  all: [
535
- { field: 'priority', op: 'eq', value: 'high' },
536
- { field: 'user.role', op: 'eq', value: 'admin' }
656
+ { field: 'email', op: 'ends_with', value: '@company.com' },
657
+ { field: 'age', op: 'between', value: [18, 65] }
537
658
  ]
538
659
  },
539
660
  then: {
540
661
  decision: 'approve',
541
662
  weight: 0.95,
542
- reason: 'High priority request from admin'
663
+ reason: 'Corporate email with valid age range'
543
664
  }
544
665
  },
545
666
  {
546
- id: 'low_amount_approve',
667
+ id: 'weekend_special_offer',
547
668
  if: {
548
669
  all: [
549
- { field: 'amount', op: 'lt', value: 1000 },
550
- { field: 'status', op: 'eq', value: 'active' }
670
+ { field: 'booking_date', op: 'day_of_week', value: 'saturday' },
671
+ { field: 'amount', op: 'between', value: [100, 500] }
551
672
  ]
552
673
  },
553
674
  then: {
554
- decision: 'approve',
555
- weight: 0.8,
556
- reason: 'Low amount with active status'
675
+ decision: 'apply_discount',
676
+ weight: 0.9,
677
+ reason: 'Weekend booking discount eligible'
678
+ }
679
+ },
680
+ {
681
+ id: 'local_delivery_zone',
682
+ if: {
683
+ field: 'delivery.location',
684
+ op: 'within_radius',
685
+ value: { center: { lat: 40.7128, lon: -74.0060 }, radius: 25 }
686
+ },
687
+ then: {
688
+ decision: 'same_day_delivery',
689
+ weight: 0.85,
690
+ reason: 'Within local delivery zone'
691
+ }
692
+ },
693
+ {
694
+ id: 'permission_check',
695
+ if: {
696
+ all: [
697
+ { field: 'user.permissions', op: 'contains_all', value: ['read', 'write'] },
698
+ { field: 'user.roles', op: 'contains_any', value: ['admin', 'manager'] }
699
+ ]
700
+ },
701
+ then: {
702
+ decision: 'grant_access',
703
+ weight: 1.0,
704
+ reason: 'User has required permissions and role'
557
705
  }
558
706
  },
559
707
  {
560
- id: 'missing_info_reject',
708
+ id: 'urgent_recent_account',
561
709
  if: {
562
- any: [
563
- { field: 'description', op: 'blank' },
564
- { field: 'assignee', op: 'blank' }
710
+ all: [
711
+ { field: 'message', op: 'contains', value: 'urgent' },
712
+ { field: 'created_at', op: 'within_days', value: 30 }
565
713
  ]
566
714
  },
567
715
  then: {
568
- decision: 'needs_info',
569
- weight: 0.7,
570
- reason: 'Missing required information'
716
+ decision: 'escalate',
717
+ weight: 0.9,
718
+ reason: 'Urgent message from recent account'
571
719
  }
572
720
  }
573
721
  ]
@@ -623,9 +771,9 @@ class RuleBuilder {
623
771
  const ruleData = this.getRuleData();
624
772
 
625
773
  try {
626
- const response = await fetch('/api/versions', {
774
+ const response = await fetch(`${this.basePath}api/versions`, {
627
775
  method: 'POST',
628
- headers: { 'Content-Type': 'application/json' },
776
+ headers: this.getAuthHeaders(),
629
777
  body: JSON.stringify({
630
778
  rule_id: ruleset,
631
779
  content: ruleData,
@@ -657,7 +805,9 @@ class RuleBuilder {
657
805
  }
658
806
 
659
807
  try {
660
- const response = await fetch(`/api/rules/${encodeURIComponent(ruleset)}/history`);
808
+ const response = await fetch(`${this.basePath}api/rules/${encodeURIComponent(ruleset)}/history`, {
809
+ headers: this.getAuthHeaders()
810
+ });
661
811
  if (response.ok) {
662
812
  const history = await response.json();
663
813
  this.displayVersionHistory(history);
@@ -740,7 +890,9 @@ class RuleBuilder {
740
890
 
741
891
  async loadVersion(versionId) {
742
892
  try {
743
- const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}`);
893
+ const response = await fetch(`${this.basePath}api/versions/${encodeURIComponent(versionId)}`, {
894
+ headers: this.getAuthHeaders()
895
+ });
744
896
  if (response.ok) {
745
897
  const version = await response.json();
746
898
  this.loadRulesFromVersion(version.content);
@@ -776,9 +928,9 @@ class RuleBuilder {
776
928
  const performedBy = prompt('Enter your name:', 'system');
777
929
  if (!performedBy) return;
778
930
 
779
- const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}/activate`, {
931
+ const response = await fetch(`${this.basePath}api/versions/${encodeURIComponent(versionId)}/activate`, {
780
932
  method: 'POST',
781
- headers: { 'Content-Type': 'application/json' },
933
+ headers: this.getAuthHeaders(),
782
934
  body: JSON.stringify({ performed_by: performedBy })
783
935
  });
784
936
 
@@ -799,7 +951,10 @@ class RuleBuilder {
799
951
  async compareVersions(versionId1, versionId2) {
800
952
  try {
801
953
  const response = await fetch(
802
- `/api/versions/${encodeURIComponent(versionId1)}/compare/${encodeURIComponent(versionId2)}`
954
+ `${this.basePath}api/versions/${encodeURIComponent(versionId1)}/compare/${encodeURIComponent(versionId2)}`,
955
+ {
956
+ headers: this.getAuthHeaders()
957
+ }
803
958
  );
804
959
 
805
960
  if (response.ok) {
@@ -867,9 +1022,9 @@ class RuleBuilder {
867
1022
  }
868
1023
 
869
1024
  try {
870
- const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}`, {
1025
+ const response = await fetch(`${this.basePath}api/versions/${encodeURIComponent(versionId)}`, {
871
1026
  method: 'DELETE',
872
- headers: { 'Content-Type': 'application/json' }
1027
+ headers: this.getAuthHeaders()
873
1028
  });
874
1029
 
875
1030
  if (response.ok) {