kbs 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
data/docs/api/rules.md
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
# Rules API Reference
|
|
2
|
+
|
|
3
|
+
Complete API reference for rule classes in KBS.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [KBS::Rule](#kbsrule) - Production rule with conditions and action
|
|
8
|
+
- [Rule Lifecycle](#rule-lifecycle)
|
|
9
|
+
- [Rule Patterns](#rule-patterns)
|
|
10
|
+
- [Best Practices](#best-practices)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## KBS::Rule
|
|
15
|
+
|
|
16
|
+
A production rule that fires when all conditions match.
|
|
17
|
+
|
|
18
|
+
**Structure**: A rule consists of:
|
|
19
|
+
1. **Name** - Unique identifier
|
|
20
|
+
2. **Priority** - Execution order (higher = more urgent)
|
|
21
|
+
3. **Conditions** - Array of patterns to match
|
|
22
|
+
4. **Action** - Lambda executed when all conditions match
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### Constructor
|
|
27
|
+
|
|
28
|
+
#### `initialize(name, conditions: [], action: nil, priority: 0, &block)`
|
|
29
|
+
|
|
30
|
+
Creates a new rule.
|
|
31
|
+
|
|
32
|
+
**Parameters**:
|
|
33
|
+
- `name` (Symbol or String) - Unique rule identifier
|
|
34
|
+
- `conditions` (Array<KBS::Condition>, optional) - Conditions to match (default: `[]`)
|
|
35
|
+
- `action` (Proc, optional) - Action lambda to execute (default: `nil`)
|
|
36
|
+
- `priority` (Integer, optional) - Rule priority (default: `0`)
|
|
37
|
+
- `&block` (Block, optional) - Configuration block yielding self
|
|
38
|
+
|
|
39
|
+
**Returns**: `KBS::Rule` instance
|
|
40
|
+
|
|
41
|
+
**Example - Direct Construction**:
|
|
42
|
+
```ruby
|
|
43
|
+
# Minimal rule
|
|
44
|
+
rule = KBS::Rule.new(:high_temperature)
|
|
45
|
+
|
|
46
|
+
# Rule with all parameters
|
|
47
|
+
rule = KBS::Rule.new(
|
|
48
|
+
:high_temperature,
|
|
49
|
+
conditions: [
|
|
50
|
+
KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
51
|
+
],
|
|
52
|
+
action: ->(facts) { puts "High temperature detected!" },
|
|
53
|
+
priority: 10
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Example - Block Configuration**:
|
|
58
|
+
```ruby
|
|
59
|
+
rule = KBS::Rule.new(:high_temperature) do |r|
|
|
60
|
+
r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
61
|
+
r.action = ->(facts) { puts "High temperature: #{facts[0][:value]}" }
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Example - Using DSL** (recommended):
|
|
66
|
+
```ruby
|
|
67
|
+
kb = KBS.knowledge_base do
|
|
68
|
+
rule "high_temperature", priority: 10 do
|
|
69
|
+
on :temperature, value: greater_than(80)
|
|
70
|
+
perform do |bindings|
|
|
71
|
+
puts "High temperature: #{bindings[:value?]}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Add to engine
|
|
77
|
+
kb.rules.each { |r| engine.add_rule(r) }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Public Attributes
|
|
83
|
+
|
|
84
|
+
#### `name`
|
|
85
|
+
|
|
86
|
+
**Type**: `Symbol` or `String`
|
|
87
|
+
|
|
88
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
89
|
+
|
|
90
|
+
**Description**: Unique rule identifier
|
|
91
|
+
|
|
92
|
+
**Example**:
|
|
93
|
+
```ruby
|
|
94
|
+
rule = KBS::Rule.new(:high_temperature, priority: 10)
|
|
95
|
+
puts rule.name # => :high_temperature
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Best Practice**: Use descriptive names that indicate the rule's purpose:
|
|
99
|
+
```ruby
|
|
100
|
+
# Good
|
|
101
|
+
:high_temperature_alert
|
|
102
|
+
:low_inventory_reorder
|
|
103
|
+
:fraud_detection_high_risk
|
|
104
|
+
|
|
105
|
+
# Less clear
|
|
106
|
+
:rule1
|
|
107
|
+
:temp_rule
|
|
108
|
+
:check
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
#### `priority`
|
|
114
|
+
|
|
115
|
+
**Type**: `Integer`
|
|
116
|
+
|
|
117
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
118
|
+
|
|
119
|
+
**Description**: Rule priority (higher = executes first in KBS::Blackboard::Engine)
|
|
120
|
+
|
|
121
|
+
**Default**: `0`
|
|
122
|
+
|
|
123
|
+
**Range**: Any integer (commonly 0-100)
|
|
124
|
+
|
|
125
|
+
**Example**:
|
|
126
|
+
```ruby
|
|
127
|
+
rule = KBS::Rule.new(:critical_alert, priority: 100)
|
|
128
|
+
puts rule.priority # => 100
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Priority Semantics**:
|
|
132
|
+
- **KBS::Engine**: Priority is stored but NOT used for execution order (rules fire in arbitrary order)
|
|
133
|
+
- **KBS::Blackboard::Engine**: Higher priority rules fire first within production nodes
|
|
134
|
+
|
|
135
|
+
**Common Priority Ranges**:
|
|
136
|
+
```ruby
|
|
137
|
+
# Critical safety rules
|
|
138
|
+
priority: 100
|
|
139
|
+
|
|
140
|
+
# Important business rules
|
|
141
|
+
priority: 50
|
|
142
|
+
|
|
143
|
+
# Standard rules
|
|
144
|
+
priority: 10
|
|
145
|
+
|
|
146
|
+
# Cleanup/logging rules
|
|
147
|
+
priority: 0
|
|
148
|
+
|
|
149
|
+
# Background tasks
|
|
150
|
+
priority: -10
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Example - Priority Ordering**:
|
|
154
|
+
```ruby
|
|
155
|
+
kb = KBS.knowledge_base do
|
|
156
|
+
rule "log_temperature", priority: 0 do
|
|
157
|
+
on :temperature, value: :temp?
|
|
158
|
+
perform { |b| puts "Logged: #{b[:temp?]}" }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
rule "critical_alert", priority: 100 do
|
|
162
|
+
on :temperature, value: greater_than(100)
|
|
163
|
+
perform { puts "CRITICAL TEMPERATURE!" }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
rule "high_alert", priority: 50 do
|
|
167
|
+
on :temperature, value: greater_than(80)
|
|
168
|
+
perform { puts "High temperature warning" }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
engine = KBS::Blackboard::Engine.new
|
|
173
|
+
kb.rules.each { |r| engine.add_rule(r) }
|
|
174
|
+
engine.add_fact(:temperature, value: 110)
|
|
175
|
+
engine.run
|
|
176
|
+
|
|
177
|
+
# Output (in priority order):
|
|
178
|
+
# CRITICAL TEMPERATURE! (priority 100)
|
|
179
|
+
# High temperature warning (priority 50)
|
|
180
|
+
# Logged: 110 (priority 0)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
#### `conditions`
|
|
186
|
+
|
|
187
|
+
**Type**: `Array<KBS::Condition>`
|
|
188
|
+
|
|
189
|
+
**Read/Write**: Yes (via `attr_accessor`)
|
|
190
|
+
|
|
191
|
+
**Description**: Array of conditions that must all match for rule to fire
|
|
192
|
+
|
|
193
|
+
**Example**:
|
|
194
|
+
```ruby
|
|
195
|
+
rule = KBS::Rule.new(:temperature_alert)
|
|
196
|
+
rule.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
197
|
+
rule.conditions << KBS::Condition.new(:sensor, status: "active")
|
|
198
|
+
|
|
199
|
+
puts rule.conditions.size # => 2
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Condition Order Matters** (for performance):
|
|
203
|
+
```ruby
|
|
204
|
+
# Good - Most selective condition first
|
|
205
|
+
rule.conditions = [
|
|
206
|
+
KBS::Condition.new(:sensor, id: 42), # Filters to 1 fact
|
|
207
|
+
KBS::Condition.new(:temperature, value: :temp?) # Then match temperature
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
# Less optimal - Less selective first
|
|
211
|
+
rule.conditions = [
|
|
212
|
+
KBS::Condition.new(:temperature, value: :temp?), # Matches many facts
|
|
213
|
+
KBS::Condition.new(:sensor, id: 42) # Could have filtered first
|
|
214
|
+
]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
See [Performance Guide](../advanced/performance.md) for condition ordering strategies.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
#### `action`
|
|
222
|
+
|
|
223
|
+
**Type**: `Proc` (lambda or proc)
|
|
224
|
+
|
|
225
|
+
**Read/Write**: Yes (via `attr_accessor`)
|
|
226
|
+
|
|
227
|
+
**Description**: Lambda executed when all conditions match
|
|
228
|
+
|
|
229
|
+
**Signature**: `action.call(facts)` or `action.call(facts, bindings)` (both supported)
|
|
230
|
+
|
|
231
|
+
**Parameters**:
|
|
232
|
+
- `facts` (Array<KBS::Fact>) - Array of matched facts (parallel to conditions array)
|
|
233
|
+
- `bindings` (Hash, optional) - Variable bindings extracted from facts
|
|
234
|
+
|
|
235
|
+
**Example - Facts Parameter**:
|
|
236
|
+
```ruby
|
|
237
|
+
rule.action = ->(facts) do
|
|
238
|
+
temp_fact = facts[0] # First condition's matched fact
|
|
239
|
+
sensor_fact = facts[1] # Second condition's matched fact
|
|
240
|
+
|
|
241
|
+
puts "Temperature: #{temp_fact[:value]} from sensor #{sensor_fact[:id]}"
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Example - Bindings Parameter**:
|
|
246
|
+
```ruby
|
|
247
|
+
# Rule with variable bindings
|
|
248
|
+
rule = KBS::Rule.new(:temperature_alert) do |r|
|
|
249
|
+
r.conditions << KBS::Condition.new(:temperature, value: :temp?, location: :loc?)
|
|
250
|
+
r.action = ->(facts, bindings) do
|
|
251
|
+
# bindings: {:temp? => 85, :loc? => "server_room"}
|
|
252
|
+
puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Example - DSL Preferred**:
|
|
258
|
+
```ruby
|
|
259
|
+
rule "temperature_alert" do
|
|
260
|
+
on :temperature, value: :temp?, location: :loc?
|
|
261
|
+
perform do |bindings|
|
|
262
|
+
# Cleaner - DSL automatically provides bindings
|
|
263
|
+
puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Action Requirements**:
|
|
269
|
+
- Must be a Proc (lambda or proc)
|
|
270
|
+
- Should be idempotent if possible (safe to run multiple times)
|
|
271
|
+
- Should not modify facts directly (use `engine.add_fact` / `engine.remove_fact` instead)
|
|
272
|
+
- May add/remove facts (triggers new rule evaluation)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### Public Methods
|
|
277
|
+
|
|
278
|
+
#### `fire(facts)`
|
|
279
|
+
|
|
280
|
+
Executes the rule's action with matched facts.
|
|
281
|
+
|
|
282
|
+
**Parameters**:
|
|
283
|
+
- `facts` (Array<KBS::Fact>) - Matched facts (one per condition)
|
|
284
|
+
|
|
285
|
+
**Returns**: Result of action lambda, or `nil` if no action
|
|
286
|
+
|
|
287
|
+
**Side Effects**:
|
|
288
|
+
- Increments internal `@fired_count`
|
|
289
|
+
- Executes action lambda
|
|
290
|
+
- Action may modify external state, add/remove facts, etc.
|
|
291
|
+
|
|
292
|
+
**Example**:
|
|
293
|
+
```ruby
|
|
294
|
+
rule = KBS::Rule.new(:log_temperature) do |r|
|
|
295
|
+
r.conditions << KBS::Condition.new(:temperature, value: :temp?)
|
|
296
|
+
r.action = ->(facts, bindings) do
|
|
297
|
+
puts "Temperature: #{bindings[:temp?]}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
302
|
+
rule.fire([fact])
|
|
303
|
+
# Output: Temperature: 85
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Note**: Typically called by the RETE engine, not user code. Users call `engine.run` which fires all activated rules.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Rule Lifecycle
|
|
311
|
+
|
|
312
|
+
### 1. Rule Creation
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
# Via DSL (recommended)
|
|
316
|
+
kb = KBS.knowledge_base do
|
|
317
|
+
rule "my_rule", priority: 10 do
|
|
318
|
+
on :temperature, value: :temp?
|
|
319
|
+
perform { |b| puts b[:temp?] }
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Or programmatically
|
|
324
|
+
rule = KBS::Rule.new(
|
|
325
|
+
:my_rule,
|
|
326
|
+
conditions: [KBS::Condition.new(:temperature, value: :temp?)],
|
|
327
|
+
action: ->(facts) { puts facts[0][:value] },
|
|
328
|
+
priority: 10
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### 2. Rule Registration
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
engine.add_rule(rule)
|
|
338
|
+
# Internally:
|
|
339
|
+
# - Adds rule to @rules array
|
|
340
|
+
# - Compiles rule into RETE network
|
|
341
|
+
# - Creates alpha memories for condition patterns
|
|
342
|
+
# - Creates join nodes (or negation nodes)
|
|
343
|
+
# - Creates production node for rule
|
|
344
|
+
# - Activates existing facts through new network
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
### 3. Rule Activation
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
engine.add_fact(:temperature, value: 85)
|
|
353
|
+
# Internally:
|
|
354
|
+
# - Fact activates matching alpha memories
|
|
355
|
+
# - Propagates through join nodes
|
|
356
|
+
# - Creates tokens in beta memories
|
|
357
|
+
# - Token reaches production node
|
|
358
|
+
# - Rule is "activated" (ready to fire)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### 4. Rule Firing
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
engine.run
|
|
367
|
+
# Internally (KBS::Engine):
|
|
368
|
+
# - Iterates production nodes
|
|
369
|
+
# - For each token in production node:
|
|
370
|
+
# - Calls rule.fire(token.facts)
|
|
371
|
+
# - Executes action lambda
|
|
372
|
+
|
|
373
|
+
# Internally (KBS::Blackboard::Engine):
|
|
374
|
+
# - Same as above, but:
|
|
375
|
+
# - Logs rule firing to audit trail
|
|
376
|
+
# - Marks token as fired (prevents duplicate firing)
|
|
377
|
+
# - Records variable bindings
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
### 5. Rule Re-firing
|
|
383
|
+
|
|
384
|
+
Rules can fire multiple times:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
rule "log_temperature" do
|
|
388
|
+
on :temperature, value: :temp?
|
|
389
|
+
perform { |b| puts "Temperature: #{b[:temp?]}" }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
engine.add_fact(:temperature, value: 85)
|
|
393
|
+
engine.add_fact(:temperature, value: 90)
|
|
394
|
+
engine.add_fact(:temperature, value: 95)
|
|
395
|
+
engine.run
|
|
396
|
+
|
|
397
|
+
# Output:
|
|
398
|
+
# Temperature: 85
|
|
399
|
+
# Temperature: 90
|
|
400
|
+
# Temperature: 95
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Each fact creates a separate activation (token) that fires independently.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Rule Patterns
|
|
408
|
+
|
|
409
|
+
### 1. Simple Rule (One Condition)
|
|
410
|
+
|
|
411
|
+
Match single fact type:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
rule "log_all_temperatures" do
|
|
415
|
+
on :temperature, value: :temp?
|
|
416
|
+
perform do |bindings|
|
|
417
|
+
puts "Temperature: #{bindings[:temp?]}"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### 2. Join Rule (Multiple Conditions)
|
|
425
|
+
|
|
426
|
+
Match multiple related facts:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
rule "sensor_temperature_alert" do
|
|
430
|
+
on :sensor, id: :sensor_id?, status: "active"
|
|
431
|
+
on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
|
|
432
|
+
perform do |bindings|
|
|
433
|
+
puts "Sensor #{bindings[:sensor_id?]} reports high temperature"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Matches when:
|
|
438
|
+
# - sensor fact with id=42, status="active" exists
|
|
439
|
+
# - temperature fact with sensor_id=42, value > 80 exists
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Variable Binding**: `:sensor_id?` in first condition must equal `sensor_id` in second condition (join test).
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
### 3. Guard Rule (Negation)
|
|
447
|
+
|
|
448
|
+
Match when fact is absent:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
rule "all_clear" do
|
|
452
|
+
on :system, status: "running"
|
|
453
|
+
negated :alert, level: "critical" # Fire when NO critical alerts exist
|
|
454
|
+
perform do
|
|
455
|
+
puts "All systems normal"
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
### 4. State Machine Rule
|
|
463
|
+
|
|
464
|
+
Rules can implement state transitions:
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
rule "pending_to_processing" do
|
|
468
|
+
on :order, id: :order_id?, status: "pending"
|
|
469
|
+
on :worker, status: "available", id: :worker_id?
|
|
470
|
+
perform do |bindings|
|
|
471
|
+
# Transition order to processing
|
|
472
|
+
order = find_order(bindings[:order_id?])
|
|
473
|
+
order.update(status: "processing", worker_id: bindings[:worker_id?])
|
|
474
|
+
|
|
475
|
+
# Update worker
|
|
476
|
+
worker = find_worker(bindings[:worker_id?])
|
|
477
|
+
worker.update(status: "busy")
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
### 5. Cleanup Rule
|
|
485
|
+
|
|
486
|
+
Low-priority rules that clean up old facts:
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
rule "expire_old_temperatures", priority: 0 do
|
|
490
|
+
on :temperature, timestamp: less_than(Time.now - 3600)
|
|
491
|
+
perform do |bindings|
|
|
492
|
+
fact = bindings[:matched_fact?]
|
|
493
|
+
fact.retract # Remove old temperature reading
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### 6. Aggregation Rule
|
|
501
|
+
|
|
502
|
+
Collect multiple facts and compute aggregate:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
rule "daily_temperature_summary", priority: 5 do
|
|
506
|
+
on :trigger, event: "end_of_day"
|
|
507
|
+
perform do
|
|
508
|
+
temps = engine.working_memory.facts
|
|
509
|
+
.select { |f| f.type == :temperature }
|
|
510
|
+
.map { |f| f[:value] }
|
|
511
|
+
|
|
512
|
+
avg = temps.sum / temps.size.to_f
|
|
513
|
+
max = temps.max
|
|
514
|
+
min = temps.min
|
|
515
|
+
|
|
516
|
+
engine.add_fact(:daily_summary, avg: avg, max: max, min: min, date: Date.today)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
### 7. Conflict Resolution Rule
|
|
524
|
+
|
|
525
|
+
Higher priority rule overrides lower priority:
|
|
526
|
+
|
|
527
|
+
```ruby
|
|
528
|
+
rule "high_risk_order", priority: 100 do
|
|
529
|
+
on :order, id: :order_id?, total: greater_than(10000)
|
|
530
|
+
perform do |bindings|
|
|
531
|
+
puts "HIGH RISK: Order #{bindings[:order_id?]} requires manual review"
|
|
532
|
+
# This fires first due to priority
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
rule "auto_approve_order", priority: 10 do
|
|
537
|
+
on :order, id: :order_id?, status: "pending"
|
|
538
|
+
perform do |bindings|
|
|
539
|
+
puts "Auto-approving order #{bindings[:order_id?]}"
|
|
540
|
+
# This fires later (if at all)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
### 8. Recursive Rule
|
|
548
|
+
|
|
549
|
+
Rule that adds facts triggering other rules:
|
|
550
|
+
|
|
551
|
+
```ruby
|
|
552
|
+
rule "calculate_fibonacci" do
|
|
553
|
+
on :fib_request, n: :n?
|
|
554
|
+
negated :fib_result, n: :n? # Not already calculated
|
|
555
|
+
perform do |bindings|
|
|
556
|
+
n = bindings[:n?]
|
|
557
|
+
|
|
558
|
+
if n <= 1
|
|
559
|
+
engine.add_fact(:fib_result, n: n, value: n)
|
|
560
|
+
else
|
|
561
|
+
# Request sub-problems
|
|
562
|
+
engine.add_fact(:fib_request, n: n - 1)
|
|
563
|
+
engine.add_fact(:fib_request, n: n - 2)
|
|
564
|
+
|
|
565
|
+
# Wait for sub-results in another rule...
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
rule "combine_fibonacci" do
|
|
571
|
+
on :fib_request, n: :n?
|
|
572
|
+
on :fib_result, n: :n_minus_1?, value: :val1?
|
|
573
|
+
on :fib_result, n: :n_minus_2?, value: :val2?
|
|
574
|
+
# ... (complex join test: ?n_minus_1 == ?n - 1, etc.)
|
|
575
|
+
perform do |bindings|
|
|
576
|
+
result = bindings[:val1?] + bindings[:val2?]
|
|
577
|
+
engine.add_fact(:fib_result, n: bindings[:n?], value: result)
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Best Practices
|
|
585
|
+
|
|
586
|
+
### 1. Descriptive Rule Names
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
# Good
|
|
590
|
+
rule "high_temperature_alert"
|
|
591
|
+
rule "low_inventory_reorder"
|
|
592
|
+
rule "fraud_detection_suspicious_transaction"
|
|
593
|
+
|
|
594
|
+
# Bad
|
|
595
|
+
rule "rule1"
|
|
596
|
+
rule "temp"
|
|
597
|
+
rule "check"
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
### 2. Order Conditions by Selectivity
|
|
603
|
+
|
|
604
|
+
Most selective (fewest matching facts) first:
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
# Good - sensor_id=42 filters to ~1 fact
|
|
608
|
+
rule "sensor_alert" do
|
|
609
|
+
on :sensor, id: 42, status: :status? # Very selective
|
|
610
|
+
on :temperature, sensor_id: 42, value: :temp? # Also selective
|
|
611
|
+
perform { ... }
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Bad - :temperature matches many facts
|
|
615
|
+
rule "sensor_alert" do
|
|
616
|
+
on :temperature, value: :temp? # Matches 1000s of facts
|
|
617
|
+
on :sensor, id: 42, status: :status? # Could have filtered first
|
|
618
|
+
perform { ... }
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Why**: RETE builds network from first to last condition. Fewer intermediate tokens = faster.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### 3. Use Priority for Critical Rules
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
rule "critical_shutdown", priority: 1000 do
|
|
630
|
+
on :temperature, value: greater_than(120)
|
|
631
|
+
perform { shutdown_system! }
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
rule "log_temperature", priority: 0 do
|
|
635
|
+
on :temperature, value: :temp?
|
|
636
|
+
perform { |b| log(b[:temp?]) }
|
|
637
|
+
end
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Critical safety rules should have high priority to fire before less important rules.
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
### 4. Keep Actions Idempotent
|
|
645
|
+
|
|
646
|
+
```ruby
|
|
647
|
+
# Good - Idempotent (safe to run multiple times)
|
|
648
|
+
rule "alert_high_temp" do
|
|
649
|
+
on :temperature, value: greater_than(80)
|
|
650
|
+
perform do |bindings|
|
|
651
|
+
# Check if alert already sent
|
|
652
|
+
unless alert_sent?(bindings[:temp?])
|
|
653
|
+
send_alert(bindings[:temp?])
|
|
654
|
+
mark_alert_sent(bindings[:temp?])
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Bad - Not idempotent (sends duplicate alerts)
|
|
660
|
+
rule "alert_high_temp" do
|
|
661
|
+
on :temperature, value: greater_than(80)
|
|
662
|
+
perform do |bindings|
|
|
663
|
+
send_alert(bindings[:temp?]) # Sends every time rule fires
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
### 5. Avoid Side Effects in Conditions
|
|
671
|
+
|
|
672
|
+
```ruby
|
|
673
|
+
# Bad - Side effect in condition predicate
|
|
674
|
+
counter = 0
|
|
675
|
+
rule "count_temps" do
|
|
676
|
+
on :temperature, value: ->(v) { counter += 1; v > 80 } # BAD!
|
|
677
|
+
perform { puts "Count: #{counter}" }
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Good - Side effects in action only
|
|
681
|
+
counter = 0
|
|
682
|
+
rule "count_temps" do
|
|
683
|
+
on :temperature, value: greater_than(80)
|
|
684
|
+
perform { counter += 1; puts "Count: #{counter}" }
|
|
685
|
+
end
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Why**: Predicates run during pattern matching (potentially multiple times). Side effects cause unpredictable behavior.
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
### 6. Use Variable Bindings for Joins
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
# Good - Variable binding creates join test
|
|
696
|
+
rule "order_inventory_check" do
|
|
697
|
+
on :order, product_id: :pid?, quantity: :qty?
|
|
698
|
+
on :inventory, product_id: :pid?, available: :available?
|
|
699
|
+
perform do |bindings|
|
|
700
|
+
if bindings[:available?] < bindings[:qty?]
|
|
701
|
+
puts "Insufficient inventory for product #{bindings[:pid?]}"
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Bad - No join test (matches all combinations)
|
|
707
|
+
rule "order_inventory_check" do
|
|
708
|
+
on :order, product_id: :pid1?, quantity: :qty?
|
|
709
|
+
on :inventory, product_id: :pid2?, available: :available?
|
|
710
|
+
perform do |bindings|
|
|
711
|
+
# No guarantee pid1 == pid2!
|
|
712
|
+
if bindings[:pid1?] == bindings[:pid2?] # Manual check in action (inefficient)
|
|
713
|
+
...
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
### 7. Document Complex Rules
|
|
722
|
+
|
|
723
|
+
```ruby
|
|
724
|
+
# Good - Documented
|
|
725
|
+
rule "portfolio_rebalancing", priority: 50 do
|
|
726
|
+
# Triggers when portfolio drift exceeds threshold
|
|
727
|
+
# Conditions:
|
|
728
|
+
# 1. Portfolio exists and is active
|
|
729
|
+
# 2. Current allocation deviates > 5% from target
|
|
730
|
+
# Action:
|
|
731
|
+
# - Calculates rebalancing trades
|
|
732
|
+
# - Creates pending orders
|
|
733
|
+
|
|
734
|
+
on :portfolio, id: :portfolio_id?, status: "active"
|
|
735
|
+
on :drift_calculation, portfolio_id: :portfolio_id?, drift: greater_than(0.05)
|
|
736
|
+
perform do |bindings|
|
|
737
|
+
# Implementation...
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
### 8. Test Rules in Isolation
|
|
745
|
+
|
|
746
|
+
```ruby
|
|
747
|
+
require 'minitest/autorun'
|
|
748
|
+
|
|
749
|
+
class TestHighTemperatureRule < Minitest::Test
|
|
750
|
+
def setup
|
|
751
|
+
@engine = KBS::Blackboard::Engine.new
|
|
752
|
+
@fired = false
|
|
753
|
+
|
|
754
|
+
@rule = KBS::Rule.new(:high_temp) do |r|
|
|
755
|
+
r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
756
|
+
r.action = ->(facts) { @fired = true }
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
@engine.add_rule(@rule)
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def test_fires_when_temperature_high
|
|
763
|
+
@engine.add_fact(:temperature, value: 85)
|
|
764
|
+
@engine.run
|
|
765
|
+
assert @fired
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def test_does_not_fire_when_temperature_low
|
|
769
|
+
@engine.add_fact(:temperature, value: 75)
|
|
770
|
+
@engine.run
|
|
771
|
+
refute @fired
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
### 9. Use Negation for Guards
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
# Good - Negation ensures system ready
|
|
782
|
+
rule "start_processing" do
|
|
783
|
+
on :work_item, status: "pending"
|
|
784
|
+
negated :system_error # Don't process if system has errors
|
|
785
|
+
perform { process_work_item }
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Alternative - Check in action (less efficient)
|
|
789
|
+
rule "start_processing" do
|
|
790
|
+
on :work_item, status: "pending"
|
|
791
|
+
perform do
|
|
792
|
+
unless system_has_errors?
|
|
793
|
+
process_work_item
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Why**: Negation in condition prevents token creation. Action-based check still creates token (wastes memory).
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
### 10. Limit Fact Growth
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
# Good - Cleanup rule prevents unbounded growth
|
|
807
|
+
rule "expire_old_facts", priority: 0 do
|
|
808
|
+
on :temperature, timestamp: less_than(Time.now - 3600)
|
|
809
|
+
perform do |bindings|
|
|
810
|
+
fact = bindings[:matched_fact?]
|
|
811
|
+
fact.retract
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Bad - No cleanup (memory leak)
|
|
816
|
+
loop do
|
|
817
|
+
engine.add_fact(:temperature, value: rand(100), timestamp: Time.now)
|
|
818
|
+
engine.run
|
|
819
|
+
sleep 1
|
|
820
|
+
# Facts accumulate forever!
|
|
821
|
+
end
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## Common Patterns Reference
|
|
827
|
+
|
|
828
|
+
### Rule Priority Examples
|
|
829
|
+
|
|
830
|
+
```ruby
|
|
831
|
+
# Emergency shutdown
|
|
832
|
+
priority: 1000
|
|
833
|
+
|
|
834
|
+
# Critical alerts
|
|
835
|
+
priority: 500
|
|
836
|
+
|
|
837
|
+
# Business logic
|
|
838
|
+
priority: 100
|
|
839
|
+
|
|
840
|
+
# Data validation
|
|
841
|
+
priority: 50
|
|
842
|
+
|
|
843
|
+
# Standard processing
|
|
844
|
+
priority: 10
|
|
845
|
+
|
|
846
|
+
# Logging/auditing
|
|
847
|
+
priority: 5
|
|
848
|
+
|
|
849
|
+
# Cleanup
|
|
850
|
+
priority: 0
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
### Action Signatures
|
|
856
|
+
|
|
857
|
+
```ruby
|
|
858
|
+
# 1. Facts only
|
|
859
|
+
action: ->(facts) do
|
|
860
|
+
temp_fact = facts[0]
|
|
861
|
+
puts temp_fact[:value]
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# 2. Facts and bindings (recommended)
|
|
865
|
+
action: ->(facts, bindings) do
|
|
866
|
+
puts bindings[:temp?]
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# 3. DSL style (cleanest)
|
|
870
|
+
perform do |bindings|
|
|
871
|
+
puts bindings[:temp?]
|
|
872
|
+
end
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
### Condition Patterns
|
|
878
|
+
|
|
879
|
+
```ruby
|
|
880
|
+
# Literal matching
|
|
881
|
+
on :temperature, location: "server_room"
|
|
882
|
+
|
|
883
|
+
# Range check
|
|
884
|
+
on :temperature, value: between(70, 90)
|
|
885
|
+
on :temperature, value: greater_than(80)
|
|
886
|
+
on :temperature, value: less_than(100)
|
|
887
|
+
|
|
888
|
+
# Variable binding
|
|
889
|
+
on :temperature, location: :loc?, value: :temp?
|
|
890
|
+
|
|
891
|
+
# Predicate
|
|
892
|
+
on :temperature, value: ->(v) { v > 80 && v < 100 }
|
|
893
|
+
|
|
894
|
+
# Negation
|
|
895
|
+
negated :alert, level: "critical"
|
|
896
|
+
|
|
897
|
+
# Collection membership
|
|
898
|
+
on :order, status: one_of("pending", "processing", "completed")
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Performance Considerations
|
|
904
|
+
|
|
905
|
+
### Rule Compilation Cost
|
|
906
|
+
|
|
907
|
+
Adding a rule to the engine compiles it into the RETE network:
|
|
908
|
+
|
|
909
|
+
```ruby
|
|
910
|
+
# Cost: O(C) where C = number of conditions
|
|
911
|
+
engine.add_rule(rule)
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
**Optimization**: Add all rules before adding facts:
|
|
915
|
+
|
|
916
|
+
```ruby
|
|
917
|
+
# Good
|
|
918
|
+
kb.rules.each { |r| engine.add_rule(r) } # Compile all rules first
|
|
919
|
+
facts.each { |f| engine.add_fact(f.type, f.attributes) } # Then add facts
|
|
920
|
+
engine.run
|
|
921
|
+
|
|
922
|
+
# Less optimal
|
|
923
|
+
facts.each do |f|
|
|
924
|
+
engine.add_fact(f.type, f.attributes)
|
|
925
|
+
kb.rules.each { |r| engine.add_rule(r) } # Recompiling for each fact!
|
|
926
|
+
engine.run
|
|
927
|
+
end
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
### Condition Ordering
|
|
933
|
+
|
|
934
|
+
Order conditions from most to least selective:
|
|
935
|
+
|
|
936
|
+
```ruby
|
|
937
|
+
# Assume:
|
|
938
|
+
# - 10,000 temperature facts
|
|
939
|
+
# - 100 sensor facts
|
|
940
|
+
# - 10 sensors with id=42
|
|
941
|
+
|
|
942
|
+
# Good (selective first)
|
|
943
|
+
rule "alert" do
|
|
944
|
+
on :sensor, id: 42, status: :status? # Filters to 10 facts
|
|
945
|
+
on :temperature, sensor_id: 42, value: :v? # Then filters to ~100 facts
|
|
946
|
+
# Creates ~10 intermediate tokens
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# Bad (unselective first)
|
|
950
|
+
rule "alert" do
|
|
951
|
+
on :temperature, value: :v? # Matches 10,000 facts!
|
|
952
|
+
on :sensor, id: 42, status: :status? # Then filters
|
|
953
|
+
# Creates 10,000 intermediate tokens (slow, memory-intensive)
|
|
954
|
+
end
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
|
|
959
|
+
### Action Complexity
|
|
960
|
+
|
|
961
|
+
Keep actions fast:
|
|
962
|
+
|
|
963
|
+
```ruby
|
|
964
|
+
# Good - Fast action
|
|
965
|
+
perform do |bindings|
|
|
966
|
+
puts "Temperature: #{bindings[:temp?]}"
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# Bad - Slow action blocks engine
|
|
970
|
+
perform do |bindings|
|
|
971
|
+
sleep 5 # Blocks engine for 5 seconds!
|
|
972
|
+
send_email_alert(bindings[:temp?]) # Network I/O
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
# Better - Offload slow work
|
|
976
|
+
perform do |bindings|
|
|
977
|
+
# Post message for async worker
|
|
978
|
+
engine.post_message("alert_system", "email_queue", bindings)
|
|
979
|
+
end
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
## Debugging Rules
|
|
985
|
+
|
|
986
|
+
### Why Didn't My Rule Fire?
|
|
987
|
+
|
|
988
|
+
```ruby
|
|
989
|
+
def debug_rule(engine, rule_name)
|
|
990
|
+
rule = engine.rules.find { |r| r.name == rule_name }
|
|
991
|
+
return "Rule not found" unless rule
|
|
992
|
+
|
|
993
|
+
puts "Rule: #{rule.name}"
|
|
994
|
+
puts "Conditions (#{rule.conditions.size}):"
|
|
995
|
+
|
|
996
|
+
rule.conditions.each_with_index do |cond, i|
|
|
997
|
+
matching_facts = engine.working_memory.facts.select { |f| f.matches?(cond.pattern.merge(type: cond.type)) }
|
|
998
|
+
|
|
999
|
+
puts " #{i + 1}. #{cond.type} #{cond.pattern}"
|
|
1000
|
+
puts " Negated: #{cond.negated}"
|
|
1001
|
+
puts " Matching facts: #{matching_facts.size}"
|
|
1002
|
+
|
|
1003
|
+
if matching_facts.empty?
|
|
1004
|
+
puts " ❌ NO MATCHING FACTS (rule can't fire)"
|
|
1005
|
+
else
|
|
1006
|
+
puts " ✓ #{matching_facts.size} facts match"
|
|
1007
|
+
matching_facts.first(3).each do |f|
|
|
1008
|
+
puts " - #{f}"
|
|
1009
|
+
end
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# Check production node
|
|
1014
|
+
prod_node = engine.production_nodes[rule.name]
|
|
1015
|
+
if prod_node
|
|
1016
|
+
puts "Production node activations: #{prod_node.tokens.size}"
|
|
1017
|
+
else
|
|
1018
|
+
puts "Production node not found (rule not compiled?)"
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
debug_rule(engine, :high_temperature)
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
## See Also
|
|
1028
|
+
|
|
1029
|
+
- [Engine API](engine.md) - Registering and running rules
|
|
1030
|
+
- [Facts API](facts.md) - Understanding fact matching
|
|
1031
|
+
- [DSL Guide](../guides/dsl.md) - Declarative rule syntax
|
|
1032
|
+
- [Writing Rules Guide](../guides/writing-rules.md) - Best practices and patterns
|
|
1033
|
+
- [Performance Guide](../advanced/performance.md) - Optimization strategies
|
|
1034
|
+
- [Testing Guide](../advanced/testing.md) - Testing rules in isolation
|