lex-learning-rate 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: 1b65632c8f5e9197247406e7b85c36ca70b05c191410904d854146d5c9bae097
4
+ data.tar.gz: e00c09af41cdd7b301f987c302fdbf147ffb6db418af395b08d4ade3d3cf5ef5
5
+ SHA512:
6
+ metadata.gz: bcb141f0edeae5372798eee9488d884eba72ce9afd87a953db5a4df939f852103846742ea1dee7f8608695e4422bb4099c37d6b17a7566f03d1112810a903407
7
+ data.tar.gz: bc7c36f9d70572b67c25c9ff60c243551d6a7ac5c06a0fbb987ce158327ec324c238190a9d499544ec2bcfbfe1b1fc9ff667816e37bc278ec9d5f5fec4ef6cae
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # lex-learning-rate
2
+
3
+ Adaptive learning rate management for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
4
+
5
+ ## What It Does
6
+
7
+ `lex-learning-rate` maintains per-domain learning rates that adapt to the agent's prediction performance. Wrong predictions raise the rate (more learning needed), correct predictions lower it (stable enough), and surprise or error events provide temporary boosts. Rates mean-revert toward a default over time. Rolling accuracy windows give a per-domain reliability signal.
8
+
9
+ Key capabilities:
10
+
11
+ - **Per-domain rates**: independent learning rate per knowledge domain
12
+ - **Prediction-driven adjustment**: wrong prediction +0.03, correct prediction -0.02
13
+ - **Event boosts**: surprise +0.05, error +0.04
14
+ - **Mean reversion**: rates drift back toward 0.15 default via decay
15
+ - **Rate labels**: very_slow / slow / moderate / fast / very_fast per rate value
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'lex-learning-rate'
23
+ ```
24
+
25
+ Or install directly:
26
+
27
+ ```
28
+ gem install lex-learning-rate
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ require 'legion/extensions/learning_rate'
35
+
36
+ client = Legion::Extensions::LearningRate::Client.new
37
+
38
+ # Record prediction outcomes
39
+ client.record_prediction(domain: :networking, correct: false)
40
+ client.record_prediction(domain: :networking, correct: false)
41
+
42
+ # Record surprise or error events
43
+ client.record_surprise(domain: :networking)
44
+
45
+ # Check current rate
46
+ result = client.current_rate(domain: :networking)
47
+ # => { domain: :networking, rate: 0.27, label: :fast, accuracy: 0.3 }
48
+
49
+ # Find domains with highest/lowest learning rates
50
+ client.fastest_domains(limit: 3)
51
+ client.slowest_domains(limit: 3)
52
+
53
+ # Feed tick results (auto-extracts prediction outcomes)
54
+ client.update_learning_rate(tick_results: tick_phase_results)
55
+
56
+ # Stats
57
+ client.learning_rate_stats
58
+ ```
59
+
60
+ ## Runner Methods
61
+
62
+ | Method | Description |
63
+ |---|---|
64
+ | `record_prediction` | Record a prediction outcome and adjust the domain rate |
65
+ | `record_surprise` | Apply a surprise boost to a domain's rate |
66
+ | `record_error` | Apply an error boost to a domain's rate |
67
+ | `current_rate` | Current rate value and label for a domain |
68
+ | `fastest_domains` | Top N domains by current learning rate |
69
+ | `slowest_domains` | Bottom N domains by current learning rate |
70
+ | `update_learning_rate` | Extract prediction outcomes from tick results and record |
71
+ | `learning_rate_stats` | Domain count, avg rate, fastest/slowest, overall accuracy |
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ bundle install
77
+ bundle exec rspec
78
+ bundle exec rubocop
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/learning_rate/helpers/constants'
4
+ require 'legion/extensions/learning_rate/helpers/rate_model'
5
+ require 'legion/extensions/learning_rate/runners/learning_rate'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module LearningRate
10
+ class Client
11
+ include Runners::LearningRate
12
+
13
+ attr_reader :rate_model
14
+
15
+ def initialize(rate_model: nil, **)
16
+ @rate_model = rate_model || Helpers::RateModel.new
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module LearningRate
6
+ module Helpers
7
+ module Constants
8
+ DEFAULT_RATE = 0.15
9
+ MIN_RATE = 0.01
10
+ MAX_RATE = 0.5
11
+ RATE_INCREASE = 0.03
12
+ RATE_DECREASE = 0.02
13
+ RATE_DECAY = 0.005
14
+ ACCURACY_WINDOW = 20
15
+ SURPRISE_BOOST = 0.05
16
+ ERROR_BOOST = 0.04
17
+ CONFIDENCE_DAMPENING = 0.03
18
+ MAX_DOMAINS = 50
19
+ MAX_RATE_HISTORY = 200
20
+
21
+ RATE_LABELS = {
22
+ (0.3..) => :fast_learning,
23
+ (0.15...0.3) => :moderate_learning,
24
+ (0.05...0.15) => :slow_learning,
25
+ (..0.05) => :consolidated
26
+ }.freeze
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module LearningRate
6
+ module Helpers
7
+ class RateModel
8
+ include Constants
9
+
10
+ attr_reader :rates, :accuracy_buffers, :rate_history
11
+
12
+ def initialize
13
+ @rates = {}
14
+ @accuracy_buffers = {}
15
+ @rate_history = []
16
+ end
17
+
18
+ def rate_for(domain)
19
+ @rates.fetch(domain, DEFAULT_RATE)
20
+ end
21
+
22
+ def record_prediction(domain:, correct:)
23
+ ensure_domain(domain)
24
+ @accuracy_buffers[domain] << (correct ? 1.0 : 0.0)
25
+ @accuracy_buffers[domain].shift while @accuracy_buffers[domain].size > ACCURACY_WINDOW
26
+ adjust_rate(domain, correct: correct)
27
+ end
28
+
29
+ def record_surprise(domain:, magnitude:)
30
+ ensure_domain(domain)
31
+ boost = magnitude * SURPRISE_BOOST
32
+ @rates[domain] = (@rates[domain] + boost).clamp(MIN_RATE, MAX_RATE)
33
+ record_event(domain, :surprise, @rates[domain])
34
+ end
35
+
36
+ def record_error(domain:, magnitude:)
37
+ ensure_domain(domain)
38
+ boost = magnitude * ERROR_BOOST
39
+ @rates[domain] = (@rates[domain] + boost).clamp(MIN_RATE, MAX_RATE)
40
+ record_event(domain, :error, @rates[domain])
41
+ end
42
+
43
+ def accuracy_for(domain)
44
+ buffer = @accuracy_buffers.fetch(domain, [])
45
+ return 0.0 if buffer.empty?
46
+
47
+ buffer.sum / buffer.size
48
+ end
49
+
50
+ def decay
51
+ @rates.each_key do |domain|
52
+ current = @rates[domain]
53
+ delta = (current - DEFAULT_RATE) * RATE_DECAY
54
+ @rates[domain] = (current - delta).clamp(MIN_RATE, MAX_RATE)
55
+ end
56
+ end
57
+
58
+ def label_for(domain)
59
+ rate = rate_for(domain)
60
+ RATE_LABELS.each do |range, lbl|
61
+ return lbl if range.cover?(rate)
62
+ end
63
+ :consolidated
64
+ end
65
+
66
+ def fastest_domains(count = 5)
67
+ @rates.sort_by { |_, r| -r }.first(count).to_h
68
+ end
69
+
70
+ def slowest_domains(count = 5)
71
+ @rates.sort_by { |_, r| r }.first(count).to_h
72
+ end
73
+
74
+ def overall_rate
75
+ return DEFAULT_RATE if @rates.empty?
76
+
77
+ @rates.values.sum / @rates.size
78
+ end
79
+
80
+ def domain_count
81
+ @rates.size
82
+ end
83
+
84
+ def to_h
85
+ {
86
+ domain_count: @rates.size,
87
+ overall_rate: overall_rate.round(4),
88
+ rates: @rates.dup,
89
+ history_size: @rate_history.size
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ def ensure_domain(domain)
96
+ @rates[domain] ||= DEFAULT_RATE
97
+ @accuracy_buffers[domain] ||= []
98
+ trim_domains(protect: domain) if @rates.size > MAX_DOMAINS
99
+ end
100
+
101
+ def adjust_rate(domain, correct:)
102
+ @rates[domain] = if correct
103
+ (@rates[domain] - RATE_DECREASE).clamp(MIN_RATE, MAX_RATE)
104
+ else
105
+ (@rates[domain] + RATE_INCREASE).clamp(MIN_RATE, MAX_RATE)
106
+ end
107
+ record_event(domain, correct ? :correct : :incorrect, @rates[domain])
108
+ end
109
+
110
+ def record_event(domain, event_type, rate)
111
+ @rate_history << { domain: domain, event: event_type, rate: rate, at: Time.now.utc }
112
+ @rate_history.shift while @rate_history.size > MAX_RATE_HISTORY
113
+ end
114
+
115
+ def trim_domains(protect: nil)
116
+ candidates = @rates.reject { |k, _| k == protect }
117
+ sorted = candidates.sort_by { |_, r| (r - DEFAULT_RATE).abs }
118
+ excess = @rates.size - MAX_DOMAINS
119
+ remove_keys = sorted.first(excess).map(&:first)
120
+ remove_keys.each do |key|
121
+ @rates.delete(key)
122
+ @accuracy_buffers.delete(key)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module LearningRate
6
+ module Runners
7
+ module LearningRate
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def record_prediction(correct:, domain: :general, **)
12
+ rate_model.record_prediction(domain: domain, correct: correct)
13
+ rate = rate_model.rate_for(domain)
14
+ accuracy = rate_model.accuracy_for(domain)
15
+ Legion::Logging.debug "[learning_rate] prediction: domain=#{domain} correct=#{correct} rate=#{rate.round(3)} accuracy=#{accuracy.round(3)}"
16
+ {
17
+ success: true,
18
+ domain: domain,
19
+ rate: rate,
20
+ accuracy: accuracy,
21
+ label: rate_model.label_for(domain)
22
+ }
23
+ end
24
+
25
+ def record_surprise(magnitude:, domain: :general, **)
26
+ rate_model.record_surprise(domain: domain, magnitude: magnitude)
27
+ rate = rate_model.rate_for(domain)
28
+ Legion::Logging.debug "[learning_rate] surprise: domain=#{domain} magnitude=#{magnitude.round(3)} rate=#{rate.round(3)}"
29
+ { success: true, domain: domain, rate: rate, label: rate_model.label_for(domain) }
30
+ end
31
+
32
+ def record_error(magnitude:, domain: :general, **)
33
+ rate_model.record_error(domain: domain, magnitude: magnitude)
34
+ rate = rate_model.rate_for(domain)
35
+ Legion::Logging.debug "[learning_rate] error: domain=#{domain} magnitude=#{magnitude.round(3)} rate=#{rate.round(3)}"
36
+ { success: true, domain: domain, rate: rate, label: rate_model.label_for(domain) }
37
+ end
38
+
39
+ def current_rate(domain: :general, **)
40
+ rate = rate_model.rate_for(domain)
41
+ accuracy = rate_model.accuracy_for(domain)
42
+ {
43
+ success: true,
44
+ domain: domain,
45
+ rate: rate,
46
+ accuracy: accuracy,
47
+ label: rate_model.label_for(domain)
48
+ }
49
+ end
50
+
51
+ def fastest_domains(count: 5, **)
52
+ domains = rate_model.fastest_domains(count)
53
+ { success: true, domains: domains, count: domains.size }
54
+ end
55
+
56
+ def slowest_domains(count: 5, **)
57
+ domains = rate_model.slowest_domains(count)
58
+ { success: true, domains: domains, count: domains.size }
59
+ end
60
+
61
+ def update_learning_rate(**)
62
+ rate_model.decay
63
+ overall = rate_model.overall_rate
64
+ Legion::Logging.debug "[learning_rate] tick: domains=#{rate_model.domain_count} overall=#{overall.round(3)}"
65
+ { success: true, domain_count: rate_model.domain_count, overall_rate: overall }
66
+ end
67
+
68
+ def learning_rate_stats(**)
69
+ { success: true, stats: rate_model.to_h }
70
+ end
71
+
72
+ private
73
+
74
+ def rate_model
75
+ @rate_model ||= Helpers::RateModel.new
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module LearningRate
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/learning_rate/version'
4
+ require 'legion/extensions/learning_rate/helpers/constants'
5
+ require 'legion/extensions/learning_rate/helpers/rate_model'
6
+ require 'legion/extensions/learning_rate/runners/learning_rate'
7
+ require 'legion/extensions/learning_rate/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module LearningRate
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-learning-rate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
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: Adapts the agent learning speed per domain based on prediction accuracy,
27
+ surprise, and errors. Fast learning when predictions fail, slow consolidation when
28
+ accurate — the agent learns how fast to learn.
29
+ email:
30
+ - matt@iverson.io
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - lib/legion/extensions/learning_rate.rb
37
+ - lib/legion/extensions/learning_rate/client.rb
38
+ - lib/legion/extensions/learning_rate/helpers/constants.rb
39
+ - lib/legion/extensions/learning_rate/helpers/rate_model.rb
40
+ - lib/legion/extensions/learning_rate/runners/learning_rate.rb
41
+ - lib/legion/extensions/learning_rate/version.rb
42
+ homepage: https://github.com/LegionIO/lex-learning-rate
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.6.9
62
+ specification_version: 4
63
+ summary: Meta-learning rate adaptation for LegionIO
64
+ test_files: []