lex-neuromodulation 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 +7 -0
- data/Gemfile +12 -0
- data/lex-neuromodulation.gemspec +29 -0
- data/lib/legion/extensions/neuromodulation/actors/drift.rb +41 -0
- data/lib/legion/extensions/neuromodulation/client.rb +24 -0
- data/lib/legion/extensions/neuromodulation/helpers/constants.rb +49 -0
- data/lib/legion/extensions/neuromodulation/helpers/modulator.rb +103 -0
- data/lib/legion/extensions/neuromodulation/helpers/modulator_system.rb +119 -0
- data/lib/legion/extensions/neuromodulation/runners/neuromodulation.rb +139 -0
- data/lib/legion/extensions/neuromodulation/version.rb +9 -0
- data/lib/legion/extensions/neuromodulation.rb +15 -0
- data/spec/legion/extensions/neuromodulation/client_spec.rb +45 -0
- data/spec/legion/extensions/neuromodulation/helpers/constants_spec.rb +66 -0
- data/spec/legion/extensions/neuromodulation/helpers/modulator_spec.rb +163 -0
- data/spec/legion/extensions/neuromodulation/helpers/modulator_system_spec.rb +181 -0
- data/spec/legion/extensions/neuromodulation/runners/neuromodulation_spec.rb +197 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9699c48e6cd9388c89541b230de7e5bfdec101a9be590af7ac37d9ee61453ef2
|
|
4
|
+
data.tar.gz: 48ca816c952d030a32969e5f02b226377d51b28e7374a47e22ff4949d164e075
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 07ec0cda24a8ad10c7541477fd5f5da565c8f074f5d5faf580e073685ccdba703f8906af7422e9edff7d445f8df2442b453bc12f08062b3d4d5af13bf65a8a10
|
|
7
|
+
data.tar.gz: d4c84c120625ac383bd2f25428e80b6430dfd0bb51d4ff90a136eb4e91cb07e4644fdcc18ccaab181045fec41e66505f7fbadf7da29f57e591a39402e07dbf9b
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/neuromodulation/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-neuromodulation'
|
|
7
|
+
spec.version = Legion::Extensions::Neuromodulation::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Neuromodulation'
|
|
12
|
+
spec.description = 'Neuromodulatory system modeling dopamine, serotonin, norepinephrine, and acetylcholine pathways for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-neuromodulation'
|
|
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-neuromodulation'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-neuromodulation'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-neuromodulation'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-neuromodulation/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-neuromodulation.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Neuromodulation
|
|
8
|
+
module Actor
|
|
9
|
+
class Drift < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Neuromodulation::Runners::Neuromodulation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'update_neuromodulation'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
30
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/neuromodulation/helpers/constants'
|
|
4
|
+
require 'legion/extensions/neuromodulation/helpers/modulator'
|
|
5
|
+
require 'legion/extensions/neuromodulation/helpers/modulator_system'
|
|
6
|
+
require 'legion/extensions/neuromodulation/runners/neuromodulation'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Neuromodulation
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::Neuromodulation
|
|
13
|
+
|
|
14
|
+
def initialize(system: nil, **)
|
|
15
|
+
@neuromod_system = system || Helpers::ModulatorSystem.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :neuromod_system
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Neuromodulation
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MODULATORS = %i[dopamine serotonin norepinephrine acetylcholine].freeze
|
|
9
|
+
DEFAULT_LEVEL = 0.5
|
|
10
|
+
LEVEL_FLOOR = 0.0
|
|
11
|
+
LEVEL_CEILING = 1.0
|
|
12
|
+
MODULATION_ALPHA = 0.15
|
|
13
|
+
BASELINE_DRIFT = 0.01
|
|
14
|
+
MAX_EVENTS = 200
|
|
15
|
+
|
|
16
|
+
OPTIMAL_RANGES = {
|
|
17
|
+
dopamine: (0.4..0.7),
|
|
18
|
+
serotonin: (0.4..0.7),
|
|
19
|
+
norepinephrine: (0.3..0.6),
|
|
20
|
+
acetylcholine: (0.4..0.7)
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
STATE_LABELS = {
|
|
24
|
+
dopamine: {
|
|
25
|
+
high: :surplus,
|
|
26
|
+
optimal: :optimal,
|
|
27
|
+
low: :deficit
|
|
28
|
+
},
|
|
29
|
+
serotonin: {
|
|
30
|
+
high: :surplus,
|
|
31
|
+
optimal: :optimal,
|
|
32
|
+
low: :deficit
|
|
33
|
+
},
|
|
34
|
+
norepinephrine: {
|
|
35
|
+
high: :surplus,
|
|
36
|
+
optimal: :optimal,
|
|
37
|
+
low: :deficit
|
|
38
|
+
},
|
|
39
|
+
acetylcholine: {
|
|
40
|
+
high: :surplus,
|
|
41
|
+
optimal: :optimal,
|
|
42
|
+
low: :deficit
|
|
43
|
+
}
|
|
44
|
+
}.freeze
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Neuromodulation
|
|
6
|
+
module Helpers
|
|
7
|
+
class Modulator
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :level, :baseline, :events
|
|
11
|
+
|
|
12
|
+
def initialize(name)
|
|
13
|
+
@name = name
|
|
14
|
+
@level = DEFAULT_LEVEL
|
|
15
|
+
@baseline = DEFAULT_LEVEL
|
|
16
|
+
@events = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def boost(amount, reason: nil)
|
|
20
|
+
old_level = @level
|
|
21
|
+
@level = clamp(@level + amount)
|
|
22
|
+
record_event(:boost, amount, reason, old_level)
|
|
23
|
+
@level
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def suppress(amount, reason: nil)
|
|
27
|
+
old_level = @level
|
|
28
|
+
@level = clamp(@level - amount)
|
|
29
|
+
record_event(:suppress, amount, reason, old_level)
|
|
30
|
+
@level
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def drift_to_baseline
|
|
34
|
+
delta = @baseline - @level
|
|
35
|
+
@level = clamp(@level + (delta * BASELINE_DRIFT))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def optimal?
|
|
39
|
+
OPTIMAL_RANGES.fetch(@name).include?(@level)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def state_label
|
|
43
|
+
range = OPTIMAL_RANGES.fetch(@name)
|
|
44
|
+
if @level > range.end
|
|
45
|
+
STATE_LABELS.dig(@name, :high)
|
|
46
|
+
elsif @level < range.begin
|
|
47
|
+
STATE_LABELS.dig(@name, :low)
|
|
48
|
+
else
|
|
49
|
+
STATE_LABELS.dig(@name, :optimal)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
INFLUENCE_MAP = {
|
|
54
|
+
dopamine: %i[learning_rate exploration_bias],
|
|
55
|
+
serotonin: %i[patience_factor],
|
|
56
|
+
norepinephrine: %i[arousal_level attention_precision],
|
|
57
|
+
acetylcholine: %i[memory_encoding attention_precision]
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
def influence_on(target_property)
|
|
61
|
+
relevant = INFLUENCE_MAP.fetch(@name, [])
|
|
62
|
+
return 0.0 unless relevant.include?(target_property)
|
|
63
|
+
|
|
64
|
+
scale(@level)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_h
|
|
68
|
+
{
|
|
69
|
+
name: @name,
|
|
70
|
+
level: @level.round(4),
|
|
71
|
+
baseline: @baseline.round(4),
|
|
72
|
+
state: state_label,
|
|
73
|
+
optimal: optimal?,
|
|
74
|
+
event_count: @events.size
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def clamp(value)
|
|
81
|
+
value.clamp(LEVEL_FLOOR, LEVEL_CEILING)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def scale(value)
|
|
85
|
+
(value - DEFAULT_LEVEL) * 2.0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def record_event(type, amount, reason, old_level)
|
|
89
|
+
@events << {
|
|
90
|
+
type: type,
|
|
91
|
+
amount: amount,
|
|
92
|
+
reason: reason,
|
|
93
|
+
old_level: old_level.round(4),
|
|
94
|
+
new_level: @level.round(4),
|
|
95
|
+
timestamp: Time.now.utc
|
|
96
|
+
}
|
|
97
|
+
@events.shift while @events.size > MAX_EVENTS
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Neuromodulation
|
|
6
|
+
module Helpers
|
|
7
|
+
class ModulatorSystem
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :modulators
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@modulators = MODULATORS.to_h { |name| [name, Modulator.new(name)] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def boost(name, amount, reason: nil)
|
|
17
|
+
validate_name!(name)
|
|
18
|
+
result = @modulators[name].boost(amount, reason: reason)
|
|
19
|
+
apply_interactions(name)
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def suppress(name, amount, reason: nil)
|
|
24
|
+
validate_name!(name)
|
|
25
|
+
result = @modulators[name].suppress(amount, reason: reason)
|
|
26
|
+
apply_interactions(name)
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def level(name)
|
|
31
|
+
validate_name!(name)
|
|
32
|
+
@modulators[name].level
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all_levels
|
|
36
|
+
@modulators.transform_values(&:level)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tick
|
|
40
|
+
@modulators.each_value(&:drift_to_baseline)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def learning_rate_modifier
|
|
44
|
+
da = @modulators[:dopamine].level
|
|
45
|
+
ach = @modulators[:acetylcholine].level
|
|
46
|
+
clamp((da * 0.6) + (ach * 0.4))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attention_precision
|
|
50
|
+
ne = @modulators[:norepinephrine].level
|
|
51
|
+
ach = @modulators[:acetylcholine].level
|
|
52
|
+
clamp((ne * 0.5) + (ach * 0.5))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exploration_bias
|
|
56
|
+
@modulators[:dopamine].level
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def patience_factor
|
|
60
|
+
@modulators[:serotonin].level
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def memory_encoding_strength
|
|
64
|
+
@modulators[:acetylcholine].level
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def arousal_level
|
|
68
|
+
@modulators[:norepinephrine].level
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def composite_influences
|
|
72
|
+
{
|
|
73
|
+
learning_rate_modifier: learning_rate_modifier.round(4),
|
|
74
|
+
attention_precision: attention_precision.round(4),
|
|
75
|
+
exploration_bias: exploration_bias.round(4),
|
|
76
|
+
patience_factor: patience_factor.round(4),
|
|
77
|
+
memory_encoding_strength: memory_encoding_strength.round(4),
|
|
78
|
+
arousal_level: arousal_level.round(4)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def balance_score
|
|
83
|
+
in_range = @modulators.values.count(&:optimal?)
|
|
84
|
+
in_range.to_f / @modulators.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_h
|
|
88
|
+
{
|
|
89
|
+
modulators: @modulators.transform_values(&:to_h),
|
|
90
|
+
influences: composite_influences,
|
|
91
|
+
balance: balance_score.round(4)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def validate_name!(name)
|
|
98
|
+
raise ArgumentError, "Unknown modulator: #{name}" unless MODULATORS.include?(name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def clamp(value)
|
|
102
|
+
value.clamp(LEVEL_FLOOR, LEVEL_CEILING)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def apply_interactions(changed)
|
|
106
|
+
case changed
|
|
107
|
+
when :dopamine
|
|
108
|
+
high_da = @modulators[:dopamine].level > 0.7
|
|
109
|
+
@modulators[:serotonin].suppress(0.05, reason: :dopamine_suppression) if high_da
|
|
110
|
+
when :norepinephrine
|
|
111
|
+
high_ne = @modulators[:norepinephrine].level > 0.8
|
|
112
|
+
@modulators[:acetylcholine].suppress(0.03, reason: :ne_suppression) if high_ne
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Neuromodulation
|
|
6
|
+
module Runners
|
|
7
|
+
module Neuromodulation
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def boost_modulator(name:, amount:, reason: nil, **)
|
|
12
|
+
mod_name = name.to_sym
|
|
13
|
+
return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
|
|
14
|
+
|
|
15
|
+
new_level = neuromod_system.boost(mod_name, amount.to_f, reason: reason)
|
|
16
|
+
Legion::Logging.debug "[neuromodulation] boost #{mod_name} by #{amount} -> #{new_level.round(4)}"
|
|
17
|
+
{
|
|
18
|
+
success: true,
|
|
19
|
+
modulator: mod_name,
|
|
20
|
+
level: new_level.round(4),
|
|
21
|
+
state: neuromod_system.modulators[mod_name].state_label
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def suppress_modulator(name:, amount:, reason: nil, **)
|
|
26
|
+
mod_name = name.to_sym
|
|
27
|
+
return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
|
|
28
|
+
|
|
29
|
+
new_level = neuromod_system.suppress(mod_name, amount.to_f, reason: reason)
|
|
30
|
+
Legion::Logging.debug "[neuromodulation] suppress #{mod_name} by #{amount} -> #{new_level.round(4)}"
|
|
31
|
+
{
|
|
32
|
+
success: true,
|
|
33
|
+
modulator: mod_name,
|
|
34
|
+
level: new_level.round(4),
|
|
35
|
+
state: neuromod_system.modulators[mod_name].state_label
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def modulator_level(name:, **)
|
|
40
|
+
mod_name = name.to_sym
|
|
41
|
+
return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
|
|
42
|
+
|
|
43
|
+
level = neuromod_system.level(mod_name)
|
|
44
|
+
{
|
|
45
|
+
success: true,
|
|
46
|
+
modulator: mod_name,
|
|
47
|
+
level: level.round(4),
|
|
48
|
+
state: neuromod_system.modulators[mod_name].state_label
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all_modulator_levels(**)
|
|
53
|
+
levels = neuromod_system.all_levels
|
|
54
|
+
Legion::Logging.debug "[neuromodulation] all levels: #{levels.map { |k, v| "#{k}=#{v.round(3)}" }.join(' ')}"
|
|
55
|
+
{ success: true, levels: levels.transform_values { |v| v.round(4) } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cognitive_influence(**)
|
|
59
|
+
influences = neuromod_system.composite_influences
|
|
60
|
+
Legion::Logging.debug '[neuromodulation] cognitive influence snapshot'
|
|
61
|
+
{ success: true, influences: influences }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def is_optimal(name:, **)
|
|
65
|
+
mod_name = name.to_sym
|
|
66
|
+
return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
|
|
67
|
+
|
|
68
|
+
optimal = neuromod_system.modulators[mod_name].optimal?
|
|
69
|
+
{
|
|
70
|
+
success: true,
|
|
71
|
+
modulator: mod_name,
|
|
72
|
+
optimal: optimal,
|
|
73
|
+
level: neuromod_system.level(mod_name).round(4),
|
|
74
|
+
range: Helpers::Constants::OPTIMAL_RANGES[mod_name].to_s
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def system_balance(**)
|
|
79
|
+
score = neuromod_system.balance_score
|
|
80
|
+
states = neuromod_system.modulators.transform_values(&:state_label)
|
|
81
|
+
status = if score >= 1.0
|
|
82
|
+
:fully_balanced
|
|
83
|
+
elsif score >= 0.75
|
|
84
|
+
:mostly_balanced
|
|
85
|
+
elsif score >= 0.5
|
|
86
|
+
:partially_balanced
|
|
87
|
+
else
|
|
88
|
+
:imbalanced
|
|
89
|
+
end
|
|
90
|
+
Legion::Logging.debug "[neuromodulation] system balance: #{score.round(2)} status=#{status}"
|
|
91
|
+
{
|
|
92
|
+
success: true,
|
|
93
|
+
score: score.round(4),
|
|
94
|
+
status: status,
|
|
95
|
+
states: states
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def modulator_history(name:, limit: 20, **)
|
|
100
|
+
mod_name = name.to_sym
|
|
101
|
+
return { success: false, error: "Unknown modulator: #{name}" } unless Helpers::Constants::MODULATORS.include?(mod_name)
|
|
102
|
+
|
|
103
|
+
events = neuromod_system.modulators[mod_name].events.last(limit.to_i)
|
|
104
|
+
{
|
|
105
|
+
success: true,
|
|
106
|
+
modulator: mod_name,
|
|
107
|
+
events: events,
|
|
108
|
+
count: events.size
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def update_neuromodulation(**)
|
|
113
|
+
neuromod_system.tick
|
|
114
|
+
levels = neuromod_system.all_levels
|
|
115
|
+
Legion::Logging.debug '[neuromodulation] drift tick completed'
|
|
116
|
+
{
|
|
117
|
+
success: true,
|
|
118
|
+
action: :drift_tick,
|
|
119
|
+
levels: levels.transform_values { |v| v.round(4) }
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def neuromodulation_stats(**)
|
|
124
|
+
{
|
|
125
|
+
success: true,
|
|
126
|
+
system: neuromod_system.to_h
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def neuromod_system
|
|
133
|
+
@neuromod_system ||= Helpers::ModulatorSystem.new
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/neuromodulation/version'
|
|
4
|
+
require 'legion/extensions/neuromodulation/helpers/constants'
|
|
5
|
+
require 'legion/extensions/neuromodulation/helpers/modulator'
|
|
6
|
+
require 'legion/extensions/neuromodulation/helpers/modulator_system'
|
|
7
|
+
require 'legion/extensions/neuromodulation/runners/neuromodulation'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Neuromodulation
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/neuromodulation/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Neuromodulation::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:boost_modulator)
|
|
10
|
+
expect(client).to respond_to(:suppress_modulator)
|
|
11
|
+
expect(client).to respond_to(:modulator_level)
|
|
12
|
+
expect(client).to respond_to(:all_modulator_levels)
|
|
13
|
+
expect(client).to respond_to(:cognitive_influence)
|
|
14
|
+
expect(client).to respond_to(:is_optimal)
|
|
15
|
+
expect(client).to respond_to(:system_balance)
|
|
16
|
+
expect(client).to respond_to(:modulator_history)
|
|
17
|
+
expect(client).to respond_to(:update_neuromodulation)
|
|
18
|
+
expect(client).to respond_to(:neuromodulation_stats)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'accepts an injected system' do
|
|
22
|
+
system = Legion::Extensions::Neuromodulation::Helpers::ModulatorSystem.new
|
|
23
|
+
system.boost(:dopamine, 0.3)
|
|
24
|
+
c = described_class.new(system: system)
|
|
25
|
+
result = c.modulator_level(name: :dopamine)
|
|
26
|
+
expect(result[:level]).to be > 0.5
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'maintains state across calls' do
|
|
30
|
+
client.boost_modulator(name: :acetylcholine, amount: 0.2)
|
|
31
|
+
level_result = client.modulator_level(name: :acetylcholine)
|
|
32
|
+
expect(level_result[:level]).to be_within(0.001).of(0.7)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'round-trips a full neuromodulation cycle' do
|
|
36
|
+
client.boost_modulator(name: :dopamine, amount: 0.15)
|
|
37
|
+
client.suppress_modulator(name: :serotonin, amount: 0.1)
|
|
38
|
+
ci = client.cognitive_influence
|
|
39
|
+
expect(ci[:influences][:exploration_bias]).to be > 0.5
|
|
40
|
+
expect(ci[:influences][:patience_factor]).to be < 0.5
|
|
41
|
+
client.update_neuromodulation
|
|
42
|
+
stats = client.neuromodulation_stats
|
|
43
|
+
expect(stats[:system][:modulators][:dopamine][:level]).to be_a(Float)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Neuromodulation::Helpers::Constants do
|
|
4
|
+
let(:mod) { Legion::Extensions::Neuromodulation::Helpers::Constants }
|
|
5
|
+
|
|
6
|
+
describe 'MODULATORS' do
|
|
7
|
+
it 'contains the four neuromodulator names' do
|
|
8
|
+
expect(mod::MODULATORS).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'is frozen' do
|
|
12
|
+
expect(mod::MODULATORS).to be_frozen
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'OPTIMAL_RANGES' do
|
|
17
|
+
it 'defines a range for each modulator' do
|
|
18
|
+
mod::MODULATORS.each do |name|
|
|
19
|
+
expect(mod::OPTIMAL_RANGES[name]).to be_a(Range)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'has non-overlapping floor/ceiling bounds' do
|
|
24
|
+
mod::OPTIMAL_RANGES.each_value do |range|
|
|
25
|
+
expect(range.begin).to be >= mod::LEVEL_FLOOR
|
|
26
|
+
expect(range.end).to be <= mod::LEVEL_CEILING
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe 'numeric constants' do
|
|
32
|
+
it 'DEFAULT_LEVEL is 0.5' do
|
|
33
|
+
expect(mod::DEFAULT_LEVEL).to eq(0.5)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'LEVEL_FLOOR is 0.0' do
|
|
37
|
+
expect(mod::LEVEL_FLOOR).to eq(0.0)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'LEVEL_CEILING is 1.0' do
|
|
41
|
+
expect(mod::LEVEL_CEILING).to eq(1.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'MODULATION_ALPHA is 0.15' do
|
|
45
|
+
expect(mod::MODULATION_ALPHA).to eq(0.15)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'BASELINE_DRIFT is 0.01' do
|
|
49
|
+
expect(mod::BASELINE_DRIFT).to eq(0.01)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'MAX_EVENTS is 200' do
|
|
53
|
+
expect(mod::MAX_EVENTS).to eq(200)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe 'STATE_LABELS' do
|
|
58
|
+
it 'defines high/optimal/low labels for each modulator' do
|
|
59
|
+
mod::MODULATORS.each do |name|
|
|
60
|
+
expect(mod::STATE_LABELS[name]).to have_key(:high)
|
|
61
|
+
expect(mod::STATE_LABELS[name]).to have_key(:optimal)
|
|
62
|
+
expect(mod::STATE_LABELS[name]).to have_key(:low)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Neuromodulation::Helpers::Modulator do
|
|
4
|
+
subject(:mod) { described_class.new(:dopamine) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'sets name' do
|
|
8
|
+
expect(mod.name).to eq(:dopamine)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'sets level to DEFAULT_LEVEL' do
|
|
12
|
+
expect(mod.level).to eq(0.5)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'sets baseline to DEFAULT_LEVEL' do
|
|
16
|
+
expect(mod.baseline).to eq(0.5)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts with empty events' do
|
|
20
|
+
expect(mod.events).to be_empty
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#boost' do
|
|
25
|
+
it 'increases level' do
|
|
26
|
+
mod.boost(0.2)
|
|
27
|
+
expect(mod.level).to be_within(0.001).of(0.7)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'clamps at LEVEL_CEILING' do
|
|
31
|
+
mod.boost(1.0)
|
|
32
|
+
expect(mod.level).to eq(1.0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'records an event' do
|
|
36
|
+
mod.boost(0.1, reason: :reward)
|
|
37
|
+
expect(mod.events.size).to eq(1)
|
|
38
|
+
expect(mod.events.last[:type]).to eq(:boost)
|
|
39
|
+
expect(mod.events.last[:reason]).to eq(:reward)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns the new level' do
|
|
43
|
+
result = mod.boost(0.1)
|
|
44
|
+
expect(result).to be_within(0.001).of(0.6)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#suppress' do
|
|
49
|
+
it 'decreases level' do
|
|
50
|
+
mod.suppress(0.2)
|
|
51
|
+
expect(mod.level).to be_within(0.001).of(0.3)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'clamps at LEVEL_FLOOR' do
|
|
55
|
+
mod.suppress(1.0)
|
|
56
|
+
expect(mod.level).to eq(0.0)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'records an event' do
|
|
60
|
+
mod.suppress(0.1, reason: :fatigue)
|
|
61
|
+
expect(mod.events.size).to eq(1)
|
|
62
|
+
expect(mod.events.last[:type]).to eq(:suppress)
|
|
63
|
+
expect(mod.events.last[:reason]).to eq(:fatigue)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns the new level' do
|
|
67
|
+
result = mod.suppress(0.1)
|
|
68
|
+
expect(result).to be_within(0.001).of(0.4)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#drift_to_baseline' do
|
|
73
|
+
it 'moves level toward baseline' do
|
|
74
|
+
mod.boost(0.3)
|
|
75
|
+
level_before = mod.level
|
|
76
|
+
mod.drift_to_baseline
|
|
77
|
+
expect(mod.level).to be < level_before
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'does not overshoot baseline' do
|
|
81
|
+
mod.boost(0.3)
|
|
82
|
+
100.times { mod.drift_to_baseline }
|
|
83
|
+
expect(mod.level).to be >= mod.baseline
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe '#optimal?' do
|
|
88
|
+
it 'returns true when level is in optimal range' do
|
|
89
|
+
expect(mod.optimal?).to be true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'returns false when level is too high' do
|
|
93
|
+
mod.boost(0.5)
|
|
94
|
+
expect(mod.optimal?).to be false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns false when level is too low' do
|
|
98
|
+
mod.suppress(0.4)
|
|
99
|
+
expect(mod.optimal?).to be false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#state_label' do
|
|
104
|
+
it 'returns :optimal at default level' do
|
|
105
|
+
expect(mod.state_label).to eq(:optimal)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'returns :surplus when high' do
|
|
109
|
+
mod.boost(0.4)
|
|
110
|
+
expect(mod.state_label).to eq(:surplus)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'returns :deficit when low' do
|
|
114
|
+
mod.suppress(0.4)
|
|
115
|
+
expect(mod.state_label).to eq(:deficit)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe '#influence_on' do
|
|
120
|
+
it 'returns a numeric influence for known property' do
|
|
121
|
+
expect(mod.influence_on(:learning_rate)).to be_a(Numeric)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'returns 0.0 for unknown property' do
|
|
125
|
+
expect(mod.influence_on(:unknown_property)).to eq(0.0)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'returns higher influence when level is high' do
|
|
129
|
+
mod.boost(0.3)
|
|
130
|
+
high_influence = mod.influence_on(:learning_rate)
|
|
131
|
+
mod2 = described_class.new(:dopamine)
|
|
132
|
+
mod2.suppress(0.3)
|
|
133
|
+
low_influence = mod2.influence_on(:learning_rate)
|
|
134
|
+
expect(high_influence).to be > low_influence
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe '#to_h' do
|
|
139
|
+
it 'returns a hash with required keys' do
|
|
140
|
+
h = mod.to_h
|
|
141
|
+
expect(h).to have_key(:name)
|
|
142
|
+
expect(h).to have_key(:level)
|
|
143
|
+
expect(h).to have_key(:baseline)
|
|
144
|
+
expect(h).to have_key(:state)
|
|
145
|
+
expect(h).to have_key(:optimal)
|
|
146
|
+
expect(h).to have_key(:event_count)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'reflects current state' do
|
|
150
|
+
mod.boost(0.3)
|
|
151
|
+
h = mod.to_h
|
|
152
|
+
expect(h[:level]).to be > 0.5
|
|
153
|
+
expect(h[:event_count]).to eq(1)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe 'event ring buffer' do
|
|
158
|
+
it 'caps events at MAX_EVENTS' do
|
|
159
|
+
250.times { mod.boost(0.001) }
|
|
160
|
+
expect(mod.events.size).to eq(200)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Neuromodulation::Helpers::ModulatorSystem do
|
|
4
|
+
subject(:system) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'creates all four modulators' do
|
|
8
|
+
expect(system.modulators.keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'starts each at default level' do
|
|
12
|
+
system.modulators.each_value do |mod|
|
|
13
|
+
expect(mod.level).to eq(0.5)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#boost' do
|
|
19
|
+
it 'raises for unknown modulator' do
|
|
20
|
+
expect { system.boost(:cortisol, 0.1) }.to raise_error(ArgumentError)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'increases the target modulator level' do
|
|
24
|
+
system.boost(:dopamine, 0.2)
|
|
25
|
+
expect(system.level(:dopamine)).to be_within(0.001).of(0.7)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'applies dopamine->serotonin suppression when dopamine goes high' do
|
|
29
|
+
serotonin_before = system.level(:serotonin)
|
|
30
|
+
system.boost(:dopamine, 0.3)
|
|
31
|
+
expect(system.level(:serotonin)).to be < serotonin_before
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'does not suppress serotonin for moderate dopamine boost' do
|
|
35
|
+
serotonin_before = system.level(:serotonin)
|
|
36
|
+
system.boost(:dopamine, 0.05)
|
|
37
|
+
expect(system.level(:serotonin)).to eq(serotonin_before)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#suppress' do
|
|
42
|
+
it 'raises for unknown modulator' do
|
|
43
|
+
expect { system.suppress(:cortisol, 0.1) }.to raise_error(ArgumentError)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'decreases the target modulator level' do
|
|
47
|
+
system.suppress(:serotonin, 0.2)
|
|
48
|
+
expect(system.level(:serotonin)).to be_within(0.001).of(0.3)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'applies norepinephrine->acetylcholine suppression when NE goes very high' do
|
|
52
|
+
ach_before = system.level(:acetylcholine)
|
|
53
|
+
system.boost(:norepinephrine, 0.35)
|
|
54
|
+
expect(system.level(:acetylcholine)).to be < ach_before
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#level' do
|
|
59
|
+
it 'raises for unknown modulator' do
|
|
60
|
+
expect { system.level(:unknown) }.to raise_error(ArgumentError)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns current level' do
|
|
64
|
+
expect(system.level(:dopamine)).to eq(0.5)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#all_levels' do
|
|
69
|
+
it 'returns a hash with all four modulators' do
|
|
70
|
+
levels = system.all_levels
|
|
71
|
+
expect(levels.keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'reflects current state' do
|
|
75
|
+
system.boost(:dopamine, 0.2)
|
|
76
|
+
levels = system.all_levels
|
|
77
|
+
expect(levels[:dopamine]).to be > 0.5
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe '#tick' do
|
|
82
|
+
it 'drifts all modulators toward baseline' do
|
|
83
|
+
system.boost(:dopamine, 0.3)
|
|
84
|
+
level_before = system.level(:dopamine)
|
|
85
|
+
system.tick
|
|
86
|
+
expect(system.level(:dopamine)).to be < level_before
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'processes all four modulators' do
|
|
90
|
+
system.boost(:norepinephrine, 0.2)
|
|
91
|
+
system.suppress(:serotonin, 0.2)
|
|
92
|
+
ne_before = system.level(:norepinephrine)
|
|
93
|
+
ser_before = system.level(:serotonin)
|
|
94
|
+
system.tick
|
|
95
|
+
expect(system.level(:norepinephrine)).to be < ne_before
|
|
96
|
+
expect(system.level(:serotonin)).to be > ser_before
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe 'composite influences' do
|
|
101
|
+
describe '#learning_rate_modifier' do
|
|
102
|
+
it 'returns a value between 0 and 1' do
|
|
103
|
+
expect(system.learning_rate_modifier).to be_between(0.0, 1.0)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'increases with higher dopamine' do
|
|
107
|
+
base = system.learning_rate_modifier
|
|
108
|
+
system.boost(:dopamine, 0.2)
|
|
109
|
+
expect(system.learning_rate_modifier).to be > base
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#attention_precision' do
|
|
114
|
+
it 'returns a value between 0 and 1' do
|
|
115
|
+
expect(system.attention_precision).to be_between(0.0, 1.0)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'increases with higher norepinephrine' do
|
|
119
|
+
base = system.attention_precision
|
|
120
|
+
system.boost(:norepinephrine, 0.1)
|
|
121
|
+
expect(system.attention_precision).to be > base
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#exploration_bias' do
|
|
126
|
+
it 'equals dopamine level' do
|
|
127
|
+
expect(system.exploration_bias).to eq(system.level(:dopamine))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '#patience_factor' do
|
|
132
|
+
it 'equals serotonin level' do
|
|
133
|
+
expect(system.patience_factor).to eq(system.level(:serotonin))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe '#memory_encoding_strength' do
|
|
138
|
+
it 'equals acetylcholine level' do
|
|
139
|
+
expect(system.memory_encoding_strength).to eq(system.level(:acetylcholine))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe '#arousal_level' do
|
|
144
|
+
it 'equals norepinephrine level' do
|
|
145
|
+
expect(system.arousal_level).to eq(system.level(:norepinephrine))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe '#composite_influences' do
|
|
150
|
+
it 'returns a hash with all influence keys' do
|
|
151
|
+
ci = system.composite_influences
|
|
152
|
+
expect(ci).to have_key(:learning_rate_modifier)
|
|
153
|
+
expect(ci).to have_key(:attention_precision)
|
|
154
|
+
expect(ci).to have_key(:exploration_bias)
|
|
155
|
+
expect(ci).to have_key(:patience_factor)
|
|
156
|
+
expect(ci).to have_key(:memory_encoding_strength)
|
|
157
|
+
expect(ci).to have_key(:arousal_level)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe '#balance_score' do
|
|
163
|
+
it 'returns 1.0 when all modulators are optimal' do
|
|
164
|
+
expect(system.balance_score).to eq(1.0)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'decreases when a modulator goes out of range' do
|
|
168
|
+
system.boost(:dopamine, 0.5)
|
|
169
|
+
expect(system.balance_score).to be < 1.0
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe '#to_h' do
|
|
174
|
+
it 'returns modulators, influences, and balance' do
|
|
175
|
+
h = system.to_h
|
|
176
|
+
expect(h).to have_key(:modulators)
|
|
177
|
+
expect(h).to have_key(:influences)
|
|
178
|
+
expect(h).to have_key(:balance)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/neuromodulation/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Neuromodulation::Runners::Neuromodulation do
|
|
6
|
+
let(:client) { Legion::Extensions::Neuromodulation::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#boost_modulator' do
|
|
9
|
+
it 'returns success true' do
|
|
10
|
+
result = client.boost_modulator(name: :dopamine, amount: 0.1)
|
|
11
|
+
expect(result[:success]).to be true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'returns updated level' do
|
|
15
|
+
result = client.boost_modulator(name: :dopamine, amount: 0.1)
|
|
16
|
+
expect(result[:level]).to be > 0.5
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns state label' do
|
|
20
|
+
result = client.boost_modulator(name: :dopamine, amount: 0.1)
|
|
21
|
+
expect(result[:state]).to be_a(Symbol)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns error for unknown modulator' do
|
|
25
|
+
result = client.boost_modulator(name: :cortisol, amount: 0.1)
|
|
26
|
+
expect(result[:success]).to be false
|
|
27
|
+
expect(result[:error]).to include('cortisol')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'accepts string modulator name' do
|
|
31
|
+
result = client.boost_modulator(name: 'serotonin', amount: 0.1)
|
|
32
|
+
expect(result[:success]).to be true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#suppress_modulator' do
|
|
37
|
+
it 'returns success true' do
|
|
38
|
+
result = client.suppress_modulator(name: :serotonin, amount: 0.1)
|
|
39
|
+
expect(result[:success]).to be true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'decreases level' do
|
|
43
|
+
result = client.suppress_modulator(name: :serotonin, amount: 0.1)
|
|
44
|
+
expect(result[:level]).to be < 0.5
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns error for unknown modulator' do
|
|
48
|
+
result = client.suppress_modulator(name: :adrenaline, amount: 0.1)
|
|
49
|
+
expect(result[:success]).to be false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#modulator_level' do
|
|
54
|
+
it 'returns current level' do
|
|
55
|
+
result = client.modulator_level(name: :norepinephrine)
|
|
56
|
+
expect(result[:success]).to be true
|
|
57
|
+
expect(result[:level]).to eq(0.5)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns error for unknown modulator' do
|
|
61
|
+
result = client.modulator_level(name: :unknown)
|
|
62
|
+
expect(result[:success]).to be false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#all_modulator_levels' do
|
|
67
|
+
it 'returns all four modulators' do
|
|
68
|
+
result = client.all_modulator_levels
|
|
69
|
+
expect(result[:success]).to be true
|
|
70
|
+
expect(result[:levels].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'reflects changes after boost' do
|
|
74
|
+
client.boost_modulator(name: :dopamine, amount: 0.2)
|
|
75
|
+
result = client.all_modulator_levels
|
|
76
|
+
expect(result[:levels][:dopamine]).to be > 0.5
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe '#cognitive_influence' do
|
|
81
|
+
it 'returns all six cognitive properties' do
|
|
82
|
+
result = client.cognitive_influence
|
|
83
|
+
expect(result[:success]).to be true
|
|
84
|
+
influences = result[:influences]
|
|
85
|
+
expect(influences).to have_key(:learning_rate_modifier)
|
|
86
|
+
expect(influences).to have_key(:attention_precision)
|
|
87
|
+
expect(influences).to have_key(:exploration_bias)
|
|
88
|
+
expect(influences).to have_key(:patience_factor)
|
|
89
|
+
expect(influences).to have_key(:memory_encoding_strength)
|
|
90
|
+
expect(influences).to have_key(:arousal_level)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#is_optimal' do
|
|
95
|
+
it 'returns true at default level' do
|
|
96
|
+
result = client.is_optimal(name: :dopamine)
|
|
97
|
+
expect(result[:success]).to be true
|
|
98
|
+
expect(result[:optimal]).to be true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns false when out of range' do
|
|
102
|
+
client.boost_modulator(name: :dopamine, amount: 0.5)
|
|
103
|
+
result = client.is_optimal(name: :dopamine)
|
|
104
|
+
expect(result[:optimal]).to be false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'includes range string' do
|
|
108
|
+
result = client.is_optimal(name: :serotonin)
|
|
109
|
+
expect(result[:range]).to be_a(String)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns error for unknown modulator' do
|
|
113
|
+
result = client.is_optimal(name: :cortisol)
|
|
114
|
+
expect(result[:success]).to be false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#system_balance' do
|
|
119
|
+
it 'returns fully_balanced at default' do
|
|
120
|
+
result = client.system_balance
|
|
121
|
+
expect(result[:success]).to be true
|
|
122
|
+
expect(result[:status]).to eq(:fully_balanced)
|
|
123
|
+
expect(result[:score]).to eq(1.0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns imbalanced when all are out of range' do
|
|
127
|
+
client.boost_modulator(name: :dopamine, amount: 0.5)
|
|
128
|
+
client.boost_modulator(name: :norepinephrine, amount: 0.5)
|
|
129
|
+
client.suppress_modulator(name: :serotonin, amount: 0.4)
|
|
130
|
+
client.suppress_modulator(name: :acetylcholine, amount: 0.4)
|
|
131
|
+
result = client.system_balance
|
|
132
|
+
expect(result[:score]).to be < 1.0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'returns state for each modulator' do
|
|
136
|
+
result = client.system_balance
|
|
137
|
+
expect(result[:states].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#modulator_history' do
|
|
142
|
+
it 'returns empty events initially' do
|
|
143
|
+
result = client.modulator_history(name: :dopamine)
|
|
144
|
+
expect(result[:success]).to be true
|
|
145
|
+
expect(result[:events]).to be_empty
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'returns events after changes' do
|
|
149
|
+
client.boost_modulator(name: :dopamine, amount: 0.1)
|
|
150
|
+
client.suppress_modulator(name: :dopamine, amount: 0.05)
|
|
151
|
+
result = client.modulator_history(name: :dopamine)
|
|
152
|
+
expect(result[:events].size).to eq(2)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'respects the limit parameter' do
|
|
156
|
+
10.times { client.boost_modulator(name: :dopamine, amount: 0.001) }
|
|
157
|
+
result = client.modulator_history(name: :dopamine, limit: 3)
|
|
158
|
+
expect(result[:events].size).to eq(3)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'returns error for unknown modulator' do
|
|
162
|
+
result = client.modulator_history(name: :unknown)
|
|
163
|
+
expect(result[:success]).to be false
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#update_neuromodulation' do
|
|
168
|
+
it 'returns success' do
|
|
169
|
+
result = client.update_neuromodulation
|
|
170
|
+
expect(result[:success]).to be true
|
|
171
|
+
expect(result[:action]).to eq(:drift_tick)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns current levels after drift' do
|
|
175
|
+
result = client.update_neuromodulation
|
|
176
|
+
expect(result[:levels].keys).to contain_exactly(:dopamine, :serotonin, :norepinephrine, :acetylcholine)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'nudges boosted modulators toward baseline' do
|
|
180
|
+
client.boost_modulator(name: :dopamine, amount: 0.3)
|
|
181
|
+
level_after_boost = client.modulator_level(name: :dopamine)[:level]
|
|
182
|
+
client.update_neuromodulation
|
|
183
|
+
level_after_tick = client.modulator_level(name: :dopamine)[:level]
|
|
184
|
+
expect(level_after_tick).to be < level_after_boost
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe '#neuromodulation_stats' do
|
|
189
|
+
it 'returns full system snapshot' do
|
|
190
|
+
result = client.neuromodulation_stats
|
|
191
|
+
expect(result[:success]).to be true
|
|
192
|
+
expect(result[:system]).to have_key(:modulators)
|
|
193
|
+
expect(result[:system]).to have_key(:influences)
|
|
194
|
+
expect(result[:system]).to have_key(:balance)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -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/neuromodulation'
|
|
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,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-neuromodulation
|
|
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: Neuromodulatory system modeling dopamine, serotonin, norepinephrine,
|
|
27
|
+
and acetylcholine pathways for brain-modeled agentic AI
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- lex-neuromodulation.gemspec
|
|
36
|
+
- lib/legion/extensions/neuromodulation.rb
|
|
37
|
+
- lib/legion/extensions/neuromodulation/actors/drift.rb
|
|
38
|
+
- lib/legion/extensions/neuromodulation/client.rb
|
|
39
|
+
- lib/legion/extensions/neuromodulation/helpers/constants.rb
|
|
40
|
+
- lib/legion/extensions/neuromodulation/helpers/modulator.rb
|
|
41
|
+
- lib/legion/extensions/neuromodulation/helpers/modulator_system.rb
|
|
42
|
+
- lib/legion/extensions/neuromodulation/runners/neuromodulation.rb
|
|
43
|
+
- lib/legion/extensions/neuromodulation/version.rb
|
|
44
|
+
- spec/legion/extensions/neuromodulation/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/neuromodulation/helpers/constants_spec.rb
|
|
46
|
+
- spec/legion/extensions/neuromodulation/helpers/modulator_spec.rb
|
|
47
|
+
- spec/legion/extensions/neuromodulation/helpers/modulator_system_spec.rb
|
|
48
|
+
- spec/legion/extensions/neuromodulation/runners/neuromodulation_spec.rb
|
|
49
|
+
- spec/spec_helper.rb
|
|
50
|
+
homepage: https://github.com/LegionIO/lex-neuromodulation
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/LegionIO/lex-neuromodulation
|
|
55
|
+
source_code_uri: https://github.com/LegionIO/lex-neuromodulation
|
|
56
|
+
documentation_uri: https://github.com/LegionIO/lex-neuromodulation
|
|
57
|
+
changelog_uri: https://github.com/LegionIO/lex-neuromodulation
|
|
58
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-neuromodulation/issues
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.4'
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: LEX Neuromodulation
|
|
77
|
+
test_files: []
|