Files
BoostAI/Backend/internal/assignmentgen/service_generate.go
2026-05-25 17:05:06 +01:00

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
}