lex-bayesian-belief 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 770dcdd91a8320f7de43996caf8d36e8e0e35a89c52c4ddd9995417afc6d1e1d
4
+ data.tar.gz: a700d35695dd6cf00adca507441852d0dec89a0ea77bbd9683d0442b4782ca62
5
+ SHA512:
6
+ metadata.gz: 62ddebb2896d4fc62499706ca323ab44b96fd8a6aa69cfcb4959b5820600dacced757d9b0b62a5f1c1baab9b92caba362cc91b5937113aba96e2315087a18acb
7
+ data.tar.gz: 283e62186e910f6e4d07a2a5a81b91f19e49d64f8a703261f15ff6217ee474e16d286f39fbd28f171e3cf1a21cc0c4e8c1074019e04bf69b670666434961817f
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/bayesian_belief/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-bayesian-belief'
7
+ spec.version = Legion::Extensions::BayesianBelief::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Bayesian Belief'
12
+ spec.description = 'Bayesian belief updating engine (prior + evidence = posterior) for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-bayesian-belief'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-bayesian-belief'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-bayesian-belief'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-bayesian-belief'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-bayesian-belief/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-bayesian-belief.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/bayesian_belief/helpers/constants'
4
+ require 'legion/extensions/bayesian_belief/helpers/belief'
5
+ require 'legion/extensions/bayesian_belief/helpers/belief_network'
6
+ require 'legion/extensions/bayesian_belief/runners/bayesian_belief'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module BayesianBelief
11
+ class Client
12
+ include Runners::BayesianBelief
13
+
14
+ def initialize(**)
15
+ @belief_network = Helpers::BeliefNetwork.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :belief_network
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module BayesianBelief
8
+ module Helpers
9
+ class Belief
10
+ include Constants
11
+
12
+ attr_reader :id, :content, :domain, :prior, :posterior,
13
+ :evidence_history, :update_count, :created_at, :last_updated_at
14
+
15
+ def initialize(content:, domain:, prior: Constants::DEFAULT_PRIOR)
16
+ @id = SecureRandom.uuid
17
+ @content = content
18
+ @domain = domain
19
+ @prior = prior.clamp(Constants::PRIOR_FLOOR, Constants::PRIOR_CEILING)
20
+ @posterior = @prior
21
+ @evidence_history = []
22
+ @update_count = 0
23
+ @created_at = Time.now.utc
24
+ @last_updated_at = @created_at
25
+ end
26
+
27
+ def update(likelihood:, evidence_id:)
28
+ clamped_likelihood = likelihood.clamp(Constants::LIKELIHOOD_FLOOR, Constants::LIKELIHOOD_CEILING)
29
+ marginal = (clamped_likelihood * @posterior) + ((1.0 - clamped_likelihood) * (1.0 - @posterior))
30
+ new_posterior = (clamped_likelihood * @posterior) / marginal
31
+ @posterior = new_posterior.clamp(Constants::PRIOR_FLOOR, Constants::PRIOR_CEILING)
32
+ @update_count += 1
33
+ @last_updated_at = Time.now.utc
34
+
35
+ @evidence_history << {
36
+ evidence_id: evidence_id,
37
+ likelihood: clamped_likelihood,
38
+ posterior_after: @posterior
39
+ }
40
+ @evidence_history.shift while @evidence_history.size > Constants::MAX_HISTORY
41
+
42
+ @posterior
43
+ end
44
+
45
+ def log_odds
46
+ Math.log(@posterior / (1.0 - @posterior))
47
+ end
48
+
49
+ def confidence_label
50
+ Constants::CONFIDENCE_LABELS.each do |range, label|
51
+ return label if range.cover?(@posterior)
52
+ end
53
+ :unknown
54
+ end
55
+
56
+ def surprise(observation_likelihood:)
57
+ clamped = observation_likelihood.clamp(Constants::LIKELIHOOD_FLOOR, 1.0)
58
+ -Math.log2(clamped)
59
+ end
60
+
61
+ def reset_to_prior!
62
+ @posterior = @prior
63
+ @update_count = 0
64
+ @evidence_history = []
65
+ @last_updated_at = Time.now.utc
66
+ @posterior
67
+ end
68
+
69
+ def stale?(threshold: Constants::STALE_THRESHOLD)
70
+ (Time.now.utc - @last_updated_at) > threshold
71
+ end
72
+
73
+ def to_h
74
+ {
75
+ id: @id,
76
+ content: @content,
77
+ domain: @domain,
78
+ prior: @prior,
79
+ posterior: @posterior,
80
+ confidence_label: confidence_label,
81
+ log_odds: log_odds,
82
+ update_count: @update_count,
83
+ evidence_history: @evidence_history,
84
+ created_at: @created_at,
85
+ last_updated_at: @last_updated_at
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module BayesianBelief
6
+ module Helpers
7
+ class BeliefNetwork
8
+ include Constants
9
+
10
+ attr_reader :beliefs
11
+
12
+ def initialize
13
+ @beliefs = {}
14
+ end
15
+
16
+ def add_belief(content:, domain:, prior: Constants::DEFAULT_PRIOR)
17
+ return nil if @beliefs.size >= Constants::MAX_HYPOTHESES
18
+
19
+ belief = Belief.new(content: content, domain: domain, prior: prior)
20
+ @beliefs[belief.id] = belief
21
+ belief
22
+ end
23
+
24
+ def update_belief(belief_id:, evidence_id:, likelihood:)
25
+ belief = @beliefs[belief_id]
26
+ return nil unless belief
27
+
28
+ belief.update(likelihood: likelihood, evidence_id: evidence_id)
29
+ belief
30
+ end
31
+
32
+ def batch_update(evidence_id:, likelihoods:)
33
+ return {} if likelihoods.empty?
34
+
35
+ updated = {}
36
+ likelihoods.each do |belief_id, likelihood|
37
+ belief = update_belief(belief_id: belief_id, evidence_id: evidence_id, likelihood: likelihood)
38
+ updated[belief_id] = belief.posterior if belief
39
+ end
40
+
41
+ normalize_posteriors(updated.keys)
42
+ updated
43
+ end
44
+
45
+ def most_probable(domain: nil, limit: 5)
46
+ filtered(domain).sort_by { |b| -b.posterior }.first(limit)
47
+ end
48
+
49
+ def least_probable(domain: nil, limit: 5)
50
+ filtered(domain).sort_by(&:posterior).first(limit)
51
+ end
52
+
53
+ def by_domain(domain:)
54
+ @beliefs.values.select { |b| b.domain == domain }
55
+ end
56
+
57
+ def posterior_distribution(domain: nil)
58
+ subset = filtered(domain)
59
+ total = subset.sum(&:posterior)
60
+ return {} if total.zero?
61
+
62
+ subset.to_h { |b| [b.id, b.posterior / total] }
63
+ end
64
+
65
+ def information_gain(belief_id:, evidence_id:, likelihood:) # rubocop:disable Lint/UnusedMethodArgument
66
+ belief = @beliefs[belief_id]
67
+ return 0.0 unless belief
68
+
69
+ prior_p = belief.posterior
70
+ clamped = likelihood.clamp(Constants::LIKELIHOOD_FLOOR, Constants::LIKELIHOOD_CEILING)
71
+ marginal = (clamped * prior_p) + ((1.0 - clamped) * (1.0 - prior_p))
72
+ post_p = (clamped * prior_p) / marginal
73
+ post_p = post_p.clamp(Constants::PRIOR_FLOOR, Constants::PRIOR_CEILING)
74
+
75
+ kl_divergence(prior_p, post_p)
76
+ end
77
+
78
+ def entropy(domain: nil)
79
+ dist = posterior_distribution(domain: domain)
80
+ return 0.0 if dist.empty?
81
+
82
+ -dist.values.sum do |prob|
83
+ next 0.0 if prob <= 0.0
84
+
85
+ prob * Math.log2(prob)
86
+ end
87
+ end
88
+
89
+ def decay_all
90
+ @beliefs.each_value do |belief|
91
+ shift = (belief.posterior - belief.prior) * Constants::DECAY_RATE
92
+ new_posterior = belief.posterior - shift
93
+ belief.instance_variable_set(:@posterior, new_posterior.clamp(Constants::PRIOR_FLOOR, Constants::PRIOR_CEILING))
94
+ belief.instance_variable_set(:@last_updated_at, Time.now.utc)
95
+ end
96
+ @beliefs.size
97
+ end
98
+
99
+ def count
100
+ @beliefs.size
101
+ end
102
+
103
+ def to_h
104
+ {
105
+ belief_count: @beliefs.size,
106
+ beliefs: @beliefs.transform_values(&:to_h)
107
+ }
108
+ end
109
+
110
+ private
111
+
112
+ def filtered(domain)
113
+ return @beliefs.values if domain.nil?
114
+
115
+ by_domain(domain: domain)
116
+ end
117
+
118
+ def normalize_posteriors(belief_ids)
119
+ subset = belief_ids.filter_map { |bid| @beliefs[bid] }
120
+ total = subset.sum(&:posterior)
121
+ return if total.zero?
122
+
123
+ subset.each do |belief|
124
+ normalized = (belief.posterior / total).clamp(Constants::PRIOR_FLOOR, Constants::PRIOR_CEILING)
125
+ belief.instance_variable_set(:@posterior, normalized)
126
+ end
127
+ end
128
+
129
+ def kl_divergence(prior_p, post_p)
130
+ return 0.0 if prior_p <= 0.0 || post_p <= 0.0
131
+
132
+ prior_q = 1.0 - prior_p
133
+ post_q = 1.0 - post_p
134
+
135
+ term1 = post_p * Math.log2(post_p / prior_p)
136
+ term2 = post_q * Math.log2(post_q / prior_q)
137
+ term1 + term2
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module BayesianBelief
6
+ module Helpers
7
+ module Constants
8
+ MAX_HYPOTHESES = 200
9
+ MAX_EVIDENCE = 500
10
+ MAX_HISTORY = 300
11
+
12
+ DEFAULT_PRIOR = 0.5
13
+ PRIOR_FLOOR = 0.001
14
+ PRIOR_CEILING = 0.999
15
+ LIKELIHOOD_FLOOR = 0.001
16
+ LIKELIHOOD_CEILING = 0.999
17
+
18
+ DECAY_RATE = 0.01
19
+ STALE_THRESHOLD = 120
20
+
21
+ CONFIDENCE_LABELS = {
22
+ (0.9..) => :certain,
23
+ (0.7...0.9) => :confident,
24
+ (0.5...0.7) => :leaning,
25
+ (0.3...0.5) => :uncertain,
26
+ (..0.3) => :doubtful
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module BayesianBelief
6
+ module Runners
7
+ module BayesianBelief
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def add_bayesian_belief(content:, domain:, prior: nil, **)
12
+ pri = (prior || Helpers::Constants::DEFAULT_PRIOR).clamp(
13
+ Helpers::Constants::PRIOR_FLOOR,
14
+ Helpers::Constants::PRIOR_CEILING
15
+ )
16
+ belief = belief_network.add_belief(content: content, domain: domain, prior: pri)
17
+ unless belief
18
+ Legion::Logging.warn "[bayesian_belief] add failed: network at capacity (#{Helpers::Constants::MAX_HYPOTHESES})"
19
+ return { success: false, reason: :capacity_exceeded, max: Helpers::Constants::MAX_HYPOTHESES }
20
+ end
21
+
22
+ Legion::Logging.debug "[bayesian_belief] add: id=#{belief.id[0..7]} domain=#{domain} prior=#{pri.round(3)}"
23
+ { success: true, belief_id: belief.id, domain: domain, prior: belief.prior, posterior: belief.posterior }
24
+ end
25
+
26
+ def update_bayesian_belief(belief_id:, evidence_id:, likelihood:, **)
27
+ clamped = likelihood.clamp(Helpers::Constants::LIKELIHOOD_FLOOR, Helpers::Constants::LIKELIHOOD_CEILING)
28
+ belief = belief_network.update_belief(belief_id: belief_id, evidence_id: evidence_id, likelihood: clamped)
29
+ unless belief
30
+ Legion::Logging.debug "[bayesian_belief] update failed: belief_id=#{belief_id} not found"
31
+ return { success: false, reason: :not_found, belief_id: belief_id }
32
+ end
33
+
34
+ Legion::Logging.debug "[bayesian_belief] update: id=#{belief_id[0..7]} evidence=#{evidence_id} " \
35
+ "likelihood=#{clamped.round(3)} posterior=#{belief.posterior.round(3)}"
36
+ {
37
+ success: true,
38
+ belief_id: belief_id,
39
+ evidence_id: evidence_id,
40
+ likelihood: clamped,
41
+ posterior: belief.posterior,
42
+ confidence_label: belief.confidence_label,
43
+ update_count: belief.update_count
44
+ }
45
+ end
46
+
47
+ def batch_bayesian_update(evidence_id:, likelihoods:, **)
48
+ if likelihoods.nil? || likelihoods.empty?
49
+ Legion::Logging.debug '[bayesian_belief] batch_update: empty likelihoods, skipping'
50
+ return { success: true, updated: 0, posteriors: {} }
51
+ end
52
+
53
+ posteriors = belief_network.batch_update(evidence_id: evidence_id, likelihoods: likelihoods)
54
+ Legion::Logging.debug "[bayesian_belief] batch_update: evidence=#{evidence_id} updated=#{posteriors.size}"
55
+ { success: true, evidence_id: evidence_id, updated: posteriors.size, posteriors: posteriors }
56
+ end
57
+
58
+ def most_probable_beliefs(domain: nil, limit: 5, **)
59
+ beliefs = belief_network.most_probable(domain: domain, limit: limit)
60
+ Legion::Logging.debug "[bayesian_belief] most_probable: domain=#{domain.inspect} count=#{beliefs.size}"
61
+ { success: true, beliefs: beliefs.map(&:to_h), count: beliefs.size }
62
+ end
63
+
64
+ def least_probable_beliefs(domain: nil, limit: 5, **)
65
+ beliefs = belief_network.least_probable(domain: domain, limit: limit)
66
+ Legion::Logging.debug "[bayesian_belief] least_probable: domain=#{domain.inspect} count=#{beliefs.size}"
67
+ { success: true, beliefs: beliefs.map(&:to_h), count: beliefs.size }
68
+ end
69
+
70
+ def posterior_distribution(domain: nil, **)
71
+ dist = belief_network.posterior_distribution(domain: domain)
72
+ Legion::Logging.debug "[bayesian_belief] posterior_distribution: domain=#{domain.inspect} size=#{dist.size}"
73
+ { success: true, distribution: dist, size: dist.size }
74
+ end
75
+
76
+ def information_gain(belief_id:, evidence_id:, likelihood:, **)
77
+ clamped = likelihood.clamp(Helpers::Constants::LIKELIHOOD_FLOOR, Helpers::Constants::LIKELIHOOD_CEILING)
78
+ gain = belief_network.information_gain(belief_id: belief_id, evidence_id: evidence_id, likelihood: clamped)
79
+ Legion::Logging.debug "[bayesian_belief] information_gain: id=#{belief_id[0..7]} likelihood=#{clamped.round(3)} gain=#{gain.round(4)}"
80
+ { success: true, belief_id: belief_id, evidence_id: evidence_id, likelihood: clamped, information_gain: gain }
81
+ end
82
+
83
+ def belief_entropy(domain: nil, **)
84
+ ent = belief_network.entropy(domain: domain)
85
+ Legion::Logging.debug "[bayesian_belief] entropy: domain=#{domain.inspect} entropy=#{ent.round(4)}"
86
+ { success: true, domain: domain, entropy: ent }
87
+ end
88
+
89
+ def update_bayesian_beliefs(**)
90
+ decayed = belief_network.decay_all
91
+ Legion::Logging.debug "[bayesian_belief] decay cycle: beliefs_updated=#{decayed}"
92
+ { success: true, decayed: decayed }
93
+ end
94
+
95
+ def bayesian_belief_stats(**)
96
+ total = belief_network.count
97
+ ent = belief_network.entropy
98
+ most = belief_network.most_probable(limit: 1).first
99
+ least = belief_network.least_probable(limit: 1).first
100
+
101
+ Legion::Logging.debug "[bayesian_belief] stats: total=#{total} entropy=#{ent.round(4)}"
102
+ {
103
+ success: true,
104
+ total_beliefs: total,
105
+ entropy: ent,
106
+ most_probable: most&.to_h,
107
+ least_probable: least&.to_h,
108
+ capacity: Helpers::Constants::MAX_HYPOTHESES
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ def belief_network
115
+ @belief_network ||= Helpers::BeliefNetwork.new
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module BayesianBelief
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/bayesian_belief/version'
4
+ require 'legion/extensions/bayesian_belief/helpers/constants'
5
+ require 'legion/extensions/bayesian_belief/helpers/belief'
6
+ require 'legion/extensions/bayesian_belief/helpers/belief_network'
7
+ require 'legion/extensions/bayesian_belief/runners/bayesian_belief'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module BayesianBelief
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/bayesian_belief/client'
4
+
5
+ RSpec.describe Legion::Extensions::BayesianBelief::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:add_bayesian_belief)
10
+ expect(client).to respond_to(:update_bayesian_belief)
11
+ expect(client).to respond_to(:batch_bayesian_update)
12
+ expect(client).to respond_to(:most_probable_beliefs)
13
+ expect(client).to respond_to(:least_probable_beliefs)
14
+ expect(client).to respond_to(:posterior_distribution)
15
+ expect(client).to respond_to(:information_gain)
16
+ expect(client).to respond_to(:belief_entropy)
17
+ expect(client).to respond_to(:update_bayesian_beliefs)
18
+ expect(client).to respond_to(:bayesian_belief_stats)
19
+ end
20
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::BayesianBelief::Helpers::BeliefNetwork do
4
+ let(:network) { described_class.new }
5
+ let(:consts) { Legion::Extensions::BayesianBelief::Helpers::Constants }
6
+
7
+ def add(content: 'hypothesis', domain: :general, prior: 0.5)
8
+ network.add_belief(content: content, domain: domain, prior: prior)
9
+ end
10
+
11
+ describe '#add_belief' do
12
+ it 'adds a belief and returns it' do
13
+ belief = add
14
+ expect(belief).to be_a(Legion::Extensions::BayesianBelief::Helpers::Belief)
15
+ expect(network.count).to eq(1)
16
+ end
17
+
18
+ it 'uses provided prior' do
19
+ belief = add(prior: 0.7)
20
+ expect(belief.prior).to eq(0.7)
21
+ end
22
+ end
23
+
24
+ describe '#update_belief' do
25
+ it 'updates and returns the belief' do
26
+ belief = add
27
+ result = network.update_belief(belief_id: belief.id, evidence_id: 'ev-1', likelihood: 0.9)
28
+ expect(result).to be_a(Legion::Extensions::BayesianBelief::Helpers::Belief)
29
+ expect(result.posterior).to be > 0.5
30
+ end
31
+
32
+ it 'returns nil for unknown belief_id' do
33
+ result = network.update_belief(belief_id: 'nonexistent', evidence_id: 'ev-1', likelihood: 0.8)
34
+ expect(result).to be_nil
35
+ end
36
+ end
37
+
38
+ describe '#batch_update' do
39
+ it 'updates multiple beliefs with same evidence' do
40
+ b1 = add(content: 'h1', domain: :test)
41
+ b2 = add(content: 'h2', domain: :test)
42
+ likelihoods = { b1.id => 0.8, b2.id => 0.3 }
43
+
44
+ result = network.batch_update(evidence_id: 'ev-batch', likelihoods: likelihoods)
45
+ expect(result.size).to eq(2)
46
+ end
47
+
48
+ it 'returns empty hash for empty likelihoods' do
49
+ result = network.batch_update(evidence_id: 'ev-1', likelihoods: {})
50
+ expect(result).to be_empty
51
+ end
52
+ end
53
+
54
+ describe '#most_probable' do
55
+ it 'returns beliefs sorted by posterior descending' do
56
+ add(content: 'low', domain: :test, prior: 0.2)
57
+ add(content: 'high', domain: :test, prior: 0.8)
58
+ add(content: 'mid', domain: :test, prior: 0.5)
59
+
60
+ beliefs = network.most_probable
61
+ expect(beliefs.first.posterior).to be >= beliefs.last.posterior
62
+ end
63
+
64
+ it 'limits results' do
65
+ 3.times { |idx| add(content: "h#{idx}", domain: :test) }
66
+ expect(network.most_probable(limit: 2).size).to eq(2)
67
+ end
68
+
69
+ it 'filters by domain' do
70
+ add(content: 'a', domain: :alpha)
71
+ add(content: 'b', domain: :beta)
72
+ results = network.most_probable(domain: :alpha)
73
+ expect(results.all? { |b| b.domain == :alpha }).to be true
74
+ end
75
+ end
76
+
77
+ describe '#least_probable' do
78
+ it 'returns beliefs sorted by posterior ascending' do
79
+ add(content: 'low', domain: :test, prior: 0.1)
80
+ add(content: 'high', domain: :test, prior: 0.9)
81
+
82
+ beliefs = network.least_probable
83
+ expect(beliefs.first.posterior).to be <= beliefs.last.posterior
84
+ end
85
+ end
86
+
87
+ describe '#by_domain' do
88
+ it 'returns only beliefs in specified domain' do
89
+ add(content: 'a', domain: :alpha)
90
+ add(content: 'b', domain: :beta)
91
+ result = network.by_domain(domain: :alpha)
92
+ expect(result.size).to eq(1)
93
+ expect(result.first.domain).to eq(:alpha)
94
+ end
95
+ end
96
+
97
+ describe '#posterior_distribution' do
98
+ it 'returns normalized probabilities that sum to ~1' do
99
+ add(content: 'h1', domain: :test, prior: 0.3)
100
+ add(content: 'h2', domain: :test, prior: 0.7)
101
+
102
+ dist = network.posterior_distribution(domain: :test)
103
+ expect(dist.values.sum).to be_within(0.001).of(1.0)
104
+ end
105
+
106
+ it 'returns empty hash when no beliefs exist' do
107
+ expect(network.posterior_distribution).to eq({})
108
+ end
109
+ end
110
+
111
+ describe '#information_gain' do
112
+ it 'returns a non-negative float' do
113
+ belief = add
114
+ gain = network.information_gain(belief_id: belief.id, evidence_id: 'ev-1', likelihood: 0.9)
115
+ expect(gain).to be >= 0.0
116
+ end
117
+
118
+ it 'returns 0.0 for unknown belief' do
119
+ gain = network.information_gain(belief_id: 'nonexistent', evidence_id: 'ev-1', likelihood: 0.8)
120
+ expect(gain).to eq(0.0)
121
+ end
122
+
123
+ it 'does not mutate the belief posterior' do
124
+ belief = add
125
+ before = belief.posterior
126
+ network.information_gain(belief_id: belief.id, evidence_id: 'ev-1', likelihood: 0.9)
127
+ expect(belief.posterior).to eq(before)
128
+ end
129
+
130
+ it 'returns higher gain for more extreme likelihoods' do
131
+ b1 = add(content: 'h1')
132
+ b2 = add(content: 'h2')
133
+
134
+ gain_strong = network.information_gain(belief_id: b1.id, evidence_id: 'ev', likelihood: 0.95)
135
+ gain_weak = network.information_gain(belief_id: b2.id, evidence_id: 'ev', likelihood: 0.55)
136
+ expect(gain_strong).to be > gain_weak
137
+ end
138
+ end
139
+
140
+ describe '#entropy' do
141
+ it 'returns 0.0 when no beliefs' do
142
+ expect(network.entropy).to eq(0.0)
143
+ end
144
+
145
+ it 'returns a positive float when beliefs exist' do
146
+ 2.times { |idx| add(content: "h#{idx}") }
147
+ expect(network.entropy).to be > 0.0
148
+ end
149
+ end
150
+
151
+ describe '#decay_all' do
152
+ it 'returns count of beliefs updated' do
153
+ 3.times { |idx| add(content: "h#{idx}") }
154
+ expect(network.decay_all).to eq(3)
155
+ end
156
+
157
+ it 'drifts posterior toward prior' do
158
+ belief = add(prior: 0.5)
159
+ belief.update(likelihood: 0.9, evidence_id: 'ev-1')
160
+ before = belief.posterior
161
+
162
+ network.decay_all
163
+
164
+ after = belief.posterior
165
+ expect(after).to be < before
166
+ end
167
+ end
168
+
169
+ describe '#to_h' do
170
+ it 'includes belief_count and beliefs keys' do
171
+ add
172
+ hash = network.to_h
173
+ expect(hash).to have_key(:belief_count)
174
+ expect(hash).to have_key(:beliefs)
175
+ expect(hash[:belief_count]).to eq(1)
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::BayesianBelief::Helpers::Belief do
4
+ let(:belief) { described_class.new(content: 'test hypothesis', domain: :general) }
5
+
6
+ describe '#initialize' do
7
+ it 'sets prior and posterior to default' do
8
+ expect(belief.prior).to eq(Legion::Extensions::BayesianBelief::Helpers::Constants::DEFAULT_PRIOR)
9
+ expect(belief.posterior).to eq(belief.prior)
10
+ end
11
+
12
+ it 'generates a uuid id' do
13
+ expect(belief.id).to match(/\A[0-9a-f-]{36}\z/)
14
+ end
15
+
16
+ it 'starts with empty evidence_history' do
17
+ expect(belief.evidence_history).to be_empty
18
+ end
19
+
20
+ it 'clamps prior to floor/ceiling' do
21
+ high = described_class.new(content: 'test', domain: :x, prior: 1.5)
22
+ low = described_class.new(content: 'test', domain: :x, prior: -0.1)
23
+ expect(high.prior).to eq(Legion::Extensions::BayesianBelief::Helpers::Constants::PRIOR_CEILING)
24
+ expect(low.prior).to eq(Legion::Extensions::BayesianBelief::Helpers::Constants::PRIOR_FLOOR)
25
+ end
26
+ end
27
+
28
+ describe '#update' do
29
+ it 'raises posterior when likelihood is high' do
30
+ before = belief.posterior
31
+ belief.update(likelihood: 0.9, evidence_id: 'ev-1')
32
+ expect(belief.posterior).to be > before
33
+ end
34
+
35
+ it 'lowers posterior when likelihood is low' do
36
+ before = belief.posterior
37
+ belief.update(likelihood: 0.1, evidence_id: 'ev-1')
38
+ expect(belief.posterior).to be < before
39
+ end
40
+
41
+ it 'records evidence in history' do
42
+ belief.update(likelihood: 0.8, evidence_id: 'ev-1')
43
+ expect(belief.evidence_history.size).to eq(1)
44
+ expect(belief.evidence_history.first[:evidence_id]).to eq('ev-1')
45
+ end
46
+
47
+ it 'increments update_count' do
48
+ belief.update(likelihood: 0.7, evidence_id: 'ev-1')
49
+ belief.update(likelihood: 0.6, evidence_id: 'ev-2')
50
+ expect(belief.update_count).to eq(2)
51
+ end
52
+
53
+ it 'clamps posterior within bounds' do
54
+ 10.times { |idx| belief.update(likelihood: 0.999, evidence_id: "ev-#{idx}") }
55
+ expect(belief.posterior).to be <= Legion::Extensions::BayesianBelief::Helpers::Constants::PRIOR_CEILING
56
+ expect(belief.posterior).to be >= Legion::Extensions::BayesianBelief::Helpers::Constants::PRIOR_FLOOR
57
+ end
58
+ end
59
+
60
+ describe '#log_odds' do
61
+ it 'returns 0.0 at prior 0.5' do
62
+ expect(belief.log_odds).to be_within(0.001).of(0.0)
63
+ end
64
+
65
+ it 'returns positive value when posterior > 0.5' do
66
+ belief.update(likelihood: 0.9, evidence_id: 'ev-1')
67
+ expect(belief.log_odds).to be > 0.0
68
+ end
69
+
70
+ it 'returns negative value when posterior < 0.5' do
71
+ belief.update(likelihood: 0.1, evidence_id: 'ev-1')
72
+ expect(belief.log_odds).to be < 0.0
73
+ end
74
+ end
75
+
76
+ describe '#confidence_label' do
77
+ it 'returns :leaning for default prior 0.5' do
78
+ expect(belief.confidence_label).to eq(:leaning)
79
+ end
80
+
81
+ it 'returns :certain for high posterior' do
82
+ high = described_class.new(content: 'test', domain: :x, prior: 0.95)
83
+ expect(high.confidence_label).to eq(:certain)
84
+ end
85
+
86
+ it 'returns :doubtful for low posterior' do
87
+ low = described_class.new(content: 'test', domain: :x, prior: 0.1)
88
+ expect(low.confidence_label).to eq(:doubtful)
89
+ end
90
+
91
+ it 'returns :confident for posterior in 0.7..0.9' do
92
+ conf = described_class.new(content: 'test', domain: :x, prior: 0.8)
93
+ expect(conf.confidence_label).to eq(:confident)
94
+ end
95
+
96
+ it 'returns :uncertain for posterior in 0.3..0.5' do
97
+ unc = described_class.new(content: 'test', domain: :x, prior: 0.4)
98
+ expect(unc.confidence_label).to eq(:uncertain)
99
+ end
100
+ end
101
+
102
+ describe '#surprise' do
103
+ it 'returns 0.0 for certain observation (likelihood 1.0)' do
104
+ expect(belief.surprise(observation_likelihood: 1.0)).to be_within(0.001).of(0.0)
105
+ end
106
+
107
+ it 'returns positive surprise for unlikely observation' do
108
+ expect(belief.surprise(observation_likelihood: 0.125)).to be_within(0.001).of(3.0)
109
+ end
110
+ end
111
+
112
+ describe '#reset_to_prior!' do
113
+ it 'resets posterior to original prior' do
114
+ belief.update(likelihood: 0.9, evidence_id: 'ev-1')
115
+ expect(belief.posterior).not_to eq(belief.prior)
116
+ belief.reset_to_prior!
117
+ expect(belief.posterior).to eq(belief.prior)
118
+ end
119
+
120
+ it 'clears evidence history and update count' do
121
+ belief.update(likelihood: 0.8, evidence_id: 'ev-1')
122
+ belief.reset_to_prior!
123
+ expect(belief.evidence_history).to be_empty
124
+ expect(belief.update_count).to eq(0)
125
+ end
126
+ end
127
+
128
+ describe '#to_h' do
129
+ it 'includes all required keys' do
130
+ hash = belief.to_h
131
+ %i[id content domain prior posterior confidence_label log_odds update_count
132
+ evidence_history created_at last_updated_at].each do |key|
133
+ expect(hash).to have_key(key)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/bayesian_belief/client'
4
+
5
+ RSpec.describe Legion::Extensions::BayesianBelief::Runners::BayesianBelief do
6
+ let(:client) { Legion::Extensions::BayesianBelief::Client.new }
7
+
8
+ describe '#add_bayesian_belief' do
9
+ it 'adds a belief and returns success' do
10
+ result = client.add_bayesian_belief(content: 'test hypothesis', domain: :general)
11
+ expect(result[:success]).to be true
12
+ expect(result[:belief_id]).to match(/\A[0-9a-f-]{36}\z/)
13
+ expect(result[:domain]).to eq(:general)
14
+ end
15
+
16
+ it 'uses default prior when none provided' do
17
+ result = client.add_bayesian_belief(content: 'test', domain: :test)
18
+ expect(result[:prior]).to eq(Legion::Extensions::BayesianBelief::Helpers::Constants::DEFAULT_PRIOR)
19
+ end
20
+
21
+ it 'uses provided prior' do
22
+ result = client.add_bayesian_belief(content: 'test', domain: :test, prior: 0.7)
23
+ expect(result[:prior]).to be_within(0.001).of(0.7)
24
+ end
25
+ end
26
+
27
+ describe '#update_bayesian_belief' do
28
+ it 'updates an existing belief' do
29
+ added = client.add_bayesian_belief(content: 'h1', domain: :test)
30
+ result = client.update_bayesian_belief(
31
+ belief_id: added[:belief_id],
32
+ evidence_id: 'ev-1',
33
+ likelihood: 0.9
34
+ )
35
+ expect(result[:success]).to be true
36
+ expect(result[:posterior]).to be > 0.5
37
+ expect(result[:update_count]).to eq(1)
38
+ end
39
+
40
+ it 'returns not_found for unknown belief' do
41
+ result = client.update_bayesian_belief(belief_id: 'nonexistent', evidence_id: 'ev-1', likelihood: 0.8)
42
+ expect(result[:success]).to be false
43
+ expect(result[:reason]).to eq(:not_found)
44
+ end
45
+
46
+ it 'returns confidence_label' do
47
+ added = client.add_bayesian_belief(content: 'h1', domain: :test, prior: 0.95)
48
+ result = client.update_bayesian_belief(
49
+ belief_id: added[:belief_id],
50
+ evidence_id: 'ev-1',
51
+ likelihood: 0.9
52
+ )
53
+ expect(result[:confidence_label]).to eq(:certain)
54
+ end
55
+ end
56
+
57
+ describe '#batch_bayesian_update' do
58
+ it 'updates multiple beliefs' do
59
+ b1 = client.add_bayesian_belief(content: 'h1', domain: :test)
60
+ b2 = client.add_bayesian_belief(content: 'h2', domain: :test)
61
+ likelihoods = { b1[:belief_id] => 0.8, b2[:belief_id] => 0.3 }
62
+
63
+ result = client.batch_bayesian_update(evidence_id: 'ev-batch', likelihoods: likelihoods)
64
+ expect(result[:success]).to be true
65
+ expect(result[:updated]).to eq(2)
66
+ end
67
+
68
+ it 'handles empty likelihoods gracefully' do
69
+ result = client.batch_bayesian_update(evidence_id: 'ev-1', likelihoods: {})
70
+ expect(result[:success]).to be true
71
+ expect(result[:updated]).to eq(0)
72
+ end
73
+ end
74
+
75
+ describe '#most_probable_beliefs' do
76
+ it 'returns beliefs sorted by probability' do
77
+ client.add_bayesian_belief(content: 'low', domain: :ranked, prior: 0.2)
78
+ client.add_bayesian_belief(content: 'high', domain: :ranked, prior: 0.8)
79
+
80
+ result = client.most_probable_beliefs(domain: :ranked)
81
+ expect(result[:success]).to be true
82
+ expect(result[:beliefs].first[:posterior]).to be >= result[:beliefs].last[:posterior]
83
+ end
84
+
85
+ it 'respects limit parameter' do
86
+ 5.times { |idx| client.add_bayesian_belief(content: "h#{idx}", domain: :limit_test) }
87
+ result = client.most_probable_beliefs(domain: :limit_test, limit: 3)
88
+ expect(result[:count]).to eq(3)
89
+ end
90
+ end
91
+
92
+ describe '#least_probable_beliefs' do
93
+ it 'returns beliefs sorted by ascending probability' do
94
+ client.add_bayesian_belief(content: 'low', domain: :sorted, prior: 0.1)
95
+ client.add_bayesian_belief(content: 'high', domain: :sorted, prior: 0.9)
96
+
97
+ result = client.least_probable_beliefs(domain: :sorted)
98
+ expect(result[:beliefs].first[:posterior]).to be <= result[:beliefs].last[:posterior]
99
+ end
100
+ end
101
+
102
+ describe '#posterior_distribution' do
103
+ it 'returns normalized distribution summing to 1' do
104
+ client.add_bayesian_belief(content: 'h1', domain: :dist, prior: 0.3)
105
+ client.add_bayesian_belief(content: 'h2', domain: :dist, prior: 0.7)
106
+
107
+ result = client.posterior_distribution(domain: :dist)
108
+ expect(result[:success]).to be true
109
+ expect(result[:distribution].values.sum).to be_within(0.001).of(1.0)
110
+ end
111
+
112
+ it 'returns empty distribution when no beliefs' do
113
+ result = client.posterior_distribution(domain: :empty_domain)
114
+ expect(result[:distribution]).to be_empty
115
+ end
116
+ end
117
+
118
+ describe '#information_gain' do
119
+ it 'computes gain without mutating the belief' do
120
+ added = client.add_bayesian_belief(content: 'h1', domain: :ig)
121
+ before = added[:posterior]
122
+ result = client.information_gain(belief_id: added[:belief_id], evidence_id: 'ev-1', likelihood: 0.9)
123
+ expect(result[:success]).to be true
124
+ expect(result[:information_gain]).to be >= 0.0
125
+
126
+ check = client.most_probable_beliefs(domain: :ig)
127
+ expect(check[:beliefs].first[:posterior]).to be_within(0.001).of(before)
128
+ end
129
+ end
130
+
131
+ describe '#belief_entropy' do
132
+ it 'returns entropy for all beliefs' do
133
+ client.add_bayesian_belief(content: 'h1', domain: :ent)
134
+ client.add_bayesian_belief(content: 'h2', domain: :ent)
135
+
136
+ result = client.belief_entropy(domain: :ent)
137
+ expect(result[:success]).to be true
138
+ expect(result[:entropy]).to be >= 0.0
139
+ end
140
+
141
+ it 'returns 0.0 entropy when no beliefs exist' do
142
+ fresh = Legion::Extensions::BayesianBelief::Client.new
143
+ result = fresh.belief_entropy
144
+ expect(result[:entropy]).to eq(0.0)
145
+ end
146
+ end
147
+
148
+ describe '#update_bayesian_beliefs (decay)' do
149
+ it 'returns success and decayed count' do
150
+ 2.times { |idx| client.add_bayesian_belief(content: "h#{idx}", domain: :decay) }
151
+ result = client.update_bayesian_beliefs
152
+ expect(result[:success]).to be true
153
+ expect(result[:decayed]).to eq(2)
154
+ end
155
+ end
156
+
157
+ describe '#bayesian_belief_stats' do
158
+ it 'returns stats hash with expected keys' do
159
+ client.add_bayesian_belief(content: 'h1', domain: :stats)
160
+ result = client.bayesian_belief_stats
161
+ expect(result[:success]).to be true
162
+ expect(result).to have_key(:total_beliefs)
163
+ expect(result).to have_key(:entropy)
164
+ expect(result).to have_key(:most_probable)
165
+ expect(result).to have_key(:least_probable)
166
+ expect(result).to have_key(:capacity)
167
+ end
168
+
169
+ it 'returns nil for most/least probable when no beliefs' do
170
+ fresh = Legion::Extensions::BayesianBelief::Client.new
171
+ result = fresh.bayesian_belief_stats
172
+ expect(result[:most_probable]).to be_nil
173
+ expect(result[:least_probable]).to be_nil
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ require 'legion/extensions/bayesian_belief'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-bayesian-belief
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Bayesian belief updating engine (prior + evidence = posterior) for brain-modeled
27
+ agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-bayesian-belief.gemspec
36
+ - lib/legion/extensions/bayesian_belief.rb
37
+ - lib/legion/extensions/bayesian_belief/client.rb
38
+ - lib/legion/extensions/bayesian_belief/helpers/belief.rb
39
+ - lib/legion/extensions/bayesian_belief/helpers/belief_network.rb
40
+ - lib/legion/extensions/bayesian_belief/helpers/constants.rb
41
+ - lib/legion/extensions/bayesian_belief/runners/bayesian_belief.rb
42
+ - lib/legion/extensions/bayesian_belief/version.rb
43
+ - spec/legion/extensions/bayesian_belief/client_spec.rb
44
+ - spec/legion/extensions/bayesian_belief/helpers/belief_network_spec.rb
45
+ - spec/legion/extensions/bayesian_belief/helpers/belief_spec.rb
46
+ - spec/legion/extensions/bayesian_belief/runners/bayesian_belief_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-bayesian-belief
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-bayesian-belief
53
+ source_code_uri: https://github.com/LegionIO/lex-bayesian-belief
54
+ documentation_uri: https://github.com/LegionIO/lex-bayesian-belief
55
+ changelog_uri: https://github.com/LegionIO/lex-bayesian-belief
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-bayesian-belief/issues
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: LEX Bayesian Belief
75
+ test_files: []