139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
package interview
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"tutor/internal/learnermemory"
|
|
"tutor/internal/workflows"
|
|
)
|
|
|
|
var ErrQuestionNotFound = errors.New("diagnostic question not found")
|
|
|
|
type Service struct {
|
|
store Store
|
|
runner workflows.Runner
|
|
memory *learnermemory.Service
|
|
ids atomic.Uint64
|
|
}
|
|
|
|
func NewService(store Store, runner workflows.Runner, memory *learnermemory.Service) *Service {
|
|
return &Service{store: store, runner: runner, memory: memory}
|
|
}
|
|
|
|
func (s *Service) CreateSession(_ context.Context, input CreateSessionInput) (Session, error) {
|
|
if strings.TrimSpace(input.UserID) == "" {
|
|
return Session{}, errors.New("user_id is required")
|
|
}
|
|
if strings.TrimSpace(input.TargetRole) == "" {
|
|
return Session{}, errors.New("target_role is required")
|
|
}
|
|
if len(input.Stack) == 0 {
|
|
return Session{}, errors.New("stack is required")
|
|
}
|
|
|
|
session := Session{
|
|
ID: s.nextID("diag"),
|
|
UserID: input.UserID,
|
|
Track: BackendDeveloperTrack,
|
|
Status: SessionInProgress,
|
|
TargetRole: input.TargetRole,
|
|
Stack: append([]string(nil), input.Stack...),
|
|
InterviewTimeline: input.InterviewTimeline,
|
|
Questions: BackendDeveloperQuestions(),
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
if s.memory != nil {
|
|
if _, err := s.memory.EnsureProfile(learnermemory.ProfileInput{
|
|
UserID: session.UserID,
|
|
TargetRole: session.TargetRole,
|
|
Stack: session.Stack,
|
|
InterviewTimeline: session.InterviewTimeline,
|
|
}); err != nil {
|
|
return Session{}, err
|
|
}
|
|
}
|
|
return s.store.Create(session)
|
|
}
|
|
|
|
func (s *Service) GetSession(id string) (Session, error) {
|
|
return s.store.Get(id)
|
|
}
|
|
|
|
func (s *Service) SubmitAnswer(ctx context.Context, input SubmitAnswerInput) (Answer, error) {
|
|
if strings.TrimSpace(input.AnswerText) == "" {
|
|
return Answer{}, errors.New("answer_text is required")
|
|
}
|
|
|
|
session, err := s.store.Get(input.SessionID)
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
|
|
question, ok := findQuestion(session.Questions, input.QuestionID)
|
|
if !ok {
|
|
return Answer{}, ErrQuestionNotFound
|
|
}
|
|
|
|
answer := Answer{
|
|
ID: s.nextID("answer"),
|
|
QuestionID: input.QuestionID,
|
|
Text: input.AnswerText,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
grade, err := s.runner.GradeInterviewAnswer(ctx, workflows.GradeAnswerInput{
|
|
UserID: session.UserID,
|
|
QuestionID: question.ID,
|
|
AnswerID: answer.ID,
|
|
AnswerText: answer.Text,
|
|
Concepts: question.Concepts,
|
|
})
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
answer.Grade = grade
|
|
if s.memory != nil {
|
|
candidate, err := s.runner.ExtractLearningMemory(ctx, grade)
|
|
if err != nil {
|
|
return Answer{}, err
|
|
}
|
|
if err := s.memory.ApplyCandidate(candidate); err != nil {
|
|
return Answer{}, err
|
|
}
|
|
}
|
|
|
|
session.Answers = append(session.Answers, answer)
|
|
if answeredQuestionCount(session.Answers) >= len(session.Questions) {
|
|
session.Status = SessionComplete
|
|
}
|
|
if _, err := s.store.Update(session); err != nil {
|
|
return Answer{}, err
|
|
}
|
|
return answer, nil
|
|
}
|
|
|
|
func answeredQuestionCount(answers []Answer) int {
|
|
answered := make(map[string]struct{}, len(answers))
|
|
for _, answer := range answers {
|
|
answered[answer.QuestionID] = struct{}{}
|
|
}
|
|
return len(answered)
|
|
}
|
|
|
|
func (s *Service) nextID(prefix string) string {
|
|
return fmt.Sprintf("%s-%d", prefix, s.ids.Add(1))
|
|
}
|
|
|
|
func findQuestion(questions []Question, id string) (Question, bool) {
|
|
for _, question := range questions {
|
|
if question.ID == id {
|
|
return question, true
|
|
}
|
|
}
|
|
return Question{}, false
|
|
}
|