decision_agent 0.2.0 → 0.3.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 +41 -1
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
- data/lib/decision_agent/dsl/schema_validator.rb +2 -1
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +3 -0
- data/lib/decision_agent/web/public/styles.css +21 -0
- data/lib/decision_agent/web/server.rb +465 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
- data/spec/auth/rbac_adapter_spec.rb +228 -0
- data/spec/dmn/decision_graph_spec.rb +282 -0
- data/spec/dmn/decision_tree_spec.rb +203 -0
- data/spec/dmn/feel/errors_spec.rb +18 -0
- data/spec/dmn/feel/functions_spec.rb +400 -0
- data/spec/dmn/feel/simple_parser_spec.rb +274 -0
- data/spec/dmn/feel/types_spec.rb +176 -0
- data/spec/dmn/feel_parser_spec.rb +489 -0
- data/spec/dmn/hit_policy_spec.rb +202 -0
- data/spec/dmn/integration_spec.rb +226 -0
- data/spec/examples.txt +1846 -1570
- data/spec/fixtures/dmn/complex_decision.dmn +81 -0
- data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
- data/spec/fixtures/dmn/simple_decision.dmn +40 -0
- data/spec/monitoring/metrics_collector_spec.rb +37 -35
- data/spec/monitoring/monitored_agent_spec.rb +14 -11
- data/spec/performance_optimizations_spec.rb +10 -3
- data/spec/thread_safety_spec.rb +10 -2
- data/spec/web_ui_rack_spec.rb +294 -0
- metadata +65 -1
|
@@ -0,0 +1,226 @@
|
|
|
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
|