LearningAdvisor Agent
| File | lib/agents/learningAgent.ts (runLearningAgent) |
| Triggered by | app/api/learning-recommendation/route.ts (on dashboard load) |
| Model | gemini-2.5-pro (Vertex AI) |
What it does
Analyzes the child's quiz history across all stories they've read and generates:
- A difficulty recommendation — should the next story be easier, same, or harder?
- 3–5 personalized story theme suggestions based on what they've enjoyed reading
- Encouragement text — warm, specific praise shown on the dashboard
- A reading insight — one sentence about their strengths or growth areas
Grounding with private data (RAG)
This agent uses Retrieval-Augmented Generation with private Firestore data:
- API route reads the last 100 stories from
users/{uid}/stories, filters to those with alastQuizScorefield, then takes the 10 most recent - Extracts: story title, prompt, score, total, language, date
- Formats this as a structured context block passed to the agent
- Agent reasons over the child's actual history — not generic advice
Tools
| Tool | Parameters |
|---|---|
submit_recommendation | difficulty, suggestedThemes[], encouragement, readingInsight, topicFocus? |
Difficulty logic
| Average score | Recommendation |
|---|---|
| ≥ 80% | "harder" — child is excelling |
| 50–79% | "same" — child is on track |
| < 50% | "easier" — child needs support |
Recent scores are weighted more than older ones.
Caching — avoid repeat Gemini calls
Every dashboard visit does a cheap Firestore read. The Gemini call is skipped if nothing has changed:
- Client reads
sessionStoragekeylearning_rec_{uid}→ shows cached recommendation instantly - Client sends
lastKnownQuizDate(the ISO date of the last quiz when the recommendation was cached) to the API - API compares it against Firestore's latest
lastQuizDate - If they match → returns
{ upToDate: true }(no Gemini call) - If a new quiz was completed → runs Gemini, returns fresh recommendation, client updates cache
Cost per repeat visit: 1 Firestore read (~0ms) + 0 Gemini tokens.
Flow
Dashboard loads
│
▼
Read sessionStorage cache → show instantly if present
│
▼
POST /api/learning-recommendation { userId, language, lastKnownQuizDate }
│
├─ Fetch last 100 stories → filter to quizzed → take 10 most recent
│
├─ latestQuizDate === lastKnownQuizDate?
│ │ YES → return { upToDate: true } (0 Gemini tokens)
│ │
│ ▼ NO
│ LearningAdvisor LLM call
│ └── submit_recommendation({
│ difficulty: "harder",
│ suggestedThemes: ["A brave bunny...", ...],
│ encouragement: "You got 4/5 on...",
│ readingInsight: "You love adventure stories!"
│ })
│
▼
Dashboard shows "Personalized for You" section
- Encouragement text
- 3 clickable theme buttons (→ /create?prompt=...)
- Difficulty badge (🚀 harder / ⭐ same / 🌱 easier)Input / Output
typescript
// Input (QuizRecord[])
{
storyTitle: string;
storyPrompt: string;
score: number;
total: number;
language: string;
completedAt: string; // ISO string
}[]
// Output (LearningRecommendation)
{
difficulty: "easier" | "same" | "harder";
suggestedThemes: string[]; // 3-5 story ideas
encouragement: string; // shown on dashboard
readingInsight: string; // 1-sentence insight
topicFocus?: string; // optional focus area
}Fallback
If no quiz history exists (quizHistory.length === 0), the agent is not called. A static fallback recommendation is returned instantly with 3 generic themes.
Observability span
Span name: agent/LearningAdvisor
| Attribute | Example value |
|---|---|
agent.name | LearningAdvisor |
agent.model | gemini-2.5-pro |
agent.language | en |
agent.quiz_count | 7 |
agent.avg_score | 72 |
agent.recommendation | same |
agent.duration_ms | 3200 |
