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,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
|