lex-mood 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: 52558938da1dfd56dce95ce2b1a368f843cc9d843b7c82276d9c75ed1ea832fa
4
+ data.tar.gz: cc469d9968204fb7c8087e8b52d41508ce8bbef4955f46d16bc57db88eed75d7
5
+ SHA512:
6
+ metadata.gz: 973d7eefaf080e8267db5f01d9e5b866bf3c623777f4ed72456549f9e9d65373f0142ec6493338102b92a9fb04ecb783e3887bcfab5264b86039e382511bb591
7
+ data.tar.gz: 5b88fdebece4cbf60e544e4372d5b6c213c1f131081c7f83f598e5d719f821b9f945cb9cc06adc8f3d02cb1c965fe09b2de5ee8f225fe2ea63a08489a1131e10
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :dev, :test do
8
+ gem 'rspec', '~> 3.12'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec', '~> 3.0'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Iverson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # lex-mood
2
+
3
+ Persistent mood state for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
4
+
5
+ ## What It Does
6
+
7
+ `lex-mood` models the agent's sustained background affect — distinct from acute emotion. Where emotion is a reactive signal spike, mood is a slow-moving four-dimensional state (valence, arousal, energy, stability) updated via EMA. The current mood classification (one of nine states) emits modulation values that bias attention, risk tolerance, and curiosity across the cognitive architecture.
8
+
9
+ Key capabilities:
10
+
11
+ - **Nine mood states**: serene, content, curious, energized, anxious, frustrated, melancholic, flat, neutral
12
+ - **Four dimensions**: valence, arousal, energy, stability — all EMA-smoothed
13
+ - **Inertia**: each mood has a resistance coefficient preventing rapid oscillation
14
+ - **Modulations**: per-mood attention_threshold, risk_tolerance, and curiosity_boost values
15
+ - **Mood trend**: improving / stable / declining based on recent history
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem 'lex-mood'
23
+ ```
24
+
25
+ Or install directly:
26
+
27
+ ```
28
+ gem install lex-mood
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ require 'legion/extensions/mood'
35
+
36
+ client = Legion::Extensions::Mood::Client.new
37
+
38
+ # Update mood from tick results
39
+ result = client.update_mood(tick_results: tick_phase_results)
40
+ # => { mood: :curious, valence: 0.62, arousal: 0.55, energy: 0.7,
41
+ # modulations: { curiosity_boost: 0.2, attention_threshold: 0.2 } }
42
+
43
+ # Query current mood
44
+ client.current_mood
45
+ # => { mood: :curious, valence: 0.62, arousal: 0.55, energy: 0.7, stability: 0.8 }
46
+
47
+ # Query a specific modulation parameter
48
+ client.mood_modulation(parameter: :risk_tolerance)
49
+ # => { parameter: :risk_tolerance, modulation: 0.5, current_mood: :curious }
50
+
51
+ # View mood history
52
+ client.mood_history(limit: 10)
53
+
54
+ # Summary stats
55
+ client.mood_stats
56
+ # => { current_mood: :curious, dominant_mood: :content, trend: :stable, ... }
57
+ ```
58
+
59
+ ## Mood States and Modulations
60
+
61
+ | Mood | Attention Threshold | Risk Tolerance | Curiosity Boost |
62
+ |---|---|---|---|
63
+ | serene | 0.3 | 0.5 | 0.1 |
64
+ | content | 0.4 | 0.5 | 0.15 |
65
+ | curious | 0.2 | 0.5 | 0.2 |
66
+ | energized | 0.3 | 0.65 | 0.1 |
67
+ | anxious | 0.1 | 0.2 | 0.05 |
68
+ | frustrated | 0.5 | 0.4 | 0.05 |
69
+ | melancholic | 0.5 | 0.35 | 0.05 |
70
+ | flat | 0.6 | 0.45 | 0.02 |
71
+ | neutral | 0.4 | 0.5 | 0.1 |
72
+
73
+ ## Runner Methods
74
+
75
+ | Method | Description |
76
+ |---|---|
77
+ | `update_mood` | Extract valence/arousal/energy from tick results and update state |
78
+ | `current_mood` | Current mood symbol and all dimension values |
79
+ | `mood_modulation` | Specific modulation value for the current mood |
80
+ | `mood_history` | Recent mood state history |
81
+ | `mood_stats` | Current mood, dominant mood, trend, modulation table |
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ bundle install
87
+ bundle exec rspec
88
+ bundle exec rubocop
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
data/lex-mood.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/mood/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-mood'
7
+ spec.version = Legion::Extensions::Mood::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Mood'
12
+ spec.description = 'Persistent mood state that emerges from emotional patterns and biases cognitive processing'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-mood'
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-mood'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-mood'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-mood'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-mood/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-mood.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mood
6
+ class Client
7
+ include Runners::Mood
8
+
9
+ attr_reader :mood_state
10
+
11
+ def initialize(mood_state: nil, **)
12
+ @mood_state = mood_state || Helpers::MoodState.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mood
6
+ module Helpers
7
+ module Constants
8
+ # Mood states (valence x arousal quadrants + neutral)
9
+ MOOD_STATES = %i[
10
+ serene
11
+ content
12
+ curious
13
+ energized
14
+ anxious
15
+ frustrated
16
+ melancholic
17
+ flat
18
+ neutral
19
+ ].freeze
20
+
21
+ # Mood dimensions
22
+ DIMENSIONS = %i[valence arousal energy stability].freeze
23
+
24
+ # EMA alpha for mood updates (slow — mood is resistant to change)
25
+ MOOD_ALPHA = 0.1
26
+
27
+ # How many ticks before mood recalculates
28
+ UPDATE_INTERVAL = 5
29
+
30
+ # Mood history capacity
31
+ MAX_MOOD_HISTORY = 200
32
+
33
+ # Dimension ranges for mood classification
34
+ MOOD_CLASSIFICATION = {
35
+ serene: { valence: (0.3..), arousal: (..0.3), energy: (0.3..) },
36
+ content: { valence: (0.3..), arousal: (0.3..0.6) },
37
+ curious: { valence: (0.1..), arousal: (0.4..0.7), energy: (0.4..) },
38
+ energized: { valence: (0.2..), arousal: (0.7..) },
39
+ anxious: { valence: (..0.3), arousal: (0.6..) },
40
+ frustrated: { valence: (..-0.1), arousal: (0.4..0.8) },
41
+ melancholic: { valence: (..-0.1), arousal: (..0.4) },
42
+ flat: { valence: (-0.2..0.2), arousal: (..0.3), energy: (..0.3) }
43
+ }.freeze
44
+
45
+ # Modulation effects: how each mood biases cognitive processing
46
+ MOOD_MODULATIONS = {
47
+ serene: { attention_threshold: -0.1, risk_tolerance: 0.1, curiosity_boost: 0.0 },
48
+ content: { attention_threshold: 0.0, risk_tolerance: 0.05, curiosity_boost: 0.05 },
49
+ curious: { attention_threshold: -0.15, risk_tolerance: 0.1, curiosity_boost: 0.2 },
50
+ energized: { attention_threshold: -0.05, risk_tolerance: 0.15, curiosity_boost: 0.1 },
51
+ anxious: { attention_threshold: -0.2, risk_tolerance: -0.2, curiosity_boost: -0.1 },
52
+ frustrated: { attention_threshold: 0.1, risk_tolerance: -0.1, curiosity_boost: -0.15 },
53
+ melancholic: { attention_threshold: 0.15, risk_tolerance: -0.15, curiosity_boost: -0.2 },
54
+ flat: { attention_threshold: 0.2, risk_tolerance: 0.0, curiosity_boost: -0.1 },
55
+ neutral: { attention_threshold: 0.0, risk_tolerance: 0.0, curiosity_boost: 0.0 }
56
+ }.freeze
57
+
58
+ # Mood inertia — how resistant each mood is to change
59
+ MOOD_INERTIA = {
60
+ serene: 0.7,
61
+ content: 0.6,
62
+ curious: 0.4,
63
+ energized: 0.3,
64
+ anxious: 0.5,
65
+ frustrated: 0.5,
66
+ melancholic: 0.8,
67
+ flat: 0.9,
68
+ neutral: 0.2
69
+ }.freeze
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mood
6
+ module Helpers
7
+ class MoodState
8
+ attr_reader :current_mood, :valence, :arousal, :energy, :stability, :history, :tick_counter
9
+
10
+ def initialize
11
+ @valence = 0.5
12
+ @arousal = 0.3
13
+ @energy = 0.5
14
+ @stability = 0.8
15
+ @current_mood = :neutral
16
+ @history = []
17
+ @tick_counter = 0
18
+ end
19
+
20
+ def update(inputs)
21
+ @tick_counter += 1
22
+ return @current_mood unless (@tick_counter % Constants::UPDATE_INTERVAL).zero?
23
+
24
+ alpha = effective_alpha
25
+ @valence = ema(@valence, inputs[:valence] || @valence, alpha)
26
+ @arousal = ema(@arousal, inputs[:arousal] || @arousal, alpha)
27
+ @energy = ema(@energy, inputs[:energy] || @energy, alpha)
28
+
29
+ compute_stability
30
+ classify_mood
31
+ record_history
32
+
33
+ @current_mood
34
+ end
35
+
36
+ def modulations
37
+ Constants::MOOD_MODULATIONS.fetch(@current_mood, Constants::MOOD_MODULATIONS[:neutral])
38
+ end
39
+
40
+ def inertia
41
+ Constants::MOOD_INERTIA.fetch(@current_mood, 0.5)
42
+ end
43
+
44
+ def duration_in_current_mood
45
+ return 0 if @history.empty?
46
+
47
+ consecutive = 0
48
+ @history.reverse_each do |entry|
49
+ break unless entry[:mood] == @current_mood
50
+
51
+ consecutive += 1
52
+ end
53
+ consecutive * Constants::UPDATE_INTERVAL
54
+ end
55
+
56
+ def mood_trend(window: 20)
57
+ recent = @history.last(window)
58
+ return :insufficient_data if recent.size < 3
59
+
60
+ valences = recent.map { |h| h[:valence] }
61
+ avg_first = valences[0...(valences.size / 2)].sum / (valences.size / 2).to_f
62
+ avg_second = valences[(valences.size / 2)..].sum / (valences.size - (valences.size / 2)).to_f
63
+
64
+ delta = avg_second - avg_first
65
+ if delta > 0.05
66
+ :improving
67
+ elsif delta < -0.05
68
+ :declining
69
+ else
70
+ :stable
71
+ end
72
+ end
73
+
74
+ def to_h
75
+ {
76
+ current_mood: @current_mood,
77
+ valence: @valence.round(3),
78
+ arousal: @arousal.round(3),
79
+ energy: @energy.round(3),
80
+ stability: @stability.round(3),
81
+ modulations: modulations,
82
+ inertia: inertia,
83
+ duration: duration_in_current_mood,
84
+ trend: mood_trend,
85
+ history_size: @history.size
86
+ }
87
+ end
88
+
89
+ private
90
+
91
+ def effective_alpha
92
+ base_alpha = Constants::MOOD_ALPHA
93
+ current_inertia = inertia
94
+ base_alpha * (1.0 - (current_inertia * 0.5))
95
+ end
96
+
97
+ def ema(current, observed, alpha)
98
+ ((current * (1.0 - alpha)) + (observed * alpha)).clamp(0.0, 1.0)
99
+ end
100
+
101
+ def compute_stability
102
+ return if @history.size < 3
103
+
104
+ recent_moods = @history.last(10).map { |h| h[:mood] }
105
+ unique_moods = recent_moods.uniq.size
106
+ @stability = (1.0 - (unique_moods.to_f / [recent_moods.size, 1].max)).clamp(0.0, 1.0)
107
+ end
108
+
109
+ def classify_mood
110
+ best_match = :neutral
111
+ best_score = 0
112
+
113
+ Constants::MOOD_CLASSIFICATION.each do |mood, criteria|
114
+ score = match_score(criteria)
115
+ if score > best_score
116
+ best_score = score
117
+ best_match = mood
118
+ end
119
+ end
120
+
121
+ @current_mood = best_match
122
+ end
123
+
124
+ def match_score(criteria)
125
+ matched = 0
126
+ total = criteria.size
127
+
128
+ matched += 1 if criteria[:valence]&.cover?(@valence)
129
+ matched += 1 if criteria[:arousal]&.cover?(@arousal)
130
+ matched += 1 if criteria[:energy]&.cover?(@energy)
131
+
132
+ matched.to_f / [total, 1].max
133
+ end
134
+
135
+ def record_history
136
+ @history << {
137
+ mood: @current_mood,
138
+ valence: @valence,
139
+ arousal: @arousal,
140
+ energy: @energy,
141
+ stability: @stability,
142
+ at: Time.now.utc
143
+ }
144
+ @history = @history.last(Constants::MAX_MOOD_HISTORY)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mood
6
+ module Runners
7
+ module Mood
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def update_mood(tick_results: {}, **)
12
+ inputs = extract_mood_inputs(tick_results)
13
+ mood_state.update(inputs)
14
+
15
+ Legion::Logging.debug "[mood] #{mood_state.current_mood} (v=#{mood_state.valence.round(2)} " \
16
+ "a=#{mood_state.arousal.round(2)} e=#{mood_state.energy.round(2)})"
17
+
18
+ {
19
+ mood: mood_state.current_mood,
20
+ valence: mood_state.valence,
21
+ arousal: mood_state.arousal,
22
+ energy: mood_state.energy,
23
+ stability: mood_state.stability,
24
+ modulations: mood_state.modulations
25
+ }
26
+ end
27
+
28
+ def current_mood(**)
29
+ Legion::Logging.debug "[mood] query: #{mood_state.current_mood}"
30
+ mood_state.to_h
31
+ end
32
+
33
+ def mood_modulation(parameter:, **)
34
+ mods = mood_state.modulations
35
+ value = mods[parameter.to_sym]
36
+
37
+ Legion::Logging.debug "[mood] modulation: #{parameter}=#{value} (mood=#{mood_state.current_mood})"
38
+
39
+ {
40
+ parameter: parameter.to_sym,
41
+ modulation: value || 0.0,
42
+ current_mood: mood_state.current_mood
43
+ }
44
+ end
45
+
46
+ def mood_history(limit: 20, **)
47
+ history = mood_state.history.last(limit)
48
+ {
49
+ entries: history.map { |h| { mood: h[:mood], valence: h[:valence], arousal: h[:arousal], at: h[:at] } },
50
+ trend: mood_state.mood_trend(window: limit),
51
+ count: history.size
52
+ }
53
+ end
54
+
55
+ def mood_stats(**)
56
+ history = mood_state.history
57
+ mood_counts = Hash.new(0)
58
+ history.each { |h| mood_counts[h[:mood]] += 1 }
59
+
60
+ dominant = mood_counts.max_by { |_, v| v }&.first
61
+
62
+ {
63
+ current_mood: mood_state.current_mood,
64
+ stability: mood_state.stability,
65
+ duration: mood_state.duration_in_current_mood,
66
+ trend: mood_state.mood_trend,
67
+ dominant_mood: dominant,
68
+ mood_distribution: mood_counts,
69
+ ticks_processed: mood_state.tick_counter
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def mood_state
76
+ @mood_state ||= Helpers::MoodState.new
77
+ end
78
+
79
+ def extract_mood_inputs(tick_results)
80
+ inputs = {}
81
+
82
+ emotion = tick_results[:emotional_evaluation]
83
+ if emotion.is_a?(Hash)
84
+ inputs[:valence] = normalize_emotional_valence(emotion)
85
+ inputs[:arousal] = emotion[:arousal] || emotion[:magnitude] || 0.3
86
+ end
87
+
88
+ inputs[:energy] = extract_energy(tick_results)
89
+
90
+ inputs
91
+ end
92
+
93
+ def normalize_emotional_valence(emotion)
94
+ if emotion[:valence].is_a?(Hash)
95
+ dims = emotion[:valence]
96
+ positive = (dims[:importance] || 0) + (dims[:familiarity] || 0)
97
+ negative = (dims[:urgency] || 0) + (dims[:novelty] || 0)
98
+ ((positive - negative + 1.0) / 2.0).clamp(0.0, 1.0)
99
+ elsif emotion[:magnitude].is_a?(Numeric)
100
+ (emotion[:magnitude] + 0.5).clamp(0.0, 1.0)
101
+ else
102
+ 0.5
103
+ end
104
+ end
105
+
106
+ def extract_energy(tick_results)
107
+ load = tick_results[:elapsed]
108
+ budget = tick_results[:budget]
109
+ return 0.5 unless load.is_a?(Numeric) && budget.is_a?(Numeric) && budget.positive?
110
+
111
+ utilization = load / budget
112
+ (1.0 - utilization).clamp(0.0, 1.0)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mood
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/mood/version'
4
+ require 'legion/extensions/mood/helpers/constants'
5
+ require 'legion/extensions/mood/helpers/mood_state'
6
+ require 'legion/extensions/mood/runners/mood'
7
+ require 'legion/extensions/mood/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Mood
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
+ RSpec.describe Legion::Extensions::Mood::Client do
4
+ it 'creates default mood state' do
5
+ client = described_class.new
6
+ expect(client.mood_state).to be_a(Legion::Extensions::Mood::Helpers::MoodState)
7
+ end
8
+
9
+ it 'accepts injected mood state' do
10
+ state = Legion::Extensions::Mood::Helpers::MoodState.new
11
+ client = described_class.new(mood_state: state)
12
+ expect(client.mood_state).to equal(state)
13
+ end
14
+
15
+ it 'includes Mood runner methods' do
16
+ client = described_class.new
17
+ expect(client).to respond_to(:update_mood, :current_mood, :mood_modulation,
18
+ :mood_history, :mood_stats)
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mood::Helpers::Constants do
4
+ it 'defines 9 mood states' do
5
+ expect(described_class::MOOD_STATES.size).to eq(9)
6
+ end
7
+
8
+ it 'defines 4 dimensions' do
9
+ expect(described_class::DIMENSIONS).to contain_exactly(:valence, :arousal, :energy, :stability)
10
+ end
11
+
12
+ it 'defines modulations for every mood state' do
13
+ described_class::MOOD_STATES.each do |mood|
14
+ expect(described_class::MOOD_MODULATIONS).to have_key(mood)
15
+ end
16
+ end
17
+
18
+ it 'defines inertia for every mood state' do
19
+ described_class::MOOD_STATES.each do |mood|
20
+ expect(described_class::MOOD_INERTIA).to have_key(mood)
21
+ end
22
+ end
23
+
24
+ it 'has inertia values between 0 and 1' do
25
+ described_class::MOOD_INERTIA.each_value do |v|
26
+ expect(v).to be_between(0.0, 1.0)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mood::Helpers::MoodState do
4
+ subject(:state) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts neutral' do
8
+ expect(state.current_mood).to eq(:neutral)
9
+ end
10
+
11
+ it 'starts with moderate values' do
12
+ expect(state.valence).to eq(0.5)
13
+ expect(state.arousal).to eq(0.3)
14
+ expect(state.energy).to eq(0.5)
15
+ end
16
+ end
17
+
18
+ describe '#update' do
19
+ it 'does not change mood before update interval' do
20
+ state.update(valence: 0.9, arousal: 0.1)
21
+ expect(state.tick_counter).to eq(1)
22
+ expect(state.history).to be_empty
23
+ end
24
+
25
+ it 'updates mood at update interval' do
26
+ Legion::Extensions::Mood::Helpers::Constants::UPDATE_INTERVAL.times do
27
+ state.update(valence: 0.8, arousal: 0.2, energy: 0.7)
28
+ end
29
+ expect(state.history.size).to eq(1)
30
+ end
31
+
32
+ it 'gradually shifts dimensions via EMA' do
33
+ original_valence = state.valence
34
+ Legion::Extensions::Mood::Helpers::Constants::UPDATE_INTERVAL.times do
35
+ state.update(valence: 0.9, arousal: 0.5, energy: 0.6)
36
+ end
37
+ expect(state.valence).to be > original_valence
38
+ end
39
+
40
+ it 'can reach serene mood with high valence and low arousal' do
41
+ 20.times do
42
+ Legion::Extensions::Mood::Helpers::Constants::UPDATE_INTERVAL.times do
43
+ state.update(valence: 0.9, arousal: 0.1, energy: 0.8)
44
+ end
45
+ end
46
+ expect(state.current_mood).to eq(:serene)
47
+ end
48
+
49
+ it 'can reach anxious mood with low valence and high arousal' do
50
+ 20.times do
51
+ Legion::Extensions::Mood::Helpers::Constants::UPDATE_INTERVAL.times do
52
+ state.update(valence: 0.1, arousal: 0.9, energy: 0.5)
53
+ end
54
+ end
55
+ expect(state.current_mood).to eq(:anxious)
56
+ end
57
+ end
58
+
59
+ describe '#modulations' do
60
+ it 'returns modulation hash for current mood' do
61
+ mods = state.modulations
62
+ expect(mods).to have_key(:attention_threshold)
63
+ expect(mods).to have_key(:risk_tolerance)
64
+ expect(mods).to have_key(:curiosity_boost)
65
+ end
66
+ end
67
+
68
+ describe '#inertia' do
69
+ it 'returns inertia value for current mood' do
70
+ expect(state.inertia).to be_a(Numeric)
71
+ expect(state.inertia).to be_between(0.0, 1.0)
72
+ end
73
+ end
74
+
75
+ describe '#duration_in_current_mood' do
76
+ it 'returns 0 with no history' do
77
+ expect(state.duration_in_current_mood).to eq(0)
78
+ end
79
+ end
80
+
81
+ describe '#mood_trend' do
82
+ it 'returns :insufficient_data with few entries' do
83
+ expect(state.mood_trend).to eq(:insufficient_data)
84
+ end
85
+ end
86
+
87
+ describe '#to_h' do
88
+ it 'returns complete state hash' do
89
+ h = state.to_h
90
+ expect(h).to include(:current_mood, :valence, :arousal, :energy,
91
+ :stability, :modulations, :inertia, :trend)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mood::Runners::Mood do
4
+ let(:client) { Legion::Extensions::Mood::Client.new }
5
+
6
+ let(:tick_results) do
7
+ {
8
+ emotional_evaluation: { magnitude: 0.6, arousal: 0.4 },
9
+ elapsed: 2.0,
10
+ budget: 5.0
11
+ }
12
+ end
13
+
14
+ describe '#update_mood' do
15
+ it 'returns mood state' do
16
+ result = client.update_mood(tick_results: tick_results)
17
+ expect(result).to have_key(:mood)
18
+ expect(result).to have_key(:modulations)
19
+ end
20
+
21
+ it 'handles empty tick results' do
22
+ result = client.update_mood(tick_results: {})
23
+ expect(result[:mood]).to be_a(Symbol)
24
+ end
25
+
26
+ it 'handles valence hash from emotion' do
27
+ results = tick_results.merge(
28
+ emotional_evaluation: {
29
+ valence: { urgency: 0.2, importance: 0.6, novelty: 0.3, familiarity: 0.7 },
30
+ arousal: 0.5
31
+ }
32
+ )
33
+ result = client.update_mood(tick_results: results)
34
+ expect(result[:mood]).to be_a(Symbol)
35
+ end
36
+ end
37
+
38
+ describe '#current_mood' do
39
+ it 'returns full mood state' do
40
+ result = client.current_mood
41
+ expect(result).to include(:current_mood, :valence, :arousal, :energy, :stability)
42
+ end
43
+ end
44
+
45
+ describe '#mood_modulation' do
46
+ it 'returns modulation for a parameter' do
47
+ result = client.mood_modulation(parameter: :curiosity_boost)
48
+ expect(result[:parameter]).to eq(:curiosity_boost)
49
+ expect(result[:modulation]).to be_a(Numeric)
50
+ end
51
+
52
+ it 'returns 0.0 for unknown parameter' do
53
+ result = client.mood_modulation(parameter: :nonexistent)
54
+ expect(result[:modulation]).to eq(0.0)
55
+ end
56
+ end
57
+
58
+ describe '#mood_history' do
59
+ it 'returns empty history initially' do
60
+ result = client.mood_history
61
+ expect(result[:entries]).to be_empty
62
+ end
63
+ end
64
+
65
+ describe '#mood_stats' do
66
+ it 'returns stats summary' do
67
+ result = client.mood_stats
68
+ expect(result).to include(:current_mood, :stability, :trend, :ticks_processed)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Logging
5
+ module_function
6
+
7
+ def debug(*); end
8
+
9
+ def info(*); end
10
+
11
+ def warn(*); end
12
+
13
+ def error(*); end
14
+ end
15
+
16
+ module Extensions
17
+ module Helpers; end
18
+ end
19
+ end
20
+
21
+ require 'legion/extensions/mood'
22
+
23
+ RSpec.configure do |config|
24
+ config.expect_with :rspec do |expectations|
25
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
26
+ end
27
+
28
+ config.mock_with :rspec do |mocks|
29
+ mocks.verify_partial_doubles = true
30
+ end
31
+
32
+ config.shared_context_metadata_behavior = :apply_to_host_groups
33
+ config.order = :random
34
+ Kernel.srand config.seed
35
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-mood
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: Persistent mood state that emerges from emotional patterns and biases
27
+ cognitive processing
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - lex-mood.gemspec
38
+ - lib/legion/extensions/mood.rb
39
+ - lib/legion/extensions/mood/client.rb
40
+ - lib/legion/extensions/mood/helpers/constants.rb
41
+ - lib/legion/extensions/mood/helpers/mood_state.rb
42
+ - lib/legion/extensions/mood/runners/mood.rb
43
+ - lib/legion/extensions/mood/version.rb
44
+ - spec/legion/extensions/mood/client_spec.rb
45
+ - spec/legion/extensions/mood/helpers/constants_spec.rb
46
+ - spec/legion/extensions/mood/helpers/mood_state_spec.rb
47
+ - spec/legion/extensions/mood/runners/mood_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/LegionIO/lex-mood
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-mood
54
+ source_code_uri: https://github.com/LegionIO/lex-mood
55
+ documentation_uri: https://github.com/LegionIO/lex-mood
56
+ changelog_uri: https://github.com/LegionIO/lex-mood
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-mood/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 Mood
76
+ test_files: []