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,737 @@
|
|
|
1
|
+
# The RETE Algorithm in KBS
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The RETE algorithm is a pattern matching algorithm for implementing production rule systems. Developed by Charles Forgy in 1979, RETE (Latin for "network") creates a discrimination network that efficiently matches rules against a working memory of facts. KBS implements the RETE algorithm with the critical **unlinking optimization** for improved performance.
|
|
6
|
+
|
|
7
|
+
## Why RETE?
|
|
8
|
+
|
|
9
|
+
Traditional rule engines evaluate all rules against all facts on every cycle, resulting in O(R × F) complexity where R is the number of rules and F is the number of facts. RETE achieves near-constant time per working memory change by:
|
|
10
|
+
|
|
11
|
+
1. **Sharing common patterns** across rules in a compiled network
|
|
12
|
+
2. **Maintaining state** between cycles (incremental matching)
|
|
13
|
+
3. **Processing only changes** rather than re-evaluating everything
|
|
14
|
+
4. **Unlinking empty nodes** to skip unnecessary computation (RETE optimization)
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
### Facts
|
|
19
|
+
|
|
20
|
+
Facts are the fundamental units of knowledge in the system. Each fact has:
|
|
21
|
+
|
|
22
|
+
- **Type**: A symbol identifying the kind of fact (e.g., `:stock`, `:alert`, `:order`)
|
|
23
|
+
- **Attributes**: Key-value pairs containing the fact's data
|
|
24
|
+
- **ID**: A unique identifier (object_id for transient facts, UUID for persisted facts)
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Creating a fact
|
|
28
|
+
fact = engine.add_fact(:stock, symbol: "AAPL", price: 150.0, volume: 1000000)
|
|
29
|
+
|
|
30
|
+
# Fact structure
|
|
31
|
+
# => stock(symbol: AAPL, price: 150.0, volume: 1000000)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Implementation**: `lib/kbs/fact.rb:4`
|
|
35
|
+
|
|
36
|
+
### Working Memory
|
|
37
|
+
|
|
38
|
+
Working memory is the collection of all facts currently known to the system. It implements the **Observer pattern** to notify the RETE network when facts are added or removed.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
class WorkingMemory
|
|
42
|
+
def add_fact(fact)
|
|
43
|
+
@facts << fact
|
|
44
|
+
notify_observers(:add, fact) # Triggers RETE propagation
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def remove_fact(fact)
|
|
48
|
+
@facts.delete(fact)
|
|
49
|
+
notify_observers(:remove, fact) # Triggers retraction
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Implementation**: `lib/kbs/working_memory.rb:4`
|
|
55
|
+
|
|
56
|
+
### Conditions and Patterns
|
|
57
|
+
|
|
58
|
+
A condition specifies a pattern that facts must match. Patterns can include:
|
|
59
|
+
|
|
60
|
+
- **Type matching**: `{ type: :stock }`
|
|
61
|
+
- **Literal values**: `{ symbol: "AAPL" }`
|
|
62
|
+
- **Variable bindings**: `{ price: :price? }` (variables start with `?`)
|
|
63
|
+
- **Predicates**: `{ price: ->(p) { p > 100 } }`
|
|
64
|
+
- **Negation**: `negated: true` (match when pattern is absent)
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Match any stock with symbol "AAPL"
|
|
68
|
+
Condition.new(:stock, { symbol: "AAPL" })
|
|
69
|
+
|
|
70
|
+
# Match stock and bind price to ?price variable
|
|
71
|
+
Condition.new(:stock, { symbol: "AAPL", price: :price? })
|
|
72
|
+
|
|
73
|
+
# Match when there is NO alert for "AAPL"
|
|
74
|
+
Condition.new(:alert, { symbol: "AAPL" }, negated: true)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Implementation**: `lib/kbs/condition.rb:4`
|
|
78
|
+
|
|
79
|
+
### Rules
|
|
80
|
+
|
|
81
|
+
Rules are production rules consisting of:
|
|
82
|
+
|
|
83
|
+
- **Conditions** (IF part): Patterns to match in working memory
|
|
84
|
+
- **Action** (THEN part): Code to execute when all conditions match
|
|
85
|
+
- **Priority**: Optional integer for conflict resolution (higher fires first)
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
rule = Rule.new("high_price_alert") do |r|
|
|
89
|
+
r.conditions = [
|
|
90
|
+
Condition.new(:stock, { symbol: :symbol?, price: :price? }),
|
|
91
|
+
Condition.new(:threshold, { symbol: :symbol?, max: :max? })
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
r.action = lambda do |facts, bindings|
|
|
95
|
+
if bindings[:price?] > bindings[:max?]
|
|
96
|
+
puts "Alert: #{bindings[:symbol?]} at #{bindings[:price?]}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Implementation**: `lib/kbs/rule.rb:4`
|
|
103
|
+
|
|
104
|
+
### Tokens
|
|
105
|
+
|
|
106
|
+
Tokens represent **partial matches** as they flow through the RETE network. A token is a linked list of facts that have matched conditions so far.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Token
|
|
110
|
+
attr_accessor :parent, :fact, :node, :children
|
|
111
|
+
|
|
112
|
+
# Reconstruct the full chain of matched facts
|
|
113
|
+
def facts
|
|
114
|
+
facts = []
|
|
115
|
+
token = self
|
|
116
|
+
while token
|
|
117
|
+
facts.unshift(token.fact) if token.fact
|
|
118
|
+
token = token.parent
|
|
119
|
+
end
|
|
120
|
+
facts
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Key insights**:
|
|
126
|
+
- The **root token** has `parent = nil`, `fact = nil` and represents "no conditions matched yet"
|
|
127
|
+
- Each join creates a **new token** linking to its parent token plus a new fact
|
|
128
|
+
- Tokens form a **tree structure** via the `children` array, enabling efficient retraction
|
|
129
|
+
|
|
130
|
+
**Implementation**: `lib/kbs/token.rb:4`
|
|
131
|
+
|
|
132
|
+
## Network Architecture
|
|
133
|
+
|
|
134
|
+
The RETE network is a **directed acyclic graph (DAG)** consisting of three layers:
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
*The three-layer RETE network architecture showing alpha memories (pattern matching), beta network (join processing), and production nodes (rule firing).*
|
|
139
|
+
|
|
140
|
+
### Layer 1: Alpha Network
|
|
141
|
+
|
|
142
|
+
The **alpha network** performs **intra-condition** tests - matching individual facts against patterns. Each `AlphaMemory` node:
|
|
143
|
+
|
|
144
|
+
- Stores facts matching a specific pattern
|
|
145
|
+
- Is shared across all rules using the same pattern (network sharing)
|
|
146
|
+
- Propagates matches to successor join nodes
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class AlphaMemory
|
|
150
|
+
attr_accessor :items, :successors, :pattern
|
|
151
|
+
|
|
152
|
+
def activate(fact)
|
|
153
|
+
return unless @linked
|
|
154
|
+
@items << fact
|
|
155
|
+
@successors.each { |s| s.right_activate(fact) }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Example**: If three rules all match `stock(symbol: "AAPL")`, they share one `AlphaMemory` node for that pattern.
|
|
161
|
+
|
|
162
|
+
**Implementation**: `lib/kbs/alpha_memory.rb:4`
|
|
163
|
+
|
|
164
|
+
### Layer 2: Beta Network
|
|
165
|
+
|
|
166
|
+
The **beta network** performs **inter-condition** tests - joining facts from different conditions. It consists of:
|
|
167
|
+
|
|
168
|
+
#### Join Nodes
|
|
169
|
+
|
|
170
|
+
`JoinNode` combines tokens from **beta memory** (left input) with facts from **alpha memory** (right input):
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
class JoinNode
|
|
174
|
+
def left_activate(token)
|
|
175
|
+
return unless @left_linked && @right_linked
|
|
176
|
+
|
|
177
|
+
@alpha_memory.items.each do |fact|
|
|
178
|
+
if perform_join_tests(token, fact)
|
|
179
|
+
new_token = Token.new(token, fact, self)
|
|
180
|
+
@successors.each { |s| s.activate(new_token) }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def right_activate(fact)
|
|
186
|
+
return unless @left_linked && @right_linked
|
|
187
|
+
|
|
188
|
+
@beta_memory.tokens.each do |token|
|
|
189
|
+
if perform_join_tests(token, fact)
|
|
190
|
+
new_token = Token.new(token, fact, self)
|
|
191
|
+
@successors.each { |s| s.activate(new_token) }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Join tests** verify:
|
|
199
|
+
- Variable consistency (e.g., both conditions match same `:symbol?`)
|
|
200
|
+
- Cross-condition predicates (e.g., price1 > price2)
|
|
201
|
+
|
|
202
|
+
**Implementation**: `lib/kbs/join_node.rb:4`
|
|
203
|
+
|
|
204
|
+
#### Beta Memory
|
|
205
|
+
|
|
206
|
+
`BetaMemory` stores tokens (partial matches) and implements the **unlinking optimization**:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
class BetaMemory
|
|
210
|
+
def add_token(token)
|
|
211
|
+
@tokens << token
|
|
212
|
+
unlink! if @tokens.empty? # Unlink when empty
|
|
213
|
+
relink! if @tokens.size == 1 # Relink when first token arrives
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def remove_token(token)
|
|
217
|
+
@tokens.delete(token)
|
|
218
|
+
unlink! if @tokens.empty? # Unlink when last token removed
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Implementation**: `lib/kbs/beta_memory.rb:4`
|
|
224
|
+
|
|
225
|
+
#### Negation Nodes
|
|
226
|
+
|
|
227
|
+
`NegationNode` implements **negated conditions** (e.g., "when there is NO matching fact"):
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class NegationNode
|
|
231
|
+
def left_activate(token)
|
|
232
|
+
matches = @alpha_memory.items.select { |fact| perform_join_tests(token, fact) }
|
|
233
|
+
|
|
234
|
+
if matches.empty?
|
|
235
|
+
# No inhibiting facts found - propagate the token
|
|
236
|
+
new_token = Token.new(token, nil, self)
|
|
237
|
+
@successors.each { |s| s.activate(new_token) }
|
|
238
|
+
else
|
|
239
|
+
# Found inhibiting facts - block propagation
|
|
240
|
+
@tokens_with_matches[token] = matches
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def right_deactivate(fact)
|
|
245
|
+
# When an inhibiting fact is removed, check if we can now propagate
|
|
246
|
+
@beta_memory.tokens.each do |token|
|
|
247
|
+
if @tokens_with_matches[token].include?(fact)
|
|
248
|
+
@tokens_with_matches[token].delete(fact)
|
|
249
|
+
|
|
250
|
+
if @tokens_with_matches[token].empty?
|
|
251
|
+
new_token = Token.new(token, nil, self)
|
|
252
|
+
@successors.each { |s| s.activate(new_token) }
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Key insight**: Negation nodes propagate tokens with `fact = nil` since there's no actual fact to include.
|
|
261
|
+
|
|
262
|
+
**Implementation**: `lib/kbs/negation_node.rb:4`
|
|
263
|
+
|
|
264
|
+
### Layer 3: Production Nodes
|
|
265
|
+
|
|
266
|
+
`ProductionNode` is the terminal node for each rule. When a token reaches a production node, all rule conditions have been satisfied:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class ProductionNode
|
|
270
|
+
def activate(token)
|
|
271
|
+
@tokens << token
|
|
272
|
+
# Don't fire immediately - wait for engine.run()
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def fire_rule(token)
|
|
276
|
+
return if token.fired?
|
|
277
|
+
@rule.fire(token.facts)
|
|
278
|
+
token.mark_fired!
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Why delay firing?** Negation nodes may need to **deactivate** tokens after they're created but before they fire. The two-phase approach (collect tokens, then fire) ensures correctness.
|
|
284
|
+
|
|
285
|
+
**Implementation**: `lib/kbs/production_node.rb:4`
|
|
286
|
+
|
|
287
|
+
## The RETE Cycle
|
|
288
|
+
|
|
289
|
+
### 1. Network Construction
|
|
290
|
+
|
|
291
|
+
When a rule is added via `engine.add_rule(rule)`, the network is built:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
def build_network_for_rule(rule)
|
|
295
|
+
current_beta = @root_beta_memory
|
|
296
|
+
|
|
297
|
+
rule.conditions.each_with_index do |condition, index|
|
|
298
|
+
# Create or reuse alpha memory
|
|
299
|
+
pattern = condition.pattern.merge(type: condition.type)
|
|
300
|
+
alpha_memory = get_or_create_alpha_memory(pattern)
|
|
301
|
+
|
|
302
|
+
# Build join tests for variable consistency
|
|
303
|
+
tests = build_join_tests(condition, index)
|
|
304
|
+
|
|
305
|
+
# Create join or negation node
|
|
306
|
+
if condition.negated
|
|
307
|
+
negation_node = NegationNode.new(alpha_memory, current_beta, tests)
|
|
308
|
+
new_beta = BetaMemory.new
|
|
309
|
+
negation_node.successors << new_beta
|
|
310
|
+
current_beta = new_beta
|
|
311
|
+
else
|
|
312
|
+
join_node = JoinNode.new(alpha_memory, current_beta, tests)
|
|
313
|
+
new_beta = BetaMemory.new
|
|
314
|
+
join_node.successors << new_beta
|
|
315
|
+
current_beta = new_beta
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Terminal production node
|
|
320
|
+
production_node = ProductionNode.new(rule)
|
|
321
|
+
current_beta.successors << production_node
|
|
322
|
+
@production_nodes[rule.name] = production_node
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Implementation**: `lib/kbs/rete_engine.rb:58`
|
|
327
|
+
|
|
328
|
+
### 2. Fact Assertion
|
|
329
|
+
|
|
330
|
+
When `engine.add_fact(:stock, symbol: "AAPL", price: 150)` is called:
|
|
331
|
+
|
|
332
|
+

|
|
333
|
+
|
|
334
|
+
*Step-by-step flow showing how a fact propagates through the RETE network from working memory to production nodes.*
|
|
335
|
+
|
|
336
|
+
### 3. Pattern Matching Flow
|
|
337
|
+
|
|
338
|
+
Let's trace a fact through the network for this rule:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Rule: Alert when AAPL stock exists but no alert exists
|
|
342
|
+
rule = Rule.new("no_alert") do |r|
|
|
343
|
+
r.conditions = [
|
|
344
|
+
Condition.new(:stock, { symbol: "AAPL" }),
|
|
345
|
+
Condition.new(:alert, { symbol: "AAPL" }, negated: true)
|
|
346
|
+
]
|
|
347
|
+
r.action = ->(facts) { puts "No alert for AAPL!" }
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+

|
|
352
|
+
|
|
353
|
+
*Complete trace showing how negation works: adding a stock fact fires the rule, adding an alert inhibits it, and removing the alert reactivates the rule.*
|
|
354
|
+
|
|
355
|
+
### 4. Rule Execution
|
|
356
|
+
|
|
357
|
+
The final phase is `engine.run()`:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
def run
|
|
361
|
+
@production_nodes.values.each do |node|
|
|
362
|
+
node.tokens.each do |token|
|
|
363
|
+
node.fire_rule(token)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Each production node fires its accumulated tokens. The `fired?` flag prevents duplicate firing.
|
|
370
|
+
|
|
371
|
+
**Implementation**: `lib/kbs/rete_engine.rb:48`
|
|
372
|
+
|
|
373
|
+
## RETE Optimization: Unlinking
|
|
374
|
+
|
|
375
|
+
### The Problem
|
|
376
|
+
|
|
377
|
+
In basic RETE, join nodes always process activations even when one input is empty:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
BetaMemory (0 tokens) ──┐
|
|
381
|
+
├──→ JoinNode ──→ (does useless work!)
|
|
382
|
+
AlphaMemory (100 facts) ┘
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
If beta memory is empty, the join will produce zero results, wasting CPU cycles.
|
|
386
|
+
|
|
387
|
+
### The Solution
|
|
388
|
+
|
|
389
|
+
RETE introduces **dynamic unlinking**: nodes automatically disconnect from the network when empty and reconnect when non-empty.
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
class BetaMemory
|
|
393
|
+
def add_token(token)
|
|
394
|
+
@tokens << token
|
|
395
|
+
relink! if @tokens.size == 1 # Reconnect when first token arrives
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def remove_token(token)
|
|
399
|
+
@tokens.delete(token)
|
|
400
|
+
unlink! if @tokens.empty? # Disconnect when empty
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def relink!
|
|
404
|
+
@linked = true
|
|
405
|
+
@successors.each { |s| s.left_relink! }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def unlink!
|
|
409
|
+
@linked = false
|
|
410
|
+
@successors.each { |s| s.left_unlink! }
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Join node respects linking state**:
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
class JoinNode
|
|
419
|
+
def left_activate(token)
|
|
420
|
+
return unless @left_linked && @right_linked # Skip if unlinked!
|
|
421
|
+
# ... perform join ...
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def right_activate(fact)
|
|
425
|
+
return unless @left_linked && @right_linked # Skip if unlinked!
|
|
426
|
+
# ... perform join ...
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Performance Impact
|
|
432
|
+
|
|
433
|
+
For rules with many conditions, unlinking can reduce RETE network activations by **90%+**:
|
|
434
|
+
|
|
435
|
+
- Empty alpha memories don't trigger join operations
|
|
436
|
+
- Empty beta memories don't process fact assertions
|
|
437
|
+
- Network "lights up" only the relevant paths
|
|
438
|
+
|
|
439
|
+
This is especially critical for:
|
|
440
|
+
- **Negated conditions** (often have empty alpha memories)
|
|
441
|
+
- **Rare patterns** (e.g., "critical alert" facts)
|
|
442
|
+
- **Complex rules** (many conditions = more opportunities for empty nodes)
|
|
443
|
+
|
|
444
|
+
## Variable Binding
|
|
445
|
+
|
|
446
|
+
Variables (symbols starting with `?`) enable cross-condition constraints and action parameterization:
|
|
447
|
+
|
|
448
|
+
### Extraction During Network Build
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
class Condition
|
|
452
|
+
def extract_variables(pattern)
|
|
453
|
+
vars = {}
|
|
454
|
+
pattern.each do |key, value|
|
|
455
|
+
if value.is_a?(Symbol) && value.to_s.start_with?('?')
|
|
456
|
+
vars[value] = key # { :symbol? => :symbol, :price? => :price }
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
vars
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Implementation**: `lib/kbs/condition.rb:16`
|
|
465
|
+
|
|
466
|
+
### Join Test Generation
|
|
467
|
+
|
|
468
|
+
Variables create **join tests** to ensure consistency:
|
|
469
|
+
|
|
470
|
+
```ruby
|
|
471
|
+
# Rule with shared ?symbol variable
|
|
472
|
+
conditions = [
|
|
473
|
+
Condition.new(:stock, { symbol: :symbol?, price: :price? }),
|
|
474
|
+
Condition.new(:order, { symbol: :symbol?, quantity: 100 })
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
# Generates join test:
|
|
478
|
+
{
|
|
479
|
+
token_field_index: 0, # Check first fact in token (stock)
|
|
480
|
+
token_field: :symbol, # Get its :symbol attribute
|
|
481
|
+
fact_field: :symbol, # Compare with order's :symbol attribute
|
|
482
|
+
operation: :eq # Must be equal
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Implementation**: `lib/kbs/join_node.rb:89`
|
|
487
|
+
|
|
488
|
+
### Action Binding
|
|
489
|
+
|
|
490
|
+
When a rule fires, bindings are extracted for the action:
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
def fire(facts)
|
|
494
|
+
bindings = extract_bindings(facts)
|
|
495
|
+
# bindings = { :symbol? => "AAPL", :price? => 150.0 }
|
|
496
|
+
|
|
497
|
+
@action.call(facts, bindings)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def extract_bindings(facts)
|
|
501
|
+
bindings = {}
|
|
502
|
+
@conditions.each_with_index do |condition, index|
|
|
503
|
+
next if condition.negated # Negated conditions have no fact
|
|
504
|
+
fact = facts[index]
|
|
505
|
+
condition.variable_bindings.each do |var, field|
|
|
506
|
+
bindings[var] = fact.attributes[field]
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
bindings
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Implementation**: `lib/kbs/rule.rb:34`
|
|
514
|
+
|
|
515
|
+
## Advanced Topics
|
|
516
|
+
|
|
517
|
+
### Conflict Resolution
|
|
518
|
+
|
|
519
|
+
When multiple rules are activated simultaneously, KBS uses **priority** (higher values fire first):
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
rule1 = Rule.new("urgent", priority: 10) { ... }
|
|
523
|
+
rule2 = Rule.new("normal", priority: 0) { ... }
|
|
524
|
+
|
|
525
|
+
# rule1 fires before rule2
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
For same-priority rules, firing order is deterministic but unspecified (depends on hash ordering).
|
|
529
|
+
|
|
530
|
+
### Fact Retraction
|
|
531
|
+
|
|
532
|
+
Removing facts triggers **recursive token deletion**:
|
|
533
|
+
|
|
534
|
+
```ruby
|
|
535
|
+
class JoinNode
|
|
536
|
+
def right_deactivate(fact)
|
|
537
|
+
tokens_to_remove = []
|
|
538
|
+
|
|
539
|
+
@beta_memory.tokens.each do |token|
|
|
540
|
+
# Find child tokens containing this fact
|
|
541
|
+
token.children.select { |child| child.fact == fact }.each do |child|
|
|
542
|
+
tokens_to_remove << child
|
|
543
|
+
@successors.each { |s| s.deactivate(child) } # Recursive!
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
tokens_to_remove.each { |token| token.parent.children.delete(token) }
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
This ensures **truth maintenance**: when a premise is removed, all derived conclusions are also removed.
|
|
553
|
+
|
|
554
|
+
**Implementation**: `lib/kbs/join_node.rb:72`
|
|
555
|
+
|
|
556
|
+
### Network Sharing
|
|
557
|
+
|
|
558
|
+
Alpha memories are **shared across rules** using pattern as the hash key:
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
def get_or_create_alpha_memory(pattern)
|
|
562
|
+
@alpha_memories[pattern] ||= AlphaMemory.new(pattern)
|
|
563
|
+
end
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
If 10 rules all match `stock(symbol: "AAPL")`, they share one `AlphaMemory` node, reducing:
|
|
567
|
+
- Memory usage (one fact store instead of 10)
|
|
568
|
+
- Computation (one pattern match instead of 10)
|
|
569
|
+
|
|
570
|
+
**Implementation**: `lib/kbs/rete_engine.rb:104`
|
|
571
|
+
|
|
572
|
+
### Incremental Matching
|
|
573
|
+
|
|
574
|
+
RETE is **incremental**: after the initial network build, only changes are processed. Adding a fact activates a small subgraph, not the entire network.
|
|
575
|
+
|
|
576
|
+
**Complexity**:
|
|
577
|
+
- Initial build: O(R × F) where R = rules, F = facts
|
|
578
|
+
- Per-fact addition: O(N) where N = activated nodes (typically << R × F)
|
|
579
|
+
- Per-fact removal: O(T) where T = tokens to remove
|
|
580
|
+
|
|
581
|
+
In practice, RETE can handle millions of facts with sub-millisecond updates.
|
|
582
|
+
|
|
583
|
+
## Debugging RETE Networks
|
|
584
|
+
|
|
585
|
+
### Visualizing Token Flow
|
|
586
|
+
|
|
587
|
+
Enable token tracing:
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
class Token
|
|
591
|
+
def to_s
|
|
592
|
+
"Token(#{facts.map(&:to_s).join(', ')})"
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# In your rule action:
|
|
597
|
+
r.action = lambda do |facts, bindings|
|
|
598
|
+
puts "Fired with facts: #{facts.map(&:to_s).join(', ')}"
|
|
599
|
+
puts "Bindings: #{bindings.inspect}"
|
|
600
|
+
end
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Inspecting Network State
|
|
604
|
+
|
|
605
|
+
Check what's in memories:
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
# Alpha memory contents
|
|
609
|
+
engine.alpha_memories.each do |pattern, memory|
|
|
610
|
+
puts "Pattern #{pattern}: #{memory.items.size} facts"
|
|
611
|
+
memory.items.each { |f| puts " - #{f}" }
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Beta memory contents (requires introspection)
|
|
615
|
+
def walk_beta_network(beta)
|
|
616
|
+
puts "Beta memory: #{beta.tokens.size} tokens"
|
|
617
|
+
beta.tokens.each { |t| puts " - #{t}" }
|
|
618
|
+
beta.successors.each do |node|
|
|
619
|
+
if node.is_a?(BetaMemory)
|
|
620
|
+
walk_beta_network(node)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Common Pitfalls
|
|
627
|
+
|
|
628
|
+
1. **Forgetting to call `engine.run()`**: Tokens accumulate but rules don't fire
|
|
629
|
+
2. **Pattern mismatches**: `{ type: :stock }` vs `Condition.new(:stock, {})` - the latter doesn't filter by type!
|
|
630
|
+
3. **Variable binding errors**: Using `?symbol` (string) instead of `:symbol?` (symbol)
|
|
631
|
+
4. **Negation timing**: Negated conditions only fire when facts are **absent**, not after they're removed (use `engine.run()` to re-evaluate)
|
|
632
|
+
|
|
633
|
+
## Performance Characteristics
|
|
634
|
+
|
|
635
|
+
### Time Complexity
|
|
636
|
+
|
|
637
|
+
| Operation | Complexity | Notes |
|
|
638
|
+
|-----------|-----------|-------|
|
|
639
|
+
| Add rule | O(C × F) | C = conditions, F = existing facts |
|
|
640
|
+
| Add fact | O(N) | N = activated nodes (avg << total nodes) |
|
|
641
|
+
| Remove fact | O(T) | T = tokens containing fact |
|
|
642
|
+
| Run rules | O(M) | M = matched tokens in production nodes |
|
|
643
|
+
|
|
644
|
+
### Space Complexity
|
|
645
|
+
|
|
646
|
+
| Structure | Space | Notes |
|
|
647
|
+
|-----------|-------|-------|
|
|
648
|
+
| Alpha memories | O(F × P) | F = facts, P = unique patterns |
|
|
649
|
+
| Beta memories | O(T) | T = partial match tokens |
|
|
650
|
+
| Tokens | O(C × M) | C = conditions, M = complete matches |
|
|
651
|
+
| Network nodes | O(R × C) | R = rules, C = avg conditions per rule |
|
|
652
|
+
|
|
653
|
+
### Optimization Strategies
|
|
654
|
+
|
|
655
|
+
1. **Pattern specificity**: Put most selective conditions first to reduce beta memory size
|
|
656
|
+
2. **Negation placement**: Place negated conditions last (they don't add facts to tokens)
|
|
657
|
+
3. **Shared patterns**: Design rules to share common patterns
|
|
658
|
+
4. **Fact pruning**: Remove obsolete facts to trigger unlinking
|
|
659
|
+
5. **Priority tuning**: Use priority to fire expensive rules last
|
|
660
|
+
|
|
661
|
+
## Comparison with Other Algorithms
|
|
662
|
+
|
|
663
|
+
### Naive Match-All
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
# O(R × F) on every cycle
|
|
667
|
+
def naive_fire_rules
|
|
668
|
+
rules.each do |rule|
|
|
669
|
+
facts.each do |fact|
|
|
670
|
+
if rule.matches?(fact)
|
|
671
|
+
rule.fire(fact)
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Problem**: Re-evaluates everything, no state preservation.
|
|
679
|
+
|
|
680
|
+
### TREAT
|
|
681
|
+
|
|
682
|
+
TREAT eliminates alpha/beta network in favor of lazy evaluation:
|
|
683
|
+
- **Pros**: Simpler implementation, lower memory
|
|
684
|
+
- **Cons**: Slower for rules that fire frequently (no memoization)
|
|
685
|
+
|
|
686
|
+
RETE is better when rules fire often; TREAT is better for sparse firing.
|
|
687
|
+
|
|
688
|
+
### Basic RETE vs RETE with Unlinking
|
|
689
|
+
|
|
690
|
+
Early RETE implementations lacked unlinking:
|
|
691
|
+
- **Without unlinking**: All nodes always active, many wasted join operations
|
|
692
|
+
- **With unlinking**: Nodes disconnect when empty, up to 10× faster
|
|
693
|
+
|
|
694
|
+
KBS implements RETE with unlinking optimization.
|
|
695
|
+
|
|
696
|
+
## Implementation Files
|
|
697
|
+
|
|
698
|
+
| Component | File | Lines |
|
|
699
|
+
|-----------|------|-------|
|
|
700
|
+
| Core engine | `lib/kbs/rete_engine.rb` | ~110 |
|
|
701
|
+
| Working memory | `lib/kbs/working_memory.rb` | ~35 |
|
|
702
|
+
| Facts | `lib/kbs/fact.rb` | ~45 |
|
|
703
|
+
| Tokens | `lib/kbs/token.rb` | ~40 |
|
|
704
|
+
| Alpha memory | `lib/kbs/alpha_memory.rb` | ~40 |
|
|
705
|
+
| Beta memory | `lib/kbs/beta_memory.rb` | ~60 |
|
|
706
|
+
| Join nodes | `lib/kbs/join_node.rb` | ~120 |
|
|
707
|
+
| Negation nodes | `lib/kbs/negation_node.rb` | ~90 |
|
|
708
|
+
| Production nodes | `lib/kbs/production_node.rb` | ~30 |
|
|
709
|
+
| Conditions | `lib/kbs/condition.rb` | ~30 |
|
|
710
|
+
| Rules | `lib/kbs/rule.rb` | ~50 |
|
|
711
|
+
|
|
712
|
+
**Total**: ~650 lines of core RETE implementation.
|
|
713
|
+
|
|
714
|
+
## Further Reading
|
|
715
|
+
|
|
716
|
+
### Academic Papers
|
|
717
|
+
|
|
718
|
+
- Forgy, C. (1982). "Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem". *Artificial Intelligence*, 19(1), 17-37.
|
|
719
|
+
- Forgy, C. (1989). "Rete: A Fast Match Algorithm". *AI Expert*, 4(1), 34-40.
|
|
720
|
+
|
|
721
|
+
### Textbooks
|
|
722
|
+
|
|
723
|
+
- Giarratano, J., & Riley, G. (2004). *Expert Systems: Principles and Programming* (4th ed.). Course Technology.
|
|
724
|
+
- Russell, S., & Norvig, P. (2020). *Artificial Intelligence: A Modern Approach* (4th ed.). Pearson. (Chapter on Rule-Based Systems)
|
|
725
|
+
|
|
726
|
+
### Online Resources
|
|
727
|
+
|
|
728
|
+
- [RETE Algorithm Visualization](http://www.jessrules.com/docs/71/rete.html) - Jess documentation
|
|
729
|
+
- [Production Systems](https://en.wikipedia.org/wiki/Production_system_(computer_science)) - Wikipedia
|
|
730
|
+
- [Rule-Based Expert Systems](https://www.cs.toronto.edu/~hector/PublicKnowledgeBase.html) - University of Toronto
|
|
731
|
+
|
|
732
|
+
## Next Steps
|
|
733
|
+
|
|
734
|
+
- **[DSL Guide](../guides/dsl.md)**: Learn how to write rules using KBS's Ruby DSL
|
|
735
|
+
- **[Blackboard Architecture](blackboard.md)**: Understand persistent memory and multi-agent systems
|
|
736
|
+
- **[Examples](../examples/index.md)**: See RETE in action with stock trading and expert systems
|
|
737
|
+
- **[Performance Tuning](../advanced/performance.md)**: Optimize your rule-based system
|