decision_agent 0.1.1 → 0.1.2

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.
data/README.md CHANGED
@@ -7,1054 +7,192 @@
7
7
 
8
8
  A production-grade, deterministic, explainable, and auditable decision engine for Ruby.
9
9
 
10
- ## The Problem
11
-
12
- Enterprise applications need to make complex decisions based on business rules, but existing solutions fall short:
13
-
14
- - **Traditional rule engines**: Often lack conflict resolution, confidence scoring, and audit replay capabilities
15
- - **Framework-coupled solutions**: Tightly bound to specific frameworks (Rails, etc.), limiting portability
16
- - **AI-first frameworks**: Non-deterministic, expensive, opaque, and unsuitable for regulated domains
10
+ **Built for regulated domains. Deterministic by design. AI-optional.**
17
11
 
18
- **DecisionAgent** solves these problems by providing:
12
+ ## Why DecisionAgent?
19
13
 
20
- 1. **Deterministic decisions** - Same input always produces same output
21
- 2. **Full explainability** - Every decision includes human-readable reasoning
22
- 3. **Audit replay** - Reproduce any historical decision exactly
23
- 4. **Conflict resolution** - Multiple evaluators with pluggable scoring strategies
24
- 5. **Framework-agnostic** - Pure Ruby, no Rails/ActiveRecord/Sidekiq dependencies
25
- 6. **AI-optional** - Rules first, AI enhancement optional
14
+ - **Deterministic** - Same input always produces same output
15
+ - **Explainable** - Every decision includes human-readable reasoning
16
+ - **Auditable** - Reproduce any historical decision exactly
17
+ - **Framework-agnostic** - Pure Ruby, works anywhere
18
+ - **Production-ready** - Comprehensive testing, error handling, and versioning
26
19
 
27
20
  ## Installation
28
21
 
29
- Add to your Gemfile:
30
-
31
- ```ruby
32
- gem 'decision_agent'
33
- ```
34
-
35
- Or install directly:
36
-
37
22
  ```bash
38
23
  gem install decision_agent
39
24
  ```
40
25
 
41
- ## Web UI - Visual Rule Builder 🎯
42
-
43
- For non-technical users, DecisionAgent includes a web-based visual rule builder:
44
-
45
- ```bash
46
- decision_agent web
26
+ Or add to your Gemfile:
27
+ ```ruby
28
+ gem 'decision_agent'
47
29
  ```
48
30
 
49
- Then open [http://localhost:4567](http://localhost:4567) in your browser.
50
-
51
- The Web UI provides:
52
- - 📝 **Visual rule creation** - Build rules using forms and dropdowns
53
- - 🔍 **Live validation** - Instant feedback on rule correctness
54
- - 📤 **Export/Import** - Download or upload rules as JSON
55
- - 📚 **Example templates** - Pre-built rule sets to get started
56
- - ✨ **No coding required** - Perfect for business analysts and domain experts
57
-
58
- See [WEB_UI.md](WEB_UI.md) for detailed documentation.
59
-
60
- <img width="1602" height="770" alt="Screenshot 2025-12-19 at 3 06 07 PM" src="https://github.com/user-attachments/assets/6ee6859c-f9f2-4f93-8bff-923986ccb1bc" />
61
-
62
-
63
31
  ## Quick Start
64
32
 
65
33
  ```ruby
66
34
  require 'decision_agent'
67
35
 
68
- # Define evaluators
69
- evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(
70
- decision: "approve",
71
- weight: 0.8,
72
- reason: "User meets basic criteria"
73
- )
74
-
75
- # Create agent
76
- agent = DecisionAgent::Agent.new(
77
- evaluators: [evaluator],
78
- scoring_strategy: DecisionAgent::Scoring::WeightedAverage.new,
79
- audit_adapter: DecisionAgent::Audit::LoggerAdapter.new
80
- )
81
-
82
- # Make decision
83
- result = agent.decide(
84
- context: { user: "alice", priority: "high" }
85
- )
86
-
87
- puts result.decision # => "approve"
88
- puts result.confidence # => 0.8
89
- puts result.explanations # => ["Decision: approve (confidence: 0.8)", ...]
90
- puts result.audit_payload # => Full audit trail
91
- ```
92
-
93
- ## Core Concepts
94
-
95
- ### 1. Agent
96
-
97
- The orchestrator that coordinates evaluators, resolves conflicts, and produces decisions.
98
-
99
- ```ruby
100
- agent = DecisionAgent::Agent.new(
101
- evaluators: [eval1, eval2, eval3],
102
- scoring_strategy: DecisionAgent::Scoring::WeightedAverage.new,
103
- audit_adapter: DecisionAgent::Audit::NullAdapter.new
104
- )
105
- ```
106
-
107
- ### 2. Evaluators
108
-
109
- Pluggable components that evaluate context and return decisions.
110
-
111
- #### StaticEvaluator
112
-
113
- Always returns the same decision:
114
-
115
- ```ruby
116
- evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(
117
- decision: "approve",
118
- weight: 0.7,
119
- reason: "Static approval rule"
120
- )
121
- ```
122
-
123
- #### JsonRuleEvaluator
124
-
125
- Evaluates context against JSON-based business rules:
126
-
127
- ```ruby
128
- rules = {
129
- version: "1.0",
130
- ruleset: "issue_triage",
131
- rules: [
132
- {
133
- id: "high_priority_rule",
134
- if: {
135
- all: [
136
- { field: "priority", op: "eq", value: "high" },
137
- { field: "hours_inactive", op: "gte", value: 4 }
138
- ]
139
- },
140
- then: {
141
- decision: "escalate",
142
- weight: 0.9,
143
- reason: "High priority issue inactive too long"
144
- }
145
- }
146
- ]
147
- }
148
-
36
+ # Define evaluator with business rules
149
37
  evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(
150
- rules_json: rules
151
- )
152
- ```
153
-
154
- ### 3. Context
155
-
156
- Immutable input data for decision-making:
157
-
158
- ```ruby
159
- context = DecisionAgent::Context.new({
160
- user: "alice",
161
- priority: "high",
162
- hours_inactive: 5
163
- })
164
- ```
165
-
166
- ### 4. Scoring Strategies
167
-
168
- Resolve conflicts when multiple evaluators return different decisions.
169
-
170
- #### WeightedAverage (Default)
171
-
172
- Sums weights for each decision, selects winner:
173
-
174
- ```ruby
175
- DecisionAgent::Scoring::WeightedAverage.new
176
- ```
177
-
178
- #### MaxWeight
179
-
180
- Selects decision with highest individual weight:
181
-
182
- ```ruby
183
- DecisionAgent::Scoring::MaxWeight.new
184
- ```
185
-
186
- #### Consensus
187
-
188
- Requires minimum agreement threshold:
189
-
190
- ```ruby
191
- DecisionAgent::Scoring::Consensus.new(minimum_agreement: 0.6)
192
- ```
193
-
194
- #### Threshold
195
-
196
- Requires minimum weight to accept decision:
197
-
198
- ```ruby
199
- DecisionAgent::Scoring::Threshold.new(
200
- threshold: 0.8,
201
- fallback_decision: "manual_review"
38
+ rules_json: {
39
+ version: "1.0",
40
+ ruleset: "approval_rules",
41
+ rules: [{
42
+ id: "high_value",
43
+ if: { field: "amount", op: "gt", value: 1000 },
44
+ then: { decision: "approve", weight: 0.9, reason: "High value transaction" }
45
+ }]
46
+ }
202
47
  )
203
- ```
204
-
205
- ### 5. Audit Adapters
206
-
207
- Record decisions for compliance and debugging.
208
48
 
209
- #### NullAdapter
49
+ # Create decision agent
50
+ agent = DecisionAgent::Agent.new(evaluators: [evaluator])
210
51
 
211
- No-op (default):
212
-
213
- ```ruby
214
- DecisionAgent::Audit::NullAdapter.new
215
- ```
216
-
217
- #### LoggerAdapter
218
-
219
- Logs to any Ruby logger:
220
-
221
- ```ruby
222
- DecisionAgent::Audit::LoggerAdapter.new(
223
- logger: Rails.logger,
224
- level: Logger::INFO
225
- )
226
- ```
227
-
228
- ## JSON Rule DSL
229
-
230
- ### Supported Operators
231
-
232
- | Operator | Description | Example |
233
- |----------|-------------|---------|
234
- | `eq` | Equal | `{ field: "status", op: "eq", value: "active" }` |
235
- | `neq` | Not equal | `{ field: "status", op: "neq", value: "closed" }` |
236
- | `gt` | Greater than | `{ field: "score", op: "gt", value: 80 }` |
237
- | `gte` | Greater than or equal | `{ field: "hours", op: "gte", value: 4 }` |
238
- | `lt` | Less than | `{ field: "temp", op: "lt", value: 32 }` |
239
- | `lte` | Less than or equal | `{ field: "temp", op: "lte", value: 32 }` |
240
- | `in` | Array membership | `{ field: "status", op: "in", value: ["open", "pending"] }` |
241
- | `present` | Field exists and not empty | `{ field: "assignee", op: "present" }` |
242
- | `blank` | Field missing, nil, or empty | `{ field: "description", op: "blank" }` |
243
-
244
- ### Condition Combinators
245
-
246
- #### all
247
-
248
- All sub-conditions must be true:
249
-
250
- ```json
251
- {
252
- "all": [
253
- { "field": "priority", "op": "eq", "value": "high" },
254
- { "field": "hours", "op": "gte", "value": 4 }
255
- ]
256
- }
257
- ```
258
-
259
- #### any
260
-
261
- At least one sub-condition must be true:
262
-
263
- ```json
264
- {
265
- "any": [
266
- { "field": "escalated", "op": "eq", "value": true },
267
- { "field": "complaints", "op": "gte", "value": 3 }
268
- ]
269
- }
270
- ```
271
-
272
- ### Nested Fields
273
-
274
- Use dot notation to access nested data:
275
-
276
- ```json
277
- {
278
- "field": "user.role",
279
- "op": "eq",
280
- "value": "admin"
281
- }
282
- ```
283
-
284
- ```ruby
285
- context = DecisionAgent::Context.new({
286
- user: { role: "admin" }
287
- })
288
- ```
289
-
290
- ### Complete Example
291
-
292
- ```json
293
- {
294
- "version": "1.0",
295
- "ruleset": "redmine_triage",
296
- "rules": [
297
- {
298
- "id": "critical_escalation",
299
- "if": {
300
- "all": [
301
- { "field": "priority", "op": "eq", "value": "critical" },
302
- {
303
- "any": [
304
- { "field": "hours_inactive", "op": "gte", "value": 2 },
305
- { "field": "customer_escalated", "op": "eq", "value": true }
306
- ]
307
- }
308
- ]
309
- },
310
- "then": {
311
- "decision": "escalate_immediately",
312
- "weight": 1.0,
313
- "reason": "Critical issue requires immediate attention"
314
- }
315
- },
316
- {
317
- "id": "auto_assign",
318
- "if": {
319
- "all": [
320
- { "field": "assignee", "op": "blank" },
321
- { "field": "priority", "op": "in", "value": ["high", "critical"] }
322
- ]
323
- },
324
- "then": {
325
- "decision": "assign_to_team_lead",
326
- "weight": 0.85,
327
- "reason": "High priority issue needs assignment"
328
- }
329
- }
330
- ]
331
- }
332
- ```
333
-
334
- ## Decision Replay
335
-
336
- Critical for compliance and debugging - replay any historical decision exactly.
337
-
338
- ### Strict Mode
339
-
340
- Fails if replayed decision differs from original:
341
-
342
- ```ruby
343
- original_result = agent.decide(context: { user: "alice" })
344
-
345
- # Later, replay the exact decision
346
- replayed_result = DecisionAgent::Replay.run(
347
- original_result.audit_payload,
348
- strict: true
349
- )
52
+ # Make decision
53
+ result = agent.decide(context: { amount: 1500 })
350
54
 
351
- # Raises ReplayMismatchError if decision changed
55
+ puts result.decision # => "approve"
56
+ puts result.confidence # => 0.9
57
+ puts result.explanations # => ["High value transaction"]
352
58
  ```
353
59
 
354
- ### Non-Strict Mode
60
+ ## Web UI - Visual Rule Builder
355
61
 
356
- Logs differences but allows evolution:
62
+ Launch the visual rule builder for non-technical users:
357
63
 
358
- ```ruby
359
- replayed_result = DecisionAgent::Replay.run(
360
- original_result.audit_payload,
361
- strict: false # Logs differences but doesn't fail
362
- )
363
- ```
364
-
365
- ### Audit Payload Structure
366
-
367
- ```ruby
368
- {
369
- timestamp: "2025-01-15T10:30:45.123456Z",
370
- context: { user: "alice", priority: "high" },
371
- feedback: {},
372
- evaluations: [
373
- {
374
- decision: "approve",
375
- weight: 0.8,
376
- reason: "Rule matched",
377
- evaluator_name: "JsonRuleEvaluator",
378
- metadata: { rule_id: "high_priority_rule" }
379
- }
380
- ],
381
- decision: "approve",
382
- confidence: 0.8,
383
- scoring_strategy: "DecisionAgent::Scoring::WeightedAverage",
384
- agent_version: "0.1.0",
385
- deterministic_hash: "a3f2b9c..."
386
- }
64
+ ```bash
65
+ decision_agent web
387
66
  ```
388
67
 
389
- ## Advanced Usage
390
-
391
- ### Multiple Evaluators with Conflict Resolution
392
-
393
- ```ruby
394
- rule_evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(
395
- rules_json: File.read("rules/triage.json")
396
- )
397
-
398
- ml_evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(
399
- decision: "review_manually",
400
- weight: 0.6,
401
- reason: "ML model suggests manual review"
402
- )
403
-
68
+ Open [http://localhost:4567](http://localhost:4567) in your browser.
69
+
70
+ <img width="1602" alt="DecisionAgent Web UI" src="https://github.com/user-attachments/assets/6ee6859c-f9f2-4f93-8bff-923986ccb1bc" />
71
+
72
+ ## Documentation
73
+
74
+ ```
75
+ 📚 DecisionAgent Documentation
76
+
77
+ ├── 🚀 Getting Started
78
+ │ ├── Installation (above)
79
+ │ ├── Quick Start (above)
80
+ │ └── Examples examples/README.md
81
+
82
+ ├── 📖 Core Documentation
83
+ │ ├── Core Concepts → wiki/CORE_CONCEPTS.md
84
+ │ ├── JSON Rule DSL → wiki/JSON_RULE_DSL.md
85
+ │ ├── API Reference → wiki/API_CONTRACT.md
86
+ │ └── Error Handling → wiki/ERROR_HANDLING.md
87
+
88
+ ├── 🎯 Advanced Features
89
+ │ ├── Versioning System → wiki/VERSIONING.md
90
+ │ ├── Decision Replay → wiki/REPLAY.md
91
+ │ ├── Advanced Usage → wiki/ADVANCED_USAGE.md
92
+ │ └── Custom Components → wiki/ADVANCED_USAGE.md#custom-components
93
+
94
+ ├── 🔌 Integration Guides
95
+ │ ├── Rails Integration → wiki/INTEGRATION.md#rails
96
+ │ ├── Redmine Plugin → wiki/INTEGRATION.md#redmine
97
+ │ ├── Standalone Service → wiki/INTEGRATION.md#standalone
98
+ │ └── Testing Guide → wiki/TESTING.md
99
+
100
+ ├── 🎨 Web UI
101
+ │ ├── User Guide → wiki/WEB_UI.md
102
+ │ └── Setup Guide → wiki/WEB_UI_SETUP.md
103
+
104
+ └── 📝 Reference
105
+ ├── Changelog → wiki/CHANGELOG.md
106
+ └── Full Wiki Index → wiki/README.md
107
+ ```
108
+
109
+ ## Key Features
110
+
111
+ ### Decision Making
112
+ - **Multiple Evaluators** - Combine rule-based, ML, and custom logic
113
+ - **Conflict Resolution** - Weighted average, consensus, threshold, max weight
114
+ - **Rich Context** - Nested data, dot notation, flexible operators
115
+
116
+ ### Auditability
117
+ - **Complete Audit Trails** - Every decision fully logged
118
+ - **Deterministic Replay** - Reproduce historical decisions exactly
119
+ - **Compliance Ready** - HIPAA, SOX, regulatory compliance support
120
+
121
+ ### Flexibility
122
+ - **Pluggable Architecture** - Custom evaluators, scoring, audit adapters
123
+ - **Framework Agnostic** - Works with Rails, Sinatra, or standalone
124
+ - **JSON Rule DSL** - Non-technical users can write rules
125
+ - **Visual Rule Builder** - Web UI for rule management
126
+
127
+ ### Production Ready
128
+ - **Comprehensive Testing** - 90%+ code coverage
129
+ - **Error Handling** - Clear, actionable error messages
130
+ - **Versioning** - Full rule version control and rollback
131
+ - **Performance** - Fast, zero external dependencies
132
+
133
+ ## Examples
134
+
135
+ ```ruby
136
+ # Multiple evaluators with conflict resolution
404
137
  agent = DecisionAgent::Agent.new(
405
138
  evaluators: [rule_evaluator, ml_evaluator],
406
- scoring_strategy: DecisionAgent::Scoring::Consensus.new(minimum_agreement: 0.7)
407
- )
408
-
409
- result = agent.decide(
410
- context: { priority: "high", complexity: "high" }
411
- )
412
-
413
- # Explanations show how conflict was resolved
414
- puts result.explanations
415
- ```
416
-
417
- ### Custom Evaluator
418
-
419
- ```ruby
420
- class CustomBusinessLogicEvaluator < DecisionAgent::Evaluators::Base
421
- def evaluate(context, feedback: {})
422
- # Your custom logic here
423
- if context[:revenue] > 100_000 && context[:customer_tier] == "enterprise"
424
- DecisionAgent::Evaluation.new(
425
- decision: "approve_immediately",
426
- weight: 0.95,
427
- reason: "High-value enterprise customer",
428
- evaluator_name: "EnterpriseCustomerEvaluator",
429
- metadata: { tier: "enterprise" }
430
- )
431
- else
432
- nil # No decision
433
- end
434
- end
435
- end
436
- ```
437
-
438
- ### Custom Scoring Strategy
439
-
440
- ```ruby
441
- class VetoScoring < DecisionAgent::Scoring::Base
442
- def score(evaluations)
443
- # If any evaluator says "reject", veto everything
444
- if evaluations.any? { |e| e.decision == "reject" }
445
- return { decision: "reject", confidence: 1.0 }
446
- end
447
-
448
- # Otherwise, use max weight
449
- max_eval = evaluations.max_by(&:weight)
450
- {
451
- decision: max_eval.decision,
452
- confidence: normalize_confidence(max_eval.weight)
453
- }
454
- end
455
- end
456
-
457
- agent = DecisionAgent::Agent.new(
458
- evaluators: [...],
459
- scoring_strategy: VetoScoring.new
460
- )
461
- ```
462
-
463
- ### Custom Audit Adapter
464
-
465
- ```ruby
466
- class DatabaseAuditAdapter < DecisionAgent::Audit::Adapter
467
- def record(decision, context)
468
- AuditLog.create!(
469
- decision: decision.decision,
470
- confidence: decision.confidence,
471
- context_json: context.to_h.to_json,
472
- audit_payload: decision.audit_payload.to_json,
473
- deterministic_hash: decision.audit_payload[:deterministic_hash]
474
- )
475
- end
476
- end
477
- ```
478
-
479
- ### Feedback Loop
480
-
481
- The `feedback` parameter allows you to pass additional context about past decisions, manual overrides, or external signals that can influence decision-making in custom evaluators.
482
-
483
- #### Built-in Evaluators and Feedback
484
-
485
- **Built-in evaluators** (`JsonRuleEvaluator`, `StaticEvaluator`) **ignore feedback** to maintain determinism. This is intentional - the same context should always produce the same decision for auditability and replay purposes.
486
-
487
- ```ruby
488
- # Feedback is accepted but not used by built-in evaluators
489
- result = agent.decide(
490
- context: { issue_id: 123 },
491
- feedback: { source: "automated", past_accuracy: 0.95 }
492
- )
493
-
494
- # The feedback is stored in the audit trail for analysis
495
- puts result.audit_payload[:feedback] # => { source: "automated", past_accuracy: 0.95 }
496
- ```
497
-
498
- #### Custom Feedback-Aware Evaluators
499
-
500
- For **adaptive behavior**, create custom evaluators that use feedback:
501
-
502
- ```ruby
503
- # See examples/feedback_aware_evaluator.rb for a complete implementation
504
- class FeedbackAwareEvaluator < DecisionAgent::Evaluators::Base
505
- def evaluate(context, feedback: {})
506
- # Use feedback to adjust decisions
507
- if feedback[:override]
508
- return Evaluation.new(
509
- decision: feedback[:override],
510
- weight: 0.9,
511
- reason: feedback[:reason] || "Manual override",
512
- evaluator_name: evaluator_name
513
- )
514
- end
515
-
516
- # Or adjust confidence based on past accuracy
517
- adjusted_weight = base_weight * feedback[:past_accuracy].to_f
518
-
519
- Evaluation.new(
520
- decision: base_decision,
521
- weight: adjusted_weight,
522
- reason: "Adjusted by past performance",
523
- evaluator_name: evaluator_name
524
- )
525
- end
526
- end
527
- ```
528
-
529
- #### Common Feedback Patterns
530
-
531
- 1. **Manual Override**: Human-in-the-loop corrections
532
- ```ruby
533
- agent.decide(
534
- context: { user_id: 123 },
535
- feedback: { override: "manual_review", reason: "Suspicious activity" }
536
- )
537
- ```
538
-
539
- 2. **Historical Performance**: Adjust confidence based on past accuracy
540
- ```ruby
541
- agent.decide(
542
- context: { transaction: tx },
543
- feedback: { past_accuracy: 0.87 } # This evaluator was 87% accurate historically
544
- )
545
- ```
546
-
547
- 3. **Source Attribution**: Weight decisions differently based on origin
548
- ```ruby
549
- agent.decide(
550
- context: { issue: issue },
551
- feedback: { source: "expert_review" } # Higher confidence for expert reviews
552
- )
553
- ```
554
-
555
- 4. **Learning Signals**: Collect data for offline model training
556
- ```ruby
557
- # Initial decision
558
- result = agent.decide(context: { user: user })
559
-
560
- # Later: user provides feedback
561
- user_feedback = {
562
- correct: false,
563
- actual_decision: "escalate",
564
- user_id: "manager_bob",
565
- timestamp: Time.now.utc.iso8601
566
- }
567
-
568
- # Log for analysis and future rule adjustments
569
- # (DecisionAgent doesn't auto-update rules - this is for your ML/analysis pipeline)
570
- FeedbackLog.create(
571
- decision_hash: result.audit_payload[:deterministic_hash],
572
- predicted: result.decision,
573
- actual: user_feedback[:actual_decision],
574
- feedback: user_feedback
575
- )
576
- ```
577
-
578
- #### Example: Complete Feedback-Aware System
579
-
580
- See [examples/feedback_aware_evaluator.rb](examples/feedback_aware_evaluator.rb) for a complete example that demonstrates:
581
- - Manual overrides with high confidence
582
- - Past accuracy-based weight adjustment
583
- - Source-based confidence boosting
584
- - Comprehensive metadata tracking
585
-
586
- **Key Principle**: Use feedback for **human oversight** and **continuous improvement**, but keep the core decision logic deterministic and auditable.
587
-
588
- ## Integration Examples
589
-
590
- ### Rails Integration
591
-
592
- ```ruby
593
- # app/services/issue_decision_service.rb
594
- class IssueDecisionService
595
- def self.decide_action(issue)
596
- agent = build_agent
597
-
598
- result = agent.decide(
599
- context: {
600
- priority: issue.priority,
601
- hours_inactive: (Time.now - issue.updated_at) / 3600,
602
- assignee: issue.assignee&.login,
603
- status: issue.status
604
- }
605
- )
606
-
607
- result
608
- end
609
-
610
- private
611
-
612
- def self.build_agent
613
- rules = JSON.parse(File.read(Rails.root.join("config/rules/issue_triage.json")))
614
-
615
- DecisionAgent::Agent.new(
616
- evaluators: [
617
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
618
- ],
619
- scoring_strategy: DecisionAgent::Scoring::WeightedAverage.new,
620
- audit_adapter: DecisionAgent::Audit::LoggerAdapter.new(logger: Rails.logger)
621
- )
622
- end
623
- end
624
- ```
625
-
626
- ### Redmine Plugin Integration
627
-
628
- ```ruby
629
- # plugins/redmine_smart_triage/lib/decision_engine.rb
630
- module RedmineSmartTriage
631
- class DecisionEngine
632
- def self.evaluate_issue(issue)
633
- agent = build_agent
634
-
635
- context = {
636
- "priority" => issue.priority.name.downcase,
637
- "status" => issue.status.name.downcase,
638
- "hours_inactive" => hours_since_update(issue),
639
- "assignee" => issue.assigned_to&.login,
640
- "tracker" => issue.tracker.name.downcase
641
- }
642
-
643
- agent.decide(context: context)
644
- end
645
-
646
- private
647
-
648
- def self.build_agent
649
- rules_path = File.join(File.dirname(__FILE__), "../config/triage_rules.json")
650
- rules = JSON.parse(File.read(rules_path))
651
-
652
- DecisionAgent::Agent.new(
653
- evaluators: [
654
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
655
- ],
656
- audit_adapter: RedmineAuditAdapter.new
657
- )
658
- end
659
-
660
- def self.hours_since_update(issue)
661
- ((Time.now - issue.updated_on) / 3600).round
662
- end
663
- end
664
-
665
- class RedmineAuditAdapter < DecisionAgent::Audit::Adapter
666
- def record(decision, context)
667
- # Store in Redmine custom field or separate table
668
- Rails.logger.info "[DecisionAgent] #{decision.decision} (confidence: #{decision.confidence})"
669
- end
670
- end
671
- end
672
- ```
673
-
674
- ### Standalone Service
675
-
676
- ```ruby
677
- #!/usr/bin/env ruby
678
- require 'decision_agent'
679
- require 'json'
680
-
681
- # Load rules
682
- rules = JSON.parse(File.read("config/rules.json"))
683
-
684
- # Build agent
685
- agent = DecisionAgent::Agent.new(
686
- evaluators: [
687
- DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
688
- ],
689
- scoring_strategy: DecisionAgent::Scoring::Threshold.new(
690
- threshold: 0.75,
691
- fallback_decision: "manual_review"
692
- ),
139
+ scoring_strategy: DecisionAgent::Scoring::Consensus.new(minimum_agreement: 0.7),
693
140
  audit_adapter: DecisionAgent::Audit::LoggerAdapter.new
694
141
  )
695
142
 
696
- # Read context from stdin
697
- context = JSON.parse(STDIN.read)
698
-
699
- # Decide
700
- result = agent.decide(context: context)
701
-
702
- # Output decision
703
- output = {
704
- decision: result.decision,
705
- confidence: result.confidence,
706
- explanations: result.explanations
143
+ # Complex rules with nested conditions
144
+ rules = {
145
+ version: "1.0",
146
+ ruleset: "fraud_detection",
147
+ rules: [{
148
+ id: "suspicious_activity",
149
+ if: {
150
+ all: [
151
+ { field: "amount", op: "gt", value: 10000 },
152
+ { any: [
153
+ { field: "user.country", op: "in", value: ["XX", "YY"] },
154
+ { field: "velocity", op: "gt", value: 5 }
155
+ ]}
156
+ ]
157
+ },
158
+ then: { decision: "flag_for_review", weight: 0.95, reason: "Suspicious patterns detected" }
159
+ }]
707
160
  }
708
-
709
- puts JSON.pretty_generate(output)
710
- ```
711
-
712
- ## Design Philosophy
713
-
714
- ### Why Deterministic > AI
715
-
716
- 1. **Regulatory Compliance**: Healthcare (HIPAA), finance (SOX), and government require auditable, explainable decisions
717
- 2. **Cost**: Rules are free to evaluate; LLM calls cost money and add latency
718
- 3. **Reliability**: Same input must produce same output for testing and legal defensibility
719
- 4. **Transparency**: Business rules are explicit and reviewable by domain experts
720
- 5. **AI Enhancement**: AI can suggest rule adjustments, but rules make final decisions
721
-
722
- ### When to Use DecisionAgent
723
-
724
- - **Regulated domains**: Healthcare, finance, legal, government
725
- - **Business rule engines**: Complex decision trees with multiple evaluators
726
- - **Compliance requirements**: Need full audit trails and decision replay
727
- - **Explainability required**: Humans must understand why decisions were made
728
- - **Deterministic systems**: Same input must always produce same output
729
-
730
- ### When NOT to Use
731
-
732
- - Simple if/else logic (just use Ruby)
733
- - Purely AI-driven decisions with no rules
734
- - Single-step validations (use standard validation libraries)
735
-
736
- ## Testing
737
-
738
- ```ruby
739
- # spec/my_decision_spec.rb
740
- RSpec.describe "My Decision Logic" do
741
- it "escalates critical issues" do
742
- rules = { ... }
743
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
744
- agent = DecisionAgent::Agent.new(evaluators: [evaluator])
745
-
746
- result = agent.decide(
747
- context: { priority: "critical", hours_inactive: 3 }
748
- )
749
-
750
- expect(result.decision).to eq("escalate")
751
- expect(result.confidence).to be > 0.8
752
- end
753
- end
754
- ```
755
-
756
- ## Error Handling
757
-
758
- All errors are namespaced under `DecisionAgent`:
759
-
760
- ### NoEvaluationsError
761
-
762
- Raised when no evaluator returns a decision (all returned `nil` or raised exceptions).
763
-
764
- ```ruby
765
- begin
766
- agent.decide(context: {})
767
- rescue DecisionAgent::NoEvaluationsError => e
768
- # No evaluator returned a decision
769
- puts e.message # => "No evaluators returned a decision"
770
-
771
- # Handle gracefully
772
- fallback_decision = "manual_review"
773
- end
774
- ```
775
-
776
- ### InvalidRuleDslError
777
-
778
- Raised when JSON rule DSL is malformed or invalid.
779
-
780
- ```ruby
781
- begin
782
- rules = { invalid: "structure" }
783
- evaluator = DecisionAgent::Evaluators::JsonRuleEvaluator.new(rules_json: rules)
784
- rescue DecisionAgent::InvalidRuleDslError => e
785
- # JSON rule DSL is malformed
786
- puts e.message # => "Invalid rule DSL structure"
787
- end
788
161
  ```
789
162
 
790
- ### ReplayMismatchError
163
+ See [examples/](examples/) for complete working examples.
791
164
 
792
- Raised in strict replay mode when replayed decision differs from original.
165
+ ## When to Use DecisionAgent
793
166
 
794
- ```ruby
795
- begin
796
- replayed_result = DecisionAgent::Replay.run(audit_payload, strict: true)
797
- rescue DecisionAgent::ReplayMismatchError => e
798
- # Replay produced different result
799
- puts "Expected: #{e.expected}" # => "approve"
800
- puts "Actual: #{e.actual}" # => "reject"
801
- puts "Differences: #{e.differences}" # => ["decision changed", "confidence changed"]
802
- end
803
- ```
804
-
805
- ### InvalidConfidenceError
806
-
807
- Raised when confidence value is outside [0.0, 1.0] range.
808
-
809
- ```ruby
810
- begin
811
- decision = DecisionAgent::Decision.new(
812
- decision: "approve",
813
- confidence: 1.5, # Invalid!
814
- explanations: [],
815
- evaluations: [],
816
- audit_payload: {}
817
- )
818
- rescue DecisionAgent::InvalidConfidenceError => e
819
- puts e.message # => "Confidence must be between 0.0 and 1.0, got: 1.5"
820
- end
821
- ```
822
-
823
- ### InvalidWeightError
824
-
825
- Raised when evaluation weight is outside [0.0, 1.0] range.
826
-
827
- ```ruby
828
- begin
829
- eval = DecisionAgent::Evaluation.new(
830
- decision: "approve",
831
- weight: -0.5, # Invalid!
832
- reason: "Test",
833
- evaluator_name: "Test"
834
- )
835
- rescue DecisionAgent::InvalidWeightError => e
836
- puts e.message # => "Weight must be between 0.0 and 1.0, got: -0.5"
837
- end
838
- ```
839
-
840
- ### Configuration Errors
841
-
842
- Raised during agent initialization when configuration is invalid.
843
-
844
- ```ruby
845
- begin
846
- # No evaluators provided
847
- agent = DecisionAgent::Agent.new(evaluators: [])
848
- rescue DecisionAgent::InvalidConfigurationError => e
849
- puts e.message # => "At least one evaluator is required"
850
- end
851
-
852
- begin
853
- # Invalid evaluator
854
- agent = DecisionAgent::Agent.new(evaluators: ["not an evaluator"])
855
- rescue DecisionAgent::InvalidEvaluatorError => e
856
- puts e.message # => "Evaluator must respond to #evaluate"
857
- end
858
- ```
859
-
860
- ## API Reference
861
-
862
- ### Agent
863
-
864
- Main orchestrator for decision-making.
865
-
866
- **Constructor:**
867
- ```ruby
868
- DecisionAgent::Agent.new(
869
- evaluators: [evaluator1, evaluator2],
870
- scoring_strategy: DecisionAgent::Scoring::WeightedAverage.new, # Optional, defaults to WeightedAverage
871
- audit_adapter: DecisionAgent::Audit::NullAdapter.new # Optional, defaults to NullAdapter
872
- )
873
- ```
874
-
875
- **Public Methods:**
876
-
877
- - `#decide(context:, feedback: {})` → `Decision`
878
- - Makes a decision based on context and optional feedback
879
- - Raises `NoEvaluationsError` if no evaluators return decisions
880
- - Returns a `Decision` object with decision, confidence, and explanations
881
-
882
- **Attributes:**
883
- - `#evaluators` → `Array` - Read-only access to configured evaluators
884
- - `#scoring_strategy` → `Scoring::Base` - Read-only access to scoring strategy
885
- - `#audit_adapter` → `Audit::Adapter` - Read-only access to audit adapter
886
-
887
- ### Decision
888
-
889
- Immutable result object representing a decision.
890
-
891
- **Constructor:**
892
- ```ruby
893
- DecisionAgent::Decision.new(
894
- decision: "approve",
895
- confidence: 0.85,
896
- explanations: ["High priority rule matched"],
897
- evaluations: [evaluation1, evaluation2],
898
- audit_payload: {...}
899
- )
900
- ```
901
-
902
- **Attributes:**
903
- - `#decision` → `String` - The final decision (frozen)
904
- - `#confidence` → `Float` - Confidence score between 0.0 and 1.0
905
- - `#explanations` → `Array<String>` - Human-readable explanations (frozen)
906
- - `#evaluations` → `Array<Evaluation>` - All evaluations that contributed (frozen)
907
- - `#audit_payload` → `Hash` - Complete audit trail for replay (frozen)
908
-
909
- **Public Methods:**
910
-
911
- - `#to_h` → `Hash` - Converts to hash representation
912
- - `#==(other)` → `Boolean` - Equality comparison (compares decision, confidence, explanations, evaluations)
913
-
914
- ### Evaluation
915
-
916
- Immutable result from a single evaluator.
917
-
918
- **Constructor:**
919
- ```ruby
920
- DecisionAgent::Evaluation.new(
921
- decision: "approve",
922
- weight: 0.8,
923
- reason: "User meets criteria",
924
- evaluator_name: "MyEvaluator",
925
- metadata: { rule_id: "R1" } # Optional, defaults to {}
926
- )
927
- ```
928
-
929
- **Attributes:**
930
- - `#decision` → `String` - The evaluator's decision (frozen)
931
- - `#weight` → `Float` - Weight between 0.0 and 1.0
932
- - `#reason` → `String` - Human-readable reason (frozen)
933
- - `#evaluator_name` → `String` - Name of the evaluator (frozen)
934
- - `#metadata` → `Hash` - Additional context (frozen)
935
-
936
- **Public Methods:**
937
-
938
- - `#to_h` → `Hash` - Converts to hash representation
939
- - `#==(other)` → `Boolean` - Equality comparison
940
-
941
- ### Context
942
-
943
- Immutable wrapper for decision context data.
944
-
945
- **Constructor:**
946
- ```ruby
947
- DecisionAgent::Context.new(
948
- user: "alice",
949
- priority: "high",
950
- nested: { role: "admin" }
951
- )
952
- ```
953
-
954
- **Public Methods:**
955
-
956
- - `#[]` → `Object` - Access context value by key (supports both string and symbol keys)
957
- - `#to_h` → `Hash` - Returns underlying hash (frozen)
958
- - `#==(other)` → `Boolean` - Equality comparison
959
-
960
- ### Evaluators::Base
961
-
962
- Base class for custom evaluators.
963
-
964
- **Public Methods:**
965
-
966
- - `#evaluate(context, feedback: {})` → `Evaluation | nil`
967
- - Must be implemented by subclasses
968
- - Returns `Evaluation` if a decision is made, `nil` otherwise
969
- - `context` is a `Context` object
970
- - `feedback` is an optional hash
971
-
972
- ### Scoring::Base
973
-
974
- Base class for custom scoring strategies.
975
-
976
- **Public Methods:**
977
-
978
- - `#score(evaluations)` → `{ decision: String, confidence: Float }`
979
- - Must be implemented by subclasses
980
- - Takes array of `Evaluation` objects
981
- - Returns hash with `:decision` and `:confidence` keys
982
- - Confidence must be between 0.0 and 1.0
983
-
984
- **Protected Methods:**
985
- - `#normalize_confidence(value)` → `Float` - Clamps value to [0.0, 1.0]
986
- - `#round_confidence(value)` → `Float` - Rounds to 4 decimal places
167
+ ✅ **Perfect for:**
168
+ - Regulated industries (healthcare, finance, legal)
169
+ - Complex business rule engines
170
+ - Audit trail requirements
171
+ - Explainable AI systems
172
+ - Multi-step decision workflows
987
173
 
988
- ### Audit::Adapter
989
-
990
- Base class for custom audit adapters.
991
-
992
- **Public Methods:**
993
-
994
- - `#record(decision, context)` → `void`
995
- - Must be implemented by subclasses
996
- - Called after each decision is made
997
- - `decision` is a `Decision` object
998
- - `context` is a `Context` object
999
-
1000
- ### Replay
1001
-
1002
- Utilities for replaying historical decisions.
1003
-
1004
- **Class Methods:**
1005
-
1006
- - `DecisionAgent::Replay.run(audit_payload, strict: true)` → `Decision`
1007
- - Replays a decision from audit payload
1008
- - `strict: true` raises `ReplayMismatchError` on differences
1009
- - `strict: false` logs differences but allows evolution
1010
-
1011
- ## Versioning
1012
-
1013
- DecisionAgent follows [Semantic Versioning 2.0.0](https://semver.org/):
1014
-
1015
- - **MAJOR** version for incompatible API changes
1016
- - **MINOR** version for backwards-compatible functionality additions
1017
- - **PATCH** version for backwards-compatible bug fixes
1018
-
1019
- ### Stability Guarantees
1020
-
1021
- - **Public API**: All classes and methods documented in this README are stable
1022
- - **Audit Payload Format**: The structure of `audit_payload` is stable and will remain replayable across versions
1023
- - **Deterministic Hash**: The algorithm for computing `deterministic_hash` is frozen to ensure replay compatibility
1024
- - **Breaking Changes**: Will only occur in major version bumps, with clear migration guides
1025
-
1026
- ### Deprecation Policy
1027
-
1028
- - Deprecated features will be marked in documentation and emit warnings
1029
- - Deprecated features will be maintained for at least one minor version before removal
1030
- - Breaking changes will be documented in CHANGELOG.md with migration instructions
174
+ **Not suitable for:**
175
+ - Simple if/else logic (use plain Ruby)
176
+ - Pure AI/ML with no rules
177
+ - Single-step validations
1031
178
 
1032
179
  ## Contributing
1033
180
 
1034
181
  1. Fork the repository
1035
182
  2. Create a feature branch
1036
183
  3. Add tests (maintain 90%+ coverage)
1037
- 4. Ensure all tests pass: `rspec`
1038
- 5. Submit a pull request
1039
-
1040
- ## License
184
+ 4. Submit a pull request
1041
185
 
1042
- MIT License. See [LICENSE.txt](LICENSE.txt).
1043
-
1044
- ## Roadmap
186
+ ## Support
1045
187
 
1046
- - [x] Rule validation CLI ✓
1047
- - [x] Web UI for rule editing ✓
1048
- - [ ] Performance benchmarks
1049
- - [ ] Prometheus metrics adapter
1050
- - [ ] Additional scoring strategies (Bayesian, etc.)
1051
- - [ ] AI evaluator adapter (optional, non-deterministic mode)
188
+ - **Issues**: [GitHub Issues](https://github.com/samaswin87/decision_agent/issues)
189
+ - **Documentation**: [Wiki](wiki/README.md)
190
+ - **Examples**: [examples/](examples/)
1052
191
 
1053
- ## Support
192
+ ## License
1054
193
 
1055
- - GitHub Issues: [https://github.com/samaswin87/decision_agent/issues](https://github.com/samaswin87/decision_agent/issues)
1056
- - Documentation: [https://github.com/samaswin87/decision_agent](https://github.com/samaswin87/decision_agent)
194
+ MIT License - see [LICENSE.txt](LICENSE.txt)
1057
195
 
1058
196
  ---
1059
197
 
1060
- **Built for regulated domains. Deterministic by design. AI-optional.**
198
+ **Star this repo** if you find it useful!