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
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
# Working with Facts
|
|
2
|
+
|
|
3
|
+
Facts are the fundamental units of knowledge in KBS. This guide covers the complete lifecycle of facts: creating, querying, updating, and removing them.
|
|
4
|
+
|
|
5
|
+
## What is a Fact?
|
|
6
|
+
|
|
7
|
+
A fact represents an observation or piece of knowledge about your domain. Facts have:
|
|
8
|
+
|
|
9
|
+
- **Type** - A symbol categorizing the fact (e.g., `:stock`, `:sensor`, `:alert`)
|
|
10
|
+
- **Attributes** - Key-value pairs describing the fact (e.g., `{ symbol: "AAPL", price: 150 }`)
|
|
11
|
+
- **Identity** - Unique instance in working memory
|
|
12
|
+
|
|
13
|
+
**Example Facts:**
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# Sensor reading
|
|
17
|
+
type: :sensor
|
|
18
|
+
attributes: { id: "bedroom", temp: 28, humidity: 65 }
|
|
19
|
+
|
|
20
|
+
# Stock quote
|
|
21
|
+
type: :stock
|
|
22
|
+
attributes: { symbol: "AAPL", price: 150.50, volume: 1000000 }
|
|
23
|
+
|
|
24
|
+
# Alert
|
|
25
|
+
type: :alert
|
|
26
|
+
attributes: { sensor_id: "bedroom", message: "High temperature" }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Fact Types
|
|
30
|
+
|
|
31
|
+
KBS provides two fact implementations:
|
|
32
|
+
|
|
33
|
+
### 1. Transient Facts (`KBS::Fact`)
|
|
34
|
+
|
|
35
|
+
In-memory facts that disappear when your program exits.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
engine = KBS::Engine.new
|
|
39
|
+
|
|
40
|
+
# Add transient fact
|
|
41
|
+
fact = engine.add_fact(:stock, { symbol: "AAPL", price: 150 })
|
|
42
|
+
|
|
43
|
+
# Facts lost on restart
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Use for:**
|
|
47
|
+
- Short-lived applications
|
|
48
|
+
- Prototyping
|
|
49
|
+
- Testing
|
|
50
|
+
- Pure computation (no persistence needed)
|
|
51
|
+
|
|
52
|
+
### 2. Persistent Facts (`KBS::Blackboard::Fact`)
|
|
53
|
+
|
|
54
|
+
Database-backed facts with UUIDs that survive restarts.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
58
|
+
|
|
59
|
+
# Add persistent fact (saved to database)
|
|
60
|
+
fact = engine.add_fact(:stock, { symbol: "AAPL", price: 150 })
|
|
61
|
+
puts fact.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
62
|
+
|
|
63
|
+
# Facts reload on next run
|
|
64
|
+
engine.close
|
|
65
|
+
|
|
66
|
+
# Next run
|
|
67
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
68
|
+
puts engine.facts.size # => 1 (fact persisted)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Use for:**
|
|
72
|
+
- Long-running systems
|
|
73
|
+
- Systems requiring restart
|
|
74
|
+
- Audit trails
|
|
75
|
+
- Multi-agent collaboration
|
|
76
|
+
|
|
77
|
+
**Both types share the same interface**, so code works identically:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
fact.type # => :stock
|
|
81
|
+
fact[:symbol] # => "AAPL"
|
|
82
|
+
fact[:price] # => 150
|
|
83
|
+
fact.attributes # => { symbol: "AAPL", price: 150 }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Creating Facts
|
|
87
|
+
|
|
88
|
+
### Basic Creation
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Method 1: Via engine (recommended)
|
|
92
|
+
fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
93
|
+
|
|
94
|
+
# Method 2: Direct instantiation
|
|
95
|
+
fact = KBS::Fact.new(:sensor, { id: "bedroom", temp: 28 })
|
|
96
|
+
engine.add_fact(fact)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**`add_fact` automatically:**
|
|
100
|
+
- Stores fact in working memory
|
|
101
|
+
- Triggers pattern matching in RETE network
|
|
102
|
+
- Notifies observers
|
|
103
|
+
- Persists to database (if using Blackboard::Engine)
|
|
104
|
+
|
|
105
|
+
### With Type Conversion
|
|
106
|
+
|
|
107
|
+
Attributes are stored as-is:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
engine.add_fact(:reading, {
|
|
111
|
+
value: 42, # Integer
|
|
112
|
+
timestamp: Time.now, # Time object
|
|
113
|
+
active: true, # Boolean
|
|
114
|
+
metadata: { foo: 1 } # Hash
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Bulk Creation
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
facts = [
|
|
122
|
+
[:stock, { symbol: "AAPL", price: 150 }],
|
|
123
|
+
[:stock, { symbol: "GOOGL", price: 2800 }],
|
|
124
|
+
[:stock, { symbol: "MSFT", price: 300 }]
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
facts.each do |type, attrs|
|
|
128
|
+
engine.add_fact(type, attrs)
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### From External Data
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require 'json'
|
|
136
|
+
|
|
137
|
+
# Load from JSON
|
|
138
|
+
json_data = File.read('sensors.json')
|
|
139
|
+
sensor_data = JSON.parse(json_data, symbolize_names: true)
|
|
140
|
+
|
|
141
|
+
sensor_data.each do |reading|
|
|
142
|
+
engine.add_fact(:sensor, {
|
|
143
|
+
id: reading[:sensor_id],
|
|
144
|
+
temp: reading[:temperature],
|
|
145
|
+
humidity: reading[:humidity]
|
|
146
|
+
})
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
require 'csv'
|
|
152
|
+
|
|
153
|
+
# Load from CSV
|
|
154
|
+
CSV.foreach('stocks.csv', headers: true) do |row|
|
|
155
|
+
engine.add_fact(:stock, {
|
|
156
|
+
symbol: row['symbol'],
|
|
157
|
+
price: row['price'].to_f,
|
|
158
|
+
volume: row['volume'].to_i
|
|
159
|
+
})
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Accessing Fact Attributes
|
|
164
|
+
|
|
165
|
+
### Array-Style Access
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
169
|
+
|
|
170
|
+
# Read attributes
|
|
171
|
+
fact[:id] # => "bedroom"
|
|
172
|
+
fact[:temp] # => 28
|
|
173
|
+
fact[:missing] # => nil
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Attributes Hash
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
fact.attributes
|
|
180
|
+
# => { id: "bedroom", temp: 28 }
|
|
181
|
+
|
|
182
|
+
# Iterate attributes
|
|
183
|
+
fact.attributes.each do |key, value|
|
|
184
|
+
puts "#{key}: #{value}"
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Type Access
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
fact.type # => :sensor
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Identity (Persistent Facts Only)
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Blackboard facts have UUIDs
|
|
198
|
+
fact.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
199
|
+
|
|
200
|
+
# Transient facts use object_id
|
|
201
|
+
fact.object_id # => 70123456789000
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Querying Facts
|
|
205
|
+
|
|
206
|
+
### Get All Facts
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
all_facts = engine.facts
|
|
210
|
+
# => [#<Fact type=:sensor>, #<Fact type=:stock>, ...]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Filter by Type
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Get all sensor facts
|
|
217
|
+
sensors = engine.facts.select { |f| f.type == :sensor }
|
|
218
|
+
|
|
219
|
+
# Get all stock facts
|
|
220
|
+
stocks = engine.facts.select { |f| f.type == :stock }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Filter by Attribute
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# Find facts with specific attribute value
|
|
227
|
+
high_temps = engine.facts.select { |f|
|
|
228
|
+
f.type == :sensor && f[:temp] && f[:temp] > 30
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Find by multiple criteria
|
|
232
|
+
aapl_stocks = engine.facts.select { |f|
|
|
233
|
+
f.type == :stock && f[:symbol] == "AAPL"
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Find Single Fact
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# Find first matching fact
|
|
241
|
+
fact = engine.facts.find { |f|
|
|
242
|
+
f.type == :sensor && f[:id] == "bedroom"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Or return nil if not found
|
|
246
|
+
fact = engine.facts.find { |f|
|
|
247
|
+
f.type == :alert && f[:severity] == "critical"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Complex Queries
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Count facts
|
|
255
|
+
sensor_count = engine.facts.count { |f| f.type == :sensor }
|
|
256
|
+
|
|
257
|
+
# Group by type
|
|
258
|
+
facts_by_type = engine.facts.group_by(&:type)
|
|
259
|
+
# => { sensor: [...], stock: [...], alert: [...] }
|
|
260
|
+
|
|
261
|
+
# Map attributes
|
|
262
|
+
symbols = engine.facts
|
|
263
|
+
.select { |f| f.type == :stock }
|
|
264
|
+
.map { |f| f[:symbol] }
|
|
265
|
+
.uniq
|
|
266
|
+
# => ["AAPL", "GOOGL", "MSFT"]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Query Helper Method
|
|
270
|
+
|
|
271
|
+
Create reusable query methods:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class QueryHelper
|
|
275
|
+
def initialize(engine)
|
|
276
|
+
@engine = engine
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def facts_of_type(type)
|
|
280
|
+
@engine.facts.select { |f| f.type == type }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def facts_where(type, &block)
|
|
284
|
+
facts_of_type(type).select(&block)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def fact_where(type, &block)
|
|
288
|
+
facts_of_type(type).find(&block)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Usage
|
|
293
|
+
helper = QueryHelper.new(engine)
|
|
294
|
+
|
|
295
|
+
# Get all high-temp sensors
|
|
296
|
+
high_temps = helper.facts_where(:sensor) { |f| f[:temp] > 30 }
|
|
297
|
+
|
|
298
|
+
# Get specific sensor
|
|
299
|
+
bedroom = helper.fact_where(:sensor) { |f| f[:id] == "bedroom" }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Updating Facts
|
|
303
|
+
|
|
304
|
+
Facts are immutable in KBS. To "update" a fact, remove the old one and add a new one.
|
|
305
|
+
|
|
306
|
+
### Update Pattern
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Find existing fact
|
|
310
|
+
old_fact = engine.facts.find { |f|
|
|
311
|
+
f.type == :sensor && f[:id] == "bedroom"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if old_fact
|
|
315
|
+
# Remove old fact
|
|
316
|
+
engine.remove_fact(old_fact)
|
|
317
|
+
|
|
318
|
+
# Add updated fact
|
|
319
|
+
engine.add_fact(:sensor, {
|
|
320
|
+
id: "bedroom",
|
|
321
|
+
temp: 30, # Updated temperature
|
|
322
|
+
humidity: 65
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
# Re-run matching
|
|
326
|
+
engine.run
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Update Helper
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
def update_fact(engine, type, matcher, new_attrs)
|
|
334
|
+
old_fact = engine.facts.find { |f|
|
|
335
|
+
f.type == type && matcher.call(f)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if old_fact
|
|
339
|
+
engine.remove_fact(old_fact)
|
|
340
|
+
engine.add_fact(type, new_attrs)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Usage
|
|
345
|
+
update_fact(engine, :sensor, ->(f) { f[:id] == "bedroom" },
|
|
346
|
+
{ id: "bedroom", temp: 30, humidity: 65 }
|
|
347
|
+
)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Blackboard Update (Persistent Facts)
|
|
351
|
+
|
|
352
|
+
Blackboard facts support in-place updates:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
356
|
+
|
|
357
|
+
fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
358
|
+
|
|
359
|
+
# Update attributes (saves to database)
|
|
360
|
+
fact.update({ temp: 30 })
|
|
361
|
+
|
|
362
|
+
# Or update via engine
|
|
363
|
+
engine.update_fact(fact.id, { temp: 32 })
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Removing Facts
|
|
367
|
+
|
|
368
|
+
### Remove Single Fact
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
# Find and remove
|
|
372
|
+
fact = engine.facts.find { |f| f.type == :alert }
|
|
373
|
+
engine.remove_fact(fact) if fact
|
|
374
|
+
|
|
375
|
+
# Re-run to propagate changes
|
|
376
|
+
engine.run
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Remove Multiple Facts
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# Remove all alerts
|
|
383
|
+
alerts = engine.facts.select { |f| f.type == :alert }
|
|
384
|
+
alerts.each { |fact| engine.remove_fact(fact) }
|
|
385
|
+
engine.run
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Remove by Criteria
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# Remove all stale sensor readings (older than 5 minutes)
|
|
392
|
+
stale = engine.facts.select { |f|
|
|
393
|
+
f.type == :sensor &&
|
|
394
|
+
f[:timestamp] &&
|
|
395
|
+
(Time.now - f[:timestamp]) > 300
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
stale.each { |fact| engine.remove_fact(fact) }
|
|
399
|
+
engine.run
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Clear All Facts
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# Clear working memory
|
|
406
|
+
engine.facts.dup.each { |f| engine.remove_fact(f) }
|
|
407
|
+
engine.run
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Note:** Use `.dup` to avoid modifying array while iterating.
|
|
411
|
+
|
|
412
|
+
## Fact Lifecycle
|
|
413
|
+
|
|
414
|
+
### Lifecycle Stages
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
1. Creation
|
|
418
|
+
├─> engine.add_fact(:type, { ... })
|
|
419
|
+
└─> Fact instantiated
|
|
420
|
+
|
|
421
|
+
2. Storage
|
|
422
|
+
├─> Added to WorkingMemory
|
|
423
|
+
└─> Persisted (if Blackboard::Engine)
|
|
424
|
+
|
|
425
|
+
3. Matching
|
|
426
|
+
├─> Alpha network activation
|
|
427
|
+
├─> Join network propagation
|
|
428
|
+
└─> Production node tokens created
|
|
429
|
+
|
|
430
|
+
4. Rule Firing
|
|
431
|
+
├─> engine.run()
|
|
432
|
+
└─> Actions execute with fact
|
|
433
|
+
|
|
434
|
+
5. Update (Optional)
|
|
435
|
+
├─> engine.remove_fact(old_fact)
|
|
436
|
+
├─> engine.add_fact(:type, new_attrs)
|
|
437
|
+
└─> Matching re-triggered
|
|
438
|
+
|
|
439
|
+
6. Removal
|
|
440
|
+
├─> engine.remove_fact(fact)
|
|
441
|
+
├─> Removed from WorkingMemory
|
|
442
|
+
├─> Deleted from database (if persistent)
|
|
443
|
+
└─> Tokens invalidated
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Observing Fact Changes
|
|
447
|
+
|
|
448
|
+
Working memory uses the Observer pattern:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
class FactObserver
|
|
452
|
+
def update(operation, fact)
|
|
453
|
+
case operation
|
|
454
|
+
when :add
|
|
455
|
+
puts "Added: #{fact.type} - #{fact.attributes}"
|
|
456
|
+
when :remove
|
|
457
|
+
puts "Removed: #{fact.type} - #{fact.attributes}"
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
observer = FactObserver.new
|
|
463
|
+
engine.working_memory.add_observer(observer)
|
|
464
|
+
|
|
465
|
+
engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
466
|
+
# Output: Added: sensor - {:id=>"bedroom", :temp=>28}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Best Practices
|
|
470
|
+
|
|
471
|
+
### 1. Use Consistent Fact Types
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
# Good: Consistent naming
|
|
475
|
+
:sensor_reading
|
|
476
|
+
:stock_quote
|
|
477
|
+
:user_alert
|
|
478
|
+
|
|
479
|
+
# Bad: Inconsistent
|
|
480
|
+
:sensor
|
|
481
|
+
:Stock
|
|
482
|
+
:UserAlert
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### 2. Keep Attributes Flat
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
# Good: Flat structure
|
|
489
|
+
engine.add_fact(:sensor, {
|
|
490
|
+
sensor_id: "bedroom",
|
|
491
|
+
temp: 28,
|
|
492
|
+
humidity: 65
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
# Bad: Nested (harder to match)
|
|
496
|
+
engine.add_fact(:sensor, {
|
|
497
|
+
id: "bedroom",
|
|
498
|
+
readings: { temp: 28, humidity: 65 }
|
|
499
|
+
})
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 3. Include Timestamps
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
# Good: Temporal reasoning enabled
|
|
506
|
+
engine.add_fact(:reading, {
|
|
507
|
+
sensor_id: "bedroom",
|
|
508
|
+
value: 28,
|
|
509
|
+
timestamp: Time.now
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### 4. Validate Before Adding
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
def add_sensor_reading(engine, id, temp)
|
|
517
|
+
# Validate
|
|
518
|
+
raise ArgumentError, "Invalid temp" unless temp.is_a?(Numeric)
|
|
519
|
+
raise ArgumentError, "Temp out of range" unless temp.between?(-50, 100)
|
|
520
|
+
|
|
521
|
+
# Add fact
|
|
522
|
+
engine.add_fact(:sensor, {
|
|
523
|
+
id: id,
|
|
524
|
+
temp: temp,
|
|
525
|
+
timestamp: Time.now
|
|
526
|
+
})
|
|
527
|
+
end
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 5. Use Symbols for Type
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
# Good
|
|
534
|
+
engine.add_fact(:sensor, { ... })
|
|
535
|
+
|
|
536
|
+
# Bad
|
|
537
|
+
engine.add_fact("sensor", { ... }) # Strings not idiomatic
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 6. Namespace Fact Types
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
# Good: Clear namespacing for large systems
|
|
544
|
+
:trading_order
|
|
545
|
+
:trading_execution
|
|
546
|
+
:trading_alert
|
|
547
|
+
|
|
548
|
+
:sensor_temp
|
|
549
|
+
:sensor_humidity
|
|
550
|
+
:sensor_pressure
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Common Patterns
|
|
554
|
+
|
|
555
|
+
### Fact Factory
|
|
556
|
+
|
|
557
|
+
```ruby
|
|
558
|
+
class SensorFactFactory
|
|
559
|
+
def self.create_reading(id, temp, humidity)
|
|
560
|
+
{
|
|
561
|
+
type: :sensor,
|
|
562
|
+
attributes: {
|
|
563
|
+
id: id,
|
|
564
|
+
temp: temp,
|
|
565
|
+
humidity: humidity,
|
|
566
|
+
timestamp: Time.now
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Usage
|
|
573
|
+
reading = SensorFactFactory.create_reading("bedroom", 28, 65)
|
|
574
|
+
engine.add_fact(reading[:type], reading[:attributes])
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Fact Builder
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
class FactBuilder
|
|
581
|
+
def initialize(type)
|
|
582
|
+
@type = type
|
|
583
|
+
@attributes = {}
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def with(key, value)
|
|
587
|
+
@attributes[key] = value
|
|
588
|
+
self
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def build
|
|
592
|
+
[@type, @attributes]
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Usage
|
|
597
|
+
type, attrs = FactBuilder.new(:stock)
|
|
598
|
+
.with(:symbol, "AAPL")
|
|
599
|
+
.with(:price, 150)
|
|
600
|
+
.with(:volume, 1000000)
|
|
601
|
+
.build
|
|
602
|
+
|
|
603
|
+
engine.add_fact(type, attrs)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Fact Repository
|
|
607
|
+
|
|
608
|
+
```ruby
|
|
609
|
+
class FactRepository
|
|
610
|
+
def initialize(engine)
|
|
611
|
+
@engine = engine
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def add(type, attributes)
|
|
615
|
+
@engine.add_fact(type, attributes.merge(created_at: Time.now))
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def find_by_id(type, id)
|
|
619
|
+
@engine.facts.find { |f| f.type == type && f[:id] == id }
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def where(type, &block)
|
|
623
|
+
@engine.facts.select { |f| f.type == type && block.call(f) }
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def remove_where(type, &block)
|
|
627
|
+
facts = where(type, &block)
|
|
628
|
+
facts.each { |f| @engine.remove_fact(f) }
|
|
629
|
+
@engine.run
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Usage
|
|
634
|
+
repo = FactRepository.new(engine)
|
|
635
|
+
repo.add(:sensor, { id: "bedroom", temp: 28 })
|
|
636
|
+
|
|
637
|
+
bedroom = repo.find_by_id(:sensor, "bedroom")
|
|
638
|
+
high_temps = repo.where(:sensor) { |f| f[:temp] > 30 }
|
|
639
|
+
repo.remove_where(:alert) { |f| f[:stale] }
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Next Steps
|
|
643
|
+
|
|
644
|
+
- **[Pattern Matching](pattern-matching.md)** - How facts match conditions
|
|
645
|
+
- **[Writing Rules](writing-rules.md)** - Using facts in rule conditions
|
|
646
|
+
- **[Blackboard Memory](blackboard-memory.md)** - Persistent fact storage
|
|
647
|
+
- **[Persistence Guide](persistence.md)** - SQLite, Redis, and hybrid storage
|
|
648
|
+
- **[API Reference](../api/facts.md)** - Complete Fact API documentation
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
*Facts are immutable knowledge. When facts change, replace them to trigger re-evaluation.*
|