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.
- checksums.yaml +4 -4
- data/README.md +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles date arithmetic operators: add_days, subtract_days, add_hours, subtract_hours, add_minutes, subtract_minutes
|
|
7
|
+
module DateArithmeticOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, context_hash, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "add_days"
|
|
11
|
+
# Adds days to a date and compares
|
|
12
|
+
return false unless actual_value
|
|
13
|
+
|
|
14
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
15
|
+
return false unless start_date
|
|
16
|
+
|
|
17
|
+
params = parse_date_arithmetic_params(expected_value, :days, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
18
|
+
return false unless params
|
|
19
|
+
|
|
20
|
+
result_date = start_date + (params[:days] * 86_400)
|
|
21
|
+
target_date = if params[:target] == "now"
|
|
22
|
+
Time.now
|
|
23
|
+
else
|
|
24
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
25
|
+
end
|
|
26
|
+
return false unless target_date
|
|
27
|
+
|
|
28
|
+
compare_date_result?(result_date, target_date, params)
|
|
29
|
+
|
|
30
|
+
when "subtract_days"
|
|
31
|
+
# Subtracts days from a date and compares
|
|
32
|
+
return false unless actual_value
|
|
33
|
+
|
|
34
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
35
|
+
return false unless start_date
|
|
36
|
+
|
|
37
|
+
params = parse_date_arithmetic_params(expected_value, :days, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
38
|
+
return false unless params
|
|
39
|
+
|
|
40
|
+
result_date = start_date - (params[:days] * 86_400)
|
|
41
|
+
target_date = if params[:target] == "now"
|
|
42
|
+
Time.now
|
|
43
|
+
else
|
|
44
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
45
|
+
end
|
|
46
|
+
return false unless target_date
|
|
47
|
+
|
|
48
|
+
compare_date_result?(result_date, target_date, params)
|
|
49
|
+
|
|
50
|
+
when "add_hours"
|
|
51
|
+
# Adds hours to a date and compares
|
|
52
|
+
return false unless actual_value
|
|
53
|
+
|
|
54
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
55
|
+
return false unless start_date
|
|
56
|
+
|
|
57
|
+
params = parse_date_arithmetic_params(expected_value, :hours, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
58
|
+
return false unless params
|
|
59
|
+
|
|
60
|
+
result_date = start_date + (params[:hours] * 3600)
|
|
61
|
+
target_date = if params[:target] == "now"
|
|
62
|
+
Time.now
|
|
63
|
+
else
|
|
64
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
65
|
+
end
|
|
66
|
+
return false unless target_date
|
|
67
|
+
|
|
68
|
+
compare_date_result?(result_date, target_date, params)
|
|
69
|
+
|
|
70
|
+
when "subtract_hours"
|
|
71
|
+
# Subtracts hours from a date and compares
|
|
72
|
+
return false unless actual_value
|
|
73
|
+
|
|
74
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
75
|
+
return false unless start_date
|
|
76
|
+
|
|
77
|
+
params = parse_date_arithmetic_params(expected_value, :hours, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
78
|
+
return false unless params
|
|
79
|
+
|
|
80
|
+
result_date = start_date - (params[:hours] * 3600)
|
|
81
|
+
target_date = if params[:target] == "now"
|
|
82
|
+
Time.now
|
|
83
|
+
else
|
|
84
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
85
|
+
end
|
|
86
|
+
return false unless target_date
|
|
87
|
+
|
|
88
|
+
compare_date_result?(result_date, target_date, params)
|
|
89
|
+
|
|
90
|
+
when "add_minutes"
|
|
91
|
+
# Adds minutes to a date and compares
|
|
92
|
+
return false unless actual_value
|
|
93
|
+
|
|
94
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
95
|
+
return false unless start_date
|
|
96
|
+
|
|
97
|
+
params = parse_date_arithmetic_params(expected_value, :minutes, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
98
|
+
return false unless params
|
|
99
|
+
|
|
100
|
+
result_date = start_date + (params[:minutes] * 60)
|
|
101
|
+
target_date = if params[:target] == "now"
|
|
102
|
+
Time.now
|
|
103
|
+
else
|
|
104
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
105
|
+
end
|
|
106
|
+
return false unless target_date
|
|
107
|
+
|
|
108
|
+
compare_date_result?(result_date, target_date, params)
|
|
109
|
+
|
|
110
|
+
when "subtract_minutes"
|
|
111
|
+
# Subtracts minutes from a date and compares
|
|
112
|
+
return false unless actual_value
|
|
113
|
+
|
|
114
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
115
|
+
return false unless start_date
|
|
116
|
+
|
|
117
|
+
params = parse_date_arithmetic_params(expected_value, :minutes, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
118
|
+
return false unless params
|
|
119
|
+
|
|
120
|
+
result_date = start_date - (params[:minutes] * 60)
|
|
121
|
+
target_date = if params[:target] == "now"
|
|
122
|
+
Time.now
|
|
123
|
+
else
|
|
124
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash, params[:target]))
|
|
125
|
+
end
|
|
126
|
+
return false unless target_date
|
|
127
|
+
|
|
128
|
+
compare_date_result?(result_date, target_date, params)
|
|
129
|
+
end
|
|
130
|
+
# Returns nil if not handled by this module
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Parse date arithmetic parameters
|
|
134
|
+
def self.parse_date_arithmetic_params(value, unit = :days, param_cache: nil, param_cache_mutex: nil)
|
|
135
|
+
return nil unless value.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
# Normalize to hash (already a hash, but normalize keys)
|
|
138
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
139
|
+
|
|
140
|
+
cache = param_cache
|
|
141
|
+
mutex = param_cache_mutex
|
|
142
|
+
if cache.nil? || mutex.nil?
|
|
143
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
144
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
cache_key = Base.normalize_param_cache_key(normalized, "date_arithmetic_#{unit}")
|
|
148
|
+
cached = cache[cache_key]
|
|
149
|
+
return cached if cached
|
|
150
|
+
|
|
151
|
+
mutex.synchronize do
|
|
152
|
+
cache[cache_key] ||= parse_date_arithmetic_params_impl(normalized, unit)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.parse_date_arithmetic_params_impl(value, unit)
|
|
157
|
+
unit_value = value[unit.to_s] || value[unit]
|
|
158
|
+
return nil unless unit_value.is_a?(Numeric)
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
unit => unit_value.to_f,
|
|
162
|
+
target: value[:target] || value["target"] || "now",
|
|
163
|
+
compare: value[:compare] || value["compare"],
|
|
164
|
+
eq: value[:eq] || value["eq"],
|
|
165
|
+
gt: value[:gt] || value["gt"],
|
|
166
|
+
lt: value[:lt] || value["lt"],
|
|
167
|
+
gte: value[:gte] || value["gte"],
|
|
168
|
+
lte: value[:lte] || value["lte"]
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Compare date result
|
|
173
|
+
def self.compare_date_result?(actual, target, params)
|
|
174
|
+
if params[:compare]
|
|
175
|
+
case params[:compare].to_s
|
|
176
|
+
when "eq", "=="
|
|
177
|
+
(actual - target).abs < 1
|
|
178
|
+
when "gt", ">"
|
|
179
|
+
actual > target
|
|
180
|
+
when "lt", "<"
|
|
181
|
+
actual < target
|
|
182
|
+
when "gte", ">="
|
|
183
|
+
actual >= target
|
|
184
|
+
when "lte", "<="
|
|
185
|
+
actual <= target
|
|
186
|
+
else
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
elsif params[:eq]
|
|
190
|
+
(actual - target).abs < 1
|
|
191
|
+
elsif params[:gt]
|
|
192
|
+
actual > target
|
|
193
|
+
elsif params[:lt]
|
|
194
|
+
actual < target
|
|
195
|
+
elsif params[:gte]
|
|
196
|
+
actual >= target
|
|
197
|
+
elsif params[:lte]
|
|
198
|
+
actual <= target
|
|
199
|
+
else
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles date/time operators: before_date, after_date, within_days, day_of_week
|
|
7
|
+
module DateTimeOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, date_cache: nil, date_cache_mutex: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
9
|
+
case op
|
|
10
|
+
when "before_date"
|
|
11
|
+
# Checks if date is before specified date
|
|
12
|
+
ConditionEvaluator.compare_dates(actual_value, expected_value, :<)
|
|
13
|
+
|
|
14
|
+
when "after_date"
|
|
15
|
+
# Checks if date is after specified date
|
|
16
|
+
ConditionEvaluator.compare_dates(actual_value, expected_value, :>)
|
|
17
|
+
|
|
18
|
+
when "within_days"
|
|
19
|
+
# Checks if date is within N days from now (past or future)
|
|
20
|
+
return false unless actual_value
|
|
21
|
+
return false unless expected_value.is_a?(Numeric)
|
|
22
|
+
|
|
23
|
+
date = ConditionEvaluator.parse_date(actual_value)
|
|
24
|
+
return false unless date
|
|
25
|
+
|
|
26
|
+
now = Time.now
|
|
27
|
+
diff_days = ((date - now) / 86_400).abs # 86400 seconds in a day
|
|
28
|
+
diff_days <= expected_value
|
|
29
|
+
|
|
30
|
+
when "day_of_week"
|
|
31
|
+
# Checks if date falls on specified day of week
|
|
32
|
+
return false unless actual_value
|
|
33
|
+
|
|
34
|
+
date = ConditionEvaluator.parse_date(actual_value)
|
|
35
|
+
return false unless date
|
|
36
|
+
|
|
37
|
+
expected_day = ConditionEvaluator.normalize_day_of_week(expected_value)
|
|
38
|
+
return false unless expected_day
|
|
39
|
+
|
|
40
|
+
date.wday == expected_day
|
|
41
|
+
end
|
|
42
|
+
# Returns nil if not handled by this module
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles duration calculation operators: duration_seconds, duration_minutes, duration_hours, duration_days
|
|
7
|
+
module DurationOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, context_hash, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "duration_seconds"
|
|
11
|
+
# Calculates duration between two dates in seconds
|
|
12
|
+
return false unless actual_value
|
|
13
|
+
|
|
14
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
15
|
+
return false unless start_date
|
|
16
|
+
|
|
17
|
+
params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
18
|
+
return false unless params
|
|
19
|
+
|
|
20
|
+
end_date = if params[:end] == "now"
|
|
21
|
+
Time.now
|
|
22
|
+
else
|
|
23
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
|
|
24
|
+
params[:end]))
|
|
25
|
+
end
|
|
26
|
+
return false unless end_date
|
|
27
|
+
|
|
28
|
+
duration = (end_date - start_date).abs
|
|
29
|
+
compare_duration_result(duration, params)
|
|
30
|
+
|
|
31
|
+
when "duration_minutes"
|
|
32
|
+
# Calculates duration between two dates in minutes
|
|
33
|
+
return false unless actual_value
|
|
34
|
+
|
|
35
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
36
|
+
return false unless start_date
|
|
37
|
+
|
|
38
|
+
params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
39
|
+
return false unless params
|
|
40
|
+
|
|
41
|
+
end_date = if params[:end] == "now"
|
|
42
|
+
Time.now
|
|
43
|
+
else
|
|
44
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
|
|
45
|
+
params[:end]))
|
|
46
|
+
end
|
|
47
|
+
return false unless end_date
|
|
48
|
+
|
|
49
|
+
duration = ((end_date - start_date).abs / 60.0)
|
|
50
|
+
compare_duration_result(duration, params)
|
|
51
|
+
|
|
52
|
+
when "duration_hours"
|
|
53
|
+
# Calculates duration between two dates in hours
|
|
54
|
+
return false unless actual_value
|
|
55
|
+
|
|
56
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
57
|
+
return false unless start_date
|
|
58
|
+
|
|
59
|
+
params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
60
|
+
return false unless params
|
|
61
|
+
|
|
62
|
+
end_date = if params[:end] == "now"
|
|
63
|
+
Time.now
|
|
64
|
+
else
|
|
65
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
|
|
66
|
+
params[:end]))
|
|
67
|
+
end
|
|
68
|
+
return false unless end_date
|
|
69
|
+
|
|
70
|
+
duration = ((end_date - start_date).abs / 3600.0)
|
|
71
|
+
compare_duration_result(duration, params)
|
|
72
|
+
|
|
73
|
+
when "duration_days"
|
|
74
|
+
# Calculates duration between two dates in days
|
|
75
|
+
return false unless actual_value
|
|
76
|
+
|
|
77
|
+
start_date = ConditionEvaluator.parse_date(actual_value)
|
|
78
|
+
return false unless start_date
|
|
79
|
+
|
|
80
|
+
params = parse_duration_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
81
|
+
return false unless params
|
|
82
|
+
|
|
83
|
+
end_date = if params[:end] == "now"
|
|
84
|
+
Time.now
|
|
85
|
+
else
|
|
86
|
+
ConditionEvaluator.parse_date(ConditionEvaluator.get_nested_value(context_hash,
|
|
87
|
+
params[:end]))
|
|
88
|
+
end
|
|
89
|
+
return false unless end_date
|
|
90
|
+
|
|
91
|
+
duration = ((end_date - start_date).abs / 86_400.0)
|
|
92
|
+
compare_duration_result(duration, params)
|
|
93
|
+
end
|
|
94
|
+
# Returns nil if not handled by this module
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Parse duration parameters
|
|
98
|
+
def self.parse_duration_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
99
|
+
return nil unless value.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
# Normalize to hash (already a hash, but normalize keys)
|
|
102
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
103
|
+
|
|
104
|
+
cache = param_cache
|
|
105
|
+
mutex = param_cache_mutex
|
|
106
|
+
if cache.nil? || mutex.nil?
|
|
107
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
108
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
cache_key = Base.normalize_param_cache_key(normalized, "duration")
|
|
112
|
+
cached = cache[cache_key]
|
|
113
|
+
return cached if cached
|
|
114
|
+
|
|
115
|
+
mutex.synchronize do
|
|
116
|
+
cache[cache_key] ||= parse_duration_params_impl(normalized)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.parse_duration_params_impl(value)
|
|
121
|
+
end_field = value[:end] || value["end"]
|
|
122
|
+
return nil unless end_field
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
end: end_field.to_s,
|
|
126
|
+
min: value[:min] || value["min"],
|
|
127
|
+
max: value[:max] || value["max"],
|
|
128
|
+
gt: value[:gt] || value["gt"],
|
|
129
|
+
lt: value[:lt] || value["lt"],
|
|
130
|
+
gte: value[:gte] || value["gte"],
|
|
131
|
+
lte: value[:lte] || value["lte"]
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Compare duration result
|
|
136
|
+
def self.compare_duration_result(actual, params)
|
|
137
|
+
result = true
|
|
138
|
+
result &&= (actual >= params[:min]) if params[:min]
|
|
139
|
+
result &&= (actual <= params[:max]) if params[:max]
|
|
140
|
+
result &&= (actual > params[:gt]) if params[:gt]
|
|
141
|
+
result &&= (actual < params[:lt]) if params[:lt]
|
|
142
|
+
result &&= (actual >= params[:gte]) if params[:gte]
|
|
143
|
+
result &&= (actual <= params[:lte]) if params[:lte]
|
|
144
|
+
result
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles financial calculation operators: compound_interest, present_value, future_value, payment
|
|
7
|
+
module FinancialOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value, param_cache: nil, param_cache_mutex: nil)
|
|
9
|
+
case op
|
|
10
|
+
when "compound_interest"
|
|
11
|
+
# Calculates compound interest: A = P(1 + r/n)^(nt)
|
|
12
|
+
return false unless actual_value.is_a?(Numeric)
|
|
13
|
+
|
|
14
|
+
params = parse_compound_interest_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
15
|
+
return false unless params
|
|
16
|
+
|
|
17
|
+
principal = actual_value
|
|
18
|
+
rate = params[:rate]
|
|
19
|
+
periods = params[:periods]
|
|
20
|
+
result = principal * ((1 + (rate / periods))**periods)
|
|
21
|
+
|
|
22
|
+
if params[:result]
|
|
23
|
+
(result.round(2) == params[:result].round(2))
|
|
24
|
+
else
|
|
25
|
+
compare_financial_result(result, params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
when "present_value"
|
|
29
|
+
# Calculates present value: PV = FV / (1 + r)^n
|
|
30
|
+
return false unless actual_value.is_a?(Numeric)
|
|
31
|
+
|
|
32
|
+
params = parse_present_value_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
33
|
+
return false unless params
|
|
34
|
+
|
|
35
|
+
future_value = actual_value
|
|
36
|
+
rate = params[:rate]
|
|
37
|
+
periods = params[:periods]
|
|
38
|
+
present_value = future_value / ((1 + rate)**periods)
|
|
39
|
+
|
|
40
|
+
if params[:result]
|
|
41
|
+
(present_value.round(2) == params[:result].round(2))
|
|
42
|
+
else
|
|
43
|
+
compare_financial_result(present_value, params)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
when "future_value"
|
|
47
|
+
# Calculates future value: FV = PV * (1 + r)^n
|
|
48
|
+
return false unless actual_value.is_a?(Numeric)
|
|
49
|
+
|
|
50
|
+
params = parse_future_value_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
51
|
+
return false unless params
|
|
52
|
+
|
|
53
|
+
present_value = actual_value
|
|
54
|
+
rate = params[:rate]
|
|
55
|
+
periods = params[:periods]
|
|
56
|
+
future_value = present_value * ((1 + rate)**periods)
|
|
57
|
+
|
|
58
|
+
if params[:result]
|
|
59
|
+
(future_value.round(2) == params[:result].round(2))
|
|
60
|
+
else
|
|
61
|
+
compare_financial_result(future_value, params)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
when "payment"
|
|
65
|
+
# Calculates loan payment: PMT = P * [r(1+r)^n] / [(1+r)^n - 1]
|
|
66
|
+
return false unless actual_value.is_a?(Numeric)
|
|
67
|
+
|
|
68
|
+
params = parse_payment_params(expected_value, param_cache: param_cache, param_cache_mutex: param_cache_mutex)
|
|
69
|
+
return false unless params
|
|
70
|
+
|
|
71
|
+
principal = actual_value
|
|
72
|
+
rate = params[:rate]
|
|
73
|
+
periods = params[:periods]
|
|
74
|
+
|
|
75
|
+
return false if rate <= 0 || periods <= 0
|
|
76
|
+
|
|
77
|
+
payment = if rate.zero?
|
|
78
|
+
principal / periods
|
|
79
|
+
else
|
|
80
|
+
principal * (rate * ((1 + rate)**periods)) / (((1 + rate)**periods) - 1)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if params[:result]
|
|
84
|
+
(payment.round(2) == params[:result].round(2))
|
|
85
|
+
else
|
|
86
|
+
compare_financial_result(payment, params)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
# Returns nil if not handled by this module
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Parse compound interest parameters
|
|
93
|
+
def self.parse_compound_interest_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
94
|
+
return nil unless value.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
97
|
+
|
|
98
|
+
cache = param_cache
|
|
99
|
+
mutex = param_cache_mutex
|
|
100
|
+
if cache.nil? || mutex.nil?
|
|
101
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
102
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
cache_key = Base.normalize_param_cache_key(normalized, "compound_interest")
|
|
106
|
+
cached = cache[cache_key]
|
|
107
|
+
return cached if cached
|
|
108
|
+
|
|
109
|
+
mutex.synchronize do
|
|
110
|
+
cache[cache_key] ||= parse_compound_interest_params_impl(normalized)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.parse_compound_interest_params_impl(value)
|
|
115
|
+
rate = value[:rate] || value["rate"]
|
|
116
|
+
periods = value[:periods] || value["periods"]
|
|
117
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
rate: rate.to_f,
|
|
121
|
+
periods: periods.to_f,
|
|
122
|
+
result: value[:result] || value["result"],
|
|
123
|
+
compare: value[:compare] || value["compare"],
|
|
124
|
+
threshold: value[:threshold] || value["threshold"]
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Parse present value parameters
|
|
129
|
+
def self.parse_present_value_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
130
|
+
return nil unless value.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
133
|
+
|
|
134
|
+
cache = param_cache
|
|
135
|
+
mutex = param_cache_mutex
|
|
136
|
+
if cache.nil? || mutex.nil?
|
|
137
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
138
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
cache_key = Base.normalize_param_cache_key(normalized, "present_value")
|
|
142
|
+
cached = cache[cache_key]
|
|
143
|
+
return cached if cached
|
|
144
|
+
|
|
145
|
+
mutex.synchronize do
|
|
146
|
+
cache[cache_key] ||= parse_present_value_params_impl(normalized)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.parse_present_value_params_impl(value)
|
|
151
|
+
rate = value[:rate] || value["rate"]
|
|
152
|
+
periods = value[:periods] || value["periods"]
|
|
153
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
rate: rate.to_f,
|
|
157
|
+
periods: periods.to_f,
|
|
158
|
+
result: value[:result] || value["result"]
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Parse future value parameters
|
|
163
|
+
def self.parse_future_value_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
164
|
+
return nil unless value.is_a?(Hash)
|
|
165
|
+
|
|
166
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
167
|
+
|
|
168
|
+
cache = param_cache
|
|
169
|
+
mutex = param_cache_mutex
|
|
170
|
+
if cache.nil? || mutex.nil?
|
|
171
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
172
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
cache_key = Base.normalize_param_cache_key(normalized, "future_value")
|
|
176
|
+
cached = cache[cache_key]
|
|
177
|
+
return cached if cached
|
|
178
|
+
|
|
179
|
+
mutex.synchronize do
|
|
180
|
+
cache[cache_key] ||= parse_future_value_params_impl(normalized)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def self.parse_future_value_params_impl(value)
|
|
185
|
+
rate = value[:rate] || value["rate"]
|
|
186
|
+
periods = value[:periods] || value["periods"]
|
|
187
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
rate: rate.to_f,
|
|
191
|
+
periods: periods.to_f,
|
|
192
|
+
result: value[:result] || value["result"]
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Parse payment parameters
|
|
197
|
+
def self.parse_payment_params(value, param_cache: nil, param_cache_mutex: nil)
|
|
198
|
+
return nil unless value.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
normalized = Base.normalize_params_to_hash(value, [])
|
|
201
|
+
|
|
202
|
+
cache = param_cache
|
|
203
|
+
mutex = param_cache_mutex
|
|
204
|
+
if cache.nil? || mutex.nil?
|
|
205
|
+
cache = ConditionEvaluator.instance_variable_get(:@param_cache)
|
|
206
|
+
mutex = ConditionEvaluator.instance_variable_get(:@param_cache_mutex)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
cache_key = Base.normalize_param_cache_key(normalized, "payment")
|
|
210
|
+
cached = cache[cache_key]
|
|
211
|
+
return cached if cached
|
|
212
|
+
|
|
213
|
+
mutex.synchronize do
|
|
214
|
+
cache[cache_key] ||= parse_payment_params_impl(normalized)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.parse_payment_params_impl(value)
|
|
219
|
+
rate = value[:rate] || value["rate"]
|
|
220
|
+
periods = value[:periods] || value["periods"]
|
|
221
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
rate: rate.to_f,
|
|
225
|
+
periods: periods.to_f,
|
|
226
|
+
result: value[:result] || value["result"]
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Compare financial result
|
|
231
|
+
def self.compare_financial_result(actual, params)
|
|
232
|
+
ConditionEvaluator.compare_financial_result(actual, params)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|