173 lines
4.3 KiB
Go
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
|
|
}
|