kbs 0.0.1 → 0.2.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 +291 -362
- 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 +865 -0
- data/docs/advanced/testing.md +827 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +1047 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1104 -0
- data/docs/architecture/blackboard.md +544 -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/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/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/rule-structure.svg +44 -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/index.md +223 -0
- data/docs/guides/blackboard-memory.md +589 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +385 -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 +914 -0
- data/docs/index.md +155 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +221 -0
- 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/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 -6
- data/examples/concurrent_inference_demo_dsl.rb +362 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/iot_demo_using_dsl.rb +1 -1
- 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/rule_source_demo.rb +123 -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_dsl.txt +9392 -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/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +101 -2
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +15 -13
- data/mkdocs.yml +181 -0
- metadata +74 -9
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
# Writing Rules
|
|
2
|
+
|
|
3
|
+
Master the art of authoring production rules. This guide covers best practices, patterns, and strategies for writing effective, maintainable, and performant rules in KBS.
|
|
4
|
+
|
|
5
|
+
## Rule Anatomy
|
|
6
|
+
|
|
7
|
+
Every rule consists of three parts:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
KBS.knowledge_base do
|
|
11
|
+
rule "rule_name", priority: 0 do
|
|
12
|
+
# 1. CONDITIONS - Pattern matching
|
|
13
|
+
on :fact_type, attr: value
|
|
14
|
+
|
|
15
|
+
# 2. ACTION - What to do when conditions match
|
|
16
|
+
perform do |facts, bindings|
|
|
17
|
+
# Execute logic
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 1. Rule Name
|
|
24
|
+
|
|
25
|
+
Choose descriptive, actionable names:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Good: Clear intent
|
|
29
|
+
"send_high_temperature_alert"
|
|
30
|
+
"cancel_duplicate_orders"
|
|
31
|
+
"escalate_critical_issues"
|
|
32
|
+
|
|
33
|
+
# Bad: Vague or cryptic
|
|
34
|
+
"rule1"
|
|
35
|
+
"process"
|
|
36
|
+
"check_stuff"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Naming Conventions:**
|
|
40
|
+
- Use snake_case
|
|
41
|
+
- Start with verb (action-oriented)
|
|
42
|
+
- Be specific about what the rule does
|
|
43
|
+
- Include domain context
|
|
44
|
+
|
|
45
|
+
### 2. Priority
|
|
46
|
+
|
|
47
|
+
Control execution order when multiple rules match:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
KBS.knowledge_base do
|
|
51
|
+
rule "critical_safety_check", priority: 100 do # Fires first
|
|
52
|
+
# ...
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
rule "normal_processing", priority: 50 do
|
|
56
|
+
# ...
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
rule "cleanup_task", priority: 10 do # Fires last
|
|
60
|
+
# ...
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Priority Guidelines:**
|
|
66
|
+
- **100+** - Safety checks, emergency shutdowns
|
|
67
|
+
- **50-99** - Business logic, processing
|
|
68
|
+
- **1-49** - Monitoring, logging, cleanup
|
|
69
|
+
- **0** - Default priority (no preference)
|
|
70
|
+
|
|
71
|
+
### 3. Conditions
|
|
72
|
+
|
|
73
|
+
Patterns that must match for the rule to fire. Order matters for performance.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
KBS.knowledge_base do
|
|
77
|
+
rule "example" do
|
|
78
|
+
# Most selective first (fewest matches)
|
|
79
|
+
on :critical_alert, severity: "critical"
|
|
80
|
+
|
|
81
|
+
# Less selective last (more matches)
|
|
82
|
+
on :sensor, id: :sensor_id?
|
|
83
|
+
|
|
84
|
+
perform do |facts, bindings|
|
|
85
|
+
# Action
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 4. Action
|
|
92
|
+
|
|
93
|
+
Code executed when all conditions match:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
KBS.knowledge_base do
|
|
97
|
+
rule "example" do
|
|
98
|
+
on :alert, message: :msg?
|
|
99
|
+
on :sensor, id: :sensor_id?
|
|
100
|
+
|
|
101
|
+
perform do |facts, bindings|
|
|
102
|
+
# Access matched facts
|
|
103
|
+
alert = facts[0]
|
|
104
|
+
sensor = facts[1]
|
|
105
|
+
|
|
106
|
+
# Access variable bindings
|
|
107
|
+
sensor_id = bindings[:sensor_id?]
|
|
108
|
+
|
|
109
|
+
# Perform action
|
|
110
|
+
notify_operator(sensor_id, alert[:message])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Condition Ordering
|
|
117
|
+
|
|
118
|
+
**Golden Rule**: Order conditions from most selective to least selective.
|
|
119
|
+
|
|
120
|
+
### Why Order Matters
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# Bad: General condition first
|
|
124
|
+
KBS.knowledge_base do
|
|
125
|
+
rule "inefficient" do
|
|
126
|
+
on :sensor, {} # 1000 matches
|
|
127
|
+
on :critical_alert, {} # 1 match
|
|
128
|
+
perform { }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
# Creates 1000 partial matches, wastes memory
|
|
132
|
+
|
|
133
|
+
# Good: Specific condition first
|
|
134
|
+
KBS.knowledge_base do
|
|
135
|
+
rule "efficient" do
|
|
136
|
+
on :critical_alert, {} # 1 match
|
|
137
|
+
on :sensor, {} # Joins with 1000
|
|
138
|
+
perform { }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
# Creates 1 partial match, efficient joins
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Selectivity Examples
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Most selective (few facts)
|
|
148
|
+
on :emergency, level: "critical"
|
|
149
|
+
on :user, role: "admin"
|
|
150
|
+
|
|
151
|
+
# Moderate selectivity
|
|
152
|
+
on :order, status: "pending"
|
|
153
|
+
on :stock, exchange: "NYSE"
|
|
154
|
+
|
|
155
|
+
# Least selective (many facts)
|
|
156
|
+
on :sensor, {}
|
|
157
|
+
on :log_entry, {}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Measuring Selectivity
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
def measure_selectivity(kb, type, pattern)
|
|
164
|
+
kb.engine.facts.count { |f|
|
|
165
|
+
f.type == type &&
|
|
166
|
+
pattern.all? { |k, v| f[k] == v }
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Compare
|
|
171
|
+
puts measure_selectivity(kb, :critical_alert, {}) # => 1
|
|
172
|
+
puts measure_selectivity(kb, :sensor, {}) # => 1000
|
|
173
|
+
|
|
174
|
+
# Order: critical_alert first, sensor second
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Action Design
|
|
178
|
+
|
|
179
|
+
### Single Responsibility
|
|
180
|
+
|
|
181
|
+
One action, one purpose:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
# Good: Focused action
|
|
185
|
+
KBS.knowledge_base do
|
|
186
|
+
rule "send_email" do
|
|
187
|
+
on :alert, email: :email?, message: :message?
|
|
188
|
+
perform do |facts, bindings|
|
|
189
|
+
send_email_alert(bindings[:email?], bindings[:message?])
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Bad: Multiple responsibilities
|
|
195
|
+
KBS.knowledge_base do
|
|
196
|
+
rule "do_everything" do
|
|
197
|
+
on :trigger, email: :email?, id: :id?, data: :data?, msg: :msg?
|
|
198
|
+
perform do |facts, bindings|
|
|
199
|
+
send_email_alert(bindings[:email?])
|
|
200
|
+
update_database(bindings[:id?])
|
|
201
|
+
call_external_api(bindings[:data?])
|
|
202
|
+
write_log_file(bindings[:msg?])
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Split complex actions into multiple rules:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
KBS.knowledge_base do
|
|
212
|
+
# Rule 1: Detect condition
|
|
213
|
+
rule "detect_high_temp", priority: 50 do
|
|
214
|
+
on :sensor, temp: :temp?, predicate: greater_than(30)
|
|
215
|
+
|
|
216
|
+
perform do |facts, bindings|
|
|
217
|
+
fact :high_temp_detected, temp: bindings[:temp?]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Rule 2: Send alert
|
|
222
|
+
rule "send_temp_alert", priority: 40 do
|
|
223
|
+
on :high_temp_detected, temp: :temp?
|
|
224
|
+
|
|
225
|
+
perform do |facts, bindings|
|
|
226
|
+
send_email("High temp: #{bindings[:temp?]}")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Rule 3: Log event
|
|
231
|
+
rule "log_temp_event", priority: 30 do
|
|
232
|
+
on :high_temp_detected, temp: :temp?
|
|
233
|
+
|
|
234
|
+
perform do |facts, bindings|
|
|
235
|
+
logger.info("Temperature spike: #{bindings[:temp?]}")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Avoid Side Effects
|
|
242
|
+
|
|
243
|
+
Actions should be deterministic and idempotent when possible:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# Good: Idempotent (safe to run multiple times)
|
|
247
|
+
kb = KBS.knowledge_base do
|
|
248
|
+
rule "update_alert" do
|
|
249
|
+
on :trigger, id: :id?
|
|
250
|
+
|
|
251
|
+
perform do |facts, bindings|
|
|
252
|
+
# Remove old alert if exists
|
|
253
|
+
old = engine.facts.find { |f| f.type == :alert && f[:id] == bindings[:id?] }
|
|
254
|
+
engine.remove_fact(old) if old
|
|
255
|
+
|
|
256
|
+
# Add new alert
|
|
257
|
+
fact :alert, id: bindings[:id?], message: "Alert!"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Bad: Non-idempotent (creates duplicates)
|
|
263
|
+
kb = KBS.knowledge_base do
|
|
264
|
+
rule "duplicate_alerts" do
|
|
265
|
+
on :trigger, id: :id?
|
|
266
|
+
|
|
267
|
+
perform do |facts, bindings|
|
|
268
|
+
# Always adds, even if alert already exists
|
|
269
|
+
fact :alert, id: bindings[:id?], message: "Alert!"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Error Handling
|
|
276
|
+
|
|
277
|
+
Protect against failures:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
KBS.knowledge_base do
|
|
281
|
+
rule "safe_email" do
|
|
282
|
+
on :alert, email: :email?, message: :message?
|
|
283
|
+
|
|
284
|
+
perform do |facts, bindings|
|
|
285
|
+
begin
|
|
286
|
+
send_email(bindings[:email?], bindings[:message?])
|
|
287
|
+
rescue Net::SMTPError => e
|
|
288
|
+
logger.error("Failed to send email: #{e.message}")
|
|
289
|
+
# Add failure fact for retry logic
|
|
290
|
+
fact :email_failure,
|
|
291
|
+
email: bindings[:email?],
|
|
292
|
+
error: e.message,
|
|
293
|
+
timestamp: Time.now
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Variable Binding Strategies
|
|
301
|
+
|
|
302
|
+
### Consistent Naming
|
|
303
|
+
|
|
304
|
+
Use descriptive, consistent variable names:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# Good: Clear intent
|
|
308
|
+
:sensor_id?
|
|
309
|
+
:temperature_celsius?
|
|
310
|
+
:alert_threshold?
|
|
311
|
+
:user_email?
|
|
312
|
+
|
|
313
|
+
# Bad: Cryptic
|
|
314
|
+
:s?
|
|
315
|
+
:t?
|
|
316
|
+
:x?
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Join Patterns
|
|
320
|
+
|
|
321
|
+
Connect facts through shared variables:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
KBS.knowledge_base do
|
|
325
|
+
# Pattern: Join sensor reading with threshold
|
|
326
|
+
rule "check_threshold" do
|
|
327
|
+
on :sensor,
|
|
328
|
+
id: :sensor_id?,
|
|
329
|
+
temp: :current_temp?
|
|
330
|
+
|
|
331
|
+
on :threshold,
|
|
332
|
+
sensor_id: :sensor_id?, # Same variable = join constraint
|
|
333
|
+
max_temp: :max_temp?
|
|
334
|
+
|
|
335
|
+
perform do |facts, bindings|
|
|
336
|
+
# Only matches when sensor_id is same in both facts
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Computed Bindings
|
|
343
|
+
|
|
344
|
+
Derive values in actions:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
KBS.knowledge_base do
|
|
348
|
+
rule "calculate_diff" do
|
|
349
|
+
on :sensor, temp: :current_temp?
|
|
350
|
+
on :threshold, max_temp: :max_temp?
|
|
351
|
+
|
|
352
|
+
perform do |facts, bindings|
|
|
353
|
+
current = bindings[:current_temp?]
|
|
354
|
+
max = bindings[:max_temp?]
|
|
355
|
+
|
|
356
|
+
# Compute derived values
|
|
357
|
+
diff = current - max
|
|
358
|
+
percentage_over = ((current / max.to_f) - 1) * 100
|
|
359
|
+
|
|
360
|
+
puts "#{diff}°C over threshold (#{percentage_over.round(1)}%)"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Rule Composition Patterns
|
|
367
|
+
|
|
368
|
+
### State Machine
|
|
369
|
+
|
|
370
|
+
Model state transitions:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
KBS.knowledge_base do
|
|
374
|
+
# Transition: pending → processing
|
|
375
|
+
rule "start_processing" do
|
|
376
|
+
on :order,
|
|
377
|
+
id: :order_id?,
|
|
378
|
+
status: "pending"
|
|
379
|
+
|
|
380
|
+
perform do |facts, bindings|
|
|
381
|
+
old_order = facts[0]
|
|
382
|
+
engine.remove_fact(old_order)
|
|
383
|
+
fact :order,
|
|
384
|
+
id: bindings[:order_id?],
|
|
385
|
+
status: "processing",
|
|
386
|
+
started_at: Time.now
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Transition: processing → completed
|
|
391
|
+
rule "complete_processing" do
|
|
392
|
+
on :order,
|
|
393
|
+
id: :order_id?,
|
|
394
|
+
status: "processing"
|
|
395
|
+
on :processing_done,
|
|
396
|
+
order_id: :order_id?
|
|
397
|
+
|
|
398
|
+
perform do |facts, bindings|
|
|
399
|
+
order = facts[0]
|
|
400
|
+
engine.remove_fact(order)
|
|
401
|
+
engine.remove_fact(facts[1]) # Remove trigger
|
|
402
|
+
fact :order,
|
|
403
|
+
id: bindings[:order_id?],
|
|
404
|
+
status: "completed",
|
|
405
|
+
completed_at: Time.now
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Guard Conditions
|
|
412
|
+
|
|
413
|
+
Prevent duplicate actions:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
KBS.knowledge_base do
|
|
417
|
+
rule "send_alert_once" do
|
|
418
|
+
on :high_temp, sensor_id: :id?
|
|
419
|
+
|
|
420
|
+
# Guard: Only fire if alert not already sent
|
|
421
|
+
without :alert_sent, sensor_id: :id?
|
|
422
|
+
|
|
423
|
+
perform do |facts, bindings|
|
|
424
|
+
send_alert(bindings[:id?])
|
|
425
|
+
|
|
426
|
+
# Record that we sent this alert
|
|
427
|
+
fact :alert_sent, sensor_id: bindings[:id?]
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Cleanup Rules
|
|
434
|
+
|
|
435
|
+
Remove stale facts:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
KBS.knowledge_base do
|
|
439
|
+
rule "cleanup_stale_alerts", priority: 1 do
|
|
440
|
+
on :alert,
|
|
441
|
+
timestamp: :time?,
|
|
442
|
+
predicate: lambda { |f|
|
|
443
|
+
(Time.now - f[:timestamp]) > 3600 # 1 hour old
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
perform do |facts, bindings|
|
|
447
|
+
engine.remove_fact(facts[0])
|
|
448
|
+
logger.info("Removed stale alert")
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Aggregation Rules
|
|
455
|
+
|
|
456
|
+
Compute over multiple facts:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
KBS.knowledge_base do
|
|
460
|
+
rule "compute_average_temp" do
|
|
461
|
+
on :compute_avg_requested, {}
|
|
462
|
+
|
|
463
|
+
perform do |facts, bindings|
|
|
464
|
+
temps = engine.facts
|
|
465
|
+
.select { |f| f.type == :sensor }
|
|
466
|
+
.map { |f| f[:temp] }
|
|
467
|
+
.compact
|
|
468
|
+
|
|
469
|
+
avg = temps.sum / temps.size.to_f
|
|
470
|
+
|
|
471
|
+
fact :average_temp, value: avg
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Temporal Rules
|
|
478
|
+
|
|
479
|
+
React to time-based conditions:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
KBS.knowledge_base do
|
|
483
|
+
rule "detect_delayed_response" do
|
|
484
|
+
on :request,
|
|
485
|
+
id: :req_id?,
|
|
486
|
+
created_at: :created?
|
|
487
|
+
|
|
488
|
+
without :response,
|
|
489
|
+
request_id: :req_id?
|
|
490
|
+
|
|
491
|
+
on :request, {},
|
|
492
|
+
predicate: lambda { |f|
|
|
493
|
+
(Time.now - f[:created_at]) > 300 # 5 minutes
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
perform do |facts, bindings|
|
|
497
|
+
alert("Request #{bindings[:req_id?]} delayed!")
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Priority Management
|
|
504
|
+
|
|
505
|
+
### Priority Levels
|
|
506
|
+
|
|
507
|
+
Establish consistent priority levels for your domain:
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
# Define priority constants
|
|
511
|
+
module Priority
|
|
512
|
+
CRITICAL = 100 # Emergency, safety
|
|
513
|
+
HIGH = 75 # Important business logic
|
|
514
|
+
NORMAL = 50 # Standard processing
|
|
515
|
+
LOW = 25 # Cleanup, logging
|
|
516
|
+
MONITORING = 10 # Metrics, diagnostics
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Use in rules
|
|
520
|
+
KBS.knowledge_base do
|
|
521
|
+
rule "emergency_shutdown", priority: Priority::CRITICAL do
|
|
522
|
+
# ...
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
rule "process_order", priority: Priority::NORMAL do
|
|
526
|
+
# ...
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Priority Inversion
|
|
532
|
+
|
|
533
|
+
Avoid priority inversions where low-priority rules block high-priority rules:
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
# Bad: Low priority rule creates fact needed by high priority rule
|
|
537
|
+
KBS.knowledge_base do
|
|
538
|
+
rule "compute_risk", priority: 10 do
|
|
539
|
+
on :data, value: :v?
|
|
540
|
+
perform do |facts, bindings|
|
|
541
|
+
fact :risk_score, value: calculate_risk(bindings[:v?])
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
rule "emergency_check", priority: 100 do
|
|
546
|
+
on :risk_score, value: :risk? # Depends on low priority rule!
|
|
547
|
+
perform do |facts, bindings|
|
|
548
|
+
emergency_shutdown if bindings[:risk?] > 90
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Fix: Make dependency higher priority
|
|
554
|
+
KBS.knowledge_base do
|
|
555
|
+
rule "compute_risk", priority: 110 do # Now runs before emergency_check
|
|
556
|
+
on :data, value: :v?
|
|
557
|
+
perform do |facts, bindings|
|
|
558
|
+
fact :risk_score, value: calculate_risk(bindings[:v?])
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
rule "emergency_check", priority: 100 do
|
|
563
|
+
on :risk_score, value: :risk?
|
|
564
|
+
perform do |facts, bindings|
|
|
565
|
+
emergency_shutdown if bindings[:risk?] > 90
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Testing Strategies
|
|
572
|
+
|
|
573
|
+
### Unit Test Rules in Isolation
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
require 'minitest/autorun'
|
|
577
|
+
require 'kbs'
|
|
578
|
+
|
|
579
|
+
class TestTemperatureRules < Minitest::Test
|
|
580
|
+
def test_fires_when_temp_exceeds_threshold
|
|
581
|
+
alert_fired = false
|
|
582
|
+
|
|
583
|
+
kb = KBS.knowledge_base do
|
|
584
|
+
rule "high_temp_alert" do
|
|
585
|
+
on :sensor, id: :id?, temp: :temp?
|
|
586
|
+
on :threshold, id: :id?, max: :max?
|
|
587
|
+
|
|
588
|
+
perform do |facts, bindings|
|
|
589
|
+
alert_fired = true if bindings[:temp?] > bindings[:max?]
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
fact :sensor, id: "bedroom", temp: 30
|
|
594
|
+
fact :threshold, id: "bedroom", max: 25
|
|
595
|
+
run
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
assert alert_fired, "Rule should fire when temp > threshold"
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def test_does_not_fire_when_temp_below_threshold
|
|
602
|
+
alert_fired = false
|
|
603
|
+
|
|
604
|
+
kb = KBS.knowledge_base do
|
|
605
|
+
rule "high_temp_alert" do
|
|
606
|
+
on :sensor, id: :id?, temp: :temp?
|
|
607
|
+
on :threshold, id: :id?, max: :max?
|
|
608
|
+
|
|
609
|
+
perform do |facts, bindings|
|
|
610
|
+
alert_fired = true if bindings[:temp?] > bindings[:max?]
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
fact :sensor, id: "bedroom", temp: 20
|
|
615
|
+
fact :threshold, id: "bedroom", max: 25
|
|
616
|
+
run
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
refute alert_fired, "Rule should not fire when temp <= threshold"
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def test_only_fires_for_matching_sensor
|
|
623
|
+
alert_fired = false
|
|
624
|
+
|
|
625
|
+
kb = KBS.knowledge_base do
|
|
626
|
+
rule "high_temp_alert" do
|
|
627
|
+
on :sensor, id: :id?, temp: :temp?
|
|
628
|
+
on :threshold, id: :id?, max: :max?
|
|
629
|
+
|
|
630
|
+
perform do |facts, bindings|
|
|
631
|
+
alert_fired = true if bindings[:temp?] > bindings[:max?]
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
fact :sensor, id: "bedroom", temp: 30
|
|
636
|
+
fact :threshold, id: "kitchen", max: 25
|
|
637
|
+
run
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
refute alert_fired, "Rule should not fire for different sensors"
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Integration Tests
|
|
646
|
+
|
|
647
|
+
Test multiple rules working together:
|
|
648
|
+
|
|
649
|
+
```ruby
|
|
650
|
+
def test_state_machine_workflow
|
|
651
|
+
kb = KBS.knowledge_base do
|
|
652
|
+
# Add state transition rules
|
|
653
|
+
rule "start_processing" do
|
|
654
|
+
on :order, id: :id?, status: "pending"
|
|
655
|
+
perform do |facts, bindings|
|
|
656
|
+
engine.remove_fact(facts[0])
|
|
657
|
+
fact :order, id: bindings[:id?], status: "processing"
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
rule "complete_processing" do
|
|
662
|
+
on :order, id: :id?, status: "processing"
|
|
663
|
+
on :processing_done, order_id: :id?
|
|
664
|
+
perform do |facts, bindings|
|
|
665
|
+
engine.remove_fact(facts[0])
|
|
666
|
+
engine.remove_fact(facts[1])
|
|
667
|
+
fact :order, id: bindings[:id?], status: "completed"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Add initial state
|
|
672
|
+
fact :order, id: 1, status: "pending"
|
|
673
|
+
run
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Should not transition yet
|
|
677
|
+
order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
|
|
678
|
+
assert_equal "pending", order[:status]
|
|
679
|
+
|
|
680
|
+
# Trigger transition
|
|
681
|
+
kb.fact :processing_done, order_id: 1
|
|
682
|
+
kb.run
|
|
683
|
+
|
|
684
|
+
# Should transition to completed
|
|
685
|
+
order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
|
|
686
|
+
assert_equal "completed", order[:status]
|
|
687
|
+
end
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Property-Based Testing
|
|
691
|
+
|
|
692
|
+
Test rule invariants:
|
|
693
|
+
|
|
694
|
+
```ruby
|
|
695
|
+
def test_no_duplicate_alerts
|
|
696
|
+
kb = KBS.knowledge_base do
|
|
697
|
+
rule "send_alert_once" do
|
|
698
|
+
on :high_temp, sensor_id: :id?
|
|
699
|
+
without :alert_sent, sensor_id: :id?
|
|
700
|
+
|
|
701
|
+
perform do |facts, bindings|
|
|
702
|
+
send_alert(bindings[:id?])
|
|
703
|
+
fact :alert_sent, sensor_id: bindings[:id?]
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Add facts
|
|
708
|
+
100.times do |i|
|
|
709
|
+
fact :high_temp, sensor_id: i
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Run engine multiple times
|
|
713
|
+
10.times { run }
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Property: At most one alert per sensor
|
|
717
|
+
alert_counts = kb.engine.facts
|
|
718
|
+
.select { |f| f.type == :alert_sent }
|
|
719
|
+
.group_by { |f| f[:sensor_id] }
|
|
720
|
+
.transform_values(&:count)
|
|
721
|
+
|
|
722
|
+
alert_counts.each do |sensor_id, count|
|
|
723
|
+
assert_equal 1, count, "Sensor #{sensor_id} has #{count} alerts, expected 1"
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
## Performance Optimization
|
|
729
|
+
|
|
730
|
+
### Minimize Negations
|
|
731
|
+
|
|
732
|
+
Negations are expensive:
|
|
733
|
+
|
|
734
|
+
```ruby
|
|
735
|
+
# Expensive: 3 negations
|
|
736
|
+
KBS.knowledge_base do
|
|
737
|
+
rule "many_negations" do
|
|
738
|
+
without :foo, {}
|
|
739
|
+
without :bar, {}
|
|
740
|
+
without :baz, {}
|
|
741
|
+
perform { }
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Better: Combine into positive condition
|
|
746
|
+
KBS.knowledge_base do
|
|
747
|
+
rule "positive_logic" do
|
|
748
|
+
on :conditions_met, {}
|
|
749
|
+
perform { }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Add conditions_met fact if foo, bar, baz don't exist
|
|
753
|
+
unless engine.facts.any? { |f| [:foo, :bar, :baz].include?(f.type) }
|
|
754
|
+
fact :conditions_met, {}
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Avoid Predicates for Simple Checks
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
# Expensive: Predicate disables network sharing
|
|
763
|
+
KBS.knowledge_base do
|
|
764
|
+
rule "with_predicate" do
|
|
765
|
+
on :stock, {}, predicate: lambda { |f| f[:symbol] == "AAPL" }
|
|
766
|
+
perform { }
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# Better: Use pattern matching
|
|
771
|
+
KBS.knowledge_base do
|
|
772
|
+
rule "with_pattern" do
|
|
773
|
+
on :stock, symbol: "AAPL"
|
|
774
|
+
perform { }
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Cache Computed Values
|
|
780
|
+
|
|
781
|
+
```ruby
|
|
782
|
+
# Bad: Recomputes every time rule fires
|
|
783
|
+
KBS.knowledge_base do
|
|
784
|
+
rule "check_average" do
|
|
785
|
+
on :sensor, temp: :temp?
|
|
786
|
+
|
|
787
|
+
perform do |facts, bindings|
|
|
788
|
+
avg = compute_expensive_average(engine.facts)
|
|
789
|
+
alert(avg) if avg > threshold
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Good: Cache as fact, recompute only when needed
|
|
795
|
+
KBS.knowledge_base do
|
|
796
|
+
rule "update_average", priority: 100 do
|
|
797
|
+
on :sensor, temp: :temp? # Triggers when sensor added
|
|
798
|
+
|
|
799
|
+
perform do |facts, bindings|
|
|
800
|
+
avg = compute_expensive_average(engine.facts)
|
|
801
|
+
fact :cached_average, value: avg
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
rule "check_average", priority: 50 do
|
|
806
|
+
on :cached_average, value: :avg?
|
|
807
|
+
|
|
808
|
+
perform do |facts, bindings|
|
|
809
|
+
alert(bindings[:avg?]) if bindings[:avg?] > threshold
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## Common Pitfalls
|
|
816
|
+
|
|
817
|
+
### 1. Infinite Loops
|
|
818
|
+
|
|
819
|
+
```ruby
|
|
820
|
+
# Bad: Rule fires itself indefinitely
|
|
821
|
+
KBS.knowledge_base do
|
|
822
|
+
rule "infinite_loop" do
|
|
823
|
+
on :sensor, temp: :temp?
|
|
824
|
+
|
|
825
|
+
perform do |facts, bindings|
|
|
826
|
+
# This triggers the rule again!
|
|
827
|
+
fact :sensor, temp: bindings[:temp?] + 1
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Fix: Add termination condition
|
|
833
|
+
KBS.knowledge_base do
|
|
834
|
+
rule "limited_increment" do
|
|
835
|
+
on :sensor, temp: :temp?
|
|
836
|
+
without :increment_done, {}
|
|
837
|
+
|
|
838
|
+
perform do |facts, bindings|
|
|
839
|
+
fact :sensor, temp: bindings[:temp?] + 1
|
|
840
|
+
fact :increment_done, {}
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
### 2. Variable Scope Confusion
|
|
847
|
+
|
|
848
|
+
```ruby
|
|
849
|
+
# Bad: Closure captures wrong variable
|
|
850
|
+
rules = []
|
|
851
|
+
%w[sensor1 sensor2 sensor3].each do |sensor|
|
|
852
|
+
# All rules reference same 'sensor' variable (last value!)
|
|
853
|
+
kb = KBS.knowledge_base do
|
|
854
|
+
rule "process_#{sensor}" do
|
|
855
|
+
on :reading, {}
|
|
856
|
+
perform { puts sensor } # Wrong!
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Fix: Force closure with parameter
|
|
862
|
+
%w[sensor1 sensor2 sensor3].each do |sensor_name|
|
|
863
|
+
captured_sensor = sensor_name # Force capture
|
|
864
|
+
|
|
865
|
+
kb = KBS.knowledge_base do
|
|
866
|
+
rule "process_#{captured_sensor}" do
|
|
867
|
+
on :reading, {}
|
|
868
|
+
perform { puts captured_sensor } # Correct
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### 3. Forgetting to Call `run`
|
|
875
|
+
|
|
876
|
+
```ruby
|
|
877
|
+
# Bad: Facts added but never matched
|
|
878
|
+
kb = KBS.knowledge_base do
|
|
879
|
+
rule "example" do
|
|
880
|
+
on :sensor, temp: :temp?
|
|
881
|
+
on :threshold, max: :max?
|
|
882
|
+
perform { }
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
fact :sensor, temp: 30
|
|
886
|
+
fact :threshold, max: 25
|
|
887
|
+
# Rules never fire!
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Good: Run after adding facts
|
|
891
|
+
kb = KBS.knowledge_base do
|
|
892
|
+
rule "example" do
|
|
893
|
+
on :sensor, temp: :temp?
|
|
894
|
+
on :threshold, max: :max?
|
|
895
|
+
perform { }
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
fact :sensor, temp: 30
|
|
899
|
+
fact :threshold, max: 25
|
|
900
|
+
run # Match and fire rules
|
|
901
|
+
end
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
## Next Steps
|
|
905
|
+
|
|
906
|
+
- **[Pattern Matching](pattern-matching.md)** - Deep dive into condition matching
|
|
907
|
+
- **[Variable Binding](variable-binding.md)** - Join tests and binding extraction
|
|
908
|
+
- **[Negation](negation.md)** - Negated condition behavior
|
|
909
|
+
- **[Performance Guide](../advanced/performance.md)** - Profiling and optimization
|
|
910
|
+
- **[Testing Guide](../advanced/testing.md)** - Comprehensive test strategies
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
*Well-designed rules are self-documenting. If a rule is hard to understand, it's probably doing too much.*
|