decision_agent 0.2.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 +313 -8
- data/bin/decision_agent +104 -0
- 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/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 +819 -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 +984 -838
- data/lib/decision_agent/dsl/schema_validator.rb +53 -14
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
- 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/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -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 +52 -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 +86 -0
- data/lib/decision_agent/web/server.rb +1059 -23
- data/lib/decision_agent.rb +60 -2
- metadata +105 -61
- 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 -481
- 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 -550
- 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/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 -1633
- 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 -499
- data/spec/monitoring/monitored_agent_spec.rb +0 -222
- 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 -486
- 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 -482
- 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 -1840
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Simulation and What-If Analysis module
|
|
2
|
+
# Provides tools for scenario testing, historical replay, impact analysis, shadow testing, and Monte Carlo simulation
|
|
3
|
+
|
|
4
|
+
require_relative "simulation/errors"
|
|
5
|
+
require_relative "simulation/replay_engine"
|
|
6
|
+
require_relative "simulation/what_if_analyzer"
|
|
7
|
+
require_relative "simulation/impact_analyzer"
|
|
8
|
+
require_relative "simulation/shadow_test_engine"
|
|
9
|
+
require_relative "simulation/scenario_engine"
|
|
10
|
+
require_relative "simulation/scenario_library"
|
|
11
|
+
require_relative "simulation/monte_carlo_simulator"
|
|
12
|
+
|
|
13
|
+
module DecisionAgent
|
|
14
|
+
module Simulation
|
|
15
|
+
# Main entry point for simulation features
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -3,7 +3,7 @@ module DecisionAgent
|
|
|
3
3
|
# MAJOR: Incremented for incompatible API changes
|
|
4
4
|
# MINOR: Incremented for backward-compatible functionality additions
|
|
5
5
|
# PATCH: Incremented for backward-compatible bug fixes
|
|
6
|
-
VERSION = "0.
|
|
6
|
+
VERSION = "1.0.1".freeze
|
|
7
7
|
|
|
8
8
|
# Validate version format (semantic versioning)
|
|
9
9
|
unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
|
|
@@ -33,10 +33,10 @@ module DecisionAgent
|
|
|
33
33
|
next_version_number = last_version ? last_version.version_number + 1 : 1
|
|
34
34
|
|
|
35
35
|
# Deactivate previous active versions
|
|
36
|
-
# Use
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
# Use update_all for better concurrency (avoids SQLite locking issues)
|
|
37
|
+
# Status "archived" is valid, so no need to trigger validations
|
|
38
|
+
rule_version_class.where(rule_id: rule_id, status: "active")
|
|
39
|
+
.update_all(status: "archived")
|
|
40
40
|
|
|
41
41
|
# Create new version
|
|
42
42
|
version = rule_version_class.create!(
|
|
@@ -87,12 +87,11 @@ module DecisionAgent
|
|
|
87
87
|
|
|
88
88
|
# Deactivate all other versions for this rule within the same transaction
|
|
89
89
|
# The lock ensures only one thread can perform this operation at a time
|
|
90
|
-
# Use
|
|
90
|
+
# Use update_all for better concurrency (avoids SQLite locking issues)
|
|
91
|
+
# Status "archived" is valid, so no need to trigger validations
|
|
91
92
|
rule_version_class.where(rule_id: version.rule_id, status: "active")
|
|
92
93
|
.where.not(id: version_id)
|
|
93
|
-
.
|
|
94
|
-
v.update!(status: "archived")
|
|
95
|
-
end
|
|
94
|
+
.update_all(status: "archived")
|
|
96
95
|
|
|
97
96
|
# Activate this version
|
|
98
97
|
version.update!(status: "active")
|
|
@@ -101,6 +100,22 @@ module DecisionAgent
|
|
|
101
100
|
serialize_version(version)
|
|
102
101
|
end
|
|
103
102
|
|
|
103
|
+
def delete_version(version_id:)
|
|
104
|
+
version = rule_version_class.find_by(id: version_id)
|
|
105
|
+
|
|
106
|
+
# Version not found
|
|
107
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
108
|
+
|
|
109
|
+
# Prevent deletion of active versions
|
|
110
|
+
raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first." if version.status == "active"
|
|
111
|
+
|
|
112
|
+
# Delete the version
|
|
113
|
+
version.destroy
|
|
114
|
+
true
|
|
115
|
+
rescue ActiveRecord::RecordNotFound
|
|
116
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
117
|
+
end
|
|
118
|
+
|
|
104
119
|
private
|
|
105
120
|
|
|
106
121
|
def rule_version_class
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../dmn/parser"
|
|
5
|
+
require_relative "../dmn/exporter"
|
|
6
|
+
require_relative "../dmn/importer"
|
|
7
|
+
require_relative "../dmn/validator"
|
|
8
|
+
require_relative "../dmn/model"
|
|
9
|
+
require_relative "../dmn/decision_tree"
|
|
10
|
+
require_relative "../dmn/decision_graph"
|
|
11
|
+
require_relative "../dmn/visualizer"
|
|
12
|
+
|
|
13
|
+
module DecisionAgent
|
|
14
|
+
module Web
|
|
15
|
+
# DMN Editor Backend
|
|
16
|
+
# Provides API endpoints for visual DMN modeling
|
|
17
|
+
class DmnEditor
|
|
18
|
+
attr_reader :storage
|
|
19
|
+
|
|
20
|
+
def initialize(storage: nil)
|
|
21
|
+
@storage = storage || {}
|
|
22
|
+
@storage_mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a new DMN model
|
|
26
|
+
def create_model(name:, namespace: nil)
|
|
27
|
+
model_id = generate_id
|
|
28
|
+
namespace ||= "http://decisonagent.com/dmn/#{model_id}"
|
|
29
|
+
|
|
30
|
+
model = Dmn::Model.new(
|
|
31
|
+
id: model_id,
|
|
32
|
+
name: name,
|
|
33
|
+
namespace: namespace
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
store_model(model_id, model)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
id: model_id,
|
|
40
|
+
name: name,
|
|
41
|
+
namespace: namespace,
|
|
42
|
+
decisions: [],
|
|
43
|
+
created_at: Time.now.utc.iso8601
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get a DMN model
|
|
48
|
+
def get_model(model_id)
|
|
49
|
+
model = retrieve_model(model_id)
|
|
50
|
+
return nil unless model
|
|
51
|
+
|
|
52
|
+
serialize_model(model)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Update DMN model metadata
|
|
56
|
+
def update_model(model_id, name: nil, namespace: nil)
|
|
57
|
+
model = retrieve_model(model_id)
|
|
58
|
+
return nil unless model
|
|
59
|
+
|
|
60
|
+
model.instance_variable_set(:@name, name) if name
|
|
61
|
+
model.instance_variable_set(:@namespace, namespace) if namespace
|
|
62
|
+
|
|
63
|
+
store_model(model_id, model)
|
|
64
|
+
serialize_model(model)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Delete a DMN model
|
|
68
|
+
# rubocop:disable Naming/PredicateMethod
|
|
69
|
+
def delete_model(model_id)
|
|
70
|
+
@storage_mutex.synchronize do
|
|
71
|
+
@storage.delete(model_id)
|
|
72
|
+
end
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
# rubocop:enable Naming/PredicateMethod
|
|
76
|
+
|
|
77
|
+
# Add a decision to a model
|
|
78
|
+
def add_decision(model_id:, decision_id:, name:, type: "decision_table")
|
|
79
|
+
model = retrieve_model(model_id)
|
|
80
|
+
return nil unless model
|
|
81
|
+
|
|
82
|
+
decision = Dmn::Decision.new(
|
|
83
|
+
id: decision_id,
|
|
84
|
+
name: name
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Initialize decision logic based on type
|
|
88
|
+
case type
|
|
89
|
+
when "decision_table"
|
|
90
|
+
decision.instance_variable_set(:@decision_table, Dmn::DecisionTable.new(
|
|
91
|
+
id: "#{decision_id}_table",
|
|
92
|
+
hit_policy: "FIRST"
|
|
93
|
+
))
|
|
94
|
+
when "decision_tree"
|
|
95
|
+
decision.instance_variable_set(:@decision_tree, Dmn::DecisionTree.new(
|
|
96
|
+
id: "#{decision_id}_tree",
|
|
97
|
+
name: name
|
|
98
|
+
))
|
|
99
|
+
when "literal"
|
|
100
|
+
decision.instance_variable_set(:@literal_expression, "")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
model.add_decision(decision)
|
|
104
|
+
store_model(model_id, model)
|
|
105
|
+
|
|
106
|
+
serialize_decision(decision)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Update a decision
|
|
110
|
+
def update_decision(model_id:, decision_id:, name: nil, logic: nil)
|
|
111
|
+
model = retrieve_model(model_id)
|
|
112
|
+
return nil unless model
|
|
113
|
+
|
|
114
|
+
decision = model.find_decision(decision_id)
|
|
115
|
+
return nil unless decision
|
|
116
|
+
|
|
117
|
+
decision.instance_variable_set(:@name, name) if name
|
|
118
|
+
|
|
119
|
+
update_decision_table(decision.decision_table, logic) if logic && decision.decision_table
|
|
120
|
+
|
|
121
|
+
store_model(model_id, model)
|
|
122
|
+
serialize_decision(decision)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Delete a decision
|
|
126
|
+
# rubocop:disable Naming/PredicateMethod
|
|
127
|
+
def delete_decision(model_id:, decision_id:)
|
|
128
|
+
model = retrieve_model(model_id)
|
|
129
|
+
return false unless model
|
|
130
|
+
|
|
131
|
+
model.decisions.reject! { |d| d.id == decision_id }
|
|
132
|
+
store_model(model_id, model)
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
# rubocop:enable Naming/PredicateMethod
|
|
136
|
+
|
|
137
|
+
# Add input to decision table
|
|
138
|
+
def add_input(model_id:, decision_id:, input_id:, label:, type_ref: nil, expression: nil)
|
|
139
|
+
model = retrieve_model(model_id)
|
|
140
|
+
return nil unless model
|
|
141
|
+
|
|
142
|
+
decision = model.find_decision(decision_id)
|
|
143
|
+
return nil unless decision&.decision_table
|
|
144
|
+
|
|
145
|
+
input = Dmn::Input.new(
|
|
146
|
+
id: input_id,
|
|
147
|
+
label: label,
|
|
148
|
+
type_ref: type_ref,
|
|
149
|
+
expression: expression
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
decision.decision_table.inputs << input
|
|
153
|
+
store_model(model_id, model)
|
|
154
|
+
|
|
155
|
+
serialize_input(input)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Add output to decision table
|
|
159
|
+
def add_output(model_id:, decision_id:, output_id:, label:, type_ref: nil, name: nil)
|
|
160
|
+
model = retrieve_model(model_id)
|
|
161
|
+
return nil unless model
|
|
162
|
+
|
|
163
|
+
decision = model.find_decision(decision_id)
|
|
164
|
+
return nil unless decision&.decision_table
|
|
165
|
+
|
|
166
|
+
output = Dmn::Output.new(
|
|
167
|
+
id: output_id,
|
|
168
|
+
label: label,
|
|
169
|
+
type_ref: type_ref,
|
|
170
|
+
name: name
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
decision.decision_table.outputs << output
|
|
174
|
+
store_model(model_id, model)
|
|
175
|
+
|
|
176
|
+
serialize_output(output)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Add rule to decision table
|
|
180
|
+
def add_rule(model_id:, decision_id:, rule_id:, input_entries:, output_entries:, description: nil)
|
|
181
|
+
model = retrieve_model(model_id)
|
|
182
|
+
return nil unless model
|
|
183
|
+
|
|
184
|
+
decision = model.find_decision(decision_id)
|
|
185
|
+
return nil unless decision&.decision_table
|
|
186
|
+
|
|
187
|
+
rule = Dmn::Rule.new(id: rule_id)
|
|
188
|
+
rule.instance_variable_set(:@input_entries, input_entries)
|
|
189
|
+
rule.instance_variable_set(:@output_entries, output_entries)
|
|
190
|
+
rule.instance_variable_set(:@description, description) if description
|
|
191
|
+
|
|
192
|
+
decision.decision_table.rules << rule
|
|
193
|
+
store_model(model_id, model)
|
|
194
|
+
|
|
195
|
+
serialize_rule(rule)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Update rule
|
|
199
|
+
def update_rule(model_id:, decision_id:, rule_id:, input_entries: nil, output_entries: nil, description: nil)
|
|
200
|
+
model = retrieve_model(model_id)
|
|
201
|
+
return nil unless model
|
|
202
|
+
|
|
203
|
+
decision = model.find_decision(decision_id)
|
|
204
|
+
return nil unless decision&.decision_table
|
|
205
|
+
|
|
206
|
+
rule = decision.decision_table.rules.find { |r| r.id == rule_id }
|
|
207
|
+
return nil unless rule
|
|
208
|
+
|
|
209
|
+
rule.instance_variable_set(:@input_entries, input_entries) if input_entries
|
|
210
|
+
rule.instance_variable_set(:@output_entries, output_entries) if output_entries
|
|
211
|
+
rule.instance_variable_set(:@description, description) if description
|
|
212
|
+
|
|
213
|
+
store_model(model_id, model)
|
|
214
|
+
serialize_rule(rule)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Delete rule
|
|
218
|
+
# rubocop:disable Naming/PredicateMethod
|
|
219
|
+
def delete_rule(model_id:, decision_id:, rule_id:)
|
|
220
|
+
model = retrieve_model(model_id)
|
|
221
|
+
return false unless model
|
|
222
|
+
|
|
223
|
+
decision = model.find_decision(decision_id)
|
|
224
|
+
return false unless decision&.decision_table
|
|
225
|
+
|
|
226
|
+
decision.decision_table.rules.reject! { |r| r.id == rule_id }
|
|
227
|
+
store_model(model_id, model)
|
|
228
|
+
true
|
|
229
|
+
end
|
|
230
|
+
# rubocop:enable Naming/PredicateMethod
|
|
231
|
+
|
|
232
|
+
# Validate a DMN model
|
|
233
|
+
def validate_model(model_id)
|
|
234
|
+
model = retrieve_model(model_id)
|
|
235
|
+
return { valid: false, errors: ["Model not found"] } unless model
|
|
236
|
+
|
|
237
|
+
validator = Dmn::Validator.new
|
|
238
|
+
validator.validate(model)
|
|
239
|
+
|
|
240
|
+
{
|
|
241
|
+
valid: validator.valid?,
|
|
242
|
+
errors: validator.errors,
|
|
243
|
+
warnings: validator.warnings
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Export DMN model to XML
|
|
248
|
+
def export_to_xml(model_id)
|
|
249
|
+
model = retrieve_model(model_id)
|
|
250
|
+
return nil unless model
|
|
251
|
+
|
|
252
|
+
exporter = Dmn::Exporter.new
|
|
253
|
+
exporter.export(model)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Import DMN model from XML
|
|
257
|
+
def import_from_xml(xml_content, name: nil)
|
|
258
|
+
parser = Dmn::Parser.new
|
|
259
|
+
model = parser.parse(xml_content)
|
|
260
|
+
|
|
261
|
+
# Generate new ID for imported model
|
|
262
|
+
model_id = generate_id
|
|
263
|
+
model.instance_variable_set(:@id, model_id)
|
|
264
|
+
model.instance_variable_set(:@name, name) if name
|
|
265
|
+
|
|
266
|
+
store_model(model_id, model)
|
|
267
|
+
|
|
268
|
+
serialize_model(model)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Generate visualization for decision tree
|
|
272
|
+
def visualize_tree(model_id:, decision_id:, format: "svg")
|
|
273
|
+
model = retrieve_model(model_id)
|
|
274
|
+
return nil unless model
|
|
275
|
+
|
|
276
|
+
decision = model.find_decision(decision_id)
|
|
277
|
+
return nil unless decision || !decision.decision_tree
|
|
278
|
+
|
|
279
|
+
case format.to_s.downcase
|
|
280
|
+
when "svg"
|
|
281
|
+
Dmn::Visualizer.tree_to_svg(decision.decision_tree)
|
|
282
|
+
when "dot"
|
|
283
|
+
Dmn::Visualizer.tree_to_dot(decision.decision_tree)
|
|
284
|
+
when "mermaid"
|
|
285
|
+
Dmn::Visualizer.tree_to_mermaid(decision.decision_tree)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Generate visualization for decision graph
|
|
290
|
+
def visualize_graph(model_id:, format: "svg")
|
|
291
|
+
model = retrieve_model(model_id)
|
|
292
|
+
return nil unless model
|
|
293
|
+
|
|
294
|
+
# Convert model to decision graph
|
|
295
|
+
graph = Dmn::DecisionGraph.new(id: model.id, name: model.name)
|
|
296
|
+
model.decisions.each do |decision|
|
|
297
|
+
node = Dmn::DecisionNode.new(
|
|
298
|
+
id: decision.id,
|
|
299
|
+
name: decision.name,
|
|
300
|
+
decision_logic: decision.decision_table || decision.decision_tree
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Add dependencies from information requirements
|
|
304
|
+
decision.information_requirements.each do |req|
|
|
305
|
+
node.add_dependency(req[:decision_id], req[:variable_name])
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
graph.add_decision(node)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
case format.to_s.downcase
|
|
312
|
+
when "svg"
|
|
313
|
+
Dmn::Visualizer.graph_to_svg(graph)
|
|
314
|
+
when "dot"
|
|
315
|
+
Dmn::Visualizer.graph_to_dot(graph)
|
|
316
|
+
when "mermaid"
|
|
317
|
+
Dmn::Visualizer.graph_to_mermaid(graph)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# List all models
|
|
322
|
+
def list_models
|
|
323
|
+
@storage_mutex.synchronize do
|
|
324
|
+
@storage.map do |id, model|
|
|
325
|
+
{
|
|
326
|
+
id: id,
|
|
327
|
+
name: model.name,
|
|
328
|
+
namespace: model.namespace,
|
|
329
|
+
decision_count: model.decisions.size
|
|
330
|
+
}
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
def generate_id
|
|
338
|
+
"dmn_#{Time.now.to_i}_#{rand(10_000)}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def store_model(model_id, model)
|
|
342
|
+
@storage_mutex.synchronize do
|
|
343
|
+
@storage[model_id] = model
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def retrieve_model(model_id)
|
|
348
|
+
@storage_mutex.synchronize do
|
|
349
|
+
@storage[model_id]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def update_decision_table(table, logic)
|
|
354
|
+
table.instance_variable_set(:@hit_policy, logic[:hit_policy]) if logic[:hit_policy]
|
|
355
|
+
table.instance_variable_set(:@inputs, logic[:inputs]) if logic[:inputs]
|
|
356
|
+
table.instance_variable_set(:@outputs, logic[:outputs]) if logic[:outputs]
|
|
357
|
+
table.instance_variable_set(:@rules, logic[:rules]) if logic[:rules]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def serialize_model(model)
|
|
361
|
+
{
|
|
362
|
+
id: model.id,
|
|
363
|
+
name: model.name,
|
|
364
|
+
namespace: model.namespace,
|
|
365
|
+
decisions: model.decisions.map { |d| serialize_decision(d) }
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def serialize_decision(decision)
|
|
370
|
+
result = {
|
|
371
|
+
id: decision.id,
|
|
372
|
+
name: decision.name
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if decision.decision_table
|
|
376
|
+
result[:decision_table] = serialize_decision_table(decision.decision_table)
|
|
377
|
+
elsif decision.decision_tree
|
|
378
|
+
result[:decision_tree] = decision.decision_tree.to_h
|
|
379
|
+
elsif decision.instance_variable_get(:@literal_expression)
|
|
380
|
+
result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
|
|
384
|
+
|
|
385
|
+
result
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def serialize_decision_table(table)
|
|
389
|
+
{
|
|
390
|
+
id: table.id,
|
|
391
|
+
hit_policy: table.hit_policy,
|
|
392
|
+
inputs: table.inputs.map { |i| serialize_input(i) },
|
|
393
|
+
outputs: table.outputs.map { |o| serialize_output(o) },
|
|
394
|
+
rules: table.rules.map { |r| serialize_rule(r) }
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def serialize_input(input)
|
|
399
|
+
{
|
|
400
|
+
id: input.id,
|
|
401
|
+
label: input.label,
|
|
402
|
+
type_ref: input.type_ref,
|
|
403
|
+
expression: input.expression
|
|
404
|
+
}
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def serialize_output(output)
|
|
408
|
+
{
|
|
409
|
+
id: output.id,
|
|
410
|
+
label: output.label,
|
|
411
|
+
type_ref: output.type_ref,
|
|
412
|
+
name: output.name
|
|
413
|
+
}
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def serialize_rule(rule)
|
|
417
|
+
{
|
|
418
|
+
id: rule.id,
|
|
419
|
+
input_entries: rule.input_entries,
|
|
420
|
+
output_entries: rule.output_entries,
|
|
421
|
+
description: rule.description
|
|
422
|
+
}
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
@@ -84,6 +84,7 @@ class RuleBuilder {
|
|
|
84
84
|
|
|
85
85
|
// Actions
|
|
86
86
|
document.getElementById('validateBtn').addEventListener('click', () => this.validateRules());
|
|
87
|
+
document.getElementById('testRuleBtn').addEventListener('click', () => this.openTestRuleModal());
|
|
87
88
|
document.getElementById('clearBtn').addEventListener('click', () => this.clearAll());
|
|
88
89
|
document.getElementById('loadExampleBtn').addEventListener('click', () => this.loadExample());
|
|
89
90
|
|
|
@@ -101,6 +102,11 @@ class RuleBuilder {
|
|
|
101
102
|
document.getElementById('closeCompareBtn').addEventListener('click', () => this.closeCompareModal());
|
|
102
103
|
document.getElementById('closeCompareModalBtn').addEventListener('click', () => this.closeCompareModal());
|
|
103
104
|
|
|
105
|
+
// Test Rule
|
|
106
|
+
document.getElementById('runTestBtn').addEventListener('click', () => this.runTest());
|
|
107
|
+
document.getElementById('closeTestRuleBtn').addEventListener('click', () => this.closeTestRuleModal());
|
|
108
|
+
document.getElementById('closeTestRuleModalBtn').addEventListener('click', () => this.closeTestRuleModal());
|
|
109
|
+
|
|
104
110
|
// Modal close on outside click
|
|
105
111
|
document.getElementById('ruleModal').addEventListener('click', (e) => {
|
|
106
112
|
if (e.target.id === 'ruleModal') {
|
|
@@ -1134,6 +1140,119 @@ class RuleBuilder {
|
|
|
1134
1140
|
document.getElementById('compareVersionsModal').classList.add('hidden');
|
|
1135
1141
|
}
|
|
1136
1142
|
|
|
1143
|
+
getRulesJSON() {
|
|
1144
|
+
const version = document.getElementById('rulesetVersion').value || '1.0';
|
|
1145
|
+
const ruleset = document.getElementById('rulesetName').value || 'my_ruleset';
|
|
1146
|
+
const rules = this.rules.map(rule => ({
|
|
1147
|
+
id: rule.id,
|
|
1148
|
+
if: rule.if,
|
|
1149
|
+
then: rule.then
|
|
1150
|
+
}));
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
version: version,
|
|
1154
|
+
ruleset: ruleset,
|
|
1155
|
+
rules: rules
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
openTestRuleModal() {
|
|
1160
|
+
document.getElementById('testRuleModal').classList.remove('hidden');
|
|
1161
|
+
document.getElementById('testContext').value = '{}';
|
|
1162
|
+
document.getElementById('testResults').classList.add('hidden');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
closeTestRuleModal() {
|
|
1166
|
+
document.getElementById('testRuleModal').classList.add('hidden');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async runTest() {
|
|
1170
|
+
const contextText = document.getElementById('testContext').value.trim();
|
|
1171
|
+
|
|
1172
|
+
// Get current rules
|
|
1173
|
+
const rules = this.getRulesJSON();
|
|
1174
|
+
|
|
1175
|
+
if (!rules || !rules.rules || rules.rules.length === 0) {
|
|
1176
|
+
alert('Please add at least one rule before testing');
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
let context;
|
|
1181
|
+
try {
|
|
1182
|
+
context = contextText ? JSON.parse(contextText) : {};
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
alert('Invalid JSON in context field: ' + e.message);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
try {
|
|
1189
|
+
const response = await fetch(`${this.basePath}api/evaluate`, {
|
|
1190
|
+
method: 'POST',
|
|
1191
|
+
headers: this.getAuthHeaders(),
|
|
1192
|
+
body: JSON.stringify({
|
|
1193
|
+
rules: rules,
|
|
1194
|
+
context: context
|
|
1195
|
+
})
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const data = await response.json();
|
|
1199
|
+
|
|
1200
|
+
if (!response.ok || !data.success) {
|
|
1201
|
+
alert('Test failed: ' + (data.error || 'Unknown error'));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Display results
|
|
1206
|
+
const resultsDiv = document.getElementById('testResults');
|
|
1207
|
+
resultsDiv.classList.remove('hidden');
|
|
1208
|
+
|
|
1209
|
+
if (data.decision) {
|
|
1210
|
+
document.getElementById('testDecisionValue').textContent = data.decision;
|
|
1211
|
+
document.getElementById('testConfidenceValue').textContent = (data.confidence || 0).toFixed(3);
|
|
1212
|
+
document.getElementById('testReasonValue').textContent = data.reason || 'N/A';
|
|
1213
|
+
|
|
1214
|
+
// Display explainability
|
|
1215
|
+
const becauseList = document.getElementById('testBecauseList');
|
|
1216
|
+
const failedList = document.getElementById('testFailedList');
|
|
1217
|
+
|
|
1218
|
+
if (data.because && data.because.length > 0) {
|
|
1219
|
+
becauseList.innerHTML = '';
|
|
1220
|
+
data.because.forEach(condition => {
|
|
1221
|
+
const li = document.createElement('li');
|
|
1222
|
+
li.textContent = condition;
|
|
1223
|
+
li.style.color = '#28a745';
|
|
1224
|
+
becauseList.appendChild(li);
|
|
1225
|
+
});
|
|
1226
|
+
document.getElementById('testBecause').style.display = 'block';
|
|
1227
|
+
} else {
|
|
1228
|
+
document.getElementById('testBecause').style.display = 'none';
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (data.failed_conditions && data.failed_conditions.length > 0) {
|
|
1232
|
+
failedList.innerHTML = '';
|
|
1233
|
+
data.failed_conditions.forEach(condition => {
|
|
1234
|
+
const li = document.createElement('li');
|
|
1235
|
+
li.textContent = condition;
|
|
1236
|
+
li.style.color = '#dc3545';
|
|
1237
|
+
failedList.appendChild(li);
|
|
1238
|
+
});
|
|
1239
|
+
document.getElementById('testFailedConditions').style.display = 'block';
|
|
1240
|
+
} else {
|
|
1241
|
+
document.getElementById('testFailedConditions').style.display = 'none';
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
document.getElementById('testExplainability').style.display = 'block';
|
|
1245
|
+
} else {
|
|
1246
|
+
document.getElementById('testDecisionValue').textContent = 'No match';
|
|
1247
|
+
document.getElementById('testConfidenceValue').textContent = 'N/A';
|
|
1248
|
+
document.getElementById('testReasonValue').textContent = data.message || 'No rules matched';
|
|
1249
|
+
document.getElementById('testExplainability').style.display = 'none';
|
|
1250
|
+
}
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
alert('Error running test: ' + error.message);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1137
1256
|
async deleteVersion(versionId) {
|
|
1138
1257
|
if (!confirm('Are you sure you want to delete this version? This action cannot be undone.')) {
|
|
1139
1258
|
return;
|