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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
  24. data/lib/decision_agent/dsl/schema_validator.rb +2 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  29. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  30. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  31. data/lib/decision_agent/web/public/index.html +3 -0
  32. data/lib/decision_agent/web/public/styles.css +21 -0
  33. data/lib/decision_agent/web/server.rb +465 -0
  34. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  35. data/spec/auth/rbac_adapter_spec.rb +228 -0
  36. data/spec/dmn/decision_graph_spec.rb +282 -0
  37. data/spec/dmn/decision_tree_spec.rb +203 -0
  38. data/spec/dmn/feel/errors_spec.rb +18 -0
  39. data/spec/dmn/feel/functions_spec.rb +400 -0
  40. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  41. data/spec/dmn/feel/types_spec.rb +176 -0
  42. data/spec/dmn/feel_parser_spec.rb +489 -0
  43. data/spec/dmn/hit_policy_spec.rb +202 -0
  44. data/spec/dmn/integration_spec.rb +226 -0
  45. data/spec/examples.txt +1846 -1570
  46. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  47. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  48. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  49. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  50. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  51. data/spec/performance_optimizations_spec.rb +10 -3
  52. data/spec/thread_safety_spec.rb +10 -2
  53. data/spec/web_ui_rack_spec.rb +294 -0
  54. 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