kbs 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
data/docs/api/engine.md
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
# Engine API Reference
|
|
2
|
+
|
|
3
|
+
Complete API reference for KBS engine classes.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [KBS::Engine](#kbsengine) - Core RETE engine
|
|
8
|
+
- [KBS::Blackboard::Engine](#kbsblackboardengine) - Persistent RETE engine with blackboard
|
|
9
|
+
- [Engine Lifecycle](#engine-lifecycle)
|
|
10
|
+
- [Advanced Topics](#advanced-topics)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## KBS::Engine
|
|
15
|
+
|
|
16
|
+
The core RETE II algorithm engine for in-memory fact processing.
|
|
17
|
+
|
|
18
|
+
### Constructor
|
|
19
|
+
|
|
20
|
+
#### `initialize()`
|
|
21
|
+
|
|
22
|
+
Creates a new in-memory RETE engine.
|
|
23
|
+
|
|
24
|
+
**Parameters**: None
|
|
25
|
+
|
|
26
|
+
**Returns**: `KBS::Engine` instance
|
|
27
|
+
|
|
28
|
+
**Example**:
|
|
29
|
+
```ruby
|
|
30
|
+
require 'kbs'
|
|
31
|
+
|
|
32
|
+
engine = KBS::Engine.new
|
|
33
|
+
# Engine ready with empty working memory
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Internal State Initialized**:
|
|
37
|
+
- `@working_memory` - WorkingMemory instance
|
|
38
|
+
- `@rules` - Array of registered rules
|
|
39
|
+
- `@alpha_memories` - Hash of pattern → AlphaMemory
|
|
40
|
+
- `@production_nodes` - Hash of rule name → ProductionNode
|
|
41
|
+
- `@root_beta_memory` - Root BetaMemory with dummy token
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### Public Methods
|
|
46
|
+
|
|
47
|
+
#### `add_rule(rule)`
|
|
48
|
+
|
|
49
|
+
Registers a rule and compiles it into the RETE network.
|
|
50
|
+
|
|
51
|
+
**Parameters**:
|
|
52
|
+
- `rule` (Rule) - Rule object with conditions and action
|
|
53
|
+
|
|
54
|
+
**Returns**: `nil`
|
|
55
|
+
|
|
56
|
+
**Side Effects**:
|
|
57
|
+
- Builds alpha memories for each condition pattern
|
|
58
|
+
- Creates join nodes or negation nodes
|
|
59
|
+
- Creates beta memories for partial matches
|
|
60
|
+
- Creates production node for rule
|
|
61
|
+
- Activates existing facts through new network paths
|
|
62
|
+
|
|
63
|
+
**Example**:
|
|
64
|
+
```ruby
|
|
65
|
+
rule = KBS::Rule.new(
|
|
66
|
+
name: "high_temperature",
|
|
67
|
+
priority: 10,
|
|
68
|
+
conditions: [
|
|
69
|
+
KBS::Condition.new(:temperature, { location: "server_room" })
|
|
70
|
+
],
|
|
71
|
+
action: ->(bindings) { puts "Alert: High temperature!" }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
engine.add_rule(rule)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Using DSL**:
|
|
78
|
+
```ruby
|
|
79
|
+
kb = KBS.knowledge_base do
|
|
80
|
+
rule "high_temperature", priority: 10 do
|
|
81
|
+
on :temperature, location: "server_room", value: greater_than(80)
|
|
82
|
+
perform do |bindings|
|
|
83
|
+
puts "Alert: #{bindings[:location?]} is #{bindings[:value?]}°F"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
kb.rules.each { |rule| engine.add_rule(rule) }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Performance Notes**:
|
|
92
|
+
- First rule with a pattern creates alpha memory
|
|
93
|
+
- Subsequent rules sharing patterns reuse alpha memory (network sharing)
|
|
94
|
+
- Cost is O(C) where C is number of conditions in rule
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
#### `add_fact(type, attributes = {})`
|
|
99
|
+
|
|
100
|
+
Adds a fact to working memory and activates matching alpha memories.
|
|
101
|
+
|
|
102
|
+
**Parameters**:
|
|
103
|
+
- `type` (Symbol) - Fact type (e.g., `:temperature`, `:order`)
|
|
104
|
+
- `attributes` (Hash) - Fact attributes (default: `{}`)
|
|
105
|
+
|
|
106
|
+
**Returns**: `KBS::Fact` - The created fact
|
|
107
|
+
|
|
108
|
+
**Side Effects**:
|
|
109
|
+
- Creates Fact object
|
|
110
|
+
- Adds to working memory
|
|
111
|
+
- Activates all matching alpha memories
|
|
112
|
+
- Propagates through join nodes
|
|
113
|
+
- May create new tokens in beta memories
|
|
114
|
+
|
|
115
|
+
**Example**:
|
|
116
|
+
```ruby
|
|
117
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
118
|
+
# => #<KBS::Fact:0x00... @type=:temperature @attributes={...}>
|
|
119
|
+
|
|
120
|
+
# Facts without attributes
|
|
121
|
+
marker = engine.add_fact(:system_ready)
|
|
122
|
+
# => #<KBS::Fact:0x00... @type=:system_ready @attributes={}>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Thread Safety**: Not thread-safe. Wrap in mutex if adding facts from multiple threads.
|
|
126
|
+
|
|
127
|
+
**Performance**: O(A × P) where A is number of alpha memories, P is pattern matching cost
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
#### `remove_fact(fact)`
|
|
132
|
+
|
|
133
|
+
Removes a fact from working memory and deactivates it in alpha memories.
|
|
134
|
+
|
|
135
|
+
**Parameters**:
|
|
136
|
+
- `fact` (KBS::Fact) - Fact object to remove (must be exact object reference)
|
|
137
|
+
|
|
138
|
+
**Returns**: `nil`
|
|
139
|
+
|
|
140
|
+
**Side Effects**:
|
|
141
|
+
- Removes from working memory
|
|
142
|
+
- Deactivates fact in all alpha memories
|
|
143
|
+
- Removes tokens containing this fact
|
|
144
|
+
- May cause negation nodes to re-evaluate
|
|
145
|
+
|
|
146
|
+
**Example**:
|
|
147
|
+
```ruby
|
|
148
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
149
|
+
engine.remove_fact(fact)
|
|
150
|
+
|
|
151
|
+
# Common pattern: Store fact reference for later removal
|
|
152
|
+
@current_alert = engine.add_fact(:alert, level: "critical")
|
|
153
|
+
# Later...
|
|
154
|
+
engine.remove_fact(@current_alert) if @current_alert
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Important**: You must keep a reference to the fact object to remove it. Finding facts requires inspecting `engine.working_memory.facts`.
|
|
158
|
+
|
|
159
|
+
**Example - Finding and Removing**:
|
|
160
|
+
```ruby
|
|
161
|
+
# Find all temperature facts
|
|
162
|
+
temp_facts = engine.working_memory.facts.select { |f| f.type == :temperature }
|
|
163
|
+
|
|
164
|
+
# Remove specific fact
|
|
165
|
+
old_fact = temp_facts.find { |f| f[:timestamp] < Time.now - 3600 }
|
|
166
|
+
engine.remove_fact(old_fact) if old_fact
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
#### `run()`
|
|
172
|
+
|
|
173
|
+
Executes all activated rules by firing production nodes.
|
|
174
|
+
|
|
175
|
+
**Parameters**: None
|
|
176
|
+
|
|
177
|
+
**Returns**: `nil`
|
|
178
|
+
|
|
179
|
+
**Side Effects**:
|
|
180
|
+
- Fires actions for all tokens in production nodes
|
|
181
|
+
- Rule actions may add/remove facts
|
|
182
|
+
- Rule actions may modify external state
|
|
183
|
+
|
|
184
|
+
**Example**:
|
|
185
|
+
```ruby
|
|
186
|
+
engine.add_fact(:temperature, value: 85)
|
|
187
|
+
engine.add_fact(:sensor, status: "active")
|
|
188
|
+
|
|
189
|
+
# Facts are in working memory but rules haven't fired
|
|
190
|
+
engine.run # Execute all matching rules
|
|
191
|
+
|
|
192
|
+
# Rules fire based on priority (highest first within each production)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Execution Order**:
|
|
196
|
+
- Production nodes fire in arbitrary order (dictionary order by rule name)
|
|
197
|
+
- Within a production node, tokens fire in insertion order
|
|
198
|
+
- For priority-based execution, use `KBS::Blackboard::Engine`
|
|
199
|
+
|
|
200
|
+
**Example - Multiple Rule Firings**:
|
|
201
|
+
```ruby
|
|
202
|
+
fired_rules = []
|
|
203
|
+
|
|
204
|
+
kb = KBS.knowledge_base do
|
|
205
|
+
rule "rule_a", priority: 10 do
|
|
206
|
+
on :temperature, value: greater_than(80)
|
|
207
|
+
perform { fired_rules << "rule_a" }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
rule "rule_b", priority: 20 do
|
|
211
|
+
on :temperature, value: greater_than(80)
|
|
212
|
+
perform { fired_rules << "rule_b" }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
kb.rules.each { |r| engine.add_rule(r) }
|
|
217
|
+
engine.add_fact(:temperature, value: 85)
|
|
218
|
+
engine.run
|
|
219
|
+
|
|
220
|
+
# Both rules fire (priority doesn't affect KBS::Engine execution order)
|
|
221
|
+
puts fired_rules # => ["rule_a", "rule_b"] or ["rule_b", "rule_a"]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Best Practice**: Call `run` after batch adding facts:
|
|
225
|
+
```ruby
|
|
226
|
+
# Good - batch facts then run once
|
|
227
|
+
engine.add_fact(:temperature, value: 85)
|
|
228
|
+
engine.add_fact(:humidity, value: 60)
|
|
229
|
+
engine.add_fact(:pressure, value: 1013)
|
|
230
|
+
engine.run
|
|
231
|
+
|
|
232
|
+
# Avoid - running after each fact (may fire rules prematurely)
|
|
233
|
+
engine.add_fact(:temperature, value: 85)
|
|
234
|
+
engine.run # Rule may fire with incomplete data
|
|
235
|
+
engine.add_fact(:humidity, value: 60)
|
|
236
|
+
engine.run
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### Public Attributes
|
|
242
|
+
|
|
243
|
+
#### `working_memory`
|
|
244
|
+
|
|
245
|
+
**Type**: `KBS::WorkingMemory`
|
|
246
|
+
|
|
247
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
248
|
+
|
|
249
|
+
**Description**: The working memory storing all facts.
|
|
250
|
+
|
|
251
|
+
**Example**:
|
|
252
|
+
```ruby
|
|
253
|
+
engine.add_fact(:temperature, value: 85)
|
|
254
|
+
engine.add_fact(:humidity, value: 60)
|
|
255
|
+
|
|
256
|
+
# Inspect all facts
|
|
257
|
+
puts engine.working_memory.facts.size # => 2
|
|
258
|
+
|
|
259
|
+
# Find specific facts
|
|
260
|
+
temps = engine.working_memory.facts.select { |f| f.type == :temperature }
|
|
261
|
+
temps.each do |fact|
|
|
262
|
+
puts "Temperature: #{fact[:value]}"
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
#### `rules`
|
|
269
|
+
|
|
270
|
+
**Type**: `Array<KBS::Rule>`
|
|
271
|
+
|
|
272
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
273
|
+
|
|
274
|
+
**Description**: All registered rules.
|
|
275
|
+
|
|
276
|
+
**Example**:
|
|
277
|
+
```ruby
|
|
278
|
+
puts "Registered rules:"
|
|
279
|
+
engine.rules.each do |rule|
|
|
280
|
+
puts " - #{rule.name} (priority: #{rule.priority})"
|
|
281
|
+
puts " Conditions: #{rule.conditions.size}"
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
#### `alpha_memories`
|
|
288
|
+
|
|
289
|
+
**Type**: `Hash<Hash, KBS::AlphaMemory>`
|
|
290
|
+
|
|
291
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
292
|
+
|
|
293
|
+
**Description**: Pattern → AlphaMemory mapping.
|
|
294
|
+
|
|
295
|
+
**Example**:
|
|
296
|
+
```ruby
|
|
297
|
+
# Inspect alpha memories (useful for debugging)
|
|
298
|
+
engine.alpha_memories.each do |pattern, memory|
|
|
299
|
+
puts "Pattern: #{pattern}"
|
|
300
|
+
puts " Facts: #{memory.facts.size}"
|
|
301
|
+
puts " Successors: #{memory.successors.size}"
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
#### `production_nodes`
|
|
308
|
+
|
|
309
|
+
**Type**: `Hash<Symbol, KBS::ProductionNode>`
|
|
310
|
+
|
|
311
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
312
|
+
|
|
313
|
+
**Description**: Rule name → ProductionNode mapping.
|
|
314
|
+
|
|
315
|
+
**Example**:
|
|
316
|
+
```ruby
|
|
317
|
+
# Check if a rule is activated
|
|
318
|
+
prod_node = engine.production_nodes[:high_temperature]
|
|
319
|
+
if prod_node && prod_node.tokens.any?
|
|
320
|
+
puts "Rule 'high_temperature' has #{prod_node.tokens.size} activations"
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### Observer Pattern
|
|
327
|
+
|
|
328
|
+
The engine implements the observer pattern to watch fact changes.
|
|
329
|
+
|
|
330
|
+
#### `update(action, fact)` (Internal)
|
|
331
|
+
|
|
332
|
+
**Parameters**:
|
|
333
|
+
- `action` (Symbol) - `:add` or `:remove`
|
|
334
|
+
- `fact` (KBS::Fact) - The fact that changed
|
|
335
|
+
|
|
336
|
+
**Description**: Called automatically by WorkingMemory when facts change. Activates/deactivates alpha memories.
|
|
337
|
+
|
|
338
|
+
**Example - Custom Observer**:
|
|
339
|
+
```ruby
|
|
340
|
+
class FactLogger
|
|
341
|
+
def update(action, fact)
|
|
342
|
+
puts "[#{Time.now}] #{action.upcase}: #{fact.type} #{fact.attributes}"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
logger = FactLogger.new
|
|
347
|
+
engine.working_memory.add_observer(logger)
|
|
348
|
+
|
|
349
|
+
engine.add_fact(:temperature, value: 85)
|
|
350
|
+
# Output: [2025-01-15 10:30:00] ADD: temperature {:value=>85}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## KBS::Blackboard::Engine
|
|
356
|
+
|
|
357
|
+
Persistent RETE engine with blackboard memory, audit logging, and message queue.
|
|
358
|
+
|
|
359
|
+
**Inherits**: `KBS::Engine`
|
|
360
|
+
|
|
361
|
+
**Key Differences from KBS::Engine**:
|
|
362
|
+
- Persistent facts (SQLite, Redis, or Hybrid)
|
|
363
|
+
- Audit trail of all fact changes
|
|
364
|
+
- Message queue for inter-agent communication
|
|
365
|
+
- Transaction support
|
|
366
|
+
- Observer notifications
|
|
367
|
+
- Rule firing logged with bindings
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### Constructor
|
|
372
|
+
|
|
373
|
+
#### `initialize(db_path: ':memory:', store: nil)`
|
|
374
|
+
|
|
375
|
+
Creates a persistent RETE engine with blackboard memory.
|
|
376
|
+
|
|
377
|
+
**Parameters**:
|
|
378
|
+
- `db_path` (String, optional) - Path to SQLite database (default: `:memory:`)
|
|
379
|
+
- `store` (Store, optional) - Custom persistence store (default: `nil`, uses SQLiteStore)
|
|
380
|
+
|
|
381
|
+
**Returns**: `KBS::Blackboard::Engine` instance
|
|
382
|
+
|
|
383
|
+
**Example - In-Memory**:
|
|
384
|
+
```ruby
|
|
385
|
+
engine = KBS::Blackboard::Engine.new
|
|
386
|
+
# Blackboard in RAM (lost on exit)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Example - SQLite Persistence**:
|
|
390
|
+
```ruby
|
|
391
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
|
|
392
|
+
# Facts persisted to knowledge_base.db
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Example - Redis Persistence**:
|
|
396
|
+
```ruby
|
|
397
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
398
|
+
|
|
399
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(url: 'redis://localhost:6379/0')
|
|
400
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
401
|
+
# Fast, distributed persistence
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Example - Hybrid Persistence**:
|
|
405
|
+
```ruby
|
|
406
|
+
require 'kbs/blackboard/persistence/hybrid_store'
|
|
407
|
+
|
|
408
|
+
store = KBS::Blackboard::Persistence::HybridStore.new(
|
|
409
|
+
redis_url: 'redis://localhost:6379/0',
|
|
410
|
+
db_path: 'audit.db'
|
|
411
|
+
)
|
|
412
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
413
|
+
# Facts in Redis, audit trail in SQLite
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Public Methods
|
|
419
|
+
|
|
420
|
+
#### `add_fact(type, attributes = {})`
|
|
421
|
+
|
|
422
|
+
Adds a persistent fact to the blackboard.
|
|
423
|
+
|
|
424
|
+
**Parameters**:
|
|
425
|
+
- `type` (Symbol) - Fact type
|
|
426
|
+
- `attributes` (Hash) - Fact attributes
|
|
427
|
+
|
|
428
|
+
**Returns**: `KBS::Blackboard::Fact` - Persistent fact with UUID
|
|
429
|
+
|
|
430
|
+
**Side Effects**:
|
|
431
|
+
- Creates fact with UUID
|
|
432
|
+
- Saves to persistent store
|
|
433
|
+
- Logs to audit trail
|
|
434
|
+
- Activates alpha memories
|
|
435
|
+
- Notifies observers
|
|
436
|
+
|
|
437
|
+
**Example**:
|
|
438
|
+
```ruby
|
|
439
|
+
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
440
|
+
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
441
|
+
|
|
442
|
+
# Fact persists across restarts
|
|
443
|
+
engine2 = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
|
|
444
|
+
reloaded_facts = engine2.blackboard.get_facts_by_type(:temperature)
|
|
445
|
+
puts reloaded_facts.first[:value] # => 85
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Difference from KBS::Engine**: Returns `KBS::Blackboard::Fact` (has `.uuid`) instead of `KBS::Fact`.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
#### `remove_fact(fact)`
|
|
453
|
+
|
|
454
|
+
Removes a persistent fact from the blackboard.
|
|
455
|
+
|
|
456
|
+
**Parameters**:
|
|
457
|
+
- `fact` (KBS::Blackboard::Fact) - Fact to remove
|
|
458
|
+
|
|
459
|
+
**Returns**: `nil`
|
|
460
|
+
|
|
461
|
+
**Side Effects**:
|
|
462
|
+
- Marks fact as inactive in store
|
|
463
|
+
- Logs removal to audit trail
|
|
464
|
+
- Deactivates in alpha memories
|
|
465
|
+
- Notifies observers
|
|
466
|
+
|
|
467
|
+
**Example**:
|
|
468
|
+
```ruby
|
|
469
|
+
fact = engine.add_fact(:temperature, value: 85)
|
|
470
|
+
engine.remove_fact(fact)
|
|
471
|
+
|
|
472
|
+
# Fact marked inactive but remains in audit trail
|
|
473
|
+
audit = engine.blackboard.audit_log.get_fact_history(fact.uuid)
|
|
474
|
+
puts audit.last[:action] # => "retract"
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
#### `run()`
|
|
480
|
+
|
|
481
|
+
Executes activated rules with audit logging.
|
|
482
|
+
|
|
483
|
+
**Parameters**: None
|
|
484
|
+
|
|
485
|
+
**Returns**: `nil`
|
|
486
|
+
|
|
487
|
+
**Side Effects**:
|
|
488
|
+
- Fires rules in production nodes
|
|
489
|
+
- Logs each rule firing to audit trail
|
|
490
|
+
- Records fact UUIDs and variable bindings
|
|
491
|
+
- Marks tokens as fired (prevents duplicate firing)
|
|
492
|
+
|
|
493
|
+
**Example**:
|
|
494
|
+
```ruby
|
|
495
|
+
engine.add_rule(my_rule)
|
|
496
|
+
engine.add_fact(:temperature, value: 85)
|
|
497
|
+
engine.run
|
|
498
|
+
|
|
499
|
+
# Check audit log
|
|
500
|
+
engine.blackboard.audit_log.entries.each do |entry|
|
|
501
|
+
next unless entry[:event_type] == "rule_fired"
|
|
502
|
+
puts "Rule #{entry[:rule_name]} fired with bindings: #{entry[:bindings]}"
|
|
503
|
+
end
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Difference from KBS::Engine**:
|
|
507
|
+
- Logs every rule firing
|
|
508
|
+
- Prevents duplicate firing of same token
|
|
509
|
+
- Records variable bindings in audit
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
#### `post_message(sender, topic, content, priority: 0)`
|
|
514
|
+
|
|
515
|
+
Posts a message to the blackboard message queue.
|
|
516
|
+
|
|
517
|
+
**Parameters**:
|
|
518
|
+
- `sender` (String) - Sender identifier (e.g., agent name)
|
|
519
|
+
- `topic` (String) - Message topic (channel)
|
|
520
|
+
- `content` (Hash) - Message payload
|
|
521
|
+
- `priority` (Integer, optional) - Message priority (default: 0, higher = more urgent)
|
|
522
|
+
|
|
523
|
+
**Returns**: `nil`
|
|
524
|
+
|
|
525
|
+
**Side Effects**:
|
|
526
|
+
- Adds message to queue
|
|
527
|
+
- Persists to store
|
|
528
|
+
- Higher priority messages consumed first
|
|
529
|
+
|
|
530
|
+
**Example**:
|
|
531
|
+
```ruby
|
|
532
|
+
# Agent 1 posts message
|
|
533
|
+
engine.post_message(
|
|
534
|
+
"trading_agent",
|
|
535
|
+
"orders",
|
|
536
|
+
{ action: "buy", symbol: "AAPL", quantity: 100 },
|
|
537
|
+
priority: 10
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Agent 2 consumes message
|
|
541
|
+
msg = engine.consume_message("orders", "execution_agent")
|
|
542
|
+
puts msg[:content][:action] # => "buy"
|
|
543
|
+
puts msg[:sender] # => "trading_agent"
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Use Cases**:
|
|
547
|
+
- Inter-agent communication
|
|
548
|
+
- Command/event bus
|
|
549
|
+
- Task queues
|
|
550
|
+
- Priority-based scheduling
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
#### `consume_message(topic, consumer)`
|
|
555
|
+
|
|
556
|
+
Retrieves and removes the highest priority message from a topic.
|
|
557
|
+
|
|
558
|
+
**Parameters**:
|
|
559
|
+
- `topic` (String) - Topic to consume from
|
|
560
|
+
- `consumer` (String) - Consumer identifier (for audit trail)
|
|
561
|
+
|
|
562
|
+
**Returns**: `Hash` or `nil` - Message hash with `:id`, `:sender`, `:topic`, `:content`, `:priority`, `:timestamp`, or `nil` if queue empty
|
|
563
|
+
|
|
564
|
+
**Side Effects**:
|
|
565
|
+
- Removes message from queue
|
|
566
|
+
- Logs consumption to audit trail (if store supports it)
|
|
567
|
+
|
|
568
|
+
**Example**:
|
|
569
|
+
```ruby
|
|
570
|
+
# Consumer loop
|
|
571
|
+
loop do
|
|
572
|
+
msg = engine.consume_message("tasks", "worker_1")
|
|
573
|
+
break unless msg
|
|
574
|
+
|
|
575
|
+
puts "Processing: #{msg[:content][:task_name]} (priority #{msg[:priority]})"
|
|
576
|
+
# Process message...
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Thread Safety**: Atomic pop operation (PostgreSQL/Redis stores support concurrent consumers)
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
#### `stats()`
|
|
585
|
+
|
|
586
|
+
Returns blackboard statistics.
|
|
587
|
+
|
|
588
|
+
**Parameters**: None
|
|
589
|
+
|
|
590
|
+
**Returns**: `Hash` with keys:
|
|
591
|
+
- `:facts_count` (Integer) - Number of active facts
|
|
592
|
+
- `:messages_count` (Integer) - Number of queued messages (all topics)
|
|
593
|
+
- `:audit_entries_count` (Integer) - Total audit log entries
|
|
594
|
+
|
|
595
|
+
**Example**:
|
|
596
|
+
```ruby
|
|
597
|
+
stats = engine.stats
|
|
598
|
+
puts "Facts: #{stats[:facts_count]}"
|
|
599
|
+
puts "Messages: #{stats[:messages_count]}"
|
|
600
|
+
puts "Audit entries: #{stats[:audit_entries_count]}"
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Performance**: May be slow for large databases (counts all rows)
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
### Public Attributes
|
|
608
|
+
|
|
609
|
+
#### `blackboard`
|
|
610
|
+
|
|
611
|
+
**Type**: `KBS::Blackboard::Memory`
|
|
612
|
+
|
|
613
|
+
**Read-only**: Yes (via `attr_reader`)
|
|
614
|
+
|
|
615
|
+
**Description**: The blackboard memory (also accessible as `working_memory`).
|
|
616
|
+
|
|
617
|
+
**Example**:
|
|
618
|
+
```ruby
|
|
619
|
+
# Access blackboard components
|
|
620
|
+
engine.blackboard.message_queue.post("agent1", "alerts", { alert: "critical" })
|
|
621
|
+
engine.blackboard.audit_log.entries.last
|
|
622
|
+
engine.blackboard.transaction { engine.add_fact(:order, status: "pending") }
|
|
623
|
+
|
|
624
|
+
# Get facts by type
|
|
625
|
+
temps = engine.blackboard.get_facts_by_type(:temperature)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Engine Lifecycle
|
|
631
|
+
|
|
632
|
+
### Typical Flow
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
# 1. Create engine
|
|
636
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
637
|
+
|
|
638
|
+
# 2. Define and register rules
|
|
639
|
+
kb = KBS.knowledge_base do
|
|
640
|
+
rule "high_temp_alert", priority: 10 do
|
|
641
|
+
on :temperature, value: greater_than(80)
|
|
642
|
+
perform do |bindings|
|
|
643
|
+
puts "Alert! Temperature: #{bindings[:value?]}"
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
kb.rules.each { |r| engine.add_rule(r) }
|
|
648
|
+
|
|
649
|
+
# 3. Add initial facts
|
|
650
|
+
engine.add_fact(:sensor, id: 1, status: "active")
|
|
651
|
+
|
|
652
|
+
# 4. Main loop
|
|
653
|
+
loop do
|
|
654
|
+
# Collect new data
|
|
655
|
+
temp = read_temperature_sensor
|
|
656
|
+
engine.add_fact(:temperature, value: temp, timestamp: Time.now)
|
|
657
|
+
|
|
658
|
+
# Execute rules
|
|
659
|
+
engine.run
|
|
660
|
+
|
|
661
|
+
# Process messages
|
|
662
|
+
while msg = engine.consume_message("tasks", "main_loop")
|
|
663
|
+
handle_task(msg[:content])
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
sleep 5
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
### Restart and Recovery
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
# Session 1 - Add facts
|
|
676
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
677
|
+
engine.add_fact(:account, id: 1, balance: 1000)
|
|
678
|
+
# Exit
|
|
679
|
+
|
|
680
|
+
# Session 2 - Facts still present
|
|
681
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
682
|
+
accounts = engine.blackboard.get_facts_by_type(:account)
|
|
683
|
+
puts accounts.first[:balance] # => 1000
|
|
684
|
+
|
|
685
|
+
# BUT: Rules must be re-registered (not persisted)
|
|
686
|
+
kb = load_rules
|
|
687
|
+
kb.rules.each { |r| engine.add_rule(r) }
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Important**: Only facts persist. Rules, alpha memories, and RETE network must be rebuilt on restart.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
### Transaction Example
|
|
695
|
+
|
|
696
|
+
```ruby
|
|
697
|
+
engine.blackboard.transaction do
|
|
698
|
+
fact1 = engine.add_fact(:order, id: 1, status: "pending")
|
|
699
|
+
fact2 = engine.add_fact(:inventory, item: "ABC", quantity: 100)
|
|
700
|
+
|
|
701
|
+
# If error occurs here, both facts are rolled back
|
|
702
|
+
raise "Validation failed" if invalid_order?(fact1)
|
|
703
|
+
end
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
**Database Support**: SQLite and PostgreSQL support ACID transactions. Redis and MongoDB require custom transaction logic.
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
## Advanced Topics
|
|
711
|
+
|
|
712
|
+
### Network Sharing
|
|
713
|
+
|
|
714
|
+
Multiple rules sharing condition patterns reuse alpha memories:
|
|
715
|
+
|
|
716
|
+
```ruby
|
|
717
|
+
# Both rules share the :temperature alpha memory
|
|
718
|
+
rule "high_temp_alert" do
|
|
719
|
+
on :temperature, value: greater_than(80)
|
|
720
|
+
perform { puts "High temperature!" }
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
rule "critical_temp_alert" do
|
|
724
|
+
on :temperature, value: greater_than(100)
|
|
725
|
+
perform { puts "CRITICAL temperature!" }
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Only 1 alpha memory created for :temperature
|
|
729
|
+
# Pattern matching happens once per fact
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
### Inspecting the RETE Network
|
|
735
|
+
|
|
736
|
+
```ruby
|
|
737
|
+
# Dump alpha memories
|
|
738
|
+
engine.alpha_memories.each do |pattern, memory|
|
|
739
|
+
puts "Pattern: #{pattern.inspect}"
|
|
740
|
+
puts " Facts in alpha memory: #{memory.facts.size}"
|
|
741
|
+
puts " Successor nodes: #{memory.successors.size}"
|
|
742
|
+
memory.successors.each do |succ|
|
|
743
|
+
puts " #{succ.class.name}"
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Dump production nodes
|
|
748
|
+
engine.production_nodes.each do |name, node|
|
|
749
|
+
puts "Rule: #{name}"
|
|
750
|
+
puts " Tokens (activations): #{node.tokens.size}"
|
|
751
|
+
node.tokens.each do |token|
|
|
752
|
+
puts " Token with #{token.facts.size} facts"
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Use Case**: Debugging why a rule didn't fire
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
### Custom Working Memory Observer
|
|
762
|
+
|
|
763
|
+
```ruby
|
|
764
|
+
class MetricsCollector
|
|
765
|
+
def initialize
|
|
766
|
+
@fact_count = 0
|
|
767
|
+
@retract_count = 0
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def update(action, fact)
|
|
771
|
+
case action
|
|
772
|
+
when :add
|
|
773
|
+
@fact_count += 1
|
|
774
|
+
when :remove
|
|
775
|
+
@retract_count += 1
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def report
|
|
780
|
+
puts "Facts added: #{@fact_count}"
|
|
781
|
+
puts "Facts retracted: #{@retract_count}"
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
metrics = MetricsCollector.new
|
|
786
|
+
engine.working_memory.add_observer(metrics)
|
|
787
|
+
|
|
788
|
+
# Run engine...
|
|
789
|
+
engine.add_fact(:temperature, value: 85)
|
|
790
|
+
engine.remove_fact(fact)
|
|
791
|
+
|
|
792
|
+
metrics.report
|
|
793
|
+
# => Facts added: 1
|
|
794
|
+
# => Facts retracted: 1
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
### Programmatic Rule Creation
|
|
800
|
+
|
|
801
|
+
```ruby
|
|
802
|
+
# Without DSL - manual Rule object
|
|
803
|
+
condition = KBS::Condition.new(:temperature, { value: -> (v) { v > 80 } })
|
|
804
|
+
action = ->(bindings) { puts "High temperature detected" }
|
|
805
|
+
rule = KBS::Rule.new(name: "high_temp", priority: 10, conditions: [condition], action: action)
|
|
806
|
+
|
|
807
|
+
engine.add_rule(rule)
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**When to Use**: Dynamically generating rules at runtime based on configuration.
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
### Engine Composition
|
|
815
|
+
|
|
816
|
+
```ruby
|
|
817
|
+
# Multiple engines with different rule sets
|
|
818
|
+
class MonitoringSystem
|
|
819
|
+
def initialize
|
|
820
|
+
@temperature_engine = KBS::Blackboard::Engine.new(db_path: 'temp.db')
|
|
821
|
+
@security_engine = KBS::Blackboard::Engine.new(db_path: 'security.db')
|
|
822
|
+
|
|
823
|
+
setup_temperature_rules(@temperature_engine)
|
|
824
|
+
setup_security_rules(@security_engine)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def process_sensor_data(data)
|
|
828
|
+
if data[:type] == :temperature
|
|
829
|
+
@temperature_engine.add_fact(:temperature, data)
|
|
830
|
+
@temperature_engine.run
|
|
831
|
+
elsif data[:type] == :motion
|
|
832
|
+
@security_engine.add_fact(:motion, data)
|
|
833
|
+
@security_engine.run
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Use Case**: Separating concerns across multiple knowledge bases
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## Performance Considerations
|
|
844
|
+
|
|
845
|
+
### Rule Ordering
|
|
846
|
+
|
|
847
|
+
Rules are added to `@rules` array in registration order, but execution order depends on when tokens reach production nodes.
|
|
848
|
+
|
|
849
|
+
```ruby
|
|
850
|
+
# Both rules activated by same fact
|
|
851
|
+
engine.add_rule(rule_a) # Registered first
|
|
852
|
+
engine.add_rule(rule_b) # Registered second
|
|
853
|
+
|
|
854
|
+
engine.add_fact(:temperature, value: 85)
|
|
855
|
+
engine.run
|
|
856
|
+
# Both fire, but order is unpredictable in KBS::Engine
|
|
857
|
+
# Use KBS::Blackboard::Engine with priority for deterministic order
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
### Fact Batching
|
|
863
|
+
|
|
864
|
+
```ruby
|
|
865
|
+
# Efficient - batch facts then run once
|
|
866
|
+
facts_to_add.each do |data|
|
|
867
|
+
engine.add_fact(:sensor_reading, data)
|
|
868
|
+
end
|
|
869
|
+
engine.run # All rules see complete dataset
|
|
870
|
+
|
|
871
|
+
# Inefficient - run after each fact
|
|
872
|
+
facts_to_add.each do |data|
|
|
873
|
+
engine.add_fact(:sensor_reading, data)
|
|
874
|
+
engine.run # May fire rules prematurely
|
|
875
|
+
end
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
### Memory Growth
|
|
881
|
+
|
|
882
|
+
```ruby
|
|
883
|
+
# Clean up old facts to prevent memory growth
|
|
884
|
+
cutoff_time = Time.now - 3600 # 1 hour ago
|
|
885
|
+
old_facts = engine.working_memory.facts.select do |fact|
|
|
886
|
+
fact[:timestamp] && fact[:timestamp] < cutoff_time
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
old_facts.each { |f| engine.remove_fact(f) }
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Production Pattern**: Implement fact expiration in a cleanup rule:
|
|
893
|
+
|
|
894
|
+
```ruby
|
|
895
|
+
rule "expire_old_facts", priority: 0 do
|
|
896
|
+
on :temperature, timestamp: ->(ts) { Time.now - ts > 3600 }
|
|
897
|
+
perform do |bindings|
|
|
898
|
+
fact = bindings[:matched_fact?]
|
|
899
|
+
engine.remove_fact(fact)
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## Error Handling
|
|
907
|
+
|
|
908
|
+
### Rule Action Errors
|
|
909
|
+
|
|
910
|
+
```ruby
|
|
911
|
+
rule "risky_operation" do
|
|
912
|
+
on :task, status: "pending"
|
|
913
|
+
perform do |bindings|
|
|
914
|
+
begin
|
|
915
|
+
perform_risky_operation(bindings[:task_id?])
|
|
916
|
+
rescue => e
|
|
917
|
+
# Log error
|
|
918
|
+
puts "Error in rule: #{e.message}"
|
|
919
|
+
|
|
920
|
+
# Add error fact for other rules to handle
|
|
921
|
+
engine.add_fact(:error, rule: "risky_operation", message: e.message)
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
### Store Connection Errors
|
|
930
|
+
|
|
931
|
+
```ruby
|
|
932
|
+
begin
|
|
933
|
+
engine = KBS::Blackboard::Engine.new(db_path: '/invalid/path/kb.db')
|
|
934
|
+
rescue Errno::EACCES => e
|
|
935
|
+
puts "Cannot access database: #{e.message}"
|
|
936
|
+
# Fallback to in-memory
|
|
937
|
+
engine = KBS::Blackboard::Engine.new
|
|
938
|
+
end
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
---
|
|
942
|
+
|
|
943
|
+
## Thread Safety
|
|
944
|
+
|
|
945
|
+
**KBS::Engine and KBS::Blackboard::Engine are NOT thread-safe.**
|
|
946
|
+
|
|
947
|
+
For multi-threaded access:
|
|
948
|
+
|
|
949
|
+
```ruby
|
|
950
|
+
require 'thread'
|
|
951
|
+
|
|
952
|
+
class ThreadSafeEngine
|
|
953
|
+
def initialize(*args)
|
|
954
|
+
@engine = KBS::Blackboard::Engine.new(*args)
|
|
955
|
+
@mutex = Mutex.new
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def add_fact(*args)
|
|
959
|
+
@mutex.synchronize { @engine.add_fact(*args) }
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def run
|
|
963
|
+
@mutex.synchronize { @engine.run }
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**Better Approach**: Use one engine per thread or message passing between threads.
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## See Also
|
|
973
|
+
|
|
974
|
+
- [Facts API](facts.md) - Working with fact objects
|
|
975
|
+
- [Rules API](rules.md) - Rule and Condition objects
|
|
976
|
+
- [Blackboard API](blackboard.md) - Memory, MessageQueue, AuditLog
|
|
977
|
+
- [DSL Guide](../guides/dsl.md) - Rule definition syntax
|
|
978
|
+
- [Performance Guide](../advanced/performance.md) - Optimization strategies
|