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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module DSL
5
+ class PatternEvaluator
6
+ attr_reader :pattern
7
+
8
+ def initialize
9
+ @pattern = {}
10
+ end
11
+
12
+ def method_missing(method, *args, &block)
13
+ if args.empty? && !block_given?
14
+ Variable.new(method)
15
+ elsif args.length == 1 && !block_given?
16
+ @pattern[method] = args.first
17
+ elsif block_given?
18
+ @pattern[method] = block
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def >(value)
25
+ ->(v) { v > value }
26
+ end
27
+
28
+ def <(value)
29
+ ->(v) { v < value }
30
+ end
31
+
32
+ def >=(value)
33
+ ->(v) { v >= value }
34
+ end
35
+
36
+ def <=(value)
37
+ ->(v) { v <= value }
38
+ end
39
+
40
+ def ==(value)
41
+ value
42
+ end
43
+
44
+ def !=(value)
45
+ ->(v) { v != value }
46
+ end
47
+
48
+ def between(min, max)
49
+ ->(v) { v >= min && v <= max }
50
+ end
51
+
52
+ def in(collection)
53
+ ->(v) { collection.include?(v) }
54
+ end
55
+
56
+ def matches(pattern)
57
+ ->(v) { v.match?(pattern) }
58
+ end
59
+
60
+ def any(*values)
61
+ ->(v) { values.include?(v) }
62
+ end
63
+
64
+ def all(*conditions)
65
+ ->(v) { conditions.all? { |c| c.is_a?(Proc) ? c.call(v) : c == v } }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module DSL
5
+ class RuleBuilder
6
+ include ConditionHelpers
7
+
8
+ attr_reader :name, :description, :conditions, :action_block
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @description = nil
13
+ @priority = 0
14
+ @conditions = []
15
+ @action_block = nil
16
+ @current_condition_group = []
17
+ @negated = false
18
+ end
19
+
20
+ def desc(description)
21
+ @description = description
22
+ self
23
+ end
24
+
25
+ def priority(level = nil)
26
+ return @priority if level.nil?
27
+ @priority = level
28
+ self
29
+ end
30
+
31
+ # Primary DSL keywords (avoid Ruby reserved words)
32
+ def on(type, pattern = {}, &block)
33
+ if block_given?
34
+ pattern = pattern.merge(evaluate_block(&block))
35
+ end
36
+ @conditions << Condition.new(type, pattern, negated: @negated)
37
+ @negated = false
38
+ self
39
+ end
40
+
41
+ def without(type = nil, pattern = {}, &block)
42
+ if type
43
+ # Direct negation: without(:problem)
44
+ @negated = true
45
+ on(type, pattern, &block)
46
+ else
47
+ # Chaining: without.on(:problem)
48
+ @negated = true
49
+ self
50
+ end
51
+ end
52
+
53
+ def perform(&block)
54
+ @action_block = block
55
+ self
56
+ end
57
+
58
+ # Aliases for readability
59
+ def given(type, pattern = {}, &block)
60
+ on(type, pattern, &block)
61
+ end
62
+
63
+ def matches(type, pattern = {}, &block)
64
+ on(type, pattern, &block)
65
+ end
66
+
67
+ def fact(type, pattern = {}, &block)
68
+ on(type, pattern, &block)
69
+ end
70
+
71
+ def exists(type, pattern = {}, &block)
72
+ on(type, pattern, &block)
73
+ end
74
+
75
+ def absent(type, pattern = {}, &block)
76
+ without.on(type, pattern, &block)
77
+ end
78
+
79
+ def missing(type, pattern = {}, &block)
80
+ without.on(type, pattern, &block)
81
+ end
82
+
83
+ def lacks(type, pattern = {}, &block)
84
+ without.on(type, pattern, &block)
85
+ end
86
+
87
+ def action(&block)
88
+ perform(&block)
89
+ end
90
+
91
+ def execute(&block)
92
+ perform(&block)
93
+ end
94
+
95
+ def then(&block)
96
+ perform(&block)
97
+ end
98
+
99
+ def build
100
+ Rule.new(@name,
101
+ conditions: @conditions,
102
+ action: @action_block,
103
+ priority: @priority)
104
+ end
105
+
106
+ private
107
+
108
+ def evaluate_block(&block)
109
+ evaluator = PatternEvaluator.new
110
+ evaluator.instance_eval(&block)
111
+ evaluator.pattern
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ module DSL
5
+ class Variable
6
+ attr_reader :name
7
+
8
+ def initialize(name)
9
+ name_str = name.to_s
10
+ @name = name_str.start_with?('?') ? name_str.to_sym : "?#{name_str}".to_sym
11
+ end
12
+
13
+ def to_sym
14
+ @name
15
+ end
16
+
17
+ def to_s
18
+ @name.to_s
19
+ end
20
+
21
+ def ==(other)
22
+ return false unless other.is_a?(Variable)
23
+ @name == other.name
24
+ end
25
+
26
+ def eql?(other)
27
+ self == other
28
+ end
29
+
30
+ def hash
31
+ @name.hash
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/kbs/dsl.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../kbs'
4
+
5
+ # DSL components
6
+ require_relative 'dsl/variable'
7
+ require_relative 'dsl/condition_helpers'
8
+ require_relative 'dsl/pattern_evaluator'
9
+ require_relative 'dsl/rule_builder'
10
+ require_relative 'dsl/knowledge_base'
11
+
12
+ module KBS
13
+ def self.knowledge_base(&block)
14
+ kb = DSL::KnowledgeBase.new
15
+ kb.instance_eval(&block) if block_given?
16
+ kb
17
+ end
18
+ end
data/lib/kbs/fact.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class Fact
5
+ attr_reader :id, :type, :attributes
6
+
7
+ def initialize(type, attributes = {})
8
+ @id = object_id
9
+ @type = type
10
+ @attributes = attributes
11
+ end
12
+
13
+ def [](key)
14
+ @attributes[key]
15
+ end
16
+
17
+ def []=(key, value)
18
+ @attributes[key] = value
19
+ end
20
+
21
+ def matches?(pattern)
22
+ return false if pattern[:type] && pattern[:type] != @type
23
+
24
+ pattern.each do |key, value|
25
+ next if key == :type
26
+
27
+ if value.is_a?(Proc)
28
+ return false unless @attributes[key] && value.call(@attributes[key])
29
+ elsif value.is_a?(Symbol) && value.to_s.start_with?('?')
30
+ next
31
+ else
32
+ return false unless @attributes[key] == value
33
+ end
34
+ end
35
+
36
+ true
37
+ end
38
+
39
+ def to_s
40
+ "#{@type}(#{@attributes.map { |k, v| "#{k}: #{v}" }.join(', ')})"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class JoinNode
5
+ attr_accessor :alpha_memory, :beta_memory, :successors, :tests
6
+ attr_reader :left_linked, :right_linked
7
+
8
+ def initialize(alpha_memory, beta_memory, tests = [])
9
+ @alpha_memory = alpha_memory
10
+ @beta_memory = beta_memory
11
+ @successors = []
12
+ @tests = tests
13
+ @left_linked = true
14
+ @right_linked = true
15
+
16
+ alpha_memory.successors << self if alpha_memory
17
+ beta_memory.successors << self if beta_memory
18
+ end
19
+
20
+ def left_unlink!
21
+ @left_linked = false
22
+ end
23
+
24
+ def left_relink!
25
+ @left_linked = true
26
+ @beta_memory.tokens.each { |token| left_activate(token) } if @beta_memory
27
+ end
28
+
29
+ def right_unlink!
30
+ @right_linked = false
31
+ end
32
+
33
+ def right_relink!
34
+ @right_linked = true
35
+ @alpha_memory.items.each { |fact| right_activate(fact) } if @alpha_memory
36
+ end
37
+
38
+ def left_activate(token)
39
+ # Left activation: a new token from beta memory needs to be joined with facts from alpha memory
40
+ return unless @left_linked && @right_linked
41
+
42
+ @alpha_memory.items.each do |fact|
43
+ if perform_join_tests(token, fact)
44
+ new_token = Token.new(token, fact, self)
45
+ token.children << new_token if token
46
+ @successors.each { |s| s.activate(new_token) }
47
+ end
48
+ end
49
+ end
50
+
51
+ def right_activate(fact)
52
+ return unless @left_linked && @right_linked
53
+
54
+ parent_tokens = @beta_memory ? @beta_memory.tokens : [Token.new(nil, nil, nil)]
55
+
56
+ parent_tokens.each do |token|
57
+ if perform_join_tests(token, fact)
58
+ new_token = Token.new(token, fact, self)
59
+ token.children << new_token if token
60
+ @successors.each { |s| s.activate(new_token) }
61
+ end
62
+ end
63
+ end
64
+
65
+ def left_deactivate(token)
66
+ token.children.each do |child|
67
+ @successors.each { |s| s.deactivate(child) if s.respond_to?(:deactivate) }
68
+ end
69
+ token.children.clear
70
+ end
71
+
72
+ def right_deactivate(fact)
73
+ tokens_to_remove = []
74
+
75
+ if @beta_memory
76
+ @beta_memory.tokens.each do |token|
77
+ token.children.select { |child| child.fact == fact }.each do |child|
78
+ tokens_to_remove << child
79
+ @successors.each { |s| s.deactivate(child) if s.respond_to?(:deactivate) }
80
+ end
81
+ end
82
+ end
83
+
84
+ tokens_to_remove.each { |token| token.parent.children.delete(token) if token.parent }
85
+ end
86
+
87
+ private
88
+
89
+ def perform_join_tests(token, fact)
90
+ @tests.all? do |test|
91
+ fact_value = fact.attributes[test[:fact_field]]
92
+
93
+ # If test has expected_value, compare against that constant
94
+ if test.key?(:expected_value)
95
+ if test[:operation] == :eq
96
+ fact_value == test[:expected_value]
97
+ elsif test[:operation] == :ne
98
+ fact_value != test[:expected_value]
99
+ else
100
+ true
101
+ end
102
+ else
103
+ # Otherwise compare with token value
104
+ token_value = token.facts[test[:token_field_index]]&.attributes&.[](test[:token_field])
105
+
106
+ if test[:operation] == :eq
107
+ token_value == fact_value
108
+ elsif test[:operation] == :ne
109
+ token_value != fact_value
110
+ else
111
+ true
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class NegationNode
5
+ attr_accessor :alpha_memory, :beta_memory, :successors, :tests
6
+
7
+ def initialize(alpha_memory, beta_memory, tests = [])
8
+ @alpha_memory = alpha_memory
9
+ @beta_memory = beta_memory
10
+ @successors = []
11
+ @tests = tests
12
+ @tokens_with_matches = Hash.new { |h, k| h[k] = [] }
13
+
14
+ alpha_memory.successors << self if alpha_memory
15
+ beta_memory.successors << self if beta_memory
16
+ end
17
+
18
+ def left_activate(token)
19
+ matches = @alpha_memory.items.select { |fact| perform_join_tests(token, fact) }
20
+
21
+ if matches.empty?
22
+ new_token = Token.new(token, nil, self)
23
+ token.children << new_token
24
+ @successors.each { |s| s.activate(new_token) }
25
+ else
26
+ @tokens_with_matches[token] = matches
27
+ end
28
+ end
29
+
30
+ def right_activate(fact)
31
+ @beta_memory.tokens.each do |token|
32
+ if perform_join_tests(token, fact)
33
+ if @tokens_with_matches[token].empty?
34
+ token.children.each do |child|
35
+ @successors.each { |s| s.deactivate(child) if s.respond_to?(:deactivate) }
36
+ end
37
+ token.children.clear
38
+ end
39
+ @tokens_with_matches[token] << fact
40
+ end
41
+ end
42
+ end
43
+
44
+ def right_deactivate(fact)
45
+ @beta_memory.tokens.each do |token|
46
+ if @tokens_with_matches[token].include?(fact)
47
+ @tokens_with_matches[token].delete(fact)
48
+
49
+ if @tokens_with_matches[token].empty?
50
+ new_token = Token.new(token, nil, self)
51
+ token.children << new_token
52
+ @successors.each { |s| s.activate(new_token) }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def perform_join_tests(token, fact)
61
+ @tests.all? do |test|
62
+ fact_value = fact.attributes[test[:fact_field]]
63
+
64
+ # If test has expected_value, compare against that constant
65
+ if test.key?(:expected_value)
66
+ if test[:operation] == :eq
67
+ fact_value == test[:expected_value]
68
+ elsif test[:operation] == :ne
69
+ fact_value != test[:expected_value]
70
+ else
71
+ true
72
+ end
73
+ else
74
+ # Otherwise compare with token value
75
+ token_value = token.facts[test[:token_field_index]]&.attributes&.[](test[:token_field])
76
+
77
+ if test[:operation] == :eq
78
+ token_value == fact_value
79
+ elsif test[:operation] == :ne
80
+ token_value != fact_value
81
+ else
82
+ true
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class ProductionNode
5
+ attr_accessor :rule, :tokens
6
+
7
+ def initialize(rule)
8
+ @rule = rule
9
+ @tokens = []
10
+ end
11
+
12
+ def activate(token)
13
+ @tokens << token
14
+ # Don't fire immediately - wait for run() to fire rules
15
+ # This allows negation nodes to deactivate tokens before they fire
16
+ end
17
+
18
+ def fire_rule(token)
19
+ return if token.fired?
20
+ @rule.fire(token.facts)
21
+ token.mark_fired!
22
+ end
23
+
24
+ def deactivate(token)
25
+ @tokens.delete(token)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class ReteEngine
5
+ attr_reader :working_memory, :rules, :alpha_memories, :production_nodes
6
+
7
+ def initialize
8
+ @working_memory = WorkingMemory.new
9
+ @rules = []
10
+ @alpha_memories = {}
11
+ @production_nodes = {}
12
+ @root_beta_memory = BetaMemory.new
13
+
14
+ # Add initial dummy token to root beta memory
15
+ # This represents "no conditions matched yet" and allows the first condition to match
16
+ @root_beta_memory.add_token(Token.new(nil, nil, nil))
17
+
18
+ @working_memory.add_observer(self)
19
+ end
20
+
21
+ def add_rule(rule)
22
+ @rules << rule
23
+ build_network_for_rule(rule)
24
+ end
25
+
26
+ def add_fact(type, attributes = {})
27
+ fact = Fact.new(type, attributes)
28
+ @working_memory.add_fact(fact)
29
+ fact
30
+ end
31
+
32
+ def remove_fact(fact)
33
+ @working_memory.remove_fact(fact)
34
+ end
35
+
36
+ def update(action, fact)
37
+ if action == :add
38
+ @alpha_memories.each do |pattern, memory|
39
+ memory.activate(fact) if fact.matches?(pattern)
40
+ end
41
+ elsif action == :remove
42
+ @alpha_memories.each do |pattern, memory|
43
+ memory.deactivate(fact) if fact.matches?(pattern)
44
+ end
45
+ end
46
+ end
47
+
48
+ def run
49
+ @production_nodes.values.each do |node|
50
+ node.tokens.each do |token|
51
+ node.fire_rule(token)
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def build_network_for_rule(rule)
59
+ current_beta = @root_beta_memory
60
+
61
+ rule.conditions.each_with_index do |condition, index|
62
+ # Build alpha memory pattern - merge condition type
63
+ pattern = condition.pattern.merge(type: condition.type)
64
+ alpha_memory = get_or_create_alpha_memory(pattern)
65
+
66
+ # Build join tests - if pattern has :type that differs from condition.type,
67
+ # add it as an attribute test since it was overwritten in the merge
68
+ tests = []
69
+ if condition.pattern[:type] && condition.pattern[:type] != condition.type
70
+ # The pattern's :type should be checked as an attribute constraint
71
+ tests << {
72
+ token_field_index: index,
73
+ token_field: :type,
74
+ fact_field: :type,
75
+ operation: :eq,
76
+ expected_value: condition.pattern[:type]
77
+ }
78
+ end
79
+
80
+ if condition.negated
81
+ negation_node = NegationNode.new(alpha_memory, current_beta, tests)
82
+ new_beta = BetaMemory.new
83
+ negation_node.successors << new_beta
84
+ current_beta = new_beta
85
+ else
86
+ join_node = JoinNode.new(alpha_memory, current_beta, tests)
87
+ new_beta = BetaMemory.new
88
+ join_node.successors << new_beta
89
+ current_beta = new_beta
90
+ end
91
+ end
92
+
93
+ production_node = ProductionNode.new(rule)
94
+ current_beta.successors << production_node
95
+ @production_nodes[rule.name] = production_node
96
+
97
+ @working_memory.facts.each do |fact|
98
+ @alpha_memories.each do |pattern, memory|
99
+ memory.activate(fact) if fact.matches?(pattern)
100
+ end
101
+ end
102
+ end
103
+
104
+ def get_or_create_alpha_memory(pattern)
105
+ @alpha_memories[pattern] ||= AlphaMemory.new(pattern)
106
+ end
107
+ end
108
+ end
data/lib/kbs/rule.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class Rule
5
+ attr_reader :name, :priority
6
+ attr_accessor :conditions, :action
7
+
8
+ def initialize(name, conditions: [], action: nil, priority: 0, &block)
9
+ @name = name
10
+ @conditions = conditions
11
+ @action = action
12
+ @priority = priority
13
+ @fired_count = 0
14
+
15
+ yield self if block_given?
16
+ end
17
+
18
+ def fire(facts)
19
+ @fired_count += 1
20
+ return unless @action
21
+
22
+ bindings = extract_bindings(facts)
23
+
24
+ # Support both 1-parameter and 2-parameter actions
25
+ if @action.arity == 1 || @action.arity == -1
26
+ @action.call(facts)
27
+ else
28
+ @action.call(facts, bindings)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def extract_bindings(facts)
35
+ bindings = {}
36
+ @conditions.each_with_index do |condition, index|
37
+ next if condition.negated
38
+ fact = facts[index]
39
+ condition.variable_bindings.each do |var, field|
40
+ bindings[var] = fact.attributes[field] if fact
41
+ end
42
+ end
43
+ bindings
44
+ end
45
+ end
46
+ end
data/lib/kbs/token.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ class Token
5
+ attr_accessor :parent, :fact, :node, :children
6
+
7
+ def initialize(parent, fact, node)
8
+ @parent = parent
9
+ @fact = fact
10
+ @node = node
11
+ @children = []
12
+ @fired = false
13
+ end
14
+
15
+ def facts
16
+ facts = []
17
+ token = self
18
+ while token
19
+ facts.unshift(token.fact) if token.fact
20
+ token = token.parent
21
+ end
22
+ facts
23
+ end
24
+
25
+ def to_s
26
+ "Token(#{facts.map(&:to_s).join(', ')})"
27
+ end
28
+
29
+ def fired?
30
+ @fired
31
+ end
32
+
33
+ def mark_fired!
34
+ @fired = true
35
+ end
36
+ end
37
+ end