175 lines
4.5 KiB
Go
175 lines
4.5 KiB
Go
|
|
package learnermemory
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"tutor/internal/workflows"
|
||
|
|
)
|
||
|
|
|
||
|
|
var ErrProfileNotFound = errors.New("learner profile not found")
|
||
|
|
|
||
|
|
type Store interface {
|
||
|
|
UpsertProfile(Profile) (Profile, error)
|
||
|
|
GetProfile(string) (Profile, error)
|
||
|
|
UpsertMastery(ConceptMastery) error
|
||
|
|
AddMisconception(Misconception) error
|
||
|
|
AddIntervention(Intervention) error
|
||
|
|
AddReviewSchedule(ReviewSchedule) error
|
||
|
|
Snapshot(string) (Snapshot, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type MemoryStore struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
profiles map[string]Profile
|
||
|
|
mastery map[string]map[string]ConceptMastery
|
||
|
|
misconceptions map[string][]Misconception
|
||
|
|
interventions map[string][]Intervention
|
||
|
|
reviewSchedules map[string][]ReviewSchedule
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewMemoryStore() *MemoryStore {
|
||
|
|
return &MemoryStore{
|
||
|
|
profiles: make(map[string]Profile),
|
||
|
|
mastery: make(map[string]map[string]ConceptMastery),
|
||
|
|
misconceptions: make(map[string][]Misconception),
|
||
|
|
interventions: make(map[string][]Intervention),
|
||
|
|
reviewSchedules: make(map[string][]ReviewSchedule),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) UpsertProfile(profile Profile) (Profile, error) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
s.profiles[profile.UserID] = cloneProfile(profile)
|
||
|
|
return profile, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) GetProfile(userID string) (Profile, error) {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
|
||
|
|
profile, ok := s.profiles[userID]
|
||
|
|
if !ok {
|
||
|
|
return Profile{}, ErrProfileNotFound
|
||
|
|
}
|
||
|
|
return cloneProfile(profile), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) UpsertMastery(mastery ConceptMastery) error {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
if _, ok := s.mastery[mastery.UserID]; !ok {
|
||
|
|
s.mastery[mastery.UserID] = make(map[string]ConceptMastery)
|
||
|
|
}
|
||
|
|
s.mastery[mastery.UserID][mastery.Concept.ID] = cloneMastery(mastery)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) AddMisconception(misconception Misconception) error {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
s.misconceptions[misconception.UserID] = append(
|
||
|
|
s.misconceptions[misconception.UserID],
|
||
|
|
cloneMisconception(misconception),
|
||
|
|
)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) AddIntervention(intervention Intervention) error {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
s.interventions[intervention.UserID] = append(
|
||
|
|
s.interventions[intervention.UserID],
|
||
|
|
cloneIntervention(intervention),
|
||
|
|
)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) AddReviewSchedule(schedule ReviewSchedule) error {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
s.reviewSchedules[schedule.UserID] = append(
|
||
|
|
s.reviewSchedules[schedule.UserID],
|
||
|
|
cloneReviewSchedule(schedule),
|
||
|
|
)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MemoryStore) Snapshot(userID string) (Snapshot, error) {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
|
||
|
|
profile, ok := s.profiles[userID]
|
||
|
|
if !ok {
|
||
|
|
return Snapshot{}, ErrProfileNotFound
|
||
|
|
}
|
||
|
|
|
||
|
|
snapshot := Snapshot{
|
||
|
|
Profile: cloneProfile(profile),
|
||
|
|
Mastery: make([]ConceptMastery, 0, len(s.mastery[userID])),
|
||
|
|
Misconceptions: cloneMisconceptions(s.misconceptions[userID]),
|
||
|
|
Interventions: cloneInterventions(s.interventions[userID]),
|
||
|
|
ReviewSchedule: cloneReviewSchedules(s.reviewSchedules[userID]),
|
||
|
|
}
|
||
|
|
for _, mastery := range s.mastery[userID] {
|
||
|
|
snapshot.Mastery = append(snapshot.Mastery, cloneMastery(mastery))
|
||
|
|
}
|
||
|
|
return snapshot, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneProfile(profile Profile) Profile {
|
||
|
|
profile.Stack = append([]string(nil), profile.Stack...)
|
||
|
|
profile.Preferences = append([]string(nil), profile.Preferences...)
|
||
|
|
return profile
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneMastery(mastery ConceptMastery) ConceptMastery {
|
||
|
|
mastery.Evidence = append([]workflows.EvidenceRef(nil), mastery.Evidence...)
|
||
|
|
return mastery
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneMisconception(misconception Misconception) Misconception {
|
||
|
|
misconception.Evidence = append([]workflows.EvidenceRef(nil), misconception.Evidence...)
|
||
|
|
return misconception
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneIntervention(intervention Intervention) Intervention {
|
||
|
|
intervention.Evidence = append([]workflows.EvidenceRef(nil), intervention.Evidence...)
|
||
|
|
return intervention
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneReviewSchedule(schedule ReviewSchedule) ReviewSchedule {
|
||
|
|
schedule.Evidence = append([]workflows.EvidenceRef(nil), schedule.Evidence...)
|
||
|
|
return schedule
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneMisconceptions(items []Misconception) []Misconception {
|
||
|
|
cloned := make([]Misconception, len(items))
|
||
|
|
for i, item := range items {
|
||
|
|
cloned[i] = cloneMisconception(item)
|
||
|
|
}
|
||
|
|
return cloned
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneInterventions(items []Intervention) []Intervention {
|
||
|
|
cloned := make([]Intervention, len(items))
|
||
|
|
for i, item := range items {
|
||
|
|
cloned[i] = cloneIntervention(item)
|
||
|
|
}
|
||
|
|
return cloned
|
||
|
|
}
|
||
|
|
|
||
|
|
func cloneReviewSchedules(items []ReviewSchedule) []ReviewSchedule {
|
||
|
|
cloned := make([]ReviewSchedule, len(items))
|
||
|
|
for i, item := range items {
|
||
|
|
cloned[i] = cloneReviewSchedule(item)
|
||
|
|
}
|
||
|
|
return cloned
|
||
|
|
}
|