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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. data/examples/stock_trading_system.rb.bak +0 -563
@@ -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