lex-cognitive-apprenticeship 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-cognitive-apprenticeship.gemspec +30 -0
- data/lib/legion/extensions/cognitive_apprenticeship/client.rb +24 -0
- data/lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship.rb +86 -0
- data/lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine.rb +105 -0
- data/lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model.rb +89 -0
- data/lib/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship.rb +108 -0
- data/lib/legion/extensions/cognitive_apprenticeship/version.rb +9 -0
- data/lib/legion/extensions/cognitive_apprenticeship.rb +15 -0
- data/spec/legion/extensions/cognitive_apprenticeship/client_spec.rb +20 -0
- data/spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine_spec.rb +146 -0
- data/spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model_spec.rb +124 -0
- data/spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_spec.rb +136 -0
- data/spec/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship_spec.rb +154 -0
- data/spec/legion/extensions/cognitive_apprenticeship_spec.rb +11 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9e2a7e552bc6b9ab40bf5ba2a02975c5a3306774558ab7e80e517167d058a5f8
|
|
4
|
+
data.tar.gz: 5f3bb14640b7ac611036ef4b0578abf03eb28d6521168ce5a44ac8523399f5db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8fc0cb1baaddd2da7583f48156ab5571bd04391a52802de11ab003866d731ad00e96ad8f5ecb05c48738846a01639acc268881fc294b2e5c44ed9c8dbfc03a9f
|
|
7
|
+
data.tar.gz: 4d2162f1c7269d7dcccb302f6fad5a157ab7c3d0ee58144703487eb1f7f26f2446893593ebc0674847a500a033272871ea1a4a7b08852475debba295c62b745c
|
data/Gemfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/cognitive_apprenticeship/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-cognitive-apprenticeship'
|
|
7
|
+
spec.version = Legion::Extensions::CognitiveApprenticeship::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Cognitive Apprenticeship'
|
|
12
|
+
spec.description = "Collins' Cognitive Apprenticeship model for brain-modeled agentic AI: " \
|
|
13
|
+
'guided learning through modeling, coaching, scaffolding, articulation, reflection, and exploration'
|
|
14
|
+
spec.homepage = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.4'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
|
|
20
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
|
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship'
|
|
22
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-apprenticeship/issues'
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-apprenticeship.gemspec Gemfile]
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ['lib']
|
|
29
|
+
spec.add_development_dependency 'legion-gaia'
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model'
|
|
4
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship'
|
|
5
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine'
|
|
6
|
+
require 'legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module CognitiveApprenticeship
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::CognitiveApprenticeship
|
|
13
|
+
|
|
14
|
+
def initialize(**)
|
|
15
|
+
@engine = Helpers::ApprenticeshipEngine.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :engine
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveApprenticeship
|
|
6
|
+
module Helpers
|
|
7
|
+
class Apprenticeship
|
|
8
|
+
include ApprenticeshipModel
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :skill_name, :domain, :mentor_id, :apprentice_id,
|
|
11
|
+
:mastery, :session_count, :created_at, :last_session_at
|
|
12
|
+
|
|
13
|
+
def initialize(skill_name:, domain:, mentor_id:, apprentice_id:)
|
|
14
|
+
data = ApprenticeshipModel.new_apprenticeship(
|
|
15
|
+
skill_name: skill_name,
|
|
16
|
+
domain: domain,
|
|
17
|
+
mentor_id: mentor_id,
|
|
18
|
+
apprentice_id: apprentice_id
|
|
19
|
+
)
|
|
20
|
+
@id = data[:id]
|
|
21
|
+
@skill_name = data[:skill_name]
|
|
22
|
+
@domain = data[:domain]
|
|
23
|
+
@mentor_id = data[:mentor_id]
|
|
24
|
+
@apprentice_id = data[:apprentice_id]
|
|
25
|
+
@mastery = data[:mastery]
|
|
26
|
+
@session_count = data[:session_count]
|
|
27
|
+
@created_at = data[:created_at]
|
|
28
|
+
@last_session_at = data[:last_session_at]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def current_phase
|
|
32
|
+
ApprenticeshipModel.phase_for(@mastery)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mastery_label
|
|
36
|
+
ApprenticeshipModel.mastery_label_for(@mastery)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def graduated?
|
|
40
|
+
@mastery >= ApprenticeshipModel::MASTERY_THRESHOLD
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def recommended_method
|
|
44
|
+
ApprenticeshipModel.phase_for(@mastery)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def learn!(method:, success:)
|
|
48
|
+
multiplier = case method
|
|
49
|
+
when :exploration then ApprenticeshipModel::EXPLORATION_MULTIPLIER
|
|
50
|
+
when :coaching then ApprenticeshipModel::COACHING_MULTIPLIER
|
|
51
|
+
else 1.0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
gain = success ? ApprenticeshipModel::LEARNING_GAIN * multiplier : 0.0
|
|
55
|
+
@mastery = ApprenticeshipModel.clamp_mastery(@mastery + gain)
|
|
56
|
+
@session_count += 1
|
|
57
|
+
@last_session_at = Time.now.utc
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def decay!
|
|
62
|
+
@mastery = ApprenticeshipModel.clamp_mastery(@mastery - ApprenticeshipModel::DECAY_RATE)
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_h
|
|
67
|
+
{
|
|
68
|
+
id: @id,
|
|
69
|
+
skill_name: @skill_name,
|
|
70
|
+
domain: @domain,
|
|
71
|
+
mentor_id: @mentor_id,
|
|
72
|
+
apprentice_id: @apprentice_id,
|
|
73
|
+
mastery: @mastery,
|
|
74
|
+
current_phase: current_phase,
|
|
75
|
+
mastery_label: mastery_label,
|
|
76
|
+
graduated: graduated?,
|
|
77
|
+
session_count: @session_count,
|
|
78
|
+
created_at: @created_at,
|
|
79
|
+
last_session_at: @last_session_at
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveApprenticeship
|
|
6
|
+
module Helpers
|
|
7
|
+
class ApprenticeshipEngine
|
|
8
|
+
def initialize
|
|
9
|
+
@apprenticeships = {}
|
|
10
|
+
@sessions = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:)
|
|
14
|
+
return nil if @apprenticeships.size >= ApprenticeshipModel::MAX_APPRENTICESHIPS
|
|
15
|
+
|
|
16
|
+
appr = Apprenticeship.new(
|
|
17
|
+
skill_name: skill_name,
|
|
18
|
+
domain: domain,
|
|
19
|
+
mentor_id: mentor_id,
|
|
20
|
+
apprentice_id: apprentice_id
|
|
21
|
+
)
|
|
22
|
+
@apprenticeships[appr.id] = appr
|
|
23
|
+
appr
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def conduct_session(apprenticeship_id:, method:, success:)
|
|
27
|
+
appr = @apprenticeships[apprenticeship_id]
|
|
28
|
+
return nil unless appr
|
|
29
|
+
|
|
30
|
+
appr.learn!(method: method, success: success)
|
|
31
|
+
record_session(apprenticeship_id: apprenticeship_id, method: method, success: success)
|
|
32
|
+
appr
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recommend_method(apprenticeship_id:)
|
|
36
|
+
appr = @apprenticeships[apprenticeship_id]
|
|
37
|
+
return nil unless appr
|
|
38
|
+
|
|
39
|
+
appr.recommended_method
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def graduated_apprenticeships
|
|
43
|
+
@apprenticeships.values.select(&:graduated?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def active_apprenticeships
|
|
47
|
+
@apprenticeships.values.reject(&:graduated?)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def by_mentor(mentor_id:)
|
|
51
|
+
@apprenticeships.values.select { |a| a.mentor_id == mentor_id }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def by_apprentice(apprentice_id:)
|
|
55
|
+
@apprenticeships.values.select { |a| a.apprentice_id == apprentice_id }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def by_domain(domain:)
|
|
59
|
+
@apprenticeships.values.select { |a| a.domain == domain }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def decay_all
|
|
63
|
+
@apprenticeships.each_value(&:decay!)
|
|
64
|
+
@apprenticeships.size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get(apprenticeship_id)
|
|
68
|
+
@apprenticeships[apprenticeship_id]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def count
|
|
72
|
+
@apprenticeships.size
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sessions
|
|
76
|
+
@sessions.dup
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_h
|
|
80
|
+
{
|
|
81
|
+
total: @apprenticeships.size,
|
|
82
|
+
active: active_apprenticeships.size,
|
|
83
|
+
graduated: graduated_apprenticeships.size,
|
|
84
|
+
sessions: @sessions.size
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def record_session(apprenticeship_id:, method:, success:)
|
|
91
|
+
return if @sessions.size >= ApprenticeshipModel::MAX_SESSIONS
|
|
92
|
+
|
|
93
|
+
@sessions << {
|
|
94
|
+
apprenticeship_id: apprenticeship_id,
|
|
95
|
+
method: method,
|
|
96
|
+
success: success,
|
|
97
|
+
recorded_at: Time.now.utc
|
|
98
|
+
}
|
|
99
|
+
@sessions.shift while @sessions.size > ApprenticeshipModel::MAX_HISTORY
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module CognitiveApprenticeship
|
|
8
|
+
module Helpers
|
|
9
|
+
module ApprenticeshipModel
|
|
10
|
+
# Capacity limits
|
|
11
|
+
MAX_APPRENTICESHIPS = 100
|
|
12
|
+
MAX_SESSIONS = 500
|
|
13
|
+
MAX_HISTORY = 300
|
|
14
|
+
|
|
15
|
+
# Mastery defaults and bounds
|
|
16
|
+
DEFAULT_MASTERY = 0.1
|
|
17
|
+
MASTERY_FLOOR = 0.0
|
|
18
|
+
MASTERY_CEILING = 1.0
|
|
19
|
+
MASTERY_THRESHOLD = 0.85
|
|
20
|
+
PHASE_THRESHOLD = 0.5
|
|
21
|
+
|
|
22
|
+
# Learning dynamics
|
|
23
|
+
LEARNING_GAIN = 0.08
|
|
24
|
+
COACHING_MULTIPLIER = 1.5
|
|
25
|
+
EXPLORATION_MULTIPLIER = 2.0
|
|
26
|
+
DECAY_RATE = 0.01
|
|
27
|
+
|
|
28
|
+
# Collins' six instructional methods
|
|
29
|
+
METHODS = %i[modeling coaching scaffolding articulation reflection exploration].freeze
|
|
30
|
+
|
|
31
|
+
# Phase labels derived from mastery range
|
|
32
|
+
PHASE_LABELS = {
|
|
33
|
+
(0.0...0.2) => :modeling,
|
|
34
|
+
(0.2...0.4) => :coaching,
|
|
35
|
+
(0.4...0.6) => :scaffolding,
|
|
36
|
+
(0.6...0.75) => :articulation,
|
|
37
|
+
(0.75...0.85) => :reflection,
|
|
38
|
+
(0.85..1.0) => :exploration
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Mastery level labels
|
|
42
|
+
MASTERY_LABELS = {
|
|
43
|
+
(0.8..) => :expert,
|
|
44
|
+
(0.6...0.8) => :proficient,
|
|
45
|
+
(0.4...0.6) => :intermediate,
|
|
46
|
+
(0.2...0.4) => :apprentice,
|
|
47
|
+
(..0.2) => :novice
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
def phase_for(mastery)
|
|
53
|
+
PHASE_LABELS.each do |range, phase|
|
|
54
|
+
return phase if range.cover?(mastery)
|
|
55
|
+
end
|
|
56
|
+
:exploration
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mastery_label_for(mastery)
|
|
60
|
+
MASTERY_LABELS.each do |range, label|
|
|
61
|
+
return label if range.cover?(mastery)
|
|
62
|
+
end
|
|
63
|
+
:expert
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clamp_mastery(value)
|
|
67
|
+
value.clamp(MASTERY_FLOOR, MASTERY_CEILING)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def new_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:)
|
|
71
|
+
now = Time.now.utc
|
|
72
|
+
{
|
|
73
|
+
id: SecureRandom.uuid,
|
|
74
|
+
skill_name: skill_name,
|
|
75
|
+
domain: domain,
|
|
76
|
+
mentor_id: mentor_id,
|
|
77
|
+
apprentice_id: apprentice_id,
|
|
78
|
+
mastery: DEFAULT_MASTERY,
|
|
79
|
+
current_phase: phase_for(DEFAULT_MASTERY),
|
|
80
|
+
session_count: 0,
|
|
81
|
+
created_at: now,
|
|
82
|
+
last_session_at: nil
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveApprenticeship
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveApprenticeship
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def create_apprenticeship(skill_name:, domain:, mentor_id:, apprentice_id:, **)
|
|
12
|
+
return { success: false, reason: :param_too_short } if [skill_name, domain, mentor_id, apprentice_id].any? { |p| p.to_s.length < 3 }
|
|
13
|
+
|
|
14
|
+
appr = engine.create_apprenticeship(
|
|
15
|
+
skill_name: skill_name,
|
|
16
|
+
domain: domain,
|
|
17
|
+
mentor_id: mentor_id,
|
|
18
|
+
apprentice_id: apprentice_id
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if appr
|
|
22
|
+
Legion::Logging.info "[cognitive_apprenticeship] created id=#{appr.id} skill=#{skill_name} domain=#{domain}"
|
|
23
|
+
{ success: true, apprenticeship: appr.to_h }
|
|
24
|
+
else
|
|
25
|
+
Legion::Logging.warn '[cognitive_apprenticeship] create failed: capacity reached'
|
|
26
|
+
{ success: false, reason: :capacity_reached }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def conduct_apprenticeship_session(apprenticeship_id:, method:, success:, **)
|
|
31
|
+
return { success: false, reason: :invalid_method } unless Helpers::ApprenticeshipModel::METHODS.include?(method.to_sym)
|
|
32
|
+
|
|
33
|
+
appr = engine.conduct_session(
|
|
34
|
+
apprenticeship_id: apprenticeship_id,
|
|
35
|
+
method: method.to_sym,
|
|
36
|
+
success: success
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if appr
|
|
40
|
+
Legion::Logging.debug "[cognitive_apprenticeship] session id=#{apprenticeship_id} method=#{method} " \
|
|
41
|
+
"success=#{success} mastery=#{appr.mastery.round(3)}"
|
|
42
|
+
{ success: true, apprenticeship: appr.to_h }
|
|
43
|
+
else
|
|
44
|
+
{ success: false, reason: :not_found }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def recommend_apprenticeship_method(apprenticeship_id:, **)
|
|
49
|
+
method = engine.recommend_method(apprenticeship_id: apprenticeship_id)
|
|
50
|
+
|
|
51
|
+
if method
|
|
52
|
+
Legion::Logging.debug "[cognitive_apprenticeship] recommend id=#{apprenticeship_id} method=#{method}"
|
|
53
|
+
{ success: true, apprenticeship_id: apprenticeship_id, recommended_method: method }
|
|
54
|
+
else
|
|
55
|
+
{ success: false, reason: :not_found }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def graduated_apprenticeships(**)
|
|
60
|
+
list = engine.graduated_apprenticeships
|
|
61
|
+
Legion::Logging.debug "[cognitive_apprenticeship] graduated count=#{list.size}"
|
|
62
|
+
{ success: true, apprenticeships: list.map(&:to_h), count: list.size }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def active_apprenticeships(**)
|
|
66
|
+
list = engine.active_apprenticeships
|
|
67
|
+
Legion::Logging.debug "[cognitive_apprenticeship] active count=#{list.size}"
|
|
68
|
+
{ success: true, apprenticeships: list.map(&:to_h), count: list.size }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def mentor_apprenticeships(mentor_id:, **)
|
|
72
|
+
list = engine.by_mentor(mentor_id: mentor_id)
|
|
73
|
+
Legion::Logging.debug "[cognitive_apprenticeship] mentor=#{mentor_id} count=#{list.size}"
|
|
74
|
+
{ success: true, mentor_id: mentor_id, apprenticeships: list.map(&:to_h), count: list.size }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def apprentice_apprenticeships(apprentice_id:, **)
|
|
78
|
+
list = engine.by_apprentice(apprentice_id: apprentice_id)
|
|
79
|
+
Legion::Logging.debug "[cognitive_apprenticeship] apprentice=#{apprentice_id} count=#{list.size}"
|
|
80
|
+
{ success: true, apprentice_id: apprentice_id, apprenticeships: list.map(&:to_h), count: list.size }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def domain_apprenticeships(domain:, **)
|
|
84
|
+
list = engine.by_domain(domain: domain)
|
|
85
|
+
Legion::Logging.debug "[cognitive_apprenticeship] domain=#{domain} count=#{list.size}"
|
|
86
|
+
{ success: true, domain: domain, apprenticeships: list.map(&:to_h), count: list.size }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_cognitive_apprenticeship(apprenticeship_id:, method:, success:, **)
|
|
90
|
+
conduct_apprenticeship_session(apprenticeship_id: apprenticeship_id, method: method, success: success)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cognitive_apprenticeship_stats(**)
|
|
94
|
+
stats = engine.to_h
|
|
95
|
+
Legion::Logging.debug "[cognitive_apprenticeship] stats=#{stats.inspect}"
|
|
96
|
+
{ success: true }.merge(stats)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def engine
|
|
102
|
+
@engine ||= Helpers::ApprenticeshipEngine.new
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_apprenticeship/version'
|
|
4
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model'
|
|
5
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship'
|
|
6
|
+
require 'legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine'
|
|
7
|
+
require 'legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module CognitiveApprenticeship
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_apprenticeship/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:create_apprenticeship)
|
|
10
|
+
expect(client).to respond_to(:conduct_apprenticeship_session)
|
|
11
|
+
expect(client).to respond_to(:recommend_apprenticeship_method)
|
|
12
|
+
expect(client).to respond_to(:graduated_apprenticeships)
|
|
13
|
+
expect(client).to respond_to(:active_apprenticeships)
|
|
14
|
+
expect(client).to respond_to(:mentor_apprenticeships)
|
|
15
|
+
expect(client).to respond_to(:apprentice_apprenticeships)
|
|
16
|
+
expect(client).to respond_to(:domain_apprenticeships)
|
|
17
|
+
expect(client).to respond_to(:update_cognitive_apprenticeship)
|
|
18
|
+
expect(client).to respond_to(:cognitive_apprenticeship_stats)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipEngine do
|
|
4
|
+
let(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:params) do
|
|
7
|
+
{
|
|
8
|
+
skill_name: 'terraform',
|
|
9
|
+
domain: 'infrastructure',
|
|
10
|
+
mentor_id: 'mentor-1',
|
|
11
|
+
apprentice_id: 'learner-1'
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_one
|
|
16
|
+
engine.create_apprenticeship(**params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '#create_apprenticeship' do
|
|
20
|
+
it 'returns a new Apprenticeship' do
|
|
21
|
+
appr = create_one
|
|
22
|
+
expect(appr).to be_a(Legion::Extensions::CognitiveApprenticeship::Helpers::Apprenticeship)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'stores the apprenticeship' do
|
|
26
|
+
appr = create_one
|
|
27
|
+
expect(engine.get(appr.id)).to eq(appr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'increments count' do
|
|
31
|
+
expect { create_one }.to change(engine, :count).by(1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns nil at capacity' do
|
|
35
|
+
stub_const('Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::MAX_APPRENTICESHIPS', 1)
|
|
36
|
+
create_one
|
|
37
|
+
expect(engine.create_apprenticeship(**params)).to be_nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#conduct_session' do
|
|
42
|
+
it 'returns the apprenticeship after learning' do
|
|
43
|
+
appr = create_one
|
|
44
|
+
result = engine.conduct_session(apprenticeship_id: appr.id, method: :modeling, success: true)
|
|
45
|
+
expect(result).to eq(appr)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns nil for unknown id' do
|
|
49
|
+
expect(engine.conduct_session(apprenticeship_id: 'nope', method: :modeling, success: true)).to be_nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'records the session' do
|
|
53
|
+
appr = create_one
|
|
54
|
+
engine.conduct_session(apprenticeship_id: appr.id, method: :coaching, success: true)
|
|
55
|
+
expect(engine.sessions.size).to eq(1)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#recommend_method' do
|
|
60
|
+
it 'returns the recommended method' do
|
|
61
|
+
appr = create_one
|
|
62
|
+
method = engine.recommend_method(apprenticeship_id: appr.id)
|
|
63
|
+
expect(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS).to include(method)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns nil for unknown id' do
|
|
67
|
+
expect(engine.recommend_method(apprenticeship_id: 'nope')).to be_nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#graduated_apprenticeships' do
|
|
72
|
+
it 'returns apprenticeships that have graduated' do
|
|
73
|
+
appr = create_one
|
|
74
|
+
30.times { engine.conduct_session(apprenticeship_id: appr.id, method: :exploration, success: true) }
|
|
75
|
+
expect(engine.graduated_apprenticeships).to include(appr)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'excludes non-graduated apprenticeships' do
|
|
79
|
+
create_one
|
|
80
|
+
expect(engine.graduated_apprenticeships).to be_empty
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '#active_apprenticeships' do
|
|
85
|
+
it 'returns non-graduated apprenticeships' do
|
|
86
|
+
create_one
|
|
87
|
+
expect(engine.active_apprenticeships.size).to eq(1)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'excludes graduated apprenticeships' do
|
|
91
|
+
appr = create_one
|
|
92
|
+
30.times { engine.conduct_session(apprenticeship_id: appr.id, method: :exploration, success: true) }
|
|
93
|
+
expect(engine.active_apprenticeships).to be_empty
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '#by_mentor' do
|
|
98
|
+
it 'returns apprenticeships for the given mentor' do
|
|
99
|
+
create_one
|
|
100
|
+
result = engine.by_mentor(mentor_id: 'mentor-1')
|
|
101
|
+
expect(result.size).to eq(1)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns empty for unknown mentor' do
|
|
105
|
+
create_one
|
|
106
|
+
expect(engine.by_mentor(mentor_id: 'nobody')).to be_empty
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '#by_apprentice' do
|
|
111
|
+
it 'returns apprenticeships for the given apprentice' do
|
|
112
|
+
create_one
|
|
113
|
+
result = engine.by_apprentice(apprentice_id: 'learner-1')
|
|
114
|
+
expect(result.size).to eq(1)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#by_domain' do
|
|
119
|
+
it 'returns apprenticeships in the given domain' do
|
|
120
|
+
create_one
|
|
121
|
+
result = engine.by_domain(domain: 'infrastructure')
|
|
122
|
+
expect(result.size).to eq(1)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'returns empty for unknown domain' do
|
|
126
|
+
create_one
|
|
127
|
+
expect(engine.by_domain(domain: 'unknown')).to be_empty
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '#decay_all' do
|
|
132
|
+
it 'applies decay to all apprenticeships and returns count' do
|
|
133
|
+
create_one
|
|
134
|
+
expect(engine.decay_all).to eq(1)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe '#to_h' do
|
|
139
|
+
it 'returns a summary hash' do
|
|
140
|
+
create_one
|
|
141
|
+
h = engine.to_h
|
|
142
|
+
expect(h).to include(:total, :active, :graduated, :sessions)
|
|
143
|
+
expect(h[:total]).to eq(1)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel do
|
|
4
|
+
subject(:model) { described_class }
|
|
5
|
+
|
|
6
|
+
describe 'constants' do
|
|
7
|
+
it 'defines METHODS with six items' do
|
|
8
|
+
expect(model::METHODS.size).to eq(6)
|
|
9
|
+
expect(model::METHODS).to include(:modeling, :coaching, :scaffolding, :articulation, :reflection, :exploration)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines MASTERY_THRESHOLD as 0.85' do
|
|
13
|
+
expect(model::MASTERY_THRESHOLD).to eq(0.85)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines LEARNING_GAIN as 0.08' do
|
|
17
|
+
expect(model::LEARNING_GAIN).to eq(0.08)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defines EXPLORATION_MULTIPLIER as 2.0' do
|
|
21
|
+
expect(model::EXPLORATION_MULTIPLIER).to eq(2.0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defines COACHING_MULTIPLIER as 1.5' do
|
|
25
|
+
expect(model::COACHING_MULTIPLIER).to eq(1.5)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines DECAY_RATE as 0.01' do
|
|
29
|
+
expect(model::DECAY_RATE).to eq(0.01)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '.phase_for' do
|
|
34
|
+
it 'returns :modeling for mastery 0.0..0.19' do
|
|
35
|
+
expect(model.phase_for(0.0)).to eq(:modeling)
|
|
36
|
+
expect(model.phase_for(0.1)).to eq(:modeling)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns :coaching for mastery 0.2..0.39' do
|
|
40
|
+
expect(model.phase_for(0.2)).to eq(:coaching)
|
|
41
|
+
expect(model.phase_for(0.3)).to eq(:coaching)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns :scaffolding for mastery 0.4..0.59' do
|
|
45
|
+
expect(model.phase_for(0.4)).to eq(:scaffolding)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns :articulation for mastery 0.6..0.74' do
|
|
49
|
+
expect(model.phase_for(0.6)).to eq(:articulation)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns :reflection for mastery 0.75..0.84' do
|
|
53
|
+
expect(model.phase_for(0.75)).to eq(:reflection)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'returns :exploration for mastery >= 0.85' do
|
|
57
|
+
expect(model.phase_for(0.85)).to eq(:exploration)
|
|
58
|
+
expect(model.phase_for(1.0)).to eq(:exploration)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '.mastery_label_for' do
|
|
63
|
+
it 'returns :novice for mastery < 0.2' do
|
|
64
|
+
expect(model.mastery_label_for(0.1)).to eq(:novice)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'returns :apprentice for mastery 0.2..0.39' do
|
|
68
|
+
expect(model.mastery_label_for(0.3)).to eq(:apprentice)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns :intermediate for mastery 0.4..0.59' do
|
|
72
|
+
expect(model.mastery_label_for(0.5)).to eq(:intermediate)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns :proficient for mastery 0.6..0.79' do
|
|
76
|
+
expect(model.mastery_label_for(0.7)).to eq(:proficient)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns :expert for mastery >= 0.8' do
|
|
80
|
+
expect(model.mastery_label_for(0.9)).to eq(:expert)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '.clamp_mastery' do
|
|
85
|
+
it 'floors at MASTERY_FLOOR' do
|
|
86
|
+
expect(model.clamp_mastery(-0.5)).to eq(0.0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'caps at MASTERY_CEILING' do
|
|
90
|
+
expect(model.clamp_mastery(1.5)).to eq(1.0)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'passes through valid values' do
|
|
94
|
+
expect(model.clamp_mastery(0.5)).to eq(0.5)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '.new_apprenticeship' do
|
|
99
|
+
let(:entry) do
|
|
100
|
+
model.new_apprenticeship(
|
|
101
|
+
skill_name: 'ruby',
|
|
102
|
+
domain: 'programming',
|
|
103
|
+
mentor_id: 'mentor-1',
|
|
104
|
+
apprentice_id: 'apprentice-1'
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'sets default mastery' do
|
|
109
|
+
expect(entry[:mastery]).to eq(model::DEFAULT_MASTERY)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'sets current_phase from mastery' do
|
|
113
|
+
expect(entry[:current_phase]).to eq(model.phase_for(model::DEFAULT_MASTERY))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'generates a uuid id' do
|
|
117
|
+
expect(entry[:id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'sets session_count to 0' do
|
|
121
|
+
expect(entry[:session_count]).to eq(0)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship::Helpers::Apprenticeship do
|
|
4
|
+
let(:appr) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
skill_name: 'ruby',
|
|
7
|
+
domain: 'programming',
|
|
8
|
+
mentor_id: 'mentor-1',
|
|
9
|
+
apprentice_id: 'apprentice-1'
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#initialize' do
|
|
14
|
+
it 'sets skill_name' do
|
|
15
|
+
expect(appr.skill_name).to eq('ruby')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'sets domain' do
|
|
19
|
+
expect(appr.domain).to eq('programming')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'starts at DEFAULT_MASTERY' do
|
|
23
|
+
expect(appr.mastery).to eq(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::DEFAULT_MASTERY)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'starts with zero sessions' do
|
|
27
|
+
expect(appr.session_count).to eq(0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'generates a uuid' do
|
|
31
|
+
expect(appr.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#current_phase' do
|
|
36
|
+
it 'returns :modeling at default mastery' do
|
|
37
|
+
expect(appr.current_phase).to eq(:modeling)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#mastery_label' do
|
|
42
|
+
it 'returns :novice at default mastery' do
|
|
43
|
+
expect(appr.mastery_label).to eq(:novice)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#graduated?' do
|
|
48
|
+
it 'returns false below threshold' do
|
|
49
|
+
expect(appr.graduated?).to be false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns true at threshold' do
|
|
53
|
+
30.times { appr.learn!(method: :exploration, success: true) }
|
|
54
|
+
expect(appr.graduated?).to be true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#recommended_method' do
|
|
59
|
+
it 'returns :modeling at default mastery' do
|
|
60
|
+
expect(appr.recommended_method).to eq(:modeling)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#learn!' do
|
|
65
|
+
it 'increases mastery on success' do
|
|
66
|
+
before = appr.mastery
|
|
67
|
+
appr.learn!(method: :modeling, success: true)
|
|
68
|
+
expect(appr.mastery).to be > before
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'does not increase mastery on failure' do
|
|
72
|
+
before = appr.mastery
|
|
73
|
+
appr.learn!(method: :modeling, success: false)
|
|
74
|
+
expect(appr.mastery).to eq(before)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'applies exploration multiplier' do
|
|
78
|
+
a1 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
|
|
79
|
+
a2 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
|
|
80
|
+
a1.learn!(method: :modeling, success: true)
|
|
81
|
+
a2.learn!(method: :exploration, success: true)
|
|
82
|
+
expect(a2.mastery).to be > a1.mastery
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'applies coaching multiplier' do
|
|
86
|
+
a1 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
|
|
87
|
+
a2 = described_class.new(skill_name: 'skill', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
|
|
88
|
+
a1.learn!(method: :modeling, success: true)
|
|
89
|
+
a2.learn!(method: :coaching, success: true)
|
|
90
|
+
expect(a2.mastery).to be > a1.mastery
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'increments session_count' do
|
|
94
|
+
appr.learn!(method: :scaffolding, success: true)
|
|
95
|
+
expect(appr.session_count).to eq(1)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'clamps mastery at ceiling' do
|
|
99
|
+
100.times { appr.learn!(method: :exploration, success: true) }
|
|
100
|
+
expect(appr.mastery).to be <= 1.0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'updates last_session_at' do
|
|
104
|
+
appr.learn!(method: :reflection, success: true)
|
|
105
|
+
expect(appr.last_session_at).not_to be_nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#decay!' do
|
|
110
|
+
it 'reduces mastery' do
|
|
111
|
+
appr.learn!(method: :exploration, success: true)
|
|
112
|
+
before = appr.mastery
|
|
113
|
+
appr.decay!
|
|
114
|
+
expect(appr.mastery).to be < before
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'clamps at floor' do
|
|
118
|
+
100.times { appr.decay! }
|
|
119
|
+
expect(appr.mastery).to be >= 0.0
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#to_h' do
|
|
124
|
+
it 'includes all expected keys' do
|
|
125
|
+
h = appr.to_h
|
|
126
|
+
expect(h).to include(:id, :skill_name, :domain, :mentor_id, :apprentice_id,
|
|
127
|
+
:mastery, :current_phase, :mastery_label, :graduated,
|
|
128
|
+
:session_count, :created_at, :last_session_at)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'reflects graduated status' do
|
|
132
|
+
30.times { appr.learn!(method: :exploration, success: true) }
|
|
133
|
+
expect(appr.to_h[:graduated]).to be true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_apprenticeship/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship::Runners::CognitiveApprenticeship do
|
|
6
|
+
let(:client) { Legion::Extensions::CognitiveApprenticeship::Client.new }
|
|
7
|
+
|
|
8
|
+
let(:valid_params) do
|
|
9
|
+
{
|
|
10
|
+
skill_name: 'ruby',
|
|
11
|
+
domain: 'programming',
|
|
12
|
+
mentor_id: 'mentor-1',
|
|
13
|
+
apprentice_id: 'learner-1'
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_one
|
|
18
|
+
client.create_apprenticeship(**valid_params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '#create_apprenticeship' do
|
|
22
|
+
it 'returns success: true with a valid apprenticeship hash' do
|
|
23
|
+
result = create_one
|
|
24
|
+
expect(result[:success]).to be true
|
|
25
|
+
expect(result[:apprenticeship]).to include(:id, :skill_name, :mastery)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns success: false when a param is too short' do
|
|
29
|
+
result = client.create_apprenticeship(skill_name: 'ab', domain: 'dom', mentor_id: 'mtr', apprentice_id: 'app')
|
|
30
|
+
expect(result[:success]).to be false
|
|
31
|
+
expect(result[:reason]).to eq(:param_too_short)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#conduct_apprenticeship_session' do
|
|
36
|
+
it 'returns success: true and updated mastery' do
|
|
37
|
+
created = create_one
|
|
38
|
+
id = created[:apprenticeship][:id]
|
|
39
|
+
result = client.conduct_apprenticeship_session(apprenticeship_id: id, method: :modeling, success: true)
|
|
40
|
+
expect(result[:success]).to be true
|
|
41
|
+
expect(result[:apprenticeship][:mastery]).to be > 0.1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns success: false for unknown apprenticeship' do
|
|
45
|
+
result = client.conduct_apprenticeship_session(apprenticeship_id: 'nope', method: :modeling, success: true)
|
|
46
|
+
expect(result[:success]).to be false
|
|
47
|
+
expect(result[:reason]).to eq(:not_found)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns success: false for invalid method' do
|
|
51
|
+
created = create_one
|
|
52
|
+
id = created[:apprenticeship][:id]
|
|
53
|
+
result = client.conduct_apprenticeship_session(apprenticeship_id: id, method: :unknown, success: true)
|
|
54
|
+
expect(result[:success]).to be false
|
|
55
|
+
expect(result[:reason]).to eq(:invalid_method)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'accepts all six valid methods' do
|
|
59
|
+
Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS.each do |method|
|
|
60
|
+
c = Legion::Extensions::CognitiveApprenticeship::Client.new
|
|
61
|
+
created = c.create_apprenticeship(**valid_params)
|
|
62
|
+
id = created[:apprenticeship][:id]
|
|
63
|
+
result = c.conduct_apprenticeship_session(apprenticeship_id: id, method: method, success: true)
|
|
64
|
+
expect(result[:success]).to be true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#recommend_apprenticeship_method' do
|
|
70
|
+
it 'returns success: true with a valid method' do
|
|
71
|
+
created = create_one
|
|
72
|
+
id = created[:apprenticeship][:id]
|
|
73
|
+
result = client.recommend_apprenticeship_method(apprenticeship_id: id)
|
|
74
|
+
expect(result[:success]).to be true
|
|
75
|
+
expect(Legion::Extensions::CognitiveApprenticeship::Helpers::ApprenticeshipModel::METHODS).to include(result[:recommended_method])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns success: false for unknown apprenticeship' do
|
|
79
|
+
result = client.recommend_apprenticeship_method(apprenticeship_id: 'nope')
|
|
80
|
+
expect(result[:success]).to be false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '#graduated_apprenticeships' do
|
|
85
|
+
it 'returns success: true and an empty list initially' do
|
|
86
|
+
result = client.graduated_apprenticeships
|
|
87
|
+
expect(result[:success]).to be true
|
|
88
|
+
expect(result[:count]).to eq(0)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'includes graduated apprenticeships' do
|
|
92
|
+
created = create_one
|
|
93
|
+
id = created[:apprenticeship][:id]
|
|
94
|
+
30.times { client.conduct_apprenticeship_session(apprenticeship_id: id, method: :exploration, success: true) }
|
|
95
|
+
result = client.graduated_apprenticeships
|
|
96
|
+
expect(result[:count]).to eq(1)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#active_apprenticeships' do
|
|
101
|
+
it 'returns the newly created apprenticeship' do
|
|
102
|
+
create_one
|
|
103
|
+
result = client.active_apprenticeships
|
|
104
|
+
expect(result[:success]).to be true
|
|
105
|
+
expect(result[:count]).to eq(1)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#mentor_apprenticeships' do
|
|
110
|
+
it 'returns apprenticeships for the mentor' do
|
|
111
|
+
create_one
|
|
112
|
+
result = client.mentor_apprenticeships(mentor_id: 'mentor-1')
|
|
113
|
+
expect(result[:success]).to be true
|
|
114
|
+
expect(result[:count]).to eq(1)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#apprentice_apprenticeships' do
|
|
119
|
+
it 'returns apprenticeships for the apprentice' do
|
|
120
|
+
create_one
|
|
121
|
+
result = client.apprentice_apprenticeships(apprentice_id: 'learner-1')
|
|
122
|
+
expect(result[:success]).to be true
|
|
123
|
+
expect(result[:count]).to eq(1)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#domain_apprenticeships' do
|
|
128
|
+
it 'returns apprenticeships for the domain' do
|
|
129
|
+
create_one
|
|
130
|
+
result = client.domain_apprenticeships(domain: 'programming')
|
|
131
|
+
expect(result[:success]).to be true
|
|
132
|
+
expect(result[:count]).to eq(1)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe '#update_cognitive_apprenticeship' do
|
|
137
|
+
it 'delegates to conduct_apprenticeship_session' do
|
|
138
|
+
created = create_one
|
|
139
|
+
id = created[:apprenticeship][:id]
|
|
140
|
+
result = client.update_cognitive_apprenticeship(apprenticeship_id: id, method: :coaching, success: true)
|
|
141
|
+
expect(result[:success]).to be true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe '#cognitive_apprenticeship_stats' do
|
|
146
|
+
it 'returns success: true with stats' do
|
|
147
|
+
create_one
|
|
148
|
+
result = client.cognitive_apprenticeship_stats
|
|
149
|
+
expect(result[:success]).to be true
|
|
150
|
+
expect(result[:total]).to eq(1)
|
|
151
|
+
expect(result).to include(:active, :graduated, :sessions)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveApprenticeship do
|
|
4
|
+
it 'has a version number' do
|
|
5
|
+
expect(Legion::Extensions::CognitiveApprenticeship::VERSION).not_to be_nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'has a version that is a string' do
|
|
9
|
+
expect(Legion::Extensions::CognitiveApprenticeship::VERSION).to be_a(String)
|
|
10
|
+
end
|
|
11
|
+
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/cognitive_apprenticeship'
|
|
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-cognitive-apprenticeship
|
|
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: 'Collins'' Cognitive Apprenticeship model for brain-modeled agentic AI:
|
|
27
|
+
guided learning through modeling, coaching, scaffolding, articulation, reflection,
|
|
28
|
+
and exploration'
|
|
29
|
+
email:
|
|
30
|
+
- matthewdiverson@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- Gemfile
|
|
36
|
+
- lex-cognitive-apprenticeship.gemspec
|
|
37
|
+
- lib/legion/extensions/cognitive_apprenticeship.rb
|
|
38
|
+
- lib/legion/extensions/cognitive_apprenticeship/client.rb
|
|
39
|
+
- lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship.rb
|
|
40
|
+
- lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine.rb
|
|
41
|
+
- lib/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model.rb
|
|
42
|
+
- lib/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship.rb
|
|
43
|
+
- lib/legion/extensions/cognitive_apprenticeship/version.rb
|
|
44
|
+
- spec/legion/extensions/cognitive_apprenticeship/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_engine_spec.rb
|
|
46
|
+
- spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_model_spec.rb
|
|
47
|
+
- spec/legion/extensions/cognitive_apprenticeship/helpers/apprenticeship_spec.rb
|
|
48
|
+
- spec/legion/extensions/cognitive_apprenticeship/runners/cognitive_apprenticeship_spec.rb
|
|
49
|
+
- spec/legion/extensions/cognitive_apprenticeship_spec.rb
|
|
50
|
+
- spec/spec_helper.rb
|
|
51
|
+
homepage: https://github.com/LegionIO/lex-cognitive-apprenticeship
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata:
|
|
55
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
|
|
56
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
|
|
57
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
|
|
58
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship
|
|
59
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-apprenticeship/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 Cognitive Apprenticeship
|
|
78
|
+
test_files: []
|