Boost Azure Demo
This commit is contained in:
244
Backend/internal/assignmentgen/service_generate.go
Normal file
244
Backend/internal/assignmentgen/service_generate.go
Normal file
@@ -0,0 +1,244 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user