kbs 0.0.1
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 +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/Rakefile +8 -0
- data/examples/README.md +531 -0
- data/examples/advanced_example.rb +270 -0
- data/examples/ai_enhanced_kbs.rb +523 -0
- data/examples/blackboard_demo.rb +50 -0
- data/examples/car_diagnostic.rb +64 -0
- data/examples/concurrent_inference_demo.rb +363 -0
- data/examples/csv_trading_system.rb +559 -0
- data/examples/iot_demo_using_dsl.rb +83 -0
- data/examples/portfolio_rebalancing_system.rb +651 -0
- data/examples/redis_trading_demo.rb +177 -0
- data/examples/sample_stock_data.csv +46 -0
- data/examples/stock_trading_advanced.rb +469 -0
- data/examples/stock_trading_system.rb.bak +563 -0
- data/examples/timestamped_trading.rb +286 -0
- data/examples/trading_demo.rb +334 -0
- data/examples/working_demo.rb +176 -0
- data/lib/kbs/alpha_memory.rb +37 -0
- data/lib/kbs/beta_memory.rb +57 -0
- data/lib/kbs/blackboard/audit_log.rb +115 -0
- data/lib/kbs/blackboard/engine.rb +83 -0
- data/lib/kbs/blackboard/fact.rb +65 -0
- data/lib/kbs/blackboard/memory.rb +191 -0
- data/lib/kbs/blackboard/message_queue.rb +96 -0
- data/lib/kbs/blackboard/persistence/hybrid_store.rb +118 -0
- data/lib/kbs/blackboard/persistence/redis_store.rb +218 -0
- data/lib/kbs/blackboard/persistence/sqlite_store.rb +242 -0
- data/lib/kbs/blackboard/persistence/store.rb +55 -0
- data/lib/kbs/blackboard/redis_audit_log.rb +107 -0
- data/lib/kbs/blackboard/redis_message_queue.rb +111 -0
- data/lib/kbs/blackboard.rb +23 -0
- data/lib/kbs/condition.rb +26 -0
- data/lib/kbs/dsl/condition_helpers.rb +57 -0
- data/lib/kbs/dsl/knowledge_base.rb +86 -0
- data/lib/kbs/dsl/pattern_evaluator.rb +69 -0
- data/lib/kbs/dsl/rule_builder.rb +115 -0
- data/lib/kbs/dsl/variable.rb +35 -0
- data/lib/kbs/dsl.rb +18 -0
- data/lib/kbs/fact.rb +43 -0
- data/lib/kbs/join_node.rb +117 -0
- data/lib/kbs/negation_node.rb +88 -0
- data/lib/kbs/production_node.rb +28 -0
- data/lib/kbs/rete_engine.rb +108 -0
- data/lib/kbs/rule.rb +46 -0
- data/lib/kbs/token.rb +37 -0
- data/lib/kbs/version.rb +5 -0
- data/lib/kbs/working_memory.rb +32 -0
- data/lib/kbs.rb +20 -0
- metadata +164 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/kbs'
|
|
4
|
+
|
|
5
|
+
class WorkingTradingDemo
|
|
6
|
+
def initialize
|
|
7
|
+
@engine = KBS::ReteEngine.new
|
|
8
|
+
setup_simple_rules
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def setup_simple_rules
|
|
12
|
+
# Rule 1: Simple stock momentum
|
|
13
|
+
momentum_rule = KBS::Rule.new(
|
|
14
|
+
"momentum_buy",
|
|
15
|
+
conditions: [
|
|
16
|
+
KBS::Condition.new(:stock, { symbol: "AAPL" })
|
|
17
|
+
],
|
|
18
|
+
action: lambda do |facts, bindings|
|
|
19
|
+
stock = facts.find { |f| f.type == :stock }
|
|
20
|
+
puts "🚀 MOMENTUM SIGNAL: #{stock[:symbol]}"
|
|
21
|
+
puts " Price: $#{stock[:price]}"
|
|
22
|
+
puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
23
|
+
puts " Recommendation: BUY"
|
|
24
|
+
end
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Rule 2: High volume alert
|
|
28
|
+
volume_rule = KBS::Rule.new(
|
|
29
|
+
"high_volume",
|
|
30
|
+
conditions: [
|
|
31
|
+
KBS::Condition.new(:stock, { volume: ->(v) { v && v > 1000000 } })
|
|
32
|
+
],
|
|
33
|
+
action: lambda do |facts, bindings|
|
|
34
|
+
stock = facts.find { |f| f.type == :stock }
|
|
35
|
+
puts "📊 HIGH VOLUME ALERT: #{stock[:symbol]}"
|
|
36
|
+
puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
37
|
+
puts " Above 1M shares traded"
|
|
38
|
+
end
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Rule 3: Price movement
|
|
42
|
+
price_rule = KBS::Rule.new(
|
|
43
|
+
"price_movement",
|
|
44
|
+
conditions: [
|
|
45
|
+
KBS::Condition.new(:stock, { price_change: ->(p) { p && p.abs > 2 } })
|
|
46
|
+
],
|
|
47
|
+
action: lambda do |facts, bindings|
|
|
48
|
+
stock = facts.find { |f| f.type == :stock }
|
|
49
|
+
direction = stock[:price_change] > 0 ? "UP" : "DOWN"
|
|
50
|
+
puts "📈 SIGNIFICANT MOVE: #{stock[:symbol]} #{direction}"
|
|
51
|
+
puts " Change: #{stock[:price_change] > 0 ? '+' : ''}#{stock[:price_change]}%"
|
|
52
|
+
end
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Rule 4: RSI signals
|
|
56
|
+
rsi_rule = KBS::Rule.new(
|
|
57
|
+
"rsi_signal",
|
|
58
|
+
conditions: [
|
|
59
|
+
KBS::Condition.new(:stock, { rsi: ->(r) { r && (r < 30 || r > 70) } })
|
|
60
|
+
],
|
|
61
|
+
action: lambda do |facts, bindings|
|
|
62
|
+
stock = facts.find { |f| f.type == :stock }
|
|
63
|
+
condition = stock[:rsi] < 30 ? "OVERSOLD" : "OVERBOUGHT"
|
|
64
|
+
action = stock[:rsi] < 30 ? "BUY" : "SELL"
|
|
65
|
+
puts "⚡ RSI SIGNAL: #{stock[:symbol]} #{condition}"
|
|
66
|
+
puts " RSI: #{stock[:rsi].round(1)}"
|
|
67
|
+
puts " Recommendation: #{action}"
|
|
68
|
+
end
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Rule 5: Multi-condition golden cross
|
|
72
|
+
golden_cross_rule = KBS::Rule.new(
|
|
73
|
+
"golden_cross_complete",
|
|
74
|
+
conditions: [
|
|
75
|
+
KBS::Condition.new(:stock, { symbol: "AAPL" }),
|
|
76
|
+
KBS::Condition.new(:ma_signal, { type: "golden_cross" })
|
|
77
|
+
],
|
|
78
|
+
action: lambda do |facts, bindings|
|
|
79
|
+
stock = facts.find { |f| f.type == :stock }
|
|
80
|
+
signal = facts.find { |f| f.type == :ma_signal }
|
|
81
|
+
puts "🌟 GOLDEN CROSS CONFIRMED: #{stock[:symbol]}"
|
|
82
|
+
puts " 50-day MA crossed above 200-day MA"
|
|
83
|
+
puts " Price: $#{stock[:price]}"
|
|
84
|
+
puts " Recommendation: STRONG BUY"
|
|
85
|
+
end
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@engine.add_rule(momentum_rule)
|
|
89
|
+
@engine.add_rule(volume_rule)
|
|
90
|
+
@engine.add_rule(price_rule)
|
|
91
|
+
@engine.add_rule(rsi_rule)
|
|
92
|
+
@engine.add_rule(golden_cross_rule)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_scenarios
|
|
96
|
+
puts "🏦 STOCK TRADING EXPERT SYSTEM"
|
|
97
|
+
puts "=" * 50
|
|
98
|
+
|
|
99
|
+
# Scenario 1: Apple momentum
|
|
100
|
+
puts "\n📊 SCENARIO 1: Apple with High Volume"
|
|
101
|
+
puts "-" * 30
|
|
102
|
+
@engine.working_memory.facts.clear
|
|
103
|
+
@engine.add_fact(:stock, {
|
|
104
|
+
symbol: "AAPL",
|
|
105
|
+
price: 185.50,
|
|
106
|
+
volume: 1_500_000,
|
|
107
|
+
price_change: 3.2,
|
|
108
|
+
rsi: 68
|
|
109
|
+
})
|
|
110
|
+
@engine.run
|
|
111
|
+
|
|
112
|
+
# Scenario 2: Google big move
|
|
113
|
+
puts "\n📊 SCENARIO 2: Google Big Price Move"
|
|
114
|
+
puts "-" * 30
|
|
115
|
+
@engine.working_memory.facts.clear
|
|
116
|
+
@engine.add_fact(:stock, {
|
|
117
|
+
symbol: "GOOGL",
|
|
118
|
+
price: 142.80,
|
|
119
|
+
volume: 800_000,
|
|
120
|
+
price_change: -4.1,
|
|
121
|
+
rsi: 75
|
|
122
|
+
})
|
|
123
|
+
@engine.run
|
|
124
|
+
|
|
125
|
+
# Scenario 3: Tesla oversold
|
|
126
|
+
puts "\n📊 SCENARIO 3: Tesla Oversold"
|
|
127
|
+
puts "-" * 30
|
|
128
|
+
@engine.working_memory.facts.clear
|
|
129
|
+
@engine.add_fact(:stock, {
|
|
130
|
+
symbol: "TSLA",
|
|
131
|
+
price: 195.40,
|
|
132
|
+
volume: 2_200_000,
|
|
133
|
+
price_change: -1.8,
|
|
134
|
+
rsi: 25
|
|
135
|
+
})
|
|
136
|
+
@engine.run
|
|
137
|
+
|
|
138
|
+
# Scenario 4: Apple Golden Cross
|
|
139
|
+
puts "\n📊 SCENARIO 4: Apple Golden Cross"
|
|
140
|
+
puts "-" * 30
|
|
141
|
+
@engine.working_memory.facts.clear
|
|
142
|
+
@engine.add_fact(:stock, {
|
|
143
|
+
symbol: "AAPL",
|
|
144
|
+
price: 190.25,
|
|
145
|
+
volume: 1_100_000,
|
|
146
|
+
price_change: 2.1,
|
|
147
|
+
rsi: 55
|
|
148
|
+
})
|
|
149
|
+
@engine.add_fact(:ma_signal, {
|
|
150
|
+
symbol: "AAPL",
|
|
151
|
+
type: "golden_cross"
|
|
152
|
+
})
|
|
153
|
+
@engine.run
|
|
154
|
+
|
|
155
|
+
# Scenario 5: Multiple signals
|
|
156
|
+
puts "\n📊 SCENARIO 5: NVIDIA Multiple Signals"
|
|
157
|
+
puts "-" * 30
|
|
158
|
+
@engine.working_memory.facts.clear
|
|
159
|
+
@engine.add_fact(:stock, {
|
|
160
|
+
symbol: "NVDA",
|
|
161
|
+
price: 425.80,
|
|
162
|
+
volume: 3_500_000,
|
|
163
|
+
price_change: 8.7,
|
|
164
|
+
rsi: 78
|
|
165
|
+
})
|
|
166
|
+
@engine.run
|
|
167
|
+
|
|
168
|
+
puts "\n" + "=" * 50
|
|
169
|
+
puts "DEMONSTRATION COMPLETE"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if __FILE__ == $0
|
|
174
|
+
demo = WorkingTradingDemo.new
|
|
175
|
+
demo.run_scenarios
|
|
176
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KBS
|
|
4
|
+
class AlphaMemory
|
|
5
|
+
attr_accessor :items, :successors, :pattern
|
|
6
|
+
attr_reader :linked
|
|
7
|
+
|
|
8
|
+
def initialize(pattern = {})
|
|
9
|
+
@items = []
|
|
10
|
+
@successors = []
|
|
11
|
+
@pattern = pattern
|
|
12
|
+
@linked = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def unlink!
|
|
16
|
+
@linked = false
|
|
17
|
+
@successors.each { |s| s.right_unlink! if s.respond_to?(:right_unlink!) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def relink!
|
|
21
|
+
@linked = true
|
|
22
|
+
@successors.each { |s| s.right_relink! if s.respond_to?(:right_relink!) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def activate(fact)
|
|
26
|
+
return unless @linked
|
|
27
|
+
@items << fact
|
|
28
|
+
@successors.each { |s| s.right_activate(fact) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deactivate(fact)
|
|
32
|
+
return unless @linked
|
|
33
|
+
@items.delete(fact)
|
|
34
|
+
@successors.each { |s| s.right_deactivate(fact) if s.respond_to?(:right_deactivate) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KBS
|
|
4
|
+
class BetaMemory
|
|
5
|
+
attr_accessor :tokens, :successors
|
|
6
|
+
attr_reader :linked
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@tokens = []
|
|
10
|
+
@successors = []
|
|
11
|
+
@linked = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def unlink!
|
|
15
|
+
@linked = false
|
|
16
|
+
@successors.each { |s| s.left_unlink! if s.respond_to?(:left_unlink!) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def relink!
|
|
20
|
+
@linked = true
|
|
21
|
+
@successors.each { |s| s.left_relink! if s.respond_to?(:left_relink!) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def activate(token)
|
|
25
|
+
add_token(token)
|
|
26
|
+
@successors.each do |s|
|
|
27
|
+
if s.respond_to?(:left_activate)
|
|
28
|
+
s.left_activate(token)
|
|
29
|
+
elsif s.respond_to?(:activate)
|
|
30
|
+
s.activate(token)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def deactivate(token)
|
|
36
|
+
remove_token(token)
|
|
37
|
+
@successors.each do |s|
|
|
38
|
+
if s.respond_to?(:left_deactivate)
|
|
39
|
+
s.left_deactivate(token)
|
|
40
|
+
elsif s.respond_to?(:deactivate)
|
|
41
|
+
s.deactivate(token)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_token(token)
|
|
47
|
+
@tokens << token
|
|
48
|
+
unlink! if @tokens.empty?
|
|
49
|
+
relink! if @tokens.size == 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def remove_token(token)
|
|
53
|
+
@tokens.delete(token)
|
|
54
|
+
unlink! if @tokens.empty?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module KBS
|
|
7
|
+
module Blackboard
|
|
8
|
+
class AuditLog
|
|
9
|
+
def initialize(db, session_id)
|
|
10
|
+
@db = db
|
|
11
|
+
@session_id = session_id
|
|
12
|
+
setup_tables
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def setup_tables
|
|
16
|
+
@db.execute_batch <<-SQL
|
|
17
|
+
CREATE TABLE IF NOT EXISTS fact_history (
|
|
18
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
|
+
fact_uuid TEXT NOT NULL,
|
|
20
|
+
fact_type TEXT NOT NULL,
|
|
21
|
+
attributes TEXT NOT NULL,
|
|
22
|
+
action TEXT NOT NULL,
|
|
23
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
session_id TEXT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS rules_fired (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
rule_name TEXT NOT NULL,
|
|
30
|
+
fact_uuids TEXT NOT NULL,
|
|
31
|
+
bindings TEXT,
|
|
32
|
+
fired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
33
|
+
session_id TEXT
|
|
34
|
+
);
|
|
35
|
+
SQL
|
|
36
|
+
|
|
37
|
+
@db.execute_batch <<-SQL
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_fact_history_uuid ON fact_history(fact_uuid);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_rules_fired_session ON rules_fired(session_id);
|
|
40
|
+
SQL
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def log_fact_change(fact_uuid, fact_type, attributes, action)
|
|
44
|
+
attributes_json = attributes.is_a?(String) ? attributes : JSON.generate(attributes)
|
|
45
|
+
|
|
46
|
+
@db.execute(
|
|
47
|
+
"INSERT INTO fact_history (fact_uuid, fact_type, attributes, action, session_id) VALUES (?, ?, ?, ?, ?)",
|
|
48
|
+
[fact_uuid, fact_type.to_s, attributes_json, action, @session_id]
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def log_rule_firing(rule_name, fact_uuids, bindings = {})
|
|
53
|
+
@db.execute(
|
|
54
|
+
"INSERT INTO rules_fired (rule_name, fact_uuids, bindings, session_id) VALUES (?, ?, ?, ?)",
|
|
55
|
+
[rule_name, JSON.generate(fact_uuids), JSON.generate(bindings), @session_id]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fact_history(fact_uuid = nil, limit: 100)
|
|
60
|
+
if fact_uuid
|
|
61
|
+
results = @db.execute(
|
|
62
|
+
"SELECT * FROM fact_history WHERE fact_uuid = ? ORDER BY timestamp DESC, id DESC LIMIT ?",
|
|
63
|
+
[fact_uuid, limit]
|
|
64
|
+
)
|
|
65
|
+
else
|
|
66
|
+
results = @db.execute(
|
|
67
|
+
"SELECT * FROM fact_history ORDER BY timestamp DESC, id DESC LIMIT ?",
|
|
68
|
+
[limit]
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
results.map do |row|
|
|
73
|
+
{
|
|
74
|
+
fact_uuid: row['fact_uuid'],
|
|
75
|
+
fact_type: row['fact_type'].to_sym,
|
|
76
|
+
attributes: JSON.parse(row['attributes'], symbolize_names: true),
|
|
77
|
+
action: row['action'],
|
|
78
|
+
timestamp: Time.parse(row['timestamp']),
|
|
79
|
+
session_id: row['session_id']
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rule_firings(rule_name = nil, limit: 100)
|
|
85
|
+
if rule_name
|
|
86
|
+
results = @db.execute(
|
|
87
|
+
"SELECT * FROM rules_fired WHERE rule_name = ? ORDER BY fired_at DESC LIMIT ?",
|
|
88
|
+
[rule_name, limit]
|
|
89
|
+
)
|
|
90
|
+
else
|
|
91
|
+
results = @db.execute(
|
|
92
|
+
"SELECT * FROM rules_fired ORDER BY fired_at DESC LIMIT ?",
|
|
93
|
+
[limit]
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
results.map do |row|
|
|
98
|
+
{
|
|
99
|
+
rule_name: row['rule_name'],
|
|
100
|
+
fact_uuids: JSON.parse(row['fact_uuids']),
|
|
101
|
+
bindings: row['bindings'] ? JSON.parse(row['bindings'], symbolize_names: true) : {},
|
|
102
|
+
fired_at: Time.parse(row['fired_at']),
|
|
103
|
+
session_id: row['session_id']
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stats
|
|
109
|
+
{
|
|
110
|
+
rules_fired: @db.get_first_value("SELECT COUNT(*) FROM rules_fired")
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../rete_engine'
|
|
4
|
+
require_relative 'memory'
|
|
5
|
+
|
|
6
|
+
module KBS
|
|
7
|
+
module Blackboard
|
|
8
|
+
# RETE engine integrated with Blackboard memory
|
|
9
|
+
class Engine < ReteEngine
|
|
10
|
+
attr_reader :blackboard
|
|
11
|
+
|
|
12
|
+
def initialize(db_path: ':memory:', store: nil)
|
|
13
|
+
super()
|
|
14
|
+
@blackboard = Memory.new(db_path: db_path, store: store)
|
|
15
|
+
@working_memory = @blackboard
|
|
16
|
+
@blackboard.add_observer(self)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_fact(type, attributes = {})
|
|
20
|
+
@blackboard.add_fact(type, attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def remove_fact(fact)
|
|
24
|
+
@blackboard.remove_fact(fact)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(action, fact)
|
|
28
|
+
if action == :add
|
|
29
|
+
@alpha_memories.each do |pattern, memory|
|
|
30
|
+
memory.activate(fact) if fact.matches?(pattern)
|
|
31
|
+
end
|
|
32
|
+
elsif action == :remove
|
|
33
|
+
@alpha_memories.each do |pattern, memory|
|
|
34
|
+
memory.deactivate(fact) if fact.matches?(pattern)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run
|
|
40
|
+
@production_nodes.values.each do |node|
|
|
41
|
+
node.tokens.each do |token|
|
|
42
|
+
# Only fire if not already fired
|
|
43
|
+
next if token.fired?
|
|
44
|
+
|
|
45
|
+
fact_uuids = token.facts.map { |f| f.respond_to?(:uuid) ? f.uuid : f.object_id.to_s }
|
|
46
|
+
bindings = extract_bindings_from_token(token, node.rule)
|
|
47
|
+
|
|
48
|
+
@blackboard.log_rule_firing(node.rule.name, fact_uuids, bindings)
|
|
49
|
+
node.fire_rule(token)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post_message(sender, topic, content, priority: 0)
|
|
55
|
+
@blackboard.post_message(sender, topic, content, priority: priority)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def consume_message(topic, consumer)
|
|
59
|
+
@blackboard.consume_message(topic, consumer)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stats
|
|
63
|
+
@blackboard.stats
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def extract_bindings_from_token(token, rule)
|
|
69
|
+
bindings = {}
|
|
70
|
+
rule.conditions.each_with_index do |condition, index|
|
|
71
|
+
next if condition.negated
|
|
72
|
+
fact = token.facts[index]
|
|
73
|
+
if fact && condition.respond_to?(:variable_bindings)
|
|
74
|
+
condition.variable_bindings.each do |var, field|
|
|
75
|
+
bindings[var] = fact.attributes[field] if fact.respond_to?(:attributes)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
bindings
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KBS
|
|
4
|
+
module Blackboard
|
|
5
|
+
# A fact in the blackboard with persistence capabilities
|
|
6
|
+
class Fact
|
|
7
|
+
attr_reader :uuid, :type, :attributes
|
|
8
|
+
|
|
9
|
+
def initialize(uuid, type, attributes, blackboard = nil)
|
|
10
|
+
@uuid = uuid
|
|
11
|
+
@type = type
|
|
12
|
+
@attributes = attributes
|
|
13
|
+
@blackboard = blackboard
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](key)
|
|
17
|
+
@attributes[key]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def []=(key, value)
|
|
21
|
+
@attributes[key] = value
|
|
22
|
+
@blackboard.update_fact(self, @attributes) if @blackboard
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update(new_attributes)
|
|
26
|
+
@attributes.merge!(new_attributes)
|
|
27
|
+
@blackboard.update_fact(self, @attributes) if @blackboard
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def retract
|
|
31
|
+
@blackboard.remove_fact(self) if @blackboard
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def matches?(pattern)
|
|
35
|
+
return false if pattern[:type] && pattern[:type] != @type
|
|
36
|
+
|
|
37
|
+
pattern.each do |key, value|
|
|
38
|
+
next if key == :type
|
|
39
|
+
|
|
40
|
+
if value.is_a?(Proc)
|
|
41
|
+
return false unless @attributes[key] && value.call(@attributes[key])
|
|
42
|
+
elsif value.is_a?(Symbol) && value.to_s.start_with?('?')
|
|
43
|
+
next
|
|
44
|
+
else
|
|
45
|
+
return false unless @attributes[key] == value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
"#{@type}(#{@uuid[0..7]}...: #{@attributes.map { |k, v| "#{k}=#{v}" }.join(', ')})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
uuid: @uuid,
|
|
59
|
+
type: @type,
|
|
60
|
+
attributes: @attributes
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|