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,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'persistence/store'
5
+ require_relative 'persistence/sqlite_store'
6
+ require_relative 'message_queue'
7
+ require_relative 'audit_log'
8
+ require_relative 'fact'
9
+
10
+ module KBS
11
+ module Blackboard
12
+ # The Blackboard Memory - central workspace for facts and coordination
13
+ class Memory
14
+ attr_reader :session_id, :store, :message_queue, :audit_log
15
+
16
+ def initialize(db_path: ':memory:', store: nil)
17
+ @session_id = SecureRandom.uuid
18
+ @observers = []
19
+
20
+ # Use provided store or create default SqliteStore
21
+ @store = store || Persistence::SqliteStore.new(
22
+ db_path: db_path,
23
+ session_id: @session_id
24
+ )
25
+
26
+ # Initialize composed components based on store type
27
+ setup_components
28
+ end
29
+
30
+ private
31
+
32
+ def setup_components
33
+ # Detect store type and create appropriate MessageQueue and AuditLog
34
+ if @store.respond_to?(:hybrid?) && @store.hybrid?
35
+ # Hybrid store: Redis for messages, SQLite for audit
36
+ require_relative 'redis_message_queue'
37
+ @message_queue = RedisMessageQueue.new(@store.connection)
38
+ @audit_log = AuditLog.new(@store.db, @session_id)
39
+ elsif @store.respond_to?(:connection)
40
+ # Pure Redis store
41
+ require_relative 'redis_message_queue'
42
+ require_relative 'redis_audit_log'
43
+ @message_queue = RedisMessageQueue.new(@store.connection)
44
+ @audit_log = RedisAuditLog.new(@store.connection, @session_id)
45
+ elsif @store.respond_to?(:db)
46
+ # Pure SQLite store
47
+ @message_queue = MessageQueue.new(@store.db)
48
+ @audit_log = AuditLog.new(@store.db, @session_id)
49
+ else
50
+ raise ArgumentError, "Store must respond to either :connection (Redis) or :db (SQLite)"
51
+ end
52
+ end
53
+
54
+ public
55
+
56
+ # Fact Management
57
+ def add_fact(type, attributes = {})
58
+ uuid = SecureRandom.uuid
59
+
60
+ @store.transaction do
61
+ @store.add_fact(uuid, type, attributes)
62
+ @audit_log.log_fact_change(uuid, type, attributes, 'ADD')
63
+ end
64
+
65
+ fact = Fact.new(uuid, type, attributes, self)
66
+ notify_observers(:add, fact)
67
+ fact
68
+ end
69
+
70
+ def remove_fact(fact)
71
+ uuid = fact.is_a?(Fact) ? fact.uuid : fact
72
+
73
+ @store.transaction do
74
+ result = @store.remove_fact(uuid)
75
+
76
+ if result
77
+ @audit_log.log_fact_change(uuid, result[:type], result[:attributes], 'REMOVE')
78
+
79
+ fact_obj = Fact.new(uuid, result[:type], result[:attributes], self)
80
+ notify_observers(:remove, fact_obj)
81
+ end
82
+ end
83
+ end
84
+
85
+ def update_fact(fact, new_attributes)
86
+ uuid = fact.is_a?(Fact) ? fact.uuid : fact
87
+
88
+ @store.transaction do
89
+ fact_type = @store.update_fact(uuid, new_attributes)
90
+
91
+ if fact_type
92
+ @audit_log.log_fact_change(uuid, fact_type, new_attributes, 'UPDATE')
93
+ end
94
+ end
95
+ end
96
+
97
+ def get_facts(type = nil, pattern = {})
98
+ fact_data = @store.get_facts(type, pattern)
99
+
100
+ fact_data.map do |data|
101
+ Fact.new(data[:uuid], data[:type], data[:attributes], self)
102
+ end
103
+ end
104
+
105
+ # Alias for compatibility with WorkingMemory interface
106
+ def facts
107
+ get_facts
108
+ end
109
+
110
+ def query_facts(sql_conditions = nil, params = [])
111
+ fact_data = @store.query_facts(sql_conditions, params)
112
+
113
+ fact_data.map do |data|
114
+ Fact.new(data[:uuid], data[:type], data[:attributes], self)
115
+ end
116
+ end
117
+
118
+ # Message Queue delegation
119
+ def post_message(sender, topic, content, priority: 0)
120
+ @message_queue.post(sender, topic, content, priority: priority)
121
+ end
122
+
123
+ def consume_message(topic, consumer)
124
+ @store.transaction do
125
+ @message_queue.consume(topic, consumer)
126
+ end
127
+ end
128
+
129
+ def peek_messages(topic, limit: 10)
130
+ @message_queue.peek(topic, limit: limit)
131
+ end
132
+
133
+ # Knowledge Source registry
134
+ def register_knowledge_source(name, description: nil, topics: [])
135
+ @store.register_knowledge_source(name, description: description, topics: topics)
136
+ end
137
+
138
+ # Audit Log delegation
139
+ def log_rule_firing(rule_name, fact_uuids, bindings = {})
140
+ @audit_log.log_rule_firing(rule_name, fact_uuids, bindings)
141
+ end
142
+
143
+ def get_history(fact_uuid = nil, limit: 100)
144
+ @audit_log.fact_history(fact_uuid, limit: limit)
145
+ end
146
+
147
+ def get_rule_firings(rule_name = nil, limit: 100)
148
+ @audit_log.rule_firings(rule_name, limit: limit)
149
+ end
150
+
151
+ # Observer pattern
152
+ def add_observer(observer)
153
+ @observers << observer
154
+ end
155
+
156
+ def notify_observers(action, fact)
157
+ @observers.each { |obs| obs.update(action, fact) }
158
+ end
159
+
160
+ # Session management
161
+ def clear_session
162
+ @store.clear_session(@session_id)
163
+ end
164
+
165
+ def transaction(&block)
166
+ @store.transaction(&block)
167
+ end
168
+
169
+ # Statistics
170
+ def stats
171
+ store_stats = @store.stats
172
+ message_stats = @message_queue.stats
173
+ audit_stats = @audit_log.stats
174
+
175
+ store_stats.merge(message_stats).merge(audit_stats)
176
+ end
177
+
178
+ # Maintenance
179
+ def vacuum
180
+ @store.vacuum
181
+ end
182
+
183
+ def close
184
+ @store.close
185
+ end
186
+
187
+ # For backward compatibility with ReteEngine
188
+ alias_method :db, :store
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module KBS
7
+ module Blackboard
8
+ class MessageQueue
9
+ def initialize(db)
10
+ @db = db
11
+ setup_table
12
+ end
13
+
14
+ def setup_table
15
+ @db.execute_batch <<-SQL
16
+ CREATE TABLE IF NOT EXISTS blackboard_messages (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ sender TEXT NOT NULL,
19
+ topic TEXT NOT NULL,
20
+ content TEXT NOT NULL,
21
+ priority INTEGER DEFAULT 0,
22
+ posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
+ consumed BOOLEAN DEFAULT 0,
24
+ consumed_by TEXT,
25
+ consumed_at TIMESTAMP
26
+ );
27
+ SQL
28
+
29
+ @db.execute <<-SQL
30
+ CREATE INDEX IF NOT EXISTS idx_messages_topic ON blackboard_messages(topic);
31
+ SQL
32
+
33
+ @db.execute <<-SQL
34
+ CREATE INDEX IF NOT EXISTS idx_messages_consumed ON blackboard_messages(consumed);
35
+ SQL
36
+ end
37
+
38
+ def post(sender, topic, content, priority: 0)
39
+ content_json = content.is_a?(String) ? content : JSON.generate(content)
40
+
41
+ @db.execute(
42
+ "INSERT INTO blackboard_messages (sender, topic, content, priority) VALUES (?, ?, ?, ?)",
43
+ [sender, topic, content_json, priority]
44
+ )
45
+ end
46
+
47
+ def consume(topic, consumer)
48
+ result = @db.get_first_row(
49
+ "SELECT * FROM blackboard_messages WHERE topic = ? AND consumed = 0 ORDER BY priority DESC, posted_at ASC LIMIT 1",
50
+ [topic]
51
+ )
52
+
53
+ if result
54
+ @db.execute(
55
+ "UPDATE blackboard_messages SET consumed = 1, consumed_by = ?, consumed_at = CURRENT_TIMESTAMP WHERE id = ?",
56
+ [consumer, result['id']]
57
+ )
58
+
59
+ {
60
+ id: result['id'],
61
+ sender: result['sender'],
62
+ topic: result['topic'],
63
+ content: JSON.parse(result['content'], symbolize_names: true),
64
+ priority: result['priority'],
65
+ posted_at: Time.parse(result['posted_at'])
66
+ }
67
+ end
68
+ end
69
+
70
+ def peek(topic, limit: 10)
71
+ results = @db.execute(
72
+ "SELECT * FROM blackboard_messages WHERE topic = ? AND consumed = 0 ORDER BY priority DESC, posted_at ASC LIMIT ?",
73
+ [topic, limit]
74
+ )
75
+
76
+ results.map do |row|
77
+ {
78
+ id: row['id'],
79
+ sender: row['sender'],
80
+ topic: row['topic'],
81
+ content: JSON.parse(row['content'], symbolize_names: true),
82
+ priority: row['priority'],
83
+ posted_at: Time.parse(row['posted_at'])
84
+ }
85
+ end
86
+ end
87
+
88
+ def stats
89
+ {
90
+ total_messages: @db.get_first_value("SELECT COUNT(*) FROM blackboard_messages"),
91
+ unconsumed_messages: @db.get_first_value("SELECT COUNT(*) FROM blackboard_messages WHERE consumed = 0")
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'store'
4
+ require_relative 'redis_store'
5
+ require_relative 'sqlite_store'
6
+
7
+ module KBS
8
+ module Blackboard
9
+ module Persistence
10
+ # Hybrid store combining Redis (facts, messages) with SQLite (audit trail)
11
+ #
12
+ # Benefits:
13
+ # - Fast in-memory fact access via Redis
14
+ # - Durable audit trail via SQLite
15
+ # - Best of both worlds for production systems
16
+ class HybridStore < Store
17
+ attr_reader :redis_store, :sqlite_store, :session_id
18
+
19
+ def initialize(
20
+ redis_url: 'redis://localhost:6379/0',
21
+ redis: nil,
22
+ db_path: 'audit.db',
23
+ session_id: nil
24
+ )
25
+ @session_id = session_id
26
+
27
+ # Redis for hot data (facts, messages)
28
+ @redis_store = RedisStore.new(
29
+ url: redis_url,
30
+ redis: redis,
31
+ session_id: @session_id
32
+ )
33
+
34
+ # SQLite for cold data (audit trail)
35
+ @sqlite_store = SqliteStore.new(
36
+ db_path: db_path,
37
+ session_id: @session_id
38
+ )
39
+ end
40
+
41
+ # Fact operations delegated to Redis (fast)
42
+ def add_fact(uuid, type, attributes)
43
+ @redis_store.add_fact(uuid, type, attributes)
44
+ end
45
+
46
+ def remove_fact(uuid)
47
+ @redis_store.remove_fact(uuid)
48
+ end
49
+
50
+ def update_fact(uuid, attributes)
51
+ @redis_store.update_fact(uuid, attributes)
52
+ end
53
+
54
+ def get_fact(uuid)
55
+ @redis_store.get_fact(uuid)
56
+ end
57
+
58
+ def get_facts(type = nil, pattern = {})
59
+ @redis_store.get_facts(type, pattern)
60
+ end
61
+
62
+ def query_facts(conditions = nil, params = [])
63
+ @redis_store.query_facts(conditions, params)
64
+ end
65
+
66
+ def register_knowledge_source(name, description: nil, topics: [])
67
+ @redis_store.register_knowledge_source(name, description: description, topics: topics)
68
+ end
69
+
70
+ def clear_session(session_id)
71
+ @redis_store.clear_session(session_id)
72
+ end
73
+
74
+ # Stats combined from both stores
75
+ def stats
76
+ redis_stats = @redis_store.stats
77
+ sqlite_stats = @sqlite_store.stats
78
+
79
+ # Prefer Redis for fact counts (authoritative)
80
+ redis_stats.merge(
81
+ audit_records: sqlite_stats[:total_facts] # SQLite tracks audit records
82
+ )
83
+ end
84
+
85
+ def vacuum
86
+ @redis_store.vacuum
87
+ @sqlite_store.vacuum
88
+ end
89
+
90
+ def transaction(&block)
91
+ # Redis and SQLite transactions are separate
92
+ # Execute block in context of both
93
+ yield
94
+ end
95
+
96
+ def close
97
+ @redis_store.close
98
+ @sqlite_store.close
99
+ end
100
+
101
+ # Provide access to both connections for MessageQueue/AuditLog
102
+ # Memory class will detect hybrid store and use appropriate components
103
+ def connection
104
+ @redis_store.connection
105
+ end
106
+
107
+ def db
108
+ @sqlite_store.db
109
+ end
110
+
111
+ # Helper to check if this is a hybrid store
112
+ def hybrid?
113
+ true
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require 'json'
5
+ require 'securerandom'
6
+ require_relative 'store'
7
+
8
+ module KBS
9
+ module Blackboard
10
+ module Persistence
11
+ class RedisStore < Store
12
+ attr_reader :redis, :session_id
13
+
14
+ def initialize(url: 'redis://localhost:6379/0', session_id: nil, redis: nil)
15
+ @session_id = session_id || SecureRandom.uuid
16
+ @redis = redis || Redis.new(url: url)
17
+ @transaction_depth = 0
18
+ end
19
+
20
+ def add_fact(uuid, type, attributes)
21
+ attributes_json = JSON.generate(attributes)
22
+ timestamp = Time.now.to_f
23
+
24
+ @redis.multi do |pipeline|
25
+ pipeline.hset("fact:#{uuid}", {
26
+ 'uuid' => uuid,
27
+ 'type' => type.to_s,
28
+ 'attributes' => attributes_json,
29
+ 'session_id' => @session_id,
30
+ 'created_at' => timestamp,
31
+ 'updated_at' => timestamp,
32
+ 'retracted' => '0'
33
+ })
34
+
35
+ # Indexes
36
+ pipeline.sadd('facts:active', uuid)
37
+ pipeline.sadd("facts:type:#{type}", uuid)
38
+ pipeline.sadd("facts:session:#{@session_id}", uuid) if @session_id
39
+ pipeline.sadd('facts:all', uuid)
40
+ end
41
+ end
42
+
43
+ def remove_fact(uuid)
44
+ fact_data = @redis.hgetall("fact:#{uuid}")
45
+ return nil if fact_data.empty? || fact_data['retracted'] == '1'
46
+
47
+ type = fact_data['type'].to_sym
48
+ attributes = JSON.parse(fact_data['attributes'], symbolize_names: true)
49
+
50
+ @redis.multi do |pipeline|
51
+ pipeline.hset("fact:#{uuid}", 'retracted', '1')
52
+ pipeline.hset("fact:#{uuid}", 'retracted_at', Time.now.to_f)
53
+ pipeline.srem('facts:active', uuid)
54
+ pipeline.srem("facts:type:#{type}", uuid)
55
+ end
56
+
57
+ { type: type, attributes: attributes }
58
+ end
59
+
60
+ def update_fact(uuid, attributes)
61
+ fact_data = @redis.hgetall("fact:#{uuid}")
62
+ return nil if fact_data.empty? || fact_data['retracted'] == '1'
63
+
64
+ attributes_json = JSON.generate(attributes)
65
+
66
+ @redis.multi do |pipeline|
67
+ pipeline.hset("fact:#{uuid}", 'attributes', attributes_json)
68
+ pipeline.hset("fact:#{uuid}", 'updated_at', Time.now.to_f)
69
+ end
70
+
71
+ fact_data['type'].to_sym
72
+ end
73
+
74
+ def get_fact(uuid)
75
+ fact_data = @redis.hgetall("fact:#{uuid}")
76
+ return nil if fact_data.empty? || fact_data['retracted'] == '1'
77
+
78
+ {
79
+ uuid: fact_data['uuid'],
80
+ type: fact_data['type'].to_sym,
81
+ attributes: JSON.parse(fact_data['attributes'], symbolize_names: true)
82
+ }
83
+ end
84
+
85
+ def get_facts(type = nil, pattern = {})
86
+ # Get UUIDs from appropriate index
87
+ uuids = if type
88
+ @redis.sinter('facts:active', "facts:type:#{type}")
89
+ else
90
+ @redis.smembers('facts:active')
91
+ end
92
+
93
+ # Fetch and filter facts
94
+ facts = []
95
+ uuids.each do |uuid|
96
+ fact_data = @redis.hgetall("fact:#{uuid}")
97
+ next if fact_data.empty? || fact_data['retracted'] == '1'
98
+
99
+ attributes = JSON.parse(fact_data['attributes'], symbolize_names: true)
100
+
101
+ if matches_pattern?(attributes, pattern)
102
+ facts << {
103
+ uuid: fact_data['uuid'],
104
+ type: fact_data['type'].to_sym,
105
+ attributes: attributes
106
+ }
107
+ end
108
+ end
109
+
110
+ facts
111
+ end
112
+
113
+ def query_facts(conditions = nil, params = [])
114
+ # Redis doesn't support SQL queries
115
+ # For complex queries, use get_facts with pattern matching
116
+ # or implement custom Redis Lua scripts
117
+ get_facts
118
+ end
119
+
120
+ def register_knowledge_source(name, description: nil, topics: [])
121
+ topics_json = JSON.generate(topics)
122
+
123
+ @redis.hset("ks:#{name}", {
124
+ 'name' => name,
125
+ 'description' => description,
126
+ 'topics' => topics_json,
127
+ 'active' => '1',
128
+ 'registered_at' => Time.now.to_f
129
+ })
130
+
131
+ @redis.sadd('knowledge_sources:active', name)
132
+ end
133
+
134
+ def clear_session(session_id)
135
+ uuids = @redis.smembers("facts:session:#{session_id}")
136
+
137
+ uuids.each do |uuid|
138
+ remove_fact(uuid)
139
+ end
140
+
141
+ @redis.del("facts:session:#{session_id}")
142
+ end
143
+
144
+ def vacuum
145
+ # Remove retracted facts from Redis to free memory
146
+ all_uuids = @redis.smembers('facts:all')
147
+
148
+ all_uuids.each do |uuid|
149
+ fact_data = @redis.hgetall("fact:#{uuid}")
150
+ if fact_data['retracted'] == '1'
151
+ # Calculate if fact is old enough to remove (e.g., > 30 days)
152
+ retracted_at = fact_data['retracted_at'].to_f
153
+ if Time.now.to_f - retracted_at > (30 * 24 * 60 * 60)
154
+ type = fact_data['type']
155
+ session_id = fact_data['session_id']
156
+
157
+ @redis.multi do |pipeline|
158
+ pipeline.del("fact:#{uuid}")
159
+ pipeline.srem('facts:all', uuid)
160
+ pipeline.srem("facts:type:#{type}", uuid)
161
+ pipeline.srem("facts:session:#{session_id}", uuid) if session_id
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ def stats
169
+ active_count = @redis.scard('facts:active')
170
+ total_count = @redis.scard('facts:all')
171
+ ks_count = @redis.scard('knowledge_sources:active')
172
+
173
+ {
174
+ total_facts: total_count,
175
+ active_facts: active_count,
176
+ knowledge_sources: ks_count
177
+ }
178
+ end
179
+
180
+ def transaction(&block)
181
+ @transaction_depth += 1
182
+ begin
183
+ if @transaction_depth == 1
184
+ # Redis MULTI/EXEC happens in individual operations
185
+ # This provides a consistent interface
186
+ yield
187
+ else
188
+ yield
189
+ end
190
+ ensure
191
+ @transaction_depth -= 1
192
+ end
193
+ end
194
+
195
+ def close
196
+ @redis.close if @redis
197
+ end
198
+
199
+ # Redis-specific helper to get connection for MessageQueue/AuditLog
200
+ def connection
201
+ @redis
202
+ end
203
+
204
+ private
205
+
206
+ def matches_pattern?(attributes, pattern)
207
+ pattern.all? do |key, value|
208
+ if value.is_a?(Proc)
209
+ attributes[key] && value.call(attributes[key])
210
+ else
211
+ attributes[key] == value
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end