decision_agent 0.3.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 +272 -7
- 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/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- 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/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -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 +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a016bb964d8daeb5676d84ba597f07af9e7b4816a8f075d3ca9feb6f1ddd2f44
|
|
4
|
+
data.tar.gz: ccc960d23a5c863b8e9be429b08188f32abe630d83e2008a3e70534401779d92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f84301ecb298b3fdc3d39b2ca02e850345d2df13eb0c8834555f1b3f08241f243dee54fd044dfd0a37bd2fa32e690dca7583c69049af68d0e951572ca222ad0
|
|
7
|
+
data.tar.gz: a52598d8c6358ea03ed16a3fd23d68fbea1ef9619f1c4dec613359427a11e9290cd6f4b9f8816fb6673d436b59bdd23b986f816a23ecac185eae1e27b86e6dee
|
data/README.md
CHANGED
|
@@ -14,8 +14,8 @@ A production-grade, deterministic, explainable, and auditable decision engine fo
|
|
|
14
14
|
DecisionAgent is designed for applications that require **deterministic, explainable, and auditable** decision-making:
|
|
15
15
|
|
|
16
16
|
- ✅ **Deterministic** - Same input always produces same output
|
|
17
|
-
- ✅ **Explainable** - Every decision includes human-readable reasoning
|
|
18
|
-
- ✅ **Auditable** - Reproduce any historical decision exactly
|
|
17
|
+
- ✅ **Explainable** - Every decision includes human-readable reasoning and machine-readable condition traces
|
|
18
|
+
- ✅ **Auditable** - Reproduce any historical decision exactly with complete explainability
|
|
19
19
|
- ✅ **Framework-agnostic** - Pure Ruby, works anywhere
|
|
20
20
|
- ✅ **Production-ready** - Comprehensive testing ([Coverage Report](coverage.md)), error handling, and versioning
|
|
21
21
|
|
|
@@ -55,9 +55,12 @@ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
|
55
55
|
# Make decision
|
|
56
56
|
result = agent.decide(context: { amount: 1500 })
|
|
57
57
|
|
|
58
|
-
puts result.decision
|
|
59
|
-
puts result.confidence
|
|
60
|
-
puts result.explanations
|
|
58
|
+
puts result.decision # => "approve"
|
|
59
|
+
puts result.confidence # => 0.9
|
|
60
|
+
puts result.explanations # => ["High value transaction"]
|
|
61
|
+
puts result.because # => ["amount > 1000"]
|
|
62
|
+
puts result.failed_conditions # => []
|
|
63
|
+
puts result.explainability # => { decision: "approve", because: [...], failed_conditions: [] }
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
|
|
@@ -69,26 +72,52 @@ See [Code Examples](docs/CODE_EXAMPLES.md) for more comprehensive examples.
|
|
|
69
72
|
- **Conflict Resolution** - Weighted average, consensus, threshold, max weight
|
|
70
73
|
- **Rich Context** - Nested data, dot notation, flexible operators
|
|
71
74
|
- **Advanced Operators** - String, numeric, date/time, collection, and geospatial operators
|
|
75
|
+
- **REST API Data Enrichment** - Fetch external data during decision-making with caching and circuit breaker
|
|
72
76
|
|
|
73
77
|
### Auditability & Compliance
|
|
74
78
|
- **Complete Audit Trails** - Every decision fully logged
|
|
79
|
+
- **Explainability Layer** - Machine-readable condition traces for every decision
|
|
80
|
+
- `result.because` - Conditions that led to the decision
|
|
81
|
+
- `result.failed_conditions` - Conditions that failed
|
|
82
|
+
- `result.explainability` - Complete machine-readable explainability data
|
|
75
83
|
- **Deterministic Replay** - Reproduce historical decisions exactly
|
|
76
84
|
- **RFC 8785 Canonical JSON** - Industry-standard deterministic hashing
|
|
77
85
|
- **Compliance Ready** - HIPAA, SOX, regulatory compliance support
|
|
78
86
|
|
|
87
|
+
### Testing & Simulation
|
|
88
|
+
- **Simulation & What-If Analysis** - Test rule changes before deployment
|
|
89
|
+
- **Historical Replay / Backtesting** - Replay past decisions with new rules (CSV, JSON, database import)
|
|
90
|
+
- **What-If Analysis** - Simulate scenarios and sensitivity analysis with decision boundary visualization
|
|
91
|
+
- **Impact Analysis** - Quantify rule change effects (decision distribution, confidence shifts, performance impact)
|
|
92
|
+
- **Shadow Testing** - Compare new rules against production without affecting outcomes
|
|
93
|
+
- **Monte Carlo Simulation** - Model probabilistic inputs and understand decision outcome probabilities
|
|
94
|
+
- **Batch Testing** - Test rules against large datasets with CSV/Excel import, coverage analysis, and resume capability
|
|
95
|
+
- **A/B Testing** - Champion/Challenger testing with statistical significance analysis
|
|
96
|
+
|
|
97
|
+
### Security & Access Control
|
|
98
|
+
- **Role-Based Access Control (RBAC)** - Enterprise-grade authentication and authorization
|
|
99
|
+
- Built-in user/role system with bcrypt password hashing
|
|
100
|
+
- Configurable adapters for Devise, CanCanCan, Pundit, or custom auth systems
|
|
101
|
+
- 5 default roles (Admin, Editor, Viewer, Auditor, Approver) with 7 permissions
|
|
102
|
+
- Password reset functionality with secure token management
|
|
103
|
+
- Comprehensive access audit logging for compliance
|
|
104
|
+
- Web UI integration with login and user management pages
|
|
105
|
+
|
|
79
106
|
### Developer Experience
|
|
80
107
|
- **Pluggable Architecture** - Custom evaluators, scoring, audit adapters
|
|
81
108
|
- **Framework Agnostic** - Works with Rails, Sinatra, or standalone
|
|
82
109
|
- **JSON Rule DSL** - Non-technical users can write rules
|
|
83
110
|
- **DMN 1.3 Support** - Industry-standard Decision Model and Notation with full FEEL expression language
|
|
84
111
|
- **Visual Rule Builder** - Web UI for rule management and DMN modeler
|
|
112
|
+
- **CLI Tools** - Command-line interface for DMN import/export and web server
|
|
85
113
|
|
|
86
114
|
### Production Features
|
|
87
115
|
- **Real-time Monitoring** - Live dashboard with WebSocket updates
|
|
116
|
+
- **Persistent Monitoring** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
88
117
|
- **Prometheus Export** - Industry-standard metrics format
|
|
89
118
|
- **Intelligent Alerting** - Anomaly detection with customizable rules
|
|
90
119
|
- **Grafana Integration** - Pre-built dashboards and alert rules
|
|
91
|
-
- **Version Control** - Full rule version control and
|
|
120
|
+
- **Version Control** - Full rule version control, rollback, and history ([Versioning Guide](docs/VERSIONING.md))
|
|
92
121
|
- **Thread-Safe** - Safe for multi-threaded servers and background jobs
|
|
93
122
|
- **High Performance** - 10,000+ decisions/second, ~0.1ms latency
|
|
94
123
|
|
|
@@ -156,6 +185,57 @@ result = agent.decide(context: { amount: 50000, credit_score: 750 })
|
|
|
156
185
|
|
|
157
186
|
See [DMN Guide](docs/DMN_GUIDE.md) for complete documentation and [DMN Examples](examples/dmn/README.md) for working examples.
|
|
158
187
|
|
|
188
|
+
## REST API Data Enrichment
|
|
189
|
+
|
|
190
|
+
DecisionAgent supports fetching external data during decision-making without manual context assembly:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
require 'decision_agent'
|
|
194
|
+
|
|
195
|
+
# Configure data enrichment endpoints
|
|
196
|
+
DecisionAgent.configure_data_enrichment do |config|
|
|
197
|
+
config.add_endpoint(:credit_bureau,
|
|
198
|
+
url: "https://api.creditbureau.com/v1/score",
|
|
199
|
+
method: :post,
|
|
200
|
+
auth: { type: :api_key, header: "X-API-Key" },
|
|
201
|
+
cache: { ttl: 3600, adapter: :memory }
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Use in rules with fetch_from_api operator
|
|
206
|
+
rules = {
|
|
207
|
+
version: "1.0",
|
|
208
|
+
ruleset: "loan_approval",
|
|
209
|
+
rules: [{
|
|
210
|
+
id: "check_credit",
|
|
211
|
+
if: {
|
|
212
|
+
field: "credit_score",
|
|
213
|
+
op: "fetch_from_api",
|
|
214
|
+
value: {
|
|
215
|
+
endpoint: "credit_bureau",
|
|
216
|
+
params: { ssn: "{{customer.ssn}}" },
|
|
217
|
+
mapping: { score: "credit_score" }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
then: { decision: "approve", weight: 0.8 }
|
|
221
|
+
}]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
|
|
225
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
226
|
+
result = agent.decide(context: { customer: { ssn: "123-45-6789" } })
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Features:**
|
|
230
|
+
- **HTTP Client** - Support for GET, POST, PUT, DELETE methods
|
|
231
|
+
- **Response Caching** - Configurable TTL per endpoint with memory adapter
|
|
232
|
+
- **Circuit Breaker** - Fail-fast after N failures to prevent cascading failures
|
|
233
|
+
- **Authentication** - API key, Basic Auth, and Bearer token support
|
|
234
|
+
- **Template Parameters** - Use `{{path}}` syntax to reference context values
|
|
235
|
+
- **Error Handling** - Graceful degradation with cached data fallback
|
|
236
|
+
|
|
237
|
+
See [Data Enrichment Guide](docs/DATA_ENRICHMENT.md) for complete documentation and [Data Enrichment Example](examples/data_enrichment_example.rb) for working examples.
|
|
238
|
+
|
|
159
239
|
## Monitoring & Analytics
|
|
160
240
|
|
|
161
241
|
Real-time monitoring, metrics, and alerting for production environments.
|
|
@@ -175,11 +255,155 @@ Open [http://localhost:4568](http://localhost:4568) for the monitoring dashboard
|
|
|
175
255
|
|
|
176
256
|
**Features:**
|
|
177
257
|
- Real-time dashboard with WebSocket updates
|
|
258
|
+
- **Persistent Storage** - Database storage for long-term analytics (PostgreSQL, MySQL, SQLite)
|
|
178
259
|
- Prometheus metrics export
|
|
179
260
|
- Intelligent alerting with anomaly detection
|
|
180
261
|
- Grafana integration with pre-built dashboards
|
|
181
262
|
|
|
182
|
-
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complete documentation.
|
|
263
|
+
See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) and [Persistent Monitoring Guide](docs/PERSISTENT_MONITORING.md) for complete documentation.
|
|
264
|
+
|
|
265
|
+
## Simulation & What-If Analysis
|
|
266
|
+
|
|
267
|
+
DecisionAgent provides comprehensive simulation capabilities to test rule changes before deployment:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
require 'decision_agent/simulation/replay_engine'
|
|
271
|
+
|
|
272
|
+
# Replay historical decisions with new rules
|
|
273
|
+
replay_engine = DecisionAgent::Simulation::ReplayEngine.new(
|
|
274
|
+
agent: agent,
|
|
275
|
+
version_manager: version_manager
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
results = replay_engine.replay(historical_data: "decisions.csv")
|
|
279
|
+
|
|
280
|
+
# What-if analysis
|
|
281
|
+
whatif = DecisionAgent::Simulation::WhatIfAnalyzer.new(agent: agent)
|
|
282
|
+
analysis = whatif.analyze(
|
|
283
|
+
base_context: { credit_score: 700, amount: 50000 },
|
|
284
|
+
scenarios: [
|
|
285
|
+
{ credit_score: 750 },
|
|
286
|
+
{ credit_score: 650 }
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Impact analysis
|
|
291
|
+
impact = DecisionAgent::Simulation::ImpactAnalyzer.new
|
|
292
|
+
comparison = impact.compare(
|
|
293
|
+
baseline: baseline_evaluator,
|
|
294
|
+
proposed: proposed_evaluator,
|
|
295
|
+
contexts: test_contexts
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Features:**
|
|
300
|
+
- **Historical Replay** - Replay past decisions with CSV/JSON/database import
|
|
301
|
+
- **What-If Analysis** - Scenario simulation with decision boundary visualization
|
|
302
|
+
- **Impact Analysis** - Quantify rule change effects (decisions, confidence, performance)
|
|
303
|
+
- **Shadow Testing** - Test new rules in production without affecting outcomes
|
|
304
|
+
- **Monte Carlo Simulation** - Probabilistic decision modeling
|
|
305
|
+
- **Web UI** - Complete simulation dashboard at `/simulation`
|
|
306
|
+
|
|
307
|
+
See [Simulation Guide](docs/SIMULATION.md) for complete documentation and [Simulation Example](examples/simulation_example.rb) for working examples.
|
|
308
|
+
|
|
309
|
+
## Role-Based Access Control (RBAC)
|
|
310
|
+
|
|
311
|
+
Enterprise-grade authentication and authorization system:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
require 'decision_agent'
|
|
315
|
+
|
|
316
|
+
# Configure RBAC (works with any auth system)
|
|
317
|
+
DecisionAgent.configure_rbac(:devise_cancan, ability_class: Ability)
|
|
318
|
+
|
|
319
|
+
# Or use built-in RBAC
|
|
320
|
+
authenticator = DecisionAgent::Auth::Authenticator.new
|
|
321
|
+
admin = authenticator.create_user(
|
|
322
|
+
email: "admin@example.com",
|
|
323
|
+
password: "secure_password",
|
|
324
|
+
roles: [:admin]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
session = authenticator.login("admin@example.com", "secure_password")
|
|
328
|
+
|
|
329
|
+
# Permission checks
|
|
330
|
+
checker = DecisionAgent.permission_checker
|
|
331
|
+
checker.can?(admin, :write) # => true
|
|
332
|
+
checker.can?(admin, :approve) # => true
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Features:**
|
|
336
|
+
- **Built-in User System** - User management with bcrypt password hashing
|
|
337
|
+
- **5 Default Roles** - Admin, Editor, Viewer, Auditor, Approver
|
|
338
|
+
- **Configurable Adapters** - Devise, CanCanCan, Pundit, or custom
|
|
339
|
+
- **Password Reset** - Secure token-based password reset
|
|
340
|
+
- **Access Audit Logging** - Comprehensive audit trail for compliance
|
|
341
|
+
- **Web UI Integration** - Login page and user management interface
|
|
342
|
+
|
|
343
|
+
See [RBAC Configuration Guide](docs/RBAC_CONFIGURATION.md) for complete documentation and [RBAC Examples](examples/rbac_configuration_examples.rb) for integration examples.
|
|
344
|
+
|
|
345
|
+
## Batch Testing
|
|
346
|
+
|
|
347
|
+
Test rules against large datasets with comprehensive analysis:
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
require 'decision_agent/testing/batch_test_runner'
|
|
351
|
+
|
|
352
|
+
runner = DecisionAgent::Testing::BatchTestRunner.new(agent: agent)
|
|
353
|
+
|
|
354
|
+
# Import from CSV or Excel
|
|
355
|
+
importer = DecisionAgent::Testing::BatchTestImporter.new
|
|
356
|
+
scenarios = importer.import_csv("test_data.csv", {
|
|
357
|
+
context_fields: ["credit_score", "amount"],
|
|
358
|
+
expected_fields: ["expected_decision"]
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
# Run batch test
|
|
362
|
+
results = runner.run(scenarios: scenarios)
|
|
363
|
+
|
|
364
|
+
puts "Total: #{results[:total]}"
|
|
365
|
+
puts "Passed: #{results[:passed]}"
|
|
366
|
+
puts "Failed: #{results[:failed]}"
|
|
367
|
+
puts "Coverage: #{results[:coverage]}"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Features:**
|
|
371
|
+
- **CSV/Excel Import** - Import test scenarios from files
|
|
372
|
+
- **Database Import** - Load test data from databases
|
|
373
|
+
- **Coverage Analysis** - Identify untested rule combinations
|
|
374
|
+
- **Resume Capability** - Continue interrupted tests from checkpoint
|
|
375
|
+
- **Progress Tracking** - Real-time progress updates for large imports
|
|
376
|
+
- **Web UI** - Complete batch testing interface with file upload
|
|
377
|
+
|
|
378
|
+
See [Batch Testing Guide](docs/BATCH_TESTING.md) for complete documentation.
|
|
379
|
+
|
|
380
|
+
## A/B Testing
|
|
381
|
+
|
|
382
|
+
Compare rule versions with statistical analysis:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
require 'decision_agent/testing/ab_test_manager'
|
|
386
|
+
|
|
387
|
+
ab_manager = DecisionAgent::Testing::AbTestManager.new(version_manager: version_manager)
|
|
388
|
+
|
|
389
|
+
test = ab_manager.create_test(
|
|
390
|
+
name: "loan_approval_v2",
|
|
391
|
+
champion_version: champion_version_id,
|
|
392
|
+
challenger_version: challenger_version_id,
|
|
393
|
+
traffic_split: 0.5
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
results = ab_manager.run_test(test_id: test.id, contexts: test_contexts)
|
|
397
|
+
ab_manager.analyze_results(test_id: test.id)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Features:**
|
|
401
|
+
- **Champion/Challenger Testing** - Compare baseline vs proposed rules
|
|
402
|
+
- **Statistical Significance** - P-value calculation and confidence intervals
|
|
403
|
+
- **Traffic Splitting** - Configurable split ratios
|
|
404
|
+
- **Decision Distribution Comparison** - Visualize differences in outcomes
|
|
405
|
+
|
|
406
|
+
See [A/B Testing Guide](docs/AB_TESTING.md) for complete documentation.
|
|
183
407
|
|
|
184
408
|
## When to Use DecisionAgent
|
|
185
409
|
|
|
@@ -204,23 +428,31 @@ See [Monitoring & Analytics Guide](docs/MONITORING_AND_ANALYTICS.md) for complet
|
|
|
204
428
|
- [Examples Directory](examples/README.md) - Working examples with explanations
|
|
205
429
|
|
|
206
430
|
### Core Features
|
|
431
|
+
- [Explainability Layer](docs/EXPLAINABILITY.md) - Machine-readable decision explanations with condition-level tracing
|
|
207
432
|
- [Advanced Operators](docs/ADVANCED_OPERATORS.md) - String, numeric, date/time, collection, and geospatial operators
|
|
433
|
+
- [Data Enrichment](docs/DATA_ENRICHMENT.md) - REST API data enrichment with caching and circuit breaker
|
|
208
434
|
- [DMN Guide](docs/DMN_GUIDE.md) - Complete DMN 1.3 support guide
|
|
209
435
|
- [DMN API Reference](docs/DMN_API.md) - DMN API documentation
|
|
210
436
|
- [FEEL Reference](docs/FEEL_REFERENCE.md) - FEEL expression language reference
|
|
211
437
|
- [DMN Migration Guide](docs/DMN_MIGRATION_GUIDE.md) - Migrating from JSON to DMN
|
|
212
438
|
- [DMN Best Practices](docs/DMN_BEST_PRACTICES.md) - DMN modeling best practices
|
|
213
439
|
- [Versioning System](docs/VERSIONING.md) - Version control for rules
|
|
440
|
+
- [Simulation & What-If Analysis](docs/SIMULATION.md) - Historical replay, what-if analysis, impact analysis, and shadow testing
|
|
214
441
|
- [A/B Testing](docs/AB_TESTING.md) - Compare rule versions with statistical analysis
|
|
442
|
+
- [Batch Testing](docs/BATCH_TESTING.md) - Test rules against large datasets with CSV/Excel import
|
|
443
|
+
- [RBAC Configuration](docs/RBAC_CONFIGURATION.md) - Role-based access control setup and integration
|
|
444
|
+
- [RBAC Quick Reference](docs/RBAC_QUICK_REFERENCE.md) - Quick reference for RBAC configuration
|
|
215
445
|
- [Web UI](docs/WEB_UI.md) - Visual rule builder
|
|
216
446
|
- [Web UI Setup](docs/WEB_UI_SETUP.md) - Setup guide
|
|
217
447
|
- [Web UI Rails Integration](docs/WEB_UI_RAILS_INTEGRATION.md) - Mount in Rails/Rack apps
|
|
218
448
|
- [Monitoring & Analytics](docs/MONITORING_AND_ANALYTICS.md) - Real-time monitoring, metrics, and alerting
|
|
219
449
|
- [Monitoring Architecture](docs/MONITORING_ARCHITECTURE.md) - System architecture and design
|
|
450
|
+
- [Persistent Monitoring](docs/PERSISTENT_MONITORING.md) - Database storage for long-term analytics
|
|
220
451
|
|
|
221
452
|
### Performance & Thread-Safety
|
|
222
453
|
- [Performance & Thread-Safety Summary](docs/PERFORMANCE_AND_THREAD_SAFETY.md) - Benchmarks and production readiness
|
|
223
454
|
- [Thread-Safety Implementation](docs/THREAD_SAFETY.md) - Technical implementation guide
|
|
455
|
+
- [Benchmarks](benchmarks/README.md) - Comprehensive benchmark suite and performance testing
|
|
224
456
|
|
|
225
457
|
### Reference
|
|
226
458
|
- [API Contract](docs/API_CONTRACT.md) - Full API reference
|
|
@@ -244,6 +476,39 @@ All data structures are deeply frozen to prevent mutation, ensuring safe concurr
|
|
|
244
476
|
|
|
245
477
|
See [Thread-Safety Guide](docs/THREAD_SAFETY.md) and [Performance Analysis](docs/PERFORMANCE_AND_THREAD_SAFETY.md) for details.
|
|
246
478
|
|
|
479
|
+
**Run Benchmarks:**
|
|
480
|
+
```bash
|
|
481
|
+
# Run all benchmarks
|
|
482
|
+
rake benchmark:all
|
|
483
|
+
|
|
484
|
+
# Run specific benchmarks
|
|
485
|
+
rake benchmark:basic # Basic decision performance
|
|
486
|
+
rake benchmark:threads # Thread-safety and scalability
|
|
487
|
+
rake benchmark:regression # Compare against baseline
|
|
488
|
+
|
|
489
|
+
# See [Benchmarks Guide](benchmarks/README.md) for complete documentation
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Latest Benchmark Results
|
|
493
|
+
|
|
494
|
+
**Last Updated:** 2026-01-06T04:03:29Z
|
|
495
|
+
|
|
496
|
+
#### Performance Comparison
|
|
497
|
+
|
|
498
|
+
| Metric | Latest (2026-01-06) | Previous (2026-01-06) | Change |
|
|
499
|
+
|--------|--------------------------------------------------|------------------------------------------------------|--------|
|
|
500
|
+
| Basic Throughput | 8966.04 decisions/sec | 9751.42 decisions/sec | ↓ 8.05% (degraded) |
|
|
501
|
+
| Basic Latency | 0.1115 ms | 0.1025 ms | ↑ 8.78% (degraded) |
|
|
502
|
+
| Multi-threaded (50 threads) Throughput | 8560.69 decisions/sec | 8849.86 decisions/sec | ↓ 3.27% (degraded) |
|
|
503
|
+
| Multi-threaded (50 threads) Latency | 0.1168 ms | 0.113 ms | ↑ 3.36% (degraded) |
|
|
504
|
+
|
|
505
|
+
**Environment:**
|
|
506
|
+
- Ruby Version: 3.3.5
|
|
507
|
+
- Hardware: x86_64
|
|
508
|
+
- OS: Darwin
|
|
509
|
+
- Git Commit: `aba46af5`
|
|
510
|
+
|
|
511
|
+
> 💡 **Note:** Run `rake benchmark:regression` to generate new benchmark results. This section is automatically updated with the last 2 benchmark runs.
|
|
247
512
|
## Contributing
|
|
248
513
|
|
|
249
514
|
1. Fork the repository
|
data/lib/decision_agent/agent.rb
CHANGED
|
@@ -6,6 +6,17 @@ module DecisionAgent
|
|
|
6
6
|
class Agent
|
|
7
7
|
attr_reader :evaluators, :scoring_strategy, :audit_adapter
|
|
8
8
|
|
|
9
|
+
# Thread-safe cache for deterministic hash computation
|
|
10
|
+
# This significantly improves performance when the same context/evaluations
|
|
11
|
+
# are processed multiple times (common in benchmarks and high-throughput scenarios)
|
|
12
|
+
@hash_cache = {}
|
|
13
|
+
@hash_cache_mutex = Mutex.new
|
|
14
|
+
@hash_cache_max_size = 1000 # Limit cache size to prevent memory bloat
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :hash_cache, :hash_cache_mutex, :hash_cache_max_size
|
|
18
|
+
end
|
|
19
|
+
|
|
9
20
|
def initialize(evaluators:, scoring_strategy: nil, audit_adapter: nil, validate_evaluations: nil)
|
|
10
21
|
@evaluators = Array(evaluators)
|
|
11
22
|
@scoring_strategy = scoring_strategy || Scoring::WeightedAverage.new
|
|
@@ -129,8 +140,68 @@ module DecisionAgent
|
|
|
129
140
|
|
|
130
141
|
def compute_deterministic_hash(payload)
|
|
131
142
|
hashable = payload.slice(:context, :evaluations, :decision, :confidence, :scoring_strategy)
|
|
143
|
+
|
|
144
|
+
# Use fast hash (MD5) as cache key to avoid expensive canonicalization on cache hits
|
|
145
|
+
# Optimized: Use direct JSON.to_json instead of recursive sorting for speed
|
|
146
|
+
# The cache key doesn't need perfect determinism, just good enough to catch duplicates
|
|
147
|
+
# This avoids the expensive sort_hash_keys recursion on every call
|
|
148
|
+
json_str = hashable.to_json
|
|
149
|
+
fast_key = Digest::MD5.hexdigest(json_str)
|
|
150
|
+
|
|
151
|
+
# Fast path: check cache without lock first (unsafe read, but acceptable for cache)
|
|
152
|
+
# This allows concurrent reads without mutex overhead
|
|
153
|
+
cache = self.class.hash_cache
|
|
154
|
+
cached_hash = cache[fast_key]
|
|
155
|
+
return cached_hash if cached_hash
|
|
156
|
+
|
|
157
|
+
# Cache miss - compute canonical JSON (required for deterministic hashing)
|
|
158
|
+
# This is expensive, but only happens on cache misses
|
|
132
159
|
canonical = canonical_json(hashable)
|
|
133
|
-
|
|
160
|
+
|
|
161
|
+
# Compute SHA256 hash (also expensive, but only on cache misses)
|
|
162
|
+
computed_hash = Digest::SHA256.hexdigest(canonical)
|
|
163
|
+
|
|
164
|
+
# Store in cache (thread-safe, with size limit)
|
|
165
|
+
# Only lock when we need to write
|
|
166
|
+
self.class.hash_cache_mutex.synchronize do
|
|
167
|
+
# Double-check after acquiring lock (another thread may have added it)
|
|
168
|
+
return self.class.hash_cache[fast_key] if self.class.hash_cache[fast_key]
|
|
169
|
+
|
|
170
|
+
# Clear cache if it gets too large (simple FIFO eviction)
|
|
171
|
+
if self.class.hash_cache.size >= self.class.hash_cache_max_size
|
|
172
|
+
# Remove oldest 10% of entries (simple approximation)
|
|
173
|
+
keys_to_remove = self.class.hash_cache.keys.first(self.class.hash_cache_max_size / 10)
|
|
174
|
+
keys_to_remove.each { |key| self.class.hash_cache.delete(key) }
|
|
175
|
+
end
|
|
176
|
+
self.class.hash_cache[fast_key] = computed_hash
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
computed_hash
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Fast hash key generation using MD5 (much faster than canonical JSON + SHA256)
|
|
183
|
+
# Used as cache key to avoid expensive canonicalization on cache hits
|
|
184
|
+
# MD5 is sufficient for cache keys (collision resistance not critical, speed is)
|
|
185
|
+
def fast_hash_key(hashable)
|
|
186
|
+
# Create a deterministic string representation for hashing
|
|
187
|
+
# Use sorted JSON to ensure determinism (though not RFC 8785 canonical)
|
|
188
|
+
json_str = sort_hash_keys(hashable).to_json
|
|
189
|
+
Digest::MD5.hexdigest(json_str)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Recursively sort hash keys for deterministic hashing
|
|
193
|
+
# This is faster than canonical JSON but still deterministic
|
|
194
|
+
# Note: This is still used by canonical_json indirectly, but fast_hash_key avoids it
|
|
195
|
+
def sort_hash_keys(obj)
|
|
196
|
+
case obj
|
|
197
|
+
when Hash
|
|
198
|
+
sorted = obj.sort.to_h
|
|
199
|
+
sorted.transform_values { |v| sort_hash_keys(v) }
|
|
200
|
+
when Array
|
|
201
|
+
obj.map { |v| sort_hash_keys(v) }
|
|
202
|
+
else
|
|
203
|
+
obj
|
|
204
|
+
end
|
|
134
205
|
end
|
|
135
206
|
|
|
136
207
|
# Uses RFC 8785 (JSON Canonicalization Scheme) for deterministic JSON serialization
|
|
@@ -4,6 +4,7 @@ module DecisionAgent
|
|
|
4
4
|
|
|
5
5
|
def initialize(data)
|
|
6
6
|
# Create a deep copy before freezing to avoid mutating the original
|
|
7
|
+
# This is necessary for thread-safety even if it adds some overhead
|
|
7
8
|
data_hash = data.is_a?(Hash) ? data : {}
|
|
8
9
|
@data = deep_freeze(deep_dup(data_hash))
|
|
9
10
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cache_adapter"
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module DecisionAgent
|
|
7
|
+
module DataEnrichment
|
|
8
|
+
module Cache
|
|
9
|
+
# In-memory cache adapter (default, no dependencies)
|
|
10
|
+
class MemoryAdapter < CacheAdapter
|
|
11
|
+
include MonitorMixin
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
super
|
|
15
|
+
@cache = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get cached value
|
|
19
|
+
#
|
|
20
|
+
# @param key [String] Cache key
|
|
21
|
+
# @return [Hash, nil] Cached data or nil if not found/expired
|
|
22
|
+
def get(key)
|
|
23
|
+
synchronize do
|
|
24
|
+
entry = @cache[key]
|
|
25
|
+
return nil unless entry
|
|
26
|
+
|
|
27
|
+
# Check if expired
|
|
28
|
+
if entry[:expires_at] < Time.now
|
|
29
|
+
@cache.delete(key)
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
entry[:value]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Set cached value
|
|
38
|
+
#
|
|
39
|
+
# @param key [String] Cache key
|
|
40
|
+
# @param value [Hash] Data to cache
|
|
41
|
+
# @param ttl [Integer] Time to live in seconds
|
|
42
|
+
def set(key, value, ttl)
|
|
43
|
+
synchronize do
|
|
44
|
+
@cache[key] = {
|
|
45
|
+
value: value,
|
|
46
|
+
expires_at: Time.now + ttl
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Delete cached value
|
|
52
|
+
#
|
|
53
|
+
# @param key [String] Cache key
|
|
54
|
+
def delete(key)
|
|
55
|
+
synchronize do
|
|
56
|
+
@cache.delete(key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Clear all cached values
|
|
61
|
+
def clear
|
|
62
|
+
synchronize do
|
|
63
|
+
@cache.clear
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get cache statistics
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] Cache statistics
|
|
70
|
+
def stats
|
|
71
|
+
synchronize do
|
|
72
|
+
now = Time.now
|
|
73
|
+
valid_entries = @cache.count { |_k, v| v[:expires_at] >= now }
|
|
74
|
+
expired_entries = @cache.size - valid_entries
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
size: @cache.size,
|
|
78
|
+
valid: valid_entries,
|
|
79
|
+
expired: expired_entries
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module DataEnrichment
|
|
5
|
+
# Base class for cache adapters
|
|
6
|
+
class CacheAdapter
|
|
7
|
+
# Get cached value
|
|
8
|
+
#
|
|
9
|
+
# @param key [String] Cache key
|
|
10
|
+
# @return [Hash, nil] Cached data or nil if not found/expired
|
|
11
|
+
def get(key)
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement #get"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Set cached value
|
|
16
|
+
#
|
|
17
|
+
# @param key [String] Cache key
|
|
18
|
+
# @param value [Hash] Data to cache
|
|
19
|
+
# @param ttl [Integer] Time to live in seconds
|
|
20
|
+
def set(key, value, ttl)
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #set"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Delete cached value
|
|
25
|
+
#
|
|
26
|
+
# @param key [String] Cache key
|
|
27
|
+
def delete(key)
|
|
28
|
+
raise NotImplementedError, "#{self.class} must implement #delete"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Clear all cached values
|
|
32
|
+
def clear
|
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #clear"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generate cache key from request parameters
|
|
37
|
+
#
|
|
38
|
+
# @param endpoint_name [Symbol] Endpoint identifier
|
|
39
|
+
# @param params [Hash] Request parameters
|
|
40
|
+
# @return [String] Cache key
|
|
41
|
+
def cache_key(endpoint_name, params)
|
|
42
|
+
# Sort params for consistent key generation
|
|
43
|
+
sorted_params = params.sort.to_h
|
|
44
|
+
param_string = sorted_params.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
45
|
+
"#{endpoint_name}:#{Digest::SHA256.hexdigest(param_string)}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|