feat: add diagnostic interview loop
This commit is contained in:
117
internal/interview/service.go
Normal file
117
internal/interview/service.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package interview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tutor/internal/workflows"
|
||||
)
|
||||
|
||||
var ErrQuestionNotFound = errors.New("diagnostic question not found")
|
||||
|
||||
type Service struct {
|
||||
store Store
|
||||
runner workflows.Runner
|
||||
ids atomic.Uint64
|
||||
}
|
||||
|
||||
func NewService(store Store, runner workflows.Runner) *Service {
|
||||
return &Service{store: store, runner: runner}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user