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