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,558 @@
|
|
|
1
|
+
# Blackboard Memory
|
|
2
|
+
|
|
3
|
+
The blackboard pattern enables multiple agents to collaborate through a shared persistent workspace. This guide covers using KBS's blackboard memory for multi-agent systems, audit trails, and persistent knowledge bases.
|
|
4
|
+
|
|
5
|
+
## What is Blackboard Memory?
|
|
6
|
+
|
|
7
|
+
The blackboard architecture consists of three components:
|
|
8
|
+
|
|
9
|
+
1. **Blackboard** (`KBS::Blackboard::Memory`) - Central shared workspace
|
|
10
|
+
2. **Knowledge Sources** (Agents) - Independent specialists that read/write facts
|
|
11
|
+
3. **Control** (Rules + Priority) - Determines which agent acts when
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
*KBS uses the blackboard pattern for persistent, multi-agent reasoning with complete audit trails.*
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
### Creating a Blackboard Engine
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'kbs'
|
|
23
|
+
|
|
24
|
+
# With SQLite (default)
|
|
25
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
26
|
+
|
|
27
|
+
# Facts persist across restarts
|
|
28
|
+
engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
29
|
+
engine.close
|
|
30
|
+
|
|
31
|
+
# Next run
|
|
32
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
33
|
+
puts engine.facts.size # => 1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Blackboard vs Regular Engine
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Regular engine (transient)
|
|
40
|
+
regular = KBS::Engine.new
|
|
41
|
+
regular.add_fact(:foo, { bar: 1 })
|
|
42
|
+
# Lost on exit
|
|
43
|
+
|
|
44
|
+
# Blackboard engine (persistent)
|
|
45
|
+
blackboard = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
46
|
+
blackboard.add_fact(:foo, { bar: 1 })
|
|
47
|
+
blackboard.close
|
|
48
|
+
# Persisted to database
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Persistent Facts
|
|
52
|
+
|
|
53
|
+
### Fact Lifecycle
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# Create fact
|
|
57
|
+
fact = engine.add_fact(:sensor, { id: "bedroom", temp: 28 })
|
|
58
|
+
|
|
59
|
+
# Fact has UUID
|
|
60
|
+
puts fact.id # => "550e8400-e29b-41d4-a716-446655440000"
|
|
61
|
+
|
|
62
|
+
# Update fact
|
|
63
|
+
engine.update_fact(fact.id, { temp: 30 })
|
|
64
|
+
|
|
65
|
+
# Query fact history
|
|
66
|
+
history = engine.fact_history(fact.id)
|
|
67
|
+
history.each do |entry|
|
|
68
|
+
puts "#{entry[:timestamp]}: #{entry[:operation]} - #{entry[:attributes]}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Delete fact
|
|
72
|
+
engine.delete_fact(fact.id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fact Attributes
|
|
76
|
+
|
|
77
|
+
Blackboard facts support the same interface as regular facts:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
fact = engine.add_fact(:stock, { symbol: "AAPL", price: 150 })
|
|
81
|
+
|
|
82
|
+
# Access
|
|
83
|
+
fact.type # => :stock
|
|
84
|
+
fact[:symbol] # => "AAPL"
|
|
85
|
+
fact.attributes # => { symbol: "AAPL", price: 150 }
|
|
86
|
+
fact.id # => UUID string
|
|
87
|
+
|
|
88
|
+
# Metadata
|
|
89
|
+
fact.created_at # => Time object
|
|
90
|
+
fact.updated_at # => Time object
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Message Queue
|
|
94
|
+
|
|
95
|
+
The blackboard includes a priority-based message queue for agent communication:
|
|
96
|
+
|
|
97
|
+
### Sending Messages
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Add message to queue
|
|
101
|
+
engine.send_message(:alerts, "High temperature detected", priority: 10)
|
|
102
|
+
engine.send_message(:alerts, "Critical failure", priority: 100) # Higher priority
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Receiving Messages
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Pop highest priority message
|
|
109
|
+
msg = engine.pop_message(:alerts)
|
|
110
|
+
puts msg[:content] # => "Critical failure"
|
|
111
|
+
puts msg[:priority] # => 100
|
|
112
|
+
|
|
113
|
+
# Process all messages
|
|
114
|
+
while (msg = engine.pop_message(:alerts))
|
|
115
|
+
process_alert(msg[:content])
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Message Topics
|
|
120
|
+
|
|
121
|
+
Organize messages by topic:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# Different topics for different concerns
|
|
125
|
+
engine.send_message(:sensor_alerts, "Temp spike", priority: 50)
|
|
126
|
+
engine.send_message(:system_events, "Startup complete", priority: 10)
|
|
127
|
+
engine.send_message(:user_notifications, "Welcome!", priority: 1)
|
|
128
|
+
|
|
129
|
+
# Agents process their own topics
|
|
130
|
+
sensor_agent_msg = engine.pop_message(:sensor_alerts)
|
|
131
|
+
system_msg = engine.pop_message(:system_events)
|
|
132
|
+
user_msg = engine.pop_message(:user_notifications)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Audit Trail
|
|
136
|
+
|
|
137
|
+
Blackboard automatically logs all changes:
|
|
138
|
+
|
|
139
|
+
### Fact Audit Log
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Add fact
|
|
143
|
+
fact = engine.add_fact(:order, { id: 1, status: "pending" })
|
|
144
|
+
|
|
145
|
+
# Update fact
|
|
146
|
+
engine.update_fact(fact.id, { status: "processing" })
|
|
147
|
+
|
|
148
|
+
# Delete fact
|
|
149
|
+
engine.delete_fact(fact.id)
|
|
150
|
+
|
|
151
|
+
# Query audit trail
|
|
152
|
+
history = engine.fact_history(fact.id)
|
|
153
|
+
history.each do |entry|
|
|
154
|
+
puts "#{entry[:timestamp]}: #{entry[:operation]}"
|
|
155
|
+
puts " Attributes: #{entry[:attributes]}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Output:
|
|
159
|
+
# 2025-01-15 10:00:00: add
|
|
160
|
+
# Attributes: {id: 1, status: "pending"}
|
|
161
|
+
# 2025-01-15 10:01:00: update
|
|
162
|
+
# Attributes: {id: 1, status: "processing"}
|
|
163
|
+
# 2025-01-15 10:02:00: delete
|
|
164
|
+
# Attributes: {id: 1, status: "processing"}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Rule Firing Log
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Enable rule firing audit
|
|
171
|
+
engine = KBS::Blackboard::Engine.new(
|
|
172
|
+
db_path: 'kb.db',
|
|
173
|
+
audit_rules: true
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Add and run rules
|
|
177
|
+
engine.add_rule(my_rule)
|
|
178
|
+
engine.run
|
|
179
|
+
|
|
180
|
+
# Query rule firings
|
|
181
|
+
firings = engine.rule_firings(rule_name: "my_rule")
|
|
182
|
+
firings.each do |firing|
|
|
183
|
+
puts "Rule '#{firing[:rule_name]}' fired at #{firing[:timestamp]}"
|
|
184
|
+
puts " Facts: #{firing[:fact_ids]}"
|
|
185
|
+
puts " Bindings: #{firing[:bindings]}"
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Multi-Agent Systems
|
|
190
|
+
|
|
191
|
+
### Agent Pattern
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
class Agent
|
|
195
|
+
def initialize(name, engine)
|
|
196
|
+
@name = name
|
|
197
|
+
@engine = engine
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def observe
|
|
201
|
+
# Read facts from blackboard
|
|
202
|
+
@engine.facts.select { |f| relevant?(f) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def decide
|
|
206
|
+
# Apply agent's expertise
|
|
207
|
+
# Return action or nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def act
|
|
211
|
+
# Write facts to blackboard
|
|
212
|
+
# Send messages to other agents
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def run
|
|
216
|
+
observations = observe
|
|
217
|
+
action = decide(observations)
|
|
218
|
+
act(action) if action
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Example: Trading System
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
class MarketDataAgent < Agent
|
|
227
|
+
def run
|
|
228
|
+
# Fetch market data
|
|
229
|
+
data = fetch_market_data()
|
|
230
|
+
|
|
231
|
+
# Post to blackboard
|
|
232
|
+
@engine.add_fact(:market_data, {
|
|
233
|
+
symbol: data[:symbol],
|
|
234
|
+
price: data[:price],
|
|
235
|
+
volume: data[:volume],
|
|
236
|
+
timestamp: Time.now
|
|
237
|
+
})
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class TradingAgent < Agent
|
|
242
|
+
def run
|
|
243
|
+
# Observe market data
|
|
244
|
+
market_facts = @engine.facts.select { |f| f.type == :market_data }
|
|
245
|
+
|
|
246
|
+
market_facts.each do |fact|
|
|
247
|
+
# Apply trading strategy
|
|
248
|
+
if buy_signal?(fact)
|
|
249
|
+
@engine.add_fact(:order, {
|
|
250
|
+
symbol: fact[:symbol],
|
|
251
|
+
type: "buy",
|
|
252
|
+
quantity: calculate_quantity(fact),
|
|
253
|
+
price: fact[:price]
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
@engine.send_message(:execution, "New buy order", priority: 50)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def buy_signal?(fact)
|
|
262
|
+
# Agent's expertise
|
|
263
|
+
fact[:price] < moving_average(fact[:symbol]) * 0.95
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class ExecutionAgent < Agent
|
|
268
|
+
def run
|
|
269
|
+
# Check for execution messages
|
|
270
|
+
while (msg = @engine.pop_message(:execution))
|
|
271
|
+
# Find pending orders
|
|
272
|
+
orders = @engine.facts.select { |f|
|
|
273
|
+
f.type == :order && !f[:executed]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
orders.each do |order|
|
|
277
|
+
execute_order(order)
|
|
278
|
+
|
|
279
|
+
# Update fact
|
|
280
|
+
@engine.update_fact(order.id, { executed: true })
|
|
281
|
+
|
|
282
|
+
# Notify
|
|
283
|
+
@engine.send_message(:notifications, "Order executed", priority: 10)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Run agents in loop
|
|
290
|
+
engine = KBS::Blackboard::Engine.new(db_path: 'trading.db')
|
|
291
|
+
|
|
292
|
+
market_agent = MarketDataAgent.new("Market", engine)
|
|
293
|
+
trading_agent = TradingAgent.new("Trading", engine)
|
|
294
|
+
execution_agent = ExecutionAgent.new("Execution", engine)
|
|
295
|
+
|
|
296
|
+
loop do
|
|
297
|
+
market_agent.run # Fetch data → blackboard
|
|
298
|
+
trading_agent.run # Analyze → create orders
|
|
299
|
+
execution_agent.run # Execute orders
|
|
300
|
+
sleep 1
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Transactions
|
|
305
|
+
|
|
306
|
+
Blackboard supports ACID transactions (SQLite backend):
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Transaction succeeds
|
|
310
|
+
engine.transaction do
|
|
311
|
+
engine.add_fact(:account, { id: 1, balance: 1000 })
|
|
312
|
+
engine.add_fact(:account, { id: 2, balance: 500 })
|
|
313
|
+
end
|
|
314
|
+
# Both facts committed
|
|
315
|
+
|
|
316
|
+
# Transaction fails
|
|
317
|
+
begin
|
|
318
|
+
engine.transaction do
|
|
319
|
+
engine.add_fact(:account, { id: 3, balance: 100 })
|
|
320
|
+
raise "Error!"
|
|
321
|
+
engine.add_fact(:account, { id: 4, balance: 200 }) # Never reached
|
|
322
|
+
end
|
|
323
|
+
rescue => e
|
|
324
|
+
puts "Transaction rolled back"
|
|
325
|
+
end
|
|
326
|
+
# No facts committed
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Observers
|
|
330
|
+
|
|
331
|
+
Monitor blackboard changes in real-time:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
class FactObserver
|
|
335
|
+
def update(operation, fact)
|
|
336
|
+
case operation
|
|
337
|
+
when :add
|
|
338
|
+
puts "Fact added: #{fact.type} - #{fact.attributes}"
|
|
339
|
+
when :remove
|
|
340
|
+
puts "Fact removed: #{fact.type} - #{fact.attributes}"
|
|
341
|
+
when :update
|
|
342
|
+
puts "Fact updated: #{fact.type} - #{fact.attributes}"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
observer = FactObserver.new
|
|
348
|
+
engine.memory.add_observer(observer)
|
|
349
|
+
|
|
350
|
+
engine.add_fact(:sensor, { temp: 28 })
|
|
351
|
+
# Output: Fact added: sensor - {:temp=>28}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Performance Tuning
|
|
355
|
+
|
|
356
|
+
### SQLite Optimization
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
engine = KBS::Blackboard::Engine.new(
|
|
360
|
+
db_path: 'kb.db',
|
|
361
|
+
journal_mode: 'WAL', # Write-Ahead Logging
|
|
362
|
+
synchronous: 'NORMAL', # Balance durability/speed
|
|
363
|
+
cache_size: -64000, # 64MB cache
|
|
364
|
+
busy_timeout: 5000 # Wait 5s for locks
|
|
365
|
+
)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Redis for Speed
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
372
|
+
|
|
373
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
374
|
+
url: 'redis://localhost:6379/0'
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
378
|
+
|
|
379
|
+
# 10-100x faster than SQLite
|
|
380
|
+
# Perfect for high-frequency updates
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Hybrid for Production
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
require 'kbs/blackboard/persistence/hybrid_store'
|
|
387
|
+
|
|
388
|
+
store = KBS::Blackboard::Persistence::HybridStore.new(
|
|
389
|
+
redis_url: 'redis://localhost:6379/0',
|
|
390
|
+
db_path: 'audit.db'
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
394
|
+
|
|
395
|
+
# Fast access (Redis) + durable audit (SQLite)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Best Practices
|
|
399
|
+
|
|
400
|
+
### 1. Use UUIDs for Fact References
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# Good: Store fact UUID
|
|
404
|
+
order_id = engine.add_fact(:order, { ... }).id
|
|
405
|
+
engine.add_fact(:payment, { order_id: order_id })
|
|
406
|
+
|
|
407
|
+
# Bad: Use attribute as reference
|
|
408
|
+
engine.add_fact(:order, { id: 1 })
|
|
409
|
+
engine.add_fact(:payment, { order_id: 1 }) # Fragile
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 2. Namespace Facts by Agent
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
# Good: Clear ownership
|
|
416
|
+
engine.add_fact(:market_agent_data, { ... })
|
|
417
|
+
engine.add_fact(:trading_agent_signal, { ... })
|
|
418
|
+
|
|
419
|
+
# Bad: Generic names
|
|
420
|
+
engine.add_fact(:data, { ... })
|
|
421
|
+
engine.add_fact(:signal, { ... })
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 3. Use Messages for Coordination
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
# Good: Explicit coordination
|
|
428
|
+
engine.send_message(:execution_queue, "Process order #123", priority: 50)
|
|
429
|
+
|
|
430
|
+
# Bad: Polling facts
|
|
431
|
+
loop do
|
|
432
|
+
orders = engine.facts.select { |f| f.type == :pending_order }
|
|
433
|
+
# Inefficient
|
|
434
|
+
end
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### 4. Clean Up Old Facts
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
# Remove stale data
|
|
441
|
+
KBS::Rule.new("cleanup_old_facts", priority: 1) do |r|
|
|
442
|
+
r.conditions = [
|
|
443
|
+
KBS::Condition.new(:market_data, {
|
|
444
|
+
timestamp: :time?
|
|
445
|
+
}, predicate: lambda { |f|
|
|
446
|
+
(Time.now - f[:timestamp]) > 3600 # 1 hour old
|
|
447
|
+
})
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
r.action = lambda do |facts, bindings|
|
|
451
|
+
engine.remove_fact(facts[0])
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### 5. Use Transactions for Multi-Fact Updates
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
# Good: Atomic updates
|
|
460
|
+
engine.transaction do
|
|
461
|
+
engine.update_fact(account1_id, { balance: new_balance1 })
|
|
462
|
+
engine.update_fact(account2_id, { balance: new_balance2 })
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Bad: Separate updates (not atomic)
|
|
466
|
+
engine.update_fact(account1_id, { balance: new_balance1 })
|
|
467
|
+
engine.update_fact(account2_id, { balance: new_balance2 })
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Common Patterns
|
|
471
|
+
|
|
472
|
+
### Leader Election
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
# Agent attempts to become leader
|
|
476
|
+
KBS::Rule.new("become_leader") do |r|
|
|
477
|
+
r.conditions = [
|
|
478
|
+
KBS::Condition.new(:agent, { name: :name? }),
|
|
479
|
+
KBS::Condition.new(:leader, {}, negated: true)
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
r.action = lambda do |facts, bindings|
|
|
483
|
+
engine.add_fact(:leader, { name: bindings[:name?] })
|
|
484
|
+
puts "#{bindings[:name?]} is now leader"
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Distributed Locking
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
# Acquire lock
|
|
493
|
+
def acquire_lock(resource_id)
|
|
494
|
+
engine.transaction do
|
|
495
|
+
lock = engine.facts.find { |f|
|
|
496
|
+
f.type == :lock && f[:resource_id] == resource_id
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if lock.nil?
|
|
500
|
+
engine.add_fact(:lock, {
|
|
501
|
+
resource_id: resource_id,
|
|
502
|
+
owner: @agent_id,
|
|
503
|
+
acquired_at: Time.now
|
|
504
|
+
})
|
|
505
|
+
true
|
|
506
|
+
else
|
|
507
|
+
false
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Release lock
|
|
513
|
+
def release_lock(resource_id)
|
|
514
|
+
lock = engine.facts.find { |f|
|
|
515
|
+
f.type == :lock &&
|
|
516
|
+
f[:resource_id] == resource_id &&
|
|
517
|
+
f[:owner] == @agent_id
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
engine.remove_fact(lock) if lock
|
|
521
|
+
end
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Event Sourcing
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
# Store events as facts
|
|
528
|
+
engine.add_fact(:event, {
|
|
529
|
+
type: "order_created",
|
|
530
|
+
aggregate_id: "order-123",
|
|
531
|
+
data: { item: "Widget", quantity: 5 },
|
|
532
|
+
timestamp: Time.now
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
# Reconstruct state from events
|
|
536
|
+
def rebuild_order(order_id)
|
|
537
|
+
events = engine.facts
|
|
538
|
+
.select { |f| f.type == :event && f[:aggregate_id] == order_id }
|
|
539
|
+
.sort_by { |f| f[:timestamp] }
|
|
540
|
+
|
|
541
|
+
state = {}
|
|
542
|
+
events.each do |event|
|
|
543
|
+
apply_event(state, event)
|
|
544
|
+
end
|
|
545
|
+
state
|
|
546
|
+
end
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Next Steps
|
|
550
|
+
|
|
551
|
+
- **[Persistence](persistence.md)** - Storage backend options
|
|
552
|
+
- **[Architecture](../architecture/blackboard.md)** - Blackboard implementation details
|
|
553
|
+
- **[Multi-Agent Example](../examples/multi-agent.md)** - Complete multi-agent system
|
|
554
|
+
- **[API Reference](../api/blackboard.md)** - Complete blackboard API
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
*The blackboard pattern enables emergent intelligence through agent collaboration. Each agent contributes expertise to solve problems no single agent could solve alone.*
|