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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/COMMITS.md +196 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/Rakefile +8 -0
  8. data/examples/README.md +531 -0
  9. data/examples/advanced_example.rb +270 -0
  10. data/examples/ai_enhanced_kbs.rb +523 -0
  11. data/examples/blackboard_demo.rb +50 -0
  12. data/examples/car_diagnostic.rb +64 -0
  13. data/examples/concurrent_inference_demo.rb +363 -0
  14. data/examples/csv_trading_system.rb +559 -0
  15. data/examples/iot_demo_using_dsl.rb +83 -0
  16. data/examples/portfolio_rebalancing_system.rb +651 -0
  17. data/examples/redis_trading_demo.rb +177 -0
  18. data/examples/sample_stock_data.csv +46 -0
  19. data/examples/stock_trading_advanced.rb +469 -0
  20. data/examples/stock_trading_system.rb.bak +563 -0
  21. data/examples/timestamped_trading.rb +286 -0
  22. data/examples/trading_demo.rb +334 -0
  23. data/examples/working_demo.rb +176 -0
  24. data/lib/kbs/alpha_memory.rb +37 -0
  25. data/lib/kbs/beta_memory.rb +57 -0
  26. data/lib/kbs/blackboard/audit_log.rb +115 -0
  27. data/lib/kbs/blackboard/engine.rb +83 -0
  28. data/lib/kbs/blackboard/fact.rb +65 -0
  29. data/lib/kbs/blackboard/memory.rb +191 -0
  30. data/lib/kbs/blackboard/message_queue.rb +96 -0
  31. data/lib/kbs/blackboard/persistence/hybrid_store.rb +118 -0
  32. data/lib/kbs/blackboard/persistence/redis_store.rb +218 -0
  33. data/lib/kbs/blackboard/persistence/sqlite_store.rb +242 -0
  34. data/lib/kbs/blackboard/persistence/store.rb +55 -0
  35. data/lib/kbs/blackboard/redis_audit_log.rb +107 -0
  36. data/lib/kbs/blackboard/redis_message_queue.rb +111 -0
  37. data/lib/kbs/blackboard.rb +23 -0
  38. data/lib/kbs/condition.rb +26 -0
  39. data/lib/kbs/dsl/condition_helpers.rb +57 -0
  40. data/lib/kbs/dsl/knowledge_base.rb +86 -0
  41. data/lib/kbs/dsl/pattern_evaluator.rb +69 -0
  42. data/lib/kbs/dsl/rule_builder.rb +115 -0
  43. data/lib/kbs/dsl/variable.rb +35 -0
  44. data/lib/kbs/dsl.rb +18 -0
  45. data/lib/kbs/fact.rb +43 -0
  46. data/lib/kbs/join_node.rb +117 -0
  47. data/lib/kbs/negation_node.rb +88 -0
  48. data/lib/kbs/production_node.rb +28 -0
  49. data/lib/kbs/rete_engine.rb +108 -0
  50. data/lib/kbs/rule.rb +46 -0
  51. data/lib/kbs/token.rb +37 -0
  52. data/lib/kbs/version.rb +5 -0
  53. data/lib/kbs/working_memory.rb +32 -0
  54. data/lib/kbs.rb +20 -0
  55. 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