Files
tutor-service/internal/interview/service.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(input.Lang),
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
}