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

173 lines
4.3 KiB
Go

package assignmentgen
import (
"context"
"fmt"
"math"
"sort"
"time"
"boostai-backend/internal/sqlc"
)
func (s *Service) BuildMixedPlan(ctx context.Context, params MixedPlanParams) (MixedPlan, error) {
if err := validateMixedPlanParams(params); err != nil {
return MixedPlan{}, err
}
ratio := normalizePersonalizedRatio(params.PersonalizedRatio)
weaknessSummary, err := s.BuildWeaknessSummary(ctx, params.StudentID)
if err != nil {
return MixedPlan{}, err
}
personalizedTopic, personalizedApplied := selectPersonalizedTopic(params.PrimaryTopic, weaknessSummary)
personalizedCount := calculatePersonalizedCount(params.TotalQuestions, ratio, personalizedApplied)
coreCount := calculateCoreCount(params.TotalQuestions, personalizedCount)
baseSeed := normalizeBaseSeed(params.BaseSeed)
personalizedDifficulty := normalizePersonalizedDifficulty(params)
return MixedPlan{
Plan: buildMixedPlanItems(params, coreCount, personalizedCount, personalizedTopic, personalizedDifficulty, baseSeed),
WeaknessSummary: weaknessSummary,
CoreCount: coreCount,
PersonalizedCount: personalizedCount,
PersonalizedTopic: personalizedTopic,
PersonalizedApplied: personalizedCount > 0,
BaseSeed: baseSeed,
}, nil
}
func validateMixedPlanParams(params MixedPlanParams) error {
if params.StudentID <= 0 {
return fmt.Errorf("student_id is required")
}
if params.PrimaryTopic == "" {
return fmt.Errorf("primary topic is required")
}
if params.PrimaryDifficulty == "" {
return fmt.Errorf("primary difficulty is required")
}
if params.TotalQuestions <= 0 {
return fmt.Errorf("total_questions must be positive")
}
if params.PersonalizedRatio < 0 || params.PersonalizedRatio >= 1 {
return fmt.Errorf("personalized_ratio must be between 0 and less than 1")
}
return nil
}
func normalizePersonalizedRatio(ratio float64) float64 {
if ratio == 0 {
return defaultPersonalizedRatio
}
return ratio
}
func normalizeBaseSeed(seed int64) int64 {
if seed == 0 {
return time.Now().UnixNano()
}
return seed
}
func normalizePersonalizedDifficulty(params MixedPlanParams) sqlc.QuestionDifficulty {
if params.PersonalizedDifficulty == "" {
return params.PrimaryDifficulty
}
return params.PersonalizedDifficulty
}
func calculateCoreCount(totalQuestions, personalizedCount int) int {
coreCount := totalQuestions - personalizedCount
if totalQuestions > 0 && coreCount <= 0 {
return 1
}
return coreCount
}
func buildMixedPlanItems(
params MixedPlanParams,
coreCount int,
personalizedCount int,
personalizedTopic sqlc.QuestionTopic,
personalizedDifficulty sqlc.QuestionDifficulty,
baseSeed int64,
) []PlanItem {
plan := make([]PlanItem, 0, 2)
if coreCount > 0 {
plan = append(plan, PlanItem{
Topic: params.PrimaryTopic,
Difficulty: params.PrimaryDifficulty,
Count: coreCount,
SourceBucket: SourceBucketCoreTopic,
Seed: baseSeed,
})
}
if personalizedCount > 0 {
plan = append(plan, PlanItem{
Topic: personalizedTopic,
Difficulty: personalizedDifficulty,
Count: personalizedCount,
SourceBucket: SourceBucketPersonalized,
Seed: baseSeed + 7919,
})
}
return plan
}
func selectPersonalizedTopic(primaryTopic sqlc.QuestionTopic, summary WeaknessSummary) (sqlc.QuestionTopic, bool) {
if len(summary.TopicScores) == 0 {
return "", false
}
type scoredTopic struct {
topic sqlc.QuestionTopic
score float64
}
topics := make([]scoredTopic, 0, len(summary.TopicScores))
for topic, score := range summary.TopicScores {
topics = append(topics, scoredTopic{topic: topic, score: score})
}
sort.SliceStable(topics, func(i, j int) bool {
if topics[i].score == topics[j].score {
return string(topics[i].topic) < string(topics[j].topic)
}
return topics[i].score < topics[j].score
})
for _, candidate := range topics {
if candidate.topic != primaryTopic {
return candidate.topic, true
}
}
return topics[0].topic, true
}
func calculatePersonalizedCount(totalQuestions int, ratio float64, personalizedApplied bool) int {
if !personalizedApplied || totalQuestions <= 1 || ratio <= 0 {
return 0
}
count := int(math.Floor(float64(totalQuestions) * ratio))
if count == 0 && totalQuestions >= 3 {
count = 1
}
if count >= totalQuestions {
count = totalQuestions - 1
}
if count < 0 {
count = 0
}
return count
}