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.
- checksums.yaml +4 -4
- data/README.md +138 -1000
- data/bin/decision_agent +5 -0
- data/lib/decision_agent/errors.rb +12 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +105 -0
- data/lib/decision_agent/versioning/adapter.rb +102 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +182 -0
- data/lib/decision_agent/versioning/version_manager.rb +135 -0
- data/lib/decision_agent/web/public/app.js +318 -0
- data/lib/decision_agent/web/public/index.html +55 -0
- data/lib/decision_agent/web/public/styles.css +219 -0
- data/lib/decision_agent/web/server.rb +166 -1
- data/lib/decision_agent.rb +4 -0
- data/lib/generators/decision_agent/install/install_generator.rb +40 -0
- data/lib/generators/decision_agent/install/templates/README +47 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +26 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +60 -0
- data/spec/versioning_spec.rb +673 -0
- metadata +17 -7
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
|
-
|
|
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
|
-
|
|
12
|
+
## Why DecisionAgent?
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
49
|
+
# Create decision agent
|
|
50
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
210
51
|
|
|
211
|
-
|
|
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
|
-
#
|
|
55
|
+
puts result.decision # => "approve"
|
|
56
|
+
puts result.confidence # => 0.9
|
|
57
|
+
puts result.explanations # => ["High value transaction"]
|
|
352
58
|
```
|
|
353
59
|
|
|
354
|
-
|
|
60
|
+
## Web UI - Visual Rule Builder
|
|
355
61
|
|
|
356
|
-
|
|
62
|
+
Launch the visual rule builder for non-technical users:
|
|
357
63
|
|
|
358
|
-
```
|
|
359
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
#
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
163
|
+
See [examples/](examples/) for complete working examples.
|
|
791
164
|
|
|
792
|
-
|
|
165
|
+
## When to Use DecisionAgent
|
|
793
166
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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.
|
|
1038
|
-
5. Submit a pull request
|
|
1039
|
-
|
|
1040
|
-
## License
|
|
184
|
+
4. Submit a pull request
|
|
1041
185
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
## Roadmap
|
|
186
|
+
## Support
|
|
1045
187
|
|
|
1046
|
-
- [
|
|
1047
|
-
- [
|
|
1048
|
-
- [
|
|
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
|
-
##
|
|
192
|
+
## License
|
|
1054
193
|
|
|
1055
|
-
-
|
|
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
|
-
**
|
|
198
|
+
⭐ **Star this repo** if you find it useful!
|