245 lines
6.5 KiB
Go
245 lines
6.5 KiB
Go
package assignmentgen
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"boostai-backend/internal/questiongen"
|
|
"boostai-backend/internal/sqlc"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func (s *Service) validateGenerateRequest(params GenerateStudentQuestionSetParams) error {
|
|
if s == nil || s.db == nil || s.db.Pool == nil {
|
|
return errors.New("assignment question generator database is not configured")
|
|
}
|
|
if s.generator == nil {
|
|
return errors.New("assignment question generator is not configured")
|
|
}
|
|
if params.AssignmentID <= 0 || params.StudentID <= 0 || params.TeacherID <= 0 {
|
|
return errors.New("assignment_id, student_id, and teacher_id are required")
|
|
}
|
|
return validatePlanItems(params.Plan)
|
|
}
|
|
|
|
func validatePlanItems(plan []PlanItem) error {
|
|
if len(plan) == 0 {
|
|
return errors.New("at least one generation plan item is required")
|
|
}
|
|
|
|
for _, item := range plan {
|
|
if item.Count <= 0 {
|
|
return fmt.Errorf("generation count must be positive for bucket %q", item.SourceBucket)
|
|
}
|
|
if strings.TrimSpace(string(item.SourceBucket)) == "" {
|
|
return errors.New("source bucket is required for every generation plan item")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateAssignmentOwnership(ctx context.Context, queries *sqlc.Queries, assignmentID, teacherID int64) error {
|
|
assignment, err := queries.GetAssignmentByID(ctx, assignmentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return fmt.Errorf("assignment %d not found", assignmentID)
|
|
}
|
|
return err
|
|
}
|
|
if assignment.TeacherID != teacherID {
|
|
return fmt.Errorf("assignment %d does not belong to teacher %d", assignmentID, teacherID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateStudentAssignment(ctx context.Context, queries *sqlc.Queries, assignmentID, studentID int64) error {
|
|
_, err := queries.GetAssignmentAssignee(ctx, sqlc.GetAssignmentAssigneeParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return fmt.Errorf("student %d is not assigned to assignment %d", studentID, assignmentID)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func clearStudentQuestionMappings(ctx context.Context, queries *sqlc.Queries, assignmentID, studentID int64) error {
|
|
return queries.DeleteAssignmentStudentQuestions(ctx, sqlc.DeleteAssignmentStudentQuestionsParams{
|
|
AssignmentID: assignmentID,
|
|
StudentID: studentID,
|
|
})
|
|
}
|
|
|
|
func normalizeQuestionDefaults(params GenerateStudentQuestionSetParams) (sqlc.QuestionStatus, string) {
|
|
questionStatus := params.QuestionStatus
|
|
if questionStatus == "" {
|
|
questionStatus = sqlc.QuestionStatusDraft
|
|
}
|
|
|
|
questionSource := strings.TrimSpace(params.QuestionSource)
|
|
if questionSource == "" {
|
|
questionSource = defaultQuestionSource
|
|
}
|
|
|
|
return questionStatus, questionSource
|
|
}
|
|
|
|
func (s *Service) generateAndStorePlan(
|
|
ctx context.Context,
|
|
queries *sqlc.Queries,
|
|
params GenerateStudentQuestionSetParams,
|
|
questionStatus sqlc.QuestionStatus,
|
|
questionSource string,
|
|
) ([]StoredStudentQuestion, error) {
|
|
stored := make([]StoredStudentQuestion, 0)
|
|
position := int32(1)
|
|
|
|
for _, item := range params.Plan {
|
|
generatedQuestions, usedSeed, err := s.generator.Generate(questiongen.GenerateParams{
|
|
Topic: item.Topic,
|
|
Difficulty: item.Difficulty,
|
|
Count: item.Count,
|
|
Seed: item.Seed,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
storedBatch, nextPosition, err := storeGeneratedQuestionBatch(
|
|
ctx,
|
|
queries,
|
|
params,
|
|
item,
|
|
generatedQuestions,
|
|
questionStatus,
|
|
questionSource,
|
|
usedSeed,
|
|
position,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stored = append(stored, storedBatch...)
|
|
position = nextPosition
|
|
}
|
|
|
|
return stored, nil
|
|
}
|
|
|
|
func storeGeneratedQuestionBatch(
|
|
ctx context.Context,
|
|
queries *sqlc.Queries,
|
|
params GenerateStudentQuestionSetParams,
|
|
item PlanItem,
|
|
generatedQuestions []questiongen.GeneratedQuestion,
|
|
questionStatus sqlc.QuestionStatus,
|
|
questionSource string,
|
|
usedSeed int64,
|
|
startPosition int32,
|
|
) ([]StoredStudentQuestion, int32, error) {
|
|
stored := make([]StoredStudentQuestion, 0, len(generatedQuestions))
|
|
position := startPosition
|
|
|
|
for _, generated := range generatedQuestions {
|
|
storedQuestion, err := storeGeneratedQuestion(
|
|
ctx,
|
|
queries,
|
|
params,
|
|
item,
|
|
generated,
|
|
questionStatus,
|
|
questionSource,
|
|
usedSeed,
|
|
position,
|
|
)
|
|
if err != nil {
|
|
return nil, startPosition, err
|
|
}
|
|
|
|
stored = append(stored, storedQuestion)
|
|
position++
|
|
}
|
|
|
|
return stored, position, nil
|
|
}
|
|
|
|
func storeGeneratedQuestion(
|
|
ctx context.Context,
|
|
queries *sqlc.Queries,
|
|
params GenerateStudentQuestionSetParams,
|
|
item PlanItem,
|
|
generated questiongen.GeneratedQuestion,
|
|
questionStatus sqlc.QuestionStatus,
|
|
questionSource string,
|
|
usedSeed int64,
|
|
position int32,
|
|
) (StoredStudentQuestion, error) {
|
|
question, err := queries.CreateQuestion(ctx, sqlc.CreateQuestionParams{
|
|
AuthorTeacherID: params.TeacherID,
|
|
Title: generated.Title,
|
|
Prompt: generated.Prompt,
|
|
Topic: nullableQuestionTopic(item.Topic),
|
|
Subject: textValue(firstNonEmpty(params.Subject, questionTopicLabel(item.Topic))),
|
|
Difficulty: nullableQuestionDifficulty(item.Difficulty),
|
|
Source: textValue(questionSource),
|
|
Status: questionStatus,
|
|
CorrectAnswer: textValue(generated.CorrectAnswer),
|
|
})
|
|
if err != nil {
|
|
return StoredStudentQuestion{}, err
|
|
}
|
|
|
|
tags := mergeTags(generated.Tags, string(item.SourceBucket), questionSource)
|
|
if err := attachQuestionTags(ctx, queries, question.ID, tags); err != nil {
|
|
return StoredStudentQuestion{}, err
|
|
}
|
|
|
|
mapping, err := queries.AddAssignmentStudentQuestion(ctx, sqlc.AddAssignmentStudentQuestionParams{
|
|
AssignmentID: params.AssignmentID,
|
|
StudentID: params.StudentID,
|
|
QuestionID: question.ID,
|
|
Position: position,
|
|
SourceBucket: string(item.SourceBucket),
|
|
SourceTopic: nullableQuestionTopic(item.Topic),
|
|
SourceDifficulty: nullableQuestionDifficulty(item.Difficulty),
|
|
GeneratorSeed: pgtype.Int8{Int64: usedSeed, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return StoredStudentQuestion{}, err
|
|
}
|
|
|
|
return StoredStudentQuestion{
|
|
Mapping: mapping,
|
|
Question: question,
|
|
Tags: tags,
|
|
UsedSeed: usedSeed,
|
|
SourceBucket: string(item.SourceBucket),
|
|
}, nil
|
|
}
|
|
|
|
func attachQuestionTags(ctx context.Context, queries *sqlc.Queries, questionID int64, tagNames []string) error {
|
|
for _, tagName := range tagNames {
|
|
tag, err := queries.CreateTag(ctx, tagName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := queries.AttachTagToQuestion(ctx, sqlc.AttachTagToQuestionParams{
|
|
QuestionID: questionID,
|
|
TagID: tag.ID,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|