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