lex-predictive-processing 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: 0b2a81f552ab46d8107dcadb31234489978d943358698284842e2513c06ee7f4
4
+ data.tar.gz: 83fdf31810c065a1b6b48db9572807362fb46a34ca14a1b4d86e028cd6d8085b
5
+ SHA512:
6
+ metadata.gz: c0710af7f0b991c387797a3ed494232eb3a7a96d687dc0a362b8a99f3e58321e912305a405e58b834c3c4afc675ddb3c81e634c0456499ba2699dcb841055cd1
7
+ data.tar.gz: 33ac74d6a364efdac1d5fe23d31991090b14bc9a14e2bce7501b1d17ec717a41aff8d6d0b994b82b4c06dd6053625ad78326b62fc0638327796da0dc30bcdfab
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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/predictive_processing/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-predictive-processing'
7
+ spec.version = Legion::Extensions::PredictiveProcessing::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Predictive Processing'
12
+ spec.description = "Andy Clark's Predictive Processing / Active Inference for brain-modeled agentic AI: " \
13
+ 'generative models, precision-weighting, active inference, and free energy minimization'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-predictive-processing'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-predictive-processing'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-predictive-processing'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-predictive-processing'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-predictive-processing/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ Dir.glob('{lib,spec}/**/*') + %w[lex-predictive-processing.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/predictive_processing/helpers/constants'
4
+ require 'legion/extensions/predictive_processing/helpers/generative_model'
5
+ require 'legion/extensions/predictive_processing/helpers/predictive_processor'
6
+ require 'legion/extensions/predictive_processing/runners/predictive_processing'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module PredictiveProcessing
11
+ class Client
12
+ include Runners::PredictiveProcessing
13
+
14
+ def initialize(processor: nil)
15
+ @default_processor = processor || Helpers::PredictiveProcessor.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :default_processor
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveProcessing
6
+ module Helpers
7
+ module Constants
8
+ MAX_MODELS = 20
9
+ MAX_PREDICTIONS_PER_MODEL = 50
10
+ MAX_HISTORY = 200
11
+ DEFAULT_PRECISION = 0.5
12
+ PRECISION_FLOOR = 0.05
13
+ PRECISION_DECAY = 0.02
14
+ MODEL_CONFIDENCE_FLOOR = 0.1
15
+ FREE_ENERGY_THRESHOLD = 0.7
16
+ ACTIVE_INFERENCE_THRESHOLD = 0.5
17
+ LEARNING_RATE = 0.1
18
+ INFERENCE_MODES = %i[perceptual active hybrid].freeze
19
+ MODEL_STATES = %i[stable updating exploring surprised].freeze
20
+ PRECISION_LABELS = {
21
+ (0.8..) => :certain,
22
+ (0.6...0.8) => :confident,
23
+ (0.4...0.6) => :uncertain,
24
+ (0.2...0.4) => :vague,
25
+ (..0.2) => :noise
26
+ }.freeze
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module PredictiveProcessing
8
+ module Helpers
9
+ class GenerativeModel
10
+ include Constants
11
+
12
+ attr_reader :id, :domain, :confidence, :precision, :prediction_error, :state,
13
+ :last_prediction, :created_at, :updated_at
14
+
15
+ def initialize(domain:)
16
+ @id = SecureRandom.uuid
17
+ @domain = domain
18
+ @confidence = DEFAULT_PRECISION
19
+ @precision = DEFAULT_PRECISION
20
+ @prediction_error = 0.0
21
+ @state = :stable
22
+ @history = []
23
+ @last_prediction = nil
24
+ @created_at = Time.now.utc
25
+ @updated_at = Time.now.utc
26
+ end
27
+
28
+ def predict(context: {})
29
+ richness = context_richness(context)
30
+ expected_val = (confidence + richness).clamp(0.0, 1.0)
31
+ @last_prediction = {
32
+ expected_value: expected_val,
33
+ confidence: confidence,
34
+ precision: precision,
35
+ domain: @domain,
36
+ context_size: context.size,
37
+ predicted_at: Time.now.utc
38
+ }
39
+ end
40
+
41
+ def observe(actual:, predicted:)
42
+ @prediction_error = compute_error(actual, predicted)
43
+ update_history(@prediction_error)
44
+ update_confidence
45
+ update_state
46
+ @updated_at = Time.now.utc
47
+ @prediction_error
48
+ end
49
+
50
+ # Free energy (surprise): high when errors are large relative to precision.
51
+ # Uses a formula that can exceed FREE_ENERGY_THRESHOLD (0.7) with sustained errors.
52
+ def free_energy
53
+ return 0.0 if @history.empty?
54
+
55
+ recent = @history.last(10)
56
+ avg_error = recent.sum.to_f / recent.size
57
+ raw = avg_error * (1.5 - (precision * 0.5))
58
+ raw.clamp(0.0, 1.0)
59
+ end
60
+
61
+ def update_model(error:)
62
+ adjustment = error * LEARNING_RATE * precision
63
+ @confidence = (@confidence - adjustment).clamp(MODEL_CONFIDENCE_FLOOR, 1.0)
64
+ @state = :updating
65
+ @updated_at = Time.now.utc
66
+ end
67
+
68
+ def stable?
69
+ free_energy <= FREE_ENERGY_THRESHOLD
70
+ end
71
+
72
+ def surprised?
73
+ free_energy > FREE_ENERGY_THRESHOLD
74
+ end
75
+
76
+ def decay
77
+ @precision = [@precision - PRECISION_DECAY, PRECISION_FLOOR].max
78
+ @updated_at = Time.now.utc
79
+ end
80
+
81
+ def precision_label
82
+ PRECISION_LABELS.find { |range, _label| range.cover?(precision) }&.last || :noise
83
+ end
84
+
85
+ def to_h
86
+ {
87
+ id: @id,
88
+ domain: @domain,
89
+ confidence: confidence,
90
+ precision: precision,
91
+ prediction_error: @prediction_error,
92
+ free_energy: free_energy,
93
+ state: @state,
94
+ precision_label: precision_label,
95
+ stable: stable?,
96
+ surprised: surprised?,
97
+ history_size: @history.size,
98
+ created_at: @created_at,
99
+ updated_at: @updated_at
100
+ }
101
+ end
102
+
103
+ private
104
+
105
+ def context_richness(context)
106
+ [context.size * 0.02, 0.2].min
107
+ end
108
+
109
+ def compute_error(actual, predicted)
110
+ return 0.0 unless actual.is_a?(Numeric) && predicted.is_a?(Numeric)
111
+
112
+ (actual - predicted).abs.clamp(0.0, 1.0)
113
+ end
114
+
115
+ def update_history(error)
116
+ @history << error
117
+ @history.shift while @history.size > MAX_HISTORY
118
+ end
119
+
120
+ def update_confidence
121
+ delta = -@prediction_error * LEARNING_RATE
122
+ @confidence = (@confidence + delta).clamp(MODEL_CONFIDENCE_FLOOR, 1.0)
123
+ end
124
+
125
+ def update_state
126
+ @state = if surprised?
127
+ :surprised
128
+ elsif @history.size < 5
129
+ :exploring
130
+ else
131
+ :stable
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveProcessing
6
+ module Helpers
7
+ class PredictiveProcessor
8
+ include Constants
9
+
10
+ attr_reader :models
11
+
12
+ def initialize
13
+ @models = {}
14
+ end
15
+
16
+ def add_model(domain:)
17
+ return { added: false, reason: :limit_reached } if @models.size >= MAX_MODELS
18
+ return { added: false, reason: :already_exists } if @models.key?(domain)
19
+
20
+ model = GenerativeModel.new(domain: domain)
21
+ @models[domain] = model
22
+ { added: true, domain: domain, model_id: model.id }
23
+ end
24
+
25
+ def predict(domain:, context: {})
26
+ model = find_or_create(domain)
27
+ model.predict(context: context)
28
+ end
29
+
30
+ def observe(domain:, actual:, predicted:)
31
+ model = @models[domain]
32
+ return { observed: false, reason: :domain_not_found } unless model
33
+
34
+ error = model.observe(actual: actual, predicted: predicted)
35
+ mode = inference_mode(domain)
36
+
37
+ model.update_model(error: error) if %i[perceptual hybrid].include?(mode)
38
+
39
+ {
40
+ observed: true,
41
+ domain: domain,
42
+ prediction_error: error,
43
+ inference_mode: mode,
44
+ free_energy: model.free_energy,
45
+ state: model.state
46
+ }
47
+ end
48
+
49
+ def inference_mode(domain)
50
+ model = @models[domain]
51
+ return :perceptual unless model
52
+
53
+ fe = model.free_energy
54
+ determine_mode(fe, model.precision)
55
+ end
56
+
57
+ def free_energy_for(domain)
58
+ model = @models[domain]
59
+ model&.free_energy
60
+ end
61
+
62
+ def global_free_energy
63
+ return 0.0 if @models.empty?
64
+
65
+ total = @models.values.sum(&:free_energy)
66
+ total / @models.size
67
+ end
68
+
69
+ def precision_weight(domain)
70
+ model = @models[domain]
71
+ model ? model.precision : DEFAULT_PRECISION
72
+ end
73
+
74
+ def models_needing_update
75
+ @models.select { |_d, m| m.surprised? }.transform_values(&:to_h)
76
+ end
77
+
78
+ def stable_models
79
+ @models.select { |_d, m| m.stable? }.transform_values(&:to_h)
80
+ end
81
+
82
+ def active_inference_candidates
83
+ @models.select { |_d, m| m.free_energy > ACTIVE_INFERENCE_THRESHOLD }
84
+ .keys
85
+ end
86
+
87
+ def tick
88
+ @models.each_value(&:decay)
89
+ end
90
+
91
+ def to_h
92
+ {
93
+ model_count: @models.size,
94
+ global_free_energy: global_free_energy,
95
+ models_needing_update: models_needing_update.size,
96
+ stable_model_count: stable_models.size,
97
+ active_inference_domains: active_inference_candidates.size,
98
+ models: @models.transform_values(&:to_h)
99
+ }
100
+ end
101
+
102
+ private
103
+
104
+ def find_or_create(domain)
105
+ @models[domain] ||= begin
106
+ model = GenerativeModel.new(domain: domain)
107
+ @models[domain] = model if @models.size < MAX_MODELS
108
+ model
109
+ end
110
+ end
111
+
112
+ def determine_mode(free_energy, precision)
113
+ if free_energy > FREE_ENERGY_THRESHOLD && precision >= ACTIVE_INFERENCE_THRESHOLD
114
+ :hybrid
115
+ elsif free_energy > ACTIVE_INFERENCE_THRESHOLD
116
+ :active
117
+ else
118
+ :perceptual
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveProcessing
6
+ module Runners
7
+ module PredictiveProcessing
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def add_generative_model(domain:, **)
12
+ return { added: false, reason: :missing_domain } if domain.nil? || domain.to_s.strip.empty?
13
+
14
+ result = default_processor.add_model(domain: domain.to_sym)
15
+ Legion::Logging.debug "[predictive_processing] add_model domain=#{domain} added=#{result[:added]}"
16
+ result
17
+ end
18
+
19
+ def predict_from_model(domain:, context: {}, **)
20
+ return { predicted: false, reason: :missing_domain } if domain.nil?
21
+
22
+ prediction = default_processor.predict(domain: domain.to_sym, context: context)
23
+ Legion::Logging.debug "[predictive_processing] predict domain=#{domain} " \
24
+ "expected=#{prediction[:expected_value]&.round(3)}"
25
+ { predicted: true, domain: domain, prediction: prediction }
26
+ end
27
+
28
+ def observe_outcome(domain:, actual:, predicted:, **)
29
+ return { observed: false, reason: :missing_domain } if domain.nil?
30
+
31
+ result = default_processor.observe(
32
+ domain: domain.to_sym,
33
+ actual: actual,
34
+ predicted: predicted
35
+ )
36
+ log_observe(domain, result)
37
+ result
38
+ end
39
+
40
+ def inference_mode(domain:, **)
41
+ return { mode: nil, reason: :missing_domain } if domain.nil?
42
+
43
+ mode = default_processor.inference_mode(domain.to_sym)
44
+ Legion::Logging.debug "[predictive_processing] inference_mode domain=#{domain} mode=#{mode}"
45
+ { domain: domain, mode: mode }
46
+ end
47
+
48
+ def free_energy(domain: nil, **)
49
+ if domain
50
+ fe = default_processor.free_energy_for(domain.to_sym)
51
+ return { domain: domain, free_energy: nil, reason: :domain_not_found } if fe.nil?
52
+
53
+ { domain: domain, free_energy: fe }
54
+ else
55
+ { global_free_energy: default_processor.global_free_energy }
56
+ end
57
+ end
58
+
59
+ def models_needing_update(**)
60
+ needing = default_processor.models_needing_update
61
+ Legion::Logging.debug "[predictive_processing] models_needing_update count=#{needing.size}"
62
+ { count: needing.size, models: needing }
63
+ end
64
+
65
+ def active_inference_candidates(**)
66
+ candidates = default_processor.active_inference_candidates
67
+ Legion::Logging.debug "[predictive_processing] active_inference_candidates count=#{candidates.size}"
68
+ { count: candidates.size, domains: candidates }
69
+ end
70
+
71
+ def update_predictive_processing(**)
72
+ default_processor.tick
73
+ Legion::Logging.debug '[predictive_processing] tick: precision decayed on all models'
74
+ { ticked: true, model_count: default_processor.models.size }
75
+ end
76
+
77
+ def predictive_processing_stats(**)
78
+ stats = default_processor.to_h
79
+ Legion::Logging.debug "[predictive_processing] stats global_fe=#{stats[:global_free_energy]&.round(3)}"
80
+ { success: true, stats: stats }
81
+ end
82
+
83
+ private
84
+
85
+ def default_processor
86
+ @default_processor ||= Helpers::PredictiveProcessor.new
87
+ end
88
+
89
+ def log_observe(domain, result)
90
+ return unless result[:observed]
91
+
92
+ Legion::Logging.debug "[predictive_processing] observe domain=#{domain} " \
93
+ "error=#{result[:prediction_error]&.round(3)} " \
94
+ "mode=#{result[:inference_mode]}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module PredictiveProcessing
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/predictive_processing/version'
4
+ require 'legion/extensions/predictive_processing/helpers/constants'
5
+ require 'legion/extensions/predictive_processing/helpers/generative_model'
6
+ require 'legion/extensions/predictive_processing/helpers/predictive_processor'
7
+ require 'legion/extensions/predictive_processing/runners/predictive_processing'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module PredictiveProcessing
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveProcessing::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'creates a client with a default processor' do
8
+ expect(client).to be_a(described_class)
9
+ end
10
+
11
+ it 'accepts an injected processor' do
12
+ processor = Legion::Extensions::PredictiveProcessing::Helpers::PredictiveProcessor.new
13
+ injected = described_class.new(processor: processor)
14
+ result = injected.predictive_processing_stats
15
+ expect(result[:success]).to be true
16
+ end
17
+ end
18
+
19
+ describe 'full workflow: add -> predict -> observe -> stats' do
20
+ it 'completes a full predictive processing cycle' do
21
+ client.add_generative_model(domain: :workflow)
22
+ prediction = client.predict_from_model(domain: :workflow, context: { step: 1 })
23
+ expect(prediction[:predicted]).to be true
24
+
25
+ expected_val = prediction[:prediction][:expected_value]
26
+ obs = client.observe_outcome(domain: :workflow, actual: 0.8, predicted: expected_val)
27
+ expect(obs[:observed]).to be true
28
+
29
+ stats = client.predictive_processing_stats
30
+ expect(stats[:stats][:model_count]).to eq(1)
31
+ end
32
+
33
+ it 'accumulates free energy after repeated high errors' do
34
+ client.add_generative_model(domain: :stress)
35
+ 5.times { client.observe_outcome(domain: :stress, actual: 1.0, predicted: 0.0) }
36
+ fe = client.free_energy(domain: :stress)
37
+ expect(fe[:free_energy]).to be > 0.0
38
+ end
39
+
40
+ it 'identifies active inference candidate after surprise' do
41
+ client.add_generative_model(domain: :act_candidate)
42
+ 5.times { client.observe_outcome(domain: :act_candidate, actual: 1.0, predicted: 0.0) }
43
+ result = client.active_inference_candidates
44
+ expect(result[:domains]).to include(:act_candidate)
45
+ end
46
+
47
+ it 'tick decays precision over time' do
48
+ client.add_generative_model(domain: :decaying)
49
+ initial_weight = client.instance_variable_get(:@default_processor).precision_weight(:decaying)
50
+ client.update_predictive_processing
51
+ after_weight = client.instance_variable_get(:@default_processor).precision_weight(:decaying)
52
+ expect(after_weight).to be < initial_weight
53
+ end
54
+ end
55
+
56
+ describe 'constants' do
57
+ it 'exposes INFERENCE_MODES' do
58
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::INFERENCE_MODES)
59
+ .to eq(%i[perceptual active hybrid])
60
+ end
61
+
62
+ it 'exposes MODEL_STATES' do
63
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::MODEL_STATES)
64
+ .to eq(%i[stable updating exploring surprised])
65
+ end
66
+
67
+ it 'exposes FREE_ENERGY_THRESHOLD' do
68
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::FREE_ENERGY_THRESHOLD)
69
+ .to eq(0.7)
70
+ end
71
+
72
+ it 'exposes ACTIVE_INFERENCE_THRESHOLD' do
73
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::ACTIVE_INFERENCE_THRESHOLD)
74
+ .to eq(0.5)
75
+ end
76
+
77
+ it 'exposes all PRECISION_LABELS keys as ranges' do
78
+ labels = Legion::Extensions::PredictiveProcessing::Helpers::Constants::PRECISION_LABELS
79
+ expect(labels.values).to include(:certain, :confident, :uncertain, :vague, :noise)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveProcessing::Helpers::GenerativeModel do
4
+ subject(:model) { described_class.new(domain: :test_domain) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a unique id' do
8
+ expect(model.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'sets domain' do
12
+ expect(model.domain).to eq(:test_domain)
13
+ end
14
+
15
+ it 'sets default confidence' do
16
+ expect(model.confidence).to eq(described_class::DEFAULT_PRECISION)
17
+ end
18
+
19
+ it 'sets default precision' do
20
+ expect(model.precision).to eq(described_class::DEFAULT_PRECISION)
21
+ end
22
+
23
+ it 'starts with zero prediction error' do
24
+ expect(model.prediction_error).to eq(0.0)
25
+ end
26
+
27
+ it 'starts in stable state' do
28
+ expect(model.state).to eq(:stable)
29
+ end
30
+
31
+ it 'records created_at timestamp' do
32
+ expect(model.created_at).to be_a(Time)
33
+ end
34
+
35
+ it 'records updated_at timestamp' do
36
+ expect(model.updated_at).to be_a(Time)
37
+ end
38
+ end
39
+
40
+ describe '#predict' do
41
+ it 'returns a prediction hash' do
42
+ result = model.predict(context: {})
43
+ expect(result).to be_a(Hash)
44
+ end
45
+
46
+ it 'includes expected_value' do
47
+ result = model.predict(context: {})
48
+ expect(result[:expected_value]).to be_a(Float)
49
+ end
50
+
51
+ it 'includes confidence' do
52
+ result = model.predict(context: {})
53
+ expect(result[:confidence]).to eq(model.confidence)
54
+ end
55
+
56
+ it 'includes domain' do
57
+ result = model.predict(context: {})
58
+ expect(result[:domain]).to eq(:test_domain)
59
+ end
60
+
61
+ it 'applies context richness bonus' do
62
+ sparse_pred = model.predict(context: {})
63
+ rich_pred = model.predict(context: { a: 1, b: 2, c: 3, d: 4, e: 5 })
64
+ expect(rich_pred[:expected_value]).to be >= sparse_pred[:expected_value]
65
+ end
66
+
67
+ it 'caps expected_value at 1.0' do
68
+ result = model.predict(context: (1..20).to_h { |i| [i, i] })
69
+ expect(result[:expected_value]).to be <= 1.0
70
+ end
71
+
72
+ it 'stores the prediction as last_prediction' do
73
+ model.predict(context: { key: :val })
74
+ expect(model.last_prediction).not_to be_nil
75
+ end
76
+
77
+ it 'uses default empty context when none provided' do
78
+ result = model.predict
79
+ expect(result[:context_size]).to eq(0)
80
+ end
81
+ end
82
+
83
+ describe '#observe' do
84
+ it 'returns a numeric error magnitude' do
85
+ error = model.observe(actual: 0.8, predicted: 0.5)
86
+ expect(error).to be_a(Float)
87
+ end
88
+
89
+ it 'computes absolute difference' do
90
+ error = model.observe(actual: 0.8, predicted: 0.5)
91
+ expect(error).to be_within(0.01).of(0.3)
92
+ end
93
+
94
+ it 'clamps error to 0..1' do
95
+ error = model.observe(actual: 5.0, predicted: 0.0)
96
+ expect(error).to eq(1.0)
97
+ end
98
+
99
+ it 'returns 0.0 for non-numeric inputs' do
100
+ error = model.observe(actual: 'abc', predicted: 0.5)
101
+ expect(error).to eq(0.0)
102
+ end
103
+
104
+ it 'updates updated_at' do
105
+ before = model.updated_at
106
+ sleep(0.01)
107
+ model.observe(actual: 0.9, predicted: 0.1)
108
+ expect(model.updated_at).to be >= before
109
+ end
110
+ end
111
+
112
+ describe '#free_energy' do
113
+ it 'returns 0.0 with no history' do
114
+ expect(model.free_energy).to eq(0.0)
115
+ end
116
+
117
+ it 'returns a positive value after observations' do
118
+ model.observe(actual: 1.0, predicted: 0.0)
119
+ expect(model.free_energy).to be >= 0.0
120
+ end
121
+
122
+ it 'returns higher free energy for larger errors' do
123
+ low_model = described_class.new(domain: :low)
124
+ high_model = described_class.new(domain: :high)
125
+ low_model.observe(actual: 0.51, predicted: 0.5)
126
+ high_model.observe(actual: 1.0, predicted: 0.0)
127
+ expect(high_model.free_energy).to be > low_model.free_energy
128
+ end
129
+ end
130
+
131
+ describe '#stable?' do
132
+ it 'returns true when free_energy is low' do
133
+ expect(model.stable?).to be true
134
+ end
135
+
136
+ it 'returns false when surprised' do
137
+ 5.times { model.observe(actual: 1.0, predicted: 0.0) }
138
+ expect(model.stable?).to be false
139
+ end
140
+ end
141
+
142
+ describe '#surprised?' do
143
+ it 'returns false initially' do
144
+ expect(model.surprised?).to be false
145
+ end
146
+
147
+ it 'returns true after repeated large errors' do
148
+ 5.times { model.observe(actual: 1.0, predicted: 0.0) }
149
+ expect(model.surprised?).to be true
150
+ end
151
+ end
152
+
153
+ describe '#update_model' do
154
+ it 'adjusts confidence downward on positive error' do
155
+ before = model.confidence
156
+ model.update_model(error: 0.5)
157
+ expect(model.confidence).to be < before
158
+ end
159
+
160
+ it 'does not drop confidence below MODEL_CONFIDENCE_FLOOR' do
161
+ 10.times { model.update_model(error: 1.0) }
162
+ expect(model.confidence).to be >= described_class::MODEL_CONFIDENCE_FLOOR
163
+ end
164
+
165
+ it 'sets state to updating' do
166
+ model.update_model(error: 0.3)
167
+ expect(model.state).to eq(:updating)
168
+ end
169
+ end
170
+
171
+ describe '#decay' do
172
+ it 'reduces precision' do
173
+ before = model.precision
174
+ model.decay
175
+ expect(model.precision).to be < before
176
+ end
177
+
178
+ it 'does not drop precision below PRECISION_FLOOR' do
179
+ 100.times { model.decay }
180
+ expect(model.precision).to be >= described_class::PRECISION_FLOOR
181
+ end
182
+ end
183
+
184
+ describe '#precision_label' do
185
+ it 'returns :uncertain for default precision (0.5)' do
186
+ expect(model.precision_label).to eq(:uncertain)
187
+ end
188
+
189
+ it 'returns :certain for high precision' do
190
+ allow(model).to receive(:precision).and_return(0.9)
191
+ expect(model.precision_label).to eq(:certain)
192
+ end
193
+
194
+ it 'returns :noise for very low precision' do
195
+ allow(model).to receive(:precision).and_return(0.1)
196
+ expect(model.precision_label).to eq(:noise)
197
+ end
198
+
199
+ it 'returns :vague for precision around 0.3' do
200
+ allow(model).to receive(:precision).and_return(0.3)
201
+ expect(model.precision_label).to eq(:vague)
202
+ end
203
+ end
204
+
205
+ describe '#to_h' do
206
+ it 'returns a hash with all required keys' do
207
+ h = model.to_h
208
+ %i[id domain confidence precision prediction_error free_energy state
209
+ precision_label stable surprised history_size created_at updated_at].each do |key|
210
+ expect(h).to have_key(key)
211
+ end
212
+ end
213
+
214
+ it 'reflects current model state' do
215
+ model.observe(actual: 0.8, predicted: 0.3)
216
+ h = model.to_h
217
+ expect(h[:history_size]).to eq(1)
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveProcessing::Helpers::PredictiveProcessor do
4
+ subject(:processor) { described_class.new }
5
+
6
+ describe '#add_model' do
7
+ it 'adds a new model for a domain' do
8
+ result = processor.add_model(domain: :perception)
9
+ expect(result[:added]).to be true
10
+ expect(result[:domain]).to eq(:perception)
11
+ end
12
+
13
+ it 'returns model_id on success' do
14
+ result = processor.add_model(domain: :cognition)
15
+ expect(result[:model_id]).to match(/\A[0-9a-f-]{36}\z/)
16
+ end
17
+
18
+ it 'rejects duplicate domains' do
19
+ processor.add_model(domain: :dup)
20
+ result = processor.add_model(domain: :dup)
21
+ expect(result[:added]).to be false
22
+ expect(result[:reason]).to eq(:already_exists)
23
+ end
24
+
25
+ it 'rejects when at model limit' do
26
+ 20.times { |i| processor.add_model(domain: :"domain_#{i}") }
27
+ result = processor.add_model(domain: :overflow)
28
+ expect(result[:added]).to be false
29
+ expect(result[:reason]).to eq(:limit_reached)
30
+ end
31
+ end
32
+
33
+ describe '#predict' do
34
+ it 'returns a prediction hash for a known domain' do
35
+ processor.add_model(domain: :action)
36
+ result = processor.predict(domain: :action, context: { urgency: 0.8 })
37
+ expect(result[:expected_value]).to be_a(Float)
38
+ end
39
+
40
+ it 'auto-creates model for unknown domain (within limit)' do
41
+ result = processor.predict(domain: :new_domain, context: {})
42
+ expect(result[:expected_value]).to be_a(Float)
43
+ end
44
+
45
+ it 'includes domain in prediction' do
46
+ result = processor.predict(domain: :emotion, context: {})
47
+ expect(result[:domain]).to eq(:emotion)
48
+ end
49
+ end
50
+
51
+ describe '#observe' do
52
+ before { processor.add_model(domain: :vision) }
53
+
54
+ it 'returns observed: true for known domain' do
55
+ result = processor.observe(domain: :vision, actual: 0.7, predicted: 0.5)
56
+ expect(result[:observed]).to be true
57
+ end
58
+
59
+ it 'returns prediction_error' do
60
+ result = processor.observe(domain: :vision, actual: 0.8, predicted: 0.5)
61
+ expect(result[:prediction_error]).to be_a(Float)
62
+ end
63
+
64
+ it 'returns inference_mode' do
65
+ result = processor.observe(domain: :vision, actual: 0.8, predicted: 0.5)
66
+ expect(described_class::INFERENCE_MODES).to include(result[:inference_mode])
67
+ end
68
+
69
+ it 'returns observed: false for unknown domain' do
70
+ result = processor.observe(domain: :unknown, actual: 0.5, predicted: 0.5)
71
+ expect(result[:observed]).to be false
72
+ expect(result[:reason]).to eq(:domain_not_found)
73
+ end
74
+
75
+ it 'returns free_energy after observation' do
76
+ result = processor.observe(domain: :vision, actual: 1.0, predicted: 0.0)
77
+ expect(result[:free_energy]).to be_a(Float)
78
+ end
79
+ end
80
+
81
+ describe '#inference_mode' do
82
+ it 'returns :perceptual for unknown domain' do
83
+ expect(processor.inference_mode(:nonexistent)).to eq(:perceptual)
84
+ end
85
+
86
+ it 'returns a valid inference mode for known domain' do
87
+ processor.add_model(domain: :memory)
88
+ mode = processor.inference_mode(:memory)
89
+ expect(described_class::INFERENCE_MODES).to include(mode)
90
+ end
91
+
92
+ it 'returns :active or :hybrid when free energy is high' do
93
+ processor.add_model(domain: :surprise)
94
+ 5.times { processor.observe(domain: :surprise, actual: 1.0, predicted: 0.0) }
95
+ mode = processor.inference_mode(:surprise)
96
+ expect(%i[active hybrid]).to include(mode)
97
+ end
98
+ end
99
+
100
+ describe '#free_energy_for' do
101
+ it 'returns nil for unknown domain' do
102
+ expect(processor.free_energy_for(:unknown)).to be_nil
103
+ end
104
+
105
+ it 'returns a float for known domain' do
106
+ processor.add_model(domain: :planning)
107
+ expect(processor.free_energy_for(:planning)).to be_a(Float)
108
+ end
109
+ end
110
+
111
+ describe '#global_free_energy' do
112
+ it 'returns 0.0 with no models' do
113
+ expect(processor.global_free_energy).to eq(0.0)
114
+ end
115
+
116
+ it 'returns average free energy across models' do
117
+ processor.add_model(domain: :d1)
118
+ processor.add_model(domain: :d2)
119
+ expect(processor.global_free_energy).to be_a(Float)
120
+ end
121
+ end
122
+
123
+ describe '#precision_weight' do
124
+ it 'returns DEFAULT_PRECISION for unknown domain' do
125
+ expect(processor.precision_weight(:unknown)).to eq(described_class::DEFAULT_PRECISION)
126
+ end
127
+
128
+ it 'returns model precision for known domain' do
129
+ processor.add_model(domain: :known)
130
+ expect(processor.precision_weight(:known)).to be_a(Float)
131
+ end
132
+ end
133
+
134
+ describe '#models_needing_update' do
135
+ it 'returns empty hash when all models are stable' do
136
+ expect(processor.models_needing_update).to be_empty
137
+ end
138
+
139
+ it 'returns domains with high free energy' do
140
+ processor.add_model(domain: :erratic)
141
+ 5.times { processor.observe(domain: :erratic, actual: 1.0, predicted: 0.0) }
142
+ needing = processor.models_needing_update
143
+ expect(needing).to have_key(:erratic)
144
+ end
145
+ end
146
+
147
+ describe '#stable_models' do
148
+ it 'returns all models initially (all stable)' do
149
+ processor.add_model(domain: :calm)
150
+ expect(processor.stable_models).to have_key(:calm)
151
+ end
152
+
153
+ it 'excludes surprised models' do
154
+ processor.add_model(domain: :upset)
155
+ 5.times { processor.observe(domain: :upset, actual: 1.0, predicted: 0.0) }
156
+ expect(processor.stable_models).not_to have_key(:upset)
157
+ end
158
+ end
159
+
160
+ describe '#active_inference_candidates' do
161
+ it 'returns empty array when all models have low free energy' do
162
+ expect(processor.active_inference_candidates).to be_empty
163
+ end
164
+
165
+ it 'returns domains exceeding active inference threshold' do
166
+ processor.add_model(domain: :high_fe)
167
+ 5.times { processor.observe(domain: :high_fe, actual: 1.0, predicted: 0.0) }
168
+ expect(processor.active_inference_candidates).to include(:high_fe)
169
+ end
170
+ end
171
+
172
+ describe '#tick' do
173
+ it 'decays precision on all models' do
174
+ processor.add_model(domain: :tickable)
175
+ before = processor.models[:tickable].precision
176
+ processor.tick
177
+ expect(processor.models[:tickable].precision).to be < before
178
+ end
179
+
180
+ it 'does not raise with no models' do
181
+ expect { processor.tick }.not_to raise_error
182
+ end
183
+ end
184
+
185
+ describe '#to_h' do
186
+ it 'includes model_count' do
187
+ processor.add_model(domain: :counted)
188
+ expect(processor.to_h[:model_count]).to eq(1)
189
+ end
190
+
191
+ it 'includes global_free_energy' do
192
+ expect(processor.to_h[:global_free_energy]).to be_a(Float)
193
+ end
194
+
195
+ it 'includes models hash' do
196
+ processor.add_model(domain: :listed)
197
+ expect(processor.to_h[:models]).to have_key(:listed)
198
+ end
199
+
200
+ it 'includes counts for needing update and stable' do
201
+ h = processor.to_h
202
+ expect(h).to have_key(:models_needing_update)
203
+ expect(h).to have_key(:stable_model_count)
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::PredictiveProcessing::Runners::PredictiveProcessing do
4
+ let(:client) { Legion::Extensions::PredictiveProcessing::Client.new }
5
+
6
+ describe '#add_generative_model' do
7
+ it 'adds a model for a valid domain' do
8
+ result = client.add_generative_model(domain: :perception)
9
+ expect(result[:added]).to be true
10
+ expect(result[:domain]).to eq(:perception)
11
+ end
12
+
13
+ it 'returns error for nil domain' do
14
+ result = client.add_generative_model(domain: nil)
15
+ expect(result[:added]).to be false
16
+ expect(result[:reason]).to eq(:missing_domain)
17
+ end
18
+
19
+ it 'returns error for empty string domain' do
20
+ result = client.add_generative_model(domain: '')
21
+ expect(result[:added]).to be false
22
+ expect(result[:reason]).to eq(:missing_domain)
23
+ end
24
+
25
+ it 'returns model_id on success' do
26
+ result = client.add_generative_model(domain: :with_id)
27
+ expect(result[:model_id]).to match(/\A[0-9a-f-]{36}\z/)
28
+ end
29
+
30
+ it 'rejects duplicate domain' do
31
+ client.add_generative_model(domain: :dup)
32
+ result = client.add_generative_model(domain: :dup)
33
+ expect(result[:added]).to be false
34
+ expect(result[:reason]).to eq(:already_exists)
35
+ end
36
+ end
37
+
38
+ describe '#predict_from_model' do
39
+ before { client.add_generative_model(domain: :action) }
40
+
41
+ it 'returns a prediction for known domain' do
42
+ result = client.predict_from_model(domain: :action, context: {})
43
+ expect(result[:predicted]).to be true
44
+ end
45
+
46
+ it 'includes domain in result' do
47
+ result = client.predict_from_model(domain: :action)
48
+ expect(result[:domain]).to eq(:action)
49
+ end
50
+
51
+ it 'includes prediction hash' do
52
+ result = client.predict_from_model(domain: :action, context: { val: 1 })
53
+ expect(result[:prediction]).to be_a(Hash)
54
+ expect(result[:prediction][:expected_value]).to be_a(Float)
55
+ end
56
+
57
+ it 'returns error for nil domain' do
58
+ result = client.predict_from_model(domain: nil)
59
+ expect(result[:predicted]).to be false
60
+ expect(result[:reason]).to eq(:missing_domain)
61
+ end
62
+
63
+ it 'auto-creates model for unknown domain' do
64
+ result = client.predict_from_model(domain: :auto_created, context: {})
65
+ expect(result[:predicted]).to be true
66
+ end
67
+ end
68
+
69
+ describe '#observe_outcome' do
70
+ before { client.add_generative_model(domain: :vision) }
71
+
72
+ it 'returns observed: true for known domain' do
73
+ result = client.observe_outcome(domain: :vision, actual: 0.7, predicted: 0.5)
74
+ expect(result[:observed]).to be true
75
+ end
76
+
77
+ it 'includes inference_mode' do
78
+ result = client.observe_outcome(domain: :vision, actual: 0.6, predicted: 0.5)
79
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::INFERENCE_MODES)
80
+ .to include(result[:inference_mode])
81
+ end
82
+
83
+ it 'returns error for nil domain' do
84
+ result = client.observe_outcome(domain: nil, actual: 0.5, predicted: 0.5)
85
+ expect(result[:observed]).to be false
86
+ expect(result[:reason]).to eq(:missing_domain)
87
+ end
88
+
89
+ it 'returns false for unknown domain' do
90
+ result = client.observe_outcome(domain: :unknown_x, actual: 0.5, predicted: 0.5)
91
+ expect(result[:observed]).to be false
92
+ end
93
+
94
+ it 'includes free_energy in result' do
95
+ result = client.observe_outcome(domain: :vision, actual: 0.9, predicted: 0.1)
96
+ expect(result[:free_energy]).to be_a(Float)
97
+ end
98
+ end
99
+
100
+ describe '#inference_mode' do
101
+ it 'returns mode for known domain' do
102
+ client.add_generative_model(domain: :motor)
103
+ result = client.inference_mode(domain: :motor)
104
+ expect(result[:domain]).to eq(:motor)
105
+ expect(Legion::Extensions::PredictiveProcessing::Helpers::Constants::INFERENCE_MODES)
106
+ .to include(result[:mode])
107
+ end
108
+
109
+ it 'returns error for nil domain' do
110
+ result = client.inference_mode(domain: nil)
111
+ expect(result[:mode]).to be_nil
112
+ expect(result[:reason]).to eq(:missing_domain)
113
+ end
114
+
115
+ it 'returns :perceptual for unknown domain' do
116
+ result = client.inference_mode(domain: :unregistered)
117
+ expect(result[:mode]).to eq(:perceptual)
118
+ end
119
+ end
120
+
121
+ describe '#free_energy' do
122
+ it 'returns global free energy when no domain given' do
123
+ result = client.free_energy
124
+ expect(result).to have_key(:global_free_energy)
125
+ end
126
+
127
+ it 'returns domain free energy when domain given' do
128
+ client.add_generative_model(domain: :fe_domain)
129
+ result = client.free_energy(domain: :fe_domain)
130
+ expect(result[:domain]).to eq(:fe_domain)
131
+ expect(result[:free_energy]).to be_a(Float)
132
+ end
133
+
134
+ it 'returns not_found for unknown domain' do
135
+ result = client.free_energy(domain: :no_such)
136
+ expect(result[:reason]).to eq(:domain_not_found)
137
+ end
138
+ end
139
+
140
+ describe '#models_needing_update' do
141
+ it 'returns count and models hash' do
142
+ result = client.models_needing_update
143
+ expect(result).to have_key(:count)
144
+ expect(result).to have_key(:models)
145
+ end
146
+
147
+ it 'returns 0 when no models exist' do
148
+ expect(client.models_needing_update[:count]).to eq(0)
149
+ end
150
+
151
+ it 'returns surprised models' do
152
+ client.add_generative_model(domain: :volatile)
153
+ 5.times { client.observe_outcome(domain: :volatile, actual: 1.0, predicted: 0.0) }
154
+ result = client.models_needing_update
155
+ expect(result[:count]).to be >= 1
156
+ end
157
+ end
158
+
159
+ describe '#active_inference_candidates' do
160
+ it 'returns count and domains array' do
161
+ result = client.active_inference_candidates
162
+ expect(result).to have_key(:count)
163
+ expect(result).to have_key(:domains)
164
+ end
165
+
166
+ it 'returns 0 when no high-free-energy models' do
167
+ expect(client.active_inference_candidates[:count]).to eq(0)
168
+ end
169
+
170
+ it 'includes domains with high free energy' do
171
+ client.add_generative_model(domain: :erratic)
172
+ 5.times { client.observe_outcome(domain: :erratic, actual: 1.0, predicted: 0.0) }
173
+ result = client.active_inference_candidates
174
+ expect(result[:domains]).to include(:erratic)
175
+ end
176
+ end
177
+
178
+ describe '#update_predictive_processing' do
179
+ it 'ticks the processor' do
180
+ result = client.update_predictive_processing
181
+ expect(result[:ticked]).to be true
182
+ end
183
+
184
+ it 'reports model count' do
185
+ client.add_generative_model(domain: :tracked)
186
+ result = client.update_predictive_processing
187
+ expect(result[:model_count]).to eq(1)
188
+ end
189
+ end
190
+
191
+ describe '#predictive_processing_stats' do
192
+ it 'returns success: true' do
193
+ result = client.predictive_processing_stats
194
+ expect(result[:success]).to be true
195
+ end
196
+
197
+ it 'returns stats hash' do
198
+ result = client.predictive_processing_stats
199
+ expect(result[:stats]).to be_a(Hash)
200
+ end
201
+
202
+ it 'includes global_free_energy in stats' do
203
+ result = client.predictive_processing_stats
204
+ expect(result[:stats]).to have_key(:global_free_energy)
205
+ end
206
+
207
+ it 'reflects added models in stats' do
208
+ client.add_generative_model(domain: :counted)
209
+ result = client.predictive_processing_stats
210
+ expect(result[:stats][:model_count]).to eq(1)
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/predictive_processing/helpers/constants'
4
+ require 'legion/extensions/predictive_processing/helpers/generative_model'
5
+ require 'legion/extensions/predictive_processing/helpers/predictive_processor'
6
+ require 'legion/extensions/predictive_processing/runners/predictive_processing'
7
+ require 'legion/extensions/predictive_processing/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Helpers
12
+ module Lex; end
13
+ end
14
+ end
15
+ end
16
+
17
+ module Legion
18
+ module Logging
19
+ def self.method_missing(*); end
20
+
21
+ def self.respond_to_missing?(*) = true
22
+ end
23
+ end
24
+
25
+ RSpec.configure do |config|
26
+ config.example_status_persistence_file_path = '.rspec_status'
27
+ config.disable_monkey_patching!
28
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
29
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-predictive-processing
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: 'Andy Clark''s Predictive Processing / Active Inference for brain-modeled
27
+ agentic AI: generative models, precision-weighting, active inference, and free energy
28
+ minimization'
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - lex-predictive-processing.gemspec
37
+ - lib/legion/extensions/predictive_processing.rb
38
+ - lib/legion/extensions/predictive_processing/client.rb
39
+ - lib/legion/extensions/predictive_processing/helpers/constants.rb
40
+ - lib/legion/extensions/predictive_processing/helpers/generative_model.rb
41
+ - lib/legion/extensions/predictive_processing/helpers/predictive_processor.rb
42
+ - lib/legion/extensions/predictive_processing/runners/predictive_processing.rb
43
+ - lib/legion/extensions/predictive_processing/version.rb
44
+ - spec/legion/extensions/predictive_processing/client_spec.rb
45
+ - spec/legion/extensions/predictive_processing/helpers/generative_model_spec.rb
46
+ - spec/legion/extensions/predictive_processing/helpers/predictive_processor_spec.rb
47
+ - spec/legion/extensions/predictive_processing/runners/predictive_processing_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-predictive-processing
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-predictive-processing
54
+ source_code_uri: https://github.com/LegionIO/lex-predictive-processing
55
+ documentation_uri: https://github.com/LegionIO/lex-predictive-processing
56
+ changelog_uri: https://github.com/LegionIO/lex-predictive-processing
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-predictive-processing/issues
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.4'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.9
74
+ specification_version: 4
75
+ summary: LEX Predictive Processing
76
+ test_files: []