lex-social-learning 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 +11 -0
- data/lex-social-learning.gemspec +29 -0
- data/lib/legion/extensions/social_learning/client.rb +21 -0
- data/lib/legion/extensions/social_learning/helpers/constants.rb +38 -0
- data/lib/legion/extensions/social_learning/helpers/model_agent.rb +78 -0
- data/lib/legion/extensions/social_learning/helpers/observed_behavior.rb +57 -0
- data/lib/legion/extensions/social_learning/helpers/social_learning_engine.rb +130 -0
- data/lib/legion/extensions/social_learning/runners/social_learning.rb +101 -0
- data/lib/legion/extensions/social_learning/version.rb +9 -0
- data/lib/legion/extensions/social_learning.rb +17 -0
- data/spec/legion/extensions/social_learning/client_spec.rb +25 -0
- data/spec/legion/extensions/social_learning/helpers/constants_spec.rb +44 -0
- data/spec/legion/extensions/social_learning/helpers/model_agent_spec.rb +120 -0
- data/spec/legion/extensions/social_learning/helpers/observed_behavior_spec.rb +81 -0
- data/spec/legion/extensions/social_learning/helpers/social_learning_engine_spec.rb +196 -0
- data/spec/legion/extensions/social_learning/runners/social_learning_spec.rb +150 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a099b9a518340e711699d958e1de852e7d04d420adca67b0d67f96c7ecbe92a9
|
|
4
|
+
data.tar.gz: 42bb370a5586e73d9d8ba4650103091345f915b8a952aefa5057a1afb233594c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 89887a76ef663f2d17fa65a20a78d350849791c7916a4f1c26fd14e4cd20ffc93a93fef6edfb0ec428dce93f70bd9f620e907d2a30879ca609ef54d93f87ccf5
|
|
7
|
+
data.tar.gz: 810ac3320303238b05e2b1107bb3def16f48a87cb455867b4a1abac2eadc5430de45ec1402cbe1befcd5561c050619326e5cd9b04ee07e9978a6cc20bbae1311
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/social_learning/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-social-learning'
|
|
7
|
+
spec.version = Legion::Extensions::SocialLearning::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Social Learning'
|
|
12
|
+
spec.description = "Bandura's Social Cognitive Theory for LegionIO: vicarious learning via model observation and reproduction"
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-social-learning'
|
|
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-social-learning'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-social-learning'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-social-learning'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-social-learning/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-social-learning.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/social_learning/helpers/constants'
|
|
4
|
+
require 'legion/extensions/social_learning/helpers/observed_behavior'
|
|
5
|
+
require 'legion/extensions/social_learning/helpers/model_agent'
|
|
6
|
+
require 'legion/extensions/social_learning/helpers/social_learning_engine'
|
|
7
|
+
require 'legion/extensions/social_learning/runners/social_learning'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module SocialLearning
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::SocialLearning
|
|
14
|
+
|
|
15
|
+
def initialize(engine: nil)
|
|
16
|
+
@engine = engine || Helpers::SocialLearningEngine.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module SocialLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_MODELS = 200
|
|
9
|
+
MAX_BEHAVIORS = 500
|
|
10
|
+
MAX_OBSERVATIONS = 1000
|
|
11
|
+
MAX_HISTORY = 300
|
|
12
|
+
|
|
13
|
+
DEFAULT_PRESTIGE = 0.5
|
|
14
|
+
PRESTIGE_FLOOR = 0.0
|
|
15
|
+
PRESTIGE_CEILING = 1.0
|
|
16
|
+
ATTENTION_THRESHOLD = 0.3
|
|
17
|
+
RETENTION_DECAY = 0.02
|
|
18
|
+
REPRODUCTION_CONFIDENCE = 0.5
|
|
19
|
+
REINFORCEMENT_BOOST = 0.15
|
|
20
|
+
PUNISHMENT_PENALTY = 0.2
|
|
21
|
+
PRESTIGE_LEARNING_RATE = 0.1
|
|
22
|
+
STALE_THRESHOLD = 120
|
|
23
|
+
|
|
24
|
+
OUTCOME_TYPES = %i[positive negative neutral].freeze
|
|
25
|
+
LEARNING_STAGES = %i[attention retention reproduction motivation].freeze
|
|
26
|
+
|
|
27
|
+
MODEL_LABELS = {
|
|
28
|
+
(0.8..) => :expert,
|
|
29
|
+
(0.6...0.8) => :proficient,
|
|
30
|
+
(0.4...0.6) => :peer,
|
|
31
|
+
(0.2...0.4) => :novice,
|
|
32
|
+
(..0.2) => :unreliable
|
|
33
|
+
}.freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module SocialLearning
|
|
8
|
+
module Helpers
|
|
9
|
+
class ModelAgent
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :agent_id, :domain, :observation_count,
|
|
13
|
+
:success_count, :created_at, :last_observed_at,
|
|
14
|
+
:observed_behaviors
|
|
15
|
+
attr_accessor :prestige
|
|
16
|
+
|
|
17
|
+
def initialize(agent_id:, domain:, prestige: Constants::DEFAULT_PRESTIGE)
|
|
18
|
+
@id = SecureRandom.uuid
|
|
19
|
+
@agent_id = agent_id
|
|
20
|
+
@domain = domain
|
|
21
|
+
@prestige = prestige.clamp(Constants::PRESTIGE_FLOOR, Constants::PRESTIGE_CEILING)
|
|
22
|
+
@observed_behaviors = []
|
|
23
|
+
@observation_count = 0
|
|
24
|
+
@success_count = 0
|
|
25
|
+
@created_at = Time.now.utc
|
|
26
|
+
@last_observed_at = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def observe!(behavior:, outcome:)
|
|
30
|
+
@observation_count += 1
|
|
31
|
+
@last_observed_at = Time.now.utc
|
|
32
|
+
|
|
33
|
+
if outcome == :positive
|
|
34
|
+
@success_count += 1
|
|
35
|
+
@prestige = (@prestige + Constants::PRESTIGE_LEARNING_RATE).clamp(
|
|
36
|
+
Constants::PRESTIGE_FLOOR,
|
|
37
|
+
Constants::PRESTIGE_CEILING
|
|
38
|
+
)
|
|
39
|
+
elsif outcome == :negative
|
|
40
|
+
@prestige = (@prestige - Constants::PRESTIGE_LEARNING_RATE).clamp(
|
|
41
|
+
Constants::PRESTIGE_FLOOR,
|
|
42
|
+
Constants::PRESTIGE_CEILING
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
behavior
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prestige_label
|
|
50
|
+
Constants::MODEL_LABELS.find { |range, _label| range.include?(@prestige) }&.last || :unknown
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def success_rate
|
|
54
|
+
return 0.0 if @observation_count.zero?
|
|
55
|
+
|
|
56
|
+
(@success_count.to_f / @observation_count).round(4)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_h
|
|
60
|
+
{
|
|
61
|
+
id: @id,
|
|
62
|
+
agent_id: @agent_id,
|
|
63
|
+
domain: @domain,
|
|
64
|
+
prestige: @prestige.round(4),
|
|
65
|
+
prestige_label: prestige_label,
|
|
66
|
+
observation_count: @observation_count,
|
|
67
|
+
success_count: @success_count,
|
|
68
|
+
success_rate: success_rate,
|
|
69
|
+
behavior_count: @observed_behaviors.size,
|
|
70
|
+
created_at: @created_at,
|
|
71
|
+
last_observed_at: @last_observed_at
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module SocialLearning
|
|
8
|
+
module Helpers
|
|
9
|
+
class ObservedBehavior
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :model_agent_id, :action, :domain, :context,
|
|
13
|
+
:outcome, :created_at
|
|
14
|
+
attr_accessor :retention, :reproduced
|
|
15
|
+
|
|
16
|
+
def initialize(model_agent_id:, action:, domain:, outcome:, context: {})
|
|
17
|
+
@id = SecureRandom.uuid
|
|
18
|
+
@model_agent_id = model_agent_id
|
|
19
|
+
@action = action
|
|
20
|
+
@domain = domain
|
|
21
|
+
@context = context
|
|
22
|
+
@outcome = outcome
|
|
23
|
+
@retention = 1.0
|
|
24
|
+
@reproduced = false
|
|
25
|
+
@created_at = Time.now.utc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def decay_retention!
|
|
29
|
+
@retention = (@retention - Constants::RETENTION_DECAY).clamp(
|
|
30
|
+
Constants::PRESTIGE_FLOOR,
|
|
31
|
+
Constants::PRESTIGE_CEILING
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def retained?
|
|
36
|
+
@retention >= Constants::REPRODUCTION_CONFIDENCE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
id: @id,
|
|
42
|
+
model_agent_id: @model_agent_id,
|
|
43
|
+
action: @action,
|
|
44
|
+
domain: @domain,
|
|
45
|
+
context: @context,
|
|
46
|
+
outcome: @outcome,
|
|
47
|
+
retention: @retention.round(4),
|
|
48
|
+
reproduced: @reproduced,
|
|
49
|
+
retained: retained?,
|
|
50
|
+
created_at: @created_at
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module SocialLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
class SocialLearningEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@models = {} # id -> ModelAgent
|
|
12
|
+
@behaviors = {} # id -> ObservedBehavior
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register_model(agent_id:, domain:, prestige: Constants::DEFAULT_PRESTIGE)
|
|
16
|
+
prune_models if @models.size >= Constants::MAX_MODELS
|
|
17
|
+
|
|
18
|
+
model = ModelAgent.new(agent_id: agent_id, domain: domain, prestige: prestige)
|
|
19
|
+
@models[model.id] = model
|
|
20
|
+
model
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def observe_behavior(model_id:, action:, domain:, outcome:, context: {})
|
|
24
|
+
model = @models.fetch(model_id, nil)
|
|
25
|
+
return nil unless model
|
|
26
|
+
return nil if model.prestige < Constants::ATTENTION_THRESHOLD
|
|
27
|
+
|
|
28
|
+
prune_behaviors if @behaviors.size >= Constants::MAX_BEHAVIORS
|
|
29
|
+
|
|
30
|
+
behavior = ObservedBehavior.new(
|
|
31
|
+
model_agent_id: model_id,
|
|
32
|
+
action: action,
|
|
33
|
+
domain: domain,
|
|
34
|
+
outcome: outcome,
|
|
35
|
+
context: context
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
model.observe!(behavior: behavior, outcome: outcome)
|
|
39
|
+
model.observed_behaviors << behavior
|
|
40
|
+
@behaviors[behavior.id] = behavior
|
|
41
|
+
behavior
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def retained_behaviors(domain: nil)
|
|
45
|
+
behaviors = @behaviors.values.select(&:retained?)
|
|
46
|
+
return behaviors unless domain
|
|
47
|
+
|
|
48
|
+
behaviors.select { |beh| beh.domain == domain }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reproducible_behaviors(domain: nil)
|
|
52
|
+
behaviors = retained_behaviors(domain: domain)
|
|
53
|
+
behaviors.select { |beh| beh.retention >= Constants::REPRODUCTION_CONFIDENCE }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reproduce_behavior(behavior_id:)
|
|
57
|
+
behavior = @behaviors.fetch(behavior_id, nil)
|
|
58
|
+
return nil unless behavior
|
|
59
|
+
return nil unless behavior.retained?
|
|
60
|
+
|
|
61
|
+
behavior.reproduced = true
|
|
62
|
+
behavior
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reinforce_reproduction(behavior_id:, outcome:)
|
|
66
|
+
behavior = @behaviors.fetch(behavior_id, nil)
|
|
67
|
+
return nil unless behavior
|
|
68
|
+
|
|
69
|
+
model = @models.fetch(behavior.model_agent_id, nil)
|
|
70
|
+
return nil unless model
|
|
71
|
+
|
|
72
|
+
case outcome
|
|
73
|
+
when :positive
|
|
74
|
+
model.prestige = (model.prestige + Constants::REINFORCEMENT_BOOST).clamp(
|
|
75
|
+
Constants::PRESTIGE_FLOOR,
|
|
76
|
+
Constants::PRESTIGE_CEILING
|
|
77
|
+
)
|
|
78
|
+
when :negative
|
|
79
|
+
model.prestige = (model.prestige - Constants::PUNISHMENT_PENALTY).clamp(
|
|
80
|
+
Constants::PRESTIGE_FLOOR,
|
|
81
|
+
Constants::PRESTIGE_CEILING
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
{ behavior: behavior.to_h, model_prestige: model.prestige.round(4) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def best_models(limit: 5)
|
|
89
|
+
@models.values
|
|
90
|
+
.sort_by { |mod| -mod.prestige }
|
|
91
|
+
.first(limit)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def by_domain(domain:)
|
|
95
|
+
@models.values.select { |mod| mod.domain == domain }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def decay_all
|
|
99
|
+
@behaviors.each_value(&:decay_retention!)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def prune_forgotten
|
|
103
|
+
@behaviors.delete_if { |_id, beh| beh.retention < 0.05 }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_h
|
|
107
|
+
{
|
|
108
|
+
model_count: @models.size,
|
|
109
|
+
behavior_count: @behaviors.size,
|
|
110
|
+
retained_count: retained_behaviors.size,
|
|
111
|
+
reproducible_count: reproducible_behaviors.size
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def prune_models
|
|
118
|
+
oldest = @models.values.min_by(&:created_at)
|
|
119
|
+
@models.delete(oldest.id) if oldest
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def prune_behaviors
|
|
123
|
+
oldest = @behaviors.values.min_by(&:created_at)
|
|
124
|
+
@behaviors.delete(oldest.id) if oldest
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module SocialLearning
|
|
6
|
+
module Runners
|
|
7
|
+
module SocialLearning
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def register_model_agent(agent_id:, domain:, prestige: nil, **)
|
|
12
|
+
init_prestige = prestige || Helpers::Constants::DEFAULT_PRESTIGE
|
|
13
|
+
Legion::Logging.debug "[social_learning] register_model agent=#{agent_id} domain=#{domain} prestige=#{init_prestige}"
|
|
14
|
+
model = engine.register_model(agent_id: agent_id, domain: domain, prestige: init_prestige)
|
|
15
|
+
{ success: true, model: model.to_h }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def observe_agent_behavior(model_id:, action:, domain:, outcome:, context: {}, **)
|
|
19
|
+
Legion::Logging.debug "[social_learning] observe model=#{model_id} action=#{action} domain=#{domain} outcome=#{outcome}"
|
|
20
|
+
behavior = engine.observe_behavior(
|
|
21
|
+
model_id: model_id,
|
|
22
|
+
action: action,
|
|
23
|
+
domain: domain,
|
|
24
|
+
outcome: outcome,
|
|
25
|
+
context: context
|
|
26
|
+
)
|
|
27
|
+
if behavior
|
|
28
|
+
{ success: true, behavior: behavior.to_h }
|
|
29
|
+
else
|
|
30
|
+
{ success: false, reason: 'model not found or below attention threshold' }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def retained_behaviors(domain: nil, **)
|
|
35
|
+
Legion::Logging.debug "[social_learning] retained_behaviors domain=#{domain.inspect}"
|
|
36
|
+
behaviors = engine.retained_behaviors(domain: domain)
|
|
37
|
+
{ success: true, behaviors: behaviors.map(&:to_h), count: behaviors.size }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reproducible_behaviors(domain: nil, **)
|
|
41
|
+
Legion::Logging.debug "[social_learning] reproducible_behaviors domain=#{domain.inspect}"
|
|
42
|
+
behaviors = engine.reproducible_behaviors(domain: domain)
|
|
43
|
+
{ success: true, behaviors: behaviors.map(&:to_h), count: behaviors.size }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reproduce_observed_behavior(behavior_id:, **)
|
|
47
|
+
Legion::Logging.debug "[social_learning] reproduce behavior_id=#{behavior_id}"
|
|
48
|
+
behavior = engine.reproduce_behavior(behavior_id: behavior_id)
|
|
49
|
+
if behavior
|
|
50
|
+
{ success: true, behavior: behavior.to_h }
|
|
51
|
+
else
|
|
52
|
+
{ success: false, reason: 'behavior not found or retention too low' }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reinforce_reproduction(behavior_id:, outcome:, **)
|
|
57
|
+
Legion::Logging.debug "[social_learning] reinforce behavior_id=#{behavior_id} outcome=#{outcome}"
|
|
58
|
+
result = engine.reinforce_reproduction(behavior_id: behavior_id, outcome: outcome)
|
|
59
|
+
if result
|
|
60
|
+
{ success: true }.merge(result)
|
|
61
|
+
else
|
|
62
|
+
{ success: false, reason: 'behavior or model not found' }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def best_model_agents(limit: 5, **)
|
|
67
|
+
lim = limit.to_i
|
|
68
|
+
Legion::Logging.debug "[social_learning] best_model_agents limit=#{lim}"
|
|
69
|
+
models = engine.best_models(limit: lim)
|
|
70
|
+
{ success: true, models: models.map(&:to_h), count: models.size }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def domain_models(domain:, **)
|
|
74
|
+
Legion::Logging.debug "[social_learning] domain_models domain=#{domain}"
|
|
75
|
+
models = engine.by_domain(domain: domain)
|
|
76
|
+
{ success: true, models: models.map(&:to_h), count: models.size }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update_social_learning(**)
|
|
80
|
+
Legion::Logging.debug '[social_learning] update_social_learning decay+prune cycle'
|
|
81
|
+
engine.decay_all
|
|
82
|
+
engine.prune_forgotten
|
|
83
|
+
stats = engine.to_h
|
|
84
|
+
{ success: true }.merge(stats)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def social_learning_stats(**)
|
|
88
|
+
Legion::Logging.debug '[social_learning] social_learning_stats'
|
|
89
|
+
{ success: true }.merge(engine.to_h)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def engine
|
|
95
|
+
@engine ||= Helpers::SocialLearningEngine.new
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/social_learning/version'
|
|
4
|
+
require 'legion/extensions/social_learning/helpers/constants'
|
|
5
|
+
require 'legion/extensions/social_learning/helpers/observed_behavior'
|
|
6
|
+
require 'legion/extensions/social_learning/helpers/model_agent'
|
|
7
|
+
require 'legion/extensions/social_learning/helpers/social_learning_engine'
|
|
8
|
+
require 'legion/extensions/social_learning/runners/social_learning'
|
|
9
|
+
require 'legion/extensions/social_learning/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module SocialLearning
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Client do
|
|
4
|
+
let(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'responds to all runner methods' do
|
|
7
|
+
expect(client).to respond_to(:register_model_agent)
|
|
8
|
+
expect(client).to respond_to(:observe_agent_behavior)
|
|
9
|
+
expect(client).to respond_to(:retained_behaviors)
|
|
10
|
+
expect(client).to respond_to(:reproducible_behaviors)
|
|
11
|
+
expect(client).to respond_to(:reproduce_observed_behavior)
|
|
12
|
+
expect(client).to respond_to(:reinforce_reproduction)
|
|
13
|
+
expect(client).to respond_to(:best_model_agents)
|
|
14
|
+
expect(client).to respond_to(:domain_models)
|
|
15
|
+
expect(client).to respond_to(:update_social_learning)
|
|
16
|
+
expect(client).to respond_to(:social_learning_stats)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'accepts an injected engine' do
|
|
20
|
+
engine = Legion::Extensions::SocialLearning::Helpers::SocialLearningEngine.new
|
|
21
|
+
injected_client = described_class.new(engine: engine)
|
|
22
|
+
result = injected_client.social_learning_stats
|
|
23
|
+
expect(result[:success]).to be true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Helpers::Constants do
|
|
4
|
+
it 'defines MAX_MODELS' do
|
|
5
|
+
expect(described_module::MAX_MODELS).to eq(200)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'defines MAX_BEHAVIORS' do
|
|
9
|
+
expect(described_module::MAX_BEHAVIORS).to eq(500)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines ATTENTION_THRESHOLD' do
|
|
13
|
+
expect(described_module::ATTENTION_THRESHOLD).to eq(0.3)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines REPRODUCTION_CONFIDENCE' do
|
|
17
|
+
expect(described_module::REPRODUCTION_CONFIDENCE).to eq(0.5)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defines OUTCOME_TYPES' do
|
|
21
|
+
expect(described_module::OUTCOME_TYPES).to eq(%i[positive negative neutral])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defines LEARNING_STAGES' do
|
|
25
|
+
expect(described_module::LEARNING_STAGES).to eq(%i[attention retention reproduction motivation])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines MODEL_LABELS mapping' do
|
|
29
|
+
expect(described_module::MODEL_LABELS).to be_a(Hash)
|
|
30
|
+
expect(described_module::MODEL_LABELS.size).to eq(5)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'PRESTIGE_FLOOR is 0.0' do
|
|
34
|
+
expect(described_module::PRESTIGE_FLOOR).to eq(0.0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'PRESTIGE_CEILING is 1.0' do
|
|
38
|
+
expect(described_module::PRESTIGE_CEILING).to eq(1.0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def described_module
|
|
42
|
+
Legion::Extensions::SocialLearning::Helpers::Constants
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Helpers::ModelAgent do
|
|
4
|
+
subject(:model) do
|
|
5
|
+
described_class.new(agent_id: 'agent-abc', domain: :ops, prestige: 0.5)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'assigns a uuid id' do
|
|
10
|
+
expect(model.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'sets the agent_id' do
|
|
14
|
+
expect(model.agent_id).to eq('agent-abc')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'sets prestige' do
|
|
18
|
+
expect(model.prestige).to eq(0.5)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'starts with zero observation_count' do
|
|
22
|
+
expect(model.observation_count).to eq(0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'starts with empty observed_behaviors' do
|
|
26
|
+
expect(model.observed_behaviors).to be_empty
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'clamps prestige above ceiling to 1.0' do
|
|
30
|
+
m = described_class.new(agent_id: 'x', domain: :test, prestige: 5.0)
|
|
31
|
+
expect(m.prestige).to eq(1.0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'clamps prestige below floor to 0.0' do
|
|
35
|
+
m = described_class.new(agent_id: 'x', domain: :test, prestige: -1.0)
|
|
36
|
+
expect(m.prestige).to eq(0.0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#observe!' do
|
|
41
|
+
let(:fake_behavior) { double('ObservedBehavior') }
|
|
42
|
+
|
|
43
|
+
it 'increments observation_count' do
|
|
44
|
+
expect { model.observe!(behavior: fake_behavior, outcome: :neutral) }
|
|
45
|
+
.to change(model, :observation_count).by(1)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'increases prestige on positive outcome' do
|
|
49
|
+
before = model.prestige
|
|
50
|
+
model.observe!(behavior: fake_behavior, outcome: :positive)
|
|
51
|
+
expect(model.prestige).to be > before
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'decreases prestige on negative outcome' do
|
|
55
|
+
before = model.prestige
|
|
56
|
+
model.observe!(behavior: fake_behavior, outcome: :negative)
|
|
57
|
+
expect(model.prestige).to be < before
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'does not change prestige on neutral outcome' do
|
|
61
|
+
before = model.prestige
|
|
62
|
+
model.observe!(behavior: fake_behavior, outcome: :neutral)
|
|
63
|
+
expect(model.prestige).to eq(before)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'tracks success_count for positive outcomes' do
|
|
67
|
+
model.observe!(behavior: fake_behavior, outcome: :positive)
|
|
68
|
+
expect(model.success_count).to eq(1)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#prestige_label' do
|
|
73
|
+
it 'returns :expert for prestige >= 0.8' do
|
|
74
|
+
model.prestige = 0.9
|
|
75
|
+
expect(model.prestige_label).to eq(:expert)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns :proficient for prestige in 0.6...0.8' do
|
|
79
|
+
model.prestige = 0.7
|
|
80
|
+
expect(model.prestige_label).to eq(:proficient)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'returns :peer for prestige in 0.4...0.6' do
|
|
84
|
+
model.prestige = 0.5
|
|
85
|
+
expect(model.prestige_label).to eq(:peer)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns :novice for prestige in 0.2...0.4' do
|
|
89
|
+
model.prestige = 0.3
|
|
90
|
+
expect(model.prestige_label).to eq(:novice)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns :unreliable for prestige below 0.2' do
|
|
94
|
+
model.prestige = 0.1
|
|
95
|
+
expect(model.prestige_label).to eq(:unreliable)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#success_rate' do
|
|
100
|
+
it 'returns 0.0 with no observations' do
|
|
101
|
+
expect(model.success_rate).to eq(0.0)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns correct ratio after observations' do
|
|
105
|
+
fake_behavior = double('ObservedBehavior')
|
|
106
|
+
model.observe!(behavior: fake_behavior, outcome: :positive)
|
|
107
|
+
model.observe!(behavior: fake_behavior, outcome: :negative)
|
|
108
|
+
expect(model.success_rate).to eq(0.5)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe '#to_h' do
|
|
113
|
+
it 'returns a hash with expected keys' do
|
|
114
|
+
hash = model.to_h
|
|
115
|
+
expect(hash).to include(:id, :agent_id, :domain, :prestige, :prestige_label,
|
|
116
|
+
:observation_count, :success_count, :success_rate,
|
|
117
|
+
:behavior_count, :created_at)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Helpers::ObservedBehavior do
|
|
4
|
+
subject(:behavior) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
model_agent_id: 'model-123',
|
|
7
|
+
action: 'write_test',
|
|
8
|
+
domain: :coding,
|
|
9
|
+
outcome: :positive,
|
|
10
|
+
context: { language: 'ruby' }
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'assigns a uuid id' do
|
|
16
|
+
expect(behavior.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'sets retention to 1.0' do
|
|
20
|
+
expect(behavior.retention).to eq(1.0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'sets reproduced to false' do
|
|
24
|
+
expect(behavior.reproduced).to be false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'stores the action' do
|
|
28
|
+
expect(behavior.action).to eq('write_test')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'stores the domain' do
|
|
32
|
+
expect(behavior.domain).to eq(:coding)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'stores the outcome' do
|
|
36
|
+
expect(behavior.outcome).to eq(:positive)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'stores context' do
|
|
40
|
+
expect(behavior.context).to eq({ language: 'ruby' })
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#decay_retention!' do
|
|
45
|
+
it 'reduces retention by RETENTION_DECAY' do
|
|
46
|
+
before = behavior.retention
|
|
47
|
+
behavior.decay_retention!
|
|
48
|
+
expect(behavior.retention).to be < before
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'does not go below 0.0' do
|
|
52
|
+
50.times { behavior.decay_retention! }
|
|
53
|
+
expect(behavior.retention).to be >= 0.0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#retained?' do
|
|
58
|
+
it 'returns true when retention is above REPRODUCTION_CONFIDENCE' do
|
|
59
|
+
expect(behavior.retained?).to be true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'returns false when retention has decayed below threshold' do
|
|
63
|
+
behavior.retention = 0.4
|
|
64
|
+
expect(behavior.retained?).to be false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#to_h' do
|
|
69
|
+
it 'returns a hash with expected keys' do
|
|
70
|
+
hash = behavior.to_h
|
|
71
|
+
expect(hash).to include(:id, :model_agent_id, :action, :domain, :outcome,
|
|
72
|
+
:retention, :reproduced, :retained, :created_at)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'rounds retention to 4 decimal places' do
|
|
76
|
+
behavior.decay_retention!
|
|
77
|
+
hash = behavior.to_h
|
|
78
|
+
expect(hash[:retention].to_s.split('.').last.length).to be <= 4
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Helpers::SocialLearningEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:model) { engine.register_model(agent_id: 'agent-1', domain: :coding) }
|
|
7
|
+
|
|
8
|
+
describe '#register_model' do
|
|
9
|
+
it 'returns a ModelAgent' do
|
|
10
|
+
expect(model).to be_a(Legion::Extensions::SocialLearning::Helpers::ModelAgent)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'assigns agent_id and domain' do
|
|
14
|
+
expect(model.agent_id).to eq('agent-1')
|
|
15
|
+
expect(model.domain).to eq(:coding)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'uses DEFAULT_PRESTIGE when none given' do
|
|
19
|
+
expect(model.prestige).to eq(Legion::Extensions::SocialLearning::Helpers::Constants::DEFAULT_PRESTIGE)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#observe_behavior' do
|
|
24
|
+
it 'returns nil for unknown model' do
|
|
25
|
+
result = engine.observe_behavior(
|
|
26
|
+
model_id: 'nonexistent', action: 'do_thing', domain: :coding, outcome: :positive
|
|
27
|
+
)
|
|
28
|
+
expect(result).to be_nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns nil when model prestige is below ATTENTION_THRESHOLD' do
|
|
32
|
+
low_model = engine.register_model(agent_id: 'low', domain: :coding, prestige: 0.1)
|
|
33
|
+
result = engine.observe_behavior(
|
|
34
|
+
model_id: low_model.id, action: 'do_thing', domain: :coding, outcome: :positive
|
|
35
|
+
)
|
|
36
|
+
expect(result).to be_nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns an ObservedBehavior when model passes attention threshold' do
|
|
40
|
+
behavior = engine.observe_behavior(
|
|
41
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
42
|
+
)
|
|
43
|
+
expect(behavior).to be_a(Legion::Extensions::SocialLearning::Helpers::ObservedBehavior)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'updates model prestige on positive outcome' do
|
|
47
|
+
before = model.prestige
|
|
48
|
+
engine.observe_behavior(
|
|
49
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
50
|
+
)
|
|
51
|
+
expect(model.prestige).to be > before
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '#retained_behaviors' do
|
|
56
|
+
before do
|
|
57
|
+
engine.observe_behavior(
|
|
58
|
+
model_id: model.id, action: 'action_a', domain: :coding, outcome: :positive
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'returns behaviors with retention above REPRODUCTION_CONFIDENCE' do
|
|
63
|
+
result = engine.retained_behaviors
|
|
64
|
+
expect(result).not_to be_empty
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'filters by domain when given' do
|
|
68
|
+
engine.observe_behavior(
|
|
69
|
+
model_id: model.id, action: 'action_b', domain: :ops, outcome: :positive
|
|
70
|
+
)
|
|
71
|
+
result = engine.retained_behaviors(domain: :coding)
|
|
72
|
+
expect(result.all? { |beh| beh.domain == :coding }).to be true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#reproducible_behaviors' do
|
|
77
|
+
it 'returns behaviors above REPRODUCTION_CONFIDENCE' do
|
|
78
|
+
engine.observe_behavior(
|
|
79
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
80
|
+
)
|
|
81
|
+
result = engine.reproducible_behaviors
|
|
82
|
+
expect(result).not_to be_empty
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '#reproduce_behavior' do
|
|
87
|
+
let(:behavior) do
|
|
88
|
+
engine.observe_behavior(
|
|
89
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'marks the behavior as reproduced' do
|
|
94
|
+
engine.reproduce_behavior(behavior_id: behavior.id)
|
|
95
|
+
expect(behavior.reproduced).to be true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns nil for unknown behavior_id' do
|
|
99
|
+
result = engine.reproduce_behavior(behavior_id: 'nonexistent')
|
|
100
|
+
expect(result).to be_nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '#reinforce_reproduction' do
|
|
105
|
+
let(:behavior) do
|
|
106
|
+
engine.observe_behavior(
|
|
107
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'boosts model prestige on positive outcome' do
|
|
112
|
+
before = model.prestige
|
|
113
|
+
engine.reinforce_reproduction(behavior_id: behavior.id, outcome: :positive)
|
|
114
|
+
expect(model.prestige).to be > before
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'penalizes model prestige on negative outcome' do
|
|
118
|
+
before = model.prestige
|
|
119
|
+
engine.reinforce_reproduction(behavior_id: behavior.id, outcome: :negative)
|
|
120
|
+
expect(model.prestige).to be < before
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'returns nil for unknown behavior_id' do
|
|
124
|
+
result = engine.reinforce_reproduction(behavior_id: 'nonexistent', outcome: :positive)
|
|
125
|
+
expect(result).to be_nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'returns a hash with behavior and model_prestige' do
|
|
129
|
+
result = engine.reinforce_reproduction(behavior_id: behavior.id, outcome: :positive)
|
|
130
|
+
expect(result).to include(:behavior, :model_prestige)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#best_models' do
|
|
135
|
+
before do
|
|
136
|
+
engine.register_model(agent_id: 'agent-2', domain: :ops, prestige: 0.9)
|
|
137
|
+
engine.register_model(agent_id: 'agent-3', domain: :coding, prestige: 0.3)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'returns models sorted by prestige descending' do
|
|
141
|
+
models = engine.best_models(limit: 3)
|
|
142
|
+
prestiges = models.map(&:prestige)
|
|
143
|
+
expect(prestiges).to eq(prestiges.sort.reverse)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'respects the limit' do
|
|
147
|
+
models = engine.best_models(limit: 2)
|
|
148
|
+
expect(models.size).to be <= 2
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe '#by_domain' do
|
|
153
|
+
before do
|
|
154
|
+
engine.register_model(agent_id: 'agent-ops', domain: :ops)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'returns only models in the given domain' do
|
|
158
|
+
result = engine.by_domain(domain: :ops)
|
|
159
|
+
expect(result.all? { |mod| mod.domain == :ops }).to be true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'returns empty array for unknown domain' do
|
|
163
|
+
result = engine.by_domain(domain: :unknown_domain)
|
|
164
|
+
expect(result).to be_empty
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#decay_all' do
|
|
169
|
+
it 'decays retention on all behaviors' do
|
|
170
|
+
behavior = engine.observe_behavior(
|
|
171
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
172
|
+
)
|
|
173
|
+
before = behavior.retention
|
|
174
|
+
engine.decay_all
|
|
175
|
+
expect(behavior.retention).to be < before
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
describe '#prune_forgotten' do
|
|
180
|
+
it 'removes behaviors below 0.05 retention' do
|
|
181
|
+
behavior = engine.observe_behavior(
|
|
182
|
+
model_id: model.id, action: 'write_test', domain: :coding, outcome: :positive
|
|
183
|
+
)
|
|
184
|
+
behavior.retention = 0.04
|
|
185
|
+
engine.prune_forgotten
|
|
186
|
+
expect(engine.retained_behaviors).not_to include(behavior)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
describe '#to_h' do
|
|
191
|
+
it 'returns stats hash' do
|
|
192
|
+
result = engine.to_h
|
|
193
|
+
expect(result).to include(:model_count, :behavior_count, :retained_count, :reproducible_count)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::SocialLearning::Runners::SocialLearning do
|
|
4
|
+
let(:client) { Legion::Extensions::SocialLearning::Client.new }
|
|
5
|
+
|
|
6
|
+
describe '#register_model_agent' do
|
|
7
|
+
it 'returns success with model hash' do
|
|
8
|
+
result = client.register_model_agent(agent_id: 'agent-1', domain: :coding)
|
|
9
|
+
expect(result[:success]).to be true
|
|
10
|
+
expect(result[:model]).to include(:id, :agent_id, :domain, :prestige)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'accepts custom prestige' do
|
|
14
|
+
result = client.register_model_agent(agent_id: 'agent-1', domain: :coding, prestige: 0.8)
|
|
15
|
+
expect(result[:model][:prestige]).to eq(0.8)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '#observe_agent_behavior' do
|
|
20
|
+
let(:model_id) do
|
|
21
|
+
client.register_model_agent(agent_id: 'agent-1', domain: :coding)[:model][:id]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns success with behavior hash when model passes attention threshold' do
|
|
25
|
+
result = client.observe_agent_behavior(
|
|
26
|
+
model_id: model_id, action: 'write_test', domain: :coding, outcome: :positive
|
|
27
|
+
)
|
|
28
|
+
expect(result[:success]).to be true
|
|
29
|
+
expect(result[:behavior]).to include(:id, :action, :retention)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns success: false for unknown model_id' do
|
|
33
|
+
result = client.observe_agent_behavior(
|
|
34
|
+
model_id: 'nonexistent', action: 'do_thing', domain: :coding, outcome: :positive
|
|
35
|
+
)
|
|
36
|
+
expect(result[:success]).to be false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe '#retained_behaviors' do
|
|
41
|
+
before do
|
|
42
|
+
model_id = client.register_model_agent(agent_id: 'agent-1', domain: :coding)[:model][:id]
|
|
43
|
+
client.observe_agent_behavior(
|
|
44
|
+
model_id: model_id, action: 'write_test', domain: :coding, outcome: :positive
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns behaviors with count' do
|
|
49
|
+
result = client.retained_behaviors
|
|
50
|
+
expect(result[:success]).to be true
|
|
51
|
+
expect(result).to include(:behaviors, :count)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'filters by domain' do
|
|
55
|
+
result = client.retained_behaviors(domain: :coding)
|
|
56
|
+
expect(result[:behaviors].all? { |beh| beh[:domain] == :coding }).to be true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#reproducible_behaviors' do
|
|
61
|
+
it 'returns success: true with behaviors list' do
|
|
62
|
+
result = client.reproducible_behaviors
|
|
63
|
+
expect(result[:success]).to be true
|
|
64
|
+
expect(result).to include(:behaviors, :count)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#reproduce_observed_behavior' do
|
|
69
|
+
let(:behavior_id) do
|
|
70
|
+
model_id = client.register_model_agent(agent_id: 'agent-1', domain: :coding)[:model][:id]
|
|
71
|
+
client.observe_agent_behavior(
|
|
72
|
+
model_id: model_id, action: 'write_test', domain: :coding, outcome: :positive
|
|
73
|
+
)[:behavior][:id]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns success with reproduced behavior' do
|
|
77
|
+
result = client.reproduce_observed_behavior(behavior_id: behavior_id)
|
|
78
|
+
expect(result[:success]).to be true
|
|
79
|
+
expect(result[:behavior][:reproduced]).to be true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'returns success: false for unknown behavior_id' do
|
|
83
|
+
result = client.reproduce_observed_behavior(behavior_id: 'nonexistent')
|
|
84
|
+
expect(result[:success]).to be false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe '#reinforce_reproduction' do
|
|
89
|
+
let(:behavior_id) do
|
|
90
|
+
model_id = client.register_model_agent(agent_id: 'agent-1', domain: :coding)[:model][:id]
|
|
91
|
+
client.observe_agent_behavior(
|
|
92
|
+
model_id: model_id, action: 'write_test', domain: :coding, outcome: :positive
|
|
93
|
+
)[:behavior][:id]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns success with model_prestige on positive outcome' do
|
|
97
|
+
result = client.reinforce_reproduction(behavior_id: behavior_id, outcome: :positive)
|
|
98
|
+
expect(result[:success]).to be true
|
|
99
|
+
expect(result[:model_prestige]).to be_a(Float)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns success: false for unknown behavior_id' do
|
|
103
|
+
result = client.reinforce_reproduction(behavior_id: 'nonexistent', outcome: :positive)
|
|
104
|
+
expect(result[:success]).to be false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#best_model_agents' do
|
|
109
|
+
before do
|
|
110
|
+
client.register_model_agent(agent_id: 'agent-1', domain: :coding, prestige: 0.9)
|
|
111
|
+
client.register_model_agent(agent_id: 'agent-2', domain: :ops, prestige: 0.4)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns models sorted by prestige' do
|
|
115
|
+
result = client.best_model_agents(limit: 2)
|
|
116
|
+
expect(result[:success]).to be true
|
|
117
|
+
prestiges = result[:models].map { |mod| mod[:prestige] }
|
|
118
|
+
expect(prestiges).to eq(prestiges.sort.reverse)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#domain_models' do
|
|
123
|
+
before do
|
|
124
|
+
client.register_model_agent(agent_id: 'agent-1', domain: :coding)
|
|
125
|
+
client.register_model_agent(agent_id: 'agent-2', domain: :ops)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'returns only models for the given domain' do
|
|
129
|
+
result = client.domain_models(domain: :coding)
|
|
130
|
+
expect(result[:success]).to be true
|
|
131
|
+
expect(result[:models].all? { |mod| mod[:domain] == :coding }).to be true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#update_social_learning' do
|
|
136
|
+
it 'returns success with stats' do
|
|
137
|
+
result = client.update_social_learning
|
|
138
|
+
expect(result[:success]).to be true
|
|
139
|
+
expect(result).to include(:model_count, :behavior_count)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe '#social_learning_stats' do
|
|
144
|
+
it 'returns success with stats hash' do
|
|
145
|
+
result = client.social_learning_stats
|
|
146
|
+
expect(result[:success]).to be true
|
|
147
|
+
expect(result).to include(:model_count, :behavior_count, :retained_count, :reproducible_count)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
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/social_learning'
|
|
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,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-social-learning
|
|
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: 'Bandura''s Social Cognitive Theory for LegionIO: vicarious learning
|
|
27
|
+
via model observation and reproduction'
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- lex-social-learning.gemspec
|
|
36
|
+
- lib/legion/extensions/social_learning.rb
|
|
37
|
+
- lib/legion/extensions/social_learning/client.rb
|
|
38
|
+
- lib/legion/extensions/social_learning/helpers/constants.rb
|
|
39
|
+
- lib/legion/extensions/social_learning/helpers/model_agent.rb
|
|
40
|
+
- lib/legion/extensions/social_learning/helpers/observed_behavior.rb
|
|
41
|
+
- lib/legion/extensions/social_learning/helpers/social_learning_engine.rb
|
|
42
|
+
- lib/legion/extensions/social_learning/runners/social_learning.rb
|
|
43
|
+
- lib/legion/extensions/social_learning/version.rb
|
|
44
|
+
- spec/legion/extensions/social_learning/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/social_learning/helpers/constants_spec.rb
|
|
46
|
+
- spec/legion/extensions/social_learning/helpers/model_agent_spec.rb
|
|
47
|
+
- spec/legion/extensions/social_learning/helpers/observed_behavior_spec.rb
|
|
48
|
+
- spec/legion/extensions/social_learning/helpers/social_learning_engine_spec.rb
|
|
49
|
+
- spec/legion/extensions/social_learning/runners/social_learning_spec.rb
|
|
50
|
+
- spec/spec_helper.rb
|
|
51
|
+
homepage: https://github.com/LegionIO/lex-social-learning
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://github.com/LegionIO/lex-social-learning
|
|
56
|
+
source_code_uri: https://github.com/LegionIO/lex-social-learning
|
|
57
|
+
documentation_uri: https://github.com/LegionIO/lex-social-learning
|
|
58
|
+
changelog_uri: https://github.com/LegionIO/lex-social-learning
|
|
59
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-social-learning/issues
|
|
60
|
+
rubygems_mfa_required: 'true'
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.4'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.6.9
|
|
76
|
+
specification_version: 4
|
|
77
|
+
summary: LEX Social Learning
|
|
78
|
+
test_files: []
|