feat: add learner memory ingestion
This commit is contained in:
113
internal/learnermemory/service.go
Normal file
113
internal/learnermemory/service.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package learnermemory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tutor/internal/workflows"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store Store
|
||||
ids atomic.Uint64
|
||||
}
|
||||
|
||||
func NewService(store Store) *Service {
|
||||
return &Service{store: store}
|
||||
}
|
||||
|
||||
func (s *Service) EnsureProfile(input ProfileInput) (Profile, error) {
|
||||
if strings.TrimSpace(input.UserID) == "" {
|
||||
return Profile{}, errors.New("user_id is required")
|
||||
}
|
||||
if strings.TrimSpace(input.TargetRole) == "" {
|
||||
return Profile{}, errors.New("target_role is required")
|
||||
}
|
||||
|
||||
profile := Profile{
|
||||
UserID: input.UserID,
|
||||
TargetRole: input.TargetRole,
|
||||
Stack: append([]string(nil), input.Stack...),
|
||||
InterviewTimeline: input.InterviewTimeline,
|
||||
Preferences: append([]string(nil), input.Preferences...),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
return s.store.UpsertProfile(profile)
|
||||
}
|
||||
|
||||
func (s *Service) ApplyCandidate(candidate workflows.MemoryUpdateCandidate) error {
|
||||
if strings.TrimSpace(candidate.UserID) == "" {
|
||||
return errors.New("candidate user_id is required")
|
||||
}
|
||||
|
||||
for _, update := range candidate.Updates {
|
||||
if len(update.Evidence) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(update.Concept.ID) == "" {
|
||||
continue
|
||||
}
|
||||
if err := s.applyUpdate(candidate.UserID, update); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Snapshot(userID string) (Snapshot, error) {
|
||||
if strings.TrimSpace(userID) == "" {
|
||||
return Snapshot{}, errors.New("user_id is required")
|
||||
}
|
||||
return s.store.Snapshot(userID)
|
||||
}
|
||||
|
||||
func (s *Service) applyUpdate(userID string, update workflows.MemoryUpdate) error {
|
||||
now := time.Now().UTC()
|
||||
switch update.Kind {
|
||||
case workflows.MemoryConceptMastery:
|
||||
return s.store.UpsertMastery(ConceptMastery{
|
||||
UserID: userID,
|
||||
Concept: update.Concept,
|
||||
State: update.ProposedState,
|
||||
Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...),
|
||||
UpdatedAt: now,
|
||||
})
|
||||
case workflows.MemoryMisconception:
|
||||
return s.store.AddMisconception(Misconception{
|
||||
ID: s.nextID("misconception"),
|
||||
UserID: userID,
|
||||
Concept: update.Concept,
|
||||
Label: update.Summary,
|
||||
Description: update.Summary,
|
||||
Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...),
|
||||
UpdatedAt: now,
|
||||
})
|
||||
case workflows.MemoryIntervention:
|
||||
return s.store.AddIntervention(Intervention{
|
||||
ID: s.nextID("intervention"),
|
||||
UserID: userID,
|
||||
Concept: update.Concept,
|
||||
Summary: update.Summary,
|
||||
Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...),
|
||||
UpdatedAt: now,
|
||||
})
|
||||
case workflows.MemoryReviewSchedule:
|
||||
return s.store.AddReviewSchedule(ReviewSchedule{
|
||||
ID: s.nextID("review"),
|
||||
UserID: userID,
|
||||
Concept: update.Concept,
|
||||
Reason: update.Summary,
|
||||
Evidence: append([]workflows.EvidenceRef(nil), update.Evidence...),
|
||||
UpdatedAt: now,
|
||||
})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) nextID(prefix string) string {
|
||||
return fmt.Sprintf("%s-%d", prefix, s.ids.Add(1))
|
||||
}
|
||||
Reference in New Issue
Block a user