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,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sqlite3'
4
+ require 'json'
5
+ require_relative 'store'
6
+
7
+ module KBS
8
+ module Blackboard
9
+ module Persistence
10
+ class SqliteStore < Store
11
+ attr_reader :db, :db_path, :session_id
12
+
13
+ def initialize(db_path: ':memory:', session_id: nil)
14
+ @db_path = db_path
15
+ @session_id = session_id
16
+ @transaction_depth = 0
17
+ setup_database
18
+ end
19
+
20
+ def setup_database
21
+ @db = SQLite3::Database.new(@db_path)
22
+ @db.results_as_hash = true
23
+ create_tables
24
+ create_indexes
25
+ setup_triggers
26
+ end
27
+
28
+ def create_tables
29
+ @db.execute_batch <<-SQL
30
+ CREATE TABLE IF NOT EXISTS facts (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ uuid TEXT UNIQUE NOT NULL,
33
+ fact_type TEXT NOT NULL,
34
+ attributes TEXT NOT NULL,
35
+ fact_timestamp TIMESTAMP,
36
+ market_timestamp TIMESTAMP,
37
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
38
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
39
+ session_id TEXT,
40
+ retracted BOOLEAN DEFAULT 0,
41
+ retracted_at TIMESTAMP,
42
+ data_source TEXT,
43
+ market_session TEXT
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS knowledge_sources (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ name TEXT UNIQUE NOT NULL,
49
+ description TEXT,
50
+ topics TEXT,
51
+ active BOOLEAN DEFAULT 1,
52
+ registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
53
+ );
54
+ SQL
55
+ end
56
+
57
+ def create_indexes
58
+ @db.execute_batch <<-SQL
59
+ CREATE INDEX IF NOT EXISTS idx_facts_type ON facts(fact_type);
60
+ CREATE INDEX IF NOT EXISTS idx_facts_session ON facts(session_id);
61
+ CREATE INDEX IF NOT EXISTS idx_facts_retracted ON facts(retracted);
62
+ CREATE INDEX IF NOT EXISTS idx_facts_timestamp ON facts(fact_timestamp);
63
+ CREATE INDEX IF NOT EXISTS idx_facts_market_timestamp ON facts(market_timestamp);
64
+ CREATE INDEX IF NOT EXISTS idx_facts_market_session ON facts(market_session);
65
+ SQL
66
+ end
67
+
68
+ def setup_triggers
69
+ @db.execute_batch <<-SQL
70
+ CREATE TRIGGER IF NOT EXISTS update_fact_timestamp
71
+ AFTER UPDATE ON facts
72
+ BEGIN
73
+ UPDATE facts SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
74
+ END;
75
+ SQL
76
+ end
77
+
78
+ def add_fact(uuid, type, attributes)
79
+ attributes_json = JSON.generate(attributes)
80
+
81
+ @db.execute(
82
+ "INSERT INTO facts (uuid, fact_type, attributes, session_id) VALUES (?, ?, ?, ?)",
83
+ [uuid, type.to_s, attributes_json, @session_id]
84
+ )
85
+ end
86
+
87
+ def remove_fact(uuid)
88
+ result = @db.get_first_row(
89
+ "SELECT fact_type, attributes FROM facts WHERE uuid = ? AND retracted = 0",
90
+ [uuid]
91
+ )
92
+
93
+ if result
94
+ @db.execute(
95
+ "UPDATE facts SET retracted = 1, retracted_at = CURRENT_TIMESTAMP WHERE uuid = ?",
96
+ [uuid]
97
+ )
98
+
99
+ {
100
+ type: result['fact_type'].to_sym,
101
+ attributes: JSON.parse(result['attributes'], symbolize_names: true)
102
+ }
103
+ end
104
+ end
105
+
106
+ def update_fact(uuid, attributes)
107
+ attributes_json = JSON.generate(attributes)
108
+
109
+ @db.execute(
110
+ "UPDATE facts SET attributes = ? WHERE uuid = ? AND retracted = 0",
111
+ [attributes_json, uuid]
112
+ )
113
+
114
+ get_fact_type(uuid)
115
+ end
116
+
117
+ def get_fact(uuid)
118
+ result = @db.get_first_row(
119
+ "SELECT * FROM facts WHERE uuid = ? AND retracted = 0",
120
+ [uuid]
121
+ )
122
+
123
+ if result
124
+ {
125
+ uuid: result['uuid'],
126
+ type: result['fact_type'].to_sym,
127
+ attributes: JSON.parse(result['attributes'], symbolize_names: true)
128
+ }
129
+ end
130
+ end
131
+
132
+ def get_facts(type = nil, pattern = {})
133
+ query = "SELECT * FROM facts WHERE retracted = 0"
134
+ params = []
135
+
136
+ if type
137
+ query += " AND fact_type = ?"
138
+ params << type.to_s
139
+ end
140
+
141
+ results = @db.execute(query, params)
142
+
143
+ results.map do |row|
144
+ attributes = JSON.parse(row['attributes'], symbolize_names: true)
145
+
146
+ if matches_pattern?(attributes, pattern)
147
+ {
148
+ uuid: row['uuid'],
149
+ type: row['fact_type'].to_sym,
150
+ attributes: attributes
151
+ }
152
+ end
153
+ end.compact
154
+ end
155
+
156
+ def query_facts(sql_conditions = nil, params = [])
157
+ query = "SELECT * FROM facts WHERE retracted = 0"
158
+ query += " AND #{sql_conditions}" if sql_conditions
159
+
160
+ results = @db.execute(query, params)
161
+
162
+ results.map do |row|
163
+ {
164
+ uuid: row['uuid'],
165
+ type: row['fact_type'].to_sym,
166
+ attributes: JSON.parse(row['attributes'], symbolize_names: true)
167
+ }
168
+ end
169
+ end
170
+
171
+ def register_knowledge_source(name, description: nil, topics: [])
172
+ topics_json = JSON.generate(topics)
173
+
174
+ @db.execute(
175
+ "INSERT OR REPLACE INTO knowledge_sources (name, description, topics) VALUES (?, ?, ?)",
176
+ [name, description, topics_json]
177
+ )
178
+ end
179
+
180
+ def clear_session(session_id)
181
+ @db.execute(
182
+ "UPDATE facts SET retracted = 1, retracted_at = CURRENT_TIMESTAMP WHERE session_id = ?",
183
+ [session_id]
184
+ )
185
+ end
186
+
187
+ def vacuum
188
+ @db.execute("VACUUM")
189
+ end
190
+
191
+ def stats
192
+ {
193
+ total_facts: @db.get_first_value("SELECT COUNT(*) FROM facts"),
194
+ active_facts: @db.get_first_value("SELECT COUNT(*) FROM facts WHERE retracted = 0"),
195
+ knowledge_sources: @db.get_first_value("SELECT COUNT(*) FROM knowledge_sources WHERE active = 1")
196
+ }
197
+ end
198
+
199
+ def transaction(&block)
200
+ @transaction_depth += 1
201
+ result = nil
202
+ begin
203
+ if @transaction_depth == 1
204
+ @db.transaction do
205
+ result = yield
206
+ end
207
+ else
208
+ result = yield
209
+ end
210
+ ensure
211
+ @transaction_depth -= 1
212
+ end
213
+ result
214
+ end
215
+
216
+ def close
217
+ @db.close if @db
218
+ end
219
+
220
+ private
221
+
222
+ def get_fact_type(uuid)
223
+ result = @db.get_first_row(
224
+ "SELECT fact_type FROM facts WHERE uuid = ?",
225
+ [uuid]
226
+ )
227
+ result ? result['fact_type'].to_sym : nil
228
+ end
229
+
230
+ def matches_pattern?(attributes, pattern)
231
+ pattern.all? do |key, value|
232
+ if value.is_a?(Proc)
233
+ attributes[key] && value.call(attributes[key])
234
+ else
235
+ attributes[key] == value
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module Blackboard
5
+ module Persistence
6
+ # Abstract interface for fact persistence
7
+ class Store
8
+ def add_fact(uuid, type, attributes)
9
+ raise NotImplementedError, "#{self.class} must implement #add_fact"
10
+ end
11
+
12
+ def remove_fact(uuid)
13
+ raise NotImplementedError, "#{self.class} must implement #remove_fact"
14
+ end
15
+
16
+ def update_fact(uuid, attributes)
17
+ raise NotImplementedError, "#{self.class} must implement #update_fact"
18
+ end
19
+
20
+ def get_fact(uuid)
21
+ raise NotImplementedError, "#{self.class} must implement #get_fact"
22
+ end
23
+
24
+ def get_facts(type = nil, pattern = {})
25
+ raise NotImplementedError, "#{self.class} must implement #get_facts"
26
+ end
27
+
28
+ def query_facts(conditions = nil, params = [])
29
+ raise NotImplementedError, "#{self.class} must implement #query_facts"
30
+ end
31
+
32
+ def clear_session(session_id)
33
+ raise NotImplementedError, "#{self.class} must implement #clear_session"
34
+ end
35
+
36
+ def stats
37
+ raise NotImplementedError, "#{self.class} must implement #stats"
38
+ end
39
+
40
+ def close
41
+ raise NotImplementedError, "#{self.class} must implement #close"
42
+ end
43
+
44
+ def vacuum
45
+ # Optional operation - no-op by default
46
+ end
47
+
48
+ def transaction(&block)
49
+ # Default: just execute the block
50
+ yield if block_given?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module KBS
7
+ module Blackboard
8
+ # Redis-based audit log using lists for history
9
+ class RedisAuditLog
10
+ def initialize(redis, session_id)
11
+ @redis = redis
12
+ @session_id = session_id
13
+ end
14
+
15
+ def log_fact_change(fact_uuid, fact_type, attributes, action)
16
+ attributes_json = attributes.is_a?(String) ? attributes : JSON.generate(attributes)
17
+ timestamp = Time.now.to_f
18
+
19
+ entry = {
20
+ 'fact_uuid' => fact_uuid,
21
+ 'fact_type' => fact_type.to_s,
22
+ 'attributes' => attributes_json,
23
+ 'action' => action,
24
+ 'timestamp' => timestamp,
25
+ 'session_id' => @session_id
26
+ }
27
+
28
+ entry_json = JSON.generate(entry)
29
+
30
+ # Add to global history (as list - newest first)
31
+ @redis.lpush('fact_history:all', entry_json)
32
+
33
+ # Add to fact-specific history
34
+ @redis.lpush("fact_history:#{fact_uuid}", entry_json)
35
+
36
+ # Optionally limit history size (e.g., keep last 10000 entries)
37
+ @redis.ltrim('fact_history:all', 0, 9999)
38
+ @redis.ltrim("fact_history:#{fact_uuid}", 0, 999)
39
+ end
40
+
41
+ def log_rule_firing(rule_name, fact_uuids, bindings = {})
42
+ timestamp = Time.now.to_f
43
+
44
+ entry = {
45
+ 'rule_name' => rule_name,
46
+ 'fact_uuids' => JSON.generate(fact_uuids),
47
+ 'bindings' => JSON.generate(bindings),
48
+ 'fired_at' => timestamp,
49
+ 'session_id' => @session_id
50
+ }
51
+
52
+ entry_json = JSON.generate(entry)
53
+
54
+ # Add to global rules fired list
55
+ @redis.lpush('rules_fired:all', entry_json)
56
+
57
+ # Add to rule-specific history
58
+ @redis.lpush("rules_fired:#{rule_name}", entry_json)
59
+
60
+ # Limit size
61
+ @redis.ltrim('rules_fired:all', 0, 9999)
62
+ @redis.ltrim("rules_fired:#{rule_name}", 0, 999)
63
+ end
64
+
65
+ def fact_history(fact_uuid = nil, limit: 100)
66
+ key = fact_uuid ? "fact_history:#{fact_uuid}" : 'fact_history:all'
67
+ entries_json = @redis.lrange(key, 0, limit - 1)
68
+
69
+ entries_json.map do |entry_json|
70
+ entry = JSON.parse(entry_json, symbolize_names: true)
71
+ {
72
+ fact_uuid: entry[:fact_uuid],
73
+ fact_type: entry[:fact_type].to_sym,
74
+ attributes: JSON.parse(entry[:attributes], symbolize_names: true),
75
+ action: entry[:action],
76
+ timestamp: Time.at(entry[:timestamp]),
77
+ session_id: entry[:session_id]
78
+ }
79
+ end
80
+ end
81
+
82
+ def rule_firings(rule_name = nil, limit: 100)
83
+ key = rule_name ? "rules_fired:#{rule_name}" : 'rules_fired:all'
84
+ entries_json = @redis.lrange(key, 0, limit - 1)
85
+
86
+ entries_json.map do |entry_json|
87
+ entry = JSON.parse(entry_json, symbolize_names: true)
88
+ {
89
+ rule_name: entry[:rule_name],
90
+ fact_uuids: JSON.parse(entry[:fact_uuids]),
91
+ bindings: JSON.parse(entry[:bindings], symbolize_names: true),
92
+ fired_at: Time.at(entry[:fired_at]),
93
+ session_id: entry[:session_id]
94
+ }
95
+ end
96
+ end
97
+
98
+ def stats
99
+ rules_fired_count = @redis.llen('rules_fired:all')
100
+
101
+ {
102
+ rules_fired: rules_fired_count
103
+ }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module KBS
7
+ module Blackboard
8
+ # Redis-based message queue using sorted sets for priority ordering
9
+ class RedisMessageQueue
10
+ def initialize(redis)
11
+ @redis = redis
12
+ @message_id_counter = "message_id_counter"
13
+ end
14
+
15
+ def post(sender, topic, content, priority: 0)
16
+ message_id = @redis.incr(@message_id_counter)
17
+ content_json = content.is_a?(String) ? content : JSON.generate(content)
18
+ timestamp = Time.now.to_f
19
+
20
+ message_data = {
21
+ 'id' => message_id,
22
+ 'sender' => sender,
23
+ 'topic' => topic,
24
+ 'content' => content_json,
25
+ 'priority' => priority,
26
+ 'posted_at' => timestamp,
27
+ 'consumed' => '0'
28
+ }
29
+
30
+ # Store message data
31
+ @redis.hset("message:#{message_id}", message_data)
32
+
33
+ # Add to topic queue with score = -priority (for DESC ordering) + timestamp (for ASC within priority)
34
+ # Score: higher priority = lower score (negative), then by timestamp
35
+ score = (-priority * 1_000_000) + timestamp
36
+ @redis.zadd("messages:#{topic}", score, message_id)
37
+
38
+ message_id
39
+ end
40
+
41
+ def consume(topic, consumer)
42
+ # Get highest priority (lowest score) unconsumed message
43
+ messages = @redis.zrange("messages:#{topic}", 0, 0)
44
+ return nil if messages.empty?
45
+
46
+ message_id = messages.first
47
+ message_data = @redis.hgetall("message:#{message_id}")
48
+
49
+ return nil if message_data.empty? || message_data['consumed'] == '1'
50
+
51
+ # Mark as consumed
52
+ @redis.multi do |pipeline|
53
+ pipeline.hset("message:#{message_id}", 'consumed', '1')
54
+ pipeline.hset("message:#{message_id}", 'consumed_by', consumer)
55
+ pipeline.hset("message:#{message_id}", 'consumed_at', Time.now.to_f)
56
+ pipeline.zrem("messages:#{topic}", message_id)
57
+ end
58
+
59
+ {
60
+ id: message_data['id'].to_i,
61
+ sender: message_data['sender'],
62
+ topic: message_data['topic'],
63
+ content: JSON.parse(message_data['content'], symbolize_names: true),
64
+ priority: message_data['priority'].to_i,
65
+ posted_at: Time.at(message_data['posted_at'].to_f)
66
+ }
67
+ end
68
+
69
+ def peek(topic, limit: 10)
70
+ # Get top N messages without consuming
71
+ message_ids = @redis.zrange("messages:#{topic}", 0, limit - 1)
72
+ messages = []
73
+
74
+ message_ids.each do |message_id|
75
+ message_data = @redis.hgetall("message:#{message_id}")
76
+ next if message_data.empty? || message_data['consumed'] == '1'
77
+
78
+ messages << {
79
+ id: message_data['id'].to_i,
80
+ sender: message_data['sender'],
81
+ topic: message_data['topic'],
82
+ content: JSON.parse(message_data['content'], symbolize_names: true),
83
+ priority: message_data['priority'].to_i,
84
+ posted_at: Time.at(message_data['posted_at'].to_f)
85
+ }
86
+ end
87
+
88
+ messages
89
+ end
90
+
91
+ def stats
92
+ # Count all unconsumed messages across all topics
93
+ topics = @redis.keys('messages:*')
94
+ total_unconsumed = 0
95
+
96
+ topics.each do |topic_key|
97
+ total_unconsumed += @redis.zcard(topic_key)
98
+ end
99
+
100
+ # Count all messages (including consumed)
101
+ all_message_keys = @redis.keys('message:*')
102
+ total_messages = all_message_keys.size
103
+
104
+ {
105
+ total_messages: total_messages,
106
+ unconsumed_messages: total_unconsumed
107
+ }
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../kbs'
4
+
5
+ # Blackboard pattern components
6
+ require_relative 'blackboard/fact'
7
+ require_relative 'blackboard/message_queue'
8
+ require_relative 'blackboard/audit_log'
9
+ require_relative 'blackboard/redis_message_queue'
10
+ require_relative 'blackboard/redis_audit_log'
11
+ require_relative 'blackboard/persistence/store'
12
+ require_relative 'blackboard/persistence/sqlite_store'
13
+ require_relative 'blackboard/persistence/redis_store'
14
+ require_relative 'blackboard/persistence/hybrid_store'
15
+ require_relative 'blackboard/memory'
16
+ require_relative 'blackboard/engine'
17
+
18
+ # Backward compatibility aliases (deprecated - will be removed in v1.0)
19
+ module KBS
20
+ BlackboardMemory = Blackboard::Memory
21
+ BlackboardEngine = Blackboard::Engine
22
+ PersistedFact = Blackboard::Fact
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class Condition
5
+ attr_reader :type, :pattern, :variable_bindings, :negated
6
+
7
+ def initialize(type, pattern = {}, negated: false)
8
+ @type = type
9
+ @pattern = pattern
10
+ @negated = negated
11
+ @variable_bindings = extract_variables(pattern)
12
+ end
13
+
14
+ private
15
+
16
+ def extract_variables(pattern)
17
+ vars = {}
18
+ pattern.each do |key, value|
19
+ if value.is_a?(Symbol) && value.to_s.start_with?('?')
20
+ vars[value] = key
21
+ end
22
+ end
23
+ vars
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module DSL
5
+ module ConditionHelpers
6
+ def less_than(value)
7
+ ->(v) { v < value }
8
+ end
9
+
10
+ def greater_than(value)
11
+ ->(v) { v > value }
12
+ end
13
+
14
+ def equals(value)
15
+ value
16
+ end
17
+
18
+ def not_equal(value)
19
+ ->(v) { v != value }
20
+ end
21
+
22
+ def one_of(*values)
23
+ ->(v) { values.include?(v) }
24
+ end
25
+
26
+ def range(min_or_range, max = nil)
27
+ if min_or_range.is_a?(Range)
28
+ ->(v) { min_or_range.include?(v) }
29
+ else
30
+ ->(v) { v >= min_or_range && v <= max }
31
+ end
32
+ end
33
+
34
+ def between(min, max)
35
+ range(min, max)
36
+ end
37
+
38
+ def any(*values)
39
+ if values.empty?
40
+ # Match anything
41
+ ->(v) { true }
42
+ else
43
+ # Match one of the values
44
+ one_of(*values)
45
+ end
46
+ end
47
+
48
+ def matches(pattern)
49
+ ->(v) { v.match?(pattern) }
50
+ end
51
+
52
+ def satisfies(&block)
53
+ block
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module DSL
5
+ class KnowledgeBase
6
+ attr_reader :engine, :rules
7
+
8
+ def initialize
9
+ @engine = ReteEngine.new
10
+ @rules = {}
11
+ @rule_builders = {}
12
+ end
13
+
14
+ def rule(name, &block)
15
+ builder = RuleBuilder.new(name)
16
+ builder.instance_eval(&block) if block_given?
17
+ @rule_builders[name] = builder
18
+ rule = builder.build
19
+ @rules[name] = rule
20
+ @engine.add_rule(rule)
21
+ builder
22
+ end
23
+
24
+ def defrule(name, &block)
25
+ rule(name, &block)
26
+ end
27
+
28
+ def fact(type, attributes = {})
29
+ @engine.add_fact(type, attributes)
30
+ end
31
+
32
+ def assert(type, attributes = {})
33
+ fact(type, attributes)
34
+ end
35
+
36
+ def retract(fact)
37
+ @engine.remove_fact(fact)
38
+ end
39
+
40
+ def run
41
+ @engine.run
42
+ end
43
+
44
+ def reset
45
+ @engine.working_memory.facts.clear
46
+ end
47
+
48
+ def facts
49
+ @engine.working_memory.facts
50
+ end
51
+
52
+ def query(type, pattern = {})
53
+ @engine.working_memory.facts.select do |fact|
54
+ next false unless fact.type == type
55
+ pattern.all? { |key, value| fact.attributes[key] == value }
56
+ end
57
+ end
58
+
59
+ def print_facts
60
+ puts "Working Memory Contents:"
61
+ puts "-" * 40
62
+ facts.each_with_index do |fact, i|
63
+ puts "#{i + 1}. #{fact}"
64
+ end
65
+ puts "-" * 40
66
+ end
67
+
68
+ def print_rules
69
+ puts "Knowledge Base Rules:"
70
+ puts "-" * 40
71
+ @rule_builders.each do |name, builder|
72
+ puts "Rule: #{name}"
73
+ puts " Description: #{builder.description}" if builder.description
74
+ puts " Priority: #{builder.priority}"
75
+ puts " Conditions: #{builder.conditions.size}"
76
+ builder.conditions.each_with_index do |cond, i|
77
+ negated = cond.negated ? "NOT " : ""
78
+ puts " #{i + 1}. #{negated}#{cond.type}(#{cond.pattern})"
79
+ end
80
+ puts ""
81
+ end
82
+ puts "-" * 40
83
+ end
84
+ end
85
+ end
86
+ end