kbs 0.0.1 → 0.2.0
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 +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +291 -362
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +865 -0
- data/docs/advanced/testing.md +827 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +1047 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1104 -0
- data/docs/architecture/blackboard.md +544 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/fact-rule-relationship.svg +65 -0
- data/docs/assets/images/fact-structure.svg +42 -0
- data/docs/assets/images/inference-cycle.svg +47 -0
- data/docs/assets/images/kb-components.svg +43 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/rule-structure.svg +44 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/index.md +223 -0
- data/docs/guides/blackboard-memory.md +589 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +385 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +914 -0
- data/docs/index.md +155 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +221 -0
- data/docs/what-is-a-fact.md +694 -0
- data/docs/what-is-a-knowledge-base.md +350 -0
- data/docs/what-is-a-rule.md +833 -0
- data/examples/.gitignore +1 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -6
- data/examples/concurrent_inference_demo_dsl.rb +362 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/iot_demo_using_dsl.rb +1 -1
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/rule_source_demo.rb +123 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp_dsl.txt +9392 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +101 -2
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +15 -13
- data/mkdocs.yml +181 -0
- metadata +74 -9
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/kbs'
|
|
4
|
+
include KBS::DSL::ConditionHelpers
|
|
5
|
+
|
|
6
|
+
# Define the knowledge base with rules
|
|
7
|
+
kb = KBS.knowledge_base do
|
|
8
|
+
# Rule 1: Simple stock momentum
|
|
9
|
+
rule "momentum_buy" do
|
|
10
|
+
on :stock, symbol: "AAPL"
|
|
11
|
+
|
|
12
|
+
perform do |facts, bindings|
|
|
13
|
+
stock = facts.find { |f| f.type == :stock }
|
|
14
|
+
puts "🚀 MOMENTUM SIGNAL: #{stock[:symbol]}"
|
|
15
|
+
puts " Price: $#{stock[:price]}"
|
|
16
|
+
puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
17
|
+
puts " Recommendation: BUY"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Rule 2: High volume alert
|
|
22
|
+
rule "high_volume" do
|
|
23
|
+
on :stock do
|
|
24
|
+
volume greater_than(1000000)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
perform do |facts, bindings|
|
|
28
|
+
stock = facts.find { |f| f.type == :stock }
|
|
29
|
+
puts "📊 HIGH VOLUME ALERT: #{stock[:symbol]}"
|
|
30
|
+
puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
31
|
+
puts " Above 1M shares traded"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Rule 3: Price movement
|
|
36
|
+
rule "price_movement" do
|
|
37
|
+
on :stock do
|
|
38
|
+
price_change satisfies { |p| p && p.abs > 2 }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
perform do |facts, bindings|
|
|
42
|
+
stock = facts.find { |f| f.type == :stock }
|
|
43
|
+
direction = stock[:price_change] > 0 ? "UP" : "DOWN"
|
|
44
|
+
puts "📈 SIGNIFICANT MOVE: #{stock[:symbol]} #{direction}"
|
|
45
|
+
puts " Change: #{stock[:price_change] > 0 ? '+' : ''}#{stock[:price_change]}%"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Rule 4: RSI signals
|
|
50
|
+
rule "rsi_signal" do
|
|
51
|
+
on :stock do
|
|
52
|
+
rsi satisfies { |r| r && (r < 30 || r > 70) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
perform do |facts, bindings|
|
|
56
|
+
stock = facts.find { |f| f.type == :stock }
|
|
57
|
+
condition = stock[:rsi] < 30 ? "OVERSOLD" : "OVERBOUGHT"
|
|
58
|
+
action = stock[:rsi] < 30 ? "BUY" : "SELL"
|
|
59
|
+
puts "⚡ RSI SIGNAL: #{stock[:symbol]} #{condition}"
|
|
60
|
+
puts " RSI: #{stock[:rsi].round(1)}"
|
|
61
|
+
puts " Recommendation: #{action}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Rule 5: Multi-condition golden cross
|
|
66
|
+
rule "golden_cross_complete" do
|
|
67
|
+
on :stock, symbol: "AAPL"
|
|
68
|
+
on :ma_signal, type: "golden_cross"
|
|
69
|
+
|
|
70
|
+
perform do |facts, bindings|
|
|
71
|
+
stock = facts.find { |f| f.type == :stock }
|
|
72
|
+
signal = facts.find { |f| f.type == :ma_signal }
|
|
73
|
+
puts "🌟 GOLDEN CROSS CONFIRMED: #{stock[:symbol]}"
|
|
74
|
+
puts " 50-day MA crossed above 200-day MA"
|
|
75
|
+
puts " Price: $#{stock[:price]}"
|
|
76
|
+
puts " Recommendation: STRONG BUY"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run_scenarios(kb)
|
|
82
|
+
puts "🏦 STOCK TRADING EXPERT SYSTEM"
|
|
83
|
+
puts "=" * 50
|
|
84
|
+
|
|
85
|
+
# Scenario 1: Apple momentum
|
|
86
|
+
puts "\n📊 SCENARIO 1: Apple with High Volume"
|
|
87
|
+
puts "-" * 30
|
|
88
|
+
kb.reset
|
|
89
|
+
kb.fact :stock, {
|
|
90
|
+
symbol: "AAPL",
|
|
91
|
+
price: 185.50,
|
|
92
|
+
volume: 1_500_000,
|
|
93
|
+
price_change: 3.2,
|
|
94
|
+
rsi: 68
|
|
95
|
+
}
|
|
96
|
+
kb.run
|
|
97
|
+
|
|
98
|
+
# Scenario 2: Google big move
|
|
99
|
+
puts "\n📊 SCENARIO 2: Google Big Price Move"
|
|
100
|
+
puts "-" * 30
|
|
101
|
+
kb.reset
|
|
102
|
+
kb.fact :stock, {
|
|
103
|
+
symbol: "GOOGL",
|
|
104
|
+
price: 142.80,
|
|
105
|
+
volume: 800_000,
|
|
106
|
+
price_change: -4.1,
|
|
107
|
+
rsi: 75
|
|
108
|
+
}
|
|
109
|
+
kb.run
|
|
110
|
+
|
|
111
|
+
# Scenario 3: Tesla oversold
|
|
112
|
+
puts "\n📊 SCENARIO 3: Tesla Oversold"
|
|
113
|
+
puts "-" * 30
|
|
114
|
+
kb.reset
|
|
115
|
+
kb.fact :stock, {
|
|
116
|
+
symbol: "TSLA",
|
|
117
|
+
price: 195.40,
|
|
118
|
+
volume: 2_200_000,
|
|
119
|
+
price_change: -1.8,
|
|
120
|
+
rsi: 25
|
|
121
|
+
}
|
|
122
|
+
kb.run
|
|
123
|
+
|
|
124
|
+
# Scenario 4: Apple Golden Cross
|
|
125
|
+
puts "\n📊 SCENARIO 4: Apple Golden Cross"
|
|
126
|
+
puts "-" * 30
|
|
127
|
+
kb.reset
|
|
128
|
+
kb.fact :stock, {
|
|
129
|
+
symbol: "AAPL",
|
|
130
|
+
price: 190.25,
|
|
131
|
+
volume: 1_100_000,
|
|
132
|
+
price_change: 2.1,
|
|
133
|
+
rsi: 55
|
|
134
|
+
}
|
|
135
|
+
kb.fact :ma_signal, {
|
|
136
|
+
symbol: "AAPL",
|
|
137
|
+
type: "golden_cross"
|
|
138
|
+
}
|
|
139
|
+
kb.run
|
|
140
|
+
|
|
141
|
+
# Scenario 5: Multiple signals
|
|
142
|
+
puts "\n📊 SCENARIO 5: NVIDIA Multiple Signals"
|
|
143
|
+
puts "-" * 30
|
|
144
|
+
kb.reset
|
|
145
|
+
kb.fact :stock, {
|
|
146
|
+
symbol: "NVDA",
|
|
147
|
+
price: 425.80,
|
|
148
|
+
volume: 3_500_000,
|
|
149
|
+
price_change: 8.7,
|
|
150
|
+
rsi: 78
|
|
151
|
+
}
|
|
152
|
+
kb.run
|
|
153
|
+
|
|
154
|
+
puts "\n" + "=" * 50
|
|
155
|
+
puts "DEMONSTRATION COMPLETE"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if __FILE__ == $0
|
|
159
|
+
run_scenarios(kb)
|
|
160
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../
|
|
3
|
+
require_relative '../engine'
|
|
4
4
|
require_relative 'memory'
|
|
5
5
|
|
|
6
6
|
module KBS
|
|
7
7
|
module Blackboard
|
|
8
|
-
#
|
|
9
|
-
class Engine <
|
|
8
|
+
# KBS engine integrated with Blackboard memory
|
|
9
|
+
class Engine < KBS::Engine
|
|
10
10
|
attr_reader :blackboard
|
|
11
11
|
|
|
12
12
|
def initialize(db_path: ':memory:', store: nil)
|
data/lib/kbs/blackboard/fact.rb
CHANGED
|
@@ -39,7 +39,7 @@ module KBS
|
|
|
39
39
|
|
|
40
40
|
if value.is_a?(Proc)
|
|
41
41
|
return false unless @attributes[key] && value.call(@attributes[key])
|
|
42
|
-
elsif value.is_a?(Symbol) && value.to_s.
|
|
42
|
+
elsif value.is_a?(Symbol) && value.to_s.end_with?('?')
|
|
43
43
|
next
|
|
44
44
|
else
|
|
45
45
|
return false unless @attributes[key] == value
|
data/lib/kbs/condition.rb
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KBS
|
|
4
|
+
# Reconstruct Ruby source from YARV bytecode.
|
|
5
|
+
#
|
|
6
|
+
# YARV bytecode is a stack machine. We simulate the stack,
|
|
7
|
+
# translating instructions back into Ruby expressions.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# KBS::Decompiler.new(some_proc).decompile # => "proc { |x| x + 1 }"
|
|
11
|
+
# KBS::Decompiler.new(some_lambda).decompile # => "->(x) { x + 1 }"
|
|
12
|
+
# KBS::Decompiler.new(some_proc).decompile_block # => "{ |x| x + 1 }"
|
|
13
|
+
#
|
|
14
|
+
class Decompiler
|
|
15
|
+
def initialize(proc_obj)
|
|
16
|
+
@iseq = RubyVM::InstructionSequence.of(proc_obj)
|
|
17
|
+
@arr = @iseq.to_a
|
|
18
|
+
@locals = @arr[10] # [:a, :b, ...]
|
|
19
|
+
@params = @arr[11] # {lead_num: 2, ...}
|
|
20
|
+
@body = @arr[13] # [1, :EVENT, [:instruction, ...], ...]
|
|
21
|
+
@children = []
|
|
22
|
+
@iseq.each_child { |c| @children << c }
|
|
23
|
+
@child_index = 0
|
|
24
|
+
@lambda = proc_obj.lambda?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def decompile
|
|
28
|
+
expr = decompile_body(@body)
|
|
29
|
+
params = build_params
|
|
30
|
+
if @lambda
|
|
31
|
+
"->(#{params}) { #{expr} }"
|
|
32
|
+
else
|
|
33
|
+
"proc { |#{params}| #{expr} }"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns just the block literal: { |params| body }
|
|
38
|
+
# Useful when the surrounding keyword (perform, satisfies, etc.)
|
|
39
|
+
# is supplied by the caller.
|
|
40
|
+
def decompile_block
|
|
41
|
+
expr = decompile_body(@body)
|
|
42
|
+
params = build_params
|
|
43
|
+
if params.empty?
|
|
44
|
+
"{ #{expr} }"
|
|
45
|
+
else
|
|
46
|
+
"{ |#{params}| #{expr} }"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_params
|
|
53
|
+
count = @params[:lead_num] || 0
|
|
54
|
+
@locals.first(count).join(", ")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decompile_body(body)
|
|
58
|
+
stack = []
|
|
59
|
+
statements = []
|
|
60
|
+
instructions = body.select { |i| i.is_a?(Array) }
|
|
61
|
+
|
|
62
|
+
instructions.each do |inst|
|
|
63
|
+
op = inst[0]
|
|
64
|
+
case op
|
|
65
|
+
when :getlocal_WC_0
|
|
66
|
+
idx = inst[1]
|
|
67
|
+
name = slot_to_name(idx)
|
|
68
|
+
stack.push(name.to_s)
|
|
69
|
+
|
|
70
|
+
when :setlocal_WC_0
|
|
71
|
+
idx = inst[1]
|
|
72
|
+
name = slot_to_name(idx)
|
|
73
|
+
val = stack.pop
|
|
74
|
+
statements << "#{name} = #{val}"
|
|
75
|
+
|
|
76
|
+
when :putobject
|
|
77
|
+
stack.push(inst[1].inspect)
|
|
78
|
+
|
|
79
|
+
when :putobject_INT2FIX_0_
|
|
80
|
+
stack.push("0")
|
|
81
|
+
|
|
82
|
+
when :putobject_INT2FIX_1_
|
|
83
|
+
stack.push("1")
|
|
84
|
+
|
|
85
|
+
when :putself
|
|
86
|
+
stack.push("self")
|
|
87
|
+
|
|
88
|
+
when :putnil
|
|
89
|
+
stack.push("nil")
|
|
90
|
+
|
|
91
|
+
when :putchilledstring, :putstring
|
|
92
|
+
stack.push(inst[1].inspect)
|
|
93
|
+
|
|
94
|
+
when :opt_plus, :opt_minus, :opt_mult, :opt_div, :opt_mod,
|
|
95
|
+
:opt_eq, :opt_neq, :opt_lt, :opt_le, :opt_gt, :opt_ge,
|
|
96
|
+
:opt_ltlt
|
|
97
|
+
op_sym = inst[1][:mid]
|
|
98
|
+
b = stack.pop
|
|
99
|
+
a = stack.pop
|
|
100
|
+
stack.push("#{a} #{op_sym} #{b}")
|
|
101
|
+
|
|
102
|
+
when :opt_send_without_block
|
|
103
|
+
calldata = inst[1]
|
|
104
|
+
method = calldata[:mid]
|
|
105
|
+
argc = calldata[:orig_argc]
|
|
106
|
+
args = stack.pop(argc)
|
|
107
|
+
receiver = stack.pop
|
|
108
|
+
call_str = format_call(receiver, method, args)
|
|
109
|
+
stack.push(call_str)
|
|
110
|
+
|
|
111
|
+
when :send
|
|
112
|
+
calldata = inst[1]
|
|
113
|
+
method = calldata[:mid]
|
|
114
|
+
argc = calldata[:orig_argc]
|
|
115
|
+
args = stack.pop(argc)
|
|
116
|
+
receiver = stack.pop
|
|
117
|
+
|
|
118
|
+
child_iseq = @children[@child_index]
|
|
119
|
+
@child_index += 1
|
|
120
|
+
child_src = decompile_child(child_iseq)
|
|
121
|
+
|
|
122
|
+
base = format_call(receiver, method, args)
|
|
123
|
+
stack.push("#{base} { #{child_src} }")
|
|
124
|
+
|
|
125
|
+
when :newarray
|
|
126
|
+
count = inst[1]
|
|
127
|
+
items = stack.pop(count)
|
|
128
|
+
stack.push("[#{items.join(', ')}]")
|
|
129
|
+
|
|
130
|
+
when :newhash
|
|
131
|
+
count = inst[1]
|
|
132
|
+
pairs = stack.pop(count)
|
|
133
|
+
entries = pairs.each_slice(2).map { |k, v| "#{k} => #{v}" }
|
|
134
|
+
stack.push("{ #{entries.join(', ')} }")
|
|
135
|
+
|
|
136
|
+
when :branchunless
|
|
137
|
+
condition = stack.pop
|
|
138
|
+
remaining = instructions[instructions.index(inst) + 1..]
|
|
139
|
+
true_val = extract_value(remaining, 0)
|
|
140
|
+
false_val = extract_value(remaining, 1)
|
|
141
|
+
if true_val && false_val
|
|
142
|
+
stack.push("#{condition} ? #{true_val} : #{false_val}")
|
|
143
|
+
break
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
when :leave
|
|
147
|
+
# ignore
|
|
148
|
+
|
|
149
|
+
when :pop
|
|
150
|
+
val = stack.pop
|
|
151
|
+
statements << val if val
|
|
152
|
+
|
|
153
|
+
else
|
|
154
|
+
stack.push("/* #{op} */")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
all = statements + [stack.last].compact
|
|
159
|
+
all.join("; ")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def format_call(receiver, method, args)
|
|
163
|
+
prefix = (receiver == "self") ? "" : "#{receiver}."
|
|
164
|
+
if args.empty?
|
|
165
|
+
"#{prefix}#{method}"
|
|
166
|
+
else
|
|
167
|
+
"#{prefix}#{method}(#{args.join(', ')})"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def decompile_child(child_iseq)
|
|
172
|
+
child_arr = child_iseq.to_a
|
|
173
|
+
child_locals = child_arr[10]
|
|
174
|
+
child_params = child_arr[11]
|
|
175
|
+
child_body = child_arr[13]
|
|
176
|
+
count = child_params[:lead_num] || 0
|
|
177
|
+
param_names = child_locals.first(count).join(", ")
|
|
178
|
+
|
|
179
|
+
saved = [@locals, @params, @body]
|
|
180
|
+
@locals, @params, @body = child_locals, child_params, child_body
|
|
181
|
+
expr = decompile_body(child_body)
|
|
182
|
+
@locals, @params, @body = saved
|
|
183
|
+
|
|
184
|
+
if count > 0
|
|
185
|
+
"|#{param_names}| #{expr}"
|
|
186
|
+
else
|
|
187
|
+
expr
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def extract_value(instructions, index)
|
|
192
|
+
values = instructions.select { |i|
|
|
193
|
+
i.is_a?(Array) && (i[0] == :putchilledstring || i[0] == :putstring || i[0] == :putobject)
|
|
194
|
+
}
|
|
195
|
+
val = values[index]
|
|
196
|
+
val[1].inspect if val
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def slot_to_name(slot_idx)
|
|
200
|
+
name_idx = @locals.size + 2 - slot_idx
|
|
201
|
+
@locals[name_idx] || "?local_#{slot_idx}"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -6,12 +6,14 @@ module KBS
|
|
|
6
6
|
attr_reader :engine, :rules
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
|
-
@engine =
|
|
9
|
+
@engine = Engine.new
|
|
10
10
|
@rules = {}
|
|
11
11
|
@rule_builders = {}
|
|
12
|
+
@rule_source_locations = {}
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def rule(name, &block)
|
|
16
|
+
@rule_source_locations[name] = block.source_location if block
|
|
15
17
|
builder = RuleBuilder.new(name)
|
|
16
18
|
builder.instance_eval(&block) if block_given?
|
|
17
19
|
@rule_builders[name] = builder
|
|
@@ -42,7 +44,7 @@ module KBS
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def reset
|
|
45
|
-
@engine.
|
|
47
|
+
@engine.reset
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def facts
|
|
@@ -65,6 +67,30 @@ module KBS
|
|
|
65
67
|
puts "-" * 40
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def rule_source(name)
|
|
71
|
+
# Try file-based source first
|
|
72
|
+
if (location = @rule_source_locations[name])
|
|
73
|
+
file, line = location
|
|
74
|
+
if file && File.exist?(file)
|
|
75
|
+
source = extract_rule_source(file, line)
|
|
76
|
+
return source if source
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fall back to reconstruction from internal state
|
|
81
|
+
reconstruct_rule_source(name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def print_rule_source(name)
|
|
85
|
+
source = rule_source(name)
|
|
86
|
+
unless source
|
|
87
|
+
puts "No source available for rule '#{name}'"
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
puts source
|
|
92
|
+
end
|
|
93
|
+
|
|
68
94
|
def print_rules
|
|
69
95
|
puts "Knowledge Base Rules:"
|
|
70
96
|
puts "-" * 40
|
|
@@ -81,6 +107,79 @@ module KBS
|
|
|
81
107
|
end
|
|
82
108
|
puts "-" * 40
|
|
83
109
|
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def reconstruct_rule_source(name)
|
|
114
|
+
builder = @rule_builders[name]
|
|
115
|
+
return nil unless builder
|
|
116
|
+
|
|
117
|
+
lines = []
|
|
118
|
+
lines << "rule #{name.inspect} do"
|
|
119
|
+
lines << " desc #{builder.description.inspect}" if builder.description
|
|
120
|
+
lines << " priority #{builder.priority}" if builder.priority != 0
|
|
121
|
+
|
|
122
|
+
builder.conditions.each do |cond|
|
|
123
|
+
keyword = cond.negated ? "without" : "on"
|
|
124
|
+
pattern_str = reconstruct_pattern(cond.pattern)
|
|
125
|
+
if pattern_str.empty?
|
|
126
|
+
lines << " #{keyword} #{cond.type.inspect}"
|
|
127
|
+
else
|
|
128
|
+
lines << " #{keyword} #{cond.type.inspect}, #{pattern_str}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if builder.action_block
|
|
133
|
+
block_str = decompile_proc_block(builder.action_block)
|
|
134
|
+
lines << " perform #{block_str}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
lines << "end"
|
|
138
|
+
lines.join("\n")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def reconstruct_pattern(pattern)
|
|
142
|
+
return "" if pattern.empty?
|
|
143
|
+
|
|
144
|
+
pattern.map do |key, value|
|
|
145
|
+
val_str = if value.is_a?(Proc)
|
|
146
|
+
Decompiler.new(value).decompile
|
|
147
|
+
else
|
|
148
|
+
value.inspect
|
|
149
|
+
end
|
|
150
|
+
"#{key}: #{val_str}"
|
|
151
|
+
end.join(", ")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def decompile_proc_block(proc_obj)
|
|
155
|
+
Decompiler.new(proc_obj).decompile_block
|
|
156
|
+
rescue => e
|
|
157
|
+
"{ <decompilation failed: #{e.message}> }"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_rule_source(file, start_line)
|
|
161
|
+
lines = File.readlines(file)
|
|
162
|
+
start_idx = start_line - 1
|
|
163
|
+
return nil if start_idx < 0 || start_idx >= lines.length
|
|
164
|
+
|
|
165
|
+
# source_location points to the block's `do` line, which is
|
|
166
|
+
# normally the same line as the `rule` call. Walk back up to
|
|
167
|
+
# 2 lines in case they are on separate lines.
|
|
168
|
+
rule_idx = start_idx.downto([start_idx - 2, 0].max).find do |i|
|
|
169
|
+
lines[i].match?(/\b(?:rule|defrule)\b/)
|
|
170
|
+
end || start_idx
|
|
171
|
+
|
|
172
|
+
base_indent = lines[rule_idx][/^\s*/].length
|
|
173
|
+
end_pattern = /^\s{#{base_indent}}end\b/
|
|
174
|
+
|
|
175
|
+
result = []
|
|
176
|
+
(rule_idx...lines.length).each do |i|
|
|
177
|
+
result << lines[i]
|
|
178
|
+
break if i > rule_idx && lines[i].match?(end_pattern)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
result.join.chomp
|
|
182
|
+
end
|
|
84
183
|
end
|
|
85
184
|
end
|
|
86
185
|
end
|
data/lib/kbs/dsl/variable.rb
CHANGED
data/lib/kbs/dsl.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module KBS
|
|
4
|
-
class
|
|
4
|
+
class Engine
|
|
5
5
|
attr_reader :working_memory, :rules, :alpha_memories, :production_nodes
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
@@ -53,6 +53,47 @@ module KBS
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Clear all transient RETE state while preserving the compiled rule network.
|
|
57
|
+
#
|
|
58
|
+
# The RETE network has four levels of transient state:
|
|
59
|
+
# 1. WorkingMemory — holds all asserted facts
|
|
60
|
+
# 2. AlphaMemory — holds facts matching each pattern
|
|
61
|
+
# 3. BetaMemory — holds tokens from condition joins
|
|
62
|
+
# 4. ProductionNode — holds tokens ready to fire
|
|
63
|
+
#
|
|
64
|
+
# Simply clearing working memory facts doesn't cascade reliably
|
|
65
|
+
# through intermediate beta memories. Stale tokens in beta memories
|
|
66
|
+
# cause false matches when new facts are asserted on the next cycle.
|
|
67
|
+
#
|
|
68
|
+
# This method clears all four levels directly while preserving the
|
|
69
|
+
# root beta memory's dummy token (needed for first-condition joins).
|
|
70
|
+
def reset
|
|
71
|
+
# 1. Clear working memory directly (bypass observer — we clear everything)
|
|
72
|
+
@working_memory.facts.clear
|
|
73
|
+
|
|
74
|
+
# 2. Clear alpha memories and their downstream beta memories
|
|
75
|
+
@alpha_memories.each_value do |alpha_mem|
|
|
76
|
+
alpha_mem.items.clear
|
|
77
|
+
|
|
78
|
+
# Each alpha memory successor is a JoinNode or NegationNode.
|
|
79
|
+
# Their successors are intermediate BetaMemory nodes.
|
|
80
|
+
# The root beta memory is never a join node successor, so
|
|
81
|
+
# its dummy token is preserved.
|
|
82
|
+
alpha_mem.successors.each do |join_node|
|
|
83
|
+
next unless join_node.respond_to?(:successors)
|
|
84
|
+
join_node.successors.each do |beta_or_prod|
|
|
85
|
+
beta_or_prod.tokens.clear if beta_or_prod.respond_to?(:tokens)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 3. Clear production node tokens
|
|
91
|
+
@production_nodes.each_value { |node| node.tokens.clear }
|
|
92
|
+
|
|
93
|
+
# 4. Clear stale child references from the root dummy token
|
|
94
|
+
@root_beta_memory.tokens.each { |t| t.children.clear }
|
|
95
|
+
end
|
|
96
|
+
|
|
56
97
|
private
|
|
57
98
|
|
|
58
99
|
def build_network_for_rule(rule)
|
data/lib/kbs/fact.rb
CHANGED
|
@@ -26,7 +26,7 @@ module KBS
|
|
|
26
26
|
|
|
27
27
|
if value.is_a?(Proc)
|
|
28
28
|
return false unless @attributes[key] && value.call(@attributes[key])
|
|
29
|
-
elsif value.is_a?(Symbol) && value.to_s.
|
|
29
|
+
elsif value.is_a?(Symbol) && value.to_s.end_with?('?')
|
|
30
30
|
next
|
|
31
31
|
else
|
|
32
32
|
return false unless @attributes[key] == value
|
data/lib/kbs/version.rb
CHANGED
data/lib/kbs.rb
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'kbs/version'
|
|
4
4
|
|
|
5
|
-
# Core RETE
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
15
|
-
require_relative
|
|
16
|
-
require_relative
|
|
5
|
+
# Core RETE classes
|
|
6
|
+
require_relative 'kbs/fact'
|
|
7
|
+
require_relative 'kbs/working_memory'
|
|
8
|
+
require_relative 'kbs/token'
|
|
9
|
+
require_relative 'kbs/alpha_memory'
|
|
10
|
+
require_relative 'kbs/beta_memory'
|
|
11
|
+
require_relative 'kbs/join_node'
|
|
12
|
+
require_relative 'kbs/negation_node'
|
|
13
|
+
require_relative 'kbs/production_node'
|
|
14
|
+
require_relative 'kbs/condition'
|
|
15
|
+
require_relative 'kbs/rule'
|
|
16
|
+
require_relative 'kbs/engine'
|
|
17
|
+
require_relative 'kbs/decompiler'
|
|
18
|
+
require_relative 'kbs/dsl'
|
|
17
19
|
|
|
18
20
|
module KBS
|
|
19
21
|
class Error < StandardError; end
|