AI Agents7 min read
Building an AI Storytelling Platform for Children with Gemini, Streamlit, and Panel
Personalized story generation with consistent narrative arcs, multi-turn branching, age-appropriate content controls, and two UI variants deployed on Cloud Run
- Gemini 2.5 Flash
- Streamlit
- Panel
- Cloud Run
- Python
- Docker
- Content Safety
The hard part of AI storytelling for children isn't generation — it's coherence. A child who tells Gemini their character's name is "Luna" and she has a purple unicorn companion expects Luna and the unicorn to appear consistently across all five chapters. The system prompt alone can't guarantee this for longer stories. The solution is a two-stage approach: plan the story structure first, then generate each chapter with the plan as a hard constraint.
Two-Stage Generation Architecture
User Input
├── Character: name, appearance, personality traits
├── Setting: enchanted forest | underwater city | space station | historical era
├── Theme: adventure | friendship | problem-solving | environmental | magical
└── Age group: toddler (3-5) | early reader (6-8) | middle grade (9-12)
↓
Stage 1: Story Planner (Gemini 2.5 Flash)
├── Generates 5-7 story beats
├── Defines character voices and motivations
├── Plans setup → conflict → resolution arc
└── Outputs structured JSON outline
↓
Stage 2: Chapter Generator (Gemini 2.5 Flash, streaming)
├── Receives: outline + previous chapters + user choices
├── Maintains character voice consistency
├── Calibrates vocabulary to age group
└── Ends with: cliffhanger (ch 1-4) or resolution (ch 5)
↓
UI Presentation
├── Streamlit: linear chapter display, "next chapter" button
└── Panel: choice cards for interactive branchingStory Planning Prompt
1import json
2from vertexai.generative_models import GenerativeModel
3
4model = GenerativeModel("gemini-2.5-flash")
5
6def plan_story(
7 character_name: str,
8 character_traits: list[str],
9 setting: str,
10 theme: str,
11 age_group: str,
12) -> dict:
13 prompt = f"""Create a story plan for a children's story.
14
15Character: {character_name} — traits: {', '.join(character_traits)}
16Setting: {setting}
17Theme: {theme}
18Age group: {age_group}
19
20Create a 5-chapter story plan as JSON:
21{{
22 "title": "Story title",
23 "characters": {{
24 "{character_name}": {{
25 "voice": "how they speak",
26 "motivation": "what they want",
27 "companions": ["any companion names and descriptions"]
28 }}
29 }},
30 "chapters": [
31 {{
32 "chapter": 1,
33 "beat": "Setup — introduce character and world",
34 "key_event": "specific event that happens",
35 "ends_with": "cliffhanger or choice"
36 }}
37 // ... chapters 2-5
38 ],
39 "resolution": "how the story ends positively"
40}}
41
42Ensure the theme of {theme} is woven throughout.
43The resolution must be positive and age-appropriate for {age_group}."""
44
45 response = model.generate_content(
46 prompt,
47 generation_config={"temperature": 0.7, "response_mime_type": "application/json"},
48 )
49 return json.loads(response.text)Age-Calibrated Chapter Generation
1AGE_PROFILES = {
2 "toddler": {
3 "word_count": "150-250 words",
4 "vocabulary": "simple words only, no complex sentences",
5 "style": "repetitive patterns are good, short sentences, lots of action",
6 "avoid": "suspense, fear, anything scary"
7 },
8 "early_reader": {
9 "word_count": "300-500 words",
10 "vocabulary": "grade 1-3 vocabulary, occasional new word with context",
11 "style": "clear narrative, dialogue, simple emotional beats",
12 "avoid": "violence, mature themes, complex subplots"
13 },
14 "middle_grade": {
15 "word_count": "500-800 words",
16 "vocabulary": "varied vocabulary, metaphors welcome",
17 "style": "character development, moral dilemmas, humor",
18 "avoid": "adult themes, graphic content"
19 }
20}
21
22def generate_chapter(
23 plan: dict,
24 chapter_num: int,
25 previous_chapters: list[str],
26 user_choice: str | None,
27 age_group: str,
28) -> str:
29 profile = AGE_PROFILES[age_group]
30 chapter_plan = plan["chapters"][chapter_num - 1]
31
32 # Inject all character details from the plan — prevents drift
33 character_context = json.dumps(plan["characters"], indent=2)
34
35 prompt = f"""Write chapter {chapter_num} of a children's story.
36
37STORY PLAN (follow this exactly):
38{json.dumps(chapter_plan, indent=2)}
39
40CHARACTERS (maintain these consistently):
41{character_context}
42
43AGE GROUP: {age_group}
44- Length: {profile['word_count']}
45- Vocabulary: {profile['vocabulary']}
46- Style: {profile['style']}
47- Avoid: {profile['avoid']}
48
49PREVIOUS STORY (maintain continuity):
50{chr(10).join(f"Chapter {i+1}: {ch}" for i, ch in enumerate(previous_chapters))}
51
52{f"USER CHOSE: {user_choice}" if user_choice else ""}
53
54Write the chapter in flowing prose. End with the planned cliffhanger or choice."""
55
56 # Stream for responsive UI
57 response_stream = model.generate_content(prompt, stream=True)
58 return "".join(chunk.text for chunk in response_stream if chunk.text)Content Safety Configuration
1from vertexai.generative_models import SafetySetting, HarmCategory, HarmBlockThreshold
2
3# Maximum safety settings for children's content
4CHILD_SAFETY_SETTINGS = [
5 SafetySetting(
6 category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
7 threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
8 ),
9 SafetySetting(
10 category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
11 threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
12 ),
13 SafetySetting(
14 category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
15 threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
16 ),
17 SafetySetting(
18 category=HarmCategory.HARM_CATEGORY_HARASSMENT,
19 threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
20 ),
21]
22
23model_with_safety = GenerativeModel(
24 "gemini-2.5-flash",
25 safety_settings=CHILD_SAFETY_SETTINGS,
26 system_instruction="""You write children's stories. You must:
27- Always portray kindness, courage, and inclusive values
28- Never include violence, fear, or adult themes
29- Give every story a positive resolution
30- Make heroes solve problems through cleverness and cooperation, not force"""
31)Streamlit vs. Panel — Which to Use
- Streamlit: deploy in minutes, shareable URL, great for demos and single-user scenarios. Chapter display is linear — click "Next Chapter" to continue. Limited layout control.
- Panel: richer widget system, interactive choice cards for story branching, better for complex layouts. More setup required, runs on Bokeh server.
- Both deploy on Cloud Run with the same Docker image — environment variable selects which app entry point to use.
- Chapters per story with consistent narrative arc
- 5
- Age groups supported (toddler, early reader, middle grade)
- 3
- UI variants (Streamlit + Panel)
- 2
- Harm blocking threshold for all 4 safety categories
- MAX