decision_agent 0.3.0 → 1.0.1
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 +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- 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
data/spec/dmn/hit_policy_spec.rb
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent"
|
|
3
|
-
require "decision_agent/dmn/model"
|
|
4
|
-
require "decision_agent/evaluators/dmn_evaluator"
|
|
5
|
-
|
|
6
|
-
RSpec.describe "DMN Hit Policies" do
|
|
7
|
-
let(:model) { DecisionAgent::Dmn::Model.new(id: "test_model", name: "Test Model") }
|
|
8
|
-
|
|
9
|
-
def create_decision_table(hit_policy, rules_data)
|
|
10
|
-
table = DecisionAgent::Dmn::DecisionTable.new(id: "test_table", hit_policy: hit_policy)
|
|
11
|
-
|
|
12
|
-
# Add inputs
|
|
13
|
-
table.add_input(DecisionAgent::Dmn::Input.new(id: "input1", label: "value"))
|
|
14
|
-
table.add_output(DecisionAgent::Dmn::Output.new(id: "output1", label: "decision", name: "decision"))
|
|
15
|
-
|
|
16
|
-
# Add rules
|
|
17
|
-
rules_data.each do |rule_data|
|
|
18
|
-
rule = DecisionAgent::Dmn::Rule.new(id: rule_data[:id], description: rule_data[:description])
|
|
19
|
-
rule.add_input_entry(rule_data[:input])
|
|
20
|
-
rule.add_output_entry(rule_data[:output])
|
|
21
|
-
table.add_rule(rule)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
decision = DecisionAgent::Dmn::Decision.new(id: "test_decision", name: "Test Decision")
|
|
25
|
-
decision.decision_table = table
|
|
26
|
-
model.add_decision(decision)
|
|
27
|
-
|
|
28
|
-
table
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
describe "UNIQUE hit policy" do
|
|
32
|
-
it "returns result when exactly one rule matches" do
|
|
33
|
-
create_decision_table("UNIQUE", [
|
|
34
|
-
{ id: "rule1", input: ">= 10", output: '"approved"', description: "High value" },
|
|
35
|
-
{ id: "rule2", input: "< 10", output: '"rejected"', description: "Low value" }
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
39
|
-
model: model,
|
|
40
|
-
decision_id: "test_decision"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
44
|
-
expect(evaluation).not_to be_nil
|
|
45
|
-
expect(evaluation.decision).to eq("approved")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "raises error when no rules match" do
|
|
49
|
-
create_decision_table("UNIQUE", [
|
|
50
|
-
{ id: "rule1", input: ">= 10", output: '"approved"', description: "High value" },
|
|
51
|
-
{ id: "rule2", input: "> 20", output: '"rejected"', description: "Very high value" }
|
|
52
|
-
])
|
|
53
|
-
|
|
54
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
55
|
-
model: model,
|
|
56
|
-
decision_id: "test_decision"
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Value 5 doesn't match >= 10 or > 20, so no rules match
|
|
60
|
-
expect do
|
|
61
|
-
evaluator.evaluate(DecisionAgent::Context.new(value: 5))
|
|
62
|
-
end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /UNIQUE hit policy requires exactly one matching rule/)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it "raises error when multiple rules match" do
|
|
66
|
-
create_decision_table("UNIQUE", [
|
|
67
|
-
{ id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
|
|
68
|
-
{ id: "rule2", input: ">= 10", output: '"approved"', description: "Rule 2" }
|
|
69
|
-
])
|
|
70
|
-
|
|
71
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
72
|
-
model: model,
|
|
73
|
-
decision_id: "test_decision"
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
expect do
|
|
77
|
-
evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
78
|
-
end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /UNIQUE hit policy requires exactly one matching rule, but 2 matched/)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
describe "FIRST hit policy" do
|
|
83
|
-
it "returns first matching rule when multiple rules match" do
|
|
84
|
-
create_decision_table("FIRST", [
|
|
85
|
-
{ id: "rule1", input: ">= 5", output: '"first"', description: "First rule" },
|
|
86
|
-
{ id: "rule2", input: ">= 10", output: '"second"', description: "Second rule" }
|
|
87
|
-
])
|
|
88
|
-
|
|
89
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
90
|
-
model: model,
|
|
91
|
-
decision_id: "test_decision"
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
95
|
-
expect(evaluation).not_to be_nil
|
|
96
|
-
expect(evaluation.decision).to eq("first")
|
|
97
|
-
expect(evaluation.metadata[:rule_id]).to eq("rule1")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
it "returns nil when no rules match" do
|
|
101
|
-
create_decision_table("FIRST", [
|
|
102
|
-
{ id: "rule1", input: ">= 10", output: '"approved"', description: "High value" }
|
|
103
|
-
])
|
|
104
|
-
|
|
105
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
106
|
-
model: model,
|
|
107
|
-
decision_id: "test_decision"
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 5))
|
|
111
|
-
expect(evaluation).to be_nil
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
describe "PRIORITY hit policy" do
|
|
116
|
-
it "returns first matching rule (rule order determines priority)" do
|
|
117
|
-
create_decision_table("PRIORITY", [
|
|
118
|
-
{ id: "rule1", input: ">= 5", output: '"high_priority"', description: "High priority rule" },
|
|
119
|
-
{ id: "rule2", input: ">= 10", output: '"low_priority"', description: "Low priority rule" }
|
|
120
|
-
])
|
|
121
|
-
|
|
122
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
123
|
-
model: model,
|
|
124
|
-
decision_id: "test_decision"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
128
|
-
expect(evaluation).not_to be_nil
|
|
129
|
-
expect(evaluation.decision).to eq("high_priority")
|
|
130
|
-
expect(evaluation.metadata[:rule_id]).to eq("rule1")
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
describe "ANY hit policy" do
|
|
135
|
-
it "returns result when all matching rules have same output" do
|
|
136
|
-
create_decision_table("ANY", [
|
|
137
|
-
{ id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
|
|
138
|
-
{ id: "rule2", input: ">= 10", output: '"approved"', description: "Rule 2" }
|
|
139
|
-
])
|
|
140
|
-
|
|
141
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
142
|
-
model: model,
|
|
143
|
-
decision_id: "test_decision"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
147
|
-
expect(evaluation).not_to be_nil
|
|
148
|
-
expect(evaluation.decision).to eq("approved")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
it "raises error when matching rules have different outputs" do
|
|
152
|
-
create_decision_table("ANY", [
|
|
153
|
-
{ id: "rule1", input: ">= 5", output: '"approved"', description: "Rule 1" },
|
|
154
|
-
{ id: "rule2", input: ">= 10", output: '"rejected"', description: "Rule 2" }
|
|
155
|
-
])
|
|
156
|
-
|
|
157
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
158
|
-
model: model,
|
|
159
|
-
decision_id: "test_decision"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
expect do
|
|
163
|
-
evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
164
|
-
end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /ANY hit policy requires all matching rules to have the same output/)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
describe "COLLECT hit policy" do
|
|
169
|
-
it "returns first match with metadata about all matches" do
|
|
170
|
-
create_decision_table("COLLECT", [
|
|
171
|
-
{ id: "rule1", input: ">= 5", output: '"match1"', description: "Rule 1" },
|
|
172
|
-
{ id: "rule2", input: ">= 10", output: '"match2"', description: "Rule 2" }
|
|
173
|
-
])
|
|
174
|
-
|
|
175
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
176
|
-
model: model,
|
|
177
|
-
decision_id: "test_decision"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 15))
|
|
181
|
-
expect(evaluation).not_to be_nil
|
|
182
|
-
expect(evaluation.decision).to eq("match1")
|
|
183
|
-
expect(evaluation.metadata[:collect_count]).to eq(2)
|
|
184
|
-
expect(evaluation.metadata[:collect_decisions]).to eq(%w[match1 match2])
|
|
185
|
-
expect(evaluation.metadata[:collect_rule_ids]).to eq(%w[rule1 rule2])
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
it "returns nil when no rules match" do
|
|
189
|
-
create_decision_table("COLLECT", [
|
|
190
|
-
{ id: "rule1", input: ">= 10", output: '"approved"', description: "High value" }
|
|
191
|
-
])
|
|
192
|
-
|
|
193
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
194
|
-
model: model,
|
|
195
|
-
decision_id: "test_decision"
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
evaluation = evaluator.evaluate(DecisionAgent::Context.new(value: 5))
|
|
199
|
-
expect(evaluation).to be_nil
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent"
|
|
3
|
-
require "decision_agent/dmn/importer"
|
|
4
|
-
require "decision_agent/dmn/exporter"
|
|
5
|
-
require "decision_agent/evaluators/dmn_evaluator"
|
|
6
|
-
require "tempfile"
|
|
7
|
-
require "fileutils"
|
|
8
|
-
|
|
9
|
-
RSpec.describe "DMN Integration" do
|
|
10
|
-
let(:simple_dmn_path) { File.expand_path("../fixtures/dmn/simple_decision.dmn", __dir__) }
|
|
11
|
-
let(:complex_dmn_path) { File.expand_path("../fixtures/dmn/complex_decision.dmn", __dir__) }
|
|
12
|
-
let(:invalid_dmn_path) { File.expand_path("../fixtures/dmn/invalid_structure.dmn", __dir__) }
|
|
13
|
-
|
|
14
|
-
# Create temporary directory for file storage adapter
|
|
15
|
-
let(:temp_dir) { Dir.mktmpdir }
|
|
16
|
-
let(:version_manager) do
|
|
17
|
-
DecisionAgent::Versioning::VersionManager.new(
|
|
18
|
-
adapter: DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: temp_dir)
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
after do
|
|
23
|
-
FileUtils.rm_rf(temp_dir)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe "import and execute simple decision" do
|
|
27
|
-
it "imports DMN file and makes decisions" do
|
|
28
|
-
# Import
|
|
29
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
30
|
-
result = importer.import(simple_dmn_path, created_by: "test")
|
|
31
|
-
|
|
32
|
-
expect(result[:decisions_imported]).to eq(1)
|
|
33
|
-
expect(result[:model]).to be_a(DecisionAgent::Dmn::Model)
|
|
34
|
-
expect(result[:model].decisions.size).to eq(1)
|
|
35
|
-
|
|
36
|
-
# Create evaluator
|
|
37
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
38
|
-
model: result[:model],
|
|
39
|
-
decision_id: "age_check"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# Test approval case (age >= 18)
|
|
43
|
-
context_approve = DecisionAgent::Context.new({ age: 25 })
|
|
44
|
-
evaluation_approve = evaluator.evaluate(context_approve)
|
|
45
|
-
|
|
46
|
-
expect(evaluation_approve).not_to be_nil
|
|
47
|
-
expect(evaluation_approve.decision).to eq("approve")
|
|
48
|
-
expect(evaluation_approve.evaluator_name).to include("DmnEvaluator")
|
|
49
|
-
|
|
50
|
-
# Test rejection case (age < 18)
|
|
51
|
-
context_reject = DecisionAgent::Context.new({ age: 15 })
|
|
52
|
-
evaluation_reject = evaluator.evaluate(context_reject)
|
|
53
|
-
|
|
54
|
-
expect(evaluation_reject).not_to be_nil
|
|
55
|
-
expect(evaluation_reject.decision).to eq("reject")
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
describe "import and execute complex decision" do
|
|
60
|
-
it "handles multi-input decision tables" do
|
|
61
|
-
# Import
|
|
62
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
63
|
-
result = importer.import(complex_dmn_path, created_by: "test")
|
|
64
|
-
|
|
65
|
-
expect(result[:decisions_imported]).to eq(1)
|
|
66
|
-
|
|
67
|
-
# Create evaluator
|
|
68
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
69
|
-
model: result[:model],
|
|
70
|
-
decision_id: "loan_approval"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Test excellent case
|
|
74
|
-
context_excellent = DecisionAgent::Context.new({
|
|
75
|
-
credit_score: 800,
|
|
76
|
-
income: 75_000,
|
|
77
|
-
loan_amount: 150_000
|
|
78
|
-
})
|
|
79
|
-
evaluation_excellent = evaluator.evaluate(context_excellent)
|
|
80
|
-
|
|
81
|
-
expect(evaluation_excellent).not_to be_nil
|
|
82
|
-
expect(evaluation_excellent.decision).to eq("approve")
|
|
83
|
-
|
|
84
|
-
# Test good case
|
|
85
|
-
context_good = DecisionAgent::Context.new({
|
|
86
|
-
credit_score: 700,
|
|
87
|
-
income: 45_000,
|
|
88
|
-
loan_amount: 100_000
|
|
89
|
-
})
|
|
90
|
-
evaluation_good = evaluator.evaluate(context_good)
|
|
91
|
-
|
|
92
|
-
expect(evaluation_good).not_to be_nil
|
|
93
|
-
expect(evaluation_good.decision).to eq("conditional_approve")
|
|
94
|
-
|
|
95
|
-
# Test rejection case
|
|
96
|
-
context_reject = DecisionAgent::Context.new({
|
|
97
|
-
credit_score: 500,
|
|
98
|
-
income: 25_000,
|
|
99
|
-
loan_amount: 100_000
|
|
100
|
-
})
|
|
101
|
-
evaluation_reject = evaluator.evaluate(context_reject)
|
|
102
|
-
|
|
103
|
-
expect(evaluation_reject).not_to be_nil
|
|
104
|
-
expect(evaluation_reject.decision).to eq("reject")
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
describe "invalid DMN handling" do
|
|
109
|
-
it "validates and rejects invalid DMN structure" do
|
|
110
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
111
|
-
|
|
112
|
-
expect do
|
|
113
|
-
importer.import(invalid_dmn_path, created_by: "test")
|
|
114
|
-
end.to raise_error(DecisionAgent::Dmn::InvalidDmnModelError, /Expected 1 input entries, got 2/)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
describe "round-trip conversion" do
|
|
119
|
-
it "preserves structure through import-export-import cycle" do
|
|
120
|
-
# Import original
|
|
121
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
122
|
-
original = importer.import(simple_dmn_path, ruleset_name: "age_check_v1", created_by: "test")
|
|
123
|
-
|
|
124
|
-
expect(original[:decisions_imported]).to eq(1)
|
|
125
|
-
|
|
126
|
-
# Export
|
|
127
|
-
exporter = DecisionAgent::Dmn::Exporter.new(version_manager: version_manager)
|
|
128
|
-
exported_xml = exporter.export("age_check_v1")
|
|
129
|
-
|
|
130
|
-
expect(exported_xml).to include('xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"')
|
|
131
|
-
expect(exported_xml).to include("<decision")
|
|
132
|
-
expect(exported_xml).to include("<decisionTable")
|
|
133
|
-
|
|
134
|
-
# Re-import
|
|
135
|
-
reimported = importer.import_from_xml(exported_xml, ruleset_name: "age_check_v2", created_by: "test")
|
|
136
|
-
|
|
137
|
-
# Compare structures
|
|
138
|
-
expect(reimported[:model].decisions.size).to eq(original[:model].decisions.size)
|
|
139
|
-
expect(reimported[:decisions_imported]).to eq(original[:decisions_imported])
|
|
140
|
-
|
|
141
|
-
# Verify it still works
|
|
142
|
-
evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
143
|
-
model: reimported[:model],
|
|
144
|
-
decision_id: reimported[:model].decisions.first.id
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
context = DecisionAgent::Context.new({ age: 25 })
|
|
148
|
-
evaluation = evaluator.evaluate(context)
|
|
149
|
-
|
|
150
|
-
expect(evaluation).not_to be_nil
|
|
151
|
-
expect(evaluation.decision).to eq("approve")
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
describe "combining with JSON evaluators" do
|
|
156
|
-
it "works alongside JsonRuleEvaluator in same agent" do
|
|
157
|
-
# Load DMN
|
|
158
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
159
|
-
dmn_result = importer.import(simple_dmn_path, created_by: "test")
|
|
160
|
-
|
|
161
|
-
dmn_evaluator = DecisionAgent::Evaluators::DmnEvaluator.new(
|
|
162
|
-
model: dmn_result[:model],
|
|
163
|
-
decision_id: "age_check"
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
# Create JSON evaluator
|
|
167
|
-
json_rules = {
|
|
168
|
-
version: "1.0",
|
|
169
|
-
ruleset: "json_rules",
|
|
170
|
-
rules: [
|
|
171
|
-
{
|
|
172
|
-
id: "priority_rule",
|
|
173
|
-
if: { field: "priority", op: "eq", value: "high" },
|
|
174
|
-
then: { decision: "escalate", weight: 0.9, reason: "High priority escalation" }
|
|
175
|
-
}
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
json_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(
|
|
180
|
-
rules_json: json_rules
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
# Use both in agent
|
|
184
|
-
agent = DecisionAgent::Agent.new(
|
|
185
|
-
evaluators: [dmn_evaluator, json_evaluator]
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
# Test with context matching both evaluators
|
|
189
|
-
decision = agent.decide(
|
|
190
|
-
context: { age: 25, priority: "high" }
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
expect(%w[approve escalate]).to include(decision.decision)
|
|
194
|
-
expect(decision.evaluations.size).to eq(2)
|
|
195
|
-
expect(decision.evaluations.map(&:evaluator_name)).to include(
|
|
196
|
-
match(/DmnEvaluator/),
|
|
197
|
-
match(/JsonRuleEvaluator/)
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
describe "versioning integration" do
|
|
203
|
-
it "stores and retrieves DMN models from versioning system" do
|
|
204
|
-
importer = DecisionAgent::Dmn::Importer.new(version_manager: version_manager)
|
|
205
|
-
|
|
206
|
-
# Import first version
|
|
207
|
-
v1 = importer.import(simple_dmn_path, ruleset_name: "age_check", created_by: "test_user")
|
|
208
|
-
|
|
209
|
-
expect(v1[:versions].size).to eq(1)
|
|
210
|
-
expect(v1[:versions].first[:rule_id]).to eq("age_check")
|
|
211
|
-
|
|
212
|
-
# Get active version
|
|
213
|
-
active = version_manager.get_active_version(rule_id: "age_check")
|
|
214
|
-
expect(active).not_to be_nil
|
|
215
|
-
expect(active[:content]).to be_a(Hash)
|
|
216
|
-
# Content may have string or symbol keys depending on storage adapter
|
|
217
|
-
rules = active[:content]["rules"] || active[:content][:rules]
|
|
218
|
-
expect(rules).to be_an(Array)
|
|
219
|
-
|
|
220
|
-
# Get version history
|
|
221
|
-
versions = version_manager.get_versions(rule_id: "age_check")
|
|
222
|
-
expect(versions.size).to eq(1)
|
|
223
|
-
expect(versions.first[:created_by]).to eq("test_user")
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|