Skip to content

QuizMaster Agent

Filelib/agents/quizMasterAgent.ts (runQuizMasterAgent)
Triggered byapp/api/live-quiz/route.ts
Modelgemini-2.5-pro (Vertex AI)

What it does

Generates quiz questions from a story page. When a child taps "Quiz Time", all 5 questions are generated in parallel (Promise.all) at once — so the child waits once upfront instead of waiting between every question.

Each question includes pre-written encouragement and correction text used for instant feedback without a second LLM call.

Performance optimisations

  • Agent cache — one LlmAgent instance is cached per langName:quizType key (e.g. English:multiple_choice). The 3 question types reuse cached agents across questions, avoiding Vertex AI cold-start overhead on every call.
  • TTS non-blocking — after the LLM returns a question, TTS is fired with .then().catch() so a TTS failure never blocks the quiz result from streaming to the client.
  • Reduced retries — max 1 retry with 500ms delay (previously 2 retries at 1500ms/3000ms). Combined with the fallback strategy this keeps worst-case failure latency low.

Question types

Rotated by question index to ensure variety across the 5 questions:

IndexType
0multiple_choice
1true_false
2fill_blank
3multiple_choice
4true_false

Tools

ToolQuestion typeKey rules
submit_multiple_choicemultiple_choiceExactly 3 options. correctAnswer must exactly match one option string
submit_true_falsetrue_falseA statement (not a question). trueLabel/falseLabel translated to story language
submit_fill_blankfill_blankOne word in a sentence replaced by ___. Must include a hint and exactly 3 wordChoices (1 correct + 2 distractors)

All question fields

Every question type includes these fields:

FieldDescription
questionThe question text or statement
correctAnswerExact answer string
encouragementShort phrase shown/spoken when child is correct (e.g. "Excellent! That's right!")
correctionGentle phrase shown/spoken when child is wrong (e.g. "Good try! Luna found a golden key.")

Fill-blank extra fields

FieldDescription
hintShort hint about the missing word
wordChoicesExactly 3 words: the correct word + 2 plausible distractors from the story, in random order

The UI renders wordChoices as 3 colored chips (read-only word bank) so the child can see the options and type the answer in a text input below.


Language support

All text fields — question, options, encouragement, correction, trueLabel, falseLabel, hint — are written in the story's language.

Supports all 21 languages: English, Spanish, French, Portuguese (Brazil), Portuguese (Portugal), German, Italian, Japanese, Korean, Chinese, Arabic, Hindi, Indonesian, Russian, Vietnamese, Malay, Filipino, Tetum, Burmese, Lao, Khmer.

Examples for Indonesian (id):

  • trueLabel: "Benar", falseLabel: "Salah"
  • question: "Apa yang ditemukan Luna di tepi danau?"
  • encouragement: "Luar biasa! Kamu benar!"

TTS audio

After generating each question, the agent synthesizes speech for the question text and returns it as a base64 audio string. This audio plays automatically when the question appears on screen.

Audio format: data:audio/wav;base64,...

Flow

Child taps "Quiz Time"


Promise.all([
    runWithRetry(index=0),  ← multiple_choice  ┐
    runWithRetry(index=1),  ← true_false        │  all fire simultaneously
    runWithRetry(index=2),  ← fill_blank        │  agent instances reused from cache
    runWithRetry(index=3),  ← multiple_choice  │
    runWithRetry(index=4),  ← true_false        ┘
])  ← each retries up to 1× on failure (500ms delay)
    │                         LLM + TTS run in parallel per question
    ▼ streams "question" event as each finishes
    ▼ sends "result" event when ALL 5 are ready
Questions shown to child (all 5 buffered, no API calls between)


Q1 → Q2 → Q3 → Q4 → Q5

Input / Output

typescript
// Input
{
  pageText: string;           // Story page text to base questions on
  storyTitle: string;
  questionIndex: number;      // 0–4, determines question type
  previousQuestions: string[]; // Always [] in practice — the route passes an empty array for all 5 parallel calls
  language?: string;          // default: "en"
}

// Output
{
  quiz: Record<string, unknown> | null;  // The question object
  audioData?: string;                    // base64 WAV for TTS playback
  error?: string;
}

Retry logic

Each of the 5 parallel calls uses runWithRetry (defined in app/api/live-quiz/route.ts):

  • Max 1 retry after the initial attempt (2 total tries)
  • Delay: 500ms before retry
  • Retries on: Vertex AI timeout, network error, or agent returning no quiz

Fill-blank fallback

If fill_blank (index 2) fails after all retries, app/api/live-quiz/route.ts automatically retries once with multiple_choice type instead. This is because fill_blank requires more complex tool output (wordChoices, hint) and is more likely to fail in non-English languages like Tetum. The fallback is implemented in the API route, not inside the agent itself.

Client-side graceful handling

If a question still fails despite the fallback, the client filters out null questions and runs the quiz with however many questions succeeded (e.g. 4 out of 5). Score is reported against the actual number of questions generated.


Stateless agent design

The LlmAgent is configured with includeContents: "none". This is critical.

InMemoryRunner (Google ADK) keeps a session history. Without this flag, the model receives its own previous tool calls and responses as context — causing it to call submit_* tools repeatedly in a loop (10+ times, 3000+ tokens). With includeContents: "none", the agent treats every invocation as a fresh call — it only sees the current user message and system instruction.

The same includeContents: "none" setting is applied to 4 agents total — QuizMaster, LearningAdvisor, ParentInsights, and StoryAdaptation. (QuizFeedback does not need it — see quiz-feedback.md.)


Observability span

Span name: agent/QuizMaster

AttributeExample value
agent.nameQuizMaster
agent.modelgemini-2.5-pro
agent.languageid
agent.quizTypemultiple_choice
agent.questionIndex2
agent.quiz_type_generatedfill_blank
agent.successtrue
agent.duration_ms8300

Simulation test cases

Run with npm run simulate:

ScenarioExpected
Valid quiz structurequestion, correctAnswer, type all present
True/false typetype === "true_false", trueLabel and falseLabel present
Fill-in-the-blanktype === "fill_blank", question contains ___
Indonesian quizquestion contains Indonesian words
Correct answer in optionscorrectAnswer matches one of the options strings
Question varietyTwo questions generated from the same page have different text (deduplication via agent context, not previousQuestions — that field is always [] in production)
Audio is generatedaudioData starts with data:audio/

Released under the MIT License.