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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +291 -362
  5. data/docs/advanced/custom-persistence.md +775 -0
  6. data/docs/advanced/debugging.md +726 -0
  7. data/docs/advanced/index.md +8 -0
  8. data/docs/advanced/performance.md +865 -0
  9. data/docs/advanced/testing.md +827 -0
  10. data/docs/api/blackboard.md +1157 -0
  11. data/docs/api/engine.md +1047 -0
  12. data/docs/api/facts.md +1212 -0
  13. data/docs/api/index.md +12 -0
  14. data/docs/api/rules.md +1104 -0
  15. data/docs/architecture/blackboard.md +544 -0
  16. data/docs/architecture/index.md +277 -0
  17. data/docs/architecture/network-structure.md +343 -0
  18. data/docs/architecture/rete-algorithm.md +737 -0
  19. data/docs/assets/css/custom.css +83 -0
  20. data/docs/assets/images/blackboard-architecture.svg +136 -0
  21. data/docs/assets/images/compiled-network.svg +101 -0
  22. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  23. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  24. data/docs/assets/images/fact-structure.svg +42 -0
  25. data/docs/assets/images/inference-cycle.svg +47 -0
  26. data/docs/assets/images/kb-components.svg +43 -0
  27. data/docs/assets/images/kbs.jpg +0 -0
  28. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  29. data/docs/assets/images/rete-network-layers.svg +96 -0
  30. data/docs/assets/images/rule-structure.svg +44 -0
  31. data/docs/assets/images/system-layers.svg +69 -0
  32. data/docs/assets/images/trading-signal-network.svg +139 -0
  33. data/docs/assets/js/mathjax.js +17 -0
  34. data/docs/examples/index.md +223 -0
  35. data/docs/guides/blackboard-memory.md +589 -0
  36. data/docs/guides/dsl.md +1321 -0
  37. data/docs/guides/facts.md +652 -0
  38. data/docs/guides/getting-started.md +385 -0
  39. data/docs/guides/index.md +23 -0
  40. data/docs/guides/negation.md +529 -0
  41. data/docs/guides/pattern-matching.md +561 -0
  42. data/docs/guides/persistence.md +451 -0
  43. data/docs/guides/variable-binding.md +491 -0
  44. data/docs/guides/writing-rules.md +914 -0
  45. data/docs/index.md +155 -0
  46. data/docs/installation.md +156 -0
  47. data/docs/quick-start.md +221 -0
  48. data/docs/what-is-a-fact.md +694 -0
  49. data/docs/what-is-a-knowledge-base.md +350 -0
  50. data/docs/what-is-a-rule.md +833 -0
  51. data/examples/.gitignore +1 -0
  52. data/examples/README.md +2 -2
  53. data/examples/advanced_example.rb +2 -2
  54. data/examples/advanced_example_dsl.rb +224 -0
  55. data/examples/ai_enhanced_kbs.rb +1 -1
  56. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  57. data/examples/blackboard_demo_dsl.rb +50 -0
  58. data/examples/car_diagnostic.rb +1 -1
  59. data/examples/car_diagnostic_dsl.rb +54 -0
  60. data/examples/concurrent_inference_demo.rb +5 -6
  61. data/examples/concurrent_inference_demo_dsl.rb +362 -0
  62. data/examples/csv_trading_system.rb +1 -1
  63. data/examples/csv_trading_system_dsl.rb +525 -0
  64. data/examples/iot_demo_using_dsl.rb +1 -1
  65. data/examples/portfolio_rebalancing_system.rb +2 -2
  66. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  67. data/examples/redis_trading_demo_dsl.rb +177 -0
  68. data/examples/rule_source_demo.rb +123 -0
  69. data/examples/run_all.rb +50 -0
  70. data/examples/run_all_dsl.rb +49 -0
  71. data/examples/stock_trading_advanced.rb +1 -1
  72. data/examples/stock_trading_advanced_dsl.rb +404 -0
  73. data/examples/temp_dsl.txt +9392 -0
  74. data/examples/timestamped_trading.rb +1 -1
  75. data/examples/timestamped_trading_dsl.rb +258 -0
  76. data/examples/trading_demo.rb +1 -1
  77. data/examples/trading_demo_dsl.rb +322 -0
  78. data/examples/working_demo.rb +1 -1
  79. data/examples/working_demo_dsl.rb +160 -0
  80. data/lib/kbs/blackboard/engine.rb +3 -3
  81. data/lib/kbs/blackboard/fact.rb +1 -1
  82. data/lib/kbs/condition.rb +1 -1
  83. data/lib/kbs/decompiler.rb +204 -0
  84. data/lib/kbs/dsl/knowledge_base.rb +101 -2
  85. data/lib/kbs/dsl/variable.rb +1 -1
  86. data/lib/kbs/dsl.rb +3 -1
  87. data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
  88. data/lib/kbs/fact.rb +1 -1
  89. data/lib/kbs/version.rb +1 -1
  90. data/lib/kbs.rb +15 -13
  91. data/mkdocs.yml +181 -0
  92. metadata +74 -9
  93. data/examples/stock_trading_system.rb.bak +0 -563
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs/blackboard'
4
+
5
+ puts "Redis-Backed High-Frequency Trading System"
6
+ puts "=" * 70
7
+
8
+ # Demo 1: Pure Redis Store (fast, in-memory)
9
+ puts "\n=== Pure Redis Store Demo ==="
10
+ puts "Fast in-memory fact storage with Redis"
11
+
12
+ begin
13
+ redis_store = KBS::Blackboard::Persistence::RedisStore.new(
14
+ url: 'redis://localhost:6379/1' # Use DB 1 for demo
15
+ )
16
+
17
+ # Clear previous demo data
18
+ redis_store.redis.flushdb
19
+ puts "(Cleared previous Redis data)"
20
+
21
+ engine = KBS::Blackboard::Engine.new(store: redis_store)
22
+
23
+ # Add trading rules (raw API since Blackboard::Engine doesn't use DSL)
24
+ spread_rule = KBS::Rule.new('tight_spread_opportunity') do |r|
25
+ r.conditions << KBS::Condition.new(:market_price, { symbol: 'AAPL' })
26
+ r.conditions << KBS::Condition.new(:order, { symbol: 'AAPL', type: 'BUY' })
27
+ r.action = ->(facts) {
28
+ price = facts.find { |f| f.type == :market_price }
29
+ order = facts.find { |f| f.type == :order }
30
+ spread = 0.02 # Tight spread
31
+ puts "\n💹 TRADING SIGNAL: Tight spread opportunity for #{price[:symbol]}"
32
+ puts " Price: $#{price[:price]}, Spread: $#{spread}"
33
+ puts " Order: #{order[:type]} #{order[:quantity]} @ $#{order[:limit]}"
34
+ }
35
+ end
36
+
37
+ high_volume_rule = KBS::Rule.new('high_volume_alert') do |r|
38
+ r.conditions << KBS::Condition.new(:market_price, { volume: ->(v) { v > 800 } })
39
+ r.action = ->(facts) {
40
+ price = facts.first
41
+ puts "\n📊 HIGH VOLUME: #{price[:symbol]} trading at #{price[:volume]} shares"
42
+ }
43
+ end
44
+
45
+ engine.add_rule(spread_rule)
46
+ engine.add_rule(high_volume_rule)
47
+
48
+ puts "\nAdding high-frequency market data..."
49
+ price1 = engine.add_fact(:market_price, { symbol: "AAPL", price: 150.25, volume: 1000 })
50
+ price2 = engine.add_fact(:market_price, { symbol: "GOOGL", price: 2800.50, volume: 500 })
51
+ order = engine.add_fact(:order, { symbol: "AAPL", type: "BUY", quantity: 100, limit: 150.00 })
52
+
53
+ puts "Facts added with UUIDs:"
54
+ puts " AAPL Price: #{price1.uuid}"
55
+ puts " GOOGL Price: #{price2.uuid}"
56
+ puts " Order: #{order.uuid}"
57
+
58
+ puts "\nRunning inference engine..."
59
+ engine.run
60
+
61
+ puts "\nPosting trading messages..."
62
+ engine.post_message("MarketDataFeed", "prices", { symbol: "AAPL", bid: 150.24, ask: 150.26 }, priority: 10)
63
+ engine.post_message("OrderManager", "orders", { action: "fill", order_id: order.uuid }, priority: 5)
64
+
65
+ puts "\nConsuming highest priority message..."
66
+ message = engine.consume_message("prices", "TradingStrategy")
67
+ puts " Received: #{message[:content]}" if message
68
+
69
+ puts "\nQuerying market prices..."
70
+ prices = engine.blackboard.get_facts(:market_price)
71
+ puts " Found #{prices.size} price(s):"
72
+ prices.each { |p| puts " - #{p}" }
73
+
74
+ puts "\nRedis Statistics:"
75
+ stats = engine.stats
76
+ stats.each do |key, value|
77
+ puts " #{key.to_s.gsub('_', ' ').capitalize}: #{value}"
78
+ end
79
+
80
+ engine.blackboard.close
81
+
82
+ rescue Redis::CannotConnectError => e
83
+ puts "\n⚠️ Redis not available: #{e.message}"
84
+ puts " Please start Redis: redis-server"
85
+ puts " Or install Redis: brew install redis (macOS)"
86
+ end
87
+
88
+ # Demo 2: Hybrid Store (Redis + SQLite)
89
+ puts "\n\n=== Hybrid Store Demo ==="
90
+ puts "Redis for fast fact access + SQLite for durable audit trail"
91
+
92
+ begin
93
+ hybrid_store = KBS::Blackboard::Persistence::HybridStore.new(
94
+ redis_url: 'redis://localhost:6379/2', # Use DB 2 for demo
95
+ db_path: ':memory:' # In-memory SQLite for demo
96
+ )
97
+
98
+ # Clear previous demo data from Redis
99
+ hybrid_store.redis_store.redis.flushdb
100
+ puts "(Cleared previous Redis data)"
101
+
102
+ engine = KBS::Blackboard::Engine.new(store: hybrid_store)
103
+
104
+ # Add monitoring rules (raw API since Blackboard::Engine doesn't use DSL)
105
+ temp_alert = KBS::Rule.new('temperature_alert') do |r|
106
+ r.conditions << KBS::Condition.new(:sensor, {
107
+ type: 'temperature',
108
+ value: ->(v) { v > 25 }
109
+ })
110
+ r.action = ->(facts) {
111
+ sensor = facts.first
112
+ puts "\n🌡️ TEMPERATURE ALERT: #{sensor[:location]} at #{sensor[:value]}°C (threshold: 25°C)"
113
+ }
114
+ end
115
+
116
+ cpu_warning = KBS::Rule.new('cpu_warning') do |r|
117
+ r.conditions << KBS::Condition.new(:sensor, {
118
+ type: 'cpu_usage',
119
+ value: ->(v) { v > 40 }
120
+ })
121
+ r.action = ->(facts) {
122
+ sensor = facts.first
123
+ puts "\n⚙️ CPU WARNING: #{sensor[:location]} at #{sensor[:value]}% (threshold: 40%)"
124
+ }
125
+ end
126
+
127
+ engine.add_rule(temp_alert)
128
+ engine.add_rule(cpu_warning)
129
+
130
+ puts "\nAdding facts (stored in Redis)..."
131
+ sensor1 = engine.add_fact(:sensor, { location: "trading_floor", type: "temperature", value: 22 })
132
+ sensor2 = engine.add_fact(:sensor, { location: "data_center", type: "cpu_usage", value: 45 })
133
+
134
+ puts "Facts added:"
135
+ puts " Sensor 1: #{sensor1.uuid}"
136
+ puts " Sensor 2: #{sensor2.uuid}"
137
+
138
+ puts "\nRunning inference engine..."
139
+ engine.run
140
+
141
+ puts "\nUpdating sensor value (audit logged to SQLite)..."
142
+ sensor1[:value] = 28
143
+
144
+ puts "\nRunning inference again after update..."
145
+ engine.run
146
+
147
+ puts "\nFact History from SQLite Audit Log:"
148
+ history = engine.blackboard.get_history(limit: 5)
149
+ history.each do |entry|
150
+ puts " [#{entry[:timestamp].strftime('%H:%M:%S')}] #{entry[:action]}: #{entry[:fact_type]}(#{entry[:attributes]})"
151
+ end
152
+
153
+ puts "\nHybrid Store Benefits:"
154
+ puts " ✓ Facts in Redis (fast reads/writes)"
155
+ puts " ✓ Messages in Redis (real-time messaging)"
156
+ puts " ✓ Audit trail in SQLite (durable, queryable)"
157
+ puts " ✓ Best of both worlds for production systems"
158
+
159
+ puts "\nStatistics:"
160
+ stats = engine.stats
161
+ stats.each do |key, value|
162
+ puts " #{key.to_s.gsub('_', ' ').capitalize}: #{value}"
163
+ end
164
+
165
+ engine.blackboard.close
166
+
167
+ rescue Redis::CannotConnectError => e
168
+ puts "\n⚠️ Redis not available: #{e.message}"
169
+ puts " Hybrid store requires Redis for fact storage"
170
+ end
171
+
172
+ puts "\n" + "=" * 70
173
+ puts "Demo complete!"
174
+ puts "\nComparison:"
175
+ puts " SQLite Store: Durable, transactional, embedded (no server needed)"
176
+ puts " Redis Store: Fast (100x), distributed, requires Redis server"
177
+ puts " Hybrid Store: Fast facts + durable audit (production recommended)"
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs'
4
+
5
+ puts "Rule Source Introspection Demo"
6
+ puts "=" * 60
7
+ puts
8
+ puts "KBS can show the source of any rule — whether it was defined"
9
+ puts "in a file (exact source) or built dynamically at runtime"
10
+ puts "(reconstructed from bytecode via KBS::Decompiler)."
11
+ puts
12
+
13
+ # ── Part 1: File-defined rules ───────────────────────────────
14
+
15
+ puts "PART 1: File-Defined Rules"
16
+ puts "-" * 60
17
+ puts
18
+
19
+ kb = KBS.knowledge_base do
20
+ rule "dead_battery" do
21
+ on :symptom, problem: "won't start"
22
+ on :symptom, problem: "no lights"
23
+
24
+ perform do |facts, bindings|
25
+ puts "DIAGNOSIS: Dead battery"
26
+ puts "RECOMMENDATION: Replace or jump-start the battery"
27
+ end
28
+ end
29
+
30
+ rule "overheating" do
31
+ on :symptom, problem: "high temperature"
32
+ on :symptom, problem: "steam from hood"
33
+ without :symptom, problem: "coolant leak"
34
+
35
+ perform do |facts, bindings|
36
+ puts "DIAGNOSIS: Engine overheating (no coolant leak)"
37
+ puts "RECOMMENDATION: Check radiator and cooling system"
38
+ end
39
+ end
40
+ end
41
+
42
+ kb.rules.each_key do |name|
43
+ puts "Source for '#{name}':"
44
+ puts
45
+ kb.print_rule_source(name)
46
+ puts
47
+ end
48
+
49
+ # ── Part 2: Dynamically-created rules ────────────────────────
50
+
51
+ puts
52
+ puts "PART 2: Dynamically-Created Rules"
53
+ puts "-" * 60
54
+ puts
55
+ puts "These rules have no source file — KBS reconstructs them"
56
+ puts "from their internal state using YARV bytecode decompilation."
57
+ puts
58
+
59
+ # Simulate what an AI agent framework might do: build rules at
60
+ # runtime from configuration data, user input, or LLM output.
61
+
62
+ dynamic_kb = KBS::DSL::KnowledgeBase.new
63
+
64
+ # Rule with lambda condition helpers
65
+ builder = KBS::DSL::RuleBuilder.new("high_temp_alert")
66
+ builder.desc "Fire when temperature exceeds safe threshold"
67
+ builder.priority 5
68
+ builder.on :sensor, location: "server_room", temp: ->(v) { v > 85 }
69
+ builder.without :alert, type: :cooling_active
70
+ builder.perform { |facts| puts "ALERT: Server room temperature critical!" }
71
+
72
+ rule = builder.build
73
+ dynamic_kb.instance_variable_get(:@rule_builders)["high_temp_alert"] = builder
74
+ dynamic_kb.instance_variable_get(:@rules)["high_temp_alert"] = rule
75
+ dynamic_kb.engine.add_rule(rule)
76
+
77
+ # Rule with multiple proc-based conditions
78
+ builder2 = KBS::DSL::RuleBuilder.new("buy_signal")
79
+ builder2.on :stock, price: ->(v) { v < 150 }, volume: ->(v) { v > 1_000_000 }
80
+ builder2.without :position, status: :open
81
+ builder2.perform { |facts| puts "BUY signal triggered" }
82
+
83
+ rule2 = builder2.build
84
+ dynamic_kb.instance_variable_get(:@rule_builders)["buy_signal"] = builder2
85
+ dynamic_kb.instance_variable_get(:@rules)["buy_signal"] = rule2
86
+ dynamic_kb.engine.add_rule(rule2)
87
+
88
+ ["high_temp_alert", "buy_signal"].each do |name|
89
+ puts "Reconstructed source for '#{name}':"
90
+ puts
91
+ dynamic_kb.print_rule_source(name)
92
+ puts
93
+ end
94
+
95
+ # ── Part 3: Decompiler standalone ─────────────────────────────
96
+
97
+ puts
98
+ puts "PART 3: KBS::Decompiler Standalone"
99
+ puts "-" * 60
100
+ puts
101
+ puts "The decompiler works on any Proc or Lambda:"
102
+ puts
103
+
104
+ examples = {
105
+ "lambda" => ->(x) { x * 2 + 1 },
106
+ "proc" => proc { |a, b| a > b },
107
+ "complex" => ->(items) { items.select { |x| x > 0 }.map { |x| x * 2 } },
108
+ }
109
+
110
+ examples.each do |label, pr|
111
+ puts " #{label}:"
112
+ puts " decompile => #{KBS::Decompiler.new(pr).decompile}"
113
+ puts " decompile_block => #{KBS::Decompiler.new(pr).decompile_block}"
114
+ puts
115
+ end
116
+
117
+ # ── Part 4: Unknown rule ──────────────────────────────────────
118
+
119
+ puts "PART 4: Unknown Rule"
120
+ puts "-" * 60
121
+ puts
122
+ kb.print_rule_source("nonexistent")
123
+ puts " rule_source returns: #{kb.rule_source('nonexistent').inspect}"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run all example files in the examples directory
5
+
6
+ require 'pathname'
7
+
8
+ # Get the directory where this script is located
9
+ examples_dir = Pathname.new(__FILE__).dirname
10
+
11
+ # Find all Ruby files except this one
12
+ example_files = Dir.glob(examples_dir.join('*.rb'))
13
+ .reject { |f| File.basename(f).start_with? 'run_all' }
14
+ .reject { |f| File.basename(f).end_with? '_dsl.rb' }
15
+ .sort
16
+
17
+ puts
18
+ puts "Running #{example_files.size} examples from #{examples_dir}"
19
+ puts
20
+ puts
21
+
22
+ example_files.each_with_index do |file, index|
23
+ filename = File.basename(file)
24
+ filename_size = filename.size + 6
25
+
26
+ STDERR.puts "Running example #{index + 1}/#{example_files.size}: #{filename} ..."
27
+
28
+ puts
29
+ puts "=" * filename_size
30
+ puts "## #{filename} ##"
31
+ puts "=" * filename_size
32
+ puts
33
+
34
+ # Run the example
35
+ system("ruby", file)
36
+
37
+ exit_status = $?.exitstatus
38
+
39
+ if exit_status != 0
40
+ puts
41
+ puts "⚠️ Example #{filename} exited with status #{exit_status}"
42
+ end
43
+
44
+ puts
45
+ end
46
+
47
+ puts
48
+ puts
49
+ puts "Completed running all #{example_files.size} examples"
50
+ puts
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run all example DSL files in the examples directory
5
+
6
+ require 'pathname'
7
+
8
+ # Get the directory where this script is located
9
+ examples_dir = Pathname.new(__FILE__).dirname
10
+
11
+ # Find all Ruby files except this one
12
+ example_files = Dir.glob(examples_dir.join('*_dsl.rb'))
13
+ .reject { |f| File.basename(f).start_with? 'run_all' }
14
+ .sort
15
+
16
+ puts
17
+ puts "Running #{example_files.size} examples from #{examples_dir}"
18
+ puts
19
+ puts
20
+
21
+ example_files.each_with_index do |file, index|
22
+ filename = File.basename(file)
23
+ filename_size = filename.size + 6
24
+
25
+ STDERR.puts "Running example #{index + 1}/#{example_files.size}: #{filename} ..."
26
+
27
+ puts
28
+ puts "=" * filename_size
29
+ puts "## #{filename} ##"
30
+ puts "=" * filename_size
31
+ puts
32
+
33
+ # Run the example
34
+ system("ruby", file)
35
+
36
+ exit_status = $?.exitstatus
37
+
38
+ if exit_status != 0
39
+ puts
40
+ puts "⚠️ Example #{filename} exited with status #{exit_status}"
41
+ end
42
+
43
+ puts
44
+ end
45
+
46
+ puts
47
+ puts
48
+ puts "Completed running all #{example_files.size} examples"
49
+ puts
@@ -4,7 +4,7 @@ require_relative '../lib/kbs'
4
4
 
5
5
  class AdvancedStockTradingSystem
6
6
  def initialize
7
- @engine = KBS::ReteEngine.new
7
+ @engine = KBS::Engine.new
8
8
  @portfolio = {
9
9
  cash: 100000,
10
10
  positions: {},