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 Helpers
|
|
6
|
+
# Parameter parsing helpers for ConditionEvaluator
|
|
7
|
+
module ParameterParsingHelpers
|
|
8
|
+
def self.parse_range(value, param_cache:, param_cache_mutex:)
|
|
9
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[min max])
|
|
10
|
+
cache_key = Operators::Base.normalize_param_cache_key(normalized, "range")
|
|
11
|
+
|
|
12
|
+
cached = param_cache[cache_key]
|
|
13
|
+
return cached if cached
|
|
14
|
+
|
|
15
|
+
param_cache_mutex.synchronize do
|
|
16
|
+
param_cache[cache_key] ||= parse_range_impl(normalized)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse_range_impl(value)
|
|
21
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[min max])
|
|
22
|
+
return nil unless normalized.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
min = normalized[:min] || normalized["min"]
|
|
25
|
+
max = normalized[:max] || normalized["max"]
|
|
26
|
+
return nil unless min && max
|
|
27
|
+
|
|
28
|
+
{ min: min, max: max }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.parse_modulo_params(value, param_cache:, param_cache_mutex:)
|
|
32
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[divisor remainder])
|
|
33
|
+
cache_key = Operators::Base.normalize_param_cache_key(normalized, "modulo")
|
|
34
|
+
|
|
35
|
+
cached = param_cache[cache_key]
|
|
36
|
+
return cached if cached
|
|
37
|
+
|
|
38
|
+
param_cache_mutex.synchronize do
|
|
39
|
+
param_cache[cache_key] ||= parse_modulo_params_impl(normalized)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.parse_modulo_params_impl(value)
|
|
44
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[divisor remainder])
|
|
45
|
+
return nil unless normalized.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
divisor = normalized[:divisor] || normalized["divisor"]
|
|
48
|
+
remainder = normalized[:remainder] || normalized["remainder"]
|
|
49
|
+
return nil unless divisor && !remainder.nil?
|
|
50
|
+
|
|
51
|
+
{ divisor: divisor, remainder: remainder }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.parse_power_params(value)
|
|
55
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[exponent result])
|
|
56
|
+
return nil unless normalized.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
exponent = normalized[:exponent] || normalized["exponent"]
|
|
59
|
+
result = normalized[:result] || normalized["result"]
|
|
60
|
+
return nil unless exponent && !result.nil?
|
|
61
|
+
|
|
62
|
+
{ exponent: exponent, result: result }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.parse_atan2_params(value)
|
|
66
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[y result])
|
|
67
|
+
return nil unless normalized.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
y = normalized[:y] || normalized["y"]
|
|
70
|
+
result = normalized[:result] || normalized["result"]
|
|
71
|
+
return nil unless y && !result.nil?
|
|
72
|
+
|
|
73
|
+
{ y: y, result: result }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.parse_gcd_lcm_params(value)
|
|
77
|
+
normalized = Operators::Base.normalize_params_to_hash(value, %i[other result])
|
|
78
|
+
return nil unless normalized.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
other = normalized[:other] || normalized["other"]
|
|
81
|
+
result = normalized[:result] || normalized["result"]
|
|
82
|
+
return nil unless other && !result.nil?
|
|
83
|
+
|
|
84
|
+
{ other: other, result: result }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.parse_percentile_params(value)
|
|
88
|
+
return nil unless value.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
percentile = value["percentile"] || value[:percentile]
|
|
91
|
+
return nil unless percentile.is_a?(Numeric) && percentile >= 0 && percentile <= 100
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
percentile: percentile.to_f,
|
|
95
|
+
threshold: value["threshold"] || value[:threshold],
|
|
96
|
+
gt: value["gt"] || value[:gt],
|
|
97
|
+
lt: value["lt"] || value[:lt],
|
|
98
|
+
gte: value["gte"] || value[:gte],
|
|
99
|
+
lte: value["lte"] || value[:lte],
|
|
100
|
+
eq: value["eq"] || value[:eq]
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.parse_duration_params(value)
|
|
105
|
+
return nil unless value.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
end_field = value["end"] || value[:end]
|
|
108
|
+
return nil unless end_field
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
end: end_field.to_s,
|
|
112
|
+
min: value["min"] || value[:min],
|
|
113
|
+
max: value["max"] || value[:max],
|
|
114
|
+
gt: value["gt"] || value[:gt],
|
|
115
|
+
lt: value["lt"] || value[:lt],
|
|
116
|
+
gte: value["gte"] || value[:gte],
|
|
117
|
+
lte: value["lte"] || value[:lte]
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.parse_date_arithmetic_params(value, unit = :days)
|
|
122
|
+
return nil unless value.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
unit_value = value[unit.to_s] || value[unit]
|
|
125
|
+
return nil unless unit_value.is_a?(Numeric)
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
unit => unit_value.to_f,
|
|
129
|
+
target: value["target"] || value[:target] || "now",
|
|
130
|
+
compare: value["compare"] || value[:compare],
|
|
131
|
+
eq: value["eq"] || value[:eq],
|
|
132
|
+
gt: value["gt"] || value[:gt],
|
|
133
|
+
lt: value["lt"] || value[:lt],
|
|
134
|
+
gte: value["gte"] || value[:gte],
|
|
135
|
+
lte: value["lte"] || value[:lte]
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.parse_moving_window_params(value)
|
|
140
|
+
return nil unless value.is_a?(Hash)
|
|
141
|
+
|
|
142
|
+
window = value["window"] || value[:window]
|
|
143
|
+
return nil unless window.is_a?(Numeric) && window.positive?
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
window: window.to_i,
|
|
147
|
+
threshold: value["threshold"] || value[:threshold],
|
|
148
|
+
gt: value["gt"] || value[:gt],
|
|
149
|
+
lt: value["lt"] || value[:lt],
|
|
150
|
+
gte: value["gte"] || value[:gte],
|
|
151
|
+
lte: value["lte"] || value[:lte]
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.parse_compound_interest_params(value)
|
|
156
|
+
return nil unless value.is_a?(Hash)
|
|
157
|
+
|
|
158
|
+
rate = value["rate"] || value[:rate]
|
|
159
|
+
periods = value["periods"] || value[:periods]
|
|
160
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
rate: rate.to_f,
|
|
164
|
+
periods: periods.to_f,
|
|
165
|
+
result: value["result"] || value[:result],
|
|
166
|
+
compare: value["compare"] || value[:compare],
|
|
167
|
+
threshold: value["threshold"] || value[:threshold]
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.parse_present_value_params(value)
|
|
172
|
+
return nil unless value.is_a?(Hash)
|
|
173
|
+
|
|
174
|
+
rate = value["rate"] || value[:rate]
|
|
175
|
+
periods = value["periods"] || value[:periods]
|
|
176
|
+
return nil unless rate.is_a?(Numeric) && periods.is_a?(Numeric) && periods.positive?
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
rate: rate.to_f,
|
|
180
|
+
periods: periods.to_f,
|
|
181
|
+
result: value["result"] || value[:result]
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.parse_future_value_params(value)
|
|
186
|
+
parse_present_value_params(value)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.parse_payment_params(value)
|
|
190
|
+
parse_compound_interest_params(value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.parse_join_params(value)
|
|
194
|
+
return nil unless value.is_a?(Hash)
|
|
195
|
+
|
|
196
|
+
separator = value["separator"] || value[:separator] || ","
|
|
197
|
+
{
|
|
198
|
+
separator: separator.to_s,
|
|
199
|
+
result: value["result"] || value[:result],
|
|
200
|
+
contains: value["contains"] || value[:contains]
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Template expansion and mapping helpers for ConditionEvaluator
|
|
7
|
+
module TemplateHelpers
|
|
8
|
+
def self.expand_template_params(params, context_hash, get_nested_value:)
|
|
9
|
+
return {} unless params.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
params.transform_values do |value|
|
|
12
|
+
expand_template_value(value, context_hash, get_nested_value: get_nested_value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.expand_template_value(value, context_hash, get_nested_value:)
|
|
17
|
+
return value unless value.is_a?(String)
|
|
18
|
+
return value unless value.match?(/\{\{.*\}\}/)
|
|
19
|
+
|
|
20
|
+
# Extract path from {{path}} syntax
|
|
21
|
+
value.gsub(/\{\{([^}]+)\}\}/) do |_match|
|
|
22
|
+
path = Regexp.last_match(1).strip
|
|
23
|
+
get_nested_value.call(context_hash, path) || value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.apply_mapping(response_data, mapping, get_nested_value:)
|
|
28
|
+
return {} unless response_data.is_a?(Hash)
|
|
29
|
+
return {} unless mapping.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
mapping.each_with_object({}) do |(source_key, target_key), result|
|
|
32
|
+
source_value = get_nested_value.call(response_data, source_key.to_s)
|
|
33
|
+
result[target_key.to_s] = source_value unless source_value.nil?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Helpers
|
|
6
|
+
# Utility helpers for ConditionEvaluator
|
|
7
|
+
module UtilityHelpers
|
|
8
|
+
def self.get_nested_value(hash, key_path, get_cached_path:)
|
|
9
|
+
keys = get_cached_path.call(key_path)
|
|
10
|
+
keys.reduce(hash) do |memo, key|
|
|
11
|
+
return nil unless memo.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
# OPTIMIZE: try symbol first (most common), then string
|
|
14
|
+
# Check key existence first to avoid double lookup
|
|
15
|
+
key_sym = key.to_sym
|
|
16
|
+
if memo.key?(key_sym)
|
|
17
|
+
memo[key_sym]
|
|
18
|
+
elsif memo.key?(key)
|
|
19
|
+
memo[key]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.comparable?(val1, val2)
|
|
25
|
+
# Both are numeric - allow comparison between different numeric types
|
|
26
|
+
# (e.g., Integer and Float are comparable in Ruby)
|
|
27
|
+
return true if val1.is_a?(Numeric) && val2.is_a?(Numeric)
|
|
28
|
+
|
|
29
|
+
# Both are strings - require exact same type
|
|
30
|
+
return val1.instance_of?(val2.class) if val1.is_a?(String) && val2.is_a?(String)
|
|
31
|
+
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.epsilon_equal?(value1, value2, epsilon = 1e-10)
|
|
36
|
+
(value1 - value2).abs < epsilon
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.string_operator?(actual_value, expected_value)
|
|
40
|
+
actual_value.is_a?(String) && expected_value.is_a?(String)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Base utilities shared across all operator modules
|
|
7
|
+
module Base
|
|
8
|
+
# Normalize params to hash - converts arrays to hashes for better performance
|
|
9
|
+
# If value is an array and keys are provided, convert to hash
|
|
10
|
+
# If value is already a hash, normalize keys to symbols
|
|
11
|
+
def self.normalize_params_to_hash(value, keys)
|
|
12
|
+
if value.is_a?(Array) && value.size == keys.size
|
|
13
|
+
# Convert array to hash for better performance with large params
|
|
14
|
+
keys.each_with_index.with_object({}) do |(key, idx), hash|
|
|
15
|
+
hash[key] = value[idx]
|
|
16
|
+
end
|
|
17
|
+
elsif value.is_a?(Hash)
|
|
18
|
+
# Normalize hash keys to symbols for consistency
|
|
19
|
+
value.each_with_object({}) do |(k, v), h|
|
|
20
|
+
key = k.is_a?(String) ? k.to_sym : k
|
|
21
|
+
h[key] = v
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Normalize parameter value for cache key generation
|
|
29
|
+
def self.normalize_param_cache_key(value, prefix)
|
|
30
|
+
case value
|
|
31
|
+
when Array
|
|
32
|
+
"#{prefix}:#{value.inspect}"
|
|
33
|
+
when Hash
|
|
34
|
+
# Normalize keys to symbols and sort for consistent cache keys
|
|
35
|
+
normalized = value.each_with_object({}) do |(k, v), h|
|
|
36
|
+
key = k.is_a?(String) ? k.to_sym : k
|
|
37
|
+
h[key] = v
|
|
38
|
+
end
|
|
39
|
+
sorted_keys = normalized.keys.sort
|
|
40
|
+
"#{prefix}:#{sorted_keys.map { |k| "#{k}:#{normalized[k]}" }.join(',')}"
|
|
41
|
+
else
|
|
42
|
+
"#{prefix}:#{value.inspect}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Compare aggregation result with expected value (supports hash with comparison operators)
|
|
47
|
+
def self.compare_aggregation_result(actual, expected)
|
|
48
|
+
if expected.is_a?(Hash)
|
|
49
|
+
result = true
|
|
50
|
+
result &&= (actual >= expected[:min]) if expected[:min]
|
|
51
|
+
result &&= (actual <= expected[:max]) if expected[:max]
|
|
52
|
+
result &&= (actual > expected[:gt]) if expected[:gt]
|
|
53
|
+
result &&= (actual < expected[:lt]) if expected[:lt]
|
|
54
|
+
result &&= (actual >= expected[:gte]) if expected[:gte]
|
|
55
|
+
result &&= (actual <= expected[:lte]) if expected[:lte]
|
|
56
|
+
result &&= (actual == expected[:eq]) if expected[:eq]
|
|
57
|
+
result
|
|
58
|
+
else
|
|
59
|
+
actual == expected
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Epsilon comparison for floating point numbers
|
|
64
|
+
def self.epsilon_equal?(a, b, epsilon = 1e-10) # rubocop:disable Naming/MethodParameterName
|
|
65
|
+
(a - b).abs < epsilon
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles basic comparison operators: eq, neq, gt, gte, lt, lte, in, present, blank
|
|
7
|
+
module BasicComparisonOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value)
|
|
9
|
+
case op
|
|
10
|
+
when "eq"
|
|
11
|
+
# Equality - uses Ruby's == for comparison
|
|
12
|
+
actual_value == expected_value
|
|
13
|
+
|
|
14
|
+
when "neq"
|
|
15
|
+
# Not equal - inverse of ==
|
|
16
|
+
actual_value != expected_value
|
|
17
|
+
|
|
18
|
+
when "gt"
|
|
19
|
+
# Greater than - only for comparable types (numbers, strings)
|
|
20
|
+
comparable?(actual_value, expected_value) && actual_value > expected_value
|
|
21
|
+
|
|
22
|
+
when "gte"
|
|
23
|
+
# Greater than or equal - only for comparable types
|
|
24
|
+
comparable?(actual_value, expected_value) && actual_value >= expected_value
|
|
25
|
+
|
|
26
|
+
when "lt"
|
|
27
|
+
# Less than - only for comparable types
|
|
28
|
+
comparable?(actual_value, expected_value) && actual_value < expected_value
|
|
29
|
+
|
|
30
|
+
when "lte"
|
|
31
|
+
# Less than or equal - only for comparable types
|
|
32
|
+
comparable?(actual_value, expected_value) && actual_value <= expected_value
|
|
33
|
+
|
|
34
|
+
when "in"
|
|
35
|
+
# Array membership - checks if actual_value is in the expected array
|
|
36
|
+
Array(expected_value).include?(actual_value)
|
|
37
|
+
|
|
38
|
+
when "present"
|
|
39
|
+
# PRESENT SEMANTICS:
|
|
40
|
+
# Returns true if value exists AND is not empty
|
|
41
|
+
# - nil: false
|
|
42
|
+
# - Empty string "": false
|
|
43
|
+
# - Empty array []: false
|
|
44
|
+
# - Empty hash {}: false
|
|
45
|
+
# - Zero 0: true (zero is a valid value)
|
|
46
|
+
# - False boolean: true (false is a valid value)
|
|
47
|
+
# - Non-empty values: true
|
|
48
|
+
!actual_value.nil? && (actual_value.respond_to?(:empty?) ? !actual_value.empty? : true)
|
|
49
|
+
|
|
50
|
+
when "blank"
|
|
51
|
+
# BLANK SEMANTICS:
|
|
52
|
+
# Returns true if value is nil OR empty
|
|
53
|
+
# - nil: true
|
|
54
|
+
# - Empty string "": true
|
|
55
|
+
# - Empty array []: true
|
|
56
|
+
# - Empty hash {}: true
|
|
57
|
+
# - Zero 0: false (zero is a valid value)
|
|
58
|
+
# - False boolean: false (false is a valid value)
|
|
59
|
+
# - Non-empty values: false
|
|
60
|
+
actual_value.nil? || (actual_value.respond_to?(:empty?) ? actual_value.empty? : false)
|
|
61
|
+
end
|
|
62
|
+
# Returns nil if not handled by this module
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Checks if two values can be compared with <, >, <=, >=
|
|
66
|
+
# Allows comparison between numeric types (Float, Integer, etc.) or same string types
|
|
67
|
+
def self.comparable?(val1, val2)
|
|
68
|
+
# Both are numeric - allow comparison between different numeric types
|
|
69
|
+
# (e.g., Integer and Float are comparable in Ruby)
|
|
70
|
+
return true if val1.is_a?(Numeric) && val2.is_a?(Numeric)
|
|
71
|
+
|
|
72
|
+
# Both are strings - require exact same type
|
|
73
|
+
return val1.instance_of?(val2.class) if val1.is_a?(String) && val2.is_a?(String)
|
|
74
|
+
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dsl
|
|
5
|
+
module Operators
|
|
6
|
+
# Handles collection operators: contains_all, contains_any, intersects, subset_of
|
|
7
|
+
module CollectionOperators
|
|
8
|
+
def self.handle(op, actual_value, expected_value)
|
|
9
|
+
case op
|
|
10
|
+
when "contains_all"
|
|
11
|
+
# Checks if array contains all specified elements
|
|
12
|
+
return false unless actual_value.is_a?(Array)
|
|
13
|
+
return false unless expected_value.is_a?(Array)
|
|
14
|
+
return true if expected_value.empty?
|
|
15
|
+
|
|
16
|
+
# OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
|
|
17
|
+
actual_set = actual_value.to_set
|
|
18
|
+
expected_value.all? { |item| actual_set.include?(item) }
|
|
19
|
+
|
|
20
|
+
when "contains_any"
|
|
21
|
+
# Checks if array contains any of the specified elements
|
|
22
|
+
return false unless actual_value.is_a?(Array)
|
|
23
|
+
return false unless expected_value.is_a?(Array)
|
|
24
|
+
return false if expected_value.empty?
|
|
25
|
+
|
|
26
|
+
# OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
|
|
27
|
+
actual_set = actual_value.to_set
|
|
28
|
+
expected_value.any? { |item| actual_set.include?(item) }
|
|
29
|
+
|
|
30
|
+
when "intersects"
|
|
31
|
+
# Checks if two arrays have any common elements
|
|
32
|
+
return false unless actual_value.is_a?(Array)
|
|
33
|
+
return false unless expected_value.is_a?(Array)
|
|
34
|
+
return false if actual_value.empty? || expected_value.empty?
|
|
35
|
+
|
|
36
|
+
# OPTIMIZE: Use Set intersection for O(n) instead of array & which creates intermediate array
|
|
37
|
+
if actual_value.size <= expected_value.size
|
|
38
|
+
expected_set = expected_value.to_set
|
|
39
|
+
actual_value.any? { |item| expected_set.include?(item) }
|
|
40
|
+
else
|
|
41
|
+
actual_set = actual_value.to_set
|
|
42
|
+
expected_value.any? { |item| actual_set.include?(item) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
when "subset_of"
|
|
46
|
+
# Checks if array is a subset of another array
|
|
47
|
+
return false unless actual_value.is_a?(Array)
|
|
48
|
+
return false unless expected_value.is_a?(Array)
|
|
49
|
+
return true if actual_value.empty?
|
|
50
|
+
|
|
51
|
+
# OPTIMIZE: Use Set for O(1) lookups instead of O(n) include?
|
|
52
|
+
expected_set = expected_value.to_set
|
|
53
|
+
actual_value.all? { |item| expected_set.include?(item) }
|
|
54
|
+
end
|
|
55
|
+
# Returns nil if not handled by this module
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|