Boost Azure Demo

This commit is contained in:
MangoPig
2026-05-25 17:05:06 +01:00
parent 675285e99d
commit 4f79137d89
230 changed files with 43275 additions and 2644 deletions

View File

@@ -0,0 +1,634 @@
package questiongen
import (
"fmt"
"math/rand"
"sort"
"strings"
"time"
"boostai-backend/internal/sqlc"
)
type Service struct{}
type GenerateParams struct {
Topic sqlc.QuestionTopic
Difficulty sqlc.QuestionDifficulty
Count int
Seed int64
}
type GeneratedQuestion struct {
Title string
Prompt string
CorrectAnswer string
WorkedSolution []string
Tags []string
}
func NewService() *Service {
return &Service{}
}
func (s *Service) Generate(params GenerateParams) ([]GeneratedQuestion, int64, error) {
count := params.Count
if count <= 0 {
count = 1
}
seed := params.Seed
if seed == 0 {
seed = time.Now().UnixNano()
}
rng := rand.New(rand.NewSource(seed))
items := make([]GeneratedQuestion, 0, count)
for i := 0; i < count; i++ {
question, err := s.generateOne(rng, params.Topic, params.Difficulty)
if err != nil {
return nil, seed, err
}
items = append(items, question)
}
return items, seed, nil
}
func (s *Service) generateOne(rng *rand.Rand, topic sqlc.QuestionTopic, difficulty sqlc.QuestionDifficulty) (GeneratedQuestion, error) {
switch topic {
case sqlc.QuestionTopicPlaceValue:
return generatePlaceValueQuestion(rng, difficulty), nil
case sqlc.QuestionTopicArithmetic:
return generateArithmeticQuestion(rng, difficulty), nil
case sqlc.QuestionTopicNegativeNumbers:
return generateNegativeNumbersQuestion(rng, difficulty), nil
case sqlc.QuestionTopicBidmas:
return generateBidmasQuestion(rng, difficulty), nil
case sqlc.QuestionTopicFractions:
return generateFractionsQuestion(rng, difficulty), nil
case sqlc.QuestionTopicAlgebra:
return generateAlgebraQuestion(rng, difficulty), nil
case sqlc.QuestionTopicGeometry:
return generateGeometryQuestion(rng, difficulty), nil
case sqlc.QuestionTopicData:
return generateDataQuestion(rng, difficulty), nil
default:
return GeneratedQuestion{}, fmt.Errorf("unsupported topic: %s", topic)
}
}
// Future word_problem work should not just bolt a `word_problem` tag onto an already-built
// abstract question. Each topic generator should eventually expose dedicated word-problem
// template families so the RNG chooses both the maths structure and a fitting real-world context
// together. That will keep prompts, answers, and worked steps consistent instead of doing a late
// text rewrite after the numbers are chosen.
func buildGeneratedTags(topic sqlc.QuestionTopic, difficulty sqlc.QuestionDifficulty, extra ...string) []string {
tags := []string{string(topic), string(difficulty), "rng_generated"}
tags = append(tags, extra...)
unique := make(map[string]struct{}, len(tags))
normalized := make([]string, 0, len(tags))
for _, tag := range tags {
value := strings.ToLower(strings.TrimSpace(tag))
if value == "" {
continue
}
if _, exists := unique[value]; exists {
continue
}
unique[value] = struct{}{}
normalized = append(normalized, value)
}
sort.Strings(normalized)
return normalized
}
func generatePlaceValueQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
var digits, targetIndex int
switch difficulty {
case sqlc.QuestionDifficultyEasy:
digits = 2
targetIndex = randomInt(rng, 0, 1)
case sqlc.QuestionDifficultyMedium:
digits = 3
targetIndex = randomInt(rng, 0, 2)
default:
digits = randomInt(rng, 4, 5)
targetIndex = randomInt(rng, 1, digits-1)
}
numberDigits := make([]int, digits)
numberDigits[0] = randomInt(rng, 1, 9)
for i := 1; i < digits; i++ {
numberDigits[i] = randomInt(rng, 0, 9)
}
number := digitsToInt(numberDigits)
digit := numberDigits[targetIndex]
placePower := digits - targetIndex - 1
placeValue := digit
for i := 0; i < placePower; i++ {
placeValue *= 10
}
placeName := placeNameFromPower(placePower)
prompt := fmt.Sprintf("What is the value of the digit %d in %d?", digit, number)
return GeneratedQuestion{
Title: fmt.Sprintf("%s Place Value", strings.Title(string(difficulty))),
Prompt: prompt,
CorrectAnswer: fmt.Sprintf("%d", placeValue),
WorkedSolution: []string{
fmt.Sprintf("In %d, the digit %d is in the %s place.", number, digit, placeName),
fmt.Sprintf("So its value is %d.", placeValue),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicPlaceValue, difficulty, placeName),
}
}
func generateArithmeticQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
a := randomInt(rng, 1, 9)
b := randomInt(rng, 1, 9)
if rng.Intn(2) == 0 {
return GeneratedQuestion{
Title: "Easy Addition",
Prompt: fmt.Sprintf("Calculate %d + %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", a+b),
WorkedSolution: []string{
fmt.Sprintf("Add the ones: %d + %d = %d.", a, b, a+b),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "addition", "single_digit"),
}
}
if a < b {
a, b = b, a
}
return GeneratedQuestion{
Title: "Easy Subtraction",
Prompt: fmt.Sprintf("Calculate %d - %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", a-b),
WorkedSolution: []string{
fmt.Sprintf("Subtract %d from %d.", b, a),
fmt.Sprintf("%d - %d = %d.", a, b, a-b),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "subtraction", "single_digit"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
if rng.Intn(2) == 0 {
a := randomInt(rng, 10, 99)
b := randomInt(rng, 10, 99)
return GeneratedQuestion{
Title: "Medium Addition",
Prompt: fmt.Sprintf("Work out %d + %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", a+b),
WorkedSolution: []string{
fmt.Sprintf("Add the numbers together: %d + %d = %d.", a, b, a+b),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "addition", "two_digit"),
}
}
a := randomInt(rng, 2, 12)
b := randomInt(rng, 2, 12)
return GeneratedQuestion{
Title: "Medium Multiplication",
Prompt: fmt.Sprintf("Calculate %d × %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", a*b),
WorkedSolution: []string{
fmt.Sprintf("Use multiplication facts: %d × %d = %d.", a, b, a*b),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "multiplication", "times_tables"),
}
}
if rng.Intn(2) == 0 {
divisor := randomInt(rng, 3, 12)
quotient := randomInt(rng, 4, 12)
dividend := divisor * quotient
return GeneratedQuestion{
Title: "Hard Division",
Prompt: fmt.Sprintf("Calculate %d ÷ %d.", dividend, divisor),
CorrectAnswer: fmt.Sprintf("%d", quotient),
WorkedSolution: []string{
fmt.Sprintf("Use the inverse of multiplication: %d × %d = %d.", divisor, quotient, dividend),
fmt.Sprintf("So %d ÷ %d = %d.", dividend, divisor, quotient),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "division", "inverse_operations"),
}
}
a := randomInt(rng, 20, 99)
b := randomInt(rng, 11, 49)
return GeneratedQuestion{
Title: "Hard Subtraction",
Prompt: fmt.Sprintf("Work out %d - %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", a-b),
WorkedSolution: []string{
fmt.Sprintf("Subtract %d from %d carefully, using column subtraction if needed.", b, a),
fmt.Sprintf("%d - %d = %d.", a, b, a-b),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicArithmetic, difficulty, "subtraction", "column_method"),
}
}
func generateNegativeNumbersQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
a := randomInt(rng, -9, 9)
b := randomInt(rng, -9, 9)
result := a + b
return GeneratedQuestion{
Title: "Easy Negative Numbers",
Prompt: fmt.Sprintf("Calculate %d + %d.", a, b),
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("Start at %d on the number line.", a),
fmt.Sprintf("Move %d steps to get %d.", b, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "addition"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
a := randomInt(rng, -20, 20)
b := randomInt(rng, -20, 20)
result := a - b
return GeneratedQuestion{
Title: "Medium Negative Numbers",
Prompt: fmt.Sprintf("Calculate %d - (%d).", a, b),
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("Subtracting %d is the same as adding %d.", b, -b),
fmt.Sprintf("So %d - (%d) = %d + %d = %d.", a, b, a, -b, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "subtraction"),
}
}
a := randomInt(rng, -30, 30)
b := randomInt(rng, -30, 30)
c := randomInt(rng, -15, 15)
result := a - b + c
prompt := fmt.Sprintf("Calculate %d - (%d) + %d.", a, b, c)
return GeneratedQuestion{
Title: "Hard Negative Numbers",
Prompt: prompt,
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("First change subtraction of a negative: %d - (%d) = %d + %d.", a, b, a, -b),
fmt.Sprintf("Then add %d to get %d.", c, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicNegativeNumbers, difficulty, "multi_step"),
}
}
func generateBidmasQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
a := randomInt(rng, 1, 20)
b := randomInt(rng, 2, 9)
c := randomInt(rng, 2, 9)
result := a + b*c
return GeneratedQuestion{
Title: "Easy BIDMAS",
Prompt: fmt.Sprintf("Work out %d + %d × %d.", a, b, c),
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("Do multiplication first: %d × %d = %d.", b, c, b*c),
fmt.Sprintf("Then add %d + %d = %d.", a, b*c, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "order_of_operations"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
a := randomInt(rng, 2, 12)
b := randomInt(rng, 3, 12)
c := randomInt(rng, 2, 10)
result := (a + b) * c
return GeneratedQuestion{
Title: "Medium BIDMAS",
Prompt: fmt.Sprintf("Work out (%d + %d) × %d.", a, b, c),
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("Work inside brackets first: %d + %d = %d.", a, b, a+b),
fmt.Sprintf("Then multiply %d × %d = %d.", a+b, c, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "brackets"),
}
}
a := randomInt(rng, 2, 12)
b := randomInt(rng, 2, 6)
c := randomInt(rng, 2, 12)
d := randomInt(rng, 2, 5)
left := a * b
right := c * d
result := left + right
return GeneratedQuestion{
Title: "Hard BIDMAS",
Prompt: fmt.Sprintf("Work out %d × %d + %d × %d.", a, b, c, d),
CorrectAnswer: fmt.Sprintf("%d", result),
WorkedSolution: []string{
fmt.Sprintf("Complete each multiplication first: %d × %d = %d and %d × %d = %d.", a, b, left, c, d, right),
fmt.Sprintf("Then add %d + %d = %d.", left, right, result),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicBidmas, difficulty, "multiple_operations"),
}
}
func generateFractionsQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
denominator := randomInt(rng, 2, 9)
numerator := randomInt(rng, 1, denominator-1)
maxMultiplier := 9 / denominator
if maxMultiplier < 1 {
maxMultiplier = 1
}
multiplier := randomInt(rng, 1, maxMultiplier)
prompt := fmt.Sprintf("What is %d/%d of %d?", numerator, denominator, denominator*multiplier)
answer := numerator * multiplier
return GeneratedQuestion{
Title: "Easy Fractions",
Prompt: prompt,
CorrectAnswer: fmt.Sprintf("%d", answer),
WorkedSolution: []string{
fmt.Sprintf("Find one part first: %d ÷ %d = %d.", denominator*multiplier, denominator, multiplier),
fmt.Sprintf("Then take %d parts: %d × %d = %d.", numerator, multiplier, numerator, answer),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "single_digit", "fraction_of_amount"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
denominator := randomInt(rng, 3, 10)
a := randomInt(rng, 1, denominator-1)
b := randomInt(rng, 1, denominator-1)
resultN, resultD := simplifyFraction(a+b, denominator)
return GeneratedQuestion{
Title: "Medium Fractions",
Prompt: fmt.Sprintf("Work out %d/%d + %d/%d. Give your answer in simplest form.", a, denominator, b, denominator),
CorrectAnswer: formatFraction(resultN, resultD),
WorkedSolution: []string{
fmt.Sprintf("The denominators are the same, so add the numerators: %d + %d = %d.", a, b, a+b),
fmt.Sprintf("This gives %d/%d.", a+b, denominator),
fmt.Sprintf("Simplify to %s.", formatFraction(resultN, resultD)),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "addition", "simplify"),
}
}
aN := randomInt(rng, 1, 8)
aD := randomInt(rng, 2, 9)
bN := randomInt(rng, 1, 8)
bD := randomInt(rng, 2, 9)
resultN, resultD := simplifyFraction(aN*bN, aD*bD)
return GeneratedQuestion{
Title: "Hard Fractions",
Prompt: fmt.Sprintf("Work out %d/%d × %d/%d. Give your answer in simplest form.", aN, aD, bN, bD),
CorrectAnswer: formatFraction(resultN, resultD),
WorkedSolution: []string{
fmt.Sprintf("Multiply the numerators: %d × %d = %d.", aN, bN, aN*bN),
fmt.Sprintf("Multiply the denominators: %d × %d = %d.", aD, bD, aD*bD),
fmt.Sprintf("This gives %d/%d, which simplifies to %s.", aN*bN, aD*bD, formatFraction(resultN, resultD)),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicFractions, difficulty, "multiplication", "simplify"),
}
}
func generateAlgebraQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
x := randomInt(rng, 1, 12)
a := randomInt(rng, 1, 12)
b := x + a
return GeneratedQuestion{
Title: "Easy Algebra",
Prompt: fmt.Sprintf("Solve x + %d = %d.", a, b),
CorrectAnswer: fmt.Sprintf("x = %d", x),
WorkedSolution: []string{
fmt.Sprintf("Subtract %d from both sides.", a),
fmt.Sprintf("x = %d - %d = %d.", b, a, x),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "one_step"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
x := randomInt(rng, 2, 12)
a := randomInt(rng, 2, 9)
b := randomInt(rng, 1, 12)
c := a*x + b
return GeneratedQuestion{
Title: "Medium Algebra",
Prompt: fmt.Sprintf("Solve %dx + %d = %d.", a, b, c),
CorrectAnswer: fmt.Sprintf("x = %d", x),
WorkedSolution: []string{
fmt.Sprintf("Subtract %d from both sides to get %dx = %d.", b, a, c-b),
fmt.Sprintf("Divide both sides by %d, so x = %d.", a, x),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "two_step"),
}
}
x := randomInt(rng, -6, 12)
a := randomInt(rng, 2, 6)
b := randomInt(rng, 1, 8)
c := randomInt(rng, 2, 6)
d := a*(x+b) - c
return GeneratedQuestion{
Title: "Hard Algebra",
Prompt: fmt.Sprintf("Solve %d(x + %d) - %d = %d.", a, b, c, d),
CorrectAnswer: fmt.Sprintf("x = %d", x),
WorkedSolution: []string{
fmt.Sprintf("Add %d to both sides: %d(x + %d) = %d.", c, a, b, d+c),
fmt.Sprintf("Divide by %d: x + %d = %d.", a, b, x+b),
fmt.Sprintf("Subtract %d, so x = %d.", b, x),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicAlgebra, difficulty, "brackets", "multi_step"),
}
}
func generateGeometryQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
side := randomInt(rng, 2, 9)
perimeter := side * 4
return GeneratedQuestion{
Title: "Easy Geometry",
Prompt: fmt.Sprintf("A square has side length %d cm. What is its perimeter?", side),
CorrectAnswer: fmt.Sprintf("%d cm", perimeter),
WorkedSolution: []string{
fmt.Sprintf("A square has 4 equal sides, so calculate 4 × %d.", side),
fmt.Sprintf("The perimeter is %d cm.", perimeter),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "perimeter", "square"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
length := randomInt(rng, 4, 15)
width := randomInt(rng, 3, 12)
area := length * width
return GeneratedQuestion{
Title: "Medium Geometry",
Prompt: fmt.Sprintf("A rectangle has length %d cm and width %d cm. What is its area?", length, width),
CorrectAnswer: fmt.Sprintf("%d cm²", area),
WorkedSolution: []string{
fmt.Sprintf("Area of a rectangle = length × width."),
fmt.Sprintf("%d × %d = %d, so the area is %d cm².", length, width, area, area),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "area", "rectangle"),
}
}
a := randomInt(rng, 20, 100)
b := randomInt(rng, 20, 100)
missing := 180 - a - b
return GeneratedQuestion{
Title: "Hard Geometry",
Prompt: fmt.Sprintf("Two angles in a triangle are %d° and %d°. Find the third angle.", a, b),
CorrectAnswer: fmt.Sprintf("%d°", missing),
WorkedSolution: []string{
fmt.Sprintf("Angles in a triangle add to 180°."),
fmt.Sprintf("First add the known angles: %d + %d = %d.", a, b, a+b),
fmt.Sprintf("Then calculate 180 - %d = %d°.", a+b, missing),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicGeometry, difficulty, "angles", "triangle"),
}
}
func generateDataQuestion(rng *rand.Rand, difficulty sqlc.QuestionDifficulty) GeneratedQuestion {
if difficulty == sqlc.QuestionDifficultyEasy {
values := sortedRandomValues(rng, 5, 1, 9)
median := values[len(values)/2]
return GeneratedQuestion{
Title: "Easy Data",
Prompt: fmt.Sprintf("Find the median of these numbers: %s.", joinInts(values)),
CorrectAnswer: fmt.Sprintf("%d", median),
WorkedSolution: []string{
fmt.Sprintf("Put the numbers in order: %s.", joinInts(values)),
fmt.Sprintf("The middle value is %d, so the median is %d.", median, median),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "median"),
}
}
if difficulty == sqlc.QuestionDifficultyMedium {
values := sortedRandomValues(rng, 5, 2, 20)
sum := 0
for _, value := range values {
sum += value
}
mean := sum / len(values)
adjustment := sum % len(values)
if adjustment != 0 {
values[len(values)-1] += len(values) - adjustment
sort.Ints(values)
sum = 0
for _, value := range values {
sum += value
}
mean = sum / len(values)
}
return GeneratedQuestion{
Title: "Medium Data",
Prompt: fmt.Sprintf("Find the mean of these numbers: %s.", joinInts(values)),
CorrectAnswer: fmt.Sprintf("%d", mean),
WorkedSolution: []string{
fmt.Sprintf("Add the numbers: the total is %d.", sum),
fmt.Sprintf("Divide by %d: %d ÷ %d = %d.", len(values), sum, len(values), mean),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "mean"),
}
}
values := sortedRandomValues(rng, 6, 5, 30)
modeIndex := randomInt(rng, 0, len(values)-1)
modeValue := values[modeIndex]
values = append(values, modeValue)
sort.Ints(values)
return GeneratedQuestion{
Title: "Hard Data",
Prompt: fmt.Sprintf("Find the mode of these numbers: %s.", joinInts(values)),
CorrectAnswer: fmt.Sprintf("%d", modeValue),
WorkedSolution: []string{
fmt.Sprintf("The mode is the value that appears most often."),
fmt.Sprintf("%d appears more than any other value, so the mode is %d.", modeValue, modeValue),
},
Tags: buildGeneratedTags(sqlc.QuestionTopicData, difficulty, "mode"),
}
}
func randomInt(rng *rand.Rand, min, max int) int {
if max <= min {
return min
}
return min + rng.Intn(max-min+1)
}
func digitsToInt(digits []int) int {
value := 0
for _, digit := range digits {
value = value*10 + digit
}
return value
}
func placeNameFromPower(power int) string {
switch power {
case 0:
return "ones"
case 1:
return "tens"
case 2:
return "hundreds"
case 3:
return "thousands"
case 4:
return "ten-thousands"
default:
return "place"
}
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
if a < 0 {
return -a
}
return a
}
func simplifyFraction(numerator, denominator int) (int, int) {
if denominator == 0 {
return numerator, denominator
}
divisor := gcd(numerator, denominator)
return numerator / divisor, denominator / divisor
}
func formatFraction(numerator, denominator int) string {
if denominator == 1 {
return fmt.Sprintf("%d", numerator)
}
return fmt.Sprintf("%d/%d", numerator, denominator)
}
func sortedRandomValues(rng *rand.Rand, count, min, max int) []int {
values := make([]int, count)
for i := 0; i < count; i++ {
values[i] = randomInt(rng, min, max)
}
sort.Ints(values)
return values
}
func joinInts(values []int) string {
parts := make([]string, 0, len(values))
for _, value := range values {
parts = append(parts, fmt.Sprintf("%d", value))
}
return strings.Join(parts, ", ")
}