kbs 0.1.0 → 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.
- checksums.yaml +4 -4
- data/README.md +85 -57
- data/docs/advanced/performance.md +109 -76
- data/docs/advanced/testing.md +399 -263
- data/docs/api/blackboard.md +1 -1
- data/docs/api/engine.md +77 -8
- data/docs/api/facts.md +3 -3
- data/docs/api/rules.md +110 -40
- data/docs/architecture/blackboard.md +108 -117
- data/docs/assets/images/fact-rule-relationship.svg +65 -0
- data/docs/assets/images/fact-structure.svg +42 -0
- data/docs/assets/images/inference-cycle.svg +47 -0
- data/docs/assets/images/kb-components.svg +43 -0
- data/docs/assets/images/rule-structure.svg +44 -0
- data/docs/assets/images/trading-signal-network.svg +1 -1
- data/docs/examples/index.md +219 -5
- data/docs/guides/blackboard-memory.md +89 -58
- data/docs/guides/dsl.md +24 -24
- data/docs/guides/getting-started.md +109 -107
- data/docs/guides/writing-rules.md +470 -311
- data/docs/index.md +16 -18
- data/docs/quick-start.md +92 -99
- data/docs/what-is-a-fact.md +694 -0
- data/docs/what-is-a-knowledge-base.md +350 -0
- data/docs/what-is-a-rule.md +833 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_example_dsl.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +1 -1
- data/examples/concurrent_inference_demo.rb +0 -1
- data/examples/concurrent_inference_demo_dsl.rb +0 -1
- data/examples/csv_trading_system_dsl.rb +1 -1
- data/examples/iot_demo_using_dsl.rb +1 -1
- data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
- data/examples/rule_source_demo.rb +123 -0
- data/examples/stock_trading_advanced_dsl.rb +1 -1
- data/examples/temp_dsl.txt +6214 -5269
- data/examples/timestamped_trading_dsl.rb +1 -1
- data/examples/trading_demo_dsl.rb +1 -1
- data/examples/working_demo_dsl.rb +1 -1
- data/lib/kbs/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +100 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/engine.rb +41 -0
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +14 -12
- data/mkdocs.yml +30 -30
- metadata +15 -10
- data/docs/DOCUMENTATION_STATUS.md +0 -158
- data/docs/examples/expert-systems.md +0 -1031
- data/docs/examples/multi-agent.md +0 -1335
- data/docs/examples/stock-trading.md +0 -488
- data/examples/knowledge_base.db +0 -0
- data/examples/temp.txt +0 -7693
data/docs/api/blackboard.md
CHANGED
|
@@ -1154,4 +1154,4 @@ archive_old_audit(memory, Date.today - 30)
|
|
|
1154
1154
|
- [Facts API](facts.md) - Persistent fact objects
|
|
1155
1155
|
- [Custom Persistence](../advanced/custom-persistence.md) - Implementing custom stores
|
|
1156
1156
|
- [Blackboard Guide](../guides/blackboard-memory.md) - Blackboard pattern overview
|
|
1157
|
-
- [
|
|
1157
|
+
- [Blackboard Examples](../examples/index.md#advanced-features) - Multi-agent coordination and blackboard systems
|
data/docs/api/engine.md
CHANGED
|
@@ -25,7 +25,7 @@ Creates a new in-memory RETE engine.
|
|
|
25
25
|
|
|
26
26
|
**Returns**: `KBS::Engine` instance
|
|
27
27
|
|
|
28
|
-
**Example**:
|
|
28
|
+
**Example - Low-level API**:
|
|
29
29
|
```ruby
|
|
30
30
|
require 'kbs'
|
|
31
31
|
|
|
@@ -33,6 +33,19 @@ engine = KBS::Engine.new
|
|
|
33
33
|
# Engine ready with empty working memory
|
|
34
34
|
```
|
|
35
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
|
+
|
|
36
49
|
**Internal State Initialized**:
|
|
37
50
|
- `@working_memory` - WorkingMemory instance
|
|
38
51
|
- `@rules` - Array of registered rules
|
|
@@ -79,7 +92,7 @@ engine.add_rule(rule)
|
|
|
79
92
|
kb = KBS.knowledge_base do
|
|
80
93
|
rule "high_temperature", priority: 10 do
|
|
81
94
|
on :temperature, location: "server_room", value: greater_than(80)
|
|
82
|
-
perform do |bindings|
|
|
95
|
+
perform do |facts, bindings|
|
|
83
96
|
puts "Alert: #{bindings[:location?]} is #{bindings[:value?]}°F"
|
|
84
97
|
end
|
|
85
98
|
end
|
|
@@ -112,7 +125,7 @@ Adds a fact to working memory and activates matching alpha memories.
|
|
|
112
125
|
- Propagates through join nodes
|
|
113
126
|
- May create new tokens in beta memories
|
|
114
127
|
|
|
115
|
-
**Example**:
|
|
128
|
+
**Example - Low-level API**:
|
|
116
129
|
```ruby
|
|
117
130
|
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
118
131
|
# => #<KBS::Fact:0x00... @type=:temperature @attributes={...}>
|
|
@@ -122,6 +135,14 @@ marker = engine.add_fact(:system_ready)
|
|
|
122
135
|
# => #<KBS::Fact:0x00... @type=:system_ready @attributes={}>
|
|
123
136
|
```
|
|
124
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
|
+
|
|
125
146
|
**Thread Safety**: Not thread-safe. Wrap in mutex if adding facts from multiple threads.
|
|
126
147
|
|
|
127
148
|
**Performance**: O(A × P) where A is number of alpha memories, P is pattern matching cost
|
|
@@ -181,7 +202,7 @@ Executes all activated rules by firing production nodes.
|
|
|
181
202
|
- Rule actions may add/remove facts
|
|
182
203
|
- Rule actions may modify external state
|
|
183
204
|
|
|
184
|
-
**Example**:
|
|
205
|
+
**Example - Low-level API**:
|
|
185
206
|
```ruby
|
|
186
207
|
engine.add_fact(:temperature, value: 85)
|
|
187
208
|
engine.add_fact(:sensor, status: "active")
|
|
@@ -192,6 +213,21 @@ engine.run # Execute all matching rules
|
|
|
192
213
|
# Rules fire based on priority (highest first within each production)
|
|
193
214
|
```
|
|
194
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
|
+
|
|
195
231
|
**Execution Order**:
|
|
196
232
|
- Production nodes fire in arbitrary order (dictionary order by rule name)
|
|
197
233
|
- Within a production node, tokens fire in insertion order
|
|
@@ -392,6 +428,24 @@ engine = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
|
|
|
392
428
|
# Facts persisted to knowledge_base.db
|
|
393
429
|
```
|
|
394
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
|
+
|
|
395
449
|
**Example - Redis Persistence**:
|
|
396
450
|
```ruby
|
|
397
451
|
require 'kbs/blackboard/persistence/redis_store'
|
|
@@ -434,7 +488,7 @@ Adds a persistent fact to the blackboard.
|
|
|
434
488
|
- Activates alpha memories
|
|
435
489
|
- Notifies observers
|
|
436
490
|
|
|
437
|
-
**Example**:
|
|
491
|
+
**Example - Low-level API**:
|
|
438
492
|
```ruby
|
|
439
493
|
fact = engine.add_fact(:temperature, location: "server_room", value: 85)
|
|
440
494
|
puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
|
|
@@ -445,6 +499,21 @@ reloaded_facts = engine2.blackboard.get_facts_by_type(:temperature)
|
|
|
445
499
|
puts reloaded_facts.first[:value] # => 85
|
|
446
500
|
```
|
|
447
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
|
+
|
|
448
517
|
**Difference from KBS::Engine**: Returns `KBS::Blackboard::Fact` (has `.uuid`) instead of `KBS::Fact`.
|
|
449
518
|
|
|
450
519
|
---
|
|
@@ -639,7 +708,7 @@ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
|
|
|
639
708
|
kb = KBS.knowledge_base do
|
|
640
709
|
rule "high_temp_alert", priority: 10 do
|
|
641
710
|
on :temperature, value: greater_than(80)
|
|
642
|
-
perform do |bindings|
|
|
711
|
+
perform do |facts, bindings|
|
|
643
712
|
puts "Alert! Temperature: #{bindings[:value?]}"
|
|
644
713
|
end
|
|
645
714
|
end
|
|
@@ -894,7 +963,7 @@ old_facts.each { |f| engine.remove_fact(f) }
|
|
|
894
963
|
```ruby
|
|
895
964
|
rule "expire_old_facts", priority: 0 do
|
|
896
965
|
on :temperature, timestamp: ->(ts) { Time.now - ts > 3600 }
|
|
897
|
-
perform do |bindings|
|
|
966
|
+
perform do |facts, bindings|
|
|
898
967
|
fact = bindings[:matched_fact?]
|
|
899
968
|
engine.remove_fact(fact)
|
|
900
969
|
end
|
|
@@ -910,7 +979,7 @@ end
|
|
|
910
979
|
```ruby
|
|
911
980
|
rule "risky_operation" do
|
|
912
981
|
on :task, status: "pending"
|
|
913
|
-
perform do |bindings|
|
|
982
|
+
perform do |facts, bindings|
|
|
914
983
|
begin
|
|
915
984
|
perform_risky_operation(bindings[:task_id?])
|
|
916
985
|
rescue => e
|
data/docs/api/facts.md
CHANGED
|
@@ -486,7 +486,7 @@ engine.remove_fact(fact)
|
|
|
486
486
|
```ruby
|
|
487
487
|
rule "auto_expire_old_alerts" do
|
|
488
488
|
on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
|
|
489
|
-
perform do |bindings|
|
|
489
|
+
perform do |facts, bindings|
|
|
490
490
|
# Fact can remove itself
|
|
491
491
|
alert_fact = bindings[:matched_fact?]
|
|
492
492
|
alert_fact.retract
|
|
@@ -1047,7 +1047,7 @@ You can't directly compare two attributes of the same fact in one condition. Use
|
|
|
1047
1047
|
# Instead: Capture variables and check in action or use join test
|
|
1048
1048
|
rule "large_order" do
|
|
1049
1049
|
on :order, quantity: :qty?, price: :price?
|
|
1050
|
-
perform do |bindings|
|
|
1050
|
+
perform do |facts, bindings|
|
|
1051
1051
|
total = bindings[:qty?] * bindings[:price?]
|
|
1052
1052
|
if total > 10000
|
|
1053
1053
|
puts "Large order: $#{total}"
|
|
@@ -1111,7 +1111,7 @@ condition = KBS::Condition.new(
|
|
|
1111
1111
|
# Better - Simple check first, complex check in action
|
|
1112
1112
|
rule "complex_log_analysis" do
|
|
1113
1113
|
on :log_entry, level: "ERROR", message: :msg? # Simple literal filter
|
|
1114
|
-
perform do |bindings|
|
|
1114
|
+
perform do |facts, bindings|
|
|
1115
1115
|
if bindings[:msg?] =~ /very.*complex.*regex.*pattern/
|
|
1116
1116
|
# Expensive check runs only on ERROR logs
|
|
1117
1117
|
end
|
data/docs/api/rules.md
CHANGED
|
@@ -38,7 +38,7 @@ Creates a new rule.
|
|
|
38
38
|
|
|
39
39
|
**Returns**: `KBS::Rule` instance
|
|
40
40
|
|
|
41
|
-
**Example - Direct Construction**:
|
|
41
|
+
**Example - Low-level API (Direct Construction)**:
|
|
42
42
|
```ruby
|
|
43
43
|
# Minimal rule
|
|
44
44
|
rule = KBS::Rule.new(:high_temperature)
|
|
@@ -54,7 +54,7 @@ rule = KBS::Rule.new(
|
|
|
54
54
|
)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
**Example - Block Configuration**:
|
|
57
|
+
**Example - Low-level API (Block Configuration)**:
|
|
58
58
|
```ruby
|
|
59
59
|
rule = KBS::Rule.new(:high_temperature) do |r|
|
|
60
60
|
r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
@@ -62,12 +62,12 @@ rule = KBS::Rule.new(:high_temperature) do |r|
|
|
|
62
62
|
end
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
**
|
|
65
|
+
**Using DSL (Recommended)**:
|
|
66
66
|
```ruby
|
|
67
67
|
kb = KBS.knowledge_base do
|
|
68
68
|
rule "high_temperature", priority: 10 do
|
|
69
69
|
on :temperature, value: greater_than(80)
|
|
70
|
-
perform do |bindings|
|
|
70
|
+
perform do |facts, bindings|
|
|
71
71
|
puts "High temperature: #{bindings[:value?]}"
|
|
72
72
|
end
|
|
73
73
|
end
|
|
@@ -89,23 +89,35 @@ kb.rules.each { |r| engine.add_rule(r) }
|
|
|
89
89
|
|
|
90
90
|
**Description**: Unique rule identifier
|
|
91
91
|
|
|
92
|
-
**Example**:
|
|
92
|
+
**Example - Low-level API**:
|
|
93
93
|
```ruby
|
|
94
94
|
rule = KBS::Rule.new(:high_temperature, priority: 10)
|
|
95
95
|
puts rule.name # => :high_temperature
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
**Using DSL (Recommended)**:
|
|
99
|
+
```ruby
|
|
100
|
+
kb = KBS.knowledge_base do
|
|
101
|
+
rule "high_temperature", priority: 10 do
|
|
102
|
+
on :temperature, value: greater_than(80)
|
|
103
|
+
perform { puts "Alert!" }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
puts kb.rules.first.name # => "high_temperature"
|
|
108
|
+
```
|
|
109
|
+
|
|
98
110
|
**Best Practice**: Use descriptive names that indicate the rule's purpose:
|
|
99
111
|
```ruby
|
|
100
112
|
# Good
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
"high_temperature_alert"
|
|
114
|
+
"low_inventory_reorder"
|
|
115
|
+
"fraud_detection_high_risk"
|
|
104
116
|
|
|
105
117
|
# Less clear
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
"rule1"
|
|
119
|
+
"temp_rule"
|
|
120
|
+
"check"
|
|
109
121
|
```
|
|
110
122
|
|
|
111
123
|
---
|
|
@@ -122,12 +134,22 @@ puts rule.name # => :high_temperature
|
|
|
122
134
|
|
|
123
135
|
**Range**: Any integer (commonly 0-100)
|
|
124
136
|
|
|
125
|
-
**Example**:
|
|
137
|
+
**Example - Low-level API**:
|
|
126
138
|
```ruby
|
|
127
139
|
rule = KBS::Rule.new(:critical_alert, priority: 100)
|
|
128
140
|
puts rule.priority # => 100
|
|
129
141
|
```
|
|
130
142
|
|
|
143
|
+
**Using DSL (Recommended)**:
|
|
144
|
+
```ruby
|
|
145
|
+
kb = KBS.knowledge_base do
|
|
146
|
+
rule "critical_alert", priority: 100 do
|
|
147
|
+
on :alert, level: "critical"
|
|
148
|
+
perform { puts "CRITICAL ALERT!" }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
131
153
|
**Priority Semantics**:
|
|
132
154
|
- **KBS::Engine**: Priority is stored but NOT used for execution order (rules fire in arbitrary order)
|
|
133
155
|
- **KBS::Blackboard::Engine**: Higher priority rules fire first within production nodes
|
|
@@ -155,7 +177,7 @@ priority: -10
|
|
|
155
177
|
kb = KBS.knowledge_base do
|
|
156
178
|
rule "log_temperature", priority: 0 do
|
|
157
179
|
on :temperature, value: :temp?
|
|
158
|
-
perform { |b| puts "Logged: #{b[:temp?]}" }
|
|
180
|
+
perform { |facts, b| puts "Logged: #{b[:temp?]}" }
|
|
159
181
|
end
|
|
160
182
|
|
|
161
183
|
rule "critical_alert", priority: 100 do
|
|
@@ -190,7 +212,7 @@ engine.run
|
|
|
190
212
|
|
|
191
213
|
**Description**: Array of conditions that must all match for rule to fire
|
|
192
214
|
|
|
193
|
-
**Example**:
|
|
215
|
+
**Example - Low-level API**:
|
|
194
216
|
```ruby
|
|
195
217
|
rule = KBS::Rule.new(:temperature_alert)
|
|
196
218
|
rule.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
|
|
@@ -199,7 +221,22 @@ rule.conditions << KBS::Condition.new(:sensor, status: "active")
|
|
|
199
221
|
puts rule.conditions.size # => 2
|
|
200
222
|
```
|
|
201
223
|
|
|
224
|
+
**Using DSL (Recommended)**:
|
|
225
|
+
```ruby
|
|
226
|
+
kb = KBS.knowledge_base do
|
|
227
|
+
rule "temperature_alert" do
|
|
228
|
+
on :temperature, value: greater_than(80)
|
|
229
|
+
on :sensor, status: "active"
|
|
230
|
+
perform { puts "Alert!" }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
puts kb.rules.first.conditions.size # => 2
|
|
235
|
+
```
|
|
236
|
+
|
|
202
237
|
**Condition Order Matters** (for performance):
|
|
238
|
+
|
|
239
|
+
**Low-level API**:
|
|
203
240
|
```ruby
|
|
204
241
|
# Good - Most selective condition first
|
|
205
242
|
rule.conditions = [
|
|
@@ -214,6 +251,23 @@ rule.conditions = [
|
|
|
214
251
|
]
|
|
215
252
|
```
|
|
216
253
|
|
|
254
|
+
**Using DSL (Recommended)**:
|
|
255
|
+
```ruby
|
|
256
|
+
# Good - Most selective condition first
|
|
257
|
+
rule "sensor_alert" do
|
|
258
|
+
on :sensor, id: 42 # Filters to 1 fact
|
|
259
|
+
on :temperature, value: :temp? # Then match temperature
|
|
260
|
+
perform { |facts, b| puts b[:temp?] }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Less optimal - Less selective first
|
|
264
|
+
rule "sensor_alert" do
|
|
265
|
+
on :temperature, value: :temp? # Matches many facts
|
|
266
|
+
on :sensor, id: 42 # Could have filtered first
|
|
267
|
+
perform { |facts, b| puts b[:temp?] }
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
217
271
|
See [Performance Guide](../advanced/performance.md) for condition ordering strategies.
|
|
218
272
|
|
|
219
273
|
---
|
|
@@ -232,7 +286,7 @@ See [Performance Guide](../advanced/performance.md) for condition ordering strat
|
|
|
232
286
|
- `facts` (Array<KBS::Fact>) - Array of matched facts (parallel to conditions array)
|
|
233
287
|
- `bindings` (Hash, optional) - Variable bindings extracted from facts
|
|
234
288
|
|
|
235
|
-
**Example - Facts Parameter**:
|
|
289
|
+
**Example - Low-level API (Facts Parameter)**:
|
|
236
290
|
```ruby
|
|
237
291
|
rule.action = ->(facts) do
|
|
238
292
|
temp_fact = facts[0] # First condition's matched fact
|
|
@@ -242,7 +296,7 @@ rule.action = ->(facts) do
|
|
|
242
296
|
end
|
|
243
297
|
```
|
|
244
298
|
|
|
245
|
-
**Example - Bindings Parameter**:
|
|
299
|
+
**Example - Low-level API (Bindings Parameter)**:
|
|
246
300
|
```ruby
|
|
247
301
|
# Rule with variable bindings
|
|
248
302
|
rule = KBS::Rule.new(:temperature_alert) do |r|
|
|
@@ -254,11 +308,11 @@ rule = KBS::Rule.new(:temperature_alert) do |r|
|
|
|
254
308
|
end
|
|
255
309
|
```
|
|
256
310
|
|
|
257
|
-
**
|
|
311
|
+
**Using DSL (Recommended)**:
|
|
258
312
|
```ruby
|
|
259
313
|
rule "temperature_alert" do
|
|
260
314
|
on :temperature, value: :temp?, location: :loc?
|
|
261
|
-
perform do |bindings|
|
|
315
|
+
perform do |facts, bindings|
|
|
262
316
|
# Cleaner - DSL automatically provides bindings
|
|
263
317
|
puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
|
|
264
318
|
end
|
|
@@ -289,7 +343,7 @@ Executes the rule's action with matched facts.
|
|
|
289
343
|
- Executes action lambda
|
|
290
344
|
- Action may modify external state, add/remove facts, etc.
|
|
291
345
|
|
|
292
|
-
**Example**:
|
|
346
|
+
**Example - Low-level API**:
|
|
293
347
|
```ruby
|
|
294
348
|
rule = KBS::Rule.new(:log_temperature) do |r|
|
|
295
349
|
r.conditions << KBS::Condition.new(:temperature, value: :temp?)
|
|
@@ -303,6 +357,22 @@ rule.fire([fact])
|
|
|
303
357
|
# Output: Temperature: 85
|
|
304
358
|
```
|
|
305
359
|
|
|
360
|
+
**Using DSL (Recommended)**:
|
|
361
|
+
```ruby
|
|
362
|
+
kb = KBS.knowledge_base do
|
|
363
|
+
rule "log_temperature" do
|
|
364
|
+
on :temperature, value: :temp?
|
|
365
|
+
perform do |facts, bindings|
|
|
366
|
+
puts "Temperature: #{bindings[:temp?]}"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
fact :temperature, value: 85
|
|
371
|
+
run # Fires the rule automatically
|
|
372
|
+
end
|
|
373
|
+
# Output: Temperature: 85
|
|
374
|
+
```
|
|
375
|
+
|
|
306
376
|
**Note**: Typically called by the RETE engine, not user code. Users call `engine.run` which fires all activated rules.
|
|
307
377
|
|
|
308
378
|
---
|
|
@@ -316,7 +386,7 @@ rule.fire([fact])
|
|
|
316
386
|
kb = KBS.knowledge_base do
|
|
317
387
|
rule "my_rule", priority: 10 do
|
|
318
388
|
on :temperature, value: :temp?
|
|
319
|
-
perform { |b| puts b[:temp?] }
|
|
389
|
+
perform { |facts, b| puts b[:temp?] }
|
|
320
390
|
end
|
|
321
391
|
end
|
|
322
392
|
|
|
@@ -386,7 +456,7 @@ Rules can fire multiple times:
|
|
|
386
456
|
```ruby
|
|
387
457
|
rule "log_temperature" do
|
|
388
458
|
on :temperature, value: :temp?
|
|
389
|
-
perform { |b| puts "Temperature: #{b[:temp?]}" }
|
|
459
|
+
perform { |facts, b| puts "Temperature: #{b[:temp?]}" }
|
|
390
460
|
end
|
|
391
461
|
|
|
392
462
|
engine.add_fact(:temperature, value: 85)
|
|
@@ -413,7 +483,7 @@ Match single fact type:
|
|
|
413
483
|
```ruby
|
|
414
484
|
rule "log_all_temperatures" do
|
|
415
485
|
on :temperature, value: :temp?
|
|
416
|
-
perform do |bindings|
|
|
486
|
+
perform do |facts, bindings|
|
|
417
487
|
puts "Temperature: #{bindings[:temp?]}"
|
|
418
488
|
end
|
|
419
489
|
end
|
|
@@ -429,7 +499,7 @@ Match multiple related facts:
|
|
|
429
499
|
rule "sensor_temperature_alert" do
|
|
430
500
|
on :sensor, id: :sensor_id?, status: "active"
|
|
431
501
|
on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
|
|
432
|
-
perform do |bindings|
|
|
502
|
+
perform do |facts, bindings|
|
|
433
503
|
puts "Sensor #{bindings[:sensor_id?]} reports high temperature"
|
|
434
504
|
end
|
|
435
505
|
end
|
|
@@ -467,7 +537,7 @@ Rules can implement state transitions:
|
|
|
467
537
|
rule "pending_to_processing" do
|
|
468
538
|
on :order, id: :order_id?, status: "pending"
|
|
469
539
|
on :worker, status: "available", id: :worker_id?
|
|
470
|
-
perform do |bindings|
|
|
540
|
+
perform do |facts, bindings|
|
|
471
541
|
# Transition order to processing
|
|
472
542
|
order = find_order(bindings[:order_id?])
|
|
473
543
|
order.update(status: "processing", worker_id: bindings[:worker_id?])
|
|
@@ -488,7 +558,7 @@ Low-priority rules that clean up old facts:
|
|
|
488
558
|
```ruby
|
|
489
559
|
rule "expire_old_temperatures", priority: 0 do
|
|
490
560
|
on :temperature, timestamp: less_than(Time.now - 3600)
|
|
491
|
-
perform do |bindings|
|
|
561
|
+
perform do |facts, bindings|
|
|
492
562
|
fact = bindings[:matched_fact?]
|
|
493
563
|
fact.retract # Remove old temperature reading
|
|
494
564
|
end
|
|
@@ -527,7 +597,7 @@ Higher priority rule overrides lower priority:
|
|
|
527
597
|
```ruby
|
|
528
598
|
rule "high_risk_order", priority: 100 do
|
|
529
599
|
on :order, id: :order_id?, total: greater_than(10000)
|
|
530
|
-
perform do |bindings|
|
|
600
|
+
perform do |facts, bindings|
|
|
531
601
|
puts "HIGH RISK: Order #{bindings[:order_id?]} requires manual review"
|
|
532
602
|
# This fires first due to priority
|
|
533
603
|
end
|
|
@@ -535,7 +605,7 @@ end
|
|
|
535
605
|
|
|
536
606
|
rule "auto_approve_order", priority: 10 do
|
|
537
607
|
on :order, id: :order_id?, status: "pending"
|
|
538
|
-
perform do |bindings|
|
|
608
|
+
perform do |facts, bindings|
|
|
539
609
|
puts "Auto-approving order #{bindings[:order_id?]}"
|
|
540
610
|
# This fires later (if at all)
|
|
541
611
|
end
|
|
@@ -552,7 +622,7 @@ Rule that adds facts triggering other rules:
|
|
|
552
622
|
rule "calculate_fibonacci" do
|
|
553
623
|
on :fib_request, n: :n?
|
|
554
624
|
negated :fib_result, n: :n? # Not already calculated
|
|
555
|
-
perform do |bindings|
|
|
625
|
+
perform do |facts, bindings|
|
|
556
626
|
n = bindings[:n?]
|
|
557
627
|
|
|
558
628
|
if n <= 1
|
|
@@ -572,7 +642,7 @@ rule "combine_fibonacci" do
|
|
|
572
642
|
on :fib_result, n: :n_minus_1?, value: :val1?
|
|
573
643
|
on :fib_result, n: :n_minus_2?, value: :val2?
|
|
574
644
|
# ... (complex join test: ?n_minus_1 == ?n - 1, etc.)
|
|
575
|
-
perform do |bindings|
|
|
645
|
+
perform do |facts, bindings|
|
|
576
646
|
result = bindings[:val1?] + bindings[:val2?]
|
|
577
647
|
engine.add_fact(:fib_result, n: bindings[:n?], value: result)
|
|
578
648
|
end
|
|
@@ -633,7 +703,7 @@ end
|
|
|
633
703
|
|
|
634
704
|
rule "log_temperature", priority: 0 do
|
|
635
705
|
on :temperature, value: :temp?
|
|
636
|
-
perform { |b| log(b[:temp?]) }
|
|
706
|
+
perform { |facts, b| log(b[:temp?]) }
|
|
637
707
|
end
|
|
638
708
|
```
|
|
639
709
|
|
|
@@ -647,7 +717,7 @@ Critical safety rules should have high priority to fire before less important ru
|
|
|
647
717
|
# Good - Idempotent (safe to run multiple times)
|
|
648
718
|
rule "alert_high_temp" do
|
|
649
719
|
on :temperature, value: greater_than(80)
|
|
650
|
-
perform do |bindings|
|
|
720
|
+
perform do |facts, bindings|
|
|
651
721
|
# Check if alert already sent
|
|
652
722
|
unless alert_sent?(bindings[:temp?])
|
|
653
723
|
send_alert(bindings[:temp?])
|
|
@@ -659,7 +729,7 @@ end
|
|
|
659
729
|
# Bad - Not idempotent (sends duplicate alerts)
|
|
660
730
|
rule "alert_high_temp" do
|
|
661
731
|
on :temperature, value: greater_than(80)
|
|
662
|
-
perform do |bindings|
|
|
732
|
+
perform do |facts, bindings|
|
|
663
733
|
send_alert(bindings[:temp?]) # Sends every time rule fires
|
|
664
734
|
end
|
|
665
735
|
end
|
|
@@ -696,7 +766,7 @@ end
|
|
|
696
766
|
rule "order_inventory_check" do
|
|
697
767
|
on :order, product_id: :pid?, quantity: :qty?
|
|
698
768
|
on :inventory, product_id: :pid?, available: :available?
|
|
699
|
-
perform do |bindings|
|
|
769
|
+
perform do |facts, bindings|
|
|
700
770
|
if bindings[:available?] < bindings[:qty?]
|
|
701
771
|
puts "Insufficient inventory for product #{bindings[:pid?]}"
|
|
702
772
|
end
|
|
@@ -707,7 +777,7 @@ end
|
|
|
707
777
|
rule "order_inventory_check" do
|
|
708
778
|
on :order, product_id: :pid1?, quantity: :qty?
|
|
709
779
|
on :inventory, product_id: :pid2?, available: :available?
|
|
710
|
-
perform do |bindings|
|
|
780
|
+
perform do |facts, bindings|
|
|
711
781
|
# No guarantee pid1 == pid2!
|
|
712
782
|
if bindings[:pid1?] == bindings[:pid2?] # Manual check in action (inefficient)
|
|
713
783
|
...
|
|
@@ -733,7 +803,7 @@ rule "portfolio_rebalancing", priority: 50 do
|
|
|
733
803
|
|
|
734
804
|
on :portfolio, id: :portfolio_id?, status: "active"
|
|
735
805
|
on :drift_calculation, portfolio_id: :portfolio_id?, drift: greater_than(0.05)
|
|
736
|
-
perform do |bindings|
|
|
806
|
+
perform do |facts, bindings|
|
|
737
807
|
# Implementation...
|
|
738
808
|
end
|
|
739
809
|
end
|
|
@@ -806,7 +876,7 @@ end
|
|
|
806
876
|
# Good - Cleanup rule prevents unbounded growth
|
|
807
877
|
rule "expire_old_facts", priority: 0 do
|
|
808
878
|
on :temperature, timestamp: less_than(Time.now - 3600)
|
|
809
|
-
perform do |bindings|
|
|
879
|
+
perform do |facts, bindings|
|
|
810
880
|
fact = bindings[:matched_fact?]
|
|
811
881
|
fact.retract
|
|
812
882
|
end
|
|
@@ -867,7 +937,7 @@ action: ->(facts, bindings) do
|
|
|
867
937
|
end
|
|
868
938
|
|
|
869
939
|
# 3. DSL style (cleanest)
|
|
870
|
-
perform do |bindings|
|
|
940
|
+
perform do |facts, bindings|
|
|
871
941
|
puts bindings[:temp?]
|
|
872
942
|
end
|
|
873
943
|
```
|
|
@@ -962,18 +1032,18 @@ Keep actions fast:
|
|
|
962
1032
|
|
|
963
1033
|
```ruby
|
|
964
1034
|
# Good - Fast action
|
|
965
|
-
perform do |bindings|
|
|
1035
|
+
perform do |facts, bindings|
|
|
966
1036
|
puts "Temperature: #{bindings[:temp?]}"
|
|
967
1037
|
end
|
|
968
1038
|
|
|
969
1039
|
# Bad - Slow action blocks engine
|
|
970
|
-
perform do |bindings|
|
|
1040
|
+
perform do |facts, bindings|
|
|
971
1041
|
sleep 5 # Blocks engine for 5 seconds!
|
|
972
1042
|
send_email_alert(bindings[:temp?]) # Network I/O
|
|
973
1043
|
end
|
|
974
1044
|
|
|
975
1045
|
# Better - Offload slow work
|
|
976
|
-
perform do |bindings|
|
|
1046
|
+
perform do |facts, bindings|
|
|
977
1047
|
# Post message for async worker
|
|
978
1048
|
engine.post_message("alert_system", "email_queue", bindings)
|
|
979
1049
|
end
|