decision_agent 0.1.7 → 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 +1132 -12
- data/lib/decision_agent/dsl/schema_validator.rb +12 -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/app.js +119 -1
- 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 +71 -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/advanced_operators_spec.rb +2147 -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 +1909 -0
- 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 +66 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "feel/evaluator"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "decision_tree"
|
|
6
|
+
|
|
7
|
+
module DecisionAgent
|
|
8
|
+
module Dmn
|
|
9
|
+
# Represents a decision in a decision graph
|
|
10
|
+
class DecisionNode
|
|
11
|
+
attr_reader :id, :name, :decision_logic, :information_requirements
|
|
12
|
+
attr_accessor :value, :evaluated
|
|
13
|
+
|
|
14
|
+
def initialize(id:, name:, decision_logic: nil)
|
|
15
|
+
@id = id
|
|
16
|
+
@name = name
|
|
17
|
+
@decision_logic = decision_logic # Can be DecisionTable, DecisionTree, or literal
|
|
18
|
+
@information_requirements = [] # Dependencies on other decisions
|
|
19
|
+
@value = nil
|
|
20
|
+
@evaluated = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_dependency(decision_id, variable_name = nil)
|
|
24
|
+
@information_requirements << {
|
|
25
|
+
decision_id: decision_id,
|
|
26
|
+
variable_name: variable_name || decision_id
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def depends_on?(decision_id)
|
|
31
|
+
@information_requirements.any? { |req| req[:decision_id] == decision_id }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset!
|
|
35
|
+
@value = nil
|
|
36
|
+
@evaluated = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
id: @id,
|
|
42
|
+
name: @name,
|
|
43
|
+
information_requirements: @information_requirements,
|
|
44
|
+
decision_logic_type: decision_logic_type
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def decision_logic_type
|
|
51
|
+
case @decision_logic
|
|
52
|
+
when DecisionTree
|
|
53
|
+
"decision_tree"
|
|
54
|
+
when Hash
|
|
55
|
+
"decision_table"
|
|
56
|
+
else
|
|
57
|
+
"literal"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Represents and evaluates a decision graph (DMN model with multiple decisions)
|
|
63
|
+
class DecisionGraph
|
|
64
|
+
attr_reader :id, :name, :decisions
|
|
65
|
+
|
|
66
|
+
def initialize(id:, name:)
|
|
67
|
+
@id = id
|
|
68
|
+
@name = name
|
|
69
|
+
@decisions = {} # decision_id => DecisionNode
|
|
70
|
+
@feel_evaluator = Feel::Evaluator.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_decision(decision)
|
|
74
|
+
@decisions[decision.id] = decision
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_decision(decision_id)
|
|
78
|
+
@decisions[decision_id]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Evaluate a specific decision (and all its dependencies)
|
|
82
|
+
def evaluate(decision_id, context)
|
|
83
|
+
decision = @decisions[decision_id]
|
|
84
|
+
raise DmnError, "Decision '#{decision_id}' not found" unless decision
|
|
85
|
+
|
|
86
|
+
# Reset all decision evaluations
|
|
87
|
+
reset_all!
|
|
88
|
+
|
|
89
|
+
# Build evaluation context
|
|
90
|
+
eval_context = context.is_a?(Hash) ? context : context.to_h
|
|
91
|
+
|
|
92
|
+
# Evaluate the requested decision (will recursively evaluate dependencies)
|
|
93
|
+
evaluate_decision(decision, eval_context)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Evaluate all decisions in the graph
|
|
97
|
+
def evaluate_all(context)
|
|
98
|
+
reset_all!
|
|
99
|
+
eval_context = context.is_a?(Hash) ? context : context.to_h
|
|
100
|
+
|
|
101
|
+
results = {}
|
|
102
|
+
@decisions.each do |decision_id, decision|
|
|
103
|
+
results[decision_id] = evaluate_decision(decision, eval_context) unless decision.evaluated
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get decisions in topological order (respecting dependencies)
|
|
110
|
+
def topological_order
|
|
111
|
+
order = []
|
|
112
|
+
visited = Set.new
|
|
113
|
+
temp_mark = Set.new
|
|
114
|
+
|
|
115
|
+
visit = lambda do |decision_id|
|
|
116
|
+
return if visited.include?(decision_id)
|
|
117
|
+
|
|
118
|
+
raise DmnError, "Circular dependency detected involving decision '#{decision_id}'" if temp_mark.include?(decision_id)
|
|
119
|
+
|
|
120
|
+
temp_mark.add(decision_id)
|
|
121
|
+
|
|
122
|
+
decision = @decisions[decision_id]
|
|
123
|
+
decision.information_requirements.each do |req|
|
|
124
|
+
visit.call(req[:decision_id]) if @decisions[req[:decision_id]]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
temp_mark.delete(decision_id)
|
|
128
|
+
visited.add(decision_id)
|
|
129
|
+
order << decision_id
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@decisions.each_key { |decision_id| visit.call(decision_id) }
|
|
133
|
+
order
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Detect circular dependencies
|
|
137
|
+
def circular_dependencies?
|
|
138
|
+
topological_order
|
|
139
|
+
false
|
|
140
|
+
rescue DmnError => e
|
|
141
|
+
e.message.include?("Circular dependency")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get all leaf decisions (no other decisions depend on them)
|
|
145
|
+
def leaf_decisions
|
|
146
|
+
dependent_decisions = Set.new
|
|
147
|
+
@decisions.each_value do |decision|
|
|
148
|
+
decision.information_requirements.each do |req|
|
|
149
|
+
dependent_decisions.add(req[:decision_id])
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@decisions.keys.reject { |id| dependent_decisions.include?(id) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get all root decisions (don't depend on other decisions)
|
|
157
|
+
def root_decisions
|
|
158
|
+
@decisions.select { |_id, decision| decision.information_requirements.empty? }.keys
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get the dependency graph as a hash
|
|
162
|
+
def dependency_graph
|
|
163
|
+
graph = {}
|
|
164
|
+
@decisions.each do |id, decision|
|
|
165
|
+
graph[id] = decision.information_requirements.map { |req| req[:decision_id] }
|
|
166
|
+
end
|
|
167
|
+
graph
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Export graph structure
|
|
171
|
+
def to_h
|
|
172
|
+
{
|
|
173
|
+
id: @id,
|
|
174
|
+
name: @name,
|
|
175
|
+
decisions: @decisions.transform_values(&:to_h),
|
|
176
|
+
dependency_graph: dependency_graph
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def reset_all!
|
|
183
|
+
@decisions.each_value(&:reset!)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def evaluate_decision(decision, context)
|
|
187
|
+
# If already evaluated, return cached value
|
|
188
|
+
return decision.value if decision.evaluated
|
|
189
|
+
|
|
190
|
+
# First, evaluate all dependencies
|
|
191
|
+
decision.information_requirements.each do |req|
|
|
192
|
+
dep_decision = @decisions[req[:decision_id]]
|
|
193
|
+
next unless dep_decision
|
|
194
|
+
|
|
195
|
+
# Recursively evaluate dependency
|
|
196
|
+
dep_value = evaluate_decision(dep_decision, context)
|
|
197
|
+
|
|
198
|
+
# Add dependency result to context with the specified variable name
|
|
199
|
+
context[req[:variable_name]] = dep_value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Now evaluate this decision with the enriched context
|
|
203
|
+
decision.value = evaluate_decision_logic(decision, context)
|
|
204
|
+
decision.evaluated = true
|
|
205
|
+
decision.value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def evaluate_decision_logic(decision, context)
|
|
209
|
+
case decision.decision_logic
|
|
210
|
+
when DecisionTree
|
|
211
|
+
# Evaluate decision tree
|
|
212
|
+
decision.decision_logic.evaluate(context)
|
|
213
|
+
when Hash
|
|
214
|
+
# Evaluate decision table (simplified)
|
|
215
|
+
evaluate_decision_table(decision.decision_logic, context)
|
|
216
|
+
when String
|
|
217
|
+
# Evaluate as FEEL expression (literal expression)
|
|
218
|
+
@feel_evaluator.evaluate(decision.decision_logic, "result", context)
|
|
219
|
+
when Proc
|
|
220
|
+
# Execute custom logic with string keys
|
|
221
|
+
string_context = context.transform_keys(&:to_s)
|
|
222
|
+
decision.decision_logic.call(string_context)
|
|
223
|
+
else
|
|
224
|
+
# Return as-is
|
|
225
|
+
decision.decision_logic
|
|
226
|
+
end
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
raise DmnError, "Failed to evaluate decision '#{decision.id}': #{e.message}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def evaluate_decision_table(table, context)
|
|
232
|
+
# Simplified decision table evaluation
|
|
233
|
+
# In a full implementation, this would delegate to the DecisionTable evaluator
|
|
234
|
+
rules = table[:rules] || []
|
|
235
|
+
|
|
236
|
+
matching_rule = rules.find do |rule|
|
|
237
|
+
rule[:conditions].all? do |input_id, condition|
|
|
238
|
+
value = context[input_id]
|
|
239
|
+
evaluate_condition(condition, value, context)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
matching_rule ? matching_rule[:output] : nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def evaluate_condition(condition, value, context)
|
|
247
|
+
return true if condition.nil? || condition == "-"
|
|
248
|
+
|
|
249
|
+
@feel_evaluator.evaluate(
|
|
250
|
+
"#{value} #{condition}",
|
|
251
|
+
"condition",
|
|
252
|
+
context
|
|
253
|
+
)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Parser for DMN decision graphs from XML
|
|
260
|
+
class DecisionGraphParser
|
|
261
|
+
def self.parse(xml_doc)
|
|
262
|
+
# Extract namespace and model info
|
|
263
|
+
definitions = xml_doc.at_xpath("//dmn:definitions") || xml_doc.root
|
|
264
|
+
model_id = definitions["id"] || "decision_graph"
|
|
265
|
+
model_name = definitions["name"] || model_id
|
|
266
|
+
|
|
267
|
+
graph = DecisionGraph.new(id: model_id, name: model_name)
|
|
268
|
+
|
|
269
|
+
# Parse all decisions
|
|
270
|
+
decisions = xml_doc.xpath("//dmn:decision")
|
|
271
|
+
decisions.each do |decision_xml|
|
|
272
|
+
decision_node = parse_decision_node(decision_xml)
|
|
273
|
+
graph.add_decision(decision_node)
|
|
274
|
+
|
|
275
|
+
# Parse information requirements (dependencies)
|
|
276
|
+
decision_id = decision_xml["id"]
|
|
277
|
+
decision_node = graph.get_decision(decision_id)
|
|
278
|
+
|
|
279
|
+
# Find all information requirements
|
|
280
|
+
info_reqs = decision_xml.xpath(".//dmn:informationRequirement")
|
|
281
|
+
info_reqs.each do |req|
|
|
282
|
+
required_decision = req.at_xpath(".//dmn:requiredDecision")
|
|
283
|
+
if required_decision
|
|
284
|
+
required_id = required_decision["href"]&.sub("#", "")
|
|
285
|
+
decision_node.add_dependency(required_id) if required_id
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
graph
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def self.parse_decision_node(decision_xml)
|
|
294
|
+
decision_id = decision_xml["id"]
|
|
295
|
+
decision_name = decision_xml["name"] || decision_id
|
|
296
|
+
|
|
297
|
+
# Check for decision table
|
|
298
|
+
decision_table = decision_xml.at_xpath(".//dmn:decisionTable")
|
|
299
|
+
if decision_table
|
|
300
|
+
# Parse decision table (simplified)
|
|
301
|
+
decision_logic = parse_decision_table(decision_table)
|
|
302
|
+
else
|
|
303
|
+
# Check for literal expression (could be decision tree or simple expression)
|
|
304
|
+
literal_expr = decision_xml.at_xpath(".//dmn:literalExpression")
|
|
305
|
+
decision_logic = literal_expr&.text&.strip
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
DecisionNode.new(
|
|
309
|
+
id: decision_id,
|
|
310
|
+
name: decision_name,
|
|
311
|
+
decision_logic: decision_logic
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def self.parse_decision_table(table_xml)
|
|
316
|
+
# Simplified decision table parsing
|
|
317
|
+
# Full implementation would use the existing DecisionTable parser
|
|
318
|
+
{
|
|
319
|
+
type: "decision_table",
|
|
320
|
+
hit_policy: table_xml["hitPolicy"] || "UNIQUE",
|
|
321
|
+
inputs: [],
|
|
322
|
+
rules: []
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "feel/evaluator"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module DecisionAgent
|
|
7
|
+
module Dmn
|
|
8
|
+
# Represents a node in a decision tree
|
|
9
|
+
class TreeNode
|
|
10
|
+
attr_reader :id, :label, :condition, :decision, :children
|
|
11
|
+
attr_accessor :parent
|
|
12
|
+
|
|
13
|
+
def initialize(id:, label: nil, condition: nil, decision: nil)
|
|
14
|
+
@id = id
|
|
15
|
+
@label = label
|
|
16
|
+
@condition = condition # FEEL expression to evaluate
|
|
17
|
+
@decision = decision # Output decision if this is a leaf node
|
|
18
|
+
@children = []
|
|
19
|
+
@parent = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_child(node)
|
|
23
|
+
node.parent = self
|
|
24
|
+
@children << node
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def leaf?
|
|
28
|
+
@children.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
id: @id,
|
|
34
|
+
label: @label,
|
|
35
|
+
condition: @condition,
|
|
36
|
+
decision: @decision,
|
|
37
|
+
children: @children.map(&:to_h)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Evaluates decision trees
|
|
43
|
+
class DecisionTree
|
|
44
|
+
attr_reader :id, :name, :root
|
|
45
|
+
|
|
46
|
+
def initialize(id:, name:, root: nil)
|
|
47
|
+
@id = id
|
|
48
|
+
@name = name
|
|
49
|
+
@root = root || TreeNode.new(id: "root", label: "Root")
|
|
50
|
+
@feel_evaluator = Feel::Evaluator.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Evaluate the decision tree with given context
|
|
54
|
+
def evaluate(context)
|
|
55
|
+
traverse(@root, context)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build a decision tree from a hash representation
|
|
59
|
+
def self.from_hash(hash)
|
|
60
|
+
tree = new(id: hash[:id], name: hash[:name])
|
|
61
|
+
tree.instance_variable_set(:@root, build_node(hash[:root]))
|
|
62
|
+
tree
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Convert tree to hash representation
|
|
66
|
+
def to_h
|
|
67
|
+
{
|
|
68
|
+
id: @id,
|
|
69
|
+
name: @name,
|
|
70
|
+
root: @root.to_h
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get all leaf nodes (decision outcomes)
|
|
75
|
+
def leaf_nodes
|
|
76
|
+
collect_leaf_nodes(@root)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get tree depth
|
|
80
|
+
def depth
|
|
81
|
+
calculate_depth(@root)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get all paths from root to leaves
|
|
85
|
+
def paths
|
|
86
|
+
collect_paths(@root, [])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def traverse(node, context)
|
|
92
|
+
# If this is a leaf node, return the decision
|
|
93
|
+
return node.decision if node.leaf?
|
|
94
|
+
|
|
95
|
+
# Track if any condition was successfully evaluated
|
|
96
|
+
any_condition_evaluated = false
|
|
97
|
+
has_children_with_conditions = node.children.any?(&:condition)
|
|
98
|
+
|
|
99
|
+
# Evaluate each child's condition until we find a match
|
|
100
|
+
node.children.each do |child|
|
|
101
|
+
next unless child.condition
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
result = @feel_evaluator.evaluate(child.condition, "condition", context.to_h)
|
|
105
|
+
any_condition_evaluated = true
|
|
106
|
+
|
|
107
|
+
return traverse(child, context) if result
|
|
108
|
+
# Condition matched, continue down this branch
|
|
109
|
+
|
|
110
|
+
# Condition evaluated to false - check if this child has a false branch
|
|
111
|
+
# If child has multiple leaf children with no conditions, take the second one
|
|
112
|
+
if !child.leaf? && child.children.all? { |c| c.condition.nil? && c.leaf? } && child.children.size > 1
|
|
113
|
+
return child.children[1].decision
|
|
114
|
+
end
|
|
115
|
+
rescue StandardError
|
|
116
|
+
# If condition evaluation fails, skip this branch
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# No matching condition found, check for a default branch (no condition)
|
|
122
|
+
# Take default if: no children have conditions, OR at least one condition was successfully evaluated
|
|
123
|
+
if !has_children_with_conditions || any_condition_evaluated
|
|
124
|
+
default_child = node.children.find { |c| c.condition.nil? }
|
|
125
|
+
return traverse(default_child, context) if default_child
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# No match found
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.build_node(hash)
|
|
133
|
+
node = TreeNode.new(
|
|
134
|
+
id: hash[:id],
|
|
135
|
+
label: hash[:label],
|
|
136
|
+
condition: hash[:condition],
|
|
137
|
+
decision: hash[:decision]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
hash[:children]&.each do |child_hash|
|
|
141
|
+
node.add_child(build_node(child_hash))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
node
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def collect_leaf_nodes(node, leaves = [])
|
|
148
|
+
if node.leaf?
|
|
149
|
+
leaves << node
|
|
150
|
+
else
|
|
151
|
+
node.children.each { |child| collect_leaf_nodes(child, leaves) }
|
|
152
|
+
end
|
|
153
|
+
leaves
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def calculate_depth(node, current_depth = 0)
|
|
157
|
+
return current_depth if node.leaf?
|
|
158
|
+
|
|
159
|
+
max_child_depth = node.children.map { |child| calculate_depth(child, current_depth + 1) }.max
|
|
160
|
+
max_child_depth || current_depth
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def collect_paths(node, current_path, paths = [])
|
|
164
|
+
current_path += [node]
|
|
165
|
+
|
|
166
|
+
if node.leaf?
|
|
167
|
+
paths << current_path
|
|
168
|
+
else
|
|
169
|
+
node.children.each { |child| collect_paths(child, current_path, paths) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
paths
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Parser for DMN decision trees (literal expressions)
|
|
177
|
+
class DecisionTreeParser
|
|
178
|
+
def self.parse(xml_element)
|
|
179
|
+
# Parse DMN literal expression (decision tree representation)
|
|
180
|
+
# This is a simplified parser - full DMN tree parsing would be more complex
|
|
181
|
+
tree_id = xml_element["id"]
|
|
182
|
+
tree_name = xml_element["name"] || tree_id
|
|
183
|
+
|
|
184
|
+
DecisionTree.new(id: tree_id, name: tree_name)
|
|
185
|
+
|
|
186
|
+
# Parse the tree structure from XML
|
|
187
|
+
# Note: This is a placeholder for full DMN literal expression parsing
|
|
188
|
+
# In a complete implementation, this would parse the DMN tree structure
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module DecisionAgent
|
|
2
|
+
module Dmn
|
|
3
|
+
# Base error for all DMN-related errors
|
|
4
|
+
class DmnError < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when DMN XML is invalid or malformed
|
|
7
|
+
class InvalidDmnXmlError < DmnError; end
|
|
8
|
+
|
|
9
|
+
# Raised when DMN model structure is invalid
|
|
10
|
+
class InvalidDmnModelError < DmnError; end
|
|
11
|
+
|
|
12
|
+
# Raised when hit policy is unsupported
|
|
13
|
+
class UnsupportedHitPolicyError < DmnError; end
|
|
14
|
+
|
|
15
|
+
# Raised when FEEL expression cannot be parsed
|
|
16
|
+
class FeelParseError < DmnError; end
|
|
17
|
+
|
|
18
|
+
# Raised when parse tree transformation to AST fails
|
|
19
|
+
class FeelTransformError < DmnError; end
|
|
20
|
+
|
|
21
|
+
# Raised when FEEL expression evaluation fails
|
|
22
|
+
class FeelEvaluationError < DmnError; end
|
|
23
|
+
|
|
24
|
+
# Raised when type conversion or type checking fails
|
|
25
|
+
class FeelTypeError < DmnError; end
|
|
26
|
+
|
|
27
|
+
# Raised when a function is not found or has invalid arguments
|
|
28
|
+
class FeelFunctionError < DmnError; end
|
|
29
|
+
end
|
|
30
|
+
end
|