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.
- checksums.yaml +4 -4
- data/README.md +84 -233
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +52 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -612
- data/spec/monitoring/metrics_collector_spec.rb +220 -2
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- 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.
|
|
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(
|
|
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: '
|
|
650
|
+
ruleset: 'example_advanced_rules',
|
|
530
651
|
rules: [
|
|
531
652
|
{
|
|
532
|
-
id: '
|
|
653
|
+
id: 'corporate_email_approval',
|
|
533
654
|
if: {
|
|
534
655
|
all: [
|
|
535
|
-
{ field: '
|
|
536
|
-
{ field: '
|
|
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: '
|
|
663
|
+
reason: 'Corporate email with valid age range'
|
|
543
664
|
}
|
|
544
665
|
},
|
|
545
666
|
{
|
|
546
|
-
id: '
|
|
667
|
+
id: 'weekend_special_offer',
|
|
547
668
|
if: {
|
|
548
669
|
all: [
|
|
549
|
-
{ field: '
|
|
550
|
-
{ field: '
|
|
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: '
|
|
555
|
-
weight: 0.
|
|
556
|
-
reason: '
|
|
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: '
|
|
708
|
+
id: 'urgent_recent_account',
|
|
561
709
|
if: {
|
|
562
|
-
|
|
563
|
-
{ field: '
|
|
564
|
-
{ field: '
|
|
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: '
|
|
569
|
-
weight: 0.
|
|
570
|
-
reason: '
|
|
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(
|
|
774
|
+
const response = await fetch(`${this.basePath}api/versions`, {
|
|
627
775
|
method: 'POST',
|
|
628
|
-
headers:
|
|
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(
|
|
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(
|
|
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(
|
|
931
|
+
const response = await fetch(`${this.basePath}api/versions/${encodeURIComponent(versionId)}/activate`, {
|
|
780
932
|
method: 'POST',
|
|
781
|
-
headers:
|
|
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
|
-
|
|
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(
|
|
1025
|
+
const response = await fetch(`${this.basePath}api/versions/${encodeURIComponent(versionId)}`, {
|
|
871
1026
|
method: 'DELETE',
|
|
872
|
-
headers:
|
|
1027
|
+
headers: this.getAuthHeaders()
|
|
873
1028
|
});
|
|
874
1029
|
|
|
875
1030
|
if (response.ok) {
|