kbs 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +85 -57
- data/docs/advanced/performance.md +109 -76
- data/docs/advanced/testing.md +399 -263
- data/docs/api/blackboard.md +1 -1
- data/docs/api/engine.md +77 -8
- data/docs/api/facts.md +3 -3
- data/docs/api/rules.md +110 -40
- data/docs/architecture/blackboard.md +108 -117
- data/docs/assets/images/fact-rule-relationship.svg +65 -0
- data/docs/assets/images/fact-structure.svg +42 -0
- data/docs/assets/images/inference-cycle.svg +47 -0
- data/docs/assets/images/kb-components.svg +43 -0
- data/docs/assets/images/rule-structure.svg +44 -0
- data/docs/assets/images/trading-signal-network.svg +1 -1
- data/docs/examples/index.md +219 -5
- data/docs/guides/blackboard-memory.md +89 -58
- data/docs/guides/dsl.md +24 -24
- data/docs/guides/getting-started.md +109 -107
- data/docs/guides/writing-rules.md +470 -311
- data/docs/index.md +16 -18
- data/docs/quick-start.md +92 -99
- data/docs/what-is-a-fact.md +694 -0
- data/docs/what-is-a-knowledge-base.md +350 -0
- data/docs/what-is-a-rule.md +833 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_example_dsl.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +1 -1
- data/examples/concurrent_inference_demo.rb +0 -1
- data/examples/concurrent_inference_demo_dsl.rb +0 -1
- data/examples/csv_trading_system_dsl.rb +1 -1
- data/examples/iot_demo_using_dsl.rb +1 -1
- data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
- data/examples/rule_source_demo.rb +123 -0
- data/examples/stock_trading_advanced_dsl.rb +1 -1
- data/examples/temp_dsl.txt +6214 -5269
- data/examples/timestamped_trading_dsl.rb +1 -1
- data/examples/trading_demo_dsl.rb +1 -1
- data/examples/working_demo_dsl.rb +1 -1
- data/lib/kbs/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +100 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/engine.rb +41 -0
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +14 -12
- data/mkdocs.yml +30 -30
- metadata +15 -10
- data/docs/DOCUMENTATION_STATUS.md +0 -158
- data/docs/examples/expert-systems.md +0 -1031
- data/docs/examples/multi-agent.md +0 -1335
- data/docs/examples/stock-trading.md +0 -488
- data/examples/knowledge_base.db +0 -0
- data/examples/temp.txt +0 -7693
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
# What is a Rule?
|
|
2
|
+
|
|
3
|
+
A **rule** is a declarative IF-THEN statement that defines what action to take when certain patterns of facts exist in the knowledge base. Rules are the "logic" that operates on facts (the "data").
|
|
4
|
+
|
|
5
|
+
## Core Concept
|
|
6
|
+
|
|
7
|
+
Think of a rule as an **automated detector and responder**:
|
|
8
|
+
|
|
9
|
+
- **IF** these patterns exist in the knowledge base (conditions)
|
|
10
|
+
- **THEN** execute this action (perform block)
|
|
11
|
+
|
|
12
|
+
Unlike procedural code that you explicitly call, rules **automatically fire** when their conditions are satisfied.
|
|
13
|
+
|
|
14
|
+
## Anatomy of a Rule
|
|
15
|
+
|
|
16
|
+
### Basic Structure
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
rule "high_temperature_alert" do
|
|
20
|
+
# 1. METADATA (optional)
|
|
21
|
+
desc "Alert when server room temperature exceeds safe threshold"
|
|
22
|
+
priority 10
|
|
23
|
+
|
|
24
|
+
# 2. CONDITIONS (the IF part)
|
|
25
|
+
on :temperature, location: "server_room", value: greater_than(80)
|
|
26
|
+
on :sensor, location: "server_room", status: "active"
|
|
27
|
+
|
|
28
|
+
# 3. ACTION (the THEN part)
|
|
29
|
+
perform do |facts, bindings|
|
|
30
|
+
send_alert("High temperature: #{bindings[:value?]}°F")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Visual Representation
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
*A rule consists of three parts: metadata (name, description, priority), conditions that must ALL match, and an action that executes when conditions are satisfied.*
|
|
40
|
+
|
|
41
|
+
## How Rules Differ from Other Programming Constructs
|
|
42
|
+
|
|
43
|
+
| Aspect | Rule | Function/Method | IF Statement | Event Handler |
|
|
44
|
+
|--------|------|-----------------|--------------|---------------|
|
|
45
|
+
| **Invocation** | Automatic (pattern match) | Manual (explicit call) | Manual (in code flow) | Event-driven (explicit bind) |
|
|
46
|
+
| **When** | When patterns exist | When called | When executed | When event fires |
|
|
47
|
+
| **Conditions** | Declarative patterns | Imperative checks | Imperative checks | Event type |
|
|
48
|
+
| **Ordering** | By priority/RETE | Call sequence | Code sequence | Event sequence |
|
|
49
|
+
| **Scope** | All facts in KB | Parameters passed | Local variables | Event payload |
|
|
50
|
+
|
|
51
|
+
**Example Comparison:**
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Function - Manual invocation
|
|
55
|
+
def check_temperature(temp)
|
|
56
|
+
if temp > 80
|
|
57
|
+
send_alert("High temp: #{temp}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
check_temperature(85) # Must explicitly call
|
|
61
|
+
|
|
62
|
+
# IF Statement - Part of code flow
|
|
63
|
+
temperature = sensor.read
|
|
64
|
+
if temperature > 80 && sensor.active?
|
|
65
|
+
send_alert("High temp: #{temperature}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Event Handler - Event binding
|
|
69
|
+
sensor.on(:reading) do |temp|
|
|
70
|
+
if temp > 80
|
|
71
|
+
send_alert("High temp: #{temp}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Rule - Declarative, automatic
|
|
76
|
+
rule "high_temperature" do
|
|
77
|
+
on :temperature, value: greater_than(80)
|
|
78
|
+
on :sensor, status: "active"
|
|
79
|
+
perform do |facts, bindings|
|
|
80
|
+
send_alert("High temp: #{bindings[:value?]}")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
# Fires automatically when facts match!
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Rule Lifecycle
|
|
87
|
+
|
|
88
|
+
### 1. Definition
|
|
89
|
+
|
|
90
|
+
Rules are defined using the DSL:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
kb = KBS.knowledge_base do
|
|
94
|
+
rule "golden_cross_signal" do
|
|
95
|
+
on :ma_50, value: :fast?
|
|
96
|
+
on :ma_200, value: :slow?
|
|
97
|
+
perform do |facts, bindings|
|
|
98
|
+
if bindings[:fast?] > bindings[:slow?]
|
|
99
|
+
puts "Buy signal: Golden cross detected"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 2. Compilation
|
|
107
|
+
|
|
108
|
+
When added to an engine, rules are compiled into a RETE network:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
engine.add_rule(rule)
|
|
112
|
+
# Rule compiled into discrimination network
|
|
113
|
+
# - Alpha nodes for each pattern
|
|
114
|
+
# - Join nodes to combine patterns
|
|
115
|
+
# - Production node for action
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Activation
|
|
119
|
+
|
|
120
|
+
As facts are added, the rule's conditions are evaluated:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
engine.add_fact(:ma_50, value: 52.3)
|
|
124
|
+
engine.add_fact(:ma_200, value: 51.8)
|
|
125
|
+
# Conditions now satisfied - rule activated
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. Firing
|
|
129
|
+
|
|
130
|
+
During `engine.run`, activated rules fire:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
engine.run
|
|
134
|
+
# → "Buy signal: Golden cross detected"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 5. Completion
|
|
138
|
+
|
|
139
|
+
Actions execute, potentially creating new facts:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
perform do |facts, bindings|
|
|
143
|
+
# Can add derived facts
|
|
144
|
+
fact :signal, type: "golden_cross", timestamp: Time.now
|
|
145
|
+
# Can retract facts
|
|
146
|
+
retract old_signal
|
|
147
|
+
# Can call external code
|
|
148
|
+
execute_trade(bindings[:symbol?])
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Rule Components in Detail
|
|
153
|
+
|
|
154
|
+
### Metadata
|
|
155
|
+
|
|
156
|
+
Optional information about the rule:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
rule "fraud_detection" do
|
|
160
|
+
desc "Flag transactions with suspicious patterns"
|
|
161
|
+
priority 100 # Higher priority = fires first (blackboard only)
|
|
162
|
+
# ... conditions and action
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Name** - Unique identifier
|
|
167
|
+
|
|
168
|
+
- Should be descriptive and actionable
|
|
169
|
+
- Use snake_case
|
|
170
|
+
- Example: `"reorder_low_inventory"`, `"escalate_critical_alert"`
|
|
171
|
+
|
|
172
|
+
**Description**—Human-readable explanation
|
|
173
|
+
|
|
174
|
+
- Documents the rule's purpose
|
|
175
|
+
- Helpful for debugging and maintenance
|
|
176
|
+
- Example: `"Reorders products when inventory falls below minimum threshold"`
|
|
177
|
+
|
|
178
|
+
**Priority**—Execution order (0-100 typical)
|
|
179
|
+
|
|
180
|
+
- Only affects `KBS::Blackboard::Engine`
|
|
181
|
+
- Higher numbers fire first
|
|
182
|
+
- Default: 0
|
|
183
|
+
|
|
184
|
+
### Conditions (The IF Part)
|
|
185
|
+
|
|
186
|
+
Patterns that must ALL match for the rule to fire:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
rule "order_fulfillment" do
|
|
190
|
+
# Condition 1: Must have pending order
|
|
191
|
+
on :order, status: "pending", product_id: :pid?, quantity: :qty?
|
|
192
|
+
|
|
193
|
+
# Condition 2: Must have inventory for same product
|
|
194
|
+
on :inventory, product_id: :pid?, available: :avail?
|
|
195
|
+
# ^^^^^^
|
|
196
|
+
# Join test - must match!
|
|
197
|
+
|
|
198
|
+
# Condition 3: Must NOT have existing shipment
|
|
199
|
+
without :shipment, order_id: :oid?
|
|
200
|
+
|
|
201
|
+
perform do |facts, bindings|
|
|
202
|
+
# Fires when ALL conditions satisfied
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Condition Types:**
|
|
208
|
+
|
|
209
|
+
1. **Positive** - Pattern must exist: `on :temperature, value: > 80`
|
|
210
|
+
2. **Negative** - Pattern must NOT exist: `without :alert`
|
|
211
|
+
3. **Join** - Variables link conditions: `:pid?` in both conditions above
|
|
212
|
+
|
|
213
|
+
### Action (The THEN Part)
|
|
214
|
+
|
|
215
|
+
Code executed when all conditions match:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
perform do |facts, bindings|
|
|
219
|
+
# facts - Array of matched facts (in condition order)
|
|
220
|
+
# bindings - Hash of variable captures {:pid? => 123, :qty? => 5}
|
|
221
|
+
|
|
222
|
+
# Can access facts
|
|
223
|
+
order = facts[0]
|
|
224
|
+
inventory = facts[1]
|
|
225
|
+
|
|
226
|
+
# Can access bindings
|
|
227
|
+
product_id = bindings[:pid?]
|
|
228
|
+
quantity = bindings[:qty?]
|
|
229
|
+
available = bindings[:avail?]
|
|
230
|
+
|
|
231
|
+
# Can make decisions
|
|
232
|
+
if available >= quantity
|
|
233
|
+
ship_order(order)
|
|
234
|
+
else
|
|
235
|
+
backorder(order)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Can add facts
|
|
239
|
+
fact :shipment, order_id: order[:id], shipped_at: Time.now
|
|
240
|
+
|
|
241
|
+
# Can retract facts
|
|
242
|
+
retract order
|
|
243
|
+
|
|
244
|
+
# Can call external code
|
|
245
|
+
notify_customer(order[:customer_id])
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## How Rules Work: The Inference Cycle
|
|
250
|
+
|
|
251
|
+
Rules participate in an automatic reasoning loop:
|
|
252
|
+
|
|
253
|
+

|
|
254
|
+
|
|
255
|
+
*Rules execute within a continuous inference cycle: facts are added, the RETE network matches patterns, activated rules fire and potentially create new facts, triggering another cycle. Inference completes when no new facts are generated.*
|
|
256
|
+
|
|
257
|
+
**Example:**
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
kb = KBS.knowledge_base do
|
|
261
|
+
# Rule 1: Detect high temperature
|
|
262
|
+
rule "detect_high_temp" do
|
|
263
|
+
on :temperature, value: greater_than(80), sensor_id: :sid?
|
|
264
|
+
without :alert, sensor_id: :sid?
|
|
265
|
+
perform do |facts, bindings|
|
|
266
|
+
# Add alert fact (triggers Rule 2)
|
|
267
|
+
fact :alert, sensor_id: bindings[:sid?], level: "high"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Rule 2: Escalate alerts
|
|
272
|
+
rule "escalate_alert" do
|
|
273
|
+
on :alert, level: "high", sensor_id: :sid?
|
|
274
|
+
on :sensor, id: :sid?, critical: true
|
|
275
|
+
perform do |facts, bindings|
|
|
276
|
+
notify_ops(bindings[:sid?])
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Add facts
|
|
281
|
+
fact :temperature, value: 85, sensor_id: 42
|
|
282
|
+
fact :sensor, id: 42, critical: true
|
|
283
|
+
|
|
284
|
+
# Run inference
|
|
285
|
+
run
|
|
286
|
+
# → Rule 1 fires, creates :alert fact
|
|
287
|
+
# → Rule 2 fires (activated by new alert), notifies ops
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Types of Rules
|
|
292
|
+
|
|
293
|
+
### 1. Detection Rules
|
|
294
|
+
|
|
295
|
+
Identify patterns and generate alerts:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
rule "detect_fraud" do
|
|
299
|
+
on :transaction, amount: greater_than(10_000)
|
|
300
|
+
on :account, new_account: true
|
|
301
|
+
perform { flag_for_review }
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 2. Derivation Rules
|
|
306
|
+
|
|
307
|
+
Infer new facts from existing facts:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
rule "derive_momentum" do
|
|
311
|
+
on :price, current: :curr?, previous: :prev?
|
|
312
|
+
perform do |facts, bindings|
|
|
313
|
+
change_pct = ((bindings[:curr?] - bindings[:prev?]) / bindings[:prev?]) * 100
|
|
314
|
+
fact :momentum, change_pct: change_pct
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 3. Reaction Rules
|
|
320
|
+
|
|
321
|
+
Take action when conditions arise:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
rule "reorder_inventory" do
|
|
325
|
+
on :inventory, product_id: :pid?, quantity: less_than(10)
|
|
326
|
+
perform do |facts, bindings|
|
|
327
|
+
create_purchase_order(bindings[:pid?], quantity: 100)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 4. State Machine Rules
|
|
333
|
+
|
|
334
|
+
Manage transitions between states:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
rule "pending_to_processing" do
|
|
338
|
+
on :order, id: :oid?, status: "pending"
|
|
339
|
+
on :worker, status: "available", id: :wid?
|
|
340
|
+
perform do |facts, bindings|
|
|
341
|
+
order = query(:order, id: bindings[:oid?]).first
|
|
342
|
+
retract order
|
|
343
|
+
fact :order, id: bindings[:oid?], status: "processing", worker_id: bindings[:wid?]
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 5. Guard Rules
|
|
349
|
+
|
|
350
|
+
Prevent invalid states:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
rule "prevent_duplicate_orders" do
|
|
354
|
+
on :order, customer_id: :cid?, product_id: :pid?, status: "pending"
|
|
355
|
+
on :order, customer_id: :cid?, product_id: :pid?, status: "processing"
|
|
356
|
+
perform do |facts, bindings|
|
|
357
|
+
cancel_duplicate_order(facts[0])
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 6. Cleanup Rules
|
|
363
|
+
|
|
364
|
+
Remove obsolete facts:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
rule "expire_old_alerts" do
|
|
368
|
+
on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
|
|
369
|
+
perform do |facts, bindings|
|
|
370
|
+
retract facts[0]
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Rule Patterns and Best Practices
|
|
376
|
+
|
|
377
|
+
### Pattern: Rule Chaining
|
|
378
|
+
|
|
379
|
+
Rules can trigger other rules:
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# Rule 1 creates fact that activates Rule 2
|
|
383
|
+
rule "detect_anomaly" do
|
|
384
|
+
on :sensor, value: :val?
|
|
385
|
+
perform { fact :anomaly, value: bindings[:val?] }
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
rule "escalate_anomaly" do
|
|
389
|
+
on :anomaly, value: greater_than(100)
|
|
390
|
+
perform { send_alert }
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Pattern: Multi-Condition Filtering
|
|
395
|
+
|
|
396
|
+
Combine multiple conditions to narrow matches:
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
rule "qualified_lead" do
|
|
400
|
+
on :customer, revenue: greater_than(100_000)
|
|
401
|
+
on :interaction, customer_id: :cid?, type: "demo_request"
|
|
402
|
+
on :product_fit, customer_id: :cid?, score: greater_than(80)
|
|
403
|
+
without :opportunity, customer_id: :cid?
|
|
404
|
+
perform { create_opportunity }
|
|
405
|
+
end
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Pattern: Exception Handling
|
|
409
|
+
|
|
410
|
+
Use negation to ensure preconditions:
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
rule "process_payment" do
|
|
414
|
+
on :order, status: "confirmed"
|
|
415
|
+
without :payment, order_id: :oid? # No payment yet
|
|
416
|
+
without :error, order_id: :oid? # No errors
|
|
417
|
+
perform { charge_customer }
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Pattern: Temporal Rules
|
|
422
|
+
|
|
423
|
+
Time-aware reasoning:
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
rule "stale_data_warning" do
|
|
427
|
+
on :reading, timestamp: ->(ts) { Time.now - ts > 300 }, sensor_id: :sid?
|
|
428
|
+
perform do |facts, bindings|
|
|
429
|
+
alert("Stale data from sensor #{bindings[:sid?]}")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Pattern: Aggregation
|
|
435
|
+
|
|
436
|
+
Collect and analyze multiple facts:
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
rule "daily_summary" do
|
|
440
|
+
on :trigger, event: "end_of_day"
|
|
441
|
+
perform do
|
|
442
|
+
temps = query(:temperature).map { |f| f[:value] }
|
|
443
|
+
avg = temps.sum / temps.size.to_f
|
|
444
|
+
fact :summary, avg_temp: avg, date: Date.today
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Rule Ordering and Priority
|
|
450
|
+
|
|
451
|
+
### Priority in KBS::Blackboard::Engine
|
|
452
|
+
|
|
453
|
+
Controls which rules fire first when multiple are activated:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
rule "critical_shutdown", priority: 100 do
|
|
457
|
+
on :temperature, value: greater_than(120)
|
|
458
|
+
perform { emergency_shutdown! }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
rule "send_warning", priority: 50 do
|
|
462
|
+
on :temperature, value: greater_than(80)
|
|
463
|
+
perform { send_warning_email }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
rule "log_reading", priority: 10 do
|
|
467
|
+
on :temperature, value: :val?
|
|
468
|
+
perform { log(bindings[:val?]) }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# With temp = 125, fires in order:
|
|
472
|
+
# 1. critical_shutdown (priority 100)
|
|
473
|
+
# 2. send_warning (priority 50)
|
|
474
|
+
# 3. log_reading (priority 10)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Priority in KBS::Engine
|
|
478
|
+
|
|
479
|
+
Priority is stored but **not used** for ordering - rules fire in arbitrary order.
|
|
480
|
+
|
|
481
|
+
### When Priority Matters
|
|
482
|
+
|
|
483
|
+
**Use priority for:**
|
|
484
|
+
|
|
485
|
+
- Critical safety checks (priority 100)
|
|
486
|
+
- System integrity rules (priority 75)
|
|
487
|
+
- Business logic (priority 50)
|
|
488
|
+
- Logging and monitoring (priority 10)
|
|
489
|
+
|
|
490
|
+
**Don't rely on priority for:**
|
|
491
|
+
|
|
492
|
+
- Sequencing actions (use fact dependencies instead)
|
|
493
|
+
- Enforcing order between independent rules
|
|
494
|
+
- Complex orchestration (use state machines)
|
|
495
|
+
|
|
496
|
+
## Rules vs. Queries
|
|
497
|
+
|
|
498
|
+
Rules are **reactive** (fire automatically), queries are **proactive** (you call them):
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
# Rule - Automatic
|
|
502
|
+
rule "alert_on_high_temp" do
|
|
503
|
+
on :temperature, value: greater_than(80)
|
|
504
|
+
perform { send_alert } # Fires automatically
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Query - Manual
|
|
508
|
+
temps = query(:temperature, value: greater_than(80))
|
|
509
|
+
temps.each { |t| send_alert } # You must iterate
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**When to use rules:**
|
|
513
|
+
|
|
514
|
+
- Continuous monitoring
|
|
515
|
+
- Event-driven reactions
|
|
516
|
+
- Complex multi-condition patterns
|
|
517
|
+
- Automatic inference
|
|
518
|
+
|
|
519
|
+
**When to use queries:**
|
|
520
|
+
|
|
521
|
+
- One-time lookups
|
|
522
|
+
- Reporting and analysis
|
|
523
|
+
- Interactive exploration
|
|
524
|
+
- When you need explicit control
|
|
525
|
+
|
|
526
|
+
## Performance Considerations
|
|
527
|
+
|
|
528
|
+
### Rule Count
|
|
529
|
+
|
|
530
|
+
- 10-100 rules: Excellent
|
|
531
|
+
- 100-1,000 rules: Very good (network sharing helps)
|
|
532
|
+
- 1,000+ rules: Good (consider grouping by domain)
|
|
533
|
+
|
|
534
|
+
### Condition Count
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
# Fast - 1-2 conditions
|
|
538
|
+
rule "simple" do
|
|
539
|
+
on :stock, symbol: "AAPL"
|
|
540
|
+
perform { ... }
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Typical - 2-4 conditions
|
|
544
|
+
rule "moderate" do
|
|
545
|
+
on :order, status: "pending"
|
|
546
|
+
on :inventory, available: greater_than(0)
|
|
547
|
+
on :customer, verified: true
|
|
548
|
+
perform { ... }
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Slower - 5+ conditions (but still efficient with RETE)
|
|
552
|
+
rule "complex" do
|
|
553
|
+
on :order, ...
|
|
554
|
+
on :customer, ...
|
|
555
|
+
on :inventory, ...
|
|
556
|
+
on :pricing, ...
|
|
557
|
+
on :shipping, ...
|
|
558
|
+
perform { ... }
|
|
559
|
+
end
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Condition Ordering Impact
|
|
563
|
+
|
|
564
|
+
**Huge impact** - order by selectivity:
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
# Bad - general first (creates many partial matches)
|
|
568
|
+
on :sensor # 1000 facts
|
|
569
|
+
on :alert, level: "critical" # 1 fact
|
|
570
|
+
# → 1000 tokens created
|
|
571
|
+
|
|
572
|
+
# Good - specific first (creates few partial matches)
|
|
573
|
+
on :alert, level: "critical" # 1 fact
|
|
574
|
+
on :sensor # 1000 facts
|
|
575
|
+
# → 1 token created
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Action Complexity
|
|
579
|
+
|
|
580
|
+
Keep actions lightweight:
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
# Good - fast action
|
|
584
|
+
perform do |facts, bindings|
|
|
585
|
+
fact :alert, level: "high"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Acceptable - moderate work
|
|
589
|
+
perform do |facts, bindings|
|
|
590
|
+
send_notification(bindings[:user_id?])
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Avoid - heavy work in action
|
|
594
|
+
perform do |facts, bindings|
|
|
595
|
+
# Don't do this in action:
|
|
596
|
+
complex_calculation()
|
|
597
|
+
database_batch_update()
|
|
598
|
+
api_call_with_retry()
|
|
599
|
+
# Instead, add a fact to trigger async processing
|
|
600
|
+
fact :work_item, type: "heavy_task", data: bindings
|
|
601
|
+
end
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## Common Pitfalls
|
|
605
|
+
|
|
606
|
+
### 1. Forgetting "All Conditions Must Match"
|
|
607
|
+
|
|
608
|
+
```ruby
|
|
609
|
+
# This rule NEVER fires if there's no :inventory fact
|
|
610
|
+
rule "process_order" do
|
|
611
|
+
on :order, status: "pending"
|
|
612
|
+
on :inventory, available: greater_than(0) # What if no inventory fact?
|
|
613
|
+
perform { ship_order }
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Fix: Use negation or optional patterns
|
|
617
|
+
rule "process_order" do
|
|
618
|
+
on :order, status: "pending"
|
|
619
|
+
without :inventory, available: less_than(1) # OK if no inventory fact
|
|
620
|
+
perform { ship_order }
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### 2. Expecting Sequential Execution
|
|
625
|
+
|
|
626
|
+
```ruby
|
|
627
|
+
# Rules don't execute in definition order
|
|
628
|
+
rule "step1" do ... end
|
|
629
|
+
rule "step2" do ... end # NOT guaranteed to fire after step1
|
|
630
|
+
|
|
631
|
+
# Use fact dependencies instead
|
|
632
|
+
rule "step1" do
|
|
633
|
+
perform { fact :step1_complete }
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
rule "step2" do
|
|
637
|
+
on :step1_complete # Depends on step1
|
|
638
|
+
perform { ... }
|
|
639
|
+
end
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### 3. Infinite Loops
|
|
643
|
+
|
|
644
|
+
```ruby
|
|
645
|
+
# Bad - creates infinite loop
|
|
646
|
+
rule "loop" do
|
|
647
|
+
on :counter, value: :val?
|
|
648
|
+
perform do |facts, bindings|
|
|
649
|
+
# Retracts and re-adds fact → rule fires again → infinite loop!
|
|
650
|
+
retract facts[0]
|
|
651
|
+
fact :counter, value: bindings[:val?] + 1
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Fix: Add termination condition
|
|
656
|
+
rule "loop" do
|
|
657
|
+
on :counter, value: less_than(10)
|
|
658
|
+
perform do |facts, bindings|
|
|
659
|
+
retract facts[0]
|
|
660
|
+
fact :counter, value: bindings[:val?] + 1
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 4. Side Effects in Conditions
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
# Wrong - side effects in predicate
|
|
669
|
+
counter = 0
|
|
670
|
+
on :stock, price: ->(p) {
|
|
671
|
+
counter += 1 # Bad! Runs many times
|
|
672
|
+
p > 100
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# Right - side effects in action
|
|
676
|
+
on :stock, price: greater_than(100)
|
|
677
|
+
perform { counter += 1 }
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### 5. Modifying Facts Instead of Retracting
|
|
681
|
+
|
|
682
|
+
```ruby
|
|
683
|
+
# Wrong - changes don't trigger rules
|
|
684
|
+
fact = engine.facts.first
|
|
685
|
+
fact[:status] = "processed" # No rules fire
|
|
686
|
+
|
|
687
|
+
# Right - retract and re-add
|
|
688
|
+
retract old_fact
|
|
689
|
+
fact :order, status: "processed" # Rules fire
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
## Testing Rules
|
|
693
|
+
|
|
694
|
+
### Unit Testing
|
|
695
|
+
|
|
696
|
+
Test rules in isolation:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
def test_high_temp_alert
|
|
700
|
+
kb = KBS.knowledge_base do
|
|
701
|
+
rule "alert" do
|
|
702
|
+
on :temperature, value: greater_than(80)
|
|
703
|
+
perform { fact :alert, level: "high" }
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
fact :temperature, value: 85
|
|
707
|
+
run
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
alerts = kb.query(:alert)
|
|
711
|
+
assert_equal 1, alerts.size
|
|
712
|
+
assert_equal "high", alerts.first[:level]
|
|
713
|
+
end
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Integration Testing
|
|
717
|
+
|
|
718
|
+
Test rule interactions:
|
|
719
|
+
|
|
720
|
+
```ruby
|
|
721
|
+
def test_alert_escalation
|
|
722
|
+
kb = KBS.knowledge_base do
|
|
723
|
+
rule "create_alert" do
|
|
724
|
+
on :temperature, value: greater_than(80)
|
|
725
|
+
perform { fact :alert, level: "high" }
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
rule "escalate_alert" do
|
|
729
|
+
on :alert, level: "high"
|
|
730
|
+
on :sensor, critical: true
|
|
731
|
+
perform { fact :escalation, priority: "urgent" }
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
fact :temperature, value: 85
|
|
735
|
+
fact :sensor, critical: true
|
|
736
|
+
run
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
assert kb.query(:alert).any?
|
|
740
|
+
assert kb.query(:escalation).any?
|
|
741
|
+
end
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Rule Design Principles
|
|
745
|
+
|
|
746
|
+
### 1. Single Responsibility
|
|
747
|
+
|
|
748
|
+
One rule, one purpose:
|
|
749
|
+
|
|
750
|
+
```ruby
|
|
751
|
+
# Good—focused
|
|
752
|
+
rule "reorder_low_inventory" do
|
|
753
|
+
on :inventory, quantity: less_than(10)
|
|
754
|
+
perform { create_purchase_order }
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# Bad—does too much
|
|
758
|
+
rule "inventory_management" do
|
|
759
|
+
on :inventory
|
|
760
|
+
perform do
|
|
761
|
+
check_quantity
|
|
762
|
+
update_forecasts
|
|
763
|
+
notify_suppliers
|
|
764
|
+
generate_reports
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### 2. Declarative Over Imperative
|
|
770
|
+
|
|
771
|
+
Express what, not how:
|
|
772
|
+
|
|
773
|
+
```ruby
|
|
774
|
+
# Good—declarative
|
|
775
|
+
rule "qualified_customer" do
|
|
776
|
+
on :customer, revenue: greater_than(100_000)
|
|
777
|
+
on :engagement, score: greater_than(80)
|
|
778
|
+
perform { create_opportunity }
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Less ideal—imperative
|
|
782
|
+
rule "check_customer" do
|
|
783
|
+
on :customer
|
|
784
|
+
perform do |facts|
|
|
785
|
+
if facts[0][:revenue] > 100_000
|
|
786
|
+
engagement = query(:engagement, customer_id: facts[0][:id]).first
|
|
787
|
+
if engagement && engagement[:score] > 80
|
|
788
|
+
create_opportunity
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### 3. Explicit Over Implicit
|
|
796
|
+
|
|
797
|
+
Make conditions explicit:
|
|
798
|
+
|
|
799
|
+
```ruby
|
|
800
|
+
# Good—clear dependencies
|
|
801
|
+
rule "ship_order" do
|
|
802
|
+
on :order, status: "paid"
|
|
803
|
+
on :inventory, available: greater_than(0)
|
|
804
|
+
without :shipment # Explicit: no existing shipment
|
|
805
|
+
perform { ship }
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# Bad—hidden assumptions
|
|
809
|
+
rule "ship_order" do
|
|
810
|
+
on :order, status: "paid"
|
|
811
|
+
perform { ship } # Implicitly assumes inventory exists
|
|
812
|
+
end
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## Further Reading
|
|
816
|
+
|
|
817
|
+
- **[Writing Rules Guide](guides/writing-rules.md)** - Detailed best practices
|
|
818
|
+
- **[Rules API Reference](api/rules.md)** - Complete method documentation
|
|
819
|
+
- **[DSL Reference](guides/dsl.md)** - Rule definition syntax
|
|
820
|
+
- **[Pattern Matching](guides/pattern-matching.md)** - Condition patterns
|
|
821
|
+
- **[RETE Algorithm](architecture/rete-algorithm.md)** - How rules are compiled and executed
|
|
822
|
+
|
|
823
|
+
## Summary
|
|
824
|
+
|
|
825
|
+
A **rule** is:
|
|
826
|
+
|
|
827
|
+
- A **declarative IF-THEN statement** that automatically fires when patterns match
|
|
828
|
+
- Composed of **conditions** (patterns to match) and **action** (code to execute)
|
|
829
|
+
- **Automatically activated** by the RETE engine when facts satisfy conditions
|
|
830
|
+
- The "logic" that operates on facts (the "data") in a knowledge base
|
|
831
|
+
- Available with optional **priority** for execution ordering (blackboard only)
|
|
832
|
+
|
|
833
|
+
Think of rules as **automated sentinels** that continuously watch for specific patterns and react instantly when those patterns appear.
|