decision_agent 0.3.0 → 1.1.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 +234 -14
- data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
- data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
- data/lib/decision_agent/agent.rb +78 -9
- data/lib/decision_agent/audit/adapter.rb +2 -0
- data/lib/decision_agent/audit/logger_adapter.rb +2 -0
- data/lib/decision_agent/audit/null_adapter.rb +2 -0
- data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
- data/lib/decision_agent/auth/authenticator.rb +2 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
- data/lib/decision_agent/auth/password_reset_token.rb +2 -0
- data/lib/decision_agent/auth/permission.rb +2 -0
- data/lib/decision_agent/auth/permission_checker.rb +2 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
- data/lib/decision_agent/auth/rbac_config.rb +2 -0
- data/lib/decision_agent/auth/role.rb +2 -0
- data/lib/decision_agent/auth/session.rb +2 -0
- data/lib/decision_agent/auth/session_manager.rb +2 -0
- data/lib/decision_agent/auth/user.rb +2 -0
- data/lib/decision_agent/context.rb +14 -0
- data/lib/decision_agent/decision.rb +113 -4
- data/lib/decision_agent/dmn/adapter.rb +2 -0
- data/lib/decision_agent/dmn/cache.rb +2 -2
- data/lib/decision_agent/dmn/decision_graph.rb +7 -7
- data/lib/decision_agent/dmn/decision_tree.rb +16 -8
- data/lib/decision_agent/dmn/errors.rb +2 -0
- data/lib/decision_agent/dmn/exporter.rb +2 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
- data/lib/decision_agent/dmn/feel/functions.rb +2 -0
- data/lib/decision_agent/dmn/feel/parser.rb +2 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
- data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
- data/lib/decision_agent/dmn/feel/types.rb +2 -0
- data/lib/decision_agent/dmn/importer.rb +2 -0
- data/lib/decision_agent/dmn/model.rb +2 -4
- data/lib/decision_agent/dmn/parser.rb +2 -0
- data/lib/decision_agent/dmn/testing.rb +3 -2
- data/lib/decision_agent/dmn/validator.rb +5 -3
- data/lib/decision_agent/dmn/visualizer.rb +7 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
- data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
- data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
- data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
- data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
- data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
- data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
- data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
- data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
- data/lib/decision_agent/dsl/operators/base.rb +70 -0
- data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
- data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
- data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
- data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
- data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
- data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
- data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
- data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
- data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
- data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
- data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
- data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
- data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
- data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
- data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
- data/lib/decision_agent/dsl/rule_parser.rb +2 -0
- data/lib/decision_agent/dsl/schema_validator.rb +37 -14
- data/lib/decision_agent/errors.rb +2 -0
- data/lib/decision_agent/evaluation.rb +14 -2
- data/lib/decision_agent/evaluators/base.rb +2 -0
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
- data/lib/decision_agent/explainability/condition_trace.rb +85 -0
- data/lib/decision_agent/explainability/explainability_result.rb +50 -0
- data/lib/decision_agent/explainability/rule_trace.rb +41 -0
- data/lib/decision_agent/explainability/trace_collector.rb +26 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
- data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
- data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
- data/lib/decision_agent/replay/replay.rb +4 -1
- data/lib/decision_agent/scoring/base.rb +2 -0
- data/lib/decision_agent/scoring/consensus.rb +2 -0
- data/lib/decision_agent/scoring/max_weight.rb +2 -0
- data/lib/decision_agent/scoring/threshold.rb +2 -0
- data/lib/decision_agent/scoring/weighted_average.rb +2 -0
- data/lib/decision_agent/simulation/errors.rb +20 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
- data/lib/decision_agent/simulation/replay_engine.rb +488 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
- data/lib/decision_agent/simulation/scenario_library.rb +165 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
- data/lib/decision_agent/simulation.rb +19 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
- data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
- data/lib/decision_agent/testing/test_scenario.rb +2 -0
- data/lib/decision_agent/version.rb +3 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
- data/lib/decision_agent/versioning/adapter.rb +9 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
- data/lib/decision_agent/versioning/version_manager.rb +9 -0
- data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
- data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
- data/lib/decision_agent/web/dmn_editor.rb +8 -67
- data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
- data/lib/decision_agent/web/public/app.js +186 -26
- data/lib/decision_agent/web/public/batch_testing.html +80 -6
- data/lib/decision_agent/web/public/dmn-editor.html +2 -2
- data/lib/decision_agent/web/public/dmn-editor.js +74 -8
- data/lib/decision_agent/web/public/index.html +69 -3
- data/lib/decision_agent/web/public/login.html +1 -1
- data/lib/decision_agent/web/public/sample_batch.csv +11 -0
- data/lib/decision_agent/web/public/sample_impact.csv +11 -0
- data/lib/decision_agent/web/public/sample_replay.csv +11 -0
- data/lib/decision_agent/web/public/sample_rules.json +118 -0
- data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
- data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
- data/lib/decision_agent/web/public/simulation.html +146 -0
- data/lib/decision_agent/web/public/simulation_impact.html +495 -0
- data/lib/decision_agent/web/public/simulation_replay.html +547 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/public/users.html +1 -1
- data/lib/decision_agent/web/rack_helpers.rb +106 -0
- data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
- data/lib/decision_agent/web/server.rb +2126 -1374
- data/lib/decision_agent.rb +19 -1
- data/lib/generators/decision_agent/install/install_generator.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
- metadata +103 -89
- 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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Simulation and What-If Analysis module
|
|
4
|
+
# Provides tools for scenario testing, historical replay, impact analysis, shadow testing, and Monte Carlo simulation
|
|
5
|
+
|
|
6
|
+
require_relative "simulation/errors"
|
|
7
|
+
require_relative "simulation/replay_engine"
|
|
8
|
+
require_relative "simulation/what_if_analyzer"
|
|
9
|
+
require_relative "simulation/impact_analyzer"
|
|
10
|
+
require_relative "simulation/shadow_test_engine"
|
|
11
|
+
require_relative "simulation/scenario_engine"
|
|
12
|
+
require_relative "simulation/scenario_library"
|
|
13
|
+
require_relative "simulation/monte_carlo_simulator"
|
|
14
|
+
|
|
15
|
+
module DecisionAgent
|
|
16
|
+
module Simulation
|
|
17
|
+
# Main entry point for simulation features
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "csv"
|
|
2
4
|
require "roo"
|
|
3
5
|
|
|
@@ -44,8 +46,9 @@ module DecisionAgent
|
|
|
44
46
|
if options[:progress_callback]
|
|
45
47
|
begin
|
|
46
48
|
total_rows = count_csv_rows(file_path, options[:skip_header])
|
|
47
|
-
rescue StandardError
|
|
49
|
+
rescue StandardError => e
|
|
48
50
|
# If counting fails, continue without progress tracking
|
|
51
|
+
warn "[DecisionAgent] Failed to count CSV rows: #{e.message}"
|
|
49
52
|
total_rows = nil
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -364,8 +367,9 @@ module DecisionAgent
|
|
|
364
367
|
count += 1
|
|
365
368
|
end
|
|
366
369
|
count
|
|
367
|
-
rescue StandardError
|
|
370
|
+
rescue StandardError => e
|
|
368
371
|
# If we can't count, return nil (progress tracking will be disabled)
|
|
372
|
+
warn "[DecisionAgent] Failed to count CSV rows for '#{file_path}': #{e.message}"
|
|
369
373
|
nil
|
|
370
374
|
end
|
|
371
375
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "json"
|
|
2
4
|
|
|
3
5
|
module DecisionAgent
|
|
@@ -153,7 +155,7 @@ module DecisionAgent
|
|
|
153
155
|
loop do
|
|
154
156
|
scenario = begin
|
|
155
157
|
queue.pop(true)
|
|
156
|
-
rescue
|
|
158
|
+
rescue ThreadError
|
|
157
159
|
nil
|
|
158
160
|
end
|
|
159
161
|
break unless scenario
|
|
@@ -228,7 +230,8 @@ module DecisionAgent
|
|
|
228
230
|
data
|
|
229
231
|
rescue JSON::ParserError
|
|
230
232
|
{ completed_scenario_ids: [], last_updated: nil }
|
|
231
|
-
rescue StandardError
|
|
233
|
+
rescue StandardError => e
|
|
234
|
+
warn "[DecisionAgent] Failed to load checkpoint file: #{e.message}"
|
|
232
235
|
{ completed_scenario_ids: [], last_updated: nil }
|
|
233
236
|
end
|
|
234
237
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DecisionAgent
|
|
2
4
|
# Semantic version: MAJOR.MINOR.PATCH
|
|
3
5
|
# MAJOR: Incremented for incompatible API changes
|
|
4
6
|
# MINOR: Incremented for backward-compatible functionality additions
|
|
5
7
|
# PATCH: Incremented for backward-compatible bug fixes
|
|
6
|
-
VERSION = "
|
|
8
|
+
VERSION = "1.1.0"
|
|
7
9
|
|
|
8
10
|
# Validate version format (semantic versioning)
|
|
9
11
|
unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "adapter"
|
|
2
4
|
require_relative "file_storage_adapter"
|
|
3
5
|
|
|
@@ -16,40 +18,43 @@ module DecisionAgent
|
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def create_version(rule_id:, content:, metadata: {})
|
|
19
|
-
# Use a transaction with pessimistic locking to prevent race conditions
|
|
20
|
-
version = nil
|
|
21
|
-
|
|
22
21
|
# Validate status if provided
|
|
23
22
|
status = metadata[:status] || "active"
|
|
24
23
|
validate_status!(status)
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
# Retry on SQLite busy exceptions (common with concurrent operations)
|
|
26
|
+
retry_with_backoff(max_retries: 10) do
|
|
27
|
+
# Use a transaction with pessimistic locking to prevent race conditions
|
|
28
|
+
version = nil
|
|
29
|
+
|
|
30
|
+
rule_version_class.transaction do
|
|
31
|
+
# Lock the last version for this rule to prevent concurrent reads
|
|
32
|
+
# This ensures only one thread can calculate the next version number at a time
|
|
33
|
+
last_version = rule_version_class.where(rule_id: rule_id)
|
|
34
|
+
.order(version_number: :desc)
|
|
35
|
+
.lock
|
|
36
|
+
.first
|
|
37
|
+
next_version_number = last_version ? last_version.version_number + 1 : 1
|
|
38
|
+
|
|
39
|
+
# Deactivate previous active versions
|
|
40
|
+
# Use update_all for better concurrency (avoids SQLite locking issues)
|
|
41
|
+
# Status "archived" is valid, so no need to trigger validations
|
|
42
|
+
rule_version_class.where(rule_id: rule_id, status: "active")
|
|
43
|
+
.update_all(status: "archived")
|
|
44
|
+
|
|
45
|
+
# Create new version
|
|
46
|
+
version = rule_version_class.create!(
|
|
47
|
+
rule_id: rule_id,
|
|
48
|
+
version_number: next_version_number,
|
|
49
|
+
content: content.to_json,
|
|
50
|
+
created_by: metadata[:created_by] || "system",
|
|
51
|
+
changelog: metadata[:changelog] || "Version #{next_version_number}",
|
|
52
|
+
status: status
|
|
53
|
+
)
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
version = rule_version_class.create!(
|
|
43
|
-
rule_id: rule_id,
|
|
44
|
-
version_number: next_version_number,
|
|
45
|
-
content: content.to_json,
|
|
46
|
-
created_by: metadata[:created_by] || "system",
|
|
47
|
-
changelog: metadata[:changelog] || "Version #{next_version_number}",
|
|
48
|
-
status: status
|
|
49
|
-
)
|
|
56
|
+
serialize_version(version)
|
|
50
57
|
end
|
|
51
|
-
|
|
52
|
-
serialize_version(version)
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
def list_versions(rule_id:, limit: nil)
|
|
@@ -60,6 +65,13 @@ module DecisionAgent
|
|
|
60
65
|
query.map { |v| serialize_version(v) }
|
|
61
66
|
end
|
|
62
67
|
|
|
68
|
+
def list_all_versions(limit: nil)
|
|
69
|
+
query = rule_version_class.order(created_at: :desc)
|
|
70
|
+
query = query.limit(limit) if limit
|
|
71
|
+
|
|
72
|
+
query.map { |v| serialize_version(v) }
|
|
73
|
+
end
|
|
74
|
+
|
|
63
75
|
def get_version(version_id:)
|
|
64
76
|
version = rule_version_class.find_by(id: version_id)
|
|
65
77
|
version ? serialize_version(version) : nil
|
|
@@ -79,26 +91,44 @@ module DecisionAgent
|
|
|
79
91
|
end
|
|
80
92
|
|
|
81
93
|
def activate_version(version_id:)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
# Retry on SQLite busy exceptions (common with concurrent operations)
|
|
95
|
+
retry_with_backoff(max_retries: 10) do
|
|
96
|
+
version = nil
|
|
97
|
+
|
|
98
|
+
rule_version_class.transaction do
|
|
99
|
+
# Find and lock the version to activate
|
|
100
|
+
version = rule_version_class.lock.find(version_id)
|
|
101
|
+
|
|
102
|
+
# Deactivate all other versions for this rule within the same transaction
|
|
103
|
+
# The lock ensures only one thread can perform this operation at a time
|
|
104
|
+
# Use update_all for better concurrency (avoids SQLite locking issues)
|
|
105
|
+
# Status "archived" is valid, so no need to trigger validations
|
|
106
|
+
rule_version_class.where(rule_id: version.rule_id, status: "active")
|
|
107
|
+
.where.not(id: version_id)
|
|
108
|
+
.update_all(status: "archived")
|
|
109
|
+
|
|
110
|
+
# Activate this version
|
|
111
|
+
version.update!(status: "active")
|
|
95
112
|
end
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
version.update!(status: "active")
|
|
114
|
+
serialize_version(version)
|
|
99
115
|
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def delete_version(version_id:)
|
|
119
|
+
version = rule_version_class.find_by(id: version_id)
|
|
120
|
+
|
|
121
|
+
# Version not found
|
|
122
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
|
|
100
123
|
|
|
101
|
-
|
|
124
|
+
# Prevent deletion of active versions
|
|
125
|
+
raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first." if version.status == "active"
|
|
126
|
+
|
|
127
|
+
# Delete the version
|
|
128
|
+
version.destroy
|
|
129
|
+
true
|
|
130
|
+
rescue ActiveRecord::RecordNotFound
|
|
131
|
+
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
102
132
|
end
|
|
103
133
|
|
|
104
134
|
private
|
|
@@ -113,6 +143,41 @@ module DecisionAgent
|
|
|
113
143
|
end
|
|
114
144
|
end
|
|
115
145
|
|
|
146
|
+
# Retry database operations that may encounter SQLite busy exceptions
|
|
147
|
+
# This is especially important for concurrent operations on different rules
|
|
148
|
+
def retry_with_backoff(max_retries: 10, base_delay: 0.01)
|
|
149
|
+
retries = 0
|
|
150
|
+
begin
|
|
151
|
+
yield
|
|
152
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
153
|
+
# Check if it's a SQLite busy exception
|
|
154
|
+
# Handle different SQLite adapter implementations
|
|
155
|
+
is_busy = begin
|
|
156
|
+
# Check the underlying exception type
|
|
157
|
+
cause = e.cause
|
|
158
|
+
if cause
|
|
159
|
+
cause.class.name.include?("BusyException") ||
|
|
160
|
+
cause.class.name.include?("SQLite3::BusyException")
|
|
161
|
+
else
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
rescue StandardError => cause_check_error
|
|
165
|
+
warn "[DecisionAgent] Error checking busy exception cause: #{cause_check_error.message}"
|
|
166
|
+
false
|
|
167
|
+
end || e.message.include?("database is locked") ||
|
|
168
|
+
e.message.include?("SQLite3::BusyException") ||
|
|
169
|
+
e.message.include?("BusyException")
|
|
170
|
+
|
|
171
|
+
raise unless is_busy && retries < max_retries
|
|
172
|
+
|
|
173
|
+
retries += 1
|
|
174
|
+
# Exponential backoff with jitter
|
|
175
|
+
delay = (base_delay * (2**retries)) + (rand * base_delay)
|
|
176
|
+
sleep(delay)
|
|
177
|
+
retry
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
116
181
|
def serialize_version(version)
|
|
117
182
|
# Parse JSON content with proper error handling
|
|
118
183
|
parsed_content = begin
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DecisionAgent
|
|
2
4
|
module Versioning
|
|
3
5
|
# Abstract base class for version storage adapters
|
|
@@ -20,6 +22,13 @@ module DecisionAgent
|
|
|
20
22
|
raise NotImplementedError, "#{self.class} must implement #list_versions"
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# List all versions across all rules
|
|
26
|
+
# @param limit [Integer, nil] Optional limit for number of versions
|
|
27
|
+
# @return [Array<Hash>] Array of version hashes
|
|
28
|
+
def list_all_versions(limit: nil)
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #list_all_versions"
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
# Get a specific version by ID
|
|
24
33
|
# @param version_id [String, Integer] The version identifier
|
|
25
34
|
# @return [Hash, nil] The version hash or nil if not found
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "adapter"
|
|
2
4
|
require "json"
|
|
3
5
|
require "fileutils"
|
|
@@ -86,12 +88,20 @@ module DecisionAgent
|
|
|
86
88
|
end
|
|
87
89
|
end
|
|
88
90
|
|
|
91
|
+
def list_all_versions(limit: nil)
|
|
92
|
+
@version_index_lock.synchronize do
|
|
93
|
+
versions = all_versions_unsafe
|
|
94
|
+
limit ? versions.take(limit) : versions
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
89
98
|
def get_version(version_id:)
|
|
90
99
|
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
91
100
|
begin
|
|
92
101
|
rule_id = get_rule_id_from_index(version_id)
|
|
93
|
-
rescue StandardError
|
|
102
|
+
rescue StandardError => e
|
|
94
103
|
# If index lookup fails, version doesn't exist
|
|
104
|
+
warn "[DecisionAgent] Version index lookup failed for '#{version_id}': #{e.message}"
|
|
95
105
|
return nil
|
|
96
106
|
end
|
|
97
107
|
return nil unless rule_id
|
|
@@ -103,8 +113,9 @@ module DecisionAgent
|
|
|
103
113
|
versions = list_versions_unsafe(rule_id: rule_id)
|
|
104
114
|
versions.find { |v| v[:id] == version_id }
|
|
105
115
|
end
|
|
106
|
-
rescue StandardError
|
|
116
|
+
rescue StandardError => e
|
|
107
117
|
# If any error occurs during lookup, treat as version not found
|
|
118
|
+
warn "[DecisionAgent] Version lookup failed for '#{version_id}': #{e.message}"
|
|
108
119
|
nil
|
|
109
120
|
end
|
|
110
121
|
end
|
|
@@ -152,8 +163,9 @@ module DecisionAgent
|
|
|
152
163
|
# Use index to find rule_id quickly - O(1) instead of O(n)
|
|
153
164
|
begin
|
|
154
165
|
rule_id = get_rule_id_from_index(version_id)
|
|
155
|
-
rescue StandardError
|
|
166
|
+
rescue StandardError => e
|
|
156
167
|
# If index lookup fails, version doesn't exist
|
|
168
|
+
warn "[DecisionAgent] Version index lookup failed for '#{version_id}': #{e.message}"
|
|
157
169
|
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
158
170
|
end
|
|
159
171
|
|
|
@@ -218,15 +230,16 @@ module DecisionAgent
|
|
|
218
230
|
rescue DecisionAgent::ValidationError, DecisionAgent::NotFoundError
|
|
219
231
|
# Re-raise expected errors
|
|
220
232
|
raise
|
|
221
|
-
rescue StandardError
|
|
233
|
+
rescue StandardError => e
|
|
222
234
|
# If any unexpected error occurs during the lock operation, treat as version not found
|
|
223
235
|
# This prevents 500 errors from propagating when version doesn't exist or is in an invalid state
|
|
224
236
|
# This handles ThreadError (deadlocks, recursive locks), SystemCallError (file system issues), etc.
|
|
225
237
|
# This is safe because if the version existed and was valid, we would have found it above
|
|
238
|
+
warn "[DecisionAgent] Version delete lock operation failed for '#{version_id}': #{e.message}"
|
|
226
239
|
begin
|
|
227
240
|
remove_from_index(version_id)
|
|
228
|
-
rescue StandardError
|
|
229
|
-
|
|
241
|
+
rescue StandardError => cleanup_error
|
|
242
|
+
warn "[DecisionAgent] Failed to clean up index for '#{version_id}': #{cleanup_error.message}"
|
|
230
243
|
end
|
|
231
244
|
raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
|
|
232
245
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DecisionAgent
|
|
2
4
|
module Versioning
|
|
3
5
|
# High-level service for managing rule versions
|
|
@@ -40,6 +42,13 @@ module DecisionAgent
|
|
|
40
42
|
@adapter.list_versions(rule_id: rule_id, limit: limit)
|
|
41
43
|
end
|
|
42
44
|
|
|
45
|
+
# Get all versions across all rules
|
|
46
|
+
# @param limit [Integer, nil] Optional limit
|
|
47
|
+
# @return [Array<Hash>] Array of versions
|
|
48
|
+
def list_all_versions(limit: nil)
|
|
49
|
+
@adapter.list_all_versions(limit: limit)
|
|
50
|
+
end
|
|
51
|
+
|
|
43
52
|
# Get a specific version
|
|
44
53
|
# @param version_id [String, Integer] The version identifier
|
|
45
54
|
# @return [Hash, nil] The version or nil
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Web
|
|
5
|
+
class DmnEditor
|
|
6
|
+
module Serialization
|
|
7
|
+
def serialize_model(model)
|
|
8
|
+
{
|
|
9
|
+
id: model.id,
|
|
10
|
+
name: model.name,
|
|
11
|
+
namespace: model.namespace,
|
|
12
|
+
decisions: model.decisions.map { |d| serialize_decision(d) }
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serialize_decision(decision)
|
|
17
|
+
result = {
|
|
18
|
+
id: decision.id,
|
|
19
|
+
name: decision.name
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if decision.decision_table
|
|
23
|
+
result[:decision_table] = serialize_decision_table(decision.decision_table)
|
|
24
|
+
elsif decision.decision_tree
|
|
25
|
+
result[:decision_tree] = decision.decision_tree.to_h
|
|
26
|
+
elsif decision.instance_variable_get(:@literal_expression)
|
|
27
|
+
result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
|
|
31
|
+
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize_decision_table(table)
|
|
36
|
+
{
|
|
37
|
+
id: table.id,
|
|
38
|
+
hit_policy: table.hit_policy,
|
|
39
|
+
inputs: table.inputs.map { |i| serialize_input(i) },
|
|
40
|
+
outputs: table.outputs.map { |o| serialize_output(o) },
|
|
41
|
+
rules: table.rules.map { |r| serialize_rule(r) }
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def serialize_input(input)
|
|
46
|
+
{
|
|
47
|
+
id: input.id,
|
|
48
|
+
label: input.label,
|
|
49
|
+
type_ref: input.type_ref,
|
|
50
|
+
expression: input.expression
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serialize_output(output)
|
|
55
|
+
{
|
|
56
|
+
id: output.id,
|
|
57
|
+
label: output.label,
|
|
58
|
+
type_ref: output.type_ref,
|
|
59
|
+
name: output.name
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def serialize_rule(rule)
|
|
64
|
+
{
|
|
65
|
+
id: rule.id,
|
|
66
|
+
input_entries: rule.input_entries,
|
|
67
|
+
output_entries: rule.output_entries,
|
|
68
|
+
description: rule.description
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Web
|
|
5
|
+
class DmnEditor
|
|
6
|
+
module XmlBuilder
|
|
7
|
+
def generate_dmn_xml(model)
|
|
8
|
+
require "nokogiri"
|
|
9
|
+
|
|
10
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
11
|
+
xml.definitions(
|
|
12
|
+
"xmlns" => "https://www.omg.org/spec/DMN/20191111/MODEL/",
|
|
13
|
+
"xmlns:dmndi" => "https://www.omg.org/spec/DMN/20191111/DMNDI/",
|
|
14
|
+
"xmlns:dc" => "http://www.omg.org/spec/DMN/20180521/DC/",
|
|
15
|
+
"id" => "definitions_#{model.id}",
|
|
16
|
+
"name" => model.name,
|
|
17
|
+
"namespace" => model.namespace || "http://decision_agent.local"
|
|
18
|
+
) do
|
|
19
|
+
model.decisions.each do |decision|
|
|
20
|
+
xml.decision(id: decision.id, name: decision.name) do
|
|
21
|
+
if decision.decision_table
|
|
22
|
+
build_decision_table_xml(xml, decision.decision_table)
|
|
23
|
+
elsif decision.decision_tree
|
|
24
|
+
xml.comment "Decision Tree (not fully supported in DMN XML export yet)"
|
|
25
|
+
elsif decision.instance_variable_get(:@literal_expression)
|
|
26
|
+
xml.literalExpression do
|
|
27
|
+
xml.text decision.instance_variable_get(:@literal_expression)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
builder.to_xml
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_decision_table_xml(xml, table)
|
|
39
|
+
xml.decisionTable(
|
|
40
|
+
id: table.id,
|
|
41
|
+
hitPolicy: table.hit_policy || "FIRST",
|
|
42
|
+
outputLabel: "output"
|
|
43
|
+
) do
|
|
44
|
+
build_inputs_xml(xml, table)
|
|
45
|
+
build_outputs_xml(xml, table)
|
|
46
|
+
build_rules_xml(xml, table)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_inputs_xml(xml, table)
|
|
53
|
+
table.inputs.each do |input|
|
|
54
|
+
xml.input(id: input.id, label: input.label) do
|
|
55
|
+
xml.inputExpression(typeRef: input.type_ref || "string") do
|
|
56
|
+
text_node = Nokogiri::XML::Node.new("text", xml.doc)
|
|
57
|
+
text_node.content = input.expression || input.label
|
|
58
|
+
xml.parent.add_child(text_node)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_outputs_xml(xml, table)
|
|
65
|
+
table.outputs.each do |output|
|
|
66
|
+
xml.output(
|
|
67
|
+
id: output.id,
|
|
68
|
+
label: output.label,
|
|
69
|
+
name: output.name || output.label,
|
|
70
|
+
typeRef: output.type_ref || "string"
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_rules_xml(xml, table)
|
|
76
|
+
table.rules.each { |rule| build_rule_xml(xml, rule) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_rule_xml(xml, rule)
|
|
80
|
+
xml.rule(id: rule.id) do
|
|
81
|
+
rule.input_entries.each_with_index do |entry, idx|
|
|
82
|
+
add_entry_element(xml, "inputEntry", "#{rule.id}_input_#{idx + 1}", entry)
|
|
83
|
+
end
|
|
84
|
+
rule.output_entries.each_with_index do |entry, idx|
|
|
85
|
+
add_entry_element(xml, "outputEntry", "#{rule.id}_output_#{idx + 1}", entry)
|
|
86
|
+
end
|
|
87
|
+
add_rule_description(xml, rule)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_entry_element(xml, tag, id, content)
|
|
92
|
+
xml.send(tag, id: id) do
|
|
93
|
+
text_node = Nokogiri::XML::Node.new("text", xml.doc)
|
|
94
|
+
text_node.content = content.to_s
|
|
95
|
+
xml.parent.add_child(text_node)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_rule_description(xml, rule)
|
|
100
|
+
return if rule.description.nil? || rule.description.empty?
|
|
101
|
+
|
|
102
|
+
xml.description { xml.text rule.description }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|