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,694 @@
|
|
|
1
|
+
# What is a Fact?
|
|
2
|
+
|
|
3
|
+
A **fact** is the fundamental unit of knowledge in KBS - a piece of information about your domain that the system can reason about. Facts are the "data" on which rules operate.
|
|
4
|
+
|
|
5
|
+
## Core Concept
|
|
6
|
+
|
|
7
|
+
Think of a fact as a **typed data record** that represents something true at a particular moment:
|
|
8
|
+
|
|
9
|
+
- "The temperature in the server room is 85°F"
|
|
10
|
+
- "Stock AAPL is trading at $150.25 with volume 1.2M"
|
|
11
|
+
- "Sensor #42 is active"
|
|
12
|
+
- "Order #123 is pending"
|
|
13
|
+
|
|
14
|
+
Each fact has:
|
|
15
|
+
|
|
16
|
+
1. **Type** - What kind of thing this is (`:temperature`, `:stock`, `:sensor`, `:order`)
|
|
17
|
+
2. **Attributes** - Key-value pairs describing it (`location: "server_room"`, `value: 85`)
|
|
18
|
+
|
|
19
|
+
## Anatomy of a Fact
|
|
20
|
+
|
|
21
|
+
### Structure
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
25
|
+
|
|
26
|
+
fact.type # => :temperature
|
|
27
|
+
fact.attributes # => {:location => "server_room", :value => 85}
|
|
28
|
+
fact[:location] # => "server_room"
|
|
29
|
+
fact[:value] # => 85
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Visual Representation
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
*A fact consists of a type symbol and a hash of attribute key-value pairs.*
|
|
37
|
+
|
|
38
|
+
## How Facts Differ from Other Data Structures
|
|
39
|
+
|
|
40
|
+
| Aspect | Fact | Plain Hash | Database Row | Object |
|
|
41
|
+
|--------|------|-----------|--------------|--------|
|
|
42
|
+
| **Type** | Explicit (`:temperature`) | None | Table name | Class |
|
|
43
|
+
| **Pattern Matching** | Built-in | Manual | SQL WHERE | Manual |
|
|
44
|
+
| **Identity** | By content & type | By reference | By primary key | By reference |
|
|
45
|
+
| **Purpose** | Reasoning & inference | General storage | Persistent storage | Behavior + data |
|
|
46
|
+
| **Lifecycle** | Add/retract from KB | Create/destroy | Insert/delete | New/GC |
|
|
47
|
+
|
|
48
|
+
**Example Comparison:**
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# Plain Hash
|
|
52
|
+
data = { location: "server_room", value: 85 }
|
|
53
|
+
# What kind of data is this? No way to tell.
|
|
54
|
+
|
|
55
|
+
# Database Row
|
|
56
|
+
# SELECT * FROM temperatures WHERE location = 'server_room'
|
|
57
|
+
# Requires SQL, separate from logic
|
|
58
|
+
|
|
59
|
+
# Object
|
|
60
|
+
class Temperature
|
|
61
|
+
attr_accessor :location, :value
|
|
62
|
+
end
|
|
63
|
+
temp = Temperature.new
|
|
64
|
+
# Has behavior but no built-in pattern matching
|
|
65
|
+
|
|
66
|
+
# Fact
|
|
67
|
+
fact = KBS::Fact.new(:temperature, location: "server_room", value: 85)
|
|
68
|
+
# Self-describing, pattern-matchable, inference-ready
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Fact Lifecycle
|
|
72
|
+
|
|
73
|
+
### 1. Creation
|
|
74
|
+
|
|
75
|
+
Facts are created and added to a knowledge base:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# In-memory knowledge base
|
|
79
|
+
kb = KBS.knowledge_base do
|
|
80
|
+
fact :temperature, location: "server_room", value: 85
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Blackboard (persistent)
|
|
84
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
85
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Pattern Matching
|
|
89
|
+
|
|
90
|
+
Once added, facts are automatically matched against rule patterns:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
rule "high_temperature" do
|
|
94
|
+
# This pattern matches our fact above
|
|
95
|
+
on :temperature, location: "server_room", value: greater_than(80)
|
|
96
|
+
perform { puts "Alert!" }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Rule Firing
|
|
101
|
+
|
|
102
|
+
When all conditions of a rule match, the rule fires:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
kb.run # → "Alert!" (rule fires because fact matches)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4. Updates (Blackboard Only)
|
|
109
|
+
|
|
110
|
+
Persistent facts can be updated:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
fact[:value] = 90 # Update persisted immediately
|
|
114
|
+
fact.update(value: 90, timestamp: Time.now) # Bulk update
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Note**: Updates do NOT trigger rule re-evaluation. To re-trigger rules, retract and re-add.
|
|
118
|
+
|
|
119
|
+
### 5. Retraction
|
|
120
|
+
|
|
121
|
+
Facts can be removed from working memory:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# DSL
|
|
125
|
+
retract fact
|
|
126
|
+
|
|
127
|
+
# Blackboard - fact can retract itself
|
|
128
|
+
fact.retract
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Fact Types (Implementations)
|
|
132
|
+
|
|
133
|
+
KBS provides two fact implementations:
|
|
134
|
+
|
|
135
|
+
### 1. Transient Facts (`KBS::Fact`)
|
|
136
|
+
|
|
137
|
+
- **Used by**: In-memory knowledge bases
|
|
138
|
+
- **Identity**: Ruby object ID
|
|
139
|
+
- **Persistence**: None (lost on process exit)
|
|
140
|
+
- **Performance**: Fast (no I/O)
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
144
|
+
puts fact.id # => 70123456789012 (Ruby object ID)
|
|
145
|
+
|
|
146
|
+
# Lightweight, perfect for short-lived reasoning
|
|
147
|
+
kb = KBS.knowledge_base do
|
|
148
|
+
fact :stock, symbol: "AAPL", price: 150
|
|
149
|
+
run
|
|
150
|
+
end
|
|
151
|
+
# Facts disappear when kb goes out of scope
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Best for**:
|
|
155
|
+
|
|
156
|
+
- Event stream processing
|
|
157
|
+
- Short-lived analyses
|
|
158
|
+
- Prototyping and testing
|
|
159
|
+
- When restart durability isn't needed
|
|
160
|
+
|
|
161
|
+
### 2. Persistent Facts (`KBS::Blackboard::Fact`)
|
|
162
|
+
|
|
163
|
+
- **Used by**: Blackboard knowledge bases
|
|
164
|
+
- **Identity**: UUID (stable across restarts)
|
|
165
|
+
- **Persistence**: SQLite, Redis, or Hybrid storage
|
|
166
|
+
- **Audit Trail**: Complete change history
|
|
167
|
+
- **Performance**: Slower (I/O overhead)
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
171
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
172
|
+
|
|
173
|
+
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
174
|
+
|
|
175
|
+
# Update persists
|
|
176
|
+
fact[:value] = 90
|
|
177
|
+
|
|
178
|
+
# Restart process
|
|
179
|
+
engine2 = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
180
|
+
reloaded = engine2.blackboard.get_facts_by_type(:temperature).first
|
|
181
|
+
puts reloaded[:value] # => 90 (persisted)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Best for**:
|
|
185
|
+
|
|
186
|
+
- Long-running systems
|
|
187
|
+
- Multi-agent coordination
|
|
188
|
+
- Audit requirements
|
|
189
|
+
- Systems that must survive restarts
|
|
190
|
+
- Distributed reasoning
|
|
191
|
+
|
|
192
|
+
## Pattern Matching
|
|
193
|
+
|
|
194
|
+
Facts excel at pattern matching—the ability to find facts that satisfy specific criteria.
|
|
195
|
+
|
|
196
|
+
### Literal Matching
|
|
197
|
+
|
|
198
|
+
Match exact values:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
fact = KBS::Fact.new(:stock, symbol: "AAPL", price: 150.25)
|
|
202
|
+
|
|
203
|
+
# Matches
|
|
204
|
+
fact.matches?(type: :stock, symbol: "AAPL") # => true
|
|
205
|
+
|
|
206
|
+
# Doesn't match
|
|
207
|
+
fact.matches?(type: :stock, symbol: "GOOGL") # => false
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Predicate Matching
|
|
211
|
+
|
|
212
|
+
Match with lambda conditions:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
216
|
+
|
|
217
|
+
# Matches
|
|
218
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 80 }) # => true
|
|
219
|
+
fact.matches?(type: :temperature, value: ->(v) { v < 100 }) # => true
|
|
220
|
+
|
|
221
|
+
# Doesn't match
|
|
222
|
+
fact.matches?(type: :temperature, value: ->(v) { v > 90 }) # => false
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Variable Binding
|
|
226
|
+
|
|
227
|
+
Capture values for use in rule actions:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
rule "report_temperature" do
|
|
231
|
+
on :temperature, location: :loc?, value: :temp?
|
|
232
|
+
# ^^^^^^ ^^^^^^
|
|
233
|
+
# Variables (end with ?)
|
|
234
|
+
|
|
235
|
+
perform do |facts, bindings|
|
|
236
|
+
# bindings contains captured values
|
|
237
|
+
puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Add fact
|
|
242
|
+
fact :temperature, location: "server_room", value: 85
|
|
243
|
+
|
|
244
|
+
run # → "server_room: 85°F"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Join Tests
|
|
248
|
+
|
|
249
|
+
Variables create joins across multiple facts:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
rule "inventory_check" do
|
|
253
|
+
on :order, product_id: :pid?, quantity: :qty?
|
|
254
|
+
on :inventory, product_id: :pid?, available: :avail?
|
|
255
|
+
# ^^^^^^
|
|
256
|
+
# Same variable = JOIN condition
|
|
257
|
+
|
|
258
|
+
perform do |facts, bindings|
|
|
259
|
+
# Only fires when BOTH facts have same product_id
|
|
260
|
+
if bindings[:avail?] < bindings[:qty?]
|
|
261
|
+
puts "Insufficient inventory for #{bindings[:pid?]}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Common Fact Patterns
|
|
268
|
+
|
|
269
|
+
### 1. Entity Facts
|
|
270
|
+
|
|
271
|
+
Represent domain objects:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
fact :customer, id: 12345, name: "Acme Corp", tier: "gold"
|
|
275
|
+
fact :product, sku: "ABC-123", price: 49.99, in_stock: true
|
|
276
|
+
fact :order, id: 789, customer_id: 12345, total: 499.90
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 2. Event Facts
|
|
280
|
+
|
|
281
|
+
Represent things that happened:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
fact :order_placed, order_id: 789, timestamp: Time.now
|
|
285
|
+
fact :payment_received, order_id: 789, amount: 499.90
|
|
286
|
+
fact :item_shipped, tracking: "1Z999", order_id: 789
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 3. Sensor Facts
|
|
290
|
+
|
|
291
|
+
Real-time measurements:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
fact :temperature, sensor_id: 42, value: 85, timestamp: Time.now
|
|
295
|
+
fact :pressure, sensor_id: 43, value: 14.7, unit: "psi"
|
|
296
|
+
fact :motion_detected, camera_id: 5, location: "entrance"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 4. State Facts
|
|
300
|
+
|
|
301
|
+
Current system state:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
fact :connection, server: "db-1", status: "active"
|
|
305
|
+
fact :worker, id: 3, status: "busy", task_id: 456
|
|
306
|
+
fact :cache, key: "user:123", valid_until: Time.now + 3600
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 5. Derived Facts
|
|
310
|
+
|
|
311
|
+
Facts inferred from other facts:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
rule "derive_alert" do
|
|
315
|
+
on :temperature, value: greater_than(80), location: :loc?
|
|
316
|
+
without :alert, location: :loc? # No existing alert
|
|
317
|
+
|
|
318
|
+
perform do |facts, bindings|
|
|
319
|
+
# Add derived fact
|
|
320
|
+
fact :alert,
|
|
321
|
+
location: bindings[:loc?],
|
|
322
|
+
level: "high",
|
|
323
|
+
source: "temperature_monitor"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 6. Flag Facts
|
|
329
|
+
|
|
330
|
+
Boolean markers (attributes optional):
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
fact :system_ready
|
|
334
|
+
fact :maintenance_mode
|
|
335
|
+
fact :debug_enabled
|
|
336
|
+
fact :cache_warmed
|
|
337
|
+
|
|
338
|
+
# Used in rules
|
|
339
|
+
rule "process_requests" do
|
|
340
|
+
on :system_ready
|
|
341
|
+
without :maintenance_mode
|
|
342
|
+
on :request, id: :req_id?
|
|
343
|
+
perform { |facts, b| handle_request(b[:req_id?]) }
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Fact vs. Rule Relationship
|
|
348
|
+
|
|
349
|
+
Facts and rules work together in a symbiotic relationship:
|
|
350
|
+
|
|
351
|
+

|
|
352
|
+
|
|
353
|
+
*Facts (data) and rules (logic) interact through pattern matching: rules match facts, execute actions, and may create new facts, continuing the inference cycle.*
|
|
354
|
+
|
|
355
|
+
**Example:**
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
# FACTS represent the current state
|
|
359
|
+
fact :stock, symbol: "AAPL", price: 150, volume: 1_000_000
|
|
360
|
+
fact :portfolio, cash: 10_000, max_position: 5_000
|
|
361
|
+
|
|
362
|
+
# RULES define logic
|
|
363
|
+
rule "momentum_buy" do
|
|
364
|
+
# IF these facts exist with these patterns...
|
|
365
|
+
on :stock, symbol: :sym?, price: :price?, volume: greater_than(500_000)
|
|
366
|
+
on :portfolio, cash: :cash?, max_position: :max?
|
|
367
|
+
|
|
368
|
+
# THEN execute this action
|
|
369
|
+
perform do |facts, bindings|
|
|
370
|
+
position_size = [bindings[:max?], bindings[:cash?] * 0.1].min
|
|
371
|
+
shares = (position_size / bindings[:price?]).floor
|
|
372
|
+
|
|
373
|
+
if shares > 0
|
|
374
|
+
# Action may create new facts
|
|
375
|
+
fact :order,
|
|
376
|
+
symbol: bindings[:sym?],
|
|
377
|
+
shares: shares,
|
|
378
|
+
type: "market_buy"
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Fact Semantics
|
|
385
|
+
|
|
386
|
+
### Open World Assumption
|
|
387
|
+
|
|
388
|
+
Facts can have any attributes. Patterns only constrain what they mention:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# Fact has 4 attributes
|
|
392
|
+
fact = KBS::Fact.new(:stock,
|
|
393
|
+
symbol: "AAPL",
|
|
394
|
+
price: 150,
|
|
395
|
+
volume: 1_000_000,
|
|
396
|
+
exchange: "NASDAQ"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Pattern only constrains 2 - still matches!
|
|
400
|
+
fact.matches?(type: :stock, symbol: "AAPL") # => true
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Closed Attribute Assumption
|
|
404
|
+
|
|
405
|
+
If a pattern requires an attribute, the fact must have it:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
fact = KBS::Fact.new(:stock, symbol: "AAPL", price: 150)
|
|
409
|
+
# No :volume attribute
|
|
410
|
+
|
|
411
|
+
# Fails - fact missing required :volume
|
|
412
|
+
fact.matches?(type: :stock, volume: greater_than(1000)) # => false
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Type Safety
|
|
416
|
+
|
|
417
|
+
Type is always checked first:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
fact = KBS::Fact.new(:stock, symbol: "AAPL")
|
|
421
|
+
|
|
422
|
+
# Fails immediately - wrong type
|
|
423
|
+
fact.matches?(type: :temperature) # => false
|
|
424
|
+
|
|
425
|
+
# Succeeds - right type
|
|
426
|
+
fact.matches?(type: :stock) # => true
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Value Immutability (Transient Facts)
|
|
430
|
+
|
|
431
|
+
Transient facts should be treated as immutable. Changing attributes doesn't trigger re-evaluation:
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
fact = KBS::Fact.new(:temperature, value: 85)
|
|
435
|
+
engine.add_fact(fact)
|
|
436
|
+
|
|
437
|
+
# Don't do this - change not tracked
|
|
438
|
+
fact[:value] = 90 # Rules won't re-fire
|
|
439
|
+
|
|
440
|
+
# Instead, retract and re-add
|
|
441
|
+
engine.remove_fact(fact)
|
|
442
|
+
new_fact = KBS::Fact.new(:temperature, value: 90)
|
|
443
|
+
engine.add_fact(new_fact)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Value Mutability (Persistent Facts)
|
|
447
|
+
|
|
448
|
+
Persistent facts track updates but don't re-trigger rules:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
452
|
+
|
|
453
|
+
# This persists but doesn't re-fire rules
|
|
454
|
+
fact[:value] = 90
|
|
455
|
+
|
|
456
|
+
# To re-trigger rules, retract and re-add
|
|
457
|
+
fact.retract
|
|
458
|
+
new_fact = engine.add_fact(:temperature, value: 90)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Performance Considerations
|
|
462
|
+
|
|
463
|
+
### Fact Count Impact
|
|
464
|
+
|
|
465
|
+
- **RETE strength**: Efficient with many facts and stable rules
|
|
466
|
+
- **Alpha memories**: Facts indexed by type
|
|
467
|
+
- **Beta network**: Partial matches cached as tokens
|
|
468
|
+
- **Unlinking**: Empty nodes deactivated automatically
|
|
469
|
+
|
|
470
|
+
**Scaling characteristics**:
|
|
471
|
+
|
|
472
|
+
- 10-1,000 facts: Excellent performance
|
|
473
|
+
- 1,000-10,000 facts: Very good (alpha memory indexing helps)
|
|
474
|
+
- 10,000-100,000 facts: Good (consider indexing strategies)
|
|
475
|
+
- 100,000+ facts: Consider domain-specific optimizations
|
|
476
|
+
|
|
477
|
+
### Attribute Count Impact
|
|
478
|
+
|
|
479
|
+
Facts can have any number of attributes:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
# Small fact (fast)
|
|
483
|
+
fact :flag, active: true
|
|
484
|
+
|
|
485
|
+
# Medium fact (typical)
|
|
486
|
+
fact :order,
|
|
487
|
+
id: 123,
|
|
488
|
+
customer_id: 456,
|
|
489
|
+
total: 99.99,
|
|
490
|
+
status: "pending"
|
|
491
|
+
|
|
492
|
+
# Large fact (fine, but consider if all attributes needed)
|
|
493
|
+
fact :trade,
|
|
494
|
+
symbol: "AAPL",
|
|
495
|
+
price: 150.25,
|
|
496
|
+
volume: 1000,
|
|
497
|
+
timestamp: Time.now,
|
|
498
|
+
order_id: 789,
|
|
499
|
+
account_id: 456,
|
|
500
|
+
commission: 1.50,
|
|
501
|
+
exchange: "NASDAQ",
|
|
502
|
+
# ... 20 more attributes
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Guideline**: Include attributes you'll pattern match on. Store auxiliary data in external systems if not needed for rules.
|
|
506
|
+
|
|
507
|
+
### Pattern Complexity Impact
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
# Fast - literal match (hash equality)
|
|
511
|
+
on :stock, symbol: "AAPL"
|
|
512
|
+
|
|
513
|
+
# Medium - simple predicate
|
|
514
|
+
on :stock, price: ->(p) { p > 100 }
|
|
515
|
+
|
|
516
|
+
# Slow - complex predicate (runs on every match attempt)
|
|
517
|
+
on :stock, price: ->(p) {
|
|
518
|
+
historical_data = fetch_history(p) # External call!
|
|
519
|
+
calculate_volatility(historical_data) > threshold
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Guideline**: Keep predicates simple. Do expensive checks in rule actions, not patterns.
|
|
524
|
+
|
|
525
|
+
## Common Pitfalls
|
|
526
|
+
|
|
527
|
+
### 1. Forgetting Fact Type
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
# Wrong - no type
|
|
531
|
+
fact = { location: "server_room", value: 85 }
|
|
532
|
+
|
|
533
|
+
# Right - always include type
|
|
534
|
+
fact :temperature, location: "server_room", value: 85
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### 2. Expecting Updates to Re-trigger Rules
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
541
|
+
|
|
542
|
+
# This rule fires
|
|
543
|
+
rule "high_temp" do
|
|
544
|
+
on :temperature, value: greater_than(80)
|
|
545
|
+
perform { puts "High!" }
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Update doesn't re-fire rule
|
|
549
|
+
fact[:value] = 90 # Rule doesn't fire again
|
|
550
|
+
|
|
551
|
+
# Must retract and re-add to re-trigger
|
|
552
|
+
fact.retract
|
|
553
|
+
engine.add_fact(:temperature, value: 90)
|
|
554
|
+
engine.run # Now rule fires
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 3. Side Effects in Predicates
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
# Wrong - side effects
|
|
561
|
+
counter = 0
|
|
562
|
+
on :stock, price: ->(p) {
|
|
563
|
+
counter += 1 # Bad! Runs on every match attempt
|
|
564
|
+
p > 100
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# Right - pure predicate
|
|
568
|
+
threshold = 100
|
|
569
|
+
on :stock, price: ->(p) { p > threshold }
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### 4. Missing Attributes in Predicates
|
|
573
|
+
|
|
574
|
+
```ruby
|
|
575
|
+
fact = KBS::Fact.new(:stock, symbol: "AAPL") # No :price
|
|
576
|
+
|
|
577
|
+
# Fails - predicate can't evaluate nil
|
|
578
|
+
fact.matches?(type: :stock, price: ->(p) { p > 100 }) # => false
|
|
579
|
+
|
|
580
|
+
# Use variable to capture nil
|
|
581
|
+
fact.matches?(type: :stock, price: :price?) # => true (binds :price? => nil)
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### 5. Confusing Negation
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
# Matches when NO critical alert EXISTS
|
|
588
|
+
without :alert, level: "critical"
|
|
589
|
+
|
|
590
|
+
# NOT the same as: Match alerts that aren't critical
|
|
591
|
+
# For that, use:
|
|
592
|
+
on :alert, level: ->(l) { l != "critical" }
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Best Practices
|
|
596
|
+
|
|
597
|
+
### 1. Use Descriptive Fact Types
|
|
598
|
+
|
|
599
|
+
```ruby
|
|
600
|
+
# Good - clear semantic meaning
|
|
601
|
+
fact :temperature_reading, sensor_id: 42, value: 85
|
|
602
|
+
fact :order_placed, order_id: 123, timestamp: Time.now
|
|
603
|
+
fact :inventory_shortage, product_id: "ABC", deficit: 50
|
|
604
|
+
|
|
605
|
+
# Avoid - vague types
|
|
606
|
+
fact :data, type: "temp", id: 42, val: 85
|
|
607
|
+
fact :event, kind: "order", timestamp: Time.now
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 2. Include Identifying Attributes
|
|
611
|
+
|
|
612
|
+
```ruby
|
|
613
|
+
# Good - can query and match specifically
|
|
614
|
+
fact :sensor, id: 42, status: "active", location: "room_1"
|
|
615
|
+
fact :order, id: 123, customer_id: 456, total: 99.99
|
|
616
|
+
|
|
617
|
+
# Harder to work with - no unique identifier
|
|
618
|
+
fact :sensor, status: "active"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### 3. Add Timestamps for Time-Based Reasoning
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
fact :temperature,
|
|
625
|
+
sensor_id: 42,
|
|
626
|
+
value: 85,
|
|
627
|
+
timestamp: Time.now
|
|
628
|
+
|
|
629
|
+
# Enables rules like:
|
|
630
|
+
rule "stale_data" do
|
|
631
|
+
on :temperature,
|
|
632
|
+
timestamp: ->(ts) { Time.now - ts > 300 }
|
|
633
|
+
perform { puts "Stale data!" }
|
|
634
|
+
end
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### 4. Use Fact Types to Model Domain
|
|
638
|
+
|
|
639
|
+
Organize facts around your domain concepts:
|
|
640
|
+
|
|
641
|
+
**Stock Trading:**
|
|
642
|
+
```ruby
|
|
643
|
+
fact :stock, symbol: "AAPL", price: 150, volume: 1_000_000
|
|
644
|
+
fact :order, id: 123, type: "buy", shares: 100
|
|
645
|
+
fact :position, symbol: "AAPL", shares: 500, cost_basis: 145
|
|
646
|
+
fact :alert, level: "high", message: "Price spike detected"
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**IoT Monitoring:**
|
|
650
|
+
```ruby
|
|
651
|
+
fact :sensor, id: 42, type: "temperature", location: "server_1"
|
|
652
|
+
fact :reading, sensor_id: 42, value: 85, timestamp: Time.now
|
|
653
|
+
fact :threshold, sensor_id: 42, max: 80, min: 60
|
|
654
|
+
fact :alert, sensor_id: 42, severity: "warning"
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 5. Keep Facts Focused
|
|
658
|
+
|
|
659
|
+
One fact = one piece of knowledge
|
|
660
|
+
|
|
661
|
+
```ruby
|
|
662
|
+
# Good - focused facts
|
|
663
|
+
fact :order, id: 123, status: "pending"
|
|
664
|
+
fact :customer, id: 456, name: "Acme"
|
|
665
|
+
fact :payment, order_id: 123, amount: 99.99
|
|
666
|
+
|
|
667
|
+
# Avoid - bloated fact with everything
|
|
668
|
+
fact :transaction,
|
|
669
|
+
order_id: 123,
|
|
670
|
+
customer_id: 456,
|
|
671
|
+
customer_name: "Acme",
|
|
672
|
+
payment_method: "credit",
|
|
673
|
+
# ... 30 more fields
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
## Further Reading
|
|
677
|
+
|
|
678
|
+
- **[Facts API Reference](api/facts.md)** - Complete method documentation
|
|
679
|
+
- **[Pattern Matching Guide](guides/pattern-matching.md)** - Detailed matching semantics
|
|
680
|
+
- **[Variable Binding Guide](guides/variable-binding.md)** - Join tests and captures
|
|
681
|
+
- **[Knowledge Base](what-is-a-knowledge-base.md)** - How facts fit into knowledge bases
|
|
682
|
+
- **[RETE Algorithm](architecture/rete-algorithm.md)** - How facts are matched efficiently
|
|
683
|
+
|
|
684
|
+
## Summary
|
|
685
|
+
|
|
686
|
+
A **fact** is:
|
|
687
|
+
|
|
688
|
+
- The fundamental unit of knowledge in KBS
|
|
689
|
+
- A **typed record** with attributes (`:type` + `{key: value}`)
|
|
690
|
+
- **Pattern-matchable** using literals, predicates, and variables
|
|
691
|
+
- Available in both **transient** (fast, volatile) and **persistent** (durable, auditable) forms
|
|
692
|
+
- The "data" that rules reason about
|
|
693
|
+
|
|
694
|
+
Think of facts as **statements of truth** that the knowledge base can automatically reason about and act upon.
|