This page shows a representative message sequence from when a user asks for flashcards about ATP.
+
A2UI enables agents to generate rich UI components as declarative JSON data, not executable code. The client application renders these using its own native components.
+
+
+
Browser Client
+ →
+
Local API Server (api-server.ts)
+ →
+
Remote Agent (Vertex AI Agent Engine)
+ →
+
Local API Server
+ →
+
Browser Renders
+
+
+
+
+
+
+
+
+
1
+ Client → Server
+ /api/chat (Intent Detection)
+
+
+
+ User types "Create flashcards for bond energy misconceptions". The orchestrator first sends this to Gemini to detect the user's intent.
+
+
+
+ Request Payload
+ JSON
+
+
+
+
+
+
+
+
+
+
2
+ Server → Client
+ /api/chat (Intent Classification)
+
+
+
+ Gemini classifies the intent as "flashcards". This tells the orchestrator to fetch A2UI content from the specialized agent.
+
+
+
+ Response
+ JSON
+
+
+
+
+
+
+
+
+
+
3
+ Client → Server
+ /a2ui-agent/a2a/query
+
+
+
+ The browser requests A2UI content generation. It specifies format: flashcards and includes context about what the user asked for.
+
+
+
+ Request Payload
+ JSON
+
+
+
+
+
+
+
+
+
+
4
+ Server → Remote Agent
+ Deployed to Vertex AI Agent Engine
+
+
+
+ The server forwards the request to a remote A2UI-generating agent deployed to Vertex AI Agent Engine. This agent is a separate service that specializes in generating A2UI content.
+
+ Key Point: This is the A2A (Agent-to-Agent) pattern described in the blog. The orchestrator delegates UI generation to a specialized remote agent deployed on Vertex AI Agent Engine - demonstrating cross-boundary agent collaboration.
+
+
+
+
+
+
+
+
5
+ Remote Agent → Server
+ Response from Vertex AI Agent Engine
+
+
+
+ The remote agent (deployed on Agent Engine) returns A2UI JSON - a declarative description of UI components. This is the core of what A2UI provides.
+
+
+
+ A2UI Messages (Parsed)
+ JSON • Scroll to see full payload
+
+
+
+
+
Understanding the A2UI Structure
+
beginRendering - Declares the surface and root component ID
+ surfaceUpdate - Contains the flat list of components with ID references
+ Components - Text, Column, Row, Flashcard - these are catalog components the client knows how to render
+
+
+
+
+
+
+
+
6
+ Server → Client
+ /a2ui-agent/a2a/query
+
+
+
+ The server sends the A2UI payload to the browser. The @a2ui/web-lib renderer processes this JSON and creates native web components.
+
+
+
+ Final Response to Browser
+ JSON
+
+
+
+
+ Result: The browser's A2UI renderer creates <a2ui-surface>, which contains <a2ui-flashcard> components styled to match the host application's theme.
+
+
+
+
+
+
+
+
+
diff --git a/samples/personalized_learning/agent/.env.template b/samples/personalized_learning/agent/.env.template
new file mode 100644
index 00000000..da5bc32a
--- /dev/null
+++ b/samples/personalized_learning/agent/.env.template
@@ -0,0 +1,15 @@
+# Personalized Learning Agent Configuration
+
+# Google Cloud Project (required)
+GOOGLE_CLOUD_PROJECT=your-project-id
+
+# Optional: GCS bucket for learner context data
+# If not set, loads from local learner_context/ directory
+# GCS_CONTEXT_BUCKET=your-bucket-name
+# GCS_CONTEXT_PREFIX=learner_context/
+
+# Model configuration
+LITELLM_MODEL=gemini-2.5-flash
+
+# Server configuration (for local development)
+PORT=8081
diff --git a/samples/personalized_learning/agent/Dockerfile b/samples/personalized_learning/agent/Dockerfile
new file mode 100644
index 00000000..6a082dda
--- /dev/null
+++ b/samples/personalized_learning/agent/Dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Install dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy agent code
+COPY *.py ./
+
+# Create learner context directory
+RUN mkdir -p /app/learner_context
+
+# Set environment variables
+ENV PORT=8080
+# GOOGLE_CLOUD_PROJECT must be set at runtime
+ENV GOOGLE_CLOUD_LOCATION=global
+
+# Expose port
+EXPOSE 8080
+
+# Run the server
+CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/samples/personalized_learning/agent/__init__.py b/samples/personalized_learning/agent/__init__.py
new file mode 100644
index 00000000..ad876331
--- /dev/null
+++ b/samples/personalized_learning/agent/__init__.py
@@ -0,0 +1,10 @@
+"""
+Personalized Learning Agent Package
+
+This package exports the ADK agent for use with `adk web` or deployment
+to Agent Engine.
+"""
+
+from .agent import root_agent
+
+__all__ = ["root_agent"]
diff --git a/samples/personalized_learning/agent/a2ui_templates.py b/samples/personalized_learning/agent/a2ui_templates.py
new file mode 100644
index 00000000..6469891c
--- /dev/null
+++ b/samples/personalized_learning/agent/a2ui_templates.py
@@ -0,0 +1,409 @@
+"""
+A2UI Templates for Learning Materials
+
+Provides templates and examples for generating A2UI JSON payloads
+for various learning material formats.
+"""
+
+SURFACE_ID = "learningContent"
+
+# Flashcard A2UI template example
+FLASHCARD_EXAMPLE = f"""
+Example A2UI JSON for a set of flashcards:
+
+[
+ {{"beginRendering": {{"surfaceId": "{SURFACE_ID}", "root": "mainColumn"}}}},
+ {{
+ "surfaceUpdate": {{
+ "surfaceId": "{SURFACE_ID}",
+ "components": [
+ {{
+ "id": "mainColumn",
+ "component": {{
+ "Column": {{
+ "children": {{"explicitList": ["headerText", "flashcardRow"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "headerText",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "Study Flashcards: ATP & Bond Energy"}},
+ "usageHint": "h2"
+ }}
+ }}
+ }},
+ {{
+ "id": "flashcardRow",
+ "component": {{
+ "Row": {{
+ "children": {{"explicitList": ["card1", "card2", "card3"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "card1",
+ "component": {{
+ "Flashcard": {{
+ "front": {{"literalString": "What happens when ATP is hydrolyzed?"}},
+ "back": {{"literalString": "ATP + H2O → ADP + Pi + Energy. Energy is released because the products are MORE STABLE than ATP."}},
+ "category": {{"literalString": "Biochemistry"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "card2",
+ "component": {{
+ "Flashcard": {{
+ "front": {{"literalString": "Does breaking a bond release or require energy?"}},
+ "back": {{"literalString": "Breaking ANY bond REQUIRES energy input. Energy is released when new, more stable bonds FORM."}},
+ "category": {{"literalString": "Chemistry"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "card3",
+ "component": {{
+ "Flashcard": {{
+ "front": {{"literalString": "Why is 'energy stored in bonds' misleading?"}},
+ "back": {{"literalString": "Bonds don't store energy like batteries. Energy comes from the STABILITY DIFFERENCE between reactants and products."}},
+ "category": {{"literalString": "MCAT Concept"}}
+ }}
+ }}
+ }}
+ ]
+ }}
+ }}
+]
+"""
+
+# Audio/Podcast A2UI template
+AUDIO_EXAMPLE = f"""
+Example A2UI JSON for an audio player (podcast):
+
+[
+ {{"beginRendering": {{"surfaceId": "{SURFACE_ID}", "root": "audioCard"}}}},
+ {{
+ "surfaceUpdate": {{
+ "surfaceId": "{SURFACE_ID}",
+ "components": [
+ {{
+ "id": "audioCard",
+ "component": {{
+ "Card": {{
+ "child": "audioContent"
+ }}
+ }}
+ }},
+ {{
+ "id": "audioContent",
+ "component": {{
+ "Column": {{
+ "children": {{"explicitList": ["audioHeader", "audioPlayer", "audioDescription"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "audioHeader",
+ "component": {{
+ "Row": {{
+ "children": {{"explicitList": ["audioIcon", "audioTitle"]}},
+ "distribution": "start",
+ "alignment": "center"
+ }}
+ }}
+ }},
+ {{
+ "id": "audioIcon",
+ "component": {{
+ "Icon": {{
+ "name": {{"literalString": "podcasts"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "audioTitle",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "ATP & Chemical Stability Podcast"}},
+ "usageHint": "h3"
+ }}
+ }}
+ }},
+ {{
+ "id": "audioPlayer",
+ "component": {{
+ "AudioPlayer": {{
+ "url": {{"literalString": "/assets/podcast.m4a"}},
+ "audioTitle": {{"literalString": "Understanding ATP Energy Release"}},
+ "audioDescription": {{"literalString": "A personalized podcast about ATP and chemical stability"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "audioDescription",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "This 10-minute podcast explains why 'energy stored in bonds' is a misconception and how to think about ATP correctly for the MCAT."}},
+ "usageHint": "body"
+ }}
+ }}
+ }}
+ ]
+ }}
+ }}
+]
+"""
+
+# Video A2UI template
+VIDEO_EXAMPLE = f"""
+Example A2UI JSON for a video player:
+
+[
+ {{"beginRendering": {{"surfaceId": "{SURFACE_ID}", "root": "videoCard"}}}},
+ {{
+ "surfaceUpdate": {{
+ "surfaceId": "{SURFACE_ID}",
+ "components": [
+ {{
+ "id": "videoCard",
+ "component": {{
+ "Card": {{
+ "child": "videoContent"
+ }}
+ }}
+ }},
+ {{
+ "id": "videoContent",
+ "component": {{
+ "Column": {{
+ "children": {{"explicitList": ["videoTitle", "videoPlayer", "videoDescription"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "videoTitle",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "Visual Explanation: Bond Energy vs. Stability"}},
+ "usageHint": "h3"
+ }}
+ }}
+ }},
+ {{
+ "id": "videoPlayer",
+ "component": {{
+ "Video": {{
+ "url": {{"literalString": "/assets/video.mp4"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "videoDescription",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "Watch this animated explanation of why breaking bonds requires energy and how ATP hydrolysis actually works."}},
+ "usageHint": "body"
+ }}
+ }}
+ }}
+ ]
+ }}
+ }}
+]
+"""
+
+# QuizCard template - interactive quiz cards with immediate feedback
+QUIZ_EXAMPLE = f"""
+Example A2UI JSON for quiz cards (interactive multiple choice with feedback):
+
+[
+ {{"beginRendering": {{"surfaceId": "{SURFACE_ID}", "root": "mainColumn"}}}},
+ {{
+ "surfaceUpdate": {{
+ "surfaceId": "{SURFACE_ID}",
+ "components": [
+ {{
+ "id": "mainColumn",
+ "component": {{
+ "Column": {{
+ "children": {{"explicitList": ["headerText", "quizRow"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "headerText",
+ "component": {{
+ "Text": {{
+ "text": {{"literalString": "Quick Quiz: ATP & Bond Energy"}},
+ "usageHint": "h3"
+ }}
+ }}
+ }},
+ {{
+ "id": "quizRow",
+ "component": {{
+ "Row": {{
+ "children": {{"explicitList": ["quiz1", "quiz2"]}},
+ "distribution": "start",
+ "alignment": "stretch"
+ }}
+ }}
+ }},
+ {{
+ "id": "quiz1",
+ "component": {{
+ "QuizCard": {{
+ "question": {{"literalString": "What happens to energy when ATP is hydrolyzed?"}},
+ "options": [
+ {{"label": {{"literalString": "Energy stored in the phosphate bond is released"}}, "value": "a", "isCorrect": false}},
+ {{"label": {{"literalString": "Energy is released because products are more stable"}}, "value": "b", "isCorrect": true}},
+ {{"label": {{"literalString": "The bond breaking itself releases energy"}}, "value": "c", "isCorrect": false}},
+ {{"label": {{"literalString": "ATP's special bonds contain more electrons"}}, "value": "d", "isCorrect": false}}
+ ],
+ "explanation": {{"literalString": "ATP hydrolysis releases energy because the products (ADP + Pi) are MORE STABLE than ATP. The phosphate groups in ATP repel each other, creating strain. When this bond is broken, the products achieve better resonance stabilization - like releasing a compressed spring."}},
+ "category": {{"literalString": "Thermodynamics"}}
+ }}
+ }}
+ }},
+ {{
+ "id": "quiz2",
+ "component": {{
+ "QuizCard": {{
+ "question": {{"literalString": "Breaking a chemical bond requires or releases energy?"}},
+ "options": [
+ {{"label": {{"literalString": "Always releases energy"}}, "value": "a", "isCorrect": false}},
+ {{"label": {{"literalString": "Always requires energy input"}}, "value": "b", "isCorrect": true}},
+ {{"label": {{"literalString": "Depends on whether it's a high-energy bond"}}, "value": "c", "isCorrect": false}},
+ {{"label": {{"literalString": "Neither - bonds are energy neutral"}}, "value": "d", "isCorrect": false}}
+ ],
+ "explanation": {{"literalString": "Breaking ANY bond REQUIRES energy (it's endothermic). This is a common MCAT trap! Energy is only released when NEW bonds FORM. Think of it like pulling apart magnets - you have to put in effort to separate them."}},
+ "category": {{"literalString": "Bond Energy"}}
+ }}
+ }}
+ }}
+ ]
+ }}
+ }}
+]
+"""
+
+
+def get_system_prompt(format_type: str, context: str) -> str:
+ """
+ Generate the system prompt for A2UI generation.
+
+ Args:
+ format_type: Type of content to generate (flashcards, audio, video, quiz)
+ context: The learner context data
+
+ Returns:
+ System prompt for the LLM
+ """
+ examples = {
+ "flashcards": FLASHCARD_EXAMPLE,
+ "audio": AUDIO_EXAMPLE,
+ "podcast": AUDIO_EXAMPLE,
+ "video": VIDEO_EXAMPLE,
+ "quiz": QUIZ_EXAMPLE,
+ }
+
+ example = examples.get(format_type.lower(), FLASHCARD_EXAMPLE)
+
+ if format_type.lower() == "flashcards":
+ return f"""You are creating MCAT study flashcards for Maria, a pre-med student.
+
+## Maria's Profile
+{context}
+
+## Your Task
+Create 4-5 high-quality flashcards about ATP and bond energy that:
+1. Directly address her misconception that "energy is stored in bonds"
+2. Use sports/gym analogies she loves (compressed springs, holding planks, etc.)
+3. Are MCAT exam-focused with precise scientific language
+4. Have COMPLETE, THOUGHTFUL answers - not placeholders or vague hints
+
+## Flashcard Quality Standards
+GOOD flashcard back:
+"Breaking ANY chemical bond requires energy input - it's endothermic. When ATP is hydrolyzed, the energy released comes from the products (ADP + Pi) being MORE STABLE than ATP. Think of it like releasing a compressed spring - the spring doesn't 'contain' energy, it's just in a high-energy state."
+
+BAD flashcard back:
+"Energy is released because products are more stable." (too vague)
+"Think of ATP like a gym analogy..." (incomplete placeholder)
+
+## A2UI JSON Format
+{example}
+
+## Rules
+- Output ONLY valid JSON - no markdown, no explanation
+- Use surfaceId: "{SURFACE_ID}"
+- Each card needs unique id (card1, card2, etc.)
+- Front: Clear question that tests understanding
+- Back: Complete explanation with analogy where helpful
+- Category: One of "Biochemistry", "Chemistry", "MCAT Concept", "Common Trap"
+
+Generate the flashcards JSON:"""
+
+ if format_type.lower() == "quiz":
+ return f"""You are creating MCAT practice quiz questions for Maria, a pre-med student.
+
+## Maria's Profile
+{context}
+
+## Your Task
+Create 2-3 interactive quiz questions about ATP and bond energy that:
+1. Test her understanding of WHY ATP hydrolysis releases energy
+2. Include plausible wrong answers that reflect common misconceptions
+3. Provide detailed explanations using sports/gym analogies she loves
+4. Are MCAT exam-style with precise scientific language
+
+## QuizCard Component Structure
+Each QuizCard must have:
+- question: The question text
+- options: Array of 4 choices, each with label, value (a/b/c/d), and isCorrect (true/false)
+- explanation: Detailed explanation shown after answering
+- category: Topic category like "Thermodynamics", "Bond Energy", "MCAT Concept"
+
+## A2UI JSON Format
+{example}
+
+## Rules
+- Output ONLY valid JSON - no markdown, no explanation
+- Use surfaceId: "{SURFACE_ID}"
+- Each quiz card needs unique id (quiz1, quiz2, etc.)
+- Exactly ONE option per question should have isCorrect: true
+- Wrong answers should be plausible misconceptions students commonly have
+- Explanations should be thorough and include analogies
+
+Generate the quiz JSON:"""
+
+ return f"""You are an A2UI content generator for personalized learning materials.
+
+## Learner Context
+{context}
+
+## Output Format
+Output valid A2UI JSON starting with beginRendering.
+
+## Template for {format_type}:
+{example}
+
+## Rules:
+1. Use surfaceId: "{SURFACE_ID}"
+2. Address the learner's specific misconceptions
+3. Use sports/gym analogies for Maria
+4. Output ONLY valid JSON
+5. All component IDs must be unique
+
+Generate the A2UI JSON:"""
diff --git a/samples/personalized_learning/agent/agent.py b/samples/personalized_learning/agent/agent.py
new file mode 100644
index 00000000..4a304049
--- /dev/null
+++ b/samples/personalized_learning/agent/agent.py
@@ -0,0 +1,1131 @@
+"""
+Personalized Learning Agent (ADK)
+
+ADK agent that generates A2UI JSON for personalized learning materials
+based on learner context data.
+
+This agent is designed to be run with `adk web` locally or deployed
+to Agent Engine.
+"""
+
+import json
+import logging
+import os
+import time
+from pathlib import Path
+from typing import Any, Optional, Tuple
+
+# Load environment variables from .env file for local development
+# In Agent Engine, these will be set by the deployment environment
+try:
+ from dotenv import load_dotenv
+ # Try multiple possible .env locations
+ env_paths = [
+ Path(__file__).parent.parent / ".env", # samples/personalized_learning/.env
+ Path(__file__).parent / ".env", # agent/.env
+ Path.cwd() / ".env", # current working directory
+ ]
+ for env_path in env_paths:
+ if env_path.exists():
+ load_dotenv(env_path)
+ break
+except ImportError:
+ # python-dotenv not available (e.g., in Agent Engine)
+ pass
+
+# Set up Vertex AI environment - only if not already set
+os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "TRUE")
+
+from google.adk.agents import Agent
+from google.adk.tools import ToolContext
+
+# ============================================================================
+# MODULE-LEVEL CONFIGURATION
+# These variables are captured by cloudpickle during deployment.
+# They are set at import time from environment variables, ensuring they
+# persist in the deployed agent even though os.environ is not pickled.
+# ============================================================================
+_CONFIG_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT")
+_CONFIG_LOCATION = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
+
+# Use relative imports - required for proper wheel packaging and Agent Engine deployment
+# These may fail in Agent Engine where files aren't available
+try:
+ from .context_loader import get_combined_context, load_context_file
+ from .a2ui_templates import get_system_prompt, SURFACE_ID as _IMPORTED_SURFACE_ID
+ from .openstax_content import fetch_content_for_topic, fetch_chapter_content
+ from .openstax_chapters import OPENSTAX_CHAPTERS, KEYWORD_HINTS, get_openstax_url_for_chapter
+ _HAS_EXTERNAL_MODULES = True
+ _HAS_OPENSTAX = True
+except Exception as e:
+ _HAS_EXTERNAL_MODULES = False
+ _HAS_OPENSTAX = False
+ _IMPORT_ERROR = str(e)
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Log warnings for degraded functionality
+if not _HAS_EXTERNAL_MODULES:
+ logger.warning(
+ "External modules (context_loader, a2ui_templates) not available. "
+ "Using embedded fallback content. Import error: %s",
+ _IMPORT_ERROR if '_IMPORT_ERROR' in globals() else "unknown"
+ )
+
+if not _HAS_OPENSTAX:
+ logger.warning(
+ "OpenStax content modules not available. Flashcards and quizzes will use "
+ "embedded content only, without textbook source material. "
+ "This may result in less accurate educational content."
+ )
+
+# Model configuration - use Gemini 2.5 Flash (available in us-central1)
+MODEL_ID = os.getenv("GENAI_MODEL", "gemini-2.5-flash")
+
+# Supported content formats
+SUPPORTED_FORMATS = ["flashcards", "audio", "podcast", "video", "quiz"]
+
+# Surface ID for A2UI rendering (use imported value if available, else fallback)
+SURFACE_ID = _IMPORTED_SURFACE_ID if _HAS_EXTERNAL_MODULES else "learningContent"
+
+# ============================================================================
+# GCS CONTEXT LOADING (for Agent Engine - loads dynamic context from GCS)
+# ============================================================================
+
+# GCS configuration - set via environment variables
+GCS_CONTEXT_BUCKET = os.getenv("GCS_CONTEXT_BUCKET", "a2ui-demo-context")
+GCS_CONTEXT_PREFIX = os.getenv("GCS_CONTEXT_PREFIX", "learner_context/")
+
+# Context files to load
+CONTEXT_FILES = [
+ "01_maria_learner_profile.txt",
+ "02_chemistry_bond_energy.txt",
+ "03_chemistry_thermodynamics.txt",
+ "04_biology_atp_cellular_respiration.txt",
+ "05_misconception_resolution.txt",
+ "06_mcat_practice_concepts.txt",
+]
+
+
+def _load_from_gcs(filename: str) -> Optional[str]:
+ """Load a context file from GCS bucket."""
+ try:
+ from google.cloud import storage
+
+ client = storage.Client()
+ bucket = client.bucket(GCS_CONTEXT_BUCKET)
+ blob = bucket.blob(f"{GCS_CONTEXT_PREFIX}{filename}")
+
+ if blob.exists():
+ content = blob.download_as_text()
+ logger.info(f"Loaded {filename} from GCS bucket {GCS_CONTEXT_BUCKET}")
+ return content
+ else:
+ logger.warning(f"File {filename} not found in GCS bucket {GCS_CONTEXT_BUCKET}")
+ return None
+
+ except Exception as e:
+ logger.warning(f"Failed to load from GCS: {e}")
+ return None
+
+
+def _load_all_context_from_gcs() -> dict[str, str]:
+ """Load all context files from GCS."""
+ context = {}
+ for filename in CONTEXT_FILES:
+ content = _load_from_gcs(filename)
+ if content:
+ context[filename] = content
+ logger.info(f"Loaded {len(context)} context files from GCS")
+ return context
+
+
+def _get_combined_context_from_gcs() -> str:
+ """Get all context combined from GCS."""
+ all_context = _load_all_context_from_gcs()
+
+ if all_context:
+ combined = []
+ for filename, content in sorted(all_context.items()):
+ combined.append(f"=== {filename} ===\n{content}\n")
+ return "\n".join(combined)
+
+ # Return empty string if GCS load failed - will trigger fallback
+ return ""
+
+
+# ============================================================================
+# EMBEDDED CONTEXT DATA (fallback when GCS is unavailable)
+# ============================================================================
+
+EMBEDDED_LEARNER_PROFILE = """
+## Learner Profile: Maria Santos
+
+**Background:**
+- Pre-med sophomore majoring in Biochemistry
+- Preparing for MCAT in 8 months
+- Works part-time as a pharmacy technician (20 hrs/week)
+
+**Learning Style:**
+- Visual-kinesthetic learner
+- Prefers analogies connecting to real-world applications
+- Responds well to gym/fitness metaphors (exercises regularly)
+- Benefits from spaced repetition for memorization
+
+**Current Progress:**
+- Completed: Cell structure, basic chemistry
+- In progress: Cellular energetics (ATP, metabolism)
+- Struggling with: Thermodynamics concepts, especially Gibbs free energy
+
+**Known Misconceptions:**
+- Believes "energy is stored in bonds" (common misconception)
+- Needs clarification that bond BREAKING releases energy in ATP hydrolysis
+"""
+
+EMBEDDED_CURRICULUM_CONTEXT = """
+## Current Topic: ATP and Cellular Energy
+
+**Learning Objectives:**
+1. Explain why ATP is considered the "energy currency" of cells
+2. Describe the structure of ATP and how it stores potential energy
+3. Understand that energy is released during hydrolysis due to product stability, not bond breaking
+4. Connect ATP usage to cellular processes like muscle contraction
+
+**Key Concepts:**
+- Adenosine triphosphate structure (adenine + ribose + 3 phosphate groups)
+- Phosphoanhydride bonds and electrostatic repulsion
+- Hydrolysis reaction: ATP + H2O → ADP + Pi + Energy
+- Gibbs free energy change (ΔG = -30.5 kJ/mol)
+- Coupled reactions in cellular metabolism
+
+**Common Misconceptions to Address:**
+- "Energy stored in bonds" - Actually, breaking bonds REQUIRES energy;
+ the energy released comes from forming more stable products (ADP + Pi)
+- ATP is not a long-term energy storage molecule (that's glycogen/fat)
+"""
+
+EMBEDDED_MISCONCEPTION_CONTEXT = """
+## Misconception Resolution: "Energy Stored in Bonds"
+
+**The Misconception:**
+Many students believe ATP releases energy because "energy is stored in the phosphate bonds."
+
+**The Reality:**
+- Breaking ANY chemical bond REQUIRES energy input (endothermic)
+- Energy is released when NEW, more stable bonds FORM (exothermic)
+- ATP hydrolysis releases energy because the products (ADP + Pi) are MORE STABLE than ATP
+
+**Why ATP is "High Energy":**
+- The three phosphate groups are negatively charged and repel each other
+- This electrostatic repulsion creates molecular strain (like a compressed spring)
+- When the terminal phosphate is removed, the products achieve better stability
+- The energy comes from relieving this strain, not from "stored bond energy"
+
+**Gym Analogy for Maria:**
+Think of ATP like holding a heavy plank position:
+- Holding the plank (ATP) requires constant energy expenditure to maintain
+- Dropping to rest (ADP + Pi) releases that tension
+- The "energy" wasn't stored in your muscles - it was the relief of an unstable state
+"""
+
+
+def _get_combined_context_fallback() -> str:
+ """Get combined context using embedded data when files aren't available."""
+ return f"""
+{EMBEDDED_LEARNER_PROFILE}
+
+{EMBEDDED_CURRICULUM_CONTEXT}
+
+{EMBEDDED_MISCONCEPTION_CONTEXT}
+"""
+
+
+def _get_system_prompt_fallback(format_type: str, context: str) -> str:
+ """Generate system prompt for A2UI generation (fallback for Agent Engine)."""
+ if format_type.lower() == "flashcards":
+ return f"""You are creating MCAT study flashcards for Maria, a pre-med student.
+
+## Maria's Profile
+{context}
+
+## Your Task
+Create 4-5 high-quality flashcards about ATP and bond energy that:
+1. Directly address her misconception that "energy is stored in bonds"
+2. Use sports/gym analogies she loves (compressed springs, holding planks, etc.)
+3. Are MCAT exam-focused with precise scientific language
+4. Have COMPLETE, THOUGHTFUL answers - not placeholders
+
+## A2UI JSON Format
+Output a JSON array starting with beginRendering, then surfaceUpdate with components.
+Use Flashcard components with front, back, and category fields.
+Use surfaceId: "{SURFACE_ID}"
+
+Generate the flashcards JSON (output ONLY valid JSON, no markdown):"""
+
+ if format_type.lower() == "quiz":
+ return f"""You are creating MCAT practice quiz questions for Maria, a pre-med student.
+
+## Maria's Profile
+{context}
+
+## Your Task
+Create 2-3 interactive quiz questions about ATP and bond energy that:
+1. Test her understanding of WHY ATP hydrolysis releases energy
+2. Include plausible wrong answers reflecting common misconceptions
+3. Provide detailed explanations using sports/gym analogies
+4. Are MCAT exam-style with precise scientific language
+
+## A2UI JSON Format
+Output a JSON array with QuizCard components. Each QuizCard has:
+- question: The question text
+- options: Array of 4 choices with label, value (a/b/c/d), isCorrect
+- explanation: Detailed explanation shown after answering
+- category: Topic category
+Use surfaceId: "{SURFACE_ID}"
+
+Generate the quiz JSON (output ONLY valid JSON, no markdown):"""
+
+ return f"""Generate A2UI JSON for {format_type} content.
+
+## Context
+{context}
+
+Use surfaceId: "{SURFACE_ID}"
+Output ONLY valid JSON, no markdown."""
+
+
+# ============================================================================
+# CACHING FOR PERFORMANCE
+# ============================================================================
+
+# Context cache with TTL
+_CONTEXT_CACHE: dict[str, Tuple[str, float]] = {}
+_CONTEXT_CACHE_TTL = 300 # 5 minutes
+
+
+def _get_cached_context() -> str:
+ """
+ Get combined context with TTL-based caching.
+
+ This avoids 6 GCS reads per request by caching the combined context
+ for 5 minutes. The cache is invalidated after TTL expires.
+ """
+ cache_key = "combined_context"
+ now = time.time()
+
+ if cache_key in _CONTEXT_CACHE:
+ content, cached_at = _CONTEXT_CACHE[cache_key]
+ if now - cached_at < _CONTEXT_CACHE_TTL:
+ logger.info("Using cached learner context (cache hit)")
+ return content
+
+ # Cache miss - load fresh
+ content = _safe_get_combined_context()
+ _CONTEXT_CACHE[cache_key] = (content, now)
+ logger.info("Loaded and cached learner context (cache miss)")
+ return content
+
+
+def clear_context_cache() -> None:
+ """Clear the context cache. Useful for testing."""
+ global _CONTEXT_CACHE
+ _CONTEXT_CACHE = {}
+ logger.info("Context cache cleared")
+
+
+# Wrapper functions with priority: local files -> GCS -> embedded fallback
+def _safe_get_combined_context() -> str:
+ """
+ Get combined context with fallback chain:
+ 1. Local files (via external modules) - for local development
+ 2. GCS bucket - for Agent Engine with dynamic context
+ 3. Embedded data - final fallback
+ """
+ # Try local files first (for local development with adk web)
+ if _HAS_EXTERNAL_MODULES:
+ try:
+ context = get_combined_context()
+ if context:
+ logger.info("Loaded context from local files")
+ return context
+ except Exception as e:
+ logger.warning(f"Failed to load context from local files: {e}")
+
+ # Try GCS (for Agent Engine deployment)
+ gcs_context = _get_combined_context_from_gcs()
+ if gcs_context:
+ logger.info("Loaded context from GCS")
+ return gcs_context
+
+ # Fall back to embedded data
+ logger.info("Using embedded fallback context")
+ return _get_combined_context_fallback()
+
+
+def _safe_load_context_file(filename: str) -> Optional[str]:
+ """
+ Load context file with fallback chain:
+ 1. Local files (via external modules)
+ 2. GCS bucket
+ 3. Embedded data
+ """
+ # Try local files first
+ if _HAS_EXTERNAL_MODULES:
+ try:
+ content = load_context_file(filename)
+ if content:
+ return content
+ except Exception as e:
+ logger.debug(f"Failed to load context file {filename} from local: {e}")
+
+ # Try GCS
+ gcs_content = _load_from_gcs(filename)
+ if gcs_content:
+ return gcs_content
+
+ # Fall back to embedded data based on filename
+ if "learner_profile" in filename:
+ return EMBEDDED_LEARNER_PROFILE
+ if "misconception" in filename:
+ return EMBEDDED_MISCONCEPTION_CONTEXT
+ return None
+
+
+def _safe_get_system_prompt(format_type: str, context: str) -> str:
+ """Get system prompt, using fallback if external modules unavailable."""
+ if _HAS_EXTERNAL_MODULES:
+ try:
+ return get_system_prompt(format_type, context)
+ except Exception as e:
+ logger.warning(f"Failed to get system prompt: {e}, using fallback")
+ return _get_system_prompt_fallback(format_type, context)
+
+
+# ============================================================================
+# TOOL FUNCTIONS
+# ============================================================================
+
+
+async def generate_flashcards(
+ tool_context: ToolContext,
+ topic: Optional[str] = None,
+) -> dict[str, Any]:
+ """
+ Generate personalized flashcard content as A2UI JSON.
+
+ Creates study flashcards tailored to the learner's profile, addressing
+ their misconceptions and using their preferred learning analogies.
+ Content is sourced from OpenStax Biology for AP Courses textbook.
+
+ Args:
+ topic: Optional topic focus (e.g., "bond energy", "ATP hydrolysis").
+ If not provided, generates general flashcards based on learner profile.
+
+ Returns:
+ A2UI JSON for flashcard components that can be rendered in the chat
+ """
+ logger.info("=" * 60)
+ logger.info("GENERATE_FLASHCARDS CALLED")
+ logger.info(f"Topic received: {topic or '(none)'}")
+ logger.info("=" * 60)
+
+ # Get learner context (profile, preferences, misconceptions) - uses cache
+ learner_context = _get_cached_context()
+
+ # Fetch OpenStax content for the topic
+ openstax_content = ""
+ sources = []
+ if topic and _HAS_OPENSTAX:
+ logger.info(f"Fetching OpenStax content for topic: {topic}")
+ try:
+ content_result = await fetch_content_for_topic(topic, max_chapters=2)
+ openstax_content = content_result.get("combined_content", "")
+ sources = content_result.get("sources", [])
+ matched_chapters = content_result.get("matched_chapters", [])
+ logger.info(f"OpenStax fetch result:")
+ logger.info(f" - Matched chapters: {matched_chapters}")
+ logger.info(f" - Sources: {sources}")
+ logger.info(f" - Content length: {len(openstax_content)} chars")
+ if not openstax_content:
+ logger.warning("NO CONTENT RETURNED from OpenStax fetch!")
+ except Exception as e:
+ logger.error(f"FAILED to fetch OpenStax content: {e}")
+ import traceback
+ logger.error(traceback.format_exc())
+
+ # Combine learner context with OpenStax source material
+ if openstax_content:
+ context = f"""## Learner Profile & Preferences
+{learner_context}
+
+## Source Material (OpenStax Biology for AP Courses)
+Use the following textbook content as the authoritative source for creating flashcards:
+
+{openstax_content}
+
+## User's Topic Request
+{topic or 'general biology concepts'}
+"""
+ else:
+ context = learner_context
+ if topic:
+ context = f"{context}\n\nUser requested focus: {topic}"
+
+ result = await _generate_a2ui_content("flashcards", context, tool_context)
+
+ # Add source attribution
+ if sources:
+ result["sources"] = sources
+
+ return result
+
+
+async def generate_quiz(
+ tool_context: ToolContext,
+ topic: Optional[str] = None,
+) -> dict[str, Any]:
+ """
+ Generate personalized quiz questions as A2UI JSON.
+
+ Creates interactive multiple-choice quiz cards with immediate feedback,
+ targeting the learner's specific misconceptions.
+ Content is sourced from OpenStax Biology for AP Courses textbook.
+
+ Args:
+ topic: Optional topic focus (e.g., "thermodynamics", "cellular respiration").
+ If not provided, generates quiz based on learner's weak areas.
+
+ Returns:
+ A2UI JSON for interactive QuizCard components
+ """
+ logger.info(f"Generating quiz for topic: {topic or 'general'}")
+
+ # Get learner context (profile, preferences, misconceptions) - uses cache
+ learner_context = _get_cached_context()
+
+ # Fetch OpenStax content for the topic
+ openstax_content = ""
+ sources = []
+ if topic and _HAS_OPENSTAX:
+ try:
+ content_result = await fetch_content_for_topic(topic, max_chapters=2)
+ openstax_content = content_result.get("combined_content", "")
+ sources = content_result.get("sources", [])
+ logger.info(f"Fetched OpenStax content from {len(sources)} chapters")
+ except Exception as e:
+ logger.warning(f"Failed to fetch OpenStax content: {e}")
+
+ # Combine learner context with OpenStax source material
+ if openstax_content:
+ context = f"""## Learner Profile & Preferences
+{learner_context}
+
+## Source Material (OpenStax Biology for AP Courses)
+Use the following textbook content as the authoritative source for creating quiz questions.
+Ensure all correct answers are factually accurate according to this source:
+
+{openstax_content}
+
+## User's Topic Request
+{topic or 'general biology concepts'}
+"""
+ else:
+ context = learner_context
+ if topic:
+ context = f"{context}\n\nUser requested focus: {topic}"
+
+ result = await _generate_a2ui_content("quiz", context, tool_context)
+
+ # Add source attribution
+ if sources:
+ result["sources"] = sources
+
+ return result
+
+
+async def get_audio_content(
+ tool_context: ToolContext,
+) -> dict[str, Any]:
+ """
+ Get pre-generated podcast/audio content as A2UI JSON.
+
+ Returns A2UI JSON for an audio player with a personalized podcast
+ that explains ATP and bond energy concepts using the learner's
+ preferred analogies.
+
+ Returns:
+ A2UI JSON for AudioPlayer component with podcast content
+ """
+ logger.info("Getting audio content")
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "audioCard"}},
+ {
+ "surfaceUpdate": {
+ "surfaceId": SURFACE_ID,
+ "components": [
+ {
+ "id": "audioCard",
+ "component": {"Card": {"child": "audioContent"}},
+ },
+ {
+ "id": "audioContent",
+ "component": {
+ "Column": {
+ "children": {
+ "explicitList": [
+ "audioHeader",
+ "audioPlayer",
+ "audioDescription",
+ ]
+ },
+ "distribution": "start",
+ "alignment": "stretch",
+ }
+ },
+ },
+ {
+ "id": "audioHeader",
+ "component": {
+ "Row": {
+ "children": {
+ "explicitList": ["audioIcon", "audioTitle"]
+ },
+ "distribution": "start",
+ "alignment": "center",
+ }
+ },
+ },
+ {
+ "id": "audioIcon",
+ "component": {
+ "Icon": {"name": {"literalString": "podcasts"}}
+ },
+ },
+ {
+ "id": "audioTitle",
+ "component": {
+ "Text": {
+ "text": {
+ "literalString": "ATP & Chemical Stability: Correcting the Misconception"
+ },
+ "usageHint": "h3",
+ }
+ },
+ },
+ {
+ "id": "audioPlayer",
+ "component": {
+ "AudioPlayer": {
+ "url": {"literalString": "/assets/podcast.m4a"},
+ "audioTitle": {
+ "literalString": "Understanding ATP Energy Release"
+ },
+ "audioDescription": {
+ "literalString": "A personalized podcast about ATP and chemical stability"
+ },
+ }
+ },
+ },
+ {
+ "id": "audioDescription",
+ "component": {
+ "Text": {
+ "text": {
+ "literalString": "This personalized podcast explains why 'energy stored in bonds' is a common misconception. Using your preferred gym analogies, it walks through how ATP hydrolysis actually releases energy through stability differences, not bond breaking. Perfect for your MCAT prep!"
+ },
+ "usageHint": "body",
+ }
+ },
+ },
+ ],
+ }
+ },
+ ]
+
+ return {
+ "format": "audio",
+ "a2ui": a2ui,
+ "surfaceId": SURFACE_ID,
+ }
+
+
+async def get_video_content(
+ tool_context: ToolContext,
+) -> dict[str, Any]:
+ """
+ Get pre-generated video content as A2UI JSON.
+
+ Returns A2UI JSON for a video player with an animated explainer
+ about ATP energy and stability using visual analogies.
+
+ Returns:
+ A2UI JSON for Video component with educational content
+ """
+ logger.info("Getting video content")
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "videoCard"}},
+ {
+ "surfaceUpdate": {
+ "surfaceId": SURFACE_ID,
+ "components": [
+ {
+ "id": "videoCard",
+ "component": {"Card": {"child": "videoContent"}},
+ },
+ {
+ "id": "videoContent",
+ "component": {
+ "Column": {
+ "children": {
+ "explicitList": [
+ "videoTitle",
+ "videoPlayer",
+ "videoDescription",
+ ]
+ },
+ "distribution": "start",
+ "alignment": "stretch",
+ }
+ },
+ },
+ {
+ "id": "videoTitle",
+ "component": {
+ "Text": {
+ "text": {
+ "literalString": "Visual Guide: ATP Energy & Stability"
+ },
+ "usageHint": "h3",
+ }
+ },
+ },
+ {
+ "id": "videoPlayer",
+ "component": {
+ "Video": {
+ "url": {"literalString": "/assets/video.mp4"},
+ }
+ },
+ },
+ {
+ "id": "videoDescription",
+ "component": {
+ "Text": {
+ "text": {
+ "literalString": "This animated explainer uses the compressed spring analogy to show why ATP releases energy. See how electrostatic repulsion in ATP makes it 'want' to become the more stable ADP + Pi."
+ },
+ "usageHint": "body",
+ }
+ },
+ },
+ ],
+ }
+ },
+ ]
+
+ return {
+ "format": "video",
+ "a2ui": a2ui,
+ "surfaceId": SURFACE_ID,
+ }
+
+
+async def get_learner_profile(
+ tool_context: ToolContext,
+) -> dict[str, Any]:
+ """
+ Get the current learner's profile and context.
+
+ Returns the learner's profile including their learning preferences,
+ current misconceptions, and study goals. Use this to understand
+ who you're helping before generating content.
+
+ Returns:
+ Learner profile with preferences, misconceptions, and goals
+ """
+ logger.info("Getting learner profile")
+
+ profile = _safe_load_context_file("01_maria_learner_profile.txt")
+ misconceptions = _safe_load_context_file("05_misconception_resolution.txt")
+
+ return {
+ "profile": profile or "No profile loaded",
+ "misconceptions": misconceptions or "No misconception data loaded",
+ "supported_formats": SUPPORTED_FORMATS,
+ }
+
+
+async def get_textbook_content(
+ tool_context: ToolContext,
+ topic: str,
+) -> dict[str, Any]:
+ """
+ Fetch relevant OpenStax textbook content for a biology topic.
+
+ Use this tool when the user asks a general biology question that needs
+ accurate, sourced information. This fetches actual textbook content
+ from OpenStax Biology for AP Courses.
+
+ Args:
+ topic: The biology topic to look up (e.g., "photosynthesis",
+ "endocrine system", "DNA replication")
+
+ Returns:
+ Textbook content with source citations
+ """
+ logger.info(f"Fetching textbook content for: {topic}")
+
+ if not _HAS_OPENSTAX:
+ return {
+ "error": "OpenStax content module not available",
+ "topic": topic,
+ }
+
+ try:
+ content_result = await fetch_content_for_topic(topic, max_chapters=3)
+
+ return {
+ "topic": topic,
+ "matched_chapters": content_result.get("matched_chapters", []),
+ "content": content_result.get("combined_content", ""),
+ "sources": content_result.get("sources", []),
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to fetch textbook content: {e}")
+ return {
+ "error": str(e),
+ "topic": topic,
+ }
+
+
+# ============================================================================
+# HELPER FUNCTIONS
+# ============================================================================
+
+
+async def _generate_a2ui_content(
+ format_type: str,
+ context: str,
+ tool_context: ToolContext,
+) -> dict[str, Any]:
+ """
+ Generate A2UI content using the Gemini model.
+
+ This is an internal helper that calls the LLM to generate A2UI JSON.
+ """
+ from google import genai
+ from google.genai import types
+
+ # Initialize client with VertexAI - use us-central1 for consistency with Agent Engine
+ # Use module-level config variables (captured by cloudpickle) with
+ # environment variable fallback for local development
+ project = _CONFIG_PROJECT or os.getenv("GOOGLE_CLOUD_PROJECT")
+ location = _CONFIG_LOCATION or os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
+
+ if not project:
+ logger.error("GOOGLE_CLOUD_PROJECT not configured")
+ return {"error": "GOOGLE_CLOUD_PROJECT not configured. Set it in environment or deploy.py."}
+
+ client = genai.Client(
+ vertexai=True,
+ project=project,
+ location=location,
+ )
+
+ system_prompt = _safe_get_system_prompt(format_type, context)
+
+ try:
+ response = client.models.generate_content(
+ model=MODEL_ID,
+ contents=f"Generate {format_type} for this learner.",
+ config=types.GenerateContentConfig(
+ system_instruction=system_prompt,
+ response_mime_type="application/json",
+ ),
+ )
+
+ response_text = response.text.strip()
+
+ try:
+ a2ui_json = json.loads(response_text)
+ logger.info(f"Successfully generated {format_type} A2UI JSON")
+ return {
+ "format": format_type,
+ "a2ui": a2ui_json,
+ "surfaceId": SURFACE_ID,
+ }
+ except json.JSONDecodeError as e:
+ logger.error(f"Failed to parse A2UI JSON: {e}")
+ return {
+ "error": "Failed to generate valid A2UI JSON",
+ "raw_response": response_text[:1000],
+ }
+
+ except Exception as e:
+ logger.error(f"Error generating content: {e}")
+ return {"error": str(e)}
+
+
+# ============================================================================
+# AGENT DEFINITION
+# ============================================================================
+
+# System prompt for tool selection and agent behavior.
+# Note: Maria's profile also appears in src/chat-orchestrator.ts (for chat responses)
+# and learner_context/ files (for dynamic personalization). This duplication is
+# intentional—the frontend and agent operate independently.
+SYSTEM_PROMPT = """# Personalized Learning Agent
+
+You are a personalized learning assistant that helps students study biology more effectively.
+You generate interactive learning materials tailored to each learner's profile,
+addressing their specific misconceptions and using their preferred learning styles.
+
+**All content is sourced from OpenStax Biology for AP Courses**, a free peer-reviewed
+college textbook. Your tools fetch actual textbook content to ensure accuracy.
+
+## Your Capabilities
+
+You can generate several types of learning content:
+
+1. **Flashcards** - Interactive study cards based on textbook content
+2. **Quiz Questions** - Multiple choice questions with detailed explanations
+3. **Audio Content** - Personalized podcast explaining concepts
+4. **Video Content** - Animated visual explanations
+5. **Textbook Content** - Look up specific topics from OpenStax
+
+## Current Learner
+
+You're helping Maria, a pre-med student preparing for the MCAT. She:
+- Loves sports/gym analogies for learning
+- Has a misconception about "energy stored in bonds"
+- Needs to understand ATP hydrolysis correctly
+- Prefers visual and kinesthetic learning
+
+## How to Respond
+
+When a user asks for learning materials:
+
+1. First call get_learner_profile() if you need more context about the learner
+2. Use the appropriate generation tool based on what they request:
+ - "flashcards" or "study cards" -> generate_flashcards(topic="...")
+ - "quiz" or "test me" or "practice questions" -> generate_quiz(topic="...")
+ - "podcast" or "audio" or "listen" -> get_audio_content()
+ - "video" or "watch" or "visual" -> get_video_content()
+
+3. For general biology questions (not requesting study materials):
+ - Use get_textbook_content(topic="...") to fetch relevant textbook content
+ - Answer the question using the fetched content
+ - Always cite the source chapter
+
+4. The tools return A2UI JSON which will be rendered as interactive components
+5. After calling a tool, briefly explain what you've generated
+
+## Content Sources
+
+All flashcards, quizzes, and answers are generated from actual OpenStax textbook content.
+When you answer questions or generate materials, you should mention the source, e.g.:
+"Based on OpenStax Biology Chapter 6.4: ATP - Adenosine Triphosphate..."
+
+## Important Notes
+
+- Always use the learner's preferred analogies (sports/gym for Maria)
+- Focus on correcting misconceptions, not just presenting facts
+- Be encouraging and supportive
+- The A2UI components are rendered automatically - just call the tools
+- Include the topic parameter when generating flashcards or quizzes
+
+## A2UI Format
+
+The tools return A2UI JSON that follows this structure:
+- beginRendering: Starts a new UI surface
+- surfaceUpdate: Contains component definitions
+- Components include: Card, Column, Row, Text, Flashcard, QuizCard, AudioPlayer, Video
+
+You don't need to understand the A2UI format in detail - just use the tools
+and explain the content to the learner.
+"""
+
+# ============================================================================
+# AGENT FACTORY FOR AGENT ENGINE DEPLOYMENT
+# ============================================================================
+# Agent Engine requires a class that creates the agent on the SERVER,
+# not a pre-instantiated agent object. This avoids serialization issues
+# with live objects (connections, locks, etc).
+
+
+def create_agent() -> Agent:
+ """Factory function to create the ADK agent.
+
+ This is called on the server side after deployment, avoiding
+ serialization of live objects.
+ """
+ return Agent(
+ name="personalized_learning_agent",
+ model=MODEL_ID,
+ instruction=SYSTEM_PROMPT,
+ tools=[
+ generate_flashcards,
+ generate_quiz,
+ get_audio_content,
+ get_video_content,
+ get_learner_profile,
+ get_textbook_content,
+ ],
+ )
+
+
+# For local development with `adk web`, we still need a module-level agent
+# This is only instantiated when running locally, not during deployment
+root_agent = create_agent()
+
+
+# ============================================================================
+# SERVER-SIDE AGENT WRAPPER FOR AGENT ENGINE DEPLOYMENT
+# ============================================================================
+# This wrapper class enables lazy initialization - the agent is created
+# on the server side after deployment, avoiding serialization of live objects.
+
+
+class ServerSideAgent:
+ """
+ Wrapper class for Agent Engine deployment using ReasoningEngine pattern.
+
+ This class is COMPLETELY SELF-CONTAINED - it does not import from the
+ 'agent' package to avoid module resolution issues during unpickling.
+ All agent creation logic is inlined here.
+
+ Usage:
+ reasoning_engines.ReasoningEngine.create(
+ ServerSideAgent, # Pass the CLASS, not an instance
+ requirements=[...],
+ )
+ """
+
+ def __init__(self):
+ """Initialize the agent on the server side."""
+ # ALL imports happen inside __init__ to avoid capture during pickling
+ import os
+ from google.adk.agents import Agent
+ from vertexai.agent_engines import AdkApp
+
+ # Model configuration
+ model_id = os.getenv("GENAI_MODEL", "gemini-2.5-flash")
+
+ # Create a simple agent with basic instruction
+ # Tools would need to be defined inline here too to avoid imports
+ self.agent = Agent(
+ name="personalized_learning_agent",
+ model=model_id,
+ instruction="""You are a personalized learning assistant that helps students study biology.
+
+You can help students understand concepts like ATP, cellular respiration, and bond energy.
+Use sports and gym analogies when explaining concepts.
+
+When asked for flashcards or quizzes, explain that this feature requires the full agent deployment.
+For now, you can have a helpful conversation about biology topics.""",
+ tools=[], # No tools for now - keep it simple
+ )
+
+ # Wrap in AdkApp for session management and tracing
+ self.app = AdkApp(agent=self.agent, enable_tracing=True)
+
+ def query(self, *, user_id: str, message: str, **kwargs):
+ """
+ Handle a query from the user.
+
+ This method signature matches what ReasoningEngine expects.
+ """
+ return self.app.query(user_id=user_id, message=message, **kwargs)
+
+ async def aquery(self, *, user_id: str, message: str, **kwargs):
+ """
+ Handle an async query from the user.
+ """
+ return await self.app.aquery(user_id=user_id, message=message, **kwargs)
+
+ def stream_query(self, *, user_id: str, message: str, **kwargs):
+ """
+ Handle a streaming query from the user.
+ """
+ return self.app.stream_query(user_id=user_id, message=message, **kwargs)
+
+
+# ============================================================================
+# LEGACY COMPATIBILITY (for server.py)
+# ============================================================================
+
+class LearningMaterialAgent:
+ """
+ Legacy wrapper for backwards compatibility with server.py.
+
+ This class wraps the ADK agent's tools to maintain the same interface
+ that server.py expects.
+ """
+
+ SUPPORTED_FORMATS = SUPPORTED_FORMATS
+
+ def __init__(self, init_client: bool = True):
+ self._init_client = init_client
+
+ async def generate_content(
+ self,
+ format_type: str,
+ additional_context: str = "",
+ ) -> dict[str, Any]:
+ """Generate content using the appropriate tool."""
+ # Create a minimal tool context (duck-typed to match ToolContext interface)
+ class MinimalToolContext:
+ def __init__(self):
+ self.state = {}
+
+ ctx = MinimalToolContext()
+
+ format_lower = format_type.lower()
+
+ if format_lower == "flashcards":
+ return await generate_flashcards(ctx, additional_context or None)
+ elif format_lower == "quiz":
+ return await generate_quiz(ctx, additional_context or None)
+ elif format_lower in ["audio", "podcast"]:
+ return await get_audio_content(ctx)
+ elif format_lower == "video":
+ return await get_video_content(ctx)
+ else:
+ return {
+ "error": f"Unsupported format: {format_type}",
+ "supported_formats": SUPPORTED_FORMATS,
+ }
+
+ async def stream(self, request: str, session_id: str = "default"):
+ """Stream response for A2A compatibility."""
+ parts = request.split(":", 1)
+ format_type = parts[0].strip().lower()
+ additional_context = parts[1].strip() if len(parts) > 1 else ""
+
+ yield {
+ "is_task_complete": False,
+ "updates": f"Generating {format_type}...",
+ }
+
+ result = await self.generate_content(format_type, additional_context)
+
+ yield {
+ "is_task_complete": True,
+ "content": result,
+ }
+
+
+# Singleton for backwards compatibility
+_agent_instance = None
+
+
+def get_agent() -> LearningMaterialAgent:
+ """Get or create the legacy agent wrapper singleton."""
+ global _agent_instance
+ if _agent_instance is None:
+ _agent_instance = LearningMaterialAgent()
+ return _agent_instance
diff --git a/samples/personalized_learning/agent/context_loader.py b/samples/personalized_learning/agent/context_loader.py
new file mode 100644
index 00000000..e9304059
--- /dev/null
+++ b/samples/personalized_learning/agent/context_loader.py
@@ -0,0 +1,137 @@
+"""
+Context Loader for Learner Profile Data
+
+Loads learner context from GCS or local filesystem.
+This simulates the handoff from an upstream personalization pipeline.
+"""
+
+import os
+import logging
+from typing import Optional
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+# GCS configuration
+GCS_BUCKET = os.getenv("GCS_CONTEXT_BUCKET", "a2ui-demo-context")
+GCS_PREFIX = os.getenv("GCS_CONTEXT_PREFIX", "learner_context/")
+
+# Local fallback path (relative to sample root)
+LOCAL_CONTEXT_PATH = Path(__file__).parent.parent / "learner_context"
+
+
+def _load_from_gcs(filename: str) -> Optional[str]:
+ """Load a context file from GCS."""
+ try:
+ from google.cloud import storage
+
+ client = storage.Client()
+ bucket = client.bucket(GCS_BUCKET)
+ blob = bucket.blob(f"{GCS_PREFIX}{filename}")
+
+ if blob.exists():
+ content = blob.download_as_text()
+ logger.info(f"Loaded {filename} from GCS bucket {GCS_BUCKET}")
+ return content
+ else:
+ logger.warning(f"File {filename} not found in GCS bucket {GCS_BUCKET}")
+ return None
+
+ except Exception as e:
+ logger.warning(f"Failed to load from GCS: {e}")
+ return None
+
+
+def _load_from_local(filename: str) -> Optional[str]:
+ """Load a context file from local filesystem."""
+ filepath = LOCAL_CONTEXT_PATH / filename
+
+ if filepath.exists():
+ content = filepath.read_text()
+ logger.info(f"Loaded {filename} from local path {filepath}")
+ return content
+ else:
+ logger.warning(f"File {filename} not found at local path {filepath}")
+ return None
+
+
+def load_context_file(filename: str) -> Optional[str]:
+ """
+ Load a context file with fallback chain: local files → GCS.
+
+ Priority order:
+ 1. Local files (for development with adk web)
+ 2. GCS bucket (for Agent Engine deployment)
+
+ This matches the fallback order in agent.py's _safe_get_combined_context().
+
+ Args:
+ filename: Name of the context file (e.g., "01_maria_learner_profile.txt")
+
+ Returns:
+ File content as string, or None if not found
+ """
+ # Try local files first (for local development)
+ content = _load_from_local(filename)
+ if content:
+ return content
+
+ # Fall back to GCS (for Agent Engine)
+ return _load_from_gcs(filename)
+
+
+def load_all_context() -> dict[str, str]:
+ """
+ Load all misconception vector context files.
+
+ Returns:
+ Dictionary mapping filename to content
+ """
+ context_files = [
+ "01_maria_learner_profile.txt",
+ "02_chemistry_bond_energy.txt",
+ "03_chemistry_thermodynamics.txt",
+ "04_biology_atp_cellular_respiration.txt",
+ "05_misconception_resolution.txt",
+ "06_mcat_practice_concepts.txt",
+ ]
+
+ context = {}
+ for filename in context_files:
+ content = load_context_file(filename)
+ if content:
+ context[filename] = content
+
+ logger.info(f"Loaded {len(context)} context files")
+ return context
+
+
+def get_learner_profile() -> Optional[str]:
+ """Get the learner profile context."""
+ return load_context_file("01_maria_learner_profile.txt")
+
+
+def get_misconception_context() -> Optional[str]:
+ """Get the misconception resolution context."""
+ return load_context_file("05_misconception_resolution.txt")
+
+
+def get_mcat_concepts() -> Optional[str]:
+ """Get the MCAT practice concepts."""
+ return load_context_file("06_mcat_practice_concepts.txt")
+
+
+def get_combined_context() -> str:
+ """
+ Get all context combined into a single string for prompting.
+
+ Returns:
+ Combined context string
+ """
+ all_context = load_all_context()
+
+ combined = []
+ for filename, content in sorted(all_context.items()):
+ combined.append(f"=== {filename} ===\n{content}\n")
+
+ return "\n".join(combined)
diff --git a/samples/personalized_learning/agent/download_openstax.py b/samples/personalized_learning/agent/download_openstax.py
new file mode 100644
index 00000000..f842c765
--- /dev/null
+++ b/samples/personalized_learning/agent/download_openstax.py
@@ -0,0 +1,309 @@
+#!/usr/bin/env python3
+"""
+Download OpenStax Biology Modules to GCS
+
+This script clones the OpenStax Biology repository from GitHub and uploads
+the module CNXML files to a GCS bucket for faster runtime access.
+
+Uses git clone with shallow clone (--depth 1) for efficient downloading,
+then uploads the modules directory to GCS.
+
+Usage:
+ python download_openstax.py --bucket YOUR_BUCKET_NAME
+ python download_openstax.py --bucket YOUR_BUCKET_NAME --local-dir ./modules # Also save locally
+ python download_openstax.py --local-only --local-dir ./modules # Save locally only
+ python download_openstax.py --list # List modules that would be downloaded
+"""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+# Add parent directory to path for imports
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from openstax_chapters import get_all_module_ids
+
+# Configuration
+GITHUB_REPO = "https://github.com/openstax/osbooks-biology-bundle.git"
+DEFAULT_PREFIX = "openstax_modules/"
+
+
+def check_git_available() -> bool:
+ """Check if git is available on the system."""
+ try:
+ subprocess.run(
+ ["git", "--version"],
+ capture_output=True,
+ check=True,
+ )
+ return True
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return False
+
+
+def clone_repo(target_dir: str) -> bool:
+ """
+ Clone the OpenStax repository with shallow clone.
+
+ Args:
+ target_dir: Directory to clone into
+
+ Returns:
+ True if successful, False otherwise
+ """
+ print(f"Cloning repository (shallow clone)...")
+ print(f" Source: {GITHUB_REPO}")
+ print(f" Target: {target_dir}")
+
+ try:
+ subprocess.run(
+ ["git", "clone", "--depth", "1", GITHUB_REPO, target_dir],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ print(" Clone completed successfully")
+ return True
+ except subprocess.CalledProcessError as e:
+ print(f" Clone failed: {e.stderr}")
+ return False
+
+
+def upload_modules_to_gcs(
+ modules_dir: Path,
+ bucket_name: str,
+ prefix: str,
+ module_ids: set[str],
+ workers: int = 10,
+) -> tuple[int, int]:
+ """
+ Upload module directories to GCS.
+
+ Args:
+ modules_dir: Local path to modules directory
+ bucket_name: GCS bucket name
+ prefix: GCS prefix for uploads
+ module_ids: Set of module IDs to upload (filters what gets uploaded)
+ workers: Number of parallel upload workers
+
+ Returns:
+ Tuple of (success_count, fail_count)
+ """
+ try:
+ from google.cloud import storage
+ except ImportError:
+ print("ERROR: google-cloud-storage not installed")
+ print(" Run: pip install google-cloud-storage")
+ return 0, len(module_ids)
+
+ from concurrent.futures import ThreadPoolExecutor, as_completed
+
+ client = storage.Client()
+ bucket = client.bucket(bucket_name)
+
+ success_count = 0
+ fail_count = 0
+ total = len(module_ids)
+
+ def upload_module(module_id: str) -> tuple[str, bool, str]:
+ """Upload a single module. Returns (module_id, success, message)."""
+ module_path = modules_dir / module_id / "index.cnxml"
+
+ if not module_path.exists():
+ return module_id, False, "not found in cloned repo"
+
+ try:
+ blob = bucket.blob(f"{prefix}{module_id}/index.cnxml")
+ blob.upload_from_filename(str(module_path), content_type="application/xml")
+ return module_id, True, "uploaded"
+ except Exception as e:
+ return module_id, False, str(e)
+
+ # Use thread pool for parallel uploads
+ with ThreadPoolExecutor(max_workers=workers) as executor:
+ futures = {executor.submit(upload_module, mid): mid for mid in sorted(module_ids)}
+
+ for future in as_completed(futures):
+ module_id, success, message = future.result()
+ if success:
+ success_count += 1
+ print(f" Uploaded {module_id} ({success_count}/{total})")
+ else:
+ fail_count += 1
+ print(f" ! {module_id}: {message}")
+
+ return success_count, fail_count
+
+
+def copy_modules_locally(
+ modules_dir: Path,
+ local_dir: Path,
+ module_ids: set[str],
+) -> tuple[int, int]:
+ """
+ Copy module directories to a local directory.
+
+ Args:
+ modules_dir: Source modules directory from clone
+ local_dir: Target local directory
+ module_ids: Set of module IDs to copy
+
+ Returns:
+ Tuple of (success_count, fail_count)
+ """
+ local_dir.mkdir(parents=True, exist_ok=True)
+
+ success_count = 0
+ fail_count = 0
+ total = len(module_ids)
+
+ for module_id in sorted(module_ids):
+ src_path = modules_dir / module_id
+ dst_path = local_dir / module_id
+
+ if not src_path.exists():
+ print(f" ! {module_id}: not found in cloned repo")
+ fail_count += 1
+ continue
+
+ try:
+ if dst_path.exists():
+ shutil.rmtree(dst_path)
+ shutil.copytree(src_path, dst_path)
+ success_count += 1
+ print(f" Copied {module_id} ({success_count}/{total})")
+ except Exception as e:
+ print(f" Failed {module_id}: {e}")
+ fail_count += 1
+
+ return success_count, fail_count
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Download OpenStax Biology modules to GCS using git clone"
+ )
+ parser.add_argument(
+ "--bucket",
+ type=str,
+ default=os.getenv("GCS_OPENSTAX_BUCKET"),
+ help="GCS bucket name for storing modules",
+ )
+ parser.add_argument(
+ "--prefix",
+ type=str,
+ default=DEFAULT_PREFIX,
+ help=f"GCS prefix for module files (default: {DEFAULT_PREFIX})",
+ )
+ parser.add_argument(
+ "--local-only",
+ action="store_true",
+ help="Only save locally, don't upload to GCS",
+ )
+ parser.add_argument(
+ "--local-dir",
+ type=str,
+ default=None,
+ help="Local directory to save modules (optional)",
+ )
+ parser.add_argument(
+ "--list",
+ action="store_true",
+ help="List modules that would be downloaded, don't download",
+ )
+ parser.add_argument(
+ "--workers",
+ type=int,
+ default=10,
+ help="Number of parallel workers for GCS uploads (default: 10)",
+ )
+
+ args = parser.parse_args()
+
+ # Get all unique module IDs we need
+ all_modules = get_all_module_ids()
+ print(f"Found {len(all_modules)} unique modules across all chapters")
+
+ if args.list:
+ print("\nModules to download:")
+ for module_id in sorted(all_modules):
+ print(f" {module_id}")
+ print(f"\nTotal: {len(all_modules)} modules")
+ return
+
+ if not args.local_only and not args.bucket:
+ print("ERROR: --bucket is required unless using --local-only")
+ sys.exit(1)
+
+ if args.local_only and not args.local_dir:
+ print("ERROR: --local-dir is required when using --local-only")
+ sys.exit(1)
+
+ # Check git is available
+ if not check_git_available():
+ print("ERROR: git is not available on this system")
+ print(" Please install git to use this script")
+ sys.exit(1)
+
+ # Clone into a temporary directory
+ with tempfile.TemporaryDirectory() as tmpdir:
+ print(f"\nUsing temporary directory: {tmpdir}")
+
+ # Clone the repository
+ if not clone_repo(tmpdir):
+ print("ERROR: Failed to clone repository")
+ sys.exit(1)
+
+ modules_dir = Path(tmpdir) / "modules"
+
+ if not modules_dir.exists():
+ print(f"ERROR: modules directory not found at {modules_dir}")
+ sys.exit(1)
+
+ # Count available modules
+ available_modules = {d.name for d in modules_dir.iterdir() if d.is_dir()}
+ all_modules_set = set(all_modules) # Convert list to set for set operations
+ needed_modules = all_modules_set & available_modules
+ missing_modules = all_modules_set - available_modules
+
+ print(f"\nModule status:")
+ print(f" Needed: {len(all_modules)}")
+ print(f" Available in repo: {len(needed_modules)}")
+ if missing_modules:
+ print(f" Missing from repo: {len(missing_modules)}")
+ for m in sorted(missing_modules)[:5]:
+ print(f" - {m}")
+ if len(missing_modules) > 5:
+ print(f" ... and {len(missing_modules) - 5} more")
+
+ # Copy locally if requested
+ if args.local_dir:
+ local_dir = Path(args.local_dir)
+ print(f"\nCopying modules to {local_dir}...")
+ local_success, local_fail = copy_modules_locally(
+ modules_dir, local_dir, needed_modules
+ )
+ print(f"Local copy complete: {local_success} succeeded, {local_fail} failed")
+
+ # Upload to GCS if not local-only
+ if not args.local_only and args.bucket:
+ print(f"\nUploading to gs://{args.bucket}/{args.prefix}...")
+ print(f"Using {args.workers} parallel workers...")
+ gcs_success, gcs_fail = upload_modules_to_gcs(
+ modules_dir, args.bucket, args.prefix, needed_modules, args.workers
+ )
+ print(f"\nUpload complete: {gcs_success} succeeded, {gcs_fail} failed")
+ print(f"Modules available at: gs://{args.bucket}/{args.prefix}")
+
+ # Temp directory is automatically cleaned up here
+ print("\nTemporary files cleaned up")
+ print("Done!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/personalized_learning/agent/openstax_chapters.py b/samples/personalized_learning/agent/openstax_chapters.py
new file mode 100644
index 00000000..a87d308b
--- /dev/null
+++ b/samples/personalized_learning/agent/openstax_chapters.py
@@ -0,0 +1,621 @@
+"""
+Complete OpenStax Biology AP Courses Chapter Index
+
+This module provides a comprehensive mapping of all chapters in the OpenStax
+Biology for AP Courses textbook, along with intelligent topic matching.
+
+Content is sourced from the OpenStax GitHub repository:
+https://github.com/openstax/osbooks-biology-bundle
+
+The module IDs (e.g., m62767) correspond to CNXML files in the modules/ directory.
+"""
+
+# GitHub raw content base URL for fetching module content
+GITHUB_RAW_BASE = "https://raw.githubusercontent.com/openstax/osbooks-biology-bundle/main/modules"
+
+# OpenStax website base URL for citations
+OPENSTAX_WEB_BASE = "https://openstax.org/books/biology-ap-courses/pages"
+
+# Complete chapter list with human-readable titles
+OPENSTAX_CHAPTERS = {
+ # Unit 1: The Chemistry of Life
+ "1-1-the-science-of-biology": "The Science of Biology",
+ "1-2-themes-and-concepts-of-biology": "Themes and Concepts of Biology",
+ "2-1-atoms-isotopes-ions-and-molecules-the-building-blocks": "Atoms, Isotopes, Ions, and Molecules: The Building Blocks",
+ "2-2-water": "Water",
+ "2-3-carbon": "Carbon",
+ "3-1-synthesis-of-biological-macromolecules": "Synthesis of Biological Macromolecules",
+ "3-2-carbohydrates": "Carbohydrates",
+ "3-3-lipids": "Lipids",
+ "3-4-proteins": "Proteins",
+ "3-5-nucleic-acids": "Nucleic Acids",
+
+ # Unit 2: The Cell
+ "4-1-studying-cells": "Studying Cells",
+ "4-2-prokaryotic-cells": "Prokaryotic Cells",
+ "4-3-eukaryotic-cells": "Eukaryotic Cells",
+ "4-4-the-endomembrane-system-and-proteins": "The Endomembrane System and Proteins",
+ "4-5-cytoskeleton": "Cytoskeleton",
+ "4-6-connections-between-cells-and-cellular-activities": "Connections Between Cells and Cellular Activities",
+ "5-1-components-and-structure": "Cell Membrane Components and Structure",
+ "5-2-passive-transport": "Passive Transport",
+ "5-3-active-transport": "Active Transport",
+ "5-4-bulk-transport": "Bulk Transport",
+ "6-1-energy-and-metabolism": "Energy and Metabolism",
+ "6-2-potential-kinetic-free-and-activation-energy": "Potential, Kinetic, Free, and Activation Energy",
+ "6-3-the-laws-of-thermodynamics": "The Laws of Thermodynamics",
+ "6-4-atp-adenosine-triphosphate": "ATP: Adenosine Triphosphate",
+ "6-5-enzymes": "Enzymes",
+ "7-1-energy-in-living-systems": "Energy in Living Systems",
+ "7-2-glycolysis": "Glycolysis",
+ "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle": "Oxidation of Pyruvate and the Citric Acid Cycle",
+ "7-4-oxidative-phosphorylation": "Oxidative Phosphorylation",
+ "7-5-metabolism-without-oxygen": "Metabolism Without Oxygen",
+ "7-6-connections-of-carbohydrate-protein-and-lipid-metabolic-pathways": "Connections of Carbohydrate, Protein, and Lipid Metabolic Pathways",
+ "7-7-regulation-of-cellular-respiration": "Regulation of Cellular Respiration",
+ "8-1-overview-of-photosynthesis": "Overview of Photosynthesis",
+ "8-2-the-light-dependent-reaction-of-photosynthesis": "The Light-Dependent Reactions of Photosynthesis",
+ "8-3-using-light-to-make-organic-molecules": "Using Light to Make Organic Molecules",
+ "9-1-signaling-molecules-and-cellular-receptors": "Signaling Molecules and Cellular Receptors",
+ "9-2-propagation-of-the-signal": "Propagation of the Signal",
+ "9-3-response-to-the-signal": "Response to the Signal",
+ "9-4-signaling-in-single-celled-organisms": "Signaling in Single-Celled Organisms",
+ "10-1-cell-division": "Cell Division",
+ "10-2-the-cell-cycle": "The Cell Cycle",
+ "10-3-control-of-the-cell-cycle": "Control of the Cell Cycle",
+ "10-4-cancer-and-the-cell-cycle": "Cancer and the Cell Cycle",
+ "10-5-prokaryotic-cell-division": "Prokaryotic Cell Division",
+
+ # Unit 3: Genetics
+ "11-1-the-process-of-meiosis": "The Process of Meiosis",
+ "11-2-sexual-reproduction": "Sexual Reproduction",
+ "12-1-mendels-experiments-and-the-laws-of-probability": "Mendel's Experiments and the Laws of Probability",
+ "12-2-characteristics-and-traits": "Characteristics and Traits",
+ "12-3-laws-of-inheritance": "Laws of Inheritance",
+ "13-1-chromosomal-theory-and-genetic-linkages": "Chromosomal Theory and Genetic Linkages",
+ "13-2-chromosomal-basis-of-inherited-disorders": "Chromosomal Basis of Inherited Disorders",
+ "14-1-historical-basis-of-modern-understanding": "Historical Basis of Modern Understanding of DNA",
+ "14-2-dna-structure-and-sequencing": "DNA Structure and Sequencing",
+ "14-3-basics-of-dna-replication": "Basics of DNA Replication",
+ "14-4-dna-replication-in-prokaryotes": "DNA Replication in Prokaryotes",
+ "14-5-dna-replication-in-eukaryotes": "DNA Replication in Eukaryotes",
+ "14-6-dna-repair": "DNA Repair",
+ "15-1-the-genetic-code": "The Genetic Code",
+ "15-2-prokaryotic-transcription": "Prokaryotic Transcription",
+ "15-3-eukaryotic-transcription": "Eukaryotic Transcription",
+ "15-4-rna-processing-in-eukaryotes": "RNA Processing in Eukaryotes",
+ "15-5-ribosomes-and-protein-synthesis": "Ribosomes and Protein Synthesis",
+ "16-1-regulation-of-gene-expression": "Regulation of Gene Expression",
+ "16-2-prokaryotic-gene-regulation": "Prokaryotic Gene Regulation",
+ "16-3-eukaryotic-epigenetic-gene-regulation": "Eukaryotic Epigenetic Gene Regulation",
+ "16-4-eukaryotic-transcriptional-gene-regulation": "Eukaryotic Transcriptional Gene Regulation",
+ "16-5-eukaryotic-post-transcriptional-gene-regulation": "Eukaryotic Post-transcriptional Gene Regulation",
+ "16-6-eukaryotic-translational-and-post-translational-gene-regulation": "Eukaryotic Translational and Post-translational Gene Regulation",
+ "16-7-cancer-and-gene-regulation": "Cancer and Gene Regulation",
+ "17-1-biotechnology": "Biotechnology",
+ "17-2-mapping-genomes": "Mapping Genomes",
+ "17-3-whole-genome-sequencing": "Whole-Genome Sequencing",
+ "17-4-applying-genomics": "Applying Genomics",
+ "17-5-genomics-and-proteomics": "Genomics and Proteomics",
+
+ # Unit 4: Evolutionary Processes
+ "18-1-understanding-evolution": "Understanding Evolution",
+ "18-2-formation-of-new-species": "Formation of New Species",
+ "18-3-reconnection-and-rates-of-speciation": "Reconnection and Rates of Speciation",
+ "19-1-population-evolution": "Population Evolution",
+ "19-2-population-genetics": "Population Genetics",
+ "19-3-adaptive-evolution": "Adaptive Evolution",
+ "20-1-organizing-life-on-earth": "Organizing Life on Earth",
+ "20-2-determining-evolutionary-relationships": "Determining Evolutionary Relationships",
+ "20-3-perspectives-on-the-phylogenetic-tree": "Perspectives on the Phylogenetic Tree",
+
+ # Unit 5: Biological Diversity
+ "21-1-viral-evolution-morphology-and-classification": "Viral Evolution, Morphology, and Classification",
+ "21-2-virus-infection-and-hosts": "Virus Infection and Hosts",
+ "21-3-prevention-and-treatment-of-viral-infections": "Prevention and Treatment of Viral Infections",
+ "21-4-other-acellular-entities-prions-and-viroids": "Other Acellular Entities: Prions and Viroids",
+ "22-1-prokaryotic-diversity": "Prokaryotic Diversity",
+ "22-2-structure-of-prokaryotes": "Structure of Prokaryotes",
+ "22-3-prokaryotic-metabolism": "Prokaryotic Metabolism",
+ "22-4-bacterial-diseases-in-humans": "Bacterial Diseases in Humans",
+ "22-5-beneficial-prokaryotes": "Beneficial Prokaryotes",
+
+ # Unit 6: Plant Structure and Function
+ "23-1-the-plant-body": "The Plant Body",
+ "23-2-stems": "Stems",
+ "23-3-roots": "Roots",
+ "23-4-leaves": "Leaves",
+ "23-5-transport-of-water-and-solutes-in-plants": "Transport of Water and Solutes in Plants",
+ "23-6-plant-sensory-systems-and-responses": "Plant Sensory Systems and Responses",
+
+ # Unit 7: Animal Structure and Function
+ "24-1-animal-form-and-function": "Animal Form and Function",
+ "24-2-animal-primary-tissues": "Animal Primary Tissues",
+ "24-3-homeostasis": "Homeostasis",
+ "25-1-digestive-systems": "Digestive Systems",
+ "25-2-nutrition-and-energy-production": "Nutrition and Energy Production",
+ "25-3-digestive-system-processes": "Digestive System Processes",
+ "25-4-digestive-system-regulation": "Digestive System Regulation",
+ "26-1-neurons-and-glial-cells": "Neurons and Glial Cells",
+ "26-2-how-neurons-communicate": "How Neurons Communicate",
+ "26-3-the-central-nervous-system": "The Central Nervous System",
+ "26-4-the-peripheral-nervous-system": "The Peripheral Nervous System",
+ "26-5-nervous-system-disorders": "Nervous System Disorders",
+ "27-1-sensory-processes": "Sensory Processes",
+ "27-2-somatosensation": "Somatosensation",
+ "27-3-taste-and-smell": "Taste and Smell",
+ "27-4-hearing-and-vestibular-sensation": "Hearing and Vestibular Sensation",
+ "27-5-vision": "Vision",
+ "28-1-types-of-hormones": "Types of Hormones",
+ "28-2-how-hormones-work": "How Hormones Work",
+ "28-3-regulation-of-body-processes": "Regulation of Body Processes",
+ "28-4-regulation-of-hormone-production": "Regulation of Hormone Production",
+ "28-5-endocrine-glands": "Endocrine Glands",
+ "29-1-types-of-skeletal-systems": "Types of Skeletal Systems",
+ "29-2-bone": "Bone",
+ "29-3-joints-and-skeletal-movement": "Joints and Skeletal Movement",
+ "29-4-muscle-contraction-and-locomotion": "Muscle Contraction and Locomotion",
+ "30-1-systems-of-gas-exchange": "Systems of Gas Exchange",
+ "30-2-gas-exchange-across-respiratory-surfaces": "Gas Exchange Across Respiratory Surfaces",
+ "30-3-breathing": "Breathing",
+ "30-4-transport-of-gases-in-human-bodily-fluids": "Transport of Gases in Human Bodily Fluids",
+ "31-1-overview-of-the-circulatory-system": "Overview of the Circulatory System",
+ "31-2-components-of-the-blood": "Components of the Blood",
+ "31-3-mammalian-heart-and-blood-vessels": "Mammalian Heart and Blood Vessels",
+ "31-4-blood-flow-and-blood-pressure-regulation": "Blood Flow and Blood Pressure Regulation",
+ "32-1-osmoregulation-and-osmotic-balance": "Osmoregulation and Osmotic Balance",
+ "32-2-the-kidneys-and-osmoregulatory-organs": "The Kidneys and Osmoregulatory Organs",
+ "32-3-excretion-systems": "Excretion Systems",
+ "32-4-nitrogenous-wastes": "Nitrogenous Wastes",
+ "32-5-hormonal-control-of-osmoregulatory-functions": "Hormonal Control of Osmoregulatory Functions",
+ "33-1-innate-immune-response": "Innate Immune Response",
+ "33-2-adaptive-immune-response": "Adaptive Immune Response",
+ "33-3-antibodies": "Antibodies",
+ "33-4-disruptions-in-the-immune-system": "Disruptions in the Immune System",
+ "34-1-reproduction-methods": "Reproduction Methods",
+ "34-2-fertilization": "Fertilization",
+ "34-3-human-reproductive-anatomy-and-gametogenesis": "Human Reproductive Anatomy and Gametogenesis",
+ "34-4-hormonal-control-of-human-reproduction": "Hormonal Control of Human Reproduction",
+ "34-5-fertilization-and-early-embryonic-development": "Fertilization and Early Embryonic Development",
+ "34-6-organogenesis-and-vertebrate-axis-formation": "Organogenesis and Vertebrate Axis Formation",
+ "34-7-human-pregnancy-and-birth": "Human Pregnancy and Birth",
+
+ # Unit 8: Ecology
+ "35-1-the-scope-of-ecology": "The Scope of Ecology",
+ "35-2-biogeography": "Biogeography",
+ "35-3-terrestrial-biomes": "Terrestrial Biomes",
+ "35-4-aquatic-biomes": "Aquatic Biomes",
+ "35-5-climate-and-the-effects-of-global-climate-change": "Climate and the Effects of Global Climate Change",
+ "36-1-population-demography": "Population Demography",
+ "36-2-life-histories-and-natural-selection": "Life Histories and Natural Selection",
+ "36-3-environmental-limits-to-population-growth": "Environmental Limits to Population Growth",
+ "36-4-population-dynamics-and-regulation": "Population Dynamics and Regulation",
+ "36-5-human-population-growth": "Human Population Growth",
+ "36-6-community-ecology": "Community Ecology",
+ "36-7-behavioral-biology-proximate-and-ultimate-causes-of-behavior": "Behavioral Biology: Proximate and Ultimate Causes of Behavior",
+ "37-1-ecology-for-ecosystems": "Ecology for Ecosystems",
+ "37-2-energy-flow-through-ecosystems": "Energy Flow Through Ecosystems",
+ "37-3-biogeochemical-cycles": "Biogeochemical Cycles",
+ "38-1-the-biodiversity-crisis": "The Biodiversity Crisis",
+ "38-2-the-importance-of-biodiversity-to-human-life": "The Importance of Biodiversity to Human Life",
+ "38-3-threats-to-biodiversity": "Threats to Biodiversity",
+ "38-4-preserving-biodiversity": "Preserving Biodiversity",
+}
+
+# Build a formatted string for LLM context
+def get_chapter_list_for_llm() -> str:
+ """Return a formatted list of all chapters for LLM context."""
+ lines = []
+ for slug, title in OPENSTAX_CHAPTERS.items():
+ lines.append(f"- {slug}: {title}")
+ return "\n".join(lines)
+
+
+# Pre-computed keyword hints for faster matching (optional optimization)
+# These keywords avoid expensive LLM calls for common biology topics.
+# EXPANDED: Added more synonyms and common phrasings to reduce LLM fallback frequency.
+KEYWORD_HINTS = {
+ # Energy & Metabolism - EXPANDED with common variations
+ "atp": ["6-4-atp-adenosine-triphosphate", "6-1-energy-and-metabolism"],
+ "adenosine triphosphate": ["6-4-atp-adenosine-triphosphate"],
+ "adp": ["6-4-atp-adenosine-triphosphate"],
+ "adenosine diphosphate": ["6-4-atp-adenosine-triphosphate"],
+ "cellular energy": ["6-4-atp-adenosine-triphosphate", "7-1-energy-in-living-systems"],
+ "cell energy": ["6-4-atp-adenosine-triphosphate", "7-1-energy-in-living-systems"],
+ "high energy bond": ["6-4-atp-adenosine-triphosphate"],
+ "phosphate bond": ["6-4-atp-adenosine-triphosphate"],
+ "phosphate group": ["6-4-atp-adenosine-triphosphate"],
+ "energy currency": ["6-4-atp-adenosine-triphosphate"],
+ "energy transfer": ["6-4-atp-adenosine-triphosphate", "7-4-oxidative-phosphorylation"],
+ "bond breaking": ["6-4-atp-adenosine-triphosphate"],
+ "bond energy": ["6-4-atp-adenosine-triphosphate", "6-1-energy-and-metabolism"],
+ "hydrolysis": ["6-4-atp-adenosine-triphosphate"],
+ "atp hydrolysis": ["6-4-atp-adenosine-triphosphate"],
+ "exergonic": ["6-2-potential-kinetic-free-and-activation-energy"],
+ "endergonic": ["6-2-potential-kinetic-free-and-activation-energy"],
+ "gibbs free energy": ["6-2-potential-kinetic-free-and-activation-energy"],
+ "thermodynamics": ["6-3-the-laws-of-thermodynamics"],
+ "first law": ["6-3-the-laws-of-thermodynamics"],
+ "second law": ["6-3-the-laws-of-thermodynamics"],
+ "entropy": ["6-3-the-laws-of-thermodynamics"],
+ "photosynthesis": ["8-1-overview-of-photosynthesis", "8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "plants make food": ["8-1-overview-of-photosynthesis"],
+ "chloroplast": ["8-1-overview-of-photosynthesis", "4-3-eukaryotic-cells"],
+ "chlorophyll": ["8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "calvin cycle": ["8-3-using-light-to-make-organic-molecules"],
+ "light reaction": ["8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "cellular respiration": ["7-1-energy-in-living-systems", "7-4-oxidative-phosphorylation"],
+ "glycolysis": ["7-2-glycolysis"],
+ "krebs": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "citric acid": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "tca cycle": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "electron transport": ["7-4-oxidative-phosphorylation"],
+ "oxidative phosphorylation": ["7-4-oxidative-phosphorylation"],
+ "fermentation": ["7-5-metabolism-without-oxygen"],
+ "anaerobic": ["7-5-metabolism-without-oxygen"],
+ "mitochondria": ["7-4-oxidative-phosphorylation", "4-3-eukaryotic-cells"],
+ "mitochondrion": ["7-4-oxidative-phosphorylation", "4-3-eukaryotic-cells"],
+
+ # Cell Division
+ "mitosis": ["10-1-cell-division", "10-2-the-cell-cycle"],
+ "meiosis": ["11-1-the-process-of-meiosis"],
+ "cell cycle": ["10-2-the-cell-cycle", "10-3-control-of-the-cell-cycle"],
+ "cell division": ["10-1-cell-division"],
+ "cancer": ["10-4-cancer-and-the-cell-cycle", "16-7-cancer-and-gene-regulation"],
+
+ # Molecular Biology
+ "dna": ["14-2-dna-structure-and-sequencing", "14-3-basics-of-dna-replication"],
+ "rna": ["15-4-rna-processing-in-eukaryotes", "3-5-nucleic-acids"],
+ "mrna": ["15-4-rna-processing-in-eukaryotes", "15-5-ribosomes-and-protein-synthesis"],
+ "trna": ["15-5-ribosomes-and-protein-synthesis"],
+ "rrna": ["15-5-ribosomes-and-protein-synthesis"],
+ "transcription": ["15-2-prokaryotic-transcription", "15-3-eukaryotic-transcription"],
+ "translation": ["15-5-ribosomes-and-protein-synthesis"],
+ "protein synthesis": ["15-5-ribosomes-and-protein-synthesis"],
+ "protein": ["3-4-proteins", "15-5-ribosomes-and-protein-synthesis"],
+ "enzyme": ["6-5-enzymes"],
+ "gene expression": ["16-1-regulation-of-gene-expression"],
+ "genetic code": ["15-1-the-genetic-code"],
+ "central dogma": ["15-1-the-genetic-code", "15-5-ribosomes-and-protein-synthesis"],
+ "codon": ["15-1-the-genetic-code"],
+ "anticodon": ["15-5-ribosomes-and-protein-synthesis"],
+ "ribosome": ["15-5-ribosomes-and-protein-synthesis", "4-3-eukaryotic-cells"],
+ "replication": ["14-3-basics-of-dna-replication", "14-4-dna-replication-in-prokaryotes"],
+
+ # Cell Structure
+ "cell membrane": ["5-1-components-and-structure"],
+ "plasma membrane": ["5-1-components-and-structure"],
+ "membrane": ["5-1-components-and-structure", "5-2-passive-transport"],
+ "phospholipid": ["5-1-components-and-structure", "3-3-lipids"],
+ "osmosis": ["5-2-passive-transport", "32-1-osmoregulation-and-osmotic-balance"],
+ "diffusion": ["5-2-passive-transport"],
+ "active transport": ["5-3-active-transport"],
+ "cytoskeleton": ["4-5-cytoskeleton"],
+ "organelle": ["4-3-eukaryotic-cells", "4-4-the-endomembrane-system-and-proteins"],
+ "nucleus": ["4-3-eukaryotic-cells"],
+ "endoplasmic reticulum": ["4-4-the-endomembrane-system-and-proteins"],
+ "golgi": ["4-4-the-endomembrane-system-and-proteins"],
+ "lysosome": ["4-4-the-endomembrane-system-and-proteins"],
+ "vesicle": ["5-4-bulk-transport", "4-4-the-endomembrane-system-and-proteins"],
+ "endocytosis": ["5-4-bulk-transport"],
+ "exocytosis": ["5-4-bulk-transport"],
+ "signal transduction": ["9-1-signaling-molecules-and-cellular-receptors", "9-2-propagation-of-the-signal"],
+ "cell signaling": ["9-1-signaling-molecules-and-cellular-receptors"],
+
+ # Nervous System
+ "neuron": ["26-1-neurons-and-glial-cells", "26-2-how-neurons-communicate"],
+ "nervous system": ["26-1-neurons-and-glial-cells", "26-3-the-central-nervous-system"],
+ "brain": ["26-3-the-central-nervous-system"],
+ "action potential": ["26-2-how-neurons-communicate"],
+ "synapse": ["26-2-how-neurons-communicate"],
+ "senses": ["27-1-sensory-processes"],
+ "vision": ["27-5-vision"],
+ "hearing": ["27-4-hearing-and-vestibular-sensation"],
+
+ # Circulatory System
+ "heart": ["31-1-overview-of-the-circulatory-system", "31-3-mammalian-heart-and-blood-vessels"],
+ "blood": ["31-2-components-of-the-blood", "31-1-overview-of-the-circulatory-system"],
+ "circulatory": ["31-1-overview-of-the-circulatory-system"],
+ "cardiovascular": ["31-1-overview-of-the-circulatory-system"],
+
+ # Immune System
+ "immune": ["33-1-innate-immune-response", "33-2-adaptive-immune-response"],
+ "antibod": ["33-3-antibodies"],
+ "infection": ["33-1-innate-immune-response"],
+ "vaccine": ["33-2-adaptive-immune-response"],
+
+ # Other Body Systems
+ "respiration": ["30-1-systems-of-gas-exchange", "30-3-breathing"],
+ "breathing": ["30-3-breathing"],
+ "lung": ["30-1-systems-of-gas-exchange"],
+ "digestion": ["25-1-digestive-systems", "25-3-digestive-system-processes"],
+ "stomach": ["25-1-digestive-systems"],
+ "intestine": ["25-3-digestive-system-processes"],
+ "hormone": ["28-1-types-of-hormones", "28-2-how-hormones-work", "28-4-regulation-of-hormone-production"],
+ "endocrine": ["28-5-endocrine-glands", "28-1-types-of-hormones", "28-2-how-hormones-work"],
+ "endocrine system": ["28-5-endocrine-glands", "28-1-types-of-hormones", "28-2-how-hormones-work", "28-3-regulation-of-body-processes"],
+ "pituitary": ["28-5-endocrine-glands", "28-4-regulation-of-hormone-production"],
+ "thyroid": ["28-5-endocrine-glands", "28-3-regulation-of-body-processes"],
+ "adrenal": ["28-5-endocrine-glands"],
+ "pancreas": ["28-5-endocrine-glands", "28-3-regulation-of-body-processes"],
+ "insulin": ["28-3-regulation-of-body-processes", "28-5-endocrine-glands"],
+ "gland": ["28-5-endocrine-glands"],
+ "muscle": ["29-4-muscle-contraction-and-locomotion"],
+ "bone": ["29-2-bone"],
+ "skeleton": ["29-1-types-of-skeletal-systems"],
+ "kidney": ["32-2-the-kidneys-and-osmoregulatory-organs"],
+ "excretion": ["32-3-excretion-systems"],
+ "reproduction": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis"],
+ "reproductive": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis"],
+ "reproductive system": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis", "34-4-hormonal-control-of-human-reproduction"],
+ "pregnancy": ["34-7-human-pregnancy-and-birth"],
+ "embryo": ["34-5-fertilization-and-early-embryonic-development"],
+
+ # Evolution & Genetics
+ "evolution": ["18-1-understanding-evolution", "19-1-population-evolution"],
+ "darwin": ["18-1-understanding-evolution"],
+ "natural selection": ["19-3-adaptive-evolution", "36-2-life-histories-and-natural-selection"],
+ "speciation": ["18-2-formation-of-new-species"],
+ "genetics": ["12-1-mendels-experiments-and-the-laws-of-probability", "12-3-laws-of-inheritance"],
+ "mendel": ["12-1-mendels-experiments-and-the-laws-of-probability"],
+ "inheritance": ["12-3-laws-of-inheritance"],
+ "heredity": ["12-3-laws-of-inheritance"],
+ "mutation": ["14-6-dna-repair"],
+ "phylogen": ["20-2-determining-evolutionary-relationships"],
+
+ # Microorganisms
+ "virus": ["21-1-viral-evolution-morphology-and-classification", "21-2-virus-infection-and-hosts"],
+ "bacteria": ["22-1-prokaryotic-diversity", "22-4-bacterial-diseases-in-humans"],
+ "prokaryote": ["4-2-prokaryotic-cells", "22-1-prokaryotic-diversity"],
+ "eukaryote": ["4-3-eukaryotic-cells"],
+
+ # Plants
+ "plant": ["23-1-the-plant-body"],
+ "leaf": ["23-4-leaves"],
+ "root": ["23-3-roots"],
+ "stem": ["23-2-stems"],
+ "xylem": ["23-5-transport-of-water-and-solutes-in-plants"],
+ "phloem": ["23-5-transport-of-water-and-solutes-in-plants"],
+
+ # Ecology
+ "ecology": ["35-1-the-scope-of-ecology", "36-6-community-ecology"],
+ "ecosystem": ["37-1-ecology-for-ecosystems", "37-2-energy-flow-through-ecosystems"],
+ "food chain": ["37-2-energy-flow-through-ecosystems"],
+ "food web": ["37-2-energy-flow-through-ecosystems"],
+ "biome": ["35-3-terrestrial-biomes", "35-4-aquatic-biomes"],
+ "population": ["36-1-population-demography", "36-3-environmental-limits-to-population-growth"],
+ "climate": ["35-5-climate-and-the-effects-of-global-climate-change"],
+ "climate change": ["35-5-climate-and-the-effects-of-global-climate-change"],
+ "biodiversity": ["38-1-the-biodiversity-crisis", "38-4-preserving-biodiversity"],
+ "carbon cycle": ["37-3-biogeochemical-cycles"],
+ "nitrogen cycle": ["37-3-biogeochemical-cycles"],
+
+ # Chemistry Basics
+ "atom": ["2-1-atoms-isotopes-ions-and-molecules-the-building-blocks"],
+ "water": ["2-2-water"],
+ "carbon": ["2-3-carbon"],
+ "carbohydrate": ["3-2-carbohydrates"],
+ "lipid": ["3-3-lipids"],
+ "nucleic acid": ["3-5-nucleic-acids"],
+
+ # Biotechnology
+ "biotechnology": ["17-1-biotechnology"],
+ "crispr": ["17-1-biotechnology"],
+ "cloning": ["17-1-biotechnology"],
+ "genome": ["17-2-mapping-genomes", "17-3-whole-genome-sequencing"],
+ "genomics": ["17-4-applying-genomics", "17-5-genomics-and-proteomics"],
+}
+
+
+# =============================================================================
+# CHAPTER TO MODULE ID MAPPING
+# Maps chapter slugs to their corresponding module IDs from the OpenStax GitHub
+# =============================================================================
+
+CHAPTER_TO_MODULES: dict[str, list[str]] = {
+ # Unit 1: The Chemistry of Life
+ "1-1-the-science-of-biology": ["m62716"],
+ "1-2-themes-and-concepts-of-biology": ["m62717", "m62718"],
+ "2-1-atoms-isotopes-ions-and-molecules-the-building-blocks": ["m62719"],
+ "2-2-water": ["m62720"],
+ "2-3-carbon": ["m62721", "m62722"],
+ "3-1-synthesis-of-biological-macromolecules": ["m62723"],
+ "3-2-carbohydrates": ["m62724"],
+ "3-3-lipids": ["m62726"],
+ "3-4-proteins": ["m62730"],
+ "3-5-nucleic-acids": ["m62733", "m62735"],
+
+ # Unit 2: The Cell
+ "4-1-studying-cells": ["m62736"],
+ "4-2-prokaryotic-cells": ["m62738"],
+ "4-3-eukaryotic-cells": ["m62740"],
+ "4-4-the-endomembrane-system-and-proteins": ["m62742", "m62743"],
+ "4-5-cytoskeleton": ["m62744"],
+ "4-6-connections-between-cells-and-cellular-activities": ["m62746"],
+ "5-1-components-and-structure": ["m62780"],
+ "5-2-passive-transport": ["m62773"],
+ "5-3-active-transport": ["m62753"],
+ "5-4-bulk-transport": ["m62770", "m62772"],
+ "6-1-energy-and-metabolism": ["m62761"],
+ "6-2-potential-kinetic-free-and-activation-energy": ["m62763"],
+ "6-3-the-laws-of-thermodynamics": ["m62764"],
+ "6-4-atp-adenosine-triphosphate": ["m62767"],
+ "6-5-enzymes": ["m62768", "m62778"],
+ "7-1-energy-in-living-systems": ["m62784"],
+ "7-2-glycolysis": ["m62785"],
+ "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle": ["m62786"],
+ "7-4-oxidative-phosphorylation": ["m62787"],
+ "7-5-metabolism-without-oxygen": ["m62788"],
+ "7-6-connections-of-carbohydrate-protein-and-lipid-metabolic-pathways": ["m62789"],
+ "7-7-regulation-of-cellular-respiration": ["m62790", "m62791", "m62792"],
+ "8-1-overview-of-photosynthesis": ["m62793"],
+ "8-2-the-light-dependent-reaction-of-photosynthesis": ["m62794"],
+ "8-3-using-light-to-make-organic-molecules": ["m62795", "m62796"],
+ "9-1-signaling-molecules-and-cellular-receptors": ["m62797"],
+ "9-2-propagation-of-the-signal": ["m62798"],
+ "9-3-response-to-the-signal": ["m62799"],
+ "9-4-signaling-in-single-celled-organisms": ["m62800", "m62801"],
+ "10-1-cell-division": ["m62802"],
+ "10-2-the-cell-cycle": ["m62803"],
+ "10-3-control-of-the-cell-cycle": ["m62804"],
+ "10-4-cancer-and-the-cell-cycle": ["m62805"],
+ "10-5-prokaryotic-cell-division": ["m62806", "m62808"],
+
+ # Unit 3: Genetics
+ "11-1-the-process-of-meiosis": ["m62809"],
+ "11-2-sexual-reproduction": ["m62810", "m62811"],
+ "12-1-mendels-experiments-and-the-laws-of-probability": ["m62812", "m62813"],
+ "12-2-characteristics-and-traits": ["m62817"],
+ "12-3-laws-of-inheritance": ["m62819"],
+ "13-1-chromosomal-theory-and-genetic-linkages": ["m62820"],
+ "13-2-chromosomal-basis-of-inherited-disorders": ["m62821", "m62822"],
+ "14-1-historical-basis-of-modern-understanding": ["m62823"],
+ "14-2-dna-structure-and-sequencing": ["m62824"],
+ "14-3-basics-of-dna-replication": ["m62825"],
+ "14-4-dna-replication-in-prokaryotes": ["m62826"],
+ "14-5-dna-replication-in-eukaryotes": ["m62827", "m62828"],
+ "14-6-dna-repair": ["m62829", "m62830"],
+ "15-1-the-genetic-code": ["m62833"],
+ "15-2-prokaryotic-transcription": ["m62837"],
+ "15-3-eukaryotic-transcription": ["m62838"],
+ "15-4-rna-processing-in-eukaryotes": ["m62840"],
+ "15-5-ribosomes-and-protein-synthesis": ["m62842", "m62843"],
+ "16-1-regulation-of-gene-expression": ["m62844"],
+ "16-2-prokaryotic-gene-regulation": ["m62845"],
+ "16-3-eukaryotic-epigenetic-gene-regulation": ["m62846"],
+ "16-4-eukaryotic-transcriptional-gene-regulation": ["m62847"],
+ "16-5-eukaryotic-post-transcriptional-gene-regulation": ["m62848"],
+ "16-6-eukaryotic-translational-and-post-translational-gene-regulation": ["m62849"],
+ "16-7-cancer-and-gene-regulation": ["m62850", "m62851"],
+ "17-1-biotechnology": ["m62852"],
+ "17-2-mapping-genomes": ["m62853"],
+ "17-3-whole-genome-sequencing": ["m62855"],
+ "17-4-applying-genomics": ["m62857"],
+ "17-5-genomics-and-proteomics": ["m62860", "m62861"],
+
+ # Unit 4: Evolutionary Processes
+ "18-1-understanding-evolution": ["m62862"],
+ "18-2-formation-of-new-species": ["m62863"],
+ "18-3-reconnection-and-rates-of-speciation": ["m62864", "m62865"],
+ "19-1-population-evolution": ["m62866"],
+ "19-2-population-genetics": ["m62867"],
+ "19-3-adaptive-evolution": ["m62868", "m62869"],
+ "20-1-organizing-life-on-earth": ["m62870"],
+ "20-2-determining-evolutionary-relationships": ["m62871"],
+ "20-3-perspectives-on-the-phylogenetic-tree": ["m62872", "m62873"],
+
+ # Unit 5: Biological Diversity
+ "21-1-viral-evolution-morphology-and-classification": ["m62874"],
+ "21-2-virus-infection-and-hosts": ["m62875"],
+ "21-3-prevention-and-treatment-of-viral-infections": ["m62876"],
+ "21-4-other-acellular-entities-prions-and-viroids": ["m62877", "m62878"],
+ "22-1-prokaryotic-diversity": ["m62879"],
+ "22-2-structure-of-prokaryotes": ["m62880"],
+ "22-3-prokaryotic-metabolism": ["m62881"],
+ "22-4-bacterial-diseases-in-humans": ["m62882"],
+ "22-5-beneficial-prokaryotes": ["m62883", "m62884"],
+
+ # Unit 6: Plant Structure and Function
+ "23-1-the-plant-body": ["m62885"],
+ "23-2-stems": ["m62886"],
+ "23-3-roots": ["m62887"],
+ "23-4-leaves": ["m62888"],
+ "23-5-transport-of-water-and-solutes-in-plants": ["m62889"],
+ "23-6-plant-sensory-systems-and-responses": ["m62890", "m62891"],
+
+ # Unit 7: Animal Structure and Function
+ "24-1-animal-form-and-function": ["m62892"],
+ "24-2-animal-primary-tissues": ["m62893"],
+ "24-3-homeostasis": ["m62894", "m62895"],
+ "25-1-digestive-systems": ["m62896"],
+ "25-2-nutrition-and-energy-production": ["m62897"],
+ "25-3-digestive-system-processes": ["m62898"],
+ "25-4-digestive-system-regulation": ["m62899", "m62900"],
+ "26-1-neurons-and-glial-cells": ["m62901"],
+ "26-2-how-neurons-communicate": ["m62902"],
+ "26-3-the-central-nervous-system": ["m62903"],
+ "26-4-the-peripheral-nervous-system": ["m62904"],
+ "26-5-nervous-system-disorders": ["m62905", "m62906"],
+ "27-1-sensory-processes": ["m62907"],
+ "27-2-somatosensation": ["m62908"],
+ "27-3-taste-and-smell": ["m62909"],
+ "27-4-hearing-and-vestibular-sensation": ["m62910"],
+ "27-5-vision": ["m62911", "m62912"],
+ "28-1-types-of-hormones": ["m62913"],
+ "28-2-how-hormones-work": ["m62914"],
+ "28-3-regulation-of-body-processes": ["m62915"],
+ "28-4-regulation-of-hormone-production": ["m62916"],
+ "28-5-endocrine-glands": ["m62917", "m62918"],
+ "29-1-types-of-skeletal-systems": ["m62919"],
+ "29-2-bone": ["m62920"],
+ "29-3-joints-and-skeletal-movement": ["m62921"],
+ "29-4-muscle-contraction-and-locomotion": ["m62922", "m62923"],
+ "30-1-systems-of-gas-exchange": ["m62924"],
+ "30-2-gas-exchange-across-respiratory-surfaces": ["m62925"],
+ "30-3-breathing": ["m62926"],
+ "30-4-transport-of-gases-in-human-bodily-fluids": ["m62927", "m62928"],
+ "31-1-overview-of-the-circulatory-system": ["m62929"],
+ "31-2-components-of-the-blood": ["m62930"],
+ "31-3-mammalian-heart-and-blood-vessels": ["m62931"],
+ "31-4-blood-flow-and-blood-pressure-regulation": ["m62932", "m62933"],
+ "32-1-osmoregulation-and-osmotic-balance": ["m62934"],
+ "32-2-the-kidneys-and-osmoregulatory-organs": ["m62935"],
+ "32-3-excretion-systems": ["m62936"],
+ "32-4-nitrogenous-wastes": ["m62937"],
+ "32-5-hormonal-control-of-osmoregulatory-functions": ["m62938", "m62939"],
+ "33-1-innate-immune-response": ["m62940"],
+ "33-2-adaptive-immune-response": ["m62941"],
+ "33-3-antibodies": ["m62942"],
+ "33-4-disruptions-in-the-immune-system": ["m62943", "m62944"],
+ "34-1-reproduction-methods": ["m62945"],
+ "34-2-fertilization": ["m62946"],
+ "34-3-human-reproductive-anatomy-and-gametogenesis": ["m62947"],
+ "34-4-hormonal-control-of-human-reproduction": ["m62948"],
+ "34-5-fertilization-and-early-embryonic-development": ["m62949"],
+ "34-6-organogenesis-and-vertebrate-axis-formation": ["m62950"],
+ "34-7-human-pregnancy-and-birth": ["m62951", "m62952"],
+
+ # Unit 8: Ecology
+ "35-1-the-scope-of-ecology": ["m62953"],
+ "35-2-biogeography": ["m62954"],
+ "35-3-terrestrial-biomes": ["m62955"],
+ "35-4-aquatic-biomes": ["m62956"],
+ "35-5-climate-and-the-effects-of-global-climate-change": ["m62957", "m62958"],
+ "36-1-population-demography": ["m62959"],
+ "36-2-life-histories-and-natural-selection": ["m62960"],
+ "36-3-environmental-limits-to-population-growth": ["m62961"],
+ "36-4-population-dynamics-and-regulation": ["m62962"],
+ "36-5-human-population-growth": ["m62963"],
+ "36-6-community-ecology": ["m62964"],
+ "36-7-behavioral-biology-proximate-and-ultimate-causes-of-behavior": ["m62965", "m62966"],
+ "37-1-ecology-for-ecosystems": ["m62967"],
+ "37-2-energy-flow-through-ecosystems": ["m62968"],
+ "37-3-biogeochemical-cycles": ["m62969", "m62970"],
+ "38-1-the-biodiversity-crisis": ["m62971"],
+ "38-2-the-importance-of-biodiversity-to-human-life": ["m62972"],
+ "38-3-threats-to-biodiversity": ["m62973"],
+ "38-4-preserving-biodiversity": ["m62974", "m62975"],
+}
+
+
+def get_module_ids_for_chapter(chapter_slug: str) -> list[str]:
+ """Get the module IDs for a given chapter slug."""
+ return CHAPTER_TO_MODULES.get(chapter_slug, [])
+
+
+def get_all_module_ids() -> list[str]:
+ """Get all unique module IDs across all chapters."""
+ all_modules = set()
+ for modules in CHAPTER_TO_MODULES.values():
+ all_modules.update(modules)
+ return sorted(all_modules)
+
+
+def get_github_url_for_module(module_id: str) -> str:
+ """Get the GitHub raw URL for a module's CNXML file."""
+ return f"{GITHUB_RAW_BASE}/{module_id}/index.cnxml"
+
+
+def get_openstax_url_for_chapter(chapter_slug: str) -> str:
+ """Get the OpenStax website URL for a chapter (for citations)."""
+ return f"{OPENSTAX_WEB_BASE}/{chapter_slug}"
diff --git a/samples/personalized_learning/agent/openstax_content.py b/samples/personalized_learning/agent/openstax_content.py
new file mode 100644
index 00000000..2bb73ffa
--- /dev/null
+++ b/samples/personalized_learning/agent/openstax_content.py
@@ -0,0 +1,584 @@
+"""
+OpenStax Content Fetcher
+
+Fetches and parses OpenStax Biology content from:
+1. GCS bucket (preferred - pre-downloaded content)
+2. GitHub raw files (fallback - fetches on demand)
+
+The content is in CNXML format and needs to be parsed to extract plain text.
+"""
+
+import asyncio
+import json
+import logging
+import os
+import re
+import time
+import xml.etree.ElementTree as ET
+from concurrent.futures import ThreadPoolExecutor
+from typing import Optional, Tuple
+
+logger = logging.getLogger(__name__)
+
+# GCS configuration
+GCS_OPENSTAX_BUCKET = os.getenv("GCS_OPENSTAX_BUCKET", "")
+GCS_OPENSTAX_PREFIX = os.getenv("GCS_OPENSTAX_PREFIX", "openstax_modules/")
+
+# GitHub configuration
+GITHUB_RAW_BASE = "https://raw.githubusercontent.com/openstax/osbooks-biology-bundle/main/modules"
+
+# CNXML namespace
+CNXML_NS = {"cnxml": "http://cnx.rice.edu/cnxml"}
+
+# ============================================================================
+# MODULE CONTENT CACHING
+# ============================================================================
+
+# Module cache with TTL - caches parsed content to avoid re-fetching
+_MODULE_CACHE: dict[str, Tuple[str, float]] = {}
+_MODULE_CACHE_TTL = 3600 # 1 hour (content rarely changes)
+
+
+def clear_module_cache() -> None:
+ """Clear the module cache. Useful for testing."""
+ global _MODULE_CACHE
+ _MODULE_CACHE = {}
+ logger.info("Module cache cleared")
+
+
+def parse_cnxml_to_text(cnxml_content: str) -> str:
+ """
+ Parse CNXML content and extract plain text.
+
+ CNXML is an XML format used by OpenStax. We extract:
+ - Title
+ - Paragraphs
+ - List items
+ - Notes and examples
+
+ We skip:
+ - Media/figures (just extract alt text if available)
+ - Equations (complex MathML)
+ - Metadata
+ """
+ try:
+ # Parse the XML
+ root = ET.fromstring(cnxml_content)
+
+ # Extract title
+ title_elem = root.find(".//cnxml:title", CNXML_NS)
+ title = title_elem.text if title_elem is not None and title_elem.text else ""
+
+ # Collect all text content
+ text_parts = []
+ if title:
+ text_parts.append(f"# {title}\n")
+
+ # Find all paragraph-like elements
+ for elem in root.iter():
+ # Skip namespace prefix for comparison
+ tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
+
+ if tag == "para":
+ para_text = _extract_text_from_element(elem)
+ if para_text.strip():
+ text_parts.append(para_text.strip())
+
+ elif tag == "section":
+ # Get section title
+ section_title = elem.find("cnxml:title", CNXML_NS)
+ if section_title is not None and section_title.text:
+ text_parts.append(f"\n## {section_title.text}\n")
+
+ elif tag == "note":
+ note_type = elem.get("type", "note")
+ note_text = _extract_text_from_element(elem)
+ if note_text.strip():
+ text_parts.append(f"\n[{note_type.upper()}]: {note_text.strip()}\n")
+
+ elif tag == "example":
+ example_text = _extract_text_from_element(elem)
+ if example_text.strip():
+ text_parts.append(f"\n[EXAMPLE]: {example_text.strip()}\n")
+
+ elif tag == "item":
+ item_text = _extract_text_from_element(elem)
+ if item_text.strip():
+ text_parts.append(f" • {item_text.strip()}")
+
+ elif tag == "term":
+ # Bold terms for emphasis
+ if elem.text:
+ # Terms are handled inline, skip here
+ pass
+
+ elif tag == "definition":
+ def_text = _extract_text_from_element(elem)
+ if def_text.strip():
+ text_parts.append(f" Definition: {def_text.strip()}")
+
+ # Join and clean up
+ full_text = "\n".join(text_parts)
+
+ # Clean up excessive whitespace
+ full_text = re.sub(r'\n{3,}', '\n\n', full_text)
+ full_text = re.sub(r' {2,}', ' ', full_text)
+
+ return full_text.strip()
+
+ except ET.ParseError as e:
+ logger.error(f"Failed to parse CNXML: {e}")
+ # Return raw content as fallback (stripped of XML tags)
+ return re.sub(r'<[^>]+>', ' ', cnxml_content).strip()
+
+
+def _extract_text_from_element(elem) -> str:
+ """Extract all text from an element and its children."""
+ texts = []
+
+ # Get element's direct text
+ if elem.text:
+ texts.append(elem.text)
+
+ # Get text from children
+ for child in elem:
+ child_text = _extract_text_from_element(child)
+ if child_text:
+ texts.append(child_text)
+ # Get tail text (text after the child element)
+ if child.tail:
+ texts.append(child.tail)
+
+ return " ".join(texts)
+
+
+def fetch_module_from_gcs(module_id: str) -> Optional[str]:
+ """
+ Fetch a module's CNXML content from GCS.
+
+ Returns None if not found or GCS is not configured.
+ """
+ if not GCS_OPENSTAX_BUCKET:
+ return None
+
+ try:
+ from google.cloud import storage
+
+ client = storage.Client()
+ bucket = client.bucket(GCS_OPENSTAX_BUCKET)
+ blob = bucket.blob(f"{GCS_OPENSTAX_PREFIX}{module_id}/index.cnxml")
+
+ if blob.exists():
+ content = blob.download_as_text()
+ logger.info(f"Loaded module {module_id} from GCS")
+ return content
+ else:
+ logger.debug(f"Module {module_id} not found in GCS")
+ return None
+
+ except Exception as e:
+ logger.warning(f"Failed to fetch from GCS: {e}")
+ return None
+
+
+def fetch_module_from_github(module_id: str) -> Optional[str]:
+ """
+ Fetch a module's CNXML content directly from GitHub.
+
+ This is the fallback when GCS is not available.
+ """
+ import urllib.request
+ import urllib.error
+
+ url = f"{GITHUB_RAW_BASE}/{module_id}/index.cnxml"
+
+ try:
+ with urllib.request.urlopen(url, timeout=10) as response:
+ content = response.read().decode('utf-8')
+ logger.info(f"Fetched module {module_id} from GitHub")
+ return content
+ except urllib.error.HTTPError as e:
+ logger.warning(f"HTTP error fetching {module_id}: {e.code}")
+ return None
+ except urllib.error.URLError as e:
+ logger.warning(f"URL error fetching {module_id}: {e.reason}")
+ return None
+ except Exception as e:
+ logger.warning(f"Error fetching {module_id}: {e}")
+ return None
+
+
+def fetch_module_content(module_id: str, parse: bool = True) -> Optional[str]:
+ """
+ Fetch a module's content, trying GCS first then GitHub.
+
+ Args:
+ module_id: The module ID (e.g., "m62767")
+ parse: If True, parse CNXML to plain text. If False, return raw CNXML.
+
+ Returns:
+ Module content as text, or None if not found.
+ """
+ # Try GCS first
+ content = fetch_module_from_gcs(module_id)
+
+ # Fall back to GitHub
+ if content is None:
+ content = fetch_module_from_github(module_id)
+
+ if content is None:
+ return None
+
+ # Parse if requested
+ if parse:
+ return parse_cnxml_to_text(content)
+
+ return content
+
+
+def fetch_module_content_cached(module_id: str, parse: bool = True) -> Optional[str]:
+ """
+ Fetch a module's content with TTL-based caching.
+
+ This wraps fetch_module_content with caching to avoid re-fetching
+ the same content within the TTL period.
+
+ Args:
+ module_id: The module ID (e.g., "m62767")
+ parse: If True, parse CNXML to plain text. If False, return raw CNXML.
+
+ Returns:
+ Module content as text, or None if not found.
+ """
+ cache_key = f"{module_id}_{parse}"
+ now = time.time()
+
+ if cache_key in _MODULE_CACHE:
+ content, cached_at = _MODULE_CACHE[cache_key]
+ if now - cached_at < _MODULE_CACHE_TTL:
+ logger.debug(f"Cache hit for module {module_id}")
+ return content
+
+ # Cache miss - fetch fresh
+ content = fetch_module_content(module_id, parse)
+ if content:
+ _MODULE_CACHE[cache_key] = (content, now)
+ logger.debug(f"Cached module {module_id}")
+
+ return content
+
+
+def fetch_chapter_content(chapter_slug: str) -> Optional[dict]:
+ """
+ Fetch all content for a chapter by fetching its modules in parallel.
+
+ Args:
+ chapter_slug: The chapter slug (e.g., "6-4-atp-adenosine-triphosphate")
+
+ Returns:
+ Dict with chapter info and combined content, or None if not found.
+ """
+ # Import here to avoid circular imports - use relative import for wheel packaging
+ from .openstax_chapters import (
+ OPENSTAX_CHAPTERS,
+ CHAPTER_TO_MODULES,
+ get_openstax_url_for_chapter,
+ )
+
+ if chapter_slug not in CHAPTER_TO_MODULES:
+ logger.warning(f"Unknown chapter: {chapter_slug}")
+ return None
+
+ module_ids = CHAPTER_TO_MODULES[chapter_slug]
+ title = OPENSTAX_CHAPTERS.get(chapter_slug, chapter_slug)
+
+ # Fetch content from all modules in parallel with caching
+ content_parts = []
+ if len(module_ids) > 1:
+ # Use parallel fetching for multiple modules
+ with ThreadPoolExecutor(max_workers=min(len(module_ids), 5)) as executor:
+ futures = {executor.submit(fetch_module_content_cached, mid): mid
+ for mid in module_ids}
+ for future in futures:
+ try:
+ result = future.result()
+ if result:
+ content_parts.append(result)
+ except Exception as e:
+ logger.warning(f"Failed to fetch module: {e}")
+ else:
+ # Single module - no need for threading overhead
+ for module_id in module_ids:
+ module_content = fetch_module_content_cached(module_id)
+ if module_content:
+ content_parts.append(module_content)
+
+ if not content_parts:
+ logger.warning(f"No content fetched for chapter: {chapter_slug}")
+ return None
+
+ return {
+ "chapter_slug": chapter_slug,
+ "title": title,
+ "url": get_openstax_url_for_chapter(chapter_slug),
+ "module_ids": module_ids,
+ "content": "\n\n---\n\n".join(content_parts),
+ }
+
+
+def fetch_multiple_chapters(chapter_slugs: list[str]) -> list[dict]:
+ """
+ Fetch content for multiple chapters in parallel.
+
+ Args:
+ chapter_slugs: List of chapter slugs to fetch.
+
+ Returns:
+ List of chapter content dicts.
+ """
+ if not chapter_slugs:
+ return []
+
+ if len(chapter_slugs) == 1:
+ # Single chapter - no need for threading overhead
+ chapter = fetch_chapter_content(chapter_slugs[0])
+ return [chapter] if chapter else []
+
+ # Parallel fetch for multiple chapters
+ results = []
+ with ThreadPoolExecutor(max_workers=min(len(chapter_slugs), 3)) as executor:
+ futures = {executor.submit(fetch_chapter_content, slug): slug
+ for slug in chapter_slugs}
+ for future in futures:
+ try:
+ result = future.result()
+ if result:
+ results.append(result)
+ except Exception as e:
+ logger.warning(f"Failed to fetch chapter: {e}")
+
+ return results
+
+
+async def fetch_multiple_chapters_async(chapter_slugs: list[str]) -> list[dict]:
+ """
+ Fetch content for multiple chapters asynchronously.
+
+ Uses asyncio.to_thread to run blocking I/O in a thread pool,
+ preventing event loop blocking in async contexts.
+
+ Args:
+ chapter_slugs: List of chapter slugs to fetch.
+
+ Returns:
+ List of chapter content dicts.
+ """
+ # Run the blocking fetch in a thread pool to avoid blocking the event loop
+ return await asyncio.to_thread(fetch_multiple_chapters, chapter_slugs)
+
+
+async def fetch_modules_for_topic(topic: str, max_modules: int = 3) -> dict:
+ """
+ Search for relevant modules using keyword matching and fetch their content.
+
+ This is the NEW module-based approach that fetches individual modules
+ instead of entire chapters, resulting in:
+ - Faster fetches (smaller content chunks)
+ - More relevant content (specific modules vs entire chapters)
+
+ Args:
+ topic: The user's topic/question
+ max_modules: Maximum number of modules to fetch
+
+ Returns:
+ Dict with matched modules and their content.
+ """
+ logger.info("=" * 60)
+ logger.info("FETCH_MODULES_FOR_TOPIC CALLED")
+ logger.info(f"Topic: {topic}")
+ logger.info(f"Max modules: {max_modules}")
+ logger.info("=" * 60)
+
+ from .openstax_modules import search_modules, get_source_citation, MODULE_INDEX, get_module_url
+
+ # Search for matching modules using keyword matching
+ logger.info("Step 1: Searching for modules using keyword matching...")
+ matched_modules = search_modules(topic, max_results=max_modules)
+ logger.info(f"Keyword matching found {len(matched_modules)} modules: {[m.get('id', m.get('title', 'unknown')) for m in matched_modules]}")
+
+ if not matched_modules:
+ # Fall back to LLM matching for chapter, then get first module
+ logger.info("Step 2: No keyword matches - falling back to LLM matching...")
+ chapter_slugs = await _llm_match_topic_to_chapters(topic, 1)
+ logger.info(f"LLM matched chapters: {chapter_slugs}")
+ if chapter_slugs:
+ # Import chapter-to-module mapping as fallback
+ from .openstax_chapters import CHAPTER_TO_MODULES
+ if chapter_slugs[0] in CHAPTER_TO_MODULES:
+ module_ids = CHAPTER_TO_MODULES[chapter_slugs[0]][:max_modules]
+ logger.info(f"Found modules from chapter mapping: {module_ids}")
+ for mid in module_ids:
+ if mid in MODULE_INDEX:
+ info = MODULE_INDEX[mid]
+ matched_modules.append({
+ "id": mid,
+ "title": info["title"],
+ "unit": info["unit"],
+ "chapter": info["chapter"],
+ "url": get_module_url(mid),
+ })
+ else:
+ logger.warning(f"Chapter {chapter_slugs[0]} not found in CHAPTER_TO_MODULES mapping")
+ else:
+ logger.warning("LLM matching also returned no chapters!")
+
+ if not matched_modules:
+ logger.error(f"NO MODULES FOUND for topic: {topic}")
+ logger.error("Both keyword matching and LLM fallback failed!")
+ return {
+ "topic": topic,
+ "matched_modules": [],
+ "combined_content": "",
+ "sources": [],
+ }
+
+ logger.info(f"Final matched modules: {[m.get('id') for m in matched_modules]}")
+
+ # Fetch module content in parallel
+ module_ids = [m["id"] for m in matched_modules]
+
+ if len(module_ids) > 1:
+ # Parallel fetch for multiple modules
+ contents = []
+ with ThreadPoolExecutor(max_workers=min(len(module_ids), 5)) as executor:
+ futures = {executor.submit(fetch_module_content_cached, mid): mid
+ for mid in module_ids}
+ for future in futures:
+ try:
+ result = future.result()
+ if result:
+ mid = futures[future]
+ contents.append((mid, result))
+ except Exception as e:
+ logger.warning(f"Failed to fetch module: {e}")
+ else:
+ # Single module - no threading overhead
+ contents = []
+ for mid in module_ids:
+ content = fetch_module_content_cached(mid)
+ if content:
+ contents.append((mid, content))
+
+ if not contents:
+ logger.warning(f"No content fetched for topic: {topic}")
+ return {
+ "topic": topic,
+ "matched_modules": matched_modules,
+ "combined_content": "",
+ "sources": [],
+ }
+
+ # Build combined content with source attribution
+ combined_parts = []
+ for mid, content in contents:
+ if mid in MODULE_INDEX:
+ info = MODULE_INDEX[mid]
+ url = get_module_url(mid)
+ combined_parts.append(f"## {info['title']}\nSource: {url}\n\n{content}")
+
+ # Generate source citation
+ source_citation = get_source_citation(module_ids)
+
+ return {
+ "topic": topic,
+ "matched_modules": matched_modules,
+ "combined_content": "\n\n===\n\n".join(combined_parts),
+ "sources": [source_citation],
+ }
+
+
+async def fetch_content_for_topic(topic: str, max_chapters: int = 3) -> dict:
+ """
+ Use LLM to match a topic to relevant chapters, then fetch their content.
+
+ This is the main entry point for getting OpenStax content based on a user query.
+ Now delegates to module-based fetching for better performance.
+
+ Args:
+ topic: The user's topic/question
+ max_chapters: Maximum number of chapters to fetch
+
+ Returns:
+ Dict with matched chapters and their content.
+ """
+ # Use the new module-based fetching for better performance
+ result = await fetch_modules_for_topic(topic, max_modules=max_chapters)
+
+ # Convert module format to chapter format for backward compatibility
+ return {
+ "topic": result["topic"],
+ "matched_chapters": [
+ {"slug": m.get("id", ""), "title": m.get("title", ""), "url": m.get("url", "")}
+ for m in result.get("matched_modules", [])
+ ],
+ "combined_content": result.get("combined_content", ""),
+ "sources": result.get("sources", []),
+ }
+
+
+async def _llm_match_topic_to_chapters(topic: str, max_chapters: int = 3) -> list[str]:
+ """
+ Use Gemini to match a topic to the most relevant chapter slugs.
+
+ Returns list of chapter slugs.
+ """
+ from .openstax_chapters import get_chapter_list_for_llm
+
+ try:
+ from google import genai
+ from google.genai import types
+
+ project = os.getenv("GOOGLE_CLOUD_PROJECT")
+ location = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1")
+ model = os.getenv("GENAI_MODEL", "gemini-2.5-flash")
+
+ client = genai.Client(
+ vertexai=True,
+ project=project,
+ location=location,
+ )
+
+ chapter_list = get_chapter_list_for_llm()
+
+ prompt = f"""Given this user topic/question about biology:
+
+"{topic}"
+
+Select the {max_chapters} most relevant chapters from this OpenStax Biology textbook that would help answer the question or teach about this topic.
+
+Available chapters:
+{chapter_list}
+
+Return ONLY a JSON array of chapter slugs (the part before the colon), nothing else.
+Example: ["6-4-atp-adenosine-triphosphate", "7-1-energy-in-living-systems"]
+"""
+
+ response = client.models.generate_content(
+ model=model,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_mime_type="application/json",
+ ),
+ )
+
+ # Parse the response
+ slugs = json.loads(response.text.strip())
+
+ if isinstance(slugs, list):
+ return slugs[:max_chapters]
+
+ except Exception as e:
+ logger.error(f"LLM chapter matching failed: {e}")
+
+ # Fallback to a default chapter
+ return ["1-1-the-science-of-biology"]
diff --git a/samples/personalized_learning/agent/openstax_modules.py b/samples/personalized_learning/agent/openstax_modules.py
new file mode 100644
index 00000000..dd5e0998
--- /dev/null
+++ b/samples/personalized_learning/agent/openstax_modules.py
@@ -0,0 +1,1667 @@
+"""
+OpenStax Module Index for Biology AP Courses.
+
+This module provides a complete index of all modules in the OpenStax Biology AP Courses textbook,
+with keyword-based search capabilities for fast topic matching.
+
+The module index is built from the collection XML:
+https://github.com/openstax/osbooks-biology-bundle/blob/main/collections/biology-ap-courses.collection.xml
+
+Module content is fetched from:
+- GCS bucket (primary): gs://{bucket}/openstax_modules/{module_id}/index.cnxml
+- GitHub (fallback): https://raw.githubusercontent.com/openstax/osbooks-biology-bundle/main/modules/{module_id}/index.cnxml
+
+Citations link to the user-friendly OpenStax website:
+https://openstax.org/books/biology-ap-courses/pages/{chapter-slug}
+"""
+
+import re
+from typing import Optional
+
+# Base URL for OpenStax textbook
+OPENSTAX_BASE_URL = "https://openstax.org/books/biology-ap-courses/pages"
+
+# Module ID to chapter slug mapping for URL generation
+# The chapter slugs match the actual OpenStax website URL structure
+MODULE_TO_CHAPTER_SLUG = {
+ # Metabolism chapter (6)
+ "m62761": "6-introduction",
+ "m62763": "6-1-energy-and-metabolism",
+ "m62764": "6-2-potential-kinetic-free-and-activation-energy",
+ "m62767": "6-3-the-laws-of-thermodynamics",
+ "m62768": "6-4-atp-adenosine-triphosphate",
+ "m62778": "6-5-enzymes",
+ # Cellular Respiration chapter (7)
+ "m62784": "7-introduction",
+ "m62786": "7-1-energy-in-living-systems",
+ "m62787": "7-2-glycolysis",
+ "m62788": "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle",
+ "m62789": "7-4-oxidative-phosphorylation",
+ "m62790": "7-5-metabolism-without-oxygen",
+ "m62791": "7-6-connections-of-carbohydrate-protein-and-lipid-metabolic-pathways",
+ "m62792": "7-7-regulation-of-cellular-respiration",
+ # Photosynthesis chapter (8)
+ "m62793": "8-introduction",
+ "m62794": "8-1-overview-of-photosynthesis",
+ "m62795": "8-2-the-light-dependent-reaction-of-photosynthesis",
+ "m62796": "8-3-using-light-to-make-organic-molecules",
+ # Cell Structure chapter (4)
+ "m62736": "4-introduction",
+ "m62738": "4-1-studying-cells",
+ "m62740": "4-2-prokaryotic-cells",
+ "m62742": "4-3-eukaryotic-cells",
+ "m62743": "4-4-the-endomembrane-system-and-proteins",
+ "m62744": "4-5-cytoskeleton",
+ "m62746": "4-6-connections-between-cells-and-cellular-activities",
+ # Plasma Membranes chapter (5)
+ "m62780": "5-introduction",
+ "m62773": "5-1-components-and-structure",
+ "m62753": "5-2-passive-transport",
+ "m62770": "5-3-active-transport",
+ "m62772": "5-4-bulk-transport",
+ # Cell Communication chapter (9)
+ "m62797": "9-introduction",
+ "m62798": "9-1-signaling-molecules-and-cellular-receptors",
+ "m62799": "9-2-propagation-of-the-signal",
+ "m62800": "9-3-response-to-the-signal",
+ "m62801": "9-4-signaling-in-single-celled-organisms",
+ # Cell Reproduction chapter (10)
+ "m62802": "10-introduction",
+ "m62803": "10-1-cell-division",
+ "m62804": "10-2-the-cell-cycle",
+ "m62805": "10-3-control-of-the-cell-cycle",
+ "m62806": "10-4-cancer-and-the-cell-cycle",
+ "m62808": "10-5-prokaryotic-cell-division",
+ # Meiosis chapter (11)
+ "m62809": "11-introduction",
+ "m62810": "11-1-the-process-of-meiosis",
+ "m62811": "11-2-sexual-reproduction",
+ # Mendel's Experiments chapter (12)
+ "m62812": "12-introduction",
+ "m62813": "12-1-mendels-experiments-and-the-laws-of-probability",
+ "m62817": "12-2-characteristics-and-traits",
+ "m62819": "12-3-laws-of-inheritance",
+ # Modern Inheritance chapter (13)
+ "m62820": "13-introduction",
+ "m62821": "13-1-chromosomal-theory-and-genetic-linkages",
+ "m62822": "13-2-chromosomal-basis-of-inherited-disorders",
+ # DNA Structure chapter (14)
+ "m62823": "14-introduction",
+ "m62824": "14-1-historical-basis-of-modern-understanding",
+ "m62825": "14-2-dna-structure-and-sequencing",
+ "m62826": "14-3-basics-of-dna-replication",
+ "m62828": "14-4-dna-replication-in-prokaryotes",
+ "m62829": "14-5-dna-replication-in-eukaryotes",
+ "m62830": "14-6-dna-repair",
+ # Genes and Proteins chapter (15)
+ "m62833": "15-introduction",
+ "m62837": "15-1-the-genetic-code",
+ "m62838": "15-2-prokaryotic-transcription",
+ "m62840": "15-3-eukaryotic-transcription",
+ "m62842": "15-4-rna-processing-in-eukaryotes",
+ "m62843": "15-5-ribosomes-and-protein-synthesis",
+ # Gene Regulation chapter (16)
+ "m62844": "16-introduction",
+ "m62845": "16-1-regulation-of-gene-expression",
+ "m62846": "16-2-prokaryotic-gene-regulation",
+ "m62847": "16-3-eukaryotic-epigenetic-gene-regulation",
+ "m62848": "16-4-eukaryotic-transcriptional-gene-regulation",
+ "m62849": "16-5-eukaryotic-post-transcriptional-gene-regulation",
+ "m62850": "16-6-eukaryotic-translational-and-post-translational-gene-regulation",
+ "m62851": "16-7-cancer-and-gene-regulation",
+ # Biotechnology chapter (17)
+ "m62852": "17-introduction",
+ "m62853": "17-1-biotechnology",
+ "m62855": "17-2-mapping-genomes",
+ "m62857": "17-3-whole-genome-sequencing",
+ "m62860": "17-4-applying-genomics",
+ "m62861": "17-5-genomics-and-proteomics",
+ # Evolution chapter (18)
+ "m62862": "18-introduction",
+ "m62863": "18-1-understanding-evolution",
+ "m62864": "18-2-formation-of-new-species",
+ "m62865": "18-3-reconnection-and-rates-of-speciation",
+ # Population Evolution chapter (19)
+ "m62866": "19-introduction",
+ "m62868": "19-1-population-evolution",
+ "m62870": "19-2-population-genetics",
+ "m62871": "19-3-adaptive-evolution",
+ # Phylogenies chapter (20)
+ "m62873": "20-introduction",
+ "m62874": "20-1-organizing-life-on-earth",
+ "m62903": "20-2-determining-evolutionary-relationships",
+ "m62876": "20-3-perspectives-on-the-phylogenetic-tree",
+ # Viruses chapter (21)
+ "m62877": "21-introduction",
+ "m62881": "21-1-viral-evolution-morphology-and-classification",
+ "m62882": "21-2-virus-infection-and-hosts",
+ "m62904": "21-3-prevention-and-treatment-of-viral-infections",
+ "m62887": "21-4-other-acellular-entities-prions-and-viroids",
+ # Prokaryotes chapter (22)
+ "m62889": "22-introduction",
+ "m62891": "22-1-prokaryotic-diversity",
+ "m62893": "22-2-structure-of-prokaryotes",
+ "m62894": "22-3-prokaryotic-metabolism",
+ "m62896": "22-4-bacterial-diseases-in-humans",
+ "m62897": "22-5-beneficial-prokaryotes",
+ # Plants chapter (23)
+ "m62899": "23-introduction",
+ "m62951": "23-1-the-plant-body",
+ "m62905": "23-2-stems",
+ "m62906": "23-3-roots",
+ "m62908": "23-4-leaves",
+ "m62969": "23-5-transport-of-water-and-solutes-in-plants",
+ "m62930": "23-6-plant-sensory-systems-and-responses",
+ # Animal Body chapter (24)
+ "m62912": "24-introduction",
+ "m62916": "24-1-animal-form-and-function",
+ "m62918": "24-2-animal-primary-tissues",
+ "m62931": "24-3-homeostasis",
+ # Nutrition and Digestion chapter (25)
+ "m62933": "25-introduction",
+ "m62919": "25-1-digestive-systems",
+ "m62920": "25-2-nutrition-and-energy-production",
+ "m62921": "25-3-digestive-system-processes",
+ "m62922": "25-4-digestive-system-regulation",
+ # Nervous System chapter (26)
+ "m62923": "26-introduction",
+ "m62924": "26-1-neurons-and-glial-cells",
+ "m62925": "26-2-how-neurons-communicate",
+ "m62926": "26-3-the-central-nervous-system",
+ "m62928": "26-4-the-peripheral-nervous-system",
+ "m62929": "26-5-nervous-system-disorders",
+ # Sensory Systems chapter (27)
+ "m62937": "27-introduction",
+ "m62994": "27-1-sensory-processes",
+ "m62946": "27-2-somatosensation",
+ "m62947": "27-3-taste-and-smell",
+ "m62954": "27-4-hearing-and-vestibular-sensation",
+ "m62957": "27-5-vision",
+ # Endocrine System chapter (28)
+ "m62959": "28-introduction",
+ "m62961": "28-1-types-of-hormones",
+ "m62963": "28-2-how-hormones-work",
+ "m62996": "28-3-regulation-of-body-processes",
+ "m62971": "28-4-regulation-of-hormone-production",
+ "m62995": "28-5-endocrine-glands",
+ # Musculoskeletal System chapter (29)
+ "m62976": "29-introduction",
+ "m62977": "29-1-types-of-skeletal-systems",
+ "m62978": "29-2-bone",
+ "m62979": "29-3-joints-and-skeletal-movement",
+ "m62980": "29-4-muscle-contraction-and-locomotion",
+ # Respiratory System chapter (30)
+ "m62981": "30-introduction",
+ "m62982": "30-1-systems-of-gas-exchange",
+ "m62998": "30-2-gas-exchange-across-respiratory-surfaces",
+ "m62987": "30-3-breathing",
+ "m62988": "30-4-transport-of-gases-in-human-bodily-fluids",
+ # Circulatory System chapter (31)
+ "m62989": "31-introduction",
+ "m62990": "31-1-overview-of-the-circulatory-system",
+ "m62991": "31-2-components-of-the-blood",
+ "m62992": "31-3-mammalian-heart-and-blood-vessels",
+ "m62993": "31-4-blood-flow-and-blood-pressure-regulation",
+ # Osmotic Regulation chapter (32)
+ "m62997": "32-introduction",
+ "m63000": "32-1-osmoregulation-and-osmotic-balance",
+ "m63001": "32-2-the-kidneys-and-osmoregulatory-organs",
+ "m63002": "32-3-excretion-systems",
+ "m63003": "32-4-nitrogenous-wastes",
+ "m63004": "32-5-hormonal-control-of-osmoregulatory-functions",
+ # Immune System chapter (33)
+ "m63005": "33-introduction",
+ "m63006": "33-1-innate-immune-response",
+ "m63007": "33-2-adaptive-immune-response",
+ "m63008": "33-3-antibodies",
+ "m63009": "33-4-disruptions-in-the-immune-system",
+ # Animal Reproduction chapter (34)
+ "m63010": "34-introduction",
+ "m63011": "34-1-reproduction-methods",
+ "m63012": "34-2-fertilization",
+ "m63013": "34-3-human-reproductive-anatomy-and-gametogenesis",
+ "m63014": "34-4-hormonal-control-of-human-reproduction",
+ "m63016": "34-5-fertilization-and-early-embryonic-development",
+ "m63043": "34-6-organogenesis-and-vertebrate-axis-formation",
+ "m63018": "34-7-human-pregnancy-and-birth",
+ # Ecology chapter (35)
+ "m63019": "35-introduction",
+ "m63021": "35-1-the-scope-of-ecology",
+ "m63023": "35-2-biogeography",
+ "m63024": "35-3-terrestrial-biomes",
+ "m63025": "35-4-aquatic-biomes",
+ "m63026": "35-5-climate-and-the-effects-of-global-climate-change",
+ # Population Ecology chapter (36)
+ "m63027": "36-introduction",
+ "m63028": "36-1-population-demography",
+ "m63029": "36-2-life-histories-and-natural-selection",
+ "m63030": "36-3-environmental-limits-to-population-growth",
+ "m63031": "36-4-population-dynamics-and-regulation",
+ "m63032": "36-5-human-population-growth",
+ "m63033": "36-6-community-ecology",
+ "m63034": "36-7-behavioral-biology-proximate-and-ultimate-causes-of-behavior",
+ # Ecosystems chapter (37)
+ "m63035": "37-introduction",
+ "m63036": "37-1-ecology-for-ecosystems",
+ "m63037": "37-2-energy-flow-through-ecosystems",
+ "m63040": "37-3-biogeochemical-cycles",
+ # Biodiversity chapter (38)
+ "m63047": "38-introduction",
+ "m63048": "38-1-the-biodiversity-crisis",
+ "m63049": "38-2-the-importance-of-biodiversity-to-human-life",
+ "m63050": "38-3-threats-to-biodiversity",
+ "m63051": "38-4-preserving-biodiversity",
+ # Chemistry chapters (1-3)
+ "m62716": "1-introduction",
+ "m62717": "1-1-the-science-of-biology",
+ "m62718": "1-2-themes-and-concepts-of-biology",
+ "m62719": "2-introduction",
+ "m62720": "2-1-atoms-isotopes-ions-and-molecules-the-building-blocks",
+ "m62721": "2-2-water",
+ "m62722": "2-3-carbon",
+ "m62723": "3-introduction",
+ "m62724": "3-1-synthesis-of-biological-macromolecules",
+ "m62726": "3-2-carbohydrates",
+ "m62730": "3-3-lipids",
+ "m62733": "3-4-proteins",
+ "m62735": "3-5-nucleic-acids",
+}
+
+# Complete module index with titles, units, and chapters
+# Generated from the collection XML
+MODULE_INDEX = {
+ "m45849": {"title": "The Periodic Table of Elements", "unit": "Front Matter", "chapter": "Front Matter"},
+ "m60107": {"title": "Geological Time", "unit": "Front Matter", "chapter": "Front Matter"},
+ "m62716": {"title": "Introduction", "unit": "The Chemistry of Life", "chapter": "The Study of Life"},
+ "m62717": {"title": "The Science of Biology", "unit": "The Chemistry of Life", "chapter": "The Study of Life"},
+ "m62718": {"title": "Themes and Concepts of Biology", "unit": "The Chemistry of Life", "chapter": "The Study of Life"},
+ "m62719": {"title": "Introduction", "unit": "The Chemistry of Life", "chapter": "The Chemical Foundation of Life"},
+ "m62720": {"title": "Atoms, Isotopes, Ions, and Molecules: The Building Blocks", "unit": "The Chemistry of Life", "chapter": "The Chemical Foundation of Life"},
+ "m62721": {"title": "Water", "unit": "The Chemistry of Life", "chapter": "The Chemical Foundation of Life"},
+ "m62722": {"title": "Carbon", "unit": "The Chemistry of Life", "chapter": "The Chemical Foundation of Life"},
+ "m62723": {"title": "Introduction", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62724": {"title": "Synthesis of Biological Macromolecules", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62726": {"title": "Carbohydrates", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62730": {"title": "Lipids", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62733": {"title": "Proteins", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62735": {"title": "Nucleic Acids", "unit": "The Chemistry of Life", "chapter": "Biological Macromolecules"},
+ "m62736": {"title": "Introduction", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62738": {"title": "Studying Cells", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62740": {"title": "Prokaryotic Cells", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62742": {"title": "Eukaryotic Cells", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62743": {"title": "The Endomembrane System and Proteins", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62744": {"title": "Cytoskeleton", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62746": {"title": "Connections between Cells and Cellular Activities", "unit": "The Cell", "chapter": "Cell Structure"},
+ "m62753": {"title": "Passive Transport", "unit": "The Cell", "chapter": "Structure and Function of Plasma Membranes"},
+ "m62761": {"title": "Introduction", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62763": {"title": "Energy and Metabolism", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62764": {"title": "Potential, Kinetic, Free, and Activation Energy", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62767": {"title": "The Laws of Thermodynamics", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62768": {"title": "ATP: Adenosine Triphosphate", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62770": {"title": "Active Transport", "unit": "The Cell", "chapter": "Structure and Function of Plasma Membranes"},
+ "m62772": {"title": "Bulk Transport", "unit": "The Cell", "chapter": "Structure and Function of Plasma Membranes"},
+ "m62773": {"title": "Components and Structure", "unit": "The Cell", "chapter": "Structure and Function of Plasma Membranes"},
+ "m62778": {"title": "Enzymes", "unit": "The Cell", "chapter": "Metabolism"},
+ "m62780": {"title": "Introduction", "unit": "The Cell", "chapter": "Structure and Function of Plasma Membranes"},
+ "m62784": {"title": "Introduction", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62786": {"title": "Energy in Living Systems", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62787": {"title": "Glycolysis", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62788": {"title": "Oxidation of Pyruvate and the Citric Acid Cycle", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62789": {"title": "Oxidative Phosphorylation", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62790": {"title": "Metabolism without Oxygen", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62791": {"title": "Connections of Carbohydrate, Protein, and Lipid Metabolic Pathways", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62792": {"title": "Regulation of Cellular Respiration", "unit": "The Cell", "chapter": "Cellular Respiration"},
+ "m62793": {"title": "Introduction", "unit": "The Cell", "chapter": "Photosynthesis"},
+ "m62794": {"title": "Overview of Photosynthesis", "unit": "The Cell", "chapter": "Photosynthesis"},
+ "m62795": {"title": "The Light-Dependent Reaction of Photosynthesis", "unit": "The Cell", "chapter": "Photosynthesis"},
+ "m62796": {"title": "Using Light to Make Organic Molecules", "unit": "The Cell", "chapter": "Photosynthesis"},
+ "m62797": {"title": "Introduction", "unit": "The Cell", "chapter": "Cell Communication"},
+ "m62798": {"title": "Signaling Molecules and Cellular Receptors", "unit": "The Cell", "chapter": "Cell Communication"},
+ "m62799": {"title": "Propagation of the Signal", "unit": "The Cell", "chapter": "Cell Communication"},
+ "m62800": {"title": "Response to the Signal", "unit": "The Cell", "chapter": "Cell Communication"},
+ "m62801": {"title": "Signaling in Single-Celled Organisms", "unit": "The Cell", "chapter": "Cell Communication"},
+ "m62802": {"title": "Introduction", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62803": {"title": "Cell Division", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62804": {"title": "The Cell Cycle", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62805": {"title": "Control of the Cell Cycle", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62806": {"title": "Cancer and the Cell Cycle", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62808": {"title": "Prokaryotic Cell Division", "unit": "The Cell", "chapter": "Cell Reproduction"},
+ "m62809": {"title": "Introduction", "unit": "Genetics", "chapter": "Meiosis and Sexual Reproduction"},
+ "m62810": {"title": "The Process of Meiosis", "unit": "Genetics", "chapter": "Meiosis and Sexual Reproduction"},
+ "m62811": {"title": "Sexual Reproduction", "unit": "Genetics", "chapter": "Meiosis and Sexual Reproduction"},
+ "m62812": {"title": "Introduction", "unit": "Genetics", "chapter": "Mendel's Experiments and Heredity"},
+ "m62813": {"title": "Mendel's Experiments and the Laws of Probability", "unit": "Genetics", "chapter": "Mendel's Experiments and Heredity"},
+ "m62817": {"title": "Characteristics and Traits", "unit": "Genetics", "chapter": "Mendel's Experiments and Heredity"},
+ "m62819": {"title": "Laws of Inheritance", "unit": "Genetics", "chapter": "Mendel's Experiments and Heredity"},
+ "m62820": {"title": "Introduction", "unit": "Genetics", "chapter": "Modern Understandings of Inheritance"},
+ "m62821": {"title": "Chromosomal Theory and Genetic Linkages", "unit": "Genetics", "chapter": "Modern Understandings of Inheritance"},
+ "m62822": {"title": "Chromosomal Basis of Inherited Disorders", "unit": "Genetics", "chapter": "Modern Understandings of Inheritance"},
+ "m62823": {"title": "Introduction", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62824": {"title": "Historical Basis of Modern Understanding", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62825": {"title": "DNA Structure and Sequencing", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62826": {"title": "Basics of DNA Replication", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62828": {"title": "DNA Replication in Prokaryotes", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62829": {"title": "DNA Replication in Eukaryotes", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62830": {"title": "DNA Repair", "unit": "Genetics", "chapter": "DNA Structure and Function"},
+ "m62833": {"title": "Introduction", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62837": {"title": "The Genetic Code", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62838": {"title": "Prokaryotic Transcription", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62840": {"title": "Eukaryotic Transcription", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62842": {"title": "RNA Processing in Eukaryotes", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62843": {"title": "Ribosomes and Protein Synthesis", "unit": "Genetics", "chapter": "Genes and Proteins"},
+ "m62844": {"title": "Introduction", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62845": {"title": "Regulation of Gene Expression", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62846": {"title": "Prokaryotic Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62847": {"title": "Eukaryotic Epigenetic Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62848": {"title": "Eukaryotic Transcriptional Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62849": {"title": "Eukaryotic Post-transcriptional Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62850": {"title": "Eukaryotic Translational and Post-translational Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62851": {"title": "Cancer and Gene Regulation", "unit": "Genetics", "chapter": "Gene Regulation"},
+ "m62852": {"title": "Introduction", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62853": {"title": "Biotechnology", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62855": {"title": "Mapping Genomes", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62857": {"title": "Whole-Genome Sequencing", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62860": {"title": "Applying Genomics", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62861": {"title": "Genomics and Proteomics", "unit": "Genetics", "chapter": "Biotechnology and Genomics"},
+ "m62862": {"title": "Introduction", "unit": "Evolutionary Processes", "chapter": "Evolution and Origin of Species"},
+ "m62863": {"title": "Understanding Evolution", "unit": "Evolutionary Processes", "chapter": "Evolution and Origin of Species"},
+ "m62864": {"title": "Formation of New Species", "unit": "Evolutionary Processes", "chapter": "Evolution and Origin of Species"},
+ "m62865": {"title": "Reconnection and Rates of Speciation", "unit": "Evolutionary Processes", "chapter": "Evolution and Origin of Species"},
+ "m62866": {"title": "Introduction", "unit": "Evolutionary Processes", "chapter": "The Evolution of Populations"},
+ "m62868": {"title": "Population Evolution", "unit": "Evolutionary Processes", "chapter": "The Evolution of Populations"},
+ "m62870": {"title": "Population Genetics", "unit": "Evolutionary Processes", "chapter": "The Evolution of Populations"},
+ "m62871": {"title": "Adaptive Evolution", "unit": "Evolutionary Processes", "chapter": "The Evolution of Populations"},
+ "m62873": {"title": "Introduction", "unit": "Evolutionary Processes", "chapter": "Phylogenies and the History of Life"},
+ "m62874": {"title": "Organizing Life on Earth", "unit": "Evolutionary Processes", "chapter": "Phylogenies and the History of Life"},
+ "m62876": {"title": "Perspectives on the Phylogenetic Tree", "unit": "Evolutionary Processes", "chapter": "Phylogenies and the History of Life"},
+ "m62877": {"title": "Introduction", "unit": "Biological Diversity", "chapter": "Viruses"},
+ "m62881": {"title": "Viral Evolution, Morphology, and Classification", "unit": "Biological Diversity", "chapter": "Viruses"},
+ "m62882": {"title": "Virus Infection and Hosts", "unit": "Biological Diversity", "chapter": "Viruses"},
+ "m62887": {"title": "Other Acellular Entities: Prions and Viroids", "unit": "Biological Diversity", "chapter": "Viruses"},
+ "m62889": {"title": "Introduction", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62891": {"title": "Prokaryotic Diversity", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62893": {"title": "Structure of Prokaryotes", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62894": {"title": "Prokaryotic Metabolism", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62896": {"title": "Bacterial Diseases in Humans", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62897": {"title": "Beneficial Prokaryotes", "unit": "Biological Diversity", "chapter": "Prokaryotes: Bacteria and Archaea"},
+ "m62899": {"title": "Introduction", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62903": {"title": "Determining Evolutionary Relationships", "unit": "Evolutionary Processes", "chapter": "Phylogenies and the History of Life"},
+ "m62904": {"title": "Prevention and Treatment of Viral Infections", "unit": "Biological Diversity", "chapter": "Viruses"},
+ "m62905": {"title": "Stems", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62906": {"title": "Roots", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62908": {"title": "Leaves", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62912": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Animal Body: Basic Form and Function"},
+ "m62916": {"title": "Animal Form and Function", "unit": "Animal Structure and Function", "chapter": "The Animal Body: Basic Form and Function"},
+ "m62918": {"title": "Animal Primary Tissues", "unit": "Animal Structure and Function", "chapter": "The Animal Body: Basic Form and Function"},
+ "m62919": {"title": "Digestive Systems", "unit": "Animal Structure and Function", "chapter": "Animal Nutrition and the Digestive System"},
+ "m62920": {"title": "Nutrition and Energy Production", "unit": "Animal Structure and Function", "chapter": "Animal Nutrition and the Digestive System"},
+ "m62921": {"title": "Digestive System Processes", "unit": "Animal Structure and Function", "chapter": "Animal Nutrition and the Digestive System"},
+ "m62922": {"title": "Digestive System Regulation", "unit": "Animal Structure and Function", "chapter": "Animal Nutrition and the Digestive System"},
+ "m62923": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62924": {"title": "Neurons and Glial Cells", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62925": {"title": "How Neurons Communicate", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62926": {"title": "The Central Nervous System", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62928": {"title": "The Peripheral Nervous System", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62929": {"title": "Nervous System Disorders", "unit": "Animal Structure and Function", "chapter": "The Nervous System"},
+ "m62930": {"title": "Plant Sensory Systems and Responses", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62931": {"title": "Homeostasis", "unit": "Animal Structure and Function", "chapter": "The Animal Body: Basic Form and Function"},
+ "m62933": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "Animal Nutrition and the Digestive System"},
+ "m62937": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62946": {"title": "Somatosensation", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62947": {"title": "Taste and Smell", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62951": {"title": "The Plant Body", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62954": {"title": "Hearing and Vestibular Sensation", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62957": {"title": "Vision", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62959": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62961": {"title": "Types of Hormones", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62963": {"title": "How Hormones Work", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62969": {"title": "Transport of Water and Solutes in Plants", "unit": "Plant Structure and Function", "chapter": "Plant Form and Physiology"},
+ "m62971": {"title": "Regulation of Hormone Production", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62976": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Musculoskeletal System"},
+ "m62977": {"title": "Types of Skeletal Systems", "unit": "Animal Structure and Function", "chapter": "The Musculoskeletal System"},
+ "m62978": {"title": "Bone", "unit": "Animal Structure and Function", "chapter": "The Musculoskeletal System"},
+ "m62979": {"title": "Joints and Skeletal Movement", "unit": "Animal Structure and Function", "chapter": "The Musculoskeletal System"},
+ "m62980": {"title": "Muscle Contraction and Locomotion", "unit": "Animal Structure and Function", "chapter": "The Musculoskeletal System"},
+ "m62981": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Respiratory System"},
+ "m62982": {"title": "Systems of Gas Exchange", "unit": "Animal Structure and Function", "chapter": "The Respiratory System"},
+ "m62987": {"title": "Breathing", "unit": "Animal Structure and Function", "chapter": "The Respiratory System"},
+ "m62988": {"title": "Transport of Gases in Human Bodily Fluids", "unit": "Animal Structure and Function", "chapter": "The Respiratory System"},
+ "m62989": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Circulatory System"},
+ "m62990": {"title": "Overview of the Circulatory System", "unit": "Animal Structure and Function", "chapter": "The Circulatory System"},
+ "m62991": {"title": "Components of the Blood", "unit": "Animal Structure and Function", "chapter": "The Circulatory System"},
+ "m62992": {"title": "Mammalian Heart and Blood Vessels", "unit": "Animal Structure and Function", "chapter": "The Circulatory System"},
+ "m62993": {"title": "Blood Flow and Blood Pressure Regulation", "unit": "Animal Structure and Function", "chapter": "The Circulatory System"},
+ "m62994": {"title": "Sensory Processes", "unit": "Animal Structure and Function", "chapter": "Sensory Systems"},
+ "m62995": {"title": "Endocrine Glands", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62996": {"title": "Regulation of Body Processes", "unit": "Animal Structure and Function", "chapter": "The Endocrine System"},
+ "m62997": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m62998": {"title": "Gas Exchange across Respiratory Surfaces", "unit": "Animal Structure and Function", "chapter": "The Respiratory System"},
+ "m63000": {"title": "Osmoregulation and Osmotic Balance", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m63001": {"title": "The Kidneys and Osmoregulatory Organs", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m63002": {"title": "Excretion Systems", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m63003": {"title": "Nitrogenous Wastes", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m63004": {"title": "Hormonal Control of Osmoregulatory Functions", "unit": "Animal Structure and Function", "chapter": "Osmotic Regulation and Excretion"},
+ "m63005": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "The Immune System"},
+ "m63006": {"title": "Innate Immune Response", "unit": "Animal Structure and Function", "chapter": "The Immune System"},
+ "m63007": {"title": "Adaptive Immune Response", "unit": "Animal Structure and Function", "chapter": "The Immune System"},
+ "m63008": {"title": "Antibodies", "unit": "Animal Structure and Function", "chapter": "The Immune System"},
+ "m63009": {"title": "Disruptions in the Immune System", "unit": "Animal Structure and Function", "chapter": "The Immune System"},
+ "m63010": {"title": "Introduction", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63011": {"title": "Reproduction Methods", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63012": {"title": "Fertilization", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63013": {"title": "Human Reproductive Anatomy and Gametogenesis", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63014": {"title": "Hormonal Control of Human Reproduction", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63016": {"title": "Fertilization and Early Embryonic Development", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63018": {"title": "Human Pregnancy and Birth", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63019": {"title": "Introduction", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63021": {"title": "The Scope of Ecology", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63023": {"title": "Biogeography", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63024": {"title": "Terrestrial Biomes", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63025": {"title": "Aquatic Biomes", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63026": {"title": "Climate and the Effects of Global Climate Change", "unit": "Ecology", "chapter": "Ecology and the Biosphere"},
+ "m63027": {"title": "Introduction", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63028": {"title": "Population Demography", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63029": {"title": "Life Histories and Natural Selection", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63030": {"title": "Environmental Limits to Population Growth", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63031": {"title": "Population Dynamics and Regulation", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63032": {"title": "Human Population Growth", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63033": {"title": "Community Ecology", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63034": {"title": "Behavioral Biology: Proximate and Ultimate Causes of Behavior", "unit": "Ecology", "chapter": "Population and Community Ecology"},
+ "m63035": {"title": "Introduction", "unit": "Ecology", "chapter": "Ecosystems"},
+ "m63036": {"title": "Ecology for Ecosystems", "unit": "Ecology", "chapter": "Ecosystems"},
+ "m63037": {"title": "Energy Flow through Ecosystems", "unit": "Ecology", "chapter": "Ecosystems"},
+ "m63040": {"title": "Biogeochemical Cycles", "unit": "Ecology", "chapter": "Ecosystems"},
+ "m63043": {"title": "Organogenesis and Vertebrate Axis Formation", "unit": "Animal Structure and Function", "chapter": "Animal Reproduction and Development"},
+ "m63047": {"title": "Introduction", "unit": "Ecology", "chapter": "Conservation Biology and Biodiversity"},
+ "m63048": {"title": "The Biodiversity Crisis", "unit": "Ecology", "chapter": "Conservation Biology and Biodiversity"},
+ "m63049": {"title": "The Importance of Biodiversity to Human Life", "unit": "Ecology", "chapter": "Conservation Biology and Biodiversity"},
+ "m63050": {"title": "Threats to Biodiversity", "unit": "Ecology", "chapter": "Conservation Biology and Biodiversity"},
+ "m63051": {"title": "Preserving Biodiversity", "unit": "Ecology", "chapter": "Conservation Biology and Biodiversity"},
+ "m64279": {"title": "Preface", "unit": "Front Matter", "chapter": "Front Matter"},
+ "m66717": {"title": "Measurements and the Metric System", "unit": "Front Matter", "chapter": "Front Matter"},
+}
+
+# Expanded keyword mappings to module IDs
+# Keywords are lowercase and map directly to specific modules
+KEYWORD_TO_MODULES = {
+ # ==========================================================================
+ # CHAPTER 1: THE STUDY OF LIFE
+ # ==========================================================================
+ "biology": ["m62717", "m62718"],
+ "science": ["m62717"],
+ "scientific method": ["m62717"],
+ "hypothesis": ["m62717"],
+ "theory": ["m62717"],
+ "life": ["m62718"],
+ "living things": ["m62718"],
+ "characteristics of life": ["m62718"],
+
+ # ==========================================================================
+ # CHAPTER 2: THE CHEMICAL FOUNDATION OF LIFE
+ # ==========================================================================
+ "atom": ["m62720"],
+ "atoms": ["m62720"],
+ "element": ["m62720"],
+ "elements": ["m62720"],
+ "isotope": ["m62720"],
+ "isotopes": ["m62720"],
+ "ion": ["m62720"],
+ "ions": ["m62720"],
+ "molecule": ["m62720"],
+ "molecules": ["m62720"],
+ "chemical bond": ["m62720"],
+ "covalent bond": ["m62720"],
+ "ionic bond": ["m62720"],
+ "hydrogen bond": ["m62721"],
+ "water": ["m62721"],
+ "polarity": ["m62721"],
+ "cohesion": ["m62721"],
+ "adhesion": ["m62721"],
+ "solvent": ["m62721"],
+ "ph": ["m62721"],
+ "acid": ["m62721"],
+ "base": ["m62721"],
+ "buffer": ["m62721"],
+ "carbon": ["m62722"],
+ "organic": ["m62722"],
+ "organic molecule": ["m62722"],
+ "hydrocarbon": ["m62722"],
+ "functional group": ["m62722"],
+
+ # ==========================================================================
+ # CHAPTER 3: BIOLOGICAL MACROMOLECULES
+ # ==========================================================================
+ "macromolecule": ["m62724", "m62726", "m62730", "m62733", "m62735"],
+ "macromolecules": ["m62724", "m62726", "m62730", "m62733", "m62735"],
+ "polymer": ["m62724"],
+ "monomer": ["m62724"],
+ "dehydration synthesis": ["m62724"],
+ "hydrolysis": ["m62724", "m62768"],
+ "carbohydrate": ["m62726"],
+ "carbohydrates": ["m62726"],
+ "sugar": ["m62726"],
+ "sugars": ["m62726"],
+ "glucose": ["m62726", "m62787"],
+ "monosaccharide": ["m62726"],
+ "disaccharide": ["m62726"],
+ "polysaccharide": ["m62726"],
+ "starch": ["m62726"],
+ "glycogen": ["m62726"],
+ "cellulose": ["m62726"],
+ "lipid": ["m62730"],
+ "lipids": ["m62730"],
+ "fat": ["m62730"],
+ "fats": ["m62730"],
+ "fatty acid": ["m62730"],
+ "triglyceride": ["m62730"],
+ "phospholipid": ["m62730", "m62773"],
+ "steroid": ["m62730"],
+ "cholesterol": ["m62730"],
+ "protein": ["m62733", "m62843"],
+ "proteins": ["m62733", "m62843"],
+ "amino acid": ["m62733"],
+ "amino acids": ["m62733"],
+ "peptide": ["m62733"],
+ "peptide bond": ["m62733"],
+ "polypeptide": ["m62733"],
+ "protein structure": ["m62733"],
+ "primary structure": ["m62733"],
+ "secondary structure": ["m62733"],
+ "tertiary structure": ["m62733"],
+ "quaternary structure": ["m62733"],
+ "nucleic acid": ["m62735"],
+ "nucleic acids": ["m62735"],
+ "nucleotide": ["m62735"],
+ "nucleotides": ["m62735"],
+
+ # ==========================================================================
+ # CHAPTER 4: CELL STRUCTURE
+ # ==========================================================================
+ "cell": ["m62738", "m62740", "m62742"],
+ "cells": ["m62738", "m62740", "m62742"],
+ "cell theory": ["m62738"],
+ "microscope": ["m62738"],
+ "prokaryote": ["m62740", "m62891"],
+ "prokaryotes": ["m62740", "m62891"],
+ "prokaryotic": ["m62740", "m62808", "m62891"],
+ "prokaryotic cell": ["m62740"],
+ "eukaryote": ["m62742"],
+ "eukaryotes": ["m62742"],
+ "eukaryotic": ["m62742", "m62829"],
+ "eukaryotic cell": ["m62742"],
+ "organelle": ["m62742", "m62743"],
+ "organelles": ["m62742", "m62743"],
+ "nucleus": ["m62742"],
+ "nucleolus": ["m62742"],
+ "ribosome": ["m62742", "m62843"],
+ "ribosomes": ["m62742", "m62843"],
+ "mitochondria": ["m62742", "m62789"],
+ "mitochondrion": ["m62742", "m62789"],
+ "chloroplast": ["m62742", "m62794"],
+ "chloroplasts": ["m62742", "m62794"],
+ "endoplasmic reticulum": ["m62743"],
+ "rough er": ["m62743"],
+ "smooth er": ["m62743"],
+ "golgi apparatus": ["m62743"],
+ "golgi": ["m62743"],
+ "lysosome": ["m62743"],
+ "lysosomes": ["m62743"],
+ "vacuole": ["m62743"],
+ "peroxisome": ["m62743"],
+ "endomembrane system": ["m62743"],
+ "endomembrane": ["m62743"],
+ "cytoskeleton": ["m62744"],
+ "microtubule": ["m62744"],
+ "microfilament": ["m62744"],
+ "intermediate filament": ["m62744"],
+ "cilia": ["m62744"],
+ "flagella": ["m62744"],
+ "cell wall": ["m62746"],
+ "extracellular matrix": ["m62746"],
+ "cell junction": ["m62746"],
+ "tight junction": ["m62746"],
+ "gap junction": ["m62746"],
+ "plasmodesmata": ["m62746"],
+
+ # ==========================================================================
+ # CHAPTER 5: STRUCTURE AND FUNCTION OF PLASMA MEMBRANES
+ # ==========================================================================
+ "membrane": ["m62773", "m62753", "m62770"],
+ "plasma membrane": ["m62773"],
+ "cell membrane": ["m62773"],
+ "fluid mosaic model": ["m62773"],
+ "membrane protein": ["m62773"],
+ "transport": ["m62753", "m62770", "m62772"],
+ "passive transport": ["m62753"],
+ "active transport": ["m62770"],
+ "diffusion": ["m62753"],
+ "facilitated diffusion": ["m62753"],
+ "osmosis": ["m62753", "m63000"],
+ "tonicity": ["m62753"],
+ "hypertonic": ["m62753"],
+ "hypotonic": ["m62753"],
+ "isotonic": ["m62753"],
+ "sodium potassium pump": ["m62770"],
+ "electrochemical gradient": ["m62770"],
+ "endocytosis": ["m62772"],
+ "exocytosis": ["m62772"],
+ "phagocytosis": ["m62772"],
+ "pinocytosis": ["m62772"],
+ "bulk transport": ["m62772"],
+
+ # ==========================================================================
+ # CHAPTER 6: METABOLISM
+ # ==========================================================================
+ "metabolism": ["m62763", "m62778", "m62761"],
+ "metabolic": ["m62763"],
+ "energy": ["m62763", "m62768", "m62764", "m62786"],
+ "potential energy": ["m62764"],
+ "kinetic energy": ["m62764"],
+ "free energy": ["m62764"],
+ "gibbs free energy": ["m62764"],
+ "activation energy": ["m62764"],
+ "exergonic": ["m62764"],
+ "endergonic": ["m62764"],
+ "thermodynamics": ["m62767"],
+ "first law of thermodynamics": ["m62767"],
+ "second law of thermodynamics": ["m62767"],
+ "entropy": ["m62767"],
+ "atp": ["m62768", "m62763", "m62786"],
+ "adenosine triphosphate": ["m62768"],
+ "adp": ["m62768"],
+ "phosphorylation": ["m62768", "m62789"],
+ "enzyme": ["m62778"],
+ "enzymes": ["m62778"],
+ "catalyst": ["m62778"],
+ "active site": ["m62778"],
+ "substrate": ["m62778"],
+ "cofactor": ["m62778"],
+ "coenzyme": ["m62778"],
+ "inhibitor": ["m62778"],
+ "competitive inhibition": ["m62778"],
+ "noncompetitive inhibition": ["m62778"],
+ "allosteric": ["m62778"],
+ "feedback inhibition": ["m62778"],
+
+ # ==========================================================================
+ # CHAPTER 7: CELLULAR RESPIRATION
+ # ==========================================================================
+ "cellular respiration": ["m62786", "m62787", "m62788", "m62789"],
+ "respiration": ["m62786", "m62982", "m62987"],
+ "aerobic respiration": ["m62786", "m62789"],
+ "glycolysis": ["m62787"],
+ "pyruvate": ["m62787", "m62788"],
+ "citric acid cycle": ["m62788"],
+ "krebs cycle": ["m62788"],
+ "tca cycle": ["m62788"],
+ "acetyl coa": ["m62788"],
+ "nadh": ["m62788", "m62789"],
+ "fadh2": ["m62788", "m62789"],
+ "electron transport chain": ["m62789"],
+ "electron transport": ["m62789"],
+ "oxidative phosphorylation": ["m62789"],
+ "chemiosmosis": ["m62789"],
+ "atp synthase": ["m62789"],
+ "fermentation": ["m62790"],
+ "anaerobic": ["m62790"],
+ "anaerobic respiration": ["m62790"],
+ "lactic acid fermentation": ["m62790"],
+ "alcohol fermentation": ["m62790"],
+
+ # ==========================================================================
+ # CHAPTER 8: PHOTOSYNTHESIS
+ # ==========================================================================
+ "photosynthesis": ["m62794", "m62795", "m62796"],
+ "chlorophyll": ["m62794", "m62795"],
+ "pigment": ["m62794"],
+ "light reaction": ["m62795"],
+ "light reactions": ["m62795"],
+ "light dependent reactions": ["m62795"],
+ "photosystem": ["m62795"],
+ "photosystem i": ["m62795"],
+ "photosystem ii": ["m62795"],
+ "calvin cycle": ["m62796"],
+ "light independent reactions": ["m62796"],
+ "carbon fixation": ["m62796"],
+ "rubisco": ["m62796"],
+ "c3 plant": ["m62796"],
+ "c4 plant": ["m62796"],
+ "cam plant": ["m62796"],
+
+ # ==========================================================================
+ # CHAPTER 9: CELL COMMUNICATION
+ # ==========================================================================
+ "cell signaling": ["m62798", "m62799", "m62800"],
+ "cell communication": ["m62798", "m62799"],
+ "signal transduction": ["m62799"],
+ "signaling molecule": ["m62798"],
+ "ligand": ["m62798"],
+ "receptor": ["m62798"],
+ "receptor protein": ["m62798"],
+ "g protein": ["m62799"],
+ "second messenger": ["m62799"],
+ "camp": ["m62799"],
+ "signal cascade": ["m62799"],
+ "kinase": ["m62799"],
+ "phosphatase": ["m62799"],
+ "apoptosis": ["m62800"],
+ "programmed cell death": ["m62800"],
+
+ # ==========================================================================
+ # CHAPTER 10: CELL REPRODUCTION
+ # ==========================================================================
+ "cell division": ["m62803"],
+ "cell cycle": ["m62804", "m62805"],
+ "interphase": ["m62804"],
+ "mitosis": ["m62803", "m62804"],
+ "mitotic phase": ["m62803"],
+ "prophase": ["m62803"],
+ "metaphase": ["m62803"],
+ "anaphase": ["m62803"],
+ "telophase": ["m62803"],
+ "cytokinesis": ["m62803"],
+ "cell plate": ["m62803"],
+ "cleavage furrow": ["m62803"],
+ "spindle": ["m62803"],
+ "centrosome": ["m62803"],
+ "centriole": ["m62803"],
+ "checkpoint": ["m62805"],
+ "cyclin": ["m62805"],
+ "cdk": ["m62805"],
+ "cancer": ["m62806", "m62851"],
+ "tumor": ["m62806"],
+ "oncogene": ["m62806"],
+ "tumor suppressor": ["m62806"],
+ "p53": ["m62806"],
+ "binary fission": ["m62808"],
+
+ # ==========================================================================
+ # CHAPTER 11: MEIOSIS AND SEXUAL REPRODUCTION
+ # ==========================================================================
+ "meiosis": ["m62810"],
+ "meiosis i": ["m62810"],
+ "meiosis ii": ["m62810"],
+ "homologous chromosomes": ["m62810"],
+ "crossing over": ["m62810"],
+ "recombination": ["m62810"],
+ "synapsis": ["m62810"],
+ "tetrad": ["m62810"],
+ "chiasma": ["m62810"],
+ "independent assortment": ["m62810"],
+ "haploid": ["m62810", "m62811"],
+ "diploid": ["m62810", "m62811"],
+ "gamete": ["m62811"],
+ "gametes": ["m62811"],
+ "sexual reproduction": ["m62811"],
+ "asexual reproduction": ["m62811"],
+ "genetic variation": ["m62811"],
+
+ # ==========================================================================
+ # CHAPTER 12: MENDEL'S EXPERIMENTS AND HEREDITY
+ # ==========================================================================
+ "mendel": ["m62813"],
+ "mendelian genetics": ["m62813"],
+ "heredity": ["m62819", "m62813"],
+ "inheritance": ["m62819"],
+ "trait": ["m62817"],
+ "traits": ["m62817"],
+ "allele": ["m62817", "m62819"],
+ "alleles": ["m62817", "m62819"],
+ "dominant": ["m62817"],
+ "recessive": ["m62817"],
+ "genotype": ["m62817"],
+ "phenotype": ["m62817"],
+ "homozygous": ["m62817"],
+ "heterozygous": ["m62817"],
+ "punnett square": ["m62813"],
+ "law of segregation": ["m62819"],
+ "law of independent assortment": ["m62819"],
+ "monohybrid cross": ["m62813"],
+ "dihybrid cross": ["m62819"],
+ "test cross": ["m62813"],
+
+ # ==========================================================================
+ # CHAPTER 13: MODERN UNDERSTANDINGS OF INHERITANCE
+ # ==========================================================================
+ "chromosome": ["m62821", "m62822"],
+ "chromosomes": ["m62821", "m62822"],
+ "chromosomal theory": ["m62821"],
+ "linked genes": ["m62821"],
+ "linkage": ["m62821"],
+ "sex linkage": ["m62821"],
+ "x linked": ["m62821"],
+ "y linked": ["m62821"],
+ "sex chromosome": ["m62821"],
+ "autosome": ["m62821"],
+ "chromosomal disorder": ["m62822"],
+ "nondisjunction": ["m62822"],
+ "aneuploidy": ["m62822"],
+ "polyploidy": ["m62822"],
+ "down syndrome": ["m62822"],
+ "trisomy": ["m62822"],
+ "monosomy": ["m62822"],
+ "deletion": ["m62822"],
+ "duplication": ["m62822"],
+ "inversion": ["m62822"],
+ "translocation": ["m62822"],
+
+ # ==========================================================================
+ # CHAPTER 14: DNA STRUCTURE AND FUNCTION
+ # ==========================================================================
+ "dna": ["m62825", "m62826", "m62828", "m62829"],
+ "deoxyribonucleic acid": ["m62825"],
+ "double helix": ["m62825"],
+ "watson and crick": ["m62824"],
+ "chargaff": ["m62824"],
+ "base pair": ["m62825"],
+ "complementary base pairing": ["m62825"],
+ "adenine": ["m62825"],
+ "thymine": ["m62825"],
+ "guanine": ["m62825"],
+ "cytosine": ["m62825"],
+ "dna replication": ["m62826", "m62828", "m62829"],
+ "semiconservative replication": ["m62826"],
+ "origin of replication": ["m62826"],
+ "replication fork": ["m62826"],
+ "helicase": ["m62826"],
+ "dna polymerase": ["m62826", "m62828"],
+ "primase": ["m62826"],
+ "ligase": ["m62826"],
+ "leading strand": ["m62826"],
+ "lagging strand": ["m62826"],
+ "okazaki fragment": ["m62826"],
+ "telomere": ["m62829"],
+ "telomerase": ["m62829"],
+ "dna repair": ["m62830"],
+ "mismatch repair": ["m62830"],
+ "mutation": ["m62830"],
+
+ # ==========================================================================
+ # CHAPTER 15: GENES AND PROTEINS
+ # ==========================================================================
+ "genetic code": ["m62837"],
+ "codon": ["m62837"],
+ "anticodon": ["m62837"],
+ "start codon": ["m62837"],
+ "stop codon": ["m62837"],
+ "central dogma": ["m62837"],
+ "transcription": ["m62838", "m62840"],
+ "rna polymerase": ["m62838", "m62840"],
+ "promoter": ["m62838", "m62840"],
+ "terminator": ["m62838"],
+ "mrna": ["m62838", "m62842"],
+ "messenger rna": ["m62838", "m62842"],
+ "rna": ["m62842", "m62735"],
+ "rna processing": ["m62842"],
+ "splicing": ["m62842"],
+ "intron": ["m62842"],
+ "exon": ["m62842"],
+ "5 cap": ["m62842"],
+ "poly a tail": ["m62842"],
+ "translation": ["m62843"],
+ "protein synthesis": ["m62843"],
+ "trna": ["m62843"],
+ "transfer rna": ["m62843"],
+ "rrna": ["m62843"],
+ "ribosomal rna": ["m62843"],
+
+ # ==========================================================================
+ # CHAPTER 16: GENE EXPRESSION
+ # ==========================================================================
+ "gene": ["m62845", "m62837"],
+ "genes": ["m62845", "m62837"],
+ "gene expression": ["m62845"],
+ "gene regulation": ["m62845", "m62846", "m62847"],
+ "operon": ["m62846"],
+ "lac operon": ["m62846"],
+ "trp operon": ["m62846"],
+ "repressor": ["m62846"],
+ "inducer": ["m62846"],
+ "epigenetics": ["m62847"],
+ "epigenetic": ["m62847"],
+ "dna methylation": ["m62847"],
+ "histone modification": ["m62847"],
+ "chromatin": ["m62847"],
+ "transcription factor": ["m62848"],
+ "enhancer": ["m62848"],
+ "silencer": ["m62848"],
+ "alternative splicing": ["m62849"],
+ "microrna": ["m62849"],
+ "sirna": ["m62849"],
+ "rna interference": ["m62849"],
+
+ # ==========================================================================
+ # CHAPTER 17: BIOTECHNOLOGY AND GENOMICS
+ # ==========================================================================
+ "biotechnology": ["m62853"],
+ "genetic engineering": ["m62853"],
+ "recombinant dna": ["m62853"],
+ "restriction enzyme": ["m62853"],
+ "plasmid": ["m62853"],
+ "vector": ["m62853"],
+ "transformation": ["m62853"],
+ "pcr": ["m62853"],
+ "polymerase chain reaction": ["m62853"],
+ "gel electrophoresis": ["m62853"],
+ "cloning": ["m62853"],
+ "transgenic": ["m62853"],
+ "gmo": ["m62853"],
+ "crispr": ["m62853"],
+ "gene therapy": ["m62853"],
+ "genome": ["m62855", "m62857"],
+ "genomics": ["m62860", "m62861"],
+ "genome mapping": ["m62855"],
+ "physical map": ["m62855"],
+ "genetic map": ["m62855"],
+ "sequencing": ["m62857"],
+ "whole genome sequencing": ["m62857"],
+ "human genome project": ["m62857"],
+ "proteomics": ["m62861"],
+ "bioinformatics": ["m62860"],
+
+ # ==========================================================================
+ # CHAPTER 18: EVOLUTION AND THE ORIGIN OF SPECIES
+ # ==========================================================================
+ "evolution": ["m62863", "m62868"],
+ "darwin": ["m62863"],
+ "natural selection": ["m62871", "m63029"],
+ "descent with modification": ["m62863"],
+ "common ancestor": ["m62863"],
+ "adaptation": ["m62871"],
+ "fitness": ["m62871"],
+ "speciation": ["m62864"],
+ "species": ["m62864"],
+ "reproductive isolation": ["m62864"],
+ "prezygotic barrier": ["m62864"],
+ "postzygotic barrier": ["m62864"],
+ "allopatric speciation": ["m62864"],
+ "sympatric speciation": ["m62864"],
+ "adaptive radiation": ["m62865"],
+ "convergent evolution": ["m62865"],
+ "divergent evolution": ["m62865"],
+ "coevolution": ["m62865"],
+
+ # ==========================================================================
+ # CHAPTER 19: THE EVOLUTION OF POPULATIONS
+ # ==========================================================================
+ "population genetics": ["m62870"],
+ "gene pool": ["m62870"],
+ "allele frequency": ["m62870"],
+ "hardy weinberg": ["m62870"],
+ "hardy weinberg equilibrium": ["m62870"],
+ "genetic drift": ["m62868"],
+ "bottleneck effect": ["m62868"],
+ "founder effect": ["m62868"],
+ "gene flow": ["m62868"],
+ "mutation": ["m62868", "m62830"],
+ "nonrandom mating": ["m62868"],
+ "sexual selection": ["m62871"],
+ "directional selection": ["m62871"],
+ "stabilizing selection": ["m62871"],
+ "disruptive selection": ["m62871"],
+ "balancing selection": ["m62871"],
+
+ # ==========================================================================
+ # CHAPTER 20: PHYLOGENIES AND THE HISTORY OF LIFE
+ # ==========================================================================
+ "phylogeny": ["m62874", "m62903"],
+ "phylogenetic tree": ["m62874", "m62903"],
+ "cladogram": ["m62903"],
+ "taxonomy": ["m62874"],
+ "classification": ["m62874"],
+ "binomial nomenclature": ["m62874"],
+ "domain": ["m62874"],
+ "kingdom": ["m62874"],
+ "phylum": ["m62874"],
+ "class": ["m62874"],
+ "order": ["m62874"],
+ "family": ["m62874"],
+ "genus": ["m62874"],
+ "clade": ["m62903"],
+ "monophyletic": ["m62903"],
+ "homology": ["m62903"],
+ "analogy": ["m62903"],
+ "molecular clock": ["m62903"],
+
+ # ==========================================================================
+ # CHAPTER 21: VIRUSES
+ # ==========================================================================
+ "virus": ["m62881", "m62882", "m62904"],
+ "viruses": ["m62881", "m62882", "m62904"],
+ "viral": ["m62881", "m62882"],
+ "capsid": ["m62881"],
+ "viral envelope": ["m62881"],
+ "bacteriophage": ["m62881"],
+ "lytic cycle": ["m62882"],
+ "lysogenic cycle": ["m62882"],
+ "retrovirus": ["m62882"],
+ "reverse transcriptase": ["m62882"],
+ "hiv": ["m62882"],
+ "aids": ["m62882"],
+ "vaccine": ["m62904"],
+ "vaccination": ["m62904"],
+ "antiviral": ["m62904"],
+ "prion": ["m62887"],
+ "viroid": ["m62887"],
+
+ # ==========================================================================
+ # CHAPTER 22: PROKARYOTES: BACTERIA AND ARCHAEA
+ # ==========================================================================
+ "bacteria": ["m62891", "m62893", "m62896"],
+ "bacterial": ["m62891", "m62893", "m62896"],
+ "archaea": ["m62891"],
+ "prokaryotic diversity": ["m62891"],
+ "bacterial structure": ["m62893"],
+ "peptidoglycan": ["m62893"],
+ "gram positive": ["m62893"],
+ "gram negative": ["m62893"],
+ "bacterial metabolism": ["m62894"],
+ "nitrogen fixation": ["m62894"],
+ "bioremediation": ["m62897"],
+ "pathogen": ["m62896"],
+ "bacterial disease": ["m62896"],
+ "antibiotic": ["m62896"],
+ "antibiotic resistance": ["m62896"],
+
+ # ==========================================================================
+ # CHAPTER 23: PLANT FORM AND PHYSIOLOGY
+ # ==========================================================================
+ "plant": ["m62951", "m62905", "m62906", "m62908"],
+ "plants": ["m62951", "m62905", "m62906", "m62908"],
+ "plant body": ["m62951"],
+ "root": ["m62906"],
+ "roots": ["m62906"],
+ "root system": ["m62906"],
+ "stem": ["m62905"],
+ "stems": ["m62905"],
+ "shoot system": ["m62905"],
+ "leaf": ["m62908"],
+ "leaves": ["m62908"],
+ "vascular tissue": ["m62969"],
+ "xylem": ["m62969"],
+ "phloem": ["m62969"],
+ "transpiration": ["m62969"],
+ "stomata": ["m62908"],
+ "guard cell": ["m62908"],
+ "meristem": ["m62951"],
+ "apical meristem": ["m62951"],
+ "lateral meristem": ["m62951"],
+ "dermal tissue": ["m62951"],
+ "ground tissue": ["m62951"],
+ "plant hormone": ["m62930"],
+ "auxin": ["m62930"],
+ "gibberellin": ["m62930"],
+ "cytokinin": ["m62930"],
+ "tropism": ["m62930"],
+ "phototropism": ["m62930"],
+ "gravitropism": ["m62930"],
+ "photoperiodism": ["m62930"],
+
+ # ==========================================================================
+ # CHAPTER 24: THE ANIMAL BODY
+ # ==========================================================================
+ "animal": ["m62916", "m62918"],
+ "animal body": ["m62916"],
+ "body plan": ["m62916"],
+ "tissue": ["m62918"],
+ "tissues": ["m62918"],
+ "epithelial tissue": ["m62918"],
+ "connective tissue": ["m62918"],
+ "muscle tissue": ["m62918"],
+ "nervous tissue": ["m62918"],
+ "organ": ["m62916"],
+ "organ system": ["m62916"],
+ "homeostasis": ["m62931"],
+ "negative feedback": ["m62931"],
+ "positive feedback": ["m62931"],
+ "thermoregulation": ["m62931"],
+
+ # ==========================================================================
+ # CHAPTER 25: NUTRITION AND THE DIGESTIVE SYSTEM
+ # ==========================================================================
+ "digestive system": ["m62919", "m62921", "m62922"],
+ "digestive": ["m62919", "m62921"],
+ "digestion": ["m62919", "m62921"],
+ "nutrition": ["m62920"],
+ "nutrient": ["m62920"],
+ "nutrients": ["m62920"],
+ "vitamin": ["m62920"],
+ "mineral": ["m62920"],
+ "mouth": ["m62921"],
+ "esophagus": ["m62921"],
+ "stomach": ["m62921"],
+ "small intestine": ["m62921"],
+ "large intestine": ["m62921"],
+ "intestine": ["m62921"],
+ "liver": ["m62921"],
+ "pancreas": ["m62921", "m62995", "m62996"],
+ "gallbladder": ["m62921"],
+ "enzyme": ["m62921", "m62778"],
+ "peristalsis": ["m62921"],
+ "absorption": ["m62921"],
+ "villi": ["m62921"],
+
+ # ==========================================================================
+ # CHAPTER 26: THE NERVOUS SYSTEM
+ # ==========================================================================
+ "nervous system": ["m62924", "m62925", "m62926", "m62928"],
+ "neuron": ["m62924", "m62925"],
+ "neurons": ["m62924", "m62925"],
+ "nerve": ["m62924"],
+ "nerves": ["m62924"],
+ "glial cell": ["m62924"],
+ "dendrite": ["m62924"],
+ "axon": ["m62924"],
+ "myelin": ["m62924"],
+ "synapse": ["m62925"],
+ "neurotransmitter": ["m62925"],
+ "action potential": ["m62925"],
+ "resting potential": ["m62925"],
+ "depolarization": ["m62925"],
+ "repolarization": ["m62925"],
+ "brain": ["m62926"],
+ "cerebrum": ["m62926"],
+ "cerebellum": ["m62926"],
+ "brainstem": ["m62926"],
+ "spinal cord": ["m62926"],
+ "central nervous system": ["m62926"],
+ "cns": ["m62926"],
+ "peripheral nervous system": ["m62928"],
+ "pns": ["m62928"],
+ "autonomic nervous system": ["m62928"],
+ "sympathetic": ["m62928"],
+ "parasympathetic": ["m62928"],
+ "somatic nervous system": ["m62928"],
+ "reflex": ["m62928"],
+
+ # ==========================================================================
+ # CHAPTER 27: SENSORY SYSTEMS
+ # ==========================================================================
+ "sensory": ["m62994", "m62946", "m62957"],
+ "sensory system": ["m62994"],
+ "sensory receptor": ["m62994"],
+ "sensation": ["m62994"],
+ "perception": ["m62994"],
+ "somatosensation": ["m62946"],
+ "touch": ["m62946"],
+ "pain": ["m62946"],
+ "proprioception": ["m62946"],
+ "taste": ["m62947"],
+ "gustation": ["m62947"],
+ "smell": ["m62947"],
+ "olfaction": ["m62947"],
+ "hearing": ["m62954"],
+ "ear": ["m62954"],
+ "cochlea": ["m62954"],
+ "vestibular": ["m62954"],
+ "balance": ["m62954"],
+ "vision": ["m62957"],
+ "eye": ["m62957"],
+ "retina": ["m62957"],
+ "rod": ["m62957"],
+ "cone": ["m62957"],
+ "lens": ["m62957"],
+ "cornea": ["m62957"],
+ "photoreceptor": ["m62957"],
+
+ # ==========================================================================
+ # CHAPTER 28: THE ENDOCRINE SYSTEM
+ # ==========================================================================
+ "endocrine": ["m62961", "m62963", "m62995", "m62996", "m62971"],
+ "endocrine system": ["m62961", "m62963", "m62995", "m62996", "m62971"],
+ "hormone": ["m62961", "m62963", "m62971", "m62996"],
+ "hormones": ["m62961", "m62963", "m62971", "m62996"],
+ "gland": ["m62995"],
+ "glands": ["m62995"],
+ "endocrine gland": ["m62995"],
+ "exocrine gland": ["m62995"],
+ "pituitary": ["m62995", "m62971"],
+ "pituitary gland": ["m62995", "m62971"],
+ "hypothalamus": ["m62971", "m62995"],
+ "thyroid": ["m62995", "m62996"],
+ "thyroid gland": ["m62995"],
+ "parathyroid": ["m62995"],
+ "adrenal": ["m62995"],
+ "adrenal gland": ["m62995"],
+ "cortisol": ["m62995"],
+ "adrenaline": ["m62995"],
+ "epinephrine": ["m62995"],
+ "insulin": ["m62996", "m62995"],
+ "glucagon": ["m62996"],
+ "diabetes": ["m62996"],
+ "growth hormone": ["m62971"],
+ "testosterone": ["m62995"],
+ "estrogen": ["m62995"],
+ "progesterone": ["m62995"],
+ "feedback loop": ["m62971"],
+
+ # ==========================================================================
+ # CHAPTER 29: THE MUSCULOSKELETAL SYSTEM
+ # ==========================================================================
+ "musculoskeletal": ["m62977", "m62978", "m62980"],
+ "skeletal system": ["m62977", "m62978"],
+ "skeleton": ["m62977"],
+ "bone": ["m62978"],
+ "bones": ["m62978"],
+ "cartilage": ["m62978"],
+ "joint": ["m62979"],
+ "joints": ["m62979"],
+ "ligament": ["m62979"],
+ "tendon": ["m62979"],
+ "muscle": ["m62980"],
+ "muscles": ["m62980"],
+ "muscular system": ["m62980"],
+ "skeletal muscle": ["m62980"],
+ "smooth muscle": ["m62980"],
+ "cardiac muscle": ["m62980"],
+ "muscle contraction": ["m62980"],
+ "sarcomere": ["m62980"],
+ "actin": ["m62980"],
+ "myosin": ["m62980"],
+ "sliding filament": ["m62980"],
+
+ # ==========================================================================
+ # CHAPTER 30: THE RESPIRATORY SYSTEM
+ # ==========================================================================
+ "respiratory system": ["m62982", "m62987", "m62988"],
+ "respiratory": ["m62982", "m62987"],
+ "breathing": ["m62987"],
+ "lung": ["m62982"],
+ "lungs": ["m62982"],
+ "gas exchange": ["m62982", "m62998"],
+ "alveoli": ["m62982"],
+ "alveolus": ["m62982"],
+ "trachea": ["m62982"],
+ "bronchi": ["m62982"],
+ "bronchiole": ["m62982"],
+ "diaphragm": ["m62987"],
+ "inhalation": ["m62987"],
+ "exhalation": ["m62987"],
+ "ventilation": ["m62987"],
+ "hemoglobin": ["m62988"],
+ "oxygen transport": ["m62988"],
+ "carbon dioxide transport": ["m62988"],
+
+ # ==========================================================================
+ # CHAPTER 31: THE CIRCULATORY SYSTEM
+ # ==========================================================================
+ "circulatory system": ["m62990", "m62991", "m62992", "m62993"],
+ "circulatory": ["m62990", "m62992"],
+ "cardiovascular": ["m62990", "m62992"],
+ "heart": ["m62992"],
+ "cardiac": ["m62992"],
+ "blood": ["m62991", "m62993"],
+ "blood vessel": ["m62992"],
+ "artery": ["m62992"],
+ "vein": ["m62992"],
+ "capillary": ["m62992"],
+ "red blood cell": ["m62991"],
+ "white blood cell": ["m62991"],
+ "platelet": ["m62991"],
+ "plasma": ["m62991"],
+ "blood pressure": ["m62993"],
+ "pulse": ["m62993"],
+ "systole": ["m62992"],
+ "diastole": ["m62992"],
+ "atrium": ["m62992"],
+ "ventricle": ["m62992"],
+
+ # ==========================================================================
+ # CHAPTER 32: OSMOTIC REGULATION AND EXCRETION
+ # ==========================================================================
+ "osmoregulation": ["m63000", "m63004"],
+ "excretion": ["m63002"],
+ "excretory system": ["m63002"],
+ "kidney": ["m63001"],
+ "kidneys": ["m63001"],
+ "nephron": ["m63001"],
+ "glomerulus": ["m63001"],
+ "filtration": ["m63001"],
+ "reabsorption": ["m63001"],
+ "secretion": ["m63001"],
+ "urine": ["m63001"],
+ "urinary system": ["m63001"],
+ "bladder": ["m63001"],
+ "urea": ["m63003"],
+ "uric acid": ["m63003"],
+ "ammonia": ["m63003"],
+ "nitrogenous waste": ["m63003"],
+ "antidiuretic hormone": ["m63004"],
+ "adh": ["m63004"],
+ "aldosterone": ["m63004"],
+
+ # ==========================================================================
+ # CHAPTER 33: THE IMMUNE SYSTEM
+ # ==========================================================================
+ "immune system": ["m63006", "m63007", "m63008", "m63009"],
+ "immune": ["m63006", "m63007"],
+ "immunity": ["m63006", "m63007"],
+ "innate immunity": ["m63006"],
+ "adaptive immunity": ["m63007"],
+ "pathogen": ["m63006"],
+ "antigen": ["m63007"],
+ "antibody": ["m63008"],
+ "antibodies": ["m63008"],
+ "lymphocyte": ["m63007"],
+ "t cell": ["m63007"],
+ "b cell": ["m63007", "m63008"],
+ "helper t cell": ["m63007"],
+ "cytotoxic t cell": ["m63007"],
+ "memory cell": ["m63007"],
+ "mhc": ["m63007"],
+ "inflammation": ["m63006"],
+ "fever": ["m63006"],
+ "phagocyte": ["m63006"],
+ "macrophage": ["m63006"],
+ "neutrophil": ["m63006"],
+ "natural killer cell": ["m63006"],
+ "complement": ["m63006"],
+ "interferon": ["m63006"],
+ "allergy": ["m63009"],
+ "autoimmune": ["m63009"],
+ "autoimmune disease": ["m63009"],
+ "immunodeficiency": ["m63009"],
+
+ # ==========================================================================
+ # CHAPTER 34: ANIMAL REPRODUCTION AND DEVELOPMENT
+ # ==========================================================================
+ "reproduction": ["m63011", "m63013", "m63014"],
+ "reproductive": ["m63011", "m63013", "m63014"],
+ "reproductive system": ["m63013", "m63014", "m63018"],
+ "asexual reproduction": ["m63011"],
+ "sexual reproduction": ["m63011", "m62811"],
+ "fertilization": ["m63012", "m63016"],
+ "internal fertilization": ["m63012"],
+ "external fertilization": ["m63012"],
+ "gametogenesis": ["m63013"],
+ "spermatogenesis": ["m63013"],
+ "oogenesis": ["m63013"],
+ "sperm": ["m63013"],
+ "egg": ["m63013"],
+ "ovum": ["m63013"],
+ "testis": ["m63013"],
+ "ovary": ["m63013"],
+ "uterus": ["m63013"],
+ "menstrual cycle": ["m63014"],
+ "ovulation": ["m63014"],
+ "pregnancy": ["m63018"],
+ "embryo": ["m63016", "m63043"],
+ "embryonic development": ["m63016"],
+ "cleavage": ["m63016"],
+ "blastula": ["m63016"],
+ "gastrula": ["m63016"],
+ "gastrulation": ["m63016"],
+ "organogenesis": ["m63043"],
+ "germ layer": ["m63043"],
+ "ectoderm": ["m63043"],
+ "mesoderm": ["m63043"],
+ "endoderm": ["m63043"],
+ "placenta": ["m63018"],
+ "fetus": ["m63018"],
+ "labor": ["m63018"],
+ "birth": ["m63018"],
+
+ # ==========================================================================
+ # CHAPTER 35: ECOLOGY AND THE BIOSPHERE
+ # ==========================================================================
+ "ecology": ["m63021", "m63033", "m63036"],
+ "biosphere": ["m63021"],
+ "biogeography": ["m63023"],
+ "biome": ["m63024", "m63025"],
+ "biomes": ["m63024", "m63025"],
+ "terrestrial biome": ["m63024"],
+ "tropical rainforest": ["m63024"],
+ "savanna": ["m63024"],
+ "desert": ["m63024"],
+ "chaparral": ["m63024"],
+ "temperate grassland": ["m63024"],
+ "temperate forest": ["m63024"],
+ "boreal forest": ["m63024"],
+ "taiga": ["m63024"],
+ "tundra": ["m63024"],
+ "aquatic biome": ["m63025"],
+ "freshwater": ["m63025"],
+ "marine": ["m63025"],
+ "estuary": ["m63025"],
+ "coral reef": ["m63025"],
+ "climate": ["m63026"],
+ "climate change": ["m63026"],
+ "global warming": ["m63026"],
+ "greenhouse effect": ["m63026"],
+ "greenhouse gas": ["m63026"],
+
+ # ==========================================================================
+ # CHAPTER 36: POPULATION AND COMMUNITY ECOLOGY
+ # ==========================================================================
+ "population": ["m63028", "m63030", "m63031"],
+ "population ecology": ["m63028"],
+ "demography": ["m63028"],
+ "population growth": ["m63032"],
+ "exponential growth": ["m63030"],
+ "logistic growth": ["m63030"],
+ "carrying capacity": ["m63030"],
+ "density dependent": ["m63031"],
+ "density independent": ["m63031"],
+ "life history": ["m63029"],
+ "survivorship": ["m63028"],
+ "age structure": ["m63028"],
+ "human population": ["m63032"],
+ "community": ["m63033"],
+ "community ecology": ["m63033"],
+ "species interaction": ["m63033"],
+ "competition": ["m63033"],
+ "predation": ["m63033"],
+ "predator": ["m63033"],
+ "prey": ["m63033"],
+ "herbivory": ["m63033"],
+ "symbiosis": ["m63033"],
+ "mutualism": ["m63033"],
+ "commensalism": ["m63033"],
+ "parasitism": ["m63033"],
+ "ecological succession": ["m63033"],
+ "primary succession": ["m63033"],
+ "secondary succession": ["m63033"],
+ "keystone species": ["m63033"],
+ "behavior": ["m63034"],
+ "animal behavior": ["m63034"],
+ "innate behavior": ["m63034"],
+ "learned behavior": ["m63034"],
+
+ # ==========================================================================
+ # CHAPTER 37: ECOSYSTEMS
+ # ==========================================================================
+ "ecosystem": ["m63036", "m63037", "m63040"],
+ "ecosystems": ["m63036", "m63037"],
+ "energy flow": ["m63037"],
+ "food chain": ["m63037"],
+ "food web": ["m63037"],
+ "trophic level": ["m63037"],
+ "producer": ["m63037"],
+ "consumer": ["m63037"],
+ "primary consumer": ["m63037"],
+ "secondary consumer": ["m63037"],
+ "tertiary consumer": ["m63037"],
+ "decomposer": ["m63037"],
+ "detritivore": ["m63037"],
+ "gross primary productivity": ["m63037"],
+ "net primary productivity": ["m63037"],
+ "biomass": ["m63037"],
+ "energy pyramid": ["m63037"],
+ "biogeochemical cycle": ["m63040"],
+ "carbon cycle": ["m63040"],
+ "nitrogen cycle": ["m63040"],
+ "phosphorus cycle": ["m63040"],
+ "water cycle": ["m63040"],
+ "hydrologic cycle": ["m63040"],
+
+ # ==========================================================================
+ # CHAPTER 38: CONSERVATION BIOLOGY AND BIODIVERSITY
+ # ==========================================================================
+ "biodiversity": ["m63048", "m63049", "m63051"],
+ "conservation": ["m63051"],
+ "conservation biology": ["m63051"],
+ "extinction": ["m63048"],
+ "mass extinction": ["m63048"],
+ "endangered species": ["m63048"],
+ "threatened species": ["m63048"],
+ "habitat loss": ["m63050"],
+ "habitat fragmentation": ["m63050"],
+ "deforestation": ["m63050"],
+ "invasive species": ["m63050"],
+ "overexploitation": ["m63050"],
+ "pollution": ["m63050"],
+ "ecosystem services": ["m63049"],
+ "genetic diversity": ["m63049"],
+ "species diversity": ["m63049"],
+ "ecosystem diversity": ["m63049"],
+ "preserve": ["m63051"],
+ "wildlife corridor": ["m63051"],
+ "sustainable": ["m63051"],
+ "sustainability": ["m63051"],
+}
+
+
+def get_module_url(module_id: str) -> str:
+ """
+ Generate the OpenStax URL for a module.
+
+ Uses the pre-computed chapter slug mapping for accurate URLs
+ that match the actual OpenStax website structure.
+ """
+ # Use the chapter slug mapping for accurate URLs
+ if module_id in MODULE_TO_CHAPTER_SLUG:
+ slug = MODULE_TO_CHAPTER_SLUG[module_id]
+ return f"{OPENSTAX_BASE_URL}/{slug}"
+
+ # Fallback for modules not in mapping
+ if module_id not in MODULE_INDEX:
+ return f"{OPENSTAX_BASE_URL}/1-introduction"
+
+ info = MODULE_INDEX[module_id]
+ title = info["title"]
+
+ # Generate slug from title as last resort
+ slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
+
+ # For introduction pages, use chapter name
+ if title == "Introduction":
+ chapter = info["chapter"]
+ slug = re.sub(r'[^a-z0-9]+', '-', chapter.lower()).strip('-')
+
+ return f"{OPENSTAX_BASE_URL}/{slug}"
+
+
+def search_modules(topic: str, max_results: int = 3) -> list[dict]:
+ """
+ Search for modules matching a topic using keyword matching.
+
+ Args:
+ topic: The search topic
+ max_results: Maximum number of results to return
+
+ Returns:
+ List of module info dicts with id, title, unit, chapter, and url
+ """
+ import logging
+ logger = logging.getLogger(__name__)
+
+ logger.info("=" * 50)
+ logger.info("SEARCH_MODULES CALLED")
+ logger.info(f"Topic: '{topic}'")
+ logger.info(f"Max results: {max_results}")
+ logger.info("=" * 50)
+
+ topic_lower = topic.lower()
+ matched_ids = set()
+ matched_keywords = [] # Track which keywords matched for debugging
+
+ # First, check direct keyword matches using word boundaries
+ # to avoid matching "stem" inside "system"
+ for keyword, module_ids in KEYWORD_TO_MODULES.items():
+ # Use word boundary matching for single-word keywords
+ # For multi-word keywords, require exact substring match
+ if ' ' in keyword:
+ # Multi-word keyword - exact substring match is fine
+ if keyword in topic_lower:
+ matched_ids.update(module_ids)
+ matched_keywords.append(keyword)
+ else:
+ # Single-word keyword - require word boundaries
+ pattern = r'\b' + re.escape(keyword) + r'\b'
+ if re.search(pattern, topic_lower):
+ matched_ids.update(module_ids)
+ matched_keywords.append(keyword)
+
+ if matched_keywords:
+ logger.info(f"KEYWORD MATCHES FOUND: {matched_keywords}")
+ logger.info(f"Matched module IDs: {list(matched_ids)[:10]}{'...' if len(matched_ids) > 10 else ''}")
+ else:
+ logger.info("No keyword matches found, falling back to title search...")
+
+ # If no keyword matches, search titles
+ if not matched_ids:
+ for module_id, info in MODULE_INDEX.items():
+ title_lower = info["title"].lower()
+ chapter_lower = info["chapter"].lower()
+
+ # Check if any word from topic is in title or chapter
+ topic_words = set(re.findall(r'\b\w+\b', topic_lower))
+ title_words = set(re.findall(r'\b\w+\b', title_lower))
+ chapter_words = set(re.findall(r'\b\w+\b', chapter_lower))
+
+ if topic_words & title_words or topic_words & chapter_words:
+ matched_ids.add(module_id)
+
+ if matched_ids:
+ logger.info(f"Title search found {len(matched_ids)} modules")
+ else:
+ logger.warning(f"NO MATCHES FOUND for topic: '{topic}'")
+
+ # Convert to result list
+ results = []
+ for mid in list(matched_ids)[:max_results]:
+ if mid in MODULE_INDEX:
+ info = MODULE_INDEX[mid]
+ # Skip introduction modules unless specifically requested
+ if info["title"] == "Introduction" and "introduction" not in topic_lower:
+ continue
+ results.append({
+ "id": mid,
+ "title": info["title"],
+ "unit": info["unit"],
+ "chapter": info["chapter"],
+ "url": get_module_url(mid),
+ })
+
+ logger.info(f"Returning {len(results)} results: {[r['id'] for r in results]}")
+ return results[:max_results]
+
+
+def get_source_citation(module_ids: list[str]) -> dict:
+ """
+ Generate a source citation for a list of modules.
+
+ Returns a dict with url, title, and provider for attribution.
+ """
+ if not module_ids:
+ return {
+ "url": OPENSTAX_BASE_URL,
+ "title": "OpenStax Biology for AP Courses",
+ "provider": "OpenStax",
+ }
+
+ # Use the first module for the citation
+ mid = module_ids[0]
+ if mid not in MODULE_INDEX:
+ return {
+ "url": OPENSTAX_BASE_URL,
+ "title": "OpenStax Biology for AP Courses",
+ "provider": "OpenStax",
+ }
+
+ info = MODULE_INDEX[mid]
+ return {
+ "url": get_module_url(mid),
+ "title": f"{info['chapter']}: {info['title']}",
+ "provider": "OpenStax Biology for AP Courses",
+ }
diff --git a/samples/personalized_learning/agent/pyproject.toml b/samples/personalized_learning/agent/pyproject.toml
new file mode 100644
index 00000000..1f34087b
--- /dev/null
+++ b/samples/personalized_learning/agent/pyproject.toml
@@ -0,0 +1,24 @@
+[project]
+name = "personalized-learning-agent"
+version = "0.1.0"
+description = "A2A Agent for generating personalized A2UI learning materials"
+requires-python = ">=3.11"
+dependencies = [
+ "google-adk>=0.3.0",
+ "google-genai>=1.0.0",
+ "google-cloud-storage>=2.10.0",
+ "python-dotenv>=1.0.0",
+ "uvicorn>=0.24.0",
+ "fastapi>=0.104.0",
+ "pydantic>=2.5.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0.0",
+ "pytest-asyncio>=0.21.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/samples/personalized_learning/agent/requirements.txt b/samples/personalized_learning/agent/requirements.txt
new file mode 100644
index 00000000..70959914
--- /dev/null
+++ b/samples/personalized_learning/agent/requirements.txt
@@ -0,0 +1,9 @@
+google-adk>=0.3.0
+google-genai>=1.0.0
+google-cloud-storage>=2.10.0
+a2a-sdk>=0.2.0
+python-dotenv>=1.0.0
+uvicorn>=0.24.0
+fastapi>=0.104.0
+pydantic>=2.5.0
+litellm>=1.0.0
diff --git a/samples/personalized_learning/agent/server.py b/samples/personalized_learning/agent/server.py
new file mode 100644
index 00000000..b7c71f20
--- /dev/null
+++ b/samples/personalized_learning/agent/server.py
@@ -0,0 +1,152 @@
+"""
+FastAPI Server for Personalized Learning Agent
+
+Provides HTTP endpoints for the A2A agent that generates A2UI learning materials.
+This can run locally or be deployed to Agent Engine.
+"""
+
+import json
+import logging
+import os
+from typing import Any
+
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel
+
+from agent import get_agent, LearningMaterialAgent
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI(
+ title="Personalized Learning Agent",
+ description="A2A agent for generating personalized A2UI learning materials",
+ version="0.1.0",
+)
+
+# CORS for local development
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+class GenerateRequest(BaseModel):
+ """Request model for content generation."""
+
+ format: str
+ context: str = ""
+ session_id: str = "default"
+
+
+class A2ARequest(BaseModel):
+ """A2A protocol request model."""
+
+ message: str
+ session_id: str = "default"
+ extensions: list[str] = []
+
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint."""
+ return {"status": "healthy", "agent": "personalized-learning-agent"}
+
+
+@app.get("/capabilities")
+async def get_capabilities():
+ """Return agent capabilities for A2A discovery."""
+ return {
+ "name": "Personalized Learning Agent",
+ "description": "Generates personalized A2UI learning materials",
+ "supported_formats": LearningMaterialAgent.SUPPORTED_FORMATS,
+ "extensions": [
+ {
+ "uri": "https://a2ui.org/a2a-extension/a2ui/v0.8",
+ "description": "Provides agent driven UI using the A2UI JSON format.",
+ }
+ ],
+ }
+
+
+@app.post("/generate")
+async def generate_content(request: GenerateRequest):
+ """
+ Generate A2UI content for the specified format.
+
+ Args:
+ request: Generation request with format and optional context
+
+ Returns:
+ A2UI JSON response
+ """
+ logger.info(f"Generate request: format={request.format}, context={request.context[:50]}...")
+
+ agent = get_agent()
+ result = await agent.generate_content(request.format, request.context)
+
+ if "error" in result:
+ raise HTTPException(status_code=400, detail=result["error"])
+
+ return result
+
+
+@app.post("/a2a/stream")
+async def a2a_stream(request: A2ARequest):
+ """
+ A2A-compatible streaming endpoint.
+
+ Args:
+ request: A2A request with message
+
+ Returns:
+ Streaming response with A2UI JSON
+ """
+ logger.info(f"A2A stream request: {request.message}")
+
+ agent = get_agent()
+
+ async def generate():
+ async for chunk in agent.stream(request.message, request.session_id):
+ yield f"data: {json.dumps(chunk)}\n\n"
+
+ return StreamingResponse(
+ generate(),
+ media_type="text/event-stream",
+ )
+
+
+@app.post("/a2a/query")
+async def a2a_query(request: A2ARequest):
+ """
+ A2A-compatible non-streaming endpoint.
+
+ Args:
+ request: A2A request with message in format "type:context"
+
+ Returns:
+ A2UI JSON response
+ """
+ logger.info(f"A2A query request: {request.message}")
+
+ # Parse message (format: "type:context" or just "type")
+ parts = request.message.split(":", 1)
+ format_type = parts[0].strip()
+ context = parts[1].strip() if len(parts) > 1 else ""
+
+ agent = get_agent()
+ result = await agent.generate_content(format_type, context)
+
+ return result
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ port = int(os.getenv("PORT", "8081"))
+ uvicorn.run(app, host="0.0.0.0", port=port)
diff --git a/samples/personalized_learning/agent/tests/test_agent.py b/samples/personalized_learning/agent/tests/test_agent.py
new file mode 100644
index 00000000..67bfa344
--- /dev/null
+++ b/samples/personalized_learning/agent/tests/test_agent.py
@@ -0,0 +1,363 @@
+"""
+Unit and Integration Tests for Personalized Learning Agent
+
+Tests the context loader, A2UI templates, and agent functionality.
+"""
+
+import json
+import os
+import sys
+import asyncio
+from pathlib import Path
+
+# Add parent directory to path for imports
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from context_loader import (
+ load_context_file,
+ load_all_context,
+ get_learner_profile,
+ get_misconception_context,
+ get_combined_context,
+)
+from a2ui_templates import (
+ get_system_prompt,
+ FLASHCARD_EXAMPLE,
+ AUDIO_EXAMPLE,
+ VIDEO_EXAMPLE,
+ SURFACE_ID,
+)
+
+# =============================================================================
+# Test Results Tracking
+# =============================================================================
+
+passed = 0
+failed = 0
+
+
+def test(name):
+ """Decorator for test functions."""
+ def decorator(fn):
+ global passed, failed
+ try:
+ result = fn()
+ if asyncio.iscoroutine(result):
+ asyncio.run(result)
+ print(f"✓ {name}")
+ passed += 1
+ except AssertionError as e:
+ print(f"✗ {name}")
+ print(f" Error: {e}")
+ failed += 1
+ except Exception as e:
+ print(f"✗ {name}")
+ print(f" Exception: {type(e).__name__}: {e}")
+ failed += 1
+ return fn
+ return decorator
+
+
+# =============================================================================
+# Context Loader Tests
+# =============================================================================
+
+print("=" * 60)
+print("Personalized Learning Agent - Python Tests")
+print("=" * 60)
+print("\n--- Context Loader Tests ---\n")
+
+
+@test("load_context_file loads maria profile")
+def test_load_maria_profile():
+ content = load_context_file("01_maria_learner_profile.txt")
+ assert content is not None, "Content should not be None"
+ assert "Maria" in content, "Content should contain 'Maria'"
+ assert "MCAT" in content, "Content should contain 'MCAT'"
+
+
+@test("load_context_file loads misconception resolution")
+def test_load_misconception():
+ content = load_context_file("05_misconception_resolution.txt")
+ assert content is not None, "Content should not be None"
+ assert "ATP" in content, "Content should contain 'ATP'"
+ assert "bond" in content.lower(), "Content should mention bonds"
+
+
+@test("load_context_file returns None for missing file")
+def test_load_missing_file():
+ content = load_context_file("nonexistent_file.txt")
+ assert content is None, "Should return None for missing file"
+
+
+@test("load_all_context loads multiple files")
+def test_load_all_context():
+ context = load_all_context()
+ assert isinstance(context, dict), "Should return a dict"
+ assert len(context) >= 1, "Should load at least one file"
+ # Check that keys are filenames
+ for key in context.keys():
+ assert key.endswith(".txt"), f"Key {key} should be a .txt filename"
+
+
+@test("get_learner_profile returns Maria's profile")
+def test_get_learner_profile():
+ profile = get_learner_profile()
+ assert profile is not None, "Profile should not be None"
+ assert "Maria" in profile, "Profile should contain Maria"
+ assert "Cymbal" in profile, "Profile should mention Cymbal University"
+
+
+@test("get_misconception_context returns resolution content")
+def test_get_misconception_context():
+ content = get_misconception_context()
+ assert content is not None, "Content should not be None"
+ assert "misconception" in content.lower(), "Should discuss misconception"
+
+
+@test("get_combined_context combines all files")
+def test_get_combined_context():
+ combined = get_combined_context()
+ assert isinstance(combined, str), "Should return a string"
+ assert len(combined) > 1000, "Combined context should be substantial"
+ # Should contain section markers
+ assert "===" in combined, "Should contain section markers"
+
+
+# =============================================================================
+# A2UI Templates Tests
+# =============================================================================
+
+print("\n--- A2UI Templates Tests ---\n")
+
+
+@test("SURFACE_ID is set correctly")
+def test_surface_id():
+ assert SURFACE_ID == "learningContent", f"SURFACE_ID should be 'learningContent', got {SURFACE_ID}"
+
+
+@test("FLASHCARD_EXAMPLE contains valid A2UI structure")
+def test_flashcard_example():
+ assert "beginRendering" in FLASHCARD_EXAMPLE
+ assert "surfaceUpdate" in FLASHCARD_EXAMPLE
+ assert "Flashcard" in FLASHCARD_EXAMPLE
+ assert SURFACE_ID in FLASHCARD_EXAMPLE
+
+
+@test("AUDIO_EXAMPLE contains valid A2UI structure")
+def test_audio_example():
+ assert "beginRendering" in AUDIO_EXAMPLE
+ assert "surfaceUpdate" in AUDIO_EXAMPLE
+ assert "Audio" in AUDIO_EXAMPLE
+ assert "/assets/podcast.m4a" in AUDIO_EXAMPLE
+
+
+@test("VIDEO_EXAMPLE contains valid A2UI structure")
+def test_video_example():
+ assert "beginRendering" in VIDEO_EXAMPLE
+ assert "surfaceUpdate" in VIDEO_EXAMPLE
+ assert "Video" in VIDEO_EXAMPLE
+ assert "/assets/video.mp4" in VIDEO_EXAMPLE
+
+
+@test("get_system_prompt generates flashcards prompt")
+def test_system_prompt_flashcards():
+ context = "Test context for Maria"
+ prompt = get_system_prompt("flashcards", context)
+ assert "flashcards" in prompt.lower()
+ assert context in prompt
+ assert SURFACE_ID in prompt
+ assert "Flashcard" in prompt
+
+
+@test("get_system_prompt generates audio prompt")
+def test_system_prompt_audio():
+ context = "Test context"
+ prompt = get_system_prompt("audio", context)
+ assert "audio" in prompt.lower() or "Audio" in prompt
+ assert context in prompt
+
+
+@test("get_system_prompt includes learner context")
+def test_system_prompt_includes_context():
+ context = "Maria is a pre-med student with ATP misconception"
+ prompt = get_system_prompt("flashcards", context)
+ assert "Maria" in prompt
+ assert "ATP" in prompt
+
+
+# =============================================================================
+# Agent Tests
+# =============================================================================
+
+print("\n--- Agent Tests ---\n")
+
+# Import agent after context tests to ensure dependencies work
+try:
+ from agent import LearningMaterialAgent, get_agent
+ AGENT_AVAILABLE = True
+except ImportError as e:
+ print(f" (Skipping agent tests: {e})")
+ AGENT_AVAILABLE = False
+
+
+if AGENT_AVAILABLE:
+ # Create a test agent without initializing the Gemini client
+ # This allows testing static methods without credentials
+ _test_agent = LearningMaterialAgent(init_client=False)
+
+ @test("LearningMaterialAgent has correct supported formats")
+ def test_agent_formats():
+ assert "flashcards" in LearningMaterialAgent.SUPPORTED_FORMATS
+ assert "audio" in LearningMaterialAgent.SUPPORTED_FORMATS
+ assert "podcast" in LearningMaterialAgent.SUPPORTED_FORMATS
+ assert "video" in LearningMaterialAgent.SUPPORTED_FORMATS
+ assert "quiz" in LearningMaterialAgent.SUPPORTED_FORMATS
+
+
+ @test("agent._get_audio_reference returns valid A2UI")
+ def test_audio_reference():
+ result = _test_agent._get_audio_reference()
+ assert result["format"] == "audio"
+ assert result["surfaceId"] == SURFACE_ID
+ assert isinstance(result["a2ui"], list)
+ assert len(result["a2ui"]) == 2
+ assert "beginRendering" in result["a2ui"][0]
+ assert "surfaceUpdate" in result["a2ui"][1]
+
+
+ @test("agent._get_video_reference returns valid A2UI")
+ def test_video_reference():
+ result = _test_agent._get_video_reference()
+ assert result["format"] == "video"
+ assert result["surfaceId"] == SURFACE_ID
+ assert isinstance(result["a2ui"], list)
+ assert len(result["a2ui"]) == 2
+
+
+ @test("audio A2UI has all required components")
+ def test_audio_components():
+ result = _test_agent._get_audio_reference()
+ components = result["a2ui"][1]["surfaceUpdate"]["components"]
+ component_ids = {c["id"] for c in components}
+
+ # Check all required components exist
+ required = {"audioCard", "audioContent", "audioHeader", "audioIcon",
+ "audioTitle", "audioPlayer", "audioDescription"}
+ missing = required - component_ids
+ assert not missing, f"Missing components: {missing}"
+
+
+ @test("video A2UI has all required components")
+ def test_video_components():
+ result = _test_agent._get_video_reference()
+ components = result["a2ui"][1]["surfaceUpdate"]["components"]
+ component_ids = {c["id"] for c in components}
+
+ required = {"videoCard", "videoContent", "videoTitle", "videoPlayer", "videoDescription"}
+ missing = required - component_ids
+ assert not missing, f"Missing components: {missing}"
+
+
+# =============================================================================
+# A2UI JSON Validation Tests
+# =============================================================================
+
+print("\n--- A2UI JSON Validation Tests ---\n")
+
+
+def validate_a2ui_message(message):
+ """Validate a single A2UI message structure."""
+ valid_keys = {"beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"}
+ message_keys = set(message.keys())
+ action_keys = message_keys & valid_keys
+
+ if len(action_keys) != 1:
+ return False, f"Expected exactly one action key, got {len(action_keys)}"
+
+ action = list(action_keys)[0]
+
+ if action == "beginRendering":
+ br = message["beginRendering"]
+ if "surfaceId" not in br or "root" not in br:
+ return False, "beginRendering missing surfaceId or root"
+
+ elif action == "surfaceUpdate":
+ su = message["surfaceUpdate"]
+ if "surfaceId" not in su:
+ return False, "surfaceUpdate missing surfaceId"
+ if "components" not in su or not isinstance(su["components"], list):
+ return False, "surfaceUpdate missing components array"
+ for comp in su["components"]:
+ if "id" not in comp or "component" not in comp:
+ return False, f"Component missing id or component: {comp}"
+
+ return True, "OK"
+
+
+def validate_a2ui_payload(messages):
+ """Validate a complete A2UI payload."""
+ if not isinstance(messages, list):
+ return False, "Payload must be a list"
+ if len(messages) == 0:
+ return False, "Payload cannot be empty"
+ if "beginRendering" not in messages[0]:
+ return False, "First message must be beginRendering"
+
+ for i, msg in enumerate(messages):
+ valid, error = validate_a2ui_message(msg)
+ if not valid:
+ return False, f"Message {i}: {error}"
+
+ # Validate component references
+ all_ids = set()
+ references = []
+
+ for msg in messages:
+ if "surfaceUpdate" in msg:
+ for comp in msg["surfaceUpdate"]["components"]:
+ all_ids.add(comp["id"])
+ comp_def = comp["component"]
+ comp_type = list(comp_def.keys())[0]
+ props = comp_def[comp_type]
+
+ if isinstance(props, dict):
+ if "child" in props and isinstance(props["child"], str):
+ references.append((comp["id"], props["child"]))
+ if "children" in props and isinstance(props["children"], dict):
+ if "explicitList" in props["children"]:
+ for child_id in props["children"]["explicitList"]:
+ references.append((comp["id"], child_id))
+
+ for parent_id, child_id in references:
+ if child_id not in all_ids:
+ return False, f"Component {parent_id} references non-existent child: {child_id}"
+
+ return True, "OK"
+
+
+if AGENT_AVAILABLE:
+ @test("audio reference passes A2UI validation")
+ def test_validate_audio():
+ result = _test_agent._get_audio_reference()
+ valid, error = validate_a2ui_payload(result["a2ui"])
+ assert valid, f"Audio A2UI validation failed: {error}"
+
+
+ @test("video reference passes A2UI validation")
+ def test_validate_video():
+ result = _test_agent._get_video_reference()
+ valid, error = validate_a2ui_payload(result["a2ui"])
+ assert valid, f"Video A2UI validation failed: {error}"
+
+
+# =============================================================================
+# Summary
+# =============================================================================
+
+print("\n" + "=" * 60)
+print(f"Python Tests Complete: {passed} passed, {failed} failed")
+print("=" * 60)
+
+if failed > 0:
+ sys.exit(1)
diff --git a/samples/personalized_learning/agent/tests/test_caching.py b/samples/personalized_learning/agent/tests/test_caching.py
new file mode 100644
index 00000000..147c74d4
--- /dev/null
+++ b/samples/personalized_learning/agent/tests/test_caching.py
@@ -0,0 +1,200 @@
+"""
+Unit tests for caching functionality in the personalized learning agent.
+
+Tests:
+- Learner context caching (TTL-based)
+- OpenStax module content caching (TTL-based)
+"""
+
+import time
+import unittest
+from unittest.mock import patch, MagicMock
+
+# Import the modules we're testing
+import sys
+import os
+
+# Add parent directories to path for imports
+parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, parent_dir)
+
+# Direct imports of the module files
+import importlib.util
+
+# Load agent.py as a module
+agent_path = os.path.join(parent_dir, 'agent.py')
+spec = importlib.util.spec_from_file_location("agent_module", agent_path)
+agent_module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(agent_module)
+
+# Import openstax_content
+import openstax_content
+
+
+class TestContextCaching(unittest.TestCase):
+ """Tests for learner context caching in agent.py"""
+
+ def setUp(self):
+ """Reset the cache before each test."""
+ agent_module.clear_context_cache()
+
+ def test_context_cache_returns_cached_value(self):
+ """Verify second call returns cached content without reloading."""
+ # First call should load context
+ with patch.object(agent_module, '_safe_get_combined_context') as mock_get:
+ mock_get.return_value = "Test context content"
+
+ result1 = agent_module._get_cached_context()
+ self.assertEqual(result1, "Test context content")
+ self.assertEqual(mock_get.call_count, 1)
+
+ # Second call should use cache (mock not called again)
+ result2 = agent_module._get_cached_context()
+ self.assertEqual(result2, "Test context content")
+ self.assertEqual(mock_get.call_count, 1) # Still 1, not 2
+
+ def test_context_cache_expires_after_ttl(self):
+ """Verify cache expires and refetches after TTL."""
+ ttl = agent_module._CONTEXT_CACHE_TTL
+
+ with patch.object(agent_module, '_safe_get_combined_context') as mock_get:
+ with patch.object(agent_module.time, 'time') as mock_time:
+ # First call at time 0
+ mock_time.return_value = 0
+ mock_get.return_value = "Original content"
+
+ result1 = agent_module._get_cached_context()
+ self.assertEqual(result1, "Original content")
+ self.assertEqual(mock_get.call_count, 1)
+
+ # Second call still within TTL
+ mock_time.return_value = ttl - 1
+ result2 = agent_module._get_cached_context()
+ self.assertEqual(mock_get.call_count, 1) # Cache hit
+
+ # Third call after TTL expires
+ mock_time.return_value = ttl + 1
+ mock_get.return_value = "Updated content"
+
+ result3 = agent_module._get_cached_context()
+ self.assertEqual(result3, "Updated content")
+ self.assertEqual(mock_get.call_count, 2) # Cache miss, refetched
+
+ def test_clear_context_cache(self):
+ """Verify clear_context_cache empties the cache."""
+ with patch.object(agent_module, '_safe_get_combined_context') as mock_get:
+ mock_get.return_value = "Test content"
+
+ # Load into cache
+ agent_module._get_cached_context()
+ self.assertEqual(mock_get.call_count, 1)
+
+ # Clear cache
+ agent_module.clear_context_cache()
+
+ # Next call should reload
+ agent_module._get_cached_context()
+ self.assertEqual(mock_get.call_count, 2)
+
+
+class TestModuleCaching(unittest.TestCase):
+ """Tests for OpenStax module content caching in openstax_content.py"""
+
+ def setUp(self):
+ """Reset the module cache before each test."""
+ openstax_content.clear_module_cache()
+
+ def test_module_cache_hit(self):
+ """Verify cached module content is returned."""
+ with patch.object(openstax_content, 'fetch_module_content') as mock_fetch:
+ mock_fetch.return_value = "Module content for m12345"
+
+ # First call
+ result1 = openstax_content.fetch_module_content_cached("m12345")
+ self.assertEqual(result1, "Module content for m12345")
+ self.assertEqual(mock_fetch.call_count, 1)
+
+ # Second call should use cache
+ result2 = openstax_content.fetch_module_content_cached("m12345")
+ self.assertEqual(result2, "Module content for m12345")
+ self.assertEqual(mock_fetch.call_count, 1) # Still 1
+
+ def test_module_cache_miss_fetches_fresh(self):
+ """Verify cache miss triggers fresh fetch."""
+ with patch.object(openstax_content, 'fetch_module_content') as mock_fetch:
+ mock_fetch.return_value = "Content A"
+
+ # Fetch module A
+ result_a = openstax_content.fetch_module_content_cached("moduleA")
+ self.assertEqual(result_a, "Content A")
+
+ # Fetch different module B (cache miss)
+ mock_fetch.return_value = "Content B"
+ result_b = openstax_content.fetch_module_content_cached("moduleB")
+ self.assertEqual(result_b, "Content B")
+
+ # Both fetches should have occurred
+ self.assertEqual(mock_fetch.call_count, 2)
+
+ def test_module_cache_ttl_expiry(self):
+ """Verify module cache expires correctly."""
+ ttl = openstax_content._MODULE_CACHE_TTL
+
+ with patch.object(openstax_content, 'fetch_module_content') as mock_fetch:
+ with patch.object(openstax_content.time, 'time') as mock_time:
+ mock_time.return_value = 0
+ mock_fetch.return_value = "Old content"
+
+ # First fetch
+ result1 = openstax_content.fetch_module_content_cached("m99999")
+ self.assertEqual(result1, "Old content")
+ self.assertEqual(mock_fetch.call_count, 1)
+
+ # Within TTL - should use cache
+ mock_time.return_value = ttl - 1
+ result2 = openstax_content.fetch_module_content_cached("m99999")
+ self.assertEqual(mock_fetch.call_count, 1)
+
+ # After TTL expires
+ mock_time.return_value = ttl + 1
+ mock_fetch.return_value = "New content"
+
+ result3 = openstax_content.fetch_module_content_cached("m99999")
+ self.assertEqual(result3, "New content")
+ self.assertEqual(mock_fetch.call_count, 2)
+
+ def test_module_cache_handles_parse_flag(self):
+ """Verify parse flag creates separate cache entries."""
+ with patch.object(openstax_content, 'fetch_module_content') as mock_fetch:
+ # Fetch with parse=True
+ mock_fetch.return_value = "Parsed content"
+ result1 = openstax_content.fetch_module_content_cached("m11111", parse=True)
+ self.assertEqual(result1, "Parsed content")
+
+ # Fetch same module with parse=False (should be cache miss)
+ mock_fetch.return_value = "Raw content"
+ result2 = openstax_content.fetch_module_content_cached("m11111", parse=False)
+ self.assertEqual(result2, "Raw content")
+
+ # Both should have been fetched (different cache keys)
+ self.assertEqual(mock_fetch.call_count, 2)
+
+ def test_module_cache_handles_none_content(self):
+ """Verify None content is not cached."""
+ with patch.object(openstax_content, 'fetch_module_content') as mock_fetch:
+ mock_fetch.return_value = None
+
+ # First call returns None
+ result1 = openstax_content.fetch_module_content_cached("missing_module")
+ self.assertIsNone(result1)
+
+ # Second call should try again (not cached)
+ result2 = openstax_content.fetch_module_content_cached("missing_module")
+ self.assertIsNone(result2)
+
+ # Both calls should have tried to fetch
+ self.assertEqual(mock_fetch.call_count, 2)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/samples/personalized_learning/agent/tests/test_keyword_hints.py b/samples/personalized_learning/agent/tests/test_keyword_hints.py
new file mode 100644
index 00000000..3220702b
--- /dev/null
+++ b/samples/personalized_learning/agent/tests/test_keyword_hints.py
@@ -0,0 +1,221 @@
+"""
+Unit tests for KEYWORD_HINTS in openstax_chapters.py.
+
+Tests:
+- New keywords map correctly to expected chapters
+- Keyword matching is case insensitive
+- Expanded keywords reduce LLM fallback scenarios
+"""
+
+import unittest
+from unittest.mock import patch
+import sys
+import os
+
+# Add parent directories to path for imports
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+class TestKeywordHints(unittest.TestCase):
+ """Tests for KEYWORD_HINTS dictionary."""
+
+ def test_atp_keywords_map_correctly(self):
+ """Verify ATP-related keywords map to correct chapters."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ atp_keywords = [
+ "atp",
+ "adp",
+ "adenosine triphosphate",
+ "adenosine diphosphate",
+ "cellular energy",
+ "cell energy",
+ "high energy bond",
+ "phosphate bond",
+ "energy currency",
+ "atp hydrolysis",
+ "hydrolysis",
+ ]
+
+ for keyword in atp_keywords:
+ self.assertIn(keyword, KEYWORD_HINTS,
+ f"Keyword '{keyword}' should be in KEYWORD_HINTS")
+ chapters = KEYWORD_HINTS[keyword]
+ self.assertTrue(
+ any("atp" in ch or "energy" in ch for ch in chapters),
+ f"Keyword '{keyword}' should map to ATP or energy chapters, got {chapters}"
+ )
+
+ def test_thermodynamics_keywords_map_correctly(self):
+ """Verify thermodynamics keywords map to correct chapters."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ thermo_keywords = [
+ "thermodynamics",
+ "exergonic",
+ "endergonic",
+ "gibbs free energy",
+ "entropy",
+ ]
+
+ expected_chapters = [
+ "6-3-the-laws-of-thermodynamics",
+ "6-2-potential-kinetic-free-and-activation-energy",
+ ]
+
+ for keyword in thermo_keywords:
+ self.assertIn(keyword, KEYWORD_HINTS,
+ f"Keyword '{keyword}' should be in KEYWORD_HINTS")
+ chapters = KEYWORD_HINTS[keyword]
+ self.assertTrue(
+ any(ch in expected_chapters for ch in chapters),
+ f"Keyword '{keyword}' should map to thermodynamics chapters, got {chapters}"
+ )
+
+ def test_photosynthesis_keywords_map_correctly(self):
+ """Verify photosynthesis keywords map to correct chapters."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ photo_keywords = [
+ "photosynthesis",
+ "chloroplast",
+ "chlorophyll",
+ "calvin cycle",
+ "light reaction",
+ ]
+
+ for keyword in photo_keywords:
+ self.assertIn(keyword, KEYWORD_HINTS,
+ f"Keyword '{keyword}' should be in KEYWORD_HINTS")
+ chapters = KEYWORD_HINTS[keyword]
+ self.assertTrue(
+ any("8-" in ch or "photosynthesis" in ch for ch in chapters),
+ f"Keyword '{keyword}' should map to photosynthesis chapters (8-*), got {chapters}"
+ )
+
+ def test_keyword_matching_case_insensitive(self):
+ """Verify keyword matching works regardless of case."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ # All keywords should be lowercase in the dictionary
+ for keyword in KEYWORD_HINTS.keys():
+ self.assertEqual(keyword, keyword.lower(),
+ f"Keyword '{keyword}' should be lowercase")
+
+ def test_new_expanded_keywords_exist(self):
+ """Verify newly added keywords are present."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ # These are keywords that were added in the latency optimization
+ new_keywords = [
+ "adp",
+ "cellular energy",
+ "cell energy",
+ "high energy bond",
+ "phosphate bond",
+ "phosphate group",
+ "energy currency",
+ "energy transfer",
+ "bond breaking",
+ "bond energy",
+ "atp hydrolysis",
+ "exergonic",
+ "endergonic",
+ "gibbs free energy",
+ "thermodynamics",
+ "first law",
+ "second law",
+ "entropy",
+ ]
+
+ for keyword in new_keywords:
+ self.assertIn(keyword, KEYWORD_HINTS,
+ f"New keyword '{keyword}' should be in KEYWORD_HINTS")
+
+ def test_keyword_chapters_are_valid(self):
+ """Verify all keyword mappings point to valid chapters."""
+ from openstax_chapters import KEYWORD_HINTS, OPENSTAX_CHAPTERS
+
+ for keyword, chapters in KEYWORD_HINTS.items():
+ self.assertIsInstance(chapters, list,
+ f"Chapters for '{keyword}' should be a list")
+ self.assertGreater(len(chapters), 0,
+ f"Chapters for '{keyword}' should not be empty")
+
+ for chapter_slug in chapters:
+ self.assertIn(chapter_slug, OPENSTAX_CHAPTERS,
+ f"Chapter '{chapter_slug}' for keyword '{keyword}' "
+ "should be in OPENSTAX_CHAPTERS")
+
+ def test_common_topics_have_keywords(self):
+ """Verify common biology topics have keyword coverage."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ common_topics = [
+ "atp",
+ "dna",
+ "rna",
+ "protein",
+ "cell",
+ "enzyme",
+ "photosynthesis",
+ "respiration",
+ "mitosis",
+ "meiosis",
+ "evolution",
+ "genetics",
+ "nervous",
+ "immune",
+ "heart",
+ "lung",
+ ]
+
+ covered = 0
+ for topic in common_topics:
+ if topic in KEYWORD_HINTS:
+ covered += 1
+
+ coverage_pct = covered / len(common_topics) * 100
+ self.assertGreater(coverage_pct, 80,
+ f"Should have >80% keyword coverage for common topics, "
+ f"got {coverage_pct:.1f}%")
+
+
+class TestKeywordMatching(unittest.TestCase):
+ """Tests for keyword matching logic."""
+
+ def test_keyword_match_finds_chapters(self):
+ """Verify keyword matching finds the right chapters for common topics."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ # Test topics that SHOULD match keywords
+ test_cases = [
+ ("atp", ["6-4-atp-adenosine-triphosphate", "6-1-energy-and-metabolism"]),
+ ("photosynthesis", ["8-1-overview-of-photosynthesis", "8-2-the-light-dependent-reaction-of-photosynthesis"]),
+ ("dna", ["14-2-dna-structure-and-sequencing", "14-3-basics-of-dna-replication"]),
+ ]
+
+ for keyword, expected_chapters in test_cases:
+ self.assertIn(keyword, KEYWORD_HINTS,
+ f"Keyword '{keyword}' should be in KEYWORD_HINTS")
+ actual_chapters = KEYWORD_HINTS[keyword]
+ for expected in expected_chapters:
+ self.assertIn(expected, actual_chapters,
+ f"Expected chapter '{expected}' for keyword '{keyword}'")
+
+ def test_keyword_match_returns_list(self):
+ """Verify all keyword mappings return lists of chapters."""
+ from openstax_chapters import KEYWORD_HINTS
+
+ for keyword, chapters in KEYWORD_HINTS.items():
+ self.assertIsInstance(chapters, list,
+ f"Chapters for '{keyword}' should be a list")
+ self.assertGreater(len(chapters), 0,
+ f"Chapters list for '{keyword}' should not be empty")
+ for chapter in chapters:
+ self.assertIsInstance(chapter, str,
+ f"Each chapter slug should be a string")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/samples/personalized_learning/agent/tests/test_parallel_fetch.py b/samples/personalized_learning/agent/tests/test_parallel_fetch.py
new file mode 100644
index 00000000..212c27f1
--- /dev/null
+++ b/samples/personalized_learning/agent/tests/test_parallel_fetch.py
@@ -0,0 +1,223 @@
+"""
+Unit tests for parallel fetching functionality in openstax_content.py.
+
+Tests:
+- Parallel chapter fetching returns all content
+- Partial failures don't break entire fetch
+- Parallel is actually faster than sequential (with mocked delays)
+"""
+
+import time
+import unittest
+from unittest.mock import patch, MagicMock
+from concurrent.futures import ThreadPoolExecutor
+
+import sys
+import os
+
+# Add parent directories to path for imports
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+class TestParallelChapterFetch(unittest.TestCase):
+ """Tests for parallel chapter fetching in openstax_content.py"""
+
+ def setUp(self):
+ """Reset caches before each test."""
+ from openstax_content import clear_module_cache
+ clear_module_cache()
+
+ def test_parallel_chapter_fetch_returns_all_content(self):
+ """Verify parallel fetch returns same content as sequential would."""
+ from openstax_content import fetch_multiple_chapters
+
+ with patch('openstax_content.fetch_chapter_content') as mock_fetch:
+ # Set up mock to return different content for each chapter
+ def side_effect(slug):
+ return {
+ "chapter_slug": slug,
+ "title": f"Title for {slug}",
+ "url": f"https://example.com/{slug}",
+ "module_ids": [f"m{hash(slug) % 10000}"],
+ "content": f"Content for {slug}",
+ }
+
+ mock_fetch.side_effect = side_effect
+
+ # Fetch multiple chapters
+ chapters = ["6-4-atp", "7-2-glycolysis", "8-1-photosynthesis"]
+ results = fetch_multiple_chapters(chapters)
+
+ # Verify all chapters were fetched
+ self.assertEqual(len(results), 3)
+
+ # Verify content is correct
+ slugs = [r["chapter_slug"] for r in results]
+ self.assertIn("6-4-atp", slugs)
+ self.assertIn("7-2-glycolysis", slugs)
+ self.assertIn("8-1-photosynthesis", slugs)
+
+ def test_parallel_fetch_handles_partial_failures(self):
+ """Verify partial failures don't break entire fetch."""
+ from openstax_content import fetch_multiple_chapters
+
+ with patch('openstax_content.fetch_chapter_content') as mock_fetch:
+ # Set up mock where one chapter fails
+ def side_effect(slug):
+ if slug == "failing-chapter":
+ raise Exception("Simulated failure")
+ return {
+ "chapter_slug": slug,
+ "title": f"Title for {slug}",
+ "url": f"https://example.com/{slug}",
+ "module_ids": ["m12345"],
+ "content": f"Content for {slug}",
+ }
+
+ mock_fetch.side_effect = side_effect
+
+ # Fetch including one failing chapter
+ chapters = ["good-chapter-1", "failing-chapter", "good-chapter-2"]
+ results = fetch_multiple_chapters(chapters)
+
+ # Should still get the two good chapters
+ self.assertEqual(len(results), 2)
+ slugs = [r["chapter_slug"] for r in results]
+ self.assertIn("good-chapter-1", slugs)
+ self.assertIn("good-chapter-2", slugs)
+ self.assertNotIn("failing-chapter", slugs)
+
+ def test_parallel_fetch_handles_none_returns(self):
+ """Verify None returns are filtered out."""
+ from openstax_content import fetch_multiple_chapters
+
+ with patch('openstax_content.fetch_chapter_content') as mock_fetch:
+ # Set up mock where one chapter returns None
+ def side_effect(slug):
+ if slug == "missing-chapter":
+ return None
+ return {
+ "chapter_slug": slug,
+ "title": f"Title for {slug}",
+ "url": f"https://example.com/{slug}",
+ "module_ids": ["m12345"],
+ "content": f"Content for {slug}",
+ }
+
+ mock_fetch.side_effect = side_effect
+
+ chapters = ["chapter-1", "missing-chapter", "chapter-2"]
+ results = fetch_multiple_chapters(chapters)
+
+ # Should only get the two valid chapters
+ self.assertEqual(len(results), 2)
+
+ def test_single_chapter_no_threading_overhead(self):
+ """Verify single chapter fetch doesn't use threading."""
+ from openstax_content import fetch_multiple_chapters
+
+ with patch('openstax_content.fetch_chapter_content') as mock_fetch:
+ with patch('openstax_content.ThreadPoolExecutor') as mock_executor:
+ mock_fetch.return_value = {
+ "chapter_slug": "single",
+ "title": "Single Chapter",
+ "url": "https://example.com/single",
+ "module_ids": ["m12345"],
+ "content": "Content",
+ }
+
+ # Fetch single chapter
+ results = fetch_multiple_chapters(["single"])
+
+ # ThreadPoolExecutor should NOT be used for single chapter
+ mock_executor.assert_not_called()
+
+ # But fetch should still work
+ self.assertEqual(len(results), 1)
+
+ def test_empty_list_returns_empty(self):
+ """Verify empty input returns empty output."""
+ from openstax_content import fetch_multiple_chapters
+
+ results = fetch_multiple_chapters([])
+ self.assertEqual(results, [])
+
+ def test_parallel_fetch_faster_than_sequential(self):
+ """Verify parallel is actually faster with simulated delays."""
+ from openstax_content import fetch_multiple_chapters
+
+ def slow_fetch(slug):
+ """Simulate slow network fetch."""
+ time.sleep(0.1) # 100ms delay
+ return {
+ "chapter_slug": slug,
+ "title": f"Title for {slug}",
+ "url": f"https://example.com/{slug}",
+ "module_ids": ["m12345"],
+ "content": f"Content for {slug}",
+ }
+
+ with patch('openstax_content.fetch_chapter_content', side_effect=slow_fetch):
+ chapters = ["ch1", "ch2", "ch3"]
+
+ start = time.time()
+ results = fetch_multiple_chapters(chapters)
+ elapsed = time.time() - start
+
+ # With 3 chapters at 100ms each:
+ # - Sequential would take ~300ms
+ # - Parallel should take ~100-150ms
+ self.assertEqual(len(results), 3)
+
+ # Parallel should be significantly faster than sequential
+ # Allow some overhead, but should be under 250ms (vs 300ms sequential)
+ self.assertLess(elapsed, 0.25,
+ f"Parallel fetch took {elapsed:.3f}s, expected < 0.25s")
+
+
+class TestParallelModuleFetch(unittest.TestCase):
+ """Tests for parallel module fetching within chapters."""
+
+ def setUp(self):
+ """Reset caches before each test."""
+ from openstax_content import clear_module_cache
+ clear_module_cache()
+
+ def test_chapter_content_fetches_modules_in_parallel(self):
+ """Verify chapter content fetches multiple modules in parallel."""
+ from openstax_content import fetch_chapter_content
+
+ # Mock the chapter mapping to have multiple modules
+ mock_modules = {
+ "test-chapter": ["m1", "m2", "m3"],
+ }
+ mock_chapters = {
+ "test-chapter": "Test Chapter Title",
+ }
+
+ with patch('openstax_content.fetch_module_content_cached') as mock_fetch:
+ with patch.dict('openstax_chapters.CHAPTER_TO_MODULES', mock_modules):
+ with patch.dict('openstax_chapters.OPENSTAX_CHAPTERS', mock_chapters):
+ with patch('openstax_chapters.get_openstax_url_for_chapter',
+ return_value="https://example.com/test"):
+
+ # Each module returns different content
+ mock_fetch.side_effect = lambda mid: f"Content for {mid}"
+
+ # Import fresh to get patched values
+ from openstax_content import fetch_chapter_content as fetch_fn
+
+ result = fetch_fn("test-chapter")
+
+ # All 3 modules should have been fetched
+ self.assertEqual(mock_fetch.call_count, 3)
+
+ # Content should be combined
+ if result:
+ self.assertIn("Content for m1", result["content"])
+ self.assertIn("Content for m2", result["content"])
+ self.assertIn("Content for m3", result["content"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/samples/personalized_learning/api-server.ts b/samples/personalized_learning/api-server.ts
new file mode 100644
index 00000000..cdf93556
--- /dev/null
+++ b/samples/personalized_learning/api-server.ts
@@ -0,0 +1,992 @@
+/*
+ * API Server for Personalized Learning Demo
+ *
+ * Handles chat API requests using Gemini via VertexAI.
+ * Also proxies A2A requests to Agent Engine.
+ * Run with: npx tsx api-server.ts
+ *
+ * Required environment variables:
+ * GOOGLE_CLOUD_PROJECT - Your GCP project ID
+ * AGENT_ENGINE_PROJECT_NUMBER - Project number for Agent Engine
+ * AGENT_ENGINE_RESOURCE_ID - Resource ID of your deployed agent
+ *
+ * Optional environment variables:
+ * API_PORT - Server port (default: 8080)
+ * GOOGLE_CLOUD_LOCATION - GCP region (default: us-central1)
+ * GENAI_MODEL - Gemini model to use (default: gemini-2.5-flash)
+ */
+
+import { createServer } from "http";
+import { execSync } from "child_process";
+import { writeFileSync, readFileSync, existsSync } from "fs";
+import { join } from "path";
+import { config } from "dotenv";
+
+// Load environment variables
+config();
+
+// =============================================================================
+// MESSAGE LOG - Captures all request/response traffic for demo purposes
+// =============================================================================
+const LOG_FILE = "./demo-message-log.json";
+let messageLog: Array<{
+ sequence: number;
+ timestamp: string;
+ direction: "CLIENT_TO_SERVER" | "SERVER_TO_AGENT" | "AGENT_TO_SERVER" | "SERVER_TO_CLIENT";
+ endpoint: string;
+ data: unknown;
+}> = [];
+let sequenceCounter = 0;
+
+function logMessage(
+ direction: "CLIENT_TO_SERVER" | "SERVER_TO_AGENT" | "AGENT_TO_SERVER" | "SERVER_TO_CLIENT",
+ endpoint: string,
+ data: unknown
+) {
+ const entry = {
+ sequence: ++sequenceCounter,
+ timestamp: new Date().toISOString(),
+ direction,
+ endpoint,
+ data,
+ };
+ messageLog.push(entry);
+
+ // Write to file after each message for real-time viewing
+ writeFileSync(LOG_FILE, JSON.stringify(messageLog, null, 2));
+ console.log(`[LOG] #${entry.sequence} ${direction} → ${endpoint}`);
+}
+
+function resetLog() {
+ messageLog = [];
+ sequenceCounter = 0;
+ writeFileSync(LOG_FILE, "[]");
+ console.log(`[LOG] Reset log file: ${LOG_FILE}`);
+}
+
+// Reset log on server start
+resetLog();
+
+const PORT = parseInt(process.env.API_PORT || "8080");
+const PROJECT = process.env.GOOGLE_CLOUD_PROJECT;
+// Use us-central1 region for consistency with Agent Engine
+const LOCATION = process.env.GOOGLE_CLOUD_LOCATION || "us-central1";
+const MODEL = process.env.GENAI_MODEL || "gemini-2.5-flash";
+
+// Validate required environment variables
+if (!PROJECT) {
+ console.error("ERROR: GOOGLE_CLOUD_PROJECT environment variable is required");
+ process.exit(1);
+}
+
+// Agent Engine Configuration - set via environment variables
+// See QUICKSTART.md for deployment instructions
+// Note: Agent Engine is deployed in us-central1 (not global like Gemini API)
+const AGENT_ENGINE_CONFIG = {
+ projectNumber: process.env.AGENT_ENGINE_PROJECT_NUMBER || "",
+ location: process.env.AGENT_ENGINE_LOCATION || "us-central1",
+ resourceId: process.env.AGENT_ENGINE_RESOURCE_ID || "",
+};
+
+if (!AGENT_ENGINE_CONFIG.projectNumber || !AGENT_ENGINE_CONFIG.resourceId) {
+ console.warn("WARNING: AGENT_ENGINE_PROJECT_NUMBER and AGENT_ENGINE_RESOURCE_ID not set.");
+ console.warn(" Agent Engine features will not work. See QUICKSTART.md for setup.");
+}
+
+// =============================================================================
+// OpenStax Source Attribution - Maps topics to specific textbook sections
+// =============================================================================
+const OPENSTAX_BASE = "https://openstax.org/books/biology-ap-courses/pages/";
+
+const OPENSTAX_SECTIONS: Record = {
+ // ATP and Energy
+ "atp": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" },
+ "bond energy": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" },
+ "energy currency": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" },
+ "hydrolysis": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" },
+ // Thermodynamics
+ "thermodynamics": { slug: "6-3-the-laws-of-thermodynamics", title: "The Laws of Thermodynamics" },
+ "gibbs": { slug: "6-2-potential-kinetic-free-and-activation-energy", title: "Potential, Kinetic, Free, and Activation Energy" },
+ "free energy": { slug: "6-2-potential-kinetic-free-and-activation-energy", title: "Potential, Kinetic, Free, and Activation Energy" },
+ // Metabolism
+ "metabolism": { slug: "6-1-energy-and-metabolism", title: "Energy and Metabolism" },
+ "enzymes": { slug: "6-5-enzymes", title: "Enzymes" },
+ "glycolysis": { slug: "7-2-glycolysis", title: "Glycolysis" },
+ "krebs": { slug: "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle", title: "Oxidation of Pyruvate and the Citric Acid Cycle" },
+ "citric acid": { slug: "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle", title: "Oxidation of Pyruvate and the Citric Acid Cycle" },
+ "oxidative phosphorylation": { slug: "7-4-oxidative-phosphorylation", title: "Oxidative Phosphorylation" },
+ "electron transport": { slug: "7-4-oxidative-phosphorylation", title: "Oxidative Phosphorylation" },
+ // Photosynthesis
+ "photosynthesis": { slug: "8-1-overview-of-photosynthesis", title: "Overview of Photosynthesis" },
+ "light reactions": { slug: "8-2-the-light-dependent-reaction-of-photosynthesis", title: "The Light-Dependent Reactions" },
+ "calvin cycle": { slug: "8-3-using-light-to-make-organic-molecules", title: "Using Light to Make Organic Molecules" },
+ // Cell structure
+ "cell membrane": { slug: "5-1-components-and-structure", title: "Cell Membrane Components and Structure" },
+ "transport": { slug: "5-2-passive-transport", title: "Passive Transport" },
+ // Reproduction
+ "reproductive system": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" },
+ "reproductive": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" },
+ "reproduction": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" },
+ // Default - intentionally empty to avoid wrong citations
+ "default": { slug: "", title: "Biology Content" },
+};
+
+function getOpenStaxSource(topic: string): { provider: string; title: string; url: string } {
+ const topicLower = topic.toLowerCase();
+
+ // Find matching section
+ for (const [keyword, section] of Object.entries(OPENSTAX_SECTIONS)) {
+ if (keyword !== "default" && topicLower.includes(keyword)) {
+ return {
+ provider: "OpenStax Biology for AP Courses",
+ title: section.title,
+ url: OPENSTAX_BASE + section.slug,
+ };
+ }
+ }
+
+ // Default fallback
+ const defaultSection = OPENSTAX_SECTIONS["default"];
+ return {
+ provider: "OpenStax Biology for AP Courses",
+ title: defaultSection.title,
+ url: OPENSTAX_BASE + defaultSection.slug,
+ };
+}
+
+// Dynamic import for google genai (ESM)
+let genai: any = null;
+
+async function initGenAI() {
+ const { GoogleGenAI } = await import("@google/genai");
+ // Use VertexAI with Application Default Credentials
+ genai = new GoogleGenAI({
+ vertexai: true,
+ project: PROJECT,
+ location: LOCATION,
+ });
+ console.log(`[API Server] Using VertexAI: ${PROJECT}/${LOCATION}`);
+ console.log(`[API Server] Model: ${MODEL}`);
+}
+
+interface ChatMessage {
+ role: string;
+ parts: { text: string }[];
+}
+
+interface ChatRequest {
+ systemPrompt: string;
+ intentGuidance: string;
+ messages: ChatMessage[];
+ userMessage: string;
+}
+
+// =============================================================================
+// ACCESS TOKEN CACHING
+// =============================================================================
+let cachedAccessToken: { token: string; expiresAt: number } | null = null;
+
+// Get Google Cloud access token with caching
+// In Cloud Run, use the metadata server. Locally, use gcloud CLI.
+async function getAccessToken(): Promise {
+ // Check cache first
+ const now = Date.now();
+ if (cachedAccessToken && cachedAccessToken.expiresAt > now + 60000) {
+ // Return cached token if it has more than 1 minute of validity
+ return cachedAccessToken.token;
+ }
+
+ // Try metadata server first (Cloud Run environment)
+ try {
+ const metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
+ const response = await fetch(metadataUrl, {
+ headers: { "Metadata-Flavor": "Google" },
+ });
+ if (response.ok) {
+ const data = await response.json();
+ // Cache the token (typical expiry is 1 hour, we use 58 minutes to be safe)
+ cachedAccessToken = {
+ token: data.access_token,
+ expiresAt: now + 3480000, // 58 minutes
+ };
+ console.log("[API Server] Cached access token from metadata server");
+ return data.access_token;
+ }
+ } catch {
+ // Not in Cloud Run, fall through to gcloud
+ }
+
+ // Fall back to gcloud CLI (local development)
+ try {
+ const token = execSync("gcloud auth print-access-token", {
+ encoding: "utf-8",
+ }).trim();
+ // Cache the token
+ cachedAccessToken = {
+ token: token,
+ expiresAt: now + 3480000, // 58 minutes
+ };
+ console.log("[API Server] Cached access token from gcloud CLI");
+ return token;
+ } catch (error) {
+ console.error("[API Server] Failed to get access token:", error);
+ throw new Error("Failed to get Google Cloud access token. Run: gcloud auth login");
+ }
+}
+
+// Query Agent Engine for A2UI content using streamQuery
+async function queryAgentEngine(format: string, context: string = ""): Promise {
+ const { projectNumber, location, resourceId } = AGENT_ENGINE_CONFIG;
+ // Use :streamQuery endpoint with stream_query method for ADK agents
+ const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectNumber}/locations/${location}/reasoningEngines/${resourceId}:streamQuery`;
+
+ const accessToken = await getAccessToken();
+
+ // For audio/video, don't include topic context - we only have one pre-built podcast/video
+ // Including a topic might confuse the agent into thinking we want topic-specific content
+ let message: string;
+ if (format === "podcast" || format === "audio" || format === "video") {
+ message = format === "video" ? "Play the video" : "Play the podcast";
+ } else {
+ message = context ? `Generate ${format} for: ${context}` : `Generate ${format}`;
+ }
+
+ console.log(`[API Server] Querying Agent Engine: ${format}`);
+ console.log(`[API Server] URL: ${url}`);
+
+ // Build the request payload
+ const requestPayload = {
+ class_method: "stream_query",
+ input: {
+ user_id: "demo-user",
+ message: message,
+ },
+ };
+
+ // LOG: Server → Agent Engine request
+ logMessage("SERVER_TO_AGENT", "Vertex AI Agent Engine (A2UI Generator)", {
+ description: "Request to remote A2UI-generating agent",
+ agentUrl: url,
+ payload: requestPayload,
+ });
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(requestPayload),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error("[API Server] Agent Engine error:", errorText);
+ throw new Error(`Agent Engine error: ${response.status}`);
+ }
+
+ // Parse the newline-delimited JSON response
+ const responseText = await response.text();
+ console.log("[API Server] Agent Engine response length:", responseText.length);
+ console.log("[API Server] Raw response (first 1000 chars):", responseText.substring(0, 1000));
+
+ // LOG: Agent Engine → Server raw response
+ logMessage("AGENT_TO_SERVER", "Vertex AI Agent Engine (raw)", {
+ description: "Raw streaming response from Agent Engine (newline-delimited JSON)",
+ responseLength: responseText.length,
+ rawChunks: responseText.trim().split("\n").slice(0, 5).map(chunk => {
+ try { return JSON.parse(chunk); } catch { return chunk.substring(0, 200); }
+ }),
+ note: "Showing first 5 chunks only",
+ });
+
+ // Extract text from all chunks
+ const chunks = responseText.trim().split("\n").filter((line: string) => line.trim());
+ let fullText = "";
+
+ let functionResponseResult = ""; // Prioritize function_response over text
+ let textParts = "";
+
+ for (const chunk of chunks) {
+ try {
+ const parsed = JSON.parse(chunk);
+ console.log("[API Server] Parsed chunk keys:", Object.keys(parsed));
+
+ // Extract from content.parts - can contain text, function_call, or function_response
+ if (parsed.content?.parts) {
+ for (const part of parsed.content.parts) {
+ // Check for function_response which contains the tool result (prioritize this)
+ if (part.function_response?.response?.result) {
+ const result = part.function_response.response.result;
+ console.log("[API Server] Found function_response result:", result.substring(0, 200));
+ functionResponseResult += result;
+ } else if (part.text) {
+ console.log("[API Server] Found text part:", part.text.substring(0, 100));
+ textParts += part.text;
+ }
+ }
+ }
+ } catch (e) {
+ console.warn("[API Server] Failed to parse chunk:", chunk.substring(0, 100));
+ }
+ }
+
+ // Prefer function_response result over text parts (agent text often just wraps the same data)
+ fullText = functionResponseResult || textParts;
+
+ console.log("[API Server] Extracted text:", fullText.substring(0, 300));
+
+ // Try to parse A2UI JSON from the response
+ try {
+ // Strip markdown code blocks if present
+ let cleaned = fullText.trim();
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
+ cleaned = cleaned.trim();
+
+ console.log("[API Server] Cleaned text:", cleaned.substring(0, 200));
+
+ // Helper to extract A2UI content and source info from various formats
+ const extractA2UIWithSource = (text: string): { a2ui: unknown[] | null; source?: { url: string; title: string; provider: string } } => {
+ // Try parsing as raw JSON array (legacy format)
+ if (text.startsWith("[")) {
+ try {
+ const parsed = JSON.parse(text);
+ if (Array.isArray(parsed)) return { a2ui: parsed };
+ } catch {}
+ }
+
+ // Try parsing as object with a2ui and source (new format)
+ if (text.startsWith("{")) {
+ try {
+ const wrapper = JSON.parse(text);
+ // New format: {a2ui: [...], source: {...}}
+ if (wrapper.a2ui && Array.isArray(wrapper.a2ui)) {
+ return { a2ui: wrapper.a2ui, source: wrapper.source || undefined };
+ }
+ // Legacy format: {"result": "..."}
+ if (wrapper.result) {
+ const inner = typeof wrapper.result === 'string'
+ ? JSON.parse(wrapper.result)
+ : wrapper.result;
+ // Check if inner is the new format
+ if (inner && inner.a2ui && Array.isArray(inner.a2ui)) {
+ return { a2ui: inner.a2ui, source: inner.source || undefined };
+ }
+ if (Array.isArray(inner)) return { a2ui: inner };
+ }
+ } catch {}
+ }
+
+ // Try to find and extract JSON array from text
+ const arrayMatch = text.match(/\[\s*\{[\s\S]*?\}\s*\]/);
+ if (arrayMatch) {
+ try {
+ const parsed = JSON.parse(arrayMatch[0]);
+ if (Array.isArray(parsed)) return { a2ui: parsed };
+ } catch {}
+ }
+
+ // Try to extract result field with regex and parse its content
+ const resultMatch = text.match(/"result"\s*:\s*"((?:[^"\\]|\\.)*)"/);
+ if (resultMatch) {
+ try {
+ // Unescape the JSON string
+ const unescaped = resultMatch[1]
+ .replace(/\\"/g, '"')
+ .replace(/\\\\/g, '\\');
+ const parsed = JSON.parse(unescaped);
+ // Check if parsed is new format
+ if (parsed && parsed.a2ui && Array.isArray(parsed.a2ui)) {
+ return { a2ui: parsed.a2ui, source: parsed.source || undefined };
+ }
+ if (Array.isArray(parsed)) return { a2ui: parsed };
+ } catch {}
+ }
+
+ return { a2ui: null };
+ };
+
+ const extracted = extractA2UIWithSource(cleaned);
+ if (extracted.a2ui) {
+ const result = {
+ format,
+ surfaceId: "learningContent",
+ a2ui: extracted.a2ui,
+ source: extracted.source,
+ };
+
+ // LOG: Parsed A2UI content
+ logMessage("AGENT_TO_SERVER", "Vertex AI Agent Engine (parsed A2UI)", {
+ description: "Successfully parsed A2UI JSON from agent response",
+ format,
+ surfaceId: "learningContent",
+ source: extracted.source,
+ a2uiMessageCount: extracted.a2ui.length,
+ a2uiMessages: extracted.a2ui,
+ });
+
+ return result;
+ }
+
+ // Return raw text if no JSON found
+ return {
+ format,
+ surfaceId: "learningContent",
+ a2ui: [],
+ rawText: fullText,
+ };
+ } catch (e) {
+ console.error("[API Server] Failed to parse agent response:", e);
+ return {
+ format,
+ surfaceId: "learningContent",
+ a2ui: [],
+ rawText: fullText,
+ error: "Failed to parse A2UI JSON",
+ };
+ }
+}
+
+/**
+ * Generate QuizCard content locally using Gemini.
+ * This is used when Agent Engine doesn't have QuizCard support.
+ */
+async function generateLocalQuiz(topic: string): Promise {
+ const systemPrompt = `You are creating MCAT practice quiz questions for Maria, a pre-med student who loves sports/gym analogies.
+
+Create 2 interactive quiz questions about "${topic || 'ATP and bond energy'}" that:
+1. Test understanding with MCAT-style questions
+2. Include plausible wrong answers reflecting common misconceptions
+3. Provide detailed explanations using sports/gym analogies
+4. Use precise scientific language
+
+Output ONLY valid JSON in this EXACT format (no markdown, no explanation):
+
+[
+ {"beginRendering": {"surfaceId": "learningContent", "root": "mainColumn"}},
+ {
+ "surfaceUpdate": {
+ "surfaceId": "learningContent",
+ "components": [
+ {
+ "id": "mainColumn",
+ "component": {
+ "Column": {
+ "children": {"explicitList": ["headerText", "quizRow"]},
+ "distribution": "start",
+ "alignment": "stretch"
+ }
+ }
+ },
+ {
+ "id": "headerText",
+ "component": {
+ "Text": {
+ "text": {"literalString": "Quick Quiz: [TOPIC]"},
+ "usageHint": "h3"
+ }
+ }
+ },
+ {
+ "id": "quizRow",
+ "component": {
+ "Row": {
+ "children": {"explicitList": ["quiz1", "quiz2"]},
+ "distribution": "start",
+ "alignment": "stretch"
+ }
+ }
+ },
+ {
+ "id": "quiz1",
+ "component": {
+ "QuizCard": {
+ "question": {"literalString": "[QUESTION 1]"},
+ "options": [
+ {"label": {"literalString": "[OPTION A]"}, "value": "a", "isCorrect": false},
+ {"label": {"literalString": "[OPTION B - CORRECT]"}, "value": "b", "isCorrect": true},
+ {"label": {"literalString": "[OPTION C]"}, "value": "c", "isCorrect": false},
+ {"label": {"literalString": "[OPTION D]"}, "value": "d", "isCorrect": false}
+ ],
+ "explanation": {"literalString": "[DETAILED EXPLANATION WITH ANALOGY]"},
+ "category": {"literalString": "[CATEGORY]"}
+ }
+ }
+ },
+ {
+ "id": "quiz2",
+ "component": {
+ "QuizCard": {
+ "question": {"literalString": "[QUESTION 2]"},
+ "options": [
+ {"label": {"literalString": "[OPTION A]"}, "value": "a", "isCorrect": false},
+ {"label": {"literalString": "[OPTION B]"}, "value": "b", "isCorrect": false},
+ {"label": {"literalString": "[OPTION C - CORRECT]"}, "value": "c", "isCorrect": true},
+ {"label": {"literalString": "[OPTION D]"}, "value": "d", "isCorrect": false}
+ ],
+ "explanation": {"literalString": "[DETAILED EXPLANATION WITH ANALOGY]"},
+ "category": {"literalString": "[CATEGORY]"}
+ }
+ }
+ }
+ ]
+ }
+ }
+]
+
+Replace all [BRACKETED] placeholders with actual content. Vary which option is correct.`;
+
+ try {
+ const response = await genai.models.generateContent({
+ model: MODEL,
+ contents: [{ role: "user", parts: [{ text: `Generate quiz questions about: ${topic || 'ATP and bond energy'}` }] }],
+ config: {
+ systemInstruction: systemPrompt,
+ responseMimeType: "application/json",
+ },
+ });
+
+ const text = response.text?.trim() || "";
+ console.log("[API Server] Local quiz generation response:", text.substring(0, 500));
+
+ // Parse the JSON
+ let cleaned = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
+ let parsed = JSON.parse(cleaned);
+
+ // Handle case where Gemini wraps the A2UI array in an object
+ // We only want the A2UI messages array, not any wrapper object
+ let a2ui: unknown[];
+ if (Array.isArray(parsed)) {
+ a2ui = parsed;
+ } else if (parsed.a2ui && Array.isArray(parsed.a2ui)) {
+ a2ui = parsed.a2ui;
+ } else if (parsed.messages && Array.isArray(parsed.messages)) {
+ a2ui = parsed.messages;
+ } else {
+ console.error("[API Server] Unexpected quiz format from Gemini:", Object.keys(parsed));
+ return null;
+ }
+
+ // Match topic to specific OpenStax section for better attribution
+ const source = getOpenStaxSource(topic);
+ return {
+ format: "quiz",
+ surfaceId: "learningContent",
+ a2ui: a2ui,
+ source,
+ };
+ } catch (error) {
+ console.error("[API Server] Local quiz generation failed:", error);
+ return null;
+ }
+}
+
+async function handleChatRequest(request: ChatRequest): Promise<{ text: string }> {
+ const { systemPrompt, intentGuidance, messages, userMessage } = request;
+
+ // Build the full system instruction
+ const fullSystemPrompt = `${systemPrompt}\n\n${intentGuidance}`;
+
+ // Convert messages to Gemini format
+ const contents = messages.map((m) => ({
+ role: m.role === "assistant" ? "model" : "user",
+ parts: m.parts,
+ }));
+
+ // Add the current user message
+ contents.push({
+ role: "user",
+ parts: [{ text: userMessage }],
+ });
+
+ try {
+ const response = await genai.models.generateContent({
+ model: MODEL,
+ contents,
+ config: {
+ systemInstruction: fullSystemPrompt,
+ },
+ });
+
+ const text = response.text || "I apologize, I couldn't generate a response.";
+ return { text };
+ } catch (error) {
+ console.error("[API Server] Error calling Gemini:", error);
+ throw error;
+ }
+}
+
+// =============================================================================
+// COMBINED INTENT + RESPONSE ENDPOINT
+// Combines intent detection and response generation in a single LLM call
+// =============================================================================
+interface CombinedChatRequest {
+ systemPrompt: string;
+ messages: ChatMessage[];
+ userMessage: string;
+ recentContext?: string;
+}
+
+interface CombinedChatResponse {
+ intent: string;
+ text: string;
+ keywords?: string; // Comma-separated keywords for content-generating intents
+}
+
+async function handleCombinedChatRequest(request: CombinedChatRequest): Promise {
+ const { systemPrompt, messages, userMessage, recentContext } = request;
+
+ const combinedSystemPrompt = `${systemPrompt}
+
+## RESPONSE FORMAT
+You MUST respond with a valid JSON object. The format depends on the intent:
+
+For content-generating intents (flashcards, podcast, video, quiz):
+{
+ "intent": "",
+ "text": "",
+ "keywords": ""
+}
+
+For non-content intents (greeting, general):
+{
+ "intent": "",
+ "text": ""
+}
+
+## INTENT CLASSIFICATION
+First analyze the user's message and conversation context to determine their intent:
+- flashcards: user wants study cards, review cards, flashcards, or is confirming a flashcard offer
+- podcast: user wants audio content, podcast, or to listen to something
+- video: user wants to watch something or see a video
+- quiz: user wants to be tested or take a quiz
+- greeting: user is just saying hello/hi
+- general: questions, explanations, or general conversation
+
+IMPORTANT: Consider the conversation context. If the user previously discussed flashcards/podcasts/videos and says things like "yes", "sure", "do it", "show me" - they are CONFIRMING a previous offer.
+
+## KEYWORDS (for flashcards, podcast, video, quiz only)
+When the intent is content-generating, you MUST include a "keywords" field with:
+1. The CORRECTED topic (fix any spelling mistakes the user made)
+2. Related biology terms that would help find relevant textbook content
+3. Specific subtopics within that subject area
+
+Examples:
+- User says "endicrone system" → keywords: "endocrine, hormone, pituitary, thyroid, adrenal, pancreas, metabolism, homeostasis"
+- User says "ATP eneryg" → keywords: "ATP, adenosine triphosphate, energy, bond, hydrolysis, ADP, phosphate"
+- User says "sell division" → keywords: "cell division, mitosis, meiosis, chromosome, DNA replication, cytokinesis"
+
+The keywords help the content retrieval system find the right OpenStax textbook sections even when the user misspells words.
+
+Then provide an appropriate conversational response following your tutor persona.`;
+
+ // Convert messages to Gemini format
+ const contents = messages.map((m) => ({
+ role: m.role === "assistant" ? "model" : "user",
+ parts: m.parts,
+ }));
+
+ // Add recent context if provided
+ let contextualMessage = userMessage;
+ if (recentContext) {
+ contextualMessage = `Recent conversation:\n${recentContext}\n\nCurrent message: "${userMessage}"`;
+ }
+
+ contents.push({
+ role: "user",
+ parts: [{ text: contextualMessage }],
+ });
+
+ try {
+ const response = await genai.models.generateContent({
+ model: MODEL,
+ contents,
+ config: {
+ systemInstruction: combinedSystemPrompt,
+ responseMimeType: "application/json",
+ },
+ });
+
+ const responseText = response.text?.trim() || "";
+ console.log("[API Server] Combined response:", responseText.substring(0, 200));
+
+ try {
+ const parsed = JSON.parse(responseText);
+ const result: CombinedChatResponse = {
+ intent: parsed.intent || "general",
+ text: parsed.text || "I apologize, I couldn't generate a response.",
+ };
+ // Include keywords if present (for content-generating intents)
+ if (parsed.keywords) {
+ result.keywords = parsed.keywords;
+ console.log("[API Server] Keywords for content retrieval:", parsed.keywords);
+ }
+ return result;
+ } catch (parseError) {
+ console.error("[API Server] Failed to parse combined response:", parseError);
+ // Fallback: return general intent with raw text
+ return {
+ intent: "general",
+ text: responseText || "I apologize, I couldn't generate a response.",
+ };
+ }
+ } catch (error) {
+ console.error("[API Server] Error calling Gemini for combined request:", error);
+ throw error;
+ }
+}
+
+function parseBody(req: any): Promise {
+ return new Promise((resolve, reject) => {
+ let body = "";
+ req.on("data", (chunk: string) => {
+ body += chunk;
+ });
+ req.on("end", () => {
+ try {
+ resolve(JSON.parse(body));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ req.on("error", reject);
+ });
+}
+
+async function main() {
+ console.log("[API Server] Initializing Gemini client...");
+ await initGenAI();
+
+ const server = createServer(async (req, res) => {
+ // CORS headers
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
+
+ if (req.method === "OPTIONS") {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ // Health check
+ if (req.url === "/health" && req.method === "GET") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ status: "healthy" }));
+ return;
+ }
+
+ // Reset message log (useful before demo)
+ if (req.url === "/reset-log" && req.method === "POST") {
+ resetLog();
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ status: "log reset", file: LOG_FILE }));
+ return;
+ }
+
+ // View current log
+ if (req.url === "/log" && req.method === "GET") {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify(messageLog, null, 2));
+ return;
+ }
+
+ // A2A Agent Engine endpoint
+ if (req.url === "/a2ui-agent/a2a/query" && req.method === "POST") {
+ try {
+ const body = await parseBody(req);
+ console.log("[API Server] ========================================");
+ console.log("[API Server] A2A QUERY - REQUESTING A2UI CONTENT");
+ console.log("[API Server] Full message:", body.message);
+ console.log("[API Server] Session ID:", body.session_id);
+ console.log("[API Server] ========================================");
+
+ // LOG: Client → Server request for A2UI content
+ logMessage("CLIENT_TO_SERVER", "/a2ui-agent/a2a/query", {
+ description: "Browser client requesting A2UI content generation",
+ requestBody: body,
+ });
+
+ // Parse format from message (e.g., "flashcards:context" or just "flashcards")
+ const parts = (body.message || "flashcards").split(":");
+ const format = parts[0].trim();
+ const context = parts.slice(1).join(":").trim();
+
+ console.log("[API Server] Parsed format:", format);
+ console.log("[API Server] Parsed context (keywords):", context);
+ console.log("[API Server] This context will be sent to Agent Engine for topic matching");
+
+ let result = await queryAgentEngine(format, context);
+
+ // If quiz was requested but Agent Engine returned Flashcards or empty,
+ // generate quiz locally using Gemini
+ if (format.toLowerCase() === "quiz") {
+ const a2uiStr = JSON.stringify(result.a2ui || []);
+ const hasFlashcards = a2uiStr.includes("Flashcard");
+ const hasQuizCards = a2uiStr.includes("QuizCard");
+ const isEmpty = !result.a2ui || result.a2ui.length === 0;
+
+ if (isEmpty || (hasFlashcards && !hasQuizCards)) {
+ console.log("[API Server] Agent Engine doesn't support QuizCard, generating locally");
+ const localQuiz = await generateLocalQuiz(context);
+ if (localQuiz) {
+ result = localQuiz;
+ }
+ }
+ }
+
+ // LOG: Server → Client response with A2UI
+ logMessage("SERVER_TO_CLIENT", "/a2ui-agent/a2a/query", {
+ description: "Sending A2UI JSON payload to browser for rendering",
+ format: result.format,
+ surfaceId: result.surfaceId,
+ source: result.source,
+ a2uiMessageCount: result.a2ui?.length || 0,
+ a2uiPayload: result.a2ui,
+ });
+
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify(result));
+ } catch (error: any) {
+ console.error("[API Server] A2A error:", error);
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ error: error.message }));
+ }
+ return;
+ }
+
+ // Chat endpoint
+ if (req.url === "/api/chat" && req.method === "POST") {
+ try {
+ const body = await parseBody(req);
+ console.log("[API Server] Chat request received");
+
+ // LOG: Client → Server chat request
+ logMessage("CLIENT_TO_SERVER", "/api/chat", {
+ description: "Browser client sending chat message to Gemini",
+ userMessage: body.userMessage,
+ intentGuidance: body.intentGuidance?.substring(0, 100) + "...",
+ conversationLength: body.messages?.length || 0,
+ });
+
+ const result = await handleChatRequest(body);
+
+ // LOG: Server → Client chat response
+ logMessage("SERVER_TO_CLIENT", "/api/chat", {
+ description: "Gemini response text (conversational layer)",
+ responseText: result.text,
+ });
+
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify(result));
+ } catch (error: any) {
+ console.error("[API Server] Error:", error);
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ error: error.message }));
+ }
+ return;
+ }
+
+ // Combined chat endpoint - performs intent detection AND response in one LLM call
+ if (req.url === "/api/chat-with-intent" && req.method === "POST") {
+ try {
+ const body = await parseBody(req);
+ console.log("[API Server] ========================================");
+ console.log("[API Server] COMBINED CHAT REQUEST RECEIVED");
+ console.log("[API Server] User message:", body.userMessage);
+ console.log("[API Server] Conversation history length:", body.messages?.length || 0);
+ console.log("[API Server] ========================================");
+
+ // LOG: Client → Server combined request
+ logMessage("CLIENT_TO_SERVER", "/api/chat-with-intent", {
+ description: "Browser client requesting combined intent+response (latency optimization)",
+ userMessage: body.userMessage,
+ recentContext: body.recentContext,
+ conversationLength: body.messages?.length || 0,
+ fullMessages: body.messages,
+ });
+
+ const result = await handleCombinedChatRequest(body);
+
+ console.log("[API Server] ========================================");
+ console.log("[API Server] GEMINI COMBINED RESPONSE:");
+ console.log("[API Server] Intent:", result.intent);
+ console.log("[API Server] Keywords:", result.keywords || "(none - not a content intent)");
+ console.log("[API Server] Text:", result.text.substring(0, 200));
+ console.log("[API Server] ========================================");
+
+ // LOG: Server → Client combined response
+ logMessage("SERVER_TO_CLIENT", "/api/chat-with-intent", {
+ description: "Combined intent detection and response in single LLM call",
+ intent: result.intent,
+ keywords: result.keywords,
+ responseText: result.text,
+ });
+
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify(result));
+ } catch (error: any) {
+ console.error("[API Server] Combined chat error:", error);
+ res.writeHead(500, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ error: error.message }));
+ }
+ return;
+ }
+
+ // Static file serving for frontend
+ const MIME_TYPES: Record = {
+ ".html": "text/html",
+ ".js": "application/javascript",
+ ".css": "text/css",
+ ".json": "application/json",
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".svg": "image/svg+xml",
+ ".ico": "image/x-icon",
+ };
+
+ // Serve static files (Vite builds to dist/, but index.html is in root for dev)
+ if (req.method === "GET") {
+ let filePath = req.url === "/" ? "/index.html" : req.url || "/index.html";
+
+ // Remove query string
+ filePath = filePath.split("?")[0];
+
+ // Try dist/ first (production build), then root (development)
+ const distPath = join(process.cwd(), "dist", filePath);
+ const rootPath = join(process.cwd(), filePath);
+
+ const fullPath = existsSync(distPath) ? distPath : rootPath;
+
+ if (existsSync(fullPath)) {
+ try {
+ const content = readFileSync(fullPath);
+ const ext = filePath.substring(filePath.lastIndexOf("."));
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
+
+ res.writeHead(200, { "Content-Type": contentType });
+ res.end(content);
+ return;
+ } catch (err) {
+ // Fall through to 404
+ }
+ }
+ }
+
+ // 404 for other routes
+ res.writeHead(404, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ error: "Not found" }));
+ });
+
+ server.listen(PORT, () => {
+ console.log(`[API Server] Running on http://localhost:${PORT}`);
+ console.log(`[API Server] Chat endpoint: POST http://localhost:${PORT}/api/chat`);
+ console.log(`\n📋 MESSAGE LOG ENABLED`);
+ console.log(` Log file: ${LOG_FILE}`);
+ console.log(` View log: GET http://localhost:${PORT}/log`);
+ console.log(` Reset log: POST http://localhost:${PORT}/reset-log`);
+ console.log(`\n After running the demo, check ${LOG_FILE} for the full message sequence!\n`);
+ });
+}
+
+main().catch(console.error);
diff --git a/samples/personalized_learning/assets/architecture.jpg b/samples/personalized_learning/assets/architecture.jpg
new file mode 100644
index 00000000..e55132a5
Binary files /dev/null and b/samples/personalized_learning/assets/architecture.jpg differ
diff --git a/samples/personalized_learning/assets/hero.png b/samples/personalized_learning/assets/hero.png
new file mode 100644
index 00000000..3cfef88c
Binary files /dev/null and b/samples/personalized_learning/assets/hero.png differ
diff --git a/samples/personalized_learning/deploy.py b/samples/personalized_learning/deploy.py
new file mode 100644
index 00000000..c66515a8
--- /dev/null
+++ b/samples/personalized_learning/deploy.py
@@ -0,0 +1,1227 @@
+#!/usr/bin/env python3
+"""
+Personalized Learning Agent - Deployment Script for Agent Engine
+
+This script deploys the ADK agent to Vertex AI Agent Engine.
+
+Required environment variables:
+ GOOGLE_CLOUD_PROJECT - Your GCP project ID
+
+Optional environment variables:
+ GOOGLE_CLOUD_LOCATION - GCP region (default: us-central1)
+
+Usage:
+ python deploy.py --project YOUR_PROJECT_ID
+ python deploy.py --project YOUR_PROJECT_ID --location us-central1
+ python deploy.py --list # List deployed agents
+"""
+
+import os
+import sys
+import argparse
+import logging
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Deploy the Personalized Learning Agent to Agent Engine"
+ )
+ parser.add_argument(
+ "--project",
+ type=str,
+ default=os.getenv("GOOGLE_CLOUD_PROJECT"),
+ help="GCP project ID",
+ )
+ parser.add_argument(
+ "--location",
+ type=str,
+ default=os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1"),
+ help="GCP location (default: us-central1)",
+ )
+ parser.add_argument(
+ "--context-bucket",
+ type=str,
+ default=None,
+ help="GCS bucket for learner context (default: {project}-learner-context)",
+ )
+ parser.add_argument(
+ "--list",
+ action="store_true",
+ help="List deployed agents instead of deploying",
+ )
+
+ args = parser.parse_args()
+
+ if not args.project:
+ print("ERROR: --project flag or GOOGLE_CLOUD_PROJECT environment variable is required")
+ sys.exit(1)
+
+ # Set context bucket (default to {project}-learner-context)
+ context_bucket = args.context_bucket or f"{args.project}-learner-context"
+
+ # Set environment variables
+ os.environ["GOOGLE_CLOUD_PROJECT"] = args.project
+ os.environ["GOOGLE_CLOUD_LOCATION"] = args.location
+ os.environ["GCS_CONTEXT_BUCKET"] = context_bucket
+
+ # Import Vertex AI modules
+ import vertexai
+ from vertexai import agent_engines
+
+ # Initialize Vertex AI
+ staging_bucket = f"gs://{args.project}_cloudbuild"
+ vertexai.init(
+ project=args.project,
+ location=args.location,
+ staging_bucket=staging_bucket,
+ )
+
+ if args.list:
+ print(f"\nDeployed agents in {args.project} ({args.location}):")
+ for engine in agent_engines.list():
+ print(f" - {engine.display_name}: {engine.resource_name}")
+ return
+
+ print(f"Deploying Personalized Learning Agent...")
+ print(f" Project: {args.project}")
+ print(f" Location: {args.location}")
+ print(f" Context bucket: gs://{context_bucket}/learner_context/")
+ print()
+
+ # =========================================================================
+ # CREATE THE ADK AGENT
+ # =========================================================================
+ # According to Vertex AI docs, we create an Agent, wrap it in AdkApp,
+ # and deploy the AdkApp directly. AdkApp is designed to be picklable.
+ # =========================================================================
+
+ import json
+ import re
+ import xml.etree.ElementTree as ET
+ from typing import Any
+ from google.adk.agents import Agent
+ from google.adk.tools import ToolContext
+ from vertexai.agent_engines import AdkApp
+
+ model_id = os.getenv("GENAI_MODEL", "gemini-2.5-flash")
+ SURFACE_ID = "learningContent"
+
+ # =========================================================================
+ # OPENSTAX CONTENT - Chapter mappings and content fetching
+ # =========================================================================
+
+ # =========================================================================
+ # COMPLETE OpenStax Biology AP Courses - Chapter mappings
+ # Copied from agent/openstax_chapters.py for Agent Engine deployment
+ # =========================================================================
+
+ OPENSTAX_CHAPTERS = {
+ # Unit 1: The Chemistry of Life
+ "1-1-the-science-of-biology": "The Science of Biology",
+ "1-2-themes-and-concepts-of-biology": "Themes and Concepts of Biology",
+ "2-1-atoms-isotopes-ions-and-molecules-the-building-blocks": "Atoms, Isotopes, Ions, and Molecules: The Building Blocks",
+ "2-2-water": "Water",
+ "2-3-carbon": "Carbon",
+ "3-1-synthesis-of-biological-macromolecules": "Synthesis of Biological Macromolecules",
+ "3-2-carbohydrates": "Carbohydrates",
+ "3-3-lipids": "Lipids",
+ "3-4-proteins": "Proteins",
+ "3-5-nucleic-acids": "Nucleic Acids",
+ # Unit 2: The Cell
+ "4-1-studying-cells": "Studying Cells",
+ "4-2-prokaryotic-cells": "Prokaryotic Cells",
+ "4-3-eukaryotic-cells": "Eukaryotic Cells",
+ "4-4-the-endomembrane-system-and-proteins": "The Endomembrane System and Proteins",
+ "4-5-cytoskeleton": "Cytoskeleton",
+ "4-6-connections-between-cells-and-cellular-activities": "Connections Between Cells and Cellular Activities",
+ "5-1-components-and-structure": "Cell Membrane Components and Structure",
+ "5-2-passive-transport": "Passive Transport",
+ "5-3-active-transport": "Active Transport",
+ "5-4-bulk-transport": "Bulk Transport",
+ "6-1-energy-and-metabolism": "Energy and Metabolism",
+ "6-2-potential-kinetic-free-and-activation-energy": "Potential, Kinetic, Free, and Activation Energy",
+ "6-3-the-laws-of-thermodynamics": "The Laws of Thermodynamics",
+ "6-4-atp-adenosine-triphosphate": "ATP: Adenosine Triphosphate",
+ "6-5-enzymes": "Enzymes",
+ "7-1-energy-in-living-systems": "Energy in Living Systems",
+ "7-2-glycolysis": "Glycolysis",
+ "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle": "Oxidation of Pyruvate and the Citric Acid Cycle",
+ "7-4-oxidative-phosphorylation": "Oxidative Phosphorylation",
+ "7-5-metabolism-without-oxygen": "Metabolism Without Oxygen",
+ "7-6-connections-of-carbohydrate-protein-and-lipid-metabolic-pathways": "Connections of Carbohydrate, Protein, and Lipid Metabolic Pathways",
+ "7-7-regulation-of-cellular-respiration": "Regulation of Cellular Respiration",
+ "8-1-overview-of-photosynthesis": "Overview of Photosynthesis",
+ "8-2-the-light-dependent-reaction-of-photosynthesis": "The Light-Dependent Reactions of Photosynthesis",
+ "8-3-using-light-to-make-organic-molecules": "Using Light to Make Organic Molecules",
+ "9-1-signaling-molecules-and-cellular-receptors": "Signaling Molecules and Cellular Receptors",
+ "9-2-propagation-of-the-signal": "Propagation of the Signal",
+ "9-3-response-to-the-signal": "Response to the Signal",
+ "9-4-signaling-in-single-celled-organisms": "Signaling in Single-Celled Organisms",
+ "10-1-cell-division": "Cell Division",
+ "10-2-the-cell-cycle": "The Cell Cycle",
+ "10-3-control-of-the-cell-cycle": "Control of the Cell Cycle",
+ "10-4-cancer-and-the-cell-cycle": "Cancer and the Cell Cycle",
+ "10-5-prokaryotic-cell-division": "Prokaryotic Cell Division",
+ # Unit 3: Genetics
+ "11-1-the-process-of-meiosis": "The Process of Meiosis",
+ "11-2-sexual-reproduction": "Sexual Reproduction",
+ "12-1-mendels-experiments-and-the-laws-of-probability": "Mendel's Experiments and the Laws of Probability",
+ "12-2-characteristics-and-traits": "Characteristics and Traits",
+ "12-3-laws-of-inheritance": "Laws of Inheritance",
+ "13-1-chromosomal-theory-and-genetic-linkages": "Chromosomal Theory and Genetic Linkages",
+ "13-2-chromosomal-basis-of-inherited-disorders": "Chromosomal Basis of Inherited Disorders",
+ "14-1-historical-basis-of-modern-understanding": "Historical Basis of Modern Understanding of DNA",
+ "14-2-dna-structure-and-sequencing": "DNA Structure and Sequencing",
+ "14-3-basics-of-dna-replication": "Basics of DNA Replication",
+ "14-4-dna-replication-in-prokaryotes": "DNA Replication in Prokaryotes",
+ "14-5-dna-replication-in-eukaryotes": "DNA Replication in Eukaryotes",
+ "14-6-dna-repair": "DNA Repair",
+ "15-1-the-genetic-code": "The Genetic Code",
+ "15-2-prokaryotic-transcription": "Prokaryotic Transcription",
+ "15-3-eukaryotic-transcription": "Eukaryotic Transcription",
+ "15-4-rna-processing-in-eukaryotes": "RNA Processing in Eukaryotes",
+ "15-5-ribosomes-and-protein-synthesis": "Ribosomes and Protein Synthesis",
+ "16-1-regulation-of-gene-expression": "Regulation of Gene Expression",
+ "16-2-prokaryotic-gene-regulation": "Prokaryotic Gene Regulation",
+ "16-3-eukaryotic-epigenetic-gene-regulation": "Eukaryotic Epigenetic Gene Regulation",
+ "16-4-eukaryotic-transcriptional-gene-regulation": "Eukaryotic Transcriptional Gene Regulation",
+ "16-5-eukaryotic-post-transcriptional-gene-regulation": "Eukaryotic Post-transcriptional Gene Regulation",
+ "16-6-eukaryotic-translational-and-post-translational-gene-regulation": "Eukaryotic Translational and Post-translational Gene Regulation",
+ "16-7-cancer-and-gene-regulation": "Cancer and Gene Regulation",
+ "17-1-biotechnology": "Biotechnology",
+ "17-2-mapping-genomes": "Mapping Genomes",
+ "17-3-whole-genome-sequencing": "Whole-Genome Sequencing",
+ "17-4-applying-genomics": "Applying Genomics",
+ "17-5-genomics-and-proteomics": "Genomics and Proteomics",
+ # Unit 4: Evolutionary Processes
+ "18-1-understanding-evolution": "Understanding Evolution",
+ "18-2-formation-of-new-species": "Formation of New Species",
+ "18-3-reconnection-and-rates-of-speciation": "Reconnection and Rates of Speciation",
+ "19-1-population-evolution": "Population Evolution",
+ "19-2-population-genetics": "Population Genetics",
+ "19-3-adaptive-evolution": "Adaptive Evolution",
+ "20-1-organizing-life-on-earth": "Organizing Life on Earth",
+ "20-2-determining-evolutionary-relationships": "Determining Evolutionary Relationships",
+ "20-3-perspectives-on-the-phylogenetic-tree": "Perspectives on the Phylogenetic Tree",
+ # Unit 5: Biological Diversity
+ "21-1-viral-evolution-morphology-and-classification": "Viral Evolution, Morphology, and Classification",
+ "21-2-virus-infection-and-hosts": "Virus Infection and Hosts",
+ "21-3-prevention-and-treatment-of-viral-infections": "Prevention and Treatment of Viral Infections",
+ "21-4-other-acellular-entities-prions-and-viroids": "Other Acellular Entities: Prions and Viroids",
+ "22-1-prokaryotic-diversity": "Prokaryotic Diversity",
+ "22-2-structure-of-prokaryotes": "Structure of Prokaryotes",
+ "22-3-prokaryotic-metabolism": "Prokaryotic Metabolism",
+ "22-4-bacterial-diseases-in-humans": "Bacterial Diseases in Humans",
+ "22-5-beneficial-prokaryotes": "Beneficial Prokaryotes",
+ # Unit 6: Plant Structure and Function
+ "23-1-the-plant-body": "The Plant Body",
+ "23-2-stems": "Stems",
+ "23-3-roots": "Roots",
+ "23-4-leaves": "Leaves",
+ "23-5-transport-of-water-and-solutes-in-plants": "Transport of Water and Solutes in Plants",
+ "23-6-plant-sensory-systems-and-responses": "Plant Sensory Systems and Responses",
+ # Unit 7: Animal Structure and Function
+ "24-1-animal-form-and-function": "Animal Form and Function",
+ "24-2-animal-primary-tissues": "Animal Primary Tissues",
+ "24-3-homeostasis": "Homeostasis",
+ "25-1-digestive-systems": "Digestive Systems",
+ "25-2-nutrition-and-energy-production": "Nutrition and Energy Production",
+ "25-3-digestive-system-processes": "Digestive System Processes",
+ "25-4-digestive-system-regulation": "Digestive System Regulation",
+ "26-1-neurons-and-glial-cells": "Neurons and Glial Cells",
+ "26-2-how-neurons-communicate": "How Neurons Communicate",
+ "26-3-the-central-nervous-system": "The Central Nervous System",
+ "26-4-the-peripheral-nervous-system": "The Peripheral Nervous System",
+ "26-5-nervous-system-disorders": "Nervous System Disorders",
+ "27-1-sensory-processes": "Sensory Processes",
+ "27-2-somatosensation": "Somatosensation",
+ "27-3-taste-and-smell": "Taste and Smell",
+ "27-4-hearing-and-vestibular-sensation": "Hearing and Vestibular Sensation",
+ "27-5-vision": "Vision",
+ "28-1-types-of-hormones": "Types of Hormones",
+ "28-2-how-hormones-work": "How Hormones Work",
+ "28-3-regulation-of-body-processes": "Regulation of Body Processes",
+ "28-4-regulation-of-hormone-production": "Regulation of Hormone Production",
+ "28-5-endocrine-glands": "Endocrine Glands",
+ "29-1-types-of-skeletal-systems": "Types of Skeletal Systems",
+ "29-2-bone": "Bone",
+ "29-3-joints-and-skeletal-movement": "Joints and Skeletal Movement",
+ "29-4-muscle-contraction-and-locomotion": "Muscle Contraction and Locomotion",
+ "30-1-systems-of-gas-exchange": "Systems of Gas Exchange",
+ "30-2-gas-exchange-across-respiratory-surfaces": "Gas Exchange Across Respiratory Surfaces",
+ "30-3-breathing": "Breathing",
+ "30-4-transport-of-gases-in-human-bodily-fluids": "Transport of Gases in Human Bodily Fluids",
+ "31-1-overview-of-the-circulatory-system": "Overview of the Circulatory System",
+ "31-2-components-of-the-blood": "Components of the Blood",
+ "31-3-mammalian-heart-and-blood-vessels": "Mammalian Heart and Blood Vessels",
+ "31-4-blood-flow-and-blood-pressure-regulation": "Blood Flow and Blood Pressure Regulation",
+ "32-1-osmoregulation-and-osmotic-balance": "Osmoregulation and Osmotic Balance",
+ "32-2-the-kidneys-and-osmoregulatory-organs": "The Kidneys and Osmoregulatory Organs",
+ "32-3-excretion-systems": "Excretion Systems",
+ "32-4-nitrogenous-wastes": "Nitrogenous Wastes",
+ "32-5-hormonal-control-of-osmoregulatory-functions": "Hormonal Control of Osmoregulatory Functions",
+ "33-1-innate-immune-response": "Innate Immune Response",
+ "33-2-adaptive-immune-response": "Adaptive Immune Response",
+ "33-3-antibodies": "Antibodies",
+ "33-4-disruptions-in-the-immune-system": "Disruptions in the Immune System",
+ "34-1-reproduction-methods": "Reproduction Methods",
+ "34-2-fertilization": "Fertilization",
+ "34-3-human-reproductive-anatomy-and-gametogenesis": "Human Reproductive Anatomy and Gametogenesis",
+ "34-4-hormonal-control-of-human-reproduction": "Hormonal Control of Human Reproduction",
+ "34-5-fertilization-and-early-embryonic-development": "Fertilization and Early Embryonic Development",
+ "34-6-organogenesis-and-vertebrate-axis-formation": "Organogenesis and Vertebrate Axis Formation",
+ "34-7-human-pregnancy-and-birth": "Human Pregnancy and Birth",
+ # Unit 8: Ecology
+ "35-1-the-scope-of-ecology": "The Scope of Ecology",
+ "35-2-biogeography": "Biogeography",
+ "35-3-terrestrial-biomes": "Terrestrial Biomes",
+ "35-4-aquatic-biomes": "Aquatic Biomes",
+ "35-5-climate-and-the-effects-of-global-climate-change": "Climate and the Effects of Global Climate Change",
+ "36-1-population-demography": "Population Demography",
+ "36-2-life-histories-and-natural-selection": "Life Histories and Natural Selection",
+ "36-3-environmental-limits-to-population-growth": "Environmental Limits to Population Growth",
+ "36-4-population-dynamics-and-regulation": "Population Dynamics and Regulation",
+ "36-5-human-population-growth": "Human Population Growth",
+ "36-6-community-ecology": "Community Ecology",
+ "36-7-behavioral-biology-proximate-and-ultimate-causes-of-behavior": "Behavioral Biology: Proximate and Ultimate Causes of Behavior",
+ "37-1-ecology-for-ecosystems": "Ecology for Ecosystems",
+ "37-2-energy-flow-through-ecosystems": "Energy Flow Through Ecosystems",
+ "37-3-biogeochemical-cycles": "Biogeochemical Cycles",
+ "38-1-the-biodiversity-crisis": "The Biodiversity Crisis",
+ "38-2-the-importance-of-biodiversity-to-human-life": "The Importance of Biodiversity to Human Life",
+ "38-3-threats-to-biodiversity": "Threats to Biodiversity",
+ "38-4-preserving-biodiversity": "Preserving Biodiversity",
+ }
+
+ # Complete chapter to module ID mapping - GENERATED FROM openstax_modules.py
+ # Each chapter slug maps to the correct module ID(s) from the OpenStax collection
+ CHAPTER_TO_MODULES = {
+ "1-1-the-science-of-biology": ["m62717"],
+ "1-2-themes-and-concepts-of-biology": ["m62718"],
+ "2-1-atoms-isotopes-ions-and-molecules-the-building-blocks": ["m62720"],
+ "2-2-water": ["m62721"],
+ "2-3-carbon": ["m62722"],
+ "3-1-synthesis-of-biological-macromolecules": ["m62724"],
+ "3-2-carbohydrates": ["m62726"],
+ "3-3-lipids": ["m62730"],
+ "3-4-proteins": ["m62733"],
+ "3-5-nucleic-acids": ["m62735"],
+ "4-1-studying-cells": ["m62738"],
+ "4-2-prokaryotic-cells": ["m62740"],
+ "4-3-eukaryotic-cells": ["m62742"],
+ "4-4-the-endomembrane-system-and-proteins": ["m62743"],
+ "4-5-cytoskeleton": ["m62744"],
+ "4-6-connections-between-cells-and-cellular-activities": ["m62746"],
+ "5-1-components-and-structure": ["m62773"],
+ "5-2-passive-transport": ["m62753"],
+ "5-3-active-transport": ["m62770"],
+ "5-4-bulk-transport": ["m62772"],
+ "6-1-energy-and-metabolism": ["m62763"],
+ "6-2-potential-kinetic-free-and-activation-energy": ["m62764"],
+ "6-3-the-laws-of-thermodynamics": ["m62767"],
+ "6-4-atp-adenosine-triphosphate": ["m62768"],
+ "6-5-enzymes": ["m62778"],
+ "7-1-energy-in-living-systems": ["m62786"],
+ "7-2-glycolysis": ["m62787"],
+ "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle": ["m62788"],
+ "7-4-oxidative-phosphorylation": ["m62789"],
+ "7-5-metabolism-without-oxygen": ["m62790"],
+ "7-6-connections-of-carbohydrate-protein-and-lipid-metabolic-pathways": ["m62791"],
+ "7-7-regulation-of-cellular-respiration": ["m62792"],
+ "8-1-overview-of-photosynthesis": ["m62794"],
+ "8-2-the-light-dependent-reaction-of-photosynthesis": ["m62795"],
+ "8-3-using-light-to-make-organic-molecules": ["m62796"],
+ "9-1-signaling-molecules-and-cellular-receptors": ["m62798"],
+ "9-2-propagation-of-the-signal": ["m62799"],
+ "9-3-response-to-the-signal": ["m62800"],
+ "9-4-signaling-in-single-celled-organisms": ["m62801"],
+ "10-1-cell-division": ["m62803"],
+ "10-2-the-cell-cycle": ["m62804"],
+ "10-3-control-of-the-cell-cycle": ["m62805"],
+ "10-4-cancer-and-the-cell-cycle": ["m62806"],
+ "10-5-prokaryotic-cell-division": ["m62808"],
+ "11-1-the-process-of-meiosis": ["m62810"],
+ "11-2-sexual-reproduction": ["m62811"],
+ "12-1-mendels-experiments-and-the-laws-of-probability": ["m62813"],
+ "12-2-characteristics-and-traits": ["m62817"],
+ "12-3-laws-of-inheritance": ["m62819"],
+ "13-1-chromosomal-theory-and-genetic-linkages": ["m62821"],
+ "13-2-chromosomal-basis-of-inherited-disorders": ["m62822"],
+ "14-1-historical-basis-of-modern-understanding": ["m62824"],
+ "14-2-dna-structure-and-sequencing": ["m62825"],
+ "14-3-basics-of-dna-replication": ["m62826"],
+ "14-4-dna-replication-in-prokaryotes": ["m62828"],
+ "14-5-dna-replication-in-eukaryotes": ["m62829"],
+ "14-6-dna-repair": ["m62830"],
+ "15-1-the-genetic-code": ["m62837"],
+ "15-2-prokaryotic-transcription": ["m62838"],
+ "15-3-eukaryotic-transcription": ["m62840"],
+ "15-4-rna-processing-in-eukaryotes": ["m62842"],
+ "15-5-ribosomes-and-protein-synthesis": ["m62843"],
+ "16-1-regulation-of-gene-expression": ["m62845"],
+ "16-2-prokaryotic-gene-regulation": ["m62846"],
+ "16-3-eukaryotic-epigenetic-gene-regulation": ["m62847"],
+ "16-4-eukaryotic-transcriptional-gene-regulation": ["m62848"],
+ "16-5-eukaryotic-post-transcriptional-gene-regulation": ["m62849"],
+ "16-6-eukaryotic-translational-and-post-translational-gene-regulation": ["m62850"],
+ "16-7-cancer-and-gene-regulation": ["m62851"],
+ "17-1-biotechnology": ["m62853"],
+ "17-2-mapping-genomes": ["m62855"],
+ "17-3-whole-genome-sequencing": ["m62857"],
+ "17-4-applying-genomics": ["m62860"],
+ "17-5-genomics-and-proteomics": ["m62861"],
+ "18-1-understanding-evolution": ["m62863"],
+ "18-2-formation-of-new-species": ["m62864"],
+ "18-3-reconnection-and-rates-of-speciation": ["m62865"],
+ "19-1-population-evolution": ["m62868"],
+ "19-2-population-genetics": ["m62870"],
+ "19-3-adaptive-evolution": ["m62871"],
+ "20-1-organizing-life-on-earth": ["m62874"],
+ "20-2-determining-evolutionary-relationships": ["m62903"],
+ "20-3-perspectives-on-the-phylogenetic-tree": ["m62876"],
+ "21-1-viral-evolution-morphology-and-classification": ["m62881"],
+ "21-2-virus-infection-and-hosts": ["m62882"],
+ "21-3-prevention-and-treatment-of-viral-infections": ["m62904"],
+ "21-4-other-acellular-entities-prions-and-viroids": ["m62887"],
+ "22-1-prokaryotic-diversity": ["m62891"],
+ "22-2-structure-of-prokaryotes": ["m62893"],
+ "22-3-prokaryotic-metabolism": ["m62894"],
+ "22-4-bacterial-diseases-in-humans": ["m62896"],
+ "22-5-beneficial-prokaryotes": ["m62897"],
+ "23-1-the-plant-body": ["m62951"],
+ "23-2-stems": ["m62905"],
+ "23-3-roots": ["m62906"],
+ "23-4-leaves": ["m62908"],
+ "23-5-transport-of-water-and-solutes-in-plants": ["m62969"],
+ "23-6-plant-sensory-systems-and-responses": ["m62930"],
+ "24-1-animal-form-and-function": ["m62916"],
+ "24-2-animal-primary-tissues": ["m62918"],
+ "24-3-homeostasis": ["m62931"],
+ "25-1-digestive-systems": ["m62919"],
+ "25-2-nutrition-and-energy-production": ["m62920"],
+ "25-3-digestive-system-processes": ["m62921"],
+ "25-4-digestive-system-regulation": ["m62922"],
+ "26-1-neurons-and-glial-cells": ["m62924"],
+ "26-2-how-neurons-communicate": ["m62925"],
+ "26-3-the-central-nervous-system": ["m62926"],
+ "26-4-the-peripheral-nervous-system": ["m62928"],
+ "26-5-nervous-system-disorders": ["m62929"],
+ "27-1-sensory-processes": ["m62994"],
+ "27-2-somatosensation": ["m62946"],
+ "27-3-taste-and-smell": ["m62947"],
+ "27-4-hearing-and-vestibular-sensation": ["m62954"],
+ "27-5-vision": ["m62957"],
+ "28-1-types-of-hormones": ["m62961"],
+ "28-2-how-hormones-work": ["m62963"],
+ "28-3-regulation-of-body-processes": ["m62996"],
+ "28-4-regulation-of-hormone-production": ["m62971"],
+ "28-5-endocrine-glands": ["m62995"],
+ "29-1-types-of-skeletal-systems": ["m62977"],
+ "29-2-bone": ["m62978"],
+ "29-3-joints-and-skeletal-movement": ["m62979"],
+ "29-4-muscle-contraction-and-locomotion": ["m62980"],
+ "30-1-systems-of-gas-exchange": ["m62982"],
+ "30-2-gas-exchange-across-respiratory-surfaces": ["m62998"],
+ "30-3-breathing": ["m62987"],
+ "30-4-transport-of-gases-in-human-bodily-fluids": ["m62988"],
+ "31-1-overview-of-the-circulatory-system": ["m62990"],
+ "31-2-components-of-the-blood": ["m62991"],
+ "31-3-mammalian-heart-and-blood-vessels": ["m62992"],
+ "31-4-blood-flow-and-blood-pressure-regulation": ["m62993"],
+ "32-1-osmoregulation-and-osmotic-balance": ["m63000"],
+ "32-2-the-kidneys-and-osmoregulatory-organs": ["m63001"],
+ "32-3-excretion-systems": ["m63002"],
+ "32-4-nitrogenous-wastes": ["m63003"],
+ "32-5-hormonal-control-of-osmoregulatory-functions": ["m63004"],
+ "33-1-innate-immune-response": ["m63006"],
+ "33-2-adaptive-immune-response": ["m63007"],
+ "33-3-antibodies": ["m63008"],
+ "33-4-disruptions-in-the-immune-system": ["m63009"],
+ "34-1-reproduction-methods": ["m63011"],
+ "34-2-fertilization": ["m63012"],
+ "34-3-human-reproductive-anatomy-and-gametogenesis": ["m63013"],
+ "34-4-hormonal-control-of-human-reproduction": ["m63014"],
+ "34-5-fertilization-and-early-embryonic-development": ["m63016"],
+ "34-6-organogenesis-and-vertebrate-axis-formation": ["m63043"],
+ "34-7-human-pregnancy-and-birth": ["m63018"],
+ "35-1-the-scope-of-ecology": ["m63021"],
+ "35-2-biogeography": ["m63023"],
+ "35-3-terrestrial-biomes": ["m63024"],
+ "35-4-aquatic-biomes": ["m63025"],
+ "35-5-climate-and-the-effects-of-global-climate-change": ["m63026"],
+ "36-1-population-demography": ["m63028"],
+ "36-2-life-histories-and-natural-selection": ["m63029"],
+ "36-3-environmental-limits-to-population-growth": ["m63030"],
+ "36-4-population-dynamics-and-regulation": ["m63031"],
+ "36-5-human-population-growth": ["m63032"],
+ "36-6-community-ecology": ["m63033"],
+ "36-7-behavioral-biology-proximate-and-ultimate-causes-of-behavior": ["m63034"],
+ "37-1-ecology-for-ecosystems": ["m63036"],
+ "37-2-energy-flow-through-ecosystems": ["m63037"],
+ "37-3-biogeochemical-cycles": ["m63040"],
+ "38-1-the-biodiversity-crisis": ["m63048"],
+ "38-2-the-importance-of-biodiversity-to-human-life": ["m63049"],
+ "38-3-threats-to-biodiversity": ["m63050"],
+ "38-4-preserving-biodiversity": ["m63051"],
+ }
+
+ # Complete keyword hints for fast matching (Tier 1)
+ KEYWORD_HINTS = {
+ # Energy & Metabolism
+ "atp": ["6-4-atp-adenosine-triphosphate", "6-1-energy-and-metabolism"],
+ "adenosine triphosphate": ["6-4-atp-adenosine-triphosphate"],
+ "photosynthesis": ["8-1-overview-of-photosynthesis", "8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "plants make food": ["8-1-overview-of-photosynthesis"],
+ "chloroplast": ["8-1-overview-of-photosynthesis", "4-3-eukaryotic-cells"],
+ "chlorophyll": ["8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "calvin cycle": ["8-3-using-light-to-make-organic-molecules"],
+ "light reaction": ["8-2-the-light-dependent-reaction-of-photosynthesis"],
+ "cellular respiration": ["7-1-energy-in-living-systems", "7-4-oxidative-phosphorylation"],
+ "glycolysis": ["7-2-glycolysis"],
+ "krebs": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "citric acid": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "tca cycle": ["7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle"],
+ "electron transport": ["7-4-oxidative-phosphorylation"],
+ "oxidative phosphorylation": ["7-4-oxidative-phosphorylation"],
+ "fermentation": ["7-5-metabolism-without-oxygen"],
+ "anaerobic": ["7-5-metabolism-without-oxygen"],
+ "mitochondria": ["7-4-oxidative-phosphorylation", "4-3-eukaryotic-cells"],
+ "mitochondrion": ["7-4-oxidative-phosphorylation", "4-3-eukaryotic-cells"],
+ # Cell Division
+ "mitosis": ["10-1-cell-division", "10-2-the-cell-cycle"],
+ "meiosis": ["11-1-the-process-of-meiosis"],
+ "cell cycle": ["10-2-the-cell-cycle", "10-3-control-of-the-cell-cycle"],
+ "cell division": ["10-1-cell-division"],
+ "cancer": ["10-4-cancer-and-the-cell-cycle", "16-7-cancer-and-gene-regulation"],
+ # Molecular Biology
+ "dna": ["14-2-dna-structure-and-sequencing", "14-3-basics-of-dna-replication"],
+ "rna": ["15-4-rna-processing-in-eukaryotes", "3-5-nucleic-acids"],
+ "mrna": ["15-4-rna-processing-in-eukaryotes", "15-5-ribosomes-and-protein-synthesis"],
+ "trna": ["15-5-ribosomes-and-protein-synthesis"],
+ "rrna": ["15-5-ribosomes-and-protein-synthesis"],
+ "transcription": ["15-2-prokaryotic-transcription", "15-3-eukaryotic-transcription"],
+ "translation": ["15-5-ribosomes-and-protein-synthesis"],
+ "protein synthesis": ["15-5-ribosomes-and-protein-synthesis"],
+ "protein": ["3-4-proteins", "15-5-ribosomes-and-protein-synthesis"],
+ "enzyme": ["6-5-enzymes"],
+ "gene expression": ["16-1-regulation-of-gene-expression"],
+ "genetic code": ["15-1-the-genetic-code"],
+ "central dogma": ["15-1-the-genetic-code", "15-5-ribosomes-and-protein-synthesis"],
+ "codon": ["15-1-the-genetic-code"],
+ "anticodon": ["15-5-ribosomes-and-protein-synthesis"],
+ "ribosome": ["15-5-ribosomes-and-protein-synthesis", "4-3-eukaryotic-cells"],
+ "replication": ["14-3-basics-of-dna-replication", "14-4-dna-replication-in-prokaryotes"],
+ # Cell Structure
+ "cell membrane": ["5-1-components-and-structure"],
+ "plasma membrane": ["5-1-components-and-structure"],
+ "membrane": ["5-1-components-and-structure", "5-2-passive-transport"],
+ "phospholipid": ["5-1-components-and-structure", "3-3-lipids"],
+ "osmosis": ["5-2-passive-transport", "32-1-osmoregulation-and-osmotic-balance"],
+ "diffusion": ["5-2-passive-transport"],
+ "active transport": ["5-3-active-transport"],
+ "cytoskeleton": ["4-5-cytoskeleton"],
+ "organelle": ["4-3-eukaryotic-cells", "4-4-the-endomembrane-system-and-proteins"],
+ "nucleus": ["4-3-eukaryotic-cells"],
+ "endoplasmic reticulum": ["4-4-the-endomembrane-system-and-proteins"],
+ "golgi": ["4-4-the-endomembrane-system-and-proteins"],
+ "lysosome": ["4-4-the-endomembrane-system-and-proteins"],
+ "vesicle": ["5-4-bulk-transport", "4-4-the-endomembrane-system-and-proteins"],
+ "endocytosis": ["5-4-bulk-transport"],
+ "exocytosis": ["5-4-bulk-transport"],
+ "signal transduction": ["9-1-signaling-molecules-and-cellular-receptors", "9-2-propagation-of-the-signal"],
+ "cell signaling": ["9-1-signaling-molecules-and-cellular-receptors"],
+ # Nervous System
+ "neuron": ["26-1-neurons-and-glial-cells", "26-2-how-neurons-communicate"],
+ "nervous system": ["26-1-neurons-and-glial-cells", "26-3-the-central-nervous-system"],
+ "brain": ["26-3-the-central-nervous-system"],
+ "action potential": ["26-2-how-neurons-communicate"],
+ "synapse": ["26-2-how-neurons-communicate"],
+ "senses": ["27-1-sensory-processes"],
+ "vision": ["27-5-vision"],
+ "hearing": ["27-4-hearing-and-vestibular-sensation"],
+ # Circulatory System
+ "heart": ["31-1-overview-of-the-circulatory-system", "31-3-mammalian-heart-and-blood-vessels"],
+ "blood": ["31-2-components-of-the-blood", "31-1-overview-of-the-circulatory-system"],
+ "circulatory": ["31-1-overview-of-the-circulatory-system"],
+ "cardiovascular": ["31-1-overview-of-the-circulatory-system"],
+ # Immune System
+ "immune": ["33-1-innate-immune-response", "33-2-adaptive-immune-response"],
+ "antibod": ["33-3-antibodies"],
+ "infection": ["33-1-innate-immune-response"],
+ "vaccine": ["33-2-adaptive-immune-response"],
+ # Other Body Systems
+ "respiration": ["30-1-systems-of-gas-exchange", "30-3-breathing"],
+ "breathing": ["30-3-breathing"],
+ "lung": ["30-1-systems-of-gas-exchange"],
+ "digestion": ["25-1-digestive-systems", "25-3-digestive-system-processes"],
+ "stomach": ["25-1-digestive-systems"],
+ "intestine": ["25-3-digestive-system-processes"],
+ "hormone": ["28-1-types-of-hormones", "28-2-how-hormones-work"],
+ "endocrine": ["28-5-endocrine-glands", "28-1-types-of-hormones", "28-2-how-hormones-work"],
+ "endocrine system": ["28-5-endocrine-glands", "28-1-types-of-hormones", "28-2-how-hormones-work"],
+ "muscle": ["29-4-muscle-contraction-and-locomotion"],
+ "bone": ["29-2-bone"],
+ "skeleton": ["29-1-types-of-skeletal-systems"],
+ "kidney": ["32-2-the-kidneys-and-osmoregulatory-organs"],
+ "excretion": ["32-3-excretion-systems"],
+ "reproduction": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis"],
+ "reproductive": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis"],
+ "reproductive system": ["34-1-reproduction-methods", "34-3-human-reproductive-anatomy-and-gametogenesis", "34-4-hormonal-control-of-human-reproduction"],
+ "pregnancy": ["34-7-human-pregnancy-and-birth"],
+ "embryo": ["34-5-fertilization-and-early-embryonic-development"],
+ # Evolution & Genetics
+ "evolution": ["18-1-understanding-evolution", "19-1-population-evolution"],
+ "darwin": ["18-1-understanding-evolution"],
+ "natural selection": ["19-3-adaptive-evolution", "36-2-life-histories-and-natural-selection"],
+ "speciation": ["18-2-formation-of-new-species"],
+ "genetics": ["12-1-mendels-experiments-and-the-laws-of-probability", "12-3-laws-of-inheritance"],
+ "mendel": ["12-1-mendels-experiments-and-the-laws-of-probability"],
+ "inheritance": ["12-3-laws-of-inheritance"],
+ "heredity": ["12-3-laws-of-inheritance"],
+ "mutation": ["14-6-dna-repair"],
+ "phylogen": ["20-2-determining-evolutionary-relationships"],
+ # Microorganisms
+ "virus": ["21-1-viral-evolution-morphology-and-classification", "21-2-virus-infection-and-hosts"],
+ "bacteria": ["22-1-prokaryotic-diversity", "22-4-bacterial-diseases-in-humans"],
+ "prokaryote": ["4-2-prokaryotic-cells", "22-1-prokaryotic-diversity"],
+ "eukaryote": ["4-3-eukaryotic-cells"],
+ # Plants
+ "plant": ["23-1-the-plant-body"],
+ "leaf": ["23-4-leaves"],
+ "root": ["23-3-roots"],
+ "stem": ["23-2-stems"],
+ "xylem": ["23-5-transport-of-water-and-solutes-in-plants"],
+ "phloem": ["23-5-transport-of-water-and-solutes-in-plants"],
+ # Ecology
+ "ecology": ["35-1-the-scope-of-ecology", "36-6-community-ecology"],
+ "ecosystem": ["37-1-ecology-for-ecosystems", "37-2-energy-flow-through-ecosystems"],
+ "food chain": ["37-2-energy-flow-through-ecosystems"],
+ "food web": ["37-2-energy-flow-through-ecosystems"],
+ "biome": ["35-3-terrestrial-biomes", "35-4-aquatic-biomes"],
+ "population": ["36-1-population-demography", "36-3-environmental-limits-to-population-growth"],
+ "climate": ["35-5-climate-and-the-effects-of-global-climate-change"],
+ "climate change": ["35-5-climate-and-the-effects-of-global-climate-change"],
+ "biodiversity": ["38-1-the-biodiversity-crisis", "38-4-preserving-biodiversity"],
+ "carbon cycle": ["37-3-biogeochemical-cycles"],
+ "nitrogen cycle": ["37-3-biogeochemical-cycles"],
+ # Chemistry Basics
+ "atom": ["2-1-atoms-isotopes-ions-and-molecules-the-building-blocks"],
+ "water": ["2-2-water"],
+ "carbon": ["2-3-carbon"],
+ "carbohydrate": ["3-2-carbohydrates"],
+ "lipid": ["3-3-lipids"],
+ "nucleic acid": ["3-5-nucleic-acids"],
+ # Biotechnology
+ "biotechnology": ["17-1-biotechnology"],
+ "crispr": ["17-1-biotechnology"],
+ "cloning": ["17-1-biotechnology"],
+ "genome": ["17-2-mapping-genomes", "17-3-whole-genome-sequencing"],
+ "genomics": ["17-4-applying-genomics", "17-5-genomics-and-proteomics"],
+ }
+
+ def get_openstax_url(chapter_slug: str) -> str:
+ """Get the OpenStax URL for a chapter."""
+ return f"https://openstax.org/books/biology-ap-courses/pages/{chapter_slug}"
+
+ def parse_cnxml_to_text(cnxml_content: str) -> str:
+ """Parse CNXML content and extract plain text."""
+ try:
+ root = ET.fromstring(cnxml_content)
+ ns = {"cnxml": "http://cnx.rice.edu/cnxml"}
+
+ text_parts = []
+ title_elem = root.find(".//cnxml:title", ns)
+ if title_elem is not None and title_elem.text:
+ text_parts.append(f"# {title_elem.text}\n")
+
+ def extract_text(elem):
+ texts = []
+ if elem.text:
+ texts.append(elem.text)
+ for child in elem:
+ texts.append(extract_text(child))
+ if child.tail:
+ texts.append(child.tail)
+ return " ".join(texts)
+
+ for elem in root.iter():
+ tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
+ if tag == "para":
+ para_text = extract_text(elem)
+ if para_text.strip():
+ text_parts.append(para_text.strip())
+
+ full_text = "\n".join(text_parts)
+ full_text = re.sub(r'\n{3,}', '\n\n', full_text)
+ return full_text.strip()
+ except Exception:
+ return re.sub(r'<[^>]+>', ' ', cnxml_content).strip()
+
+ def get_chapter_list_for_llm() -> str:
+ """Return a formatted list of all chapters for LLM context.
+
+ Uses the complete OPENSTAX_CHAPTERS mapping defined above.
+ """
+ lines = []
+ for slug, title in OPENSTAX_CHAPTERS.items():
+ lines.append(f"- {slug}: {title}")
+ return "\n".join(lines)
+
+ def llm_match_topic_to_chapters(topic: str, max_chapters: int = 2) -> list:
+ """Use Gemini to match a topic to the most relevant chapter slugs (Tier 2 matching).
+
+ This is called when keyword matching (Tier 1) fails. It handles:
+ - Misspellings (e.g., "meitosis" -> meiosis)
+ - Alternate terms (e.g., "cell energy" -> ATP)
+ - Complex queries that don't match simple keywords
+
+ Returns empty list [] if the topic is not covered in the biology textbook.
+ """
+ from google import genai
+ from google.genai import types
+
+ try:
+ # Use us-central1 for consistency with Agent Engine
+ client = genai.Client(
+ vertexai=True,
+ project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
+ location="us-central1",
+ )
+
+ chapter_list = get_chapter_list_for_llm()
+
+ prompt = f"""You are a biology textbook expert. Match the user's topic to the MOST relevant chapters.
+
+User's topic: "{topic}"
+
+Available chapters from OpenStax Biology for AP Courses:
+{chapter_list}
+
+INSTRUCTIONS:
+1. Return EXACTLY {max_chapters} chapter slugs that BEST match the topic
+2. Order by relevance - put the MOST relevant chapter FIRST
+3. For biology topics (even misspelled like "meitosis"), ALWAYS find matching chapters
+4. Return empty [] ONLY for non-biology topics (physics, history, literature, etc.)
+5. Match the topic DIRECTLY - "reproductive system" should match reproduction chapters (34-*), not meiosis
+
+EXAMPLES:
+- "reproductive system" → ["34-3-human-reproductive-anatomy-and-gametogenesis", "34-1-reproduction-methods"]
+- "endocrine system" → ["28-5-endocrine-glands", "28-1-types-of-hormones"]
+- "meiosis" → ["11-1-the-process-of-meiosis", "11-2-sexual-reproduction"]
+- "ATP" → ["6-4-atp-adenosine-triphosphate", "6-1-energy-and-metabolism"]
+- "quantum physics" → []
+
+Return ONLY a JSON array with exactly {max_chapters} slugs (or [] for non-biology):"""
+
+ response = client.models.generate_content(
+ model=model_id,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_mime_type="application/json",
+ ),
+ )
+
+ slugs = json.loads(response.text.strip())
+ if isinstance(slugs, list):
+ # Validate that returned slugs actually exist in our chapter mapping
+ valid_slugs = [s for s in slugs if s in OPENSTAX_CHAPTERS]
+ return valid_slugs[:max_chapters]
+
+ except Exception as e:
+ logger.warning(f"LLM chapter matching failed: {e}")
+
+ return [] # Return empty if LLM fails
+
+ def fetch_openstax_content(topic: str) -> dict:
+ """Fetch OpenStax content for a topic using keyword matching with LLM fallback."""
+ import urllib.request
+ import urllib.error
+
+ topic_lower = topic.lower()
+ matched_slugs = set()
+
+ # First try keyword matching (fast path)
+ # Use word boundary matching to avoid false positives like "vision" in "cell division"
+ for keyword, slugs in KEYWORD_HINTS.items():
+ # Check for word boundary match using regex
+ # This ensures "vision" doesn't match "cell division"
+ pattern = r'\b' + re.escape(keyword) + r'\b'
+ if re.search(pattern, topic_lower):
+ matched_slugs.update(slugs)
+
+ # If no keyword match, use LLM to find relevant chapters
+ if not matched_slugs:
+ llm_slugs = llm_match_topic_to_chapters(topic)
+ if llm_slugs:
+ matched_slugs.update(llm_slugs)
+
+ # If still no match (LLM found nothing relevant), return empty with clear message
+ if not matched_slugs:
+ return {
+ "content": "",
+ "sources": [],
+ "note": f"I couldn't find any OpenStax Biology content related to '{topic}'. This topic may not be covered in the AP Biology curriculum."
+ }
+
+ chapter_slugs = list(matched_slugs)[:2]
+ content_parts = []
+ sources = []
+
+ for slug in chapter_slugs:
+ module_ids = CHAPTER_TO_MODULES.get(slug, [])
+ if not module_ids:
+ # Skip chapters without module mappings
+ continue
+
+ title = OPENSTAX_CHAPTERS.get(slug, slug)
+ url = get_openstax_url(slug)
+ chapter_content_found = False
+
+ for module_id in module_ids:
+ github_url = f"https://raw.githubusercontent.com/openstax/osbooks-biology-bundle/main/modules/{module_id}/index.cnxml"
+ try:
+ with urllib.request.urlopen(github_url, timeout=10) as response:
+ cnxml = response.read().decode('utf-8')
+ text = parse_cnxml_to_text(cnxml)
+ if text:
+ content_parts.append(f"## {title}\n\n{text}")
+ chapter_content_found = True
+ except Exception:
+ pass
+
+ # Only add source if we actually got content for this chapter
+ if chapter_content_found:
+ sources.append({"title": title, "url": url, "provider": "OpenStax Biology for AP Courses"})
+
+ return {
+ "content": "\n\n---\n\n".join(content_parts) if content_parts else "",
+ "sources": sources,
+ }
+
+ # =========================================================================
+ # TOOL FUNCTIONS
+ # =========================================================================
+
+ async def generate_flashcards(
+ tool_context: ToolContext,
+ topic: str,
+ ) -> str:
+ """
+ Generate personalized flashcard content as A2UI JSON.
+
+ Args:
+ topic: The topic for flashcards (e.g., "endocrine system", "photosynthesis", "meiosis")
+
+ Returns:
+ A2UI JSON string for Flashcard components
+ """
+ from google import genai
+ from google.genai import types
+
+ client = genai.Client(
+ vertexai=True,
+ project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
+ location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
+ )
+
+ # Fetch OpenStax content for context - REQUIRED
+ openstax_data = fetch_openstax_content(topic)
+ textbook_context = openstax_data.get("content", "")
+ sources = openstax_data.get("sources", [])
+
+ # If no OpenStax content found, return an error message instead of making up content
+ if not textbook_context or not sources:
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "message"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": f"No Content Available: {topic}"}, "usageHint": "h3"}}},
+ {"id": "message", "component": {"Text": {"text": {"literalString": f"Sorry, I couldn't find any OpenStax Biology content related to '{topic}'. This topic may not be covered in the AP Biology curriculum, or try rephrasing your request with more specific biology terms."}}}},
+ ]
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+ return json.dumps({"format": "flashcards", "a2ui": a2ui, "surfaceId": SURFACE_ID, "source": {"title": "No content found", "url": "", "provider": "OpenStax Biology for AP Courses"}})
+
+ prompt = f'''Create 4 MCAT study flashcards about "{topic}" for Maria (pre-med, loves gym analogies).
+Use gym/sports analogies in the answers where appropriate.
+IMPORTANT: Base all content ONLY on the textbook content provided below. Do not add information not present in the source.
+
+Textbook source content:
+{textbook_context[:4000]}'''
+
+ flashcard_schema = {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "front": {"type": "string", "description": "The question on the front of the flashcard"},
+ "back": {"type": "string", "description": "The answer on the back, using gym analogies"},
+ "category": {"type": "string", "description": "Category like Biochemistry"},
+ },
+ "required": ["front", "back", "category"],
+ },
+ }
+
+ response = client.models.generate_content(
+ model=model_id,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_mime_type="application/json",
+ response_schema=flashcard_schema,
+ ),
+ )
+ cards = json.loads(response.text.strip())
+
+ # Handle case where LLM returns empty or invalid response
+ if not cards or not isinstance(cards, list) or len(cards) == 0:
+ logger.warning(f"LLM returned empty flashcards for topic: {topic}, sources: {[s.get('title') for s in sources]}")
+ # Create fallback flashcards - this usually means content mismatch
+ source_title = sources[0].get('title', topic) if sources else topic
+ cards = [
+ {
+ "front": f"What are the key concepts in {source_title}?",
+ "back": f"Review the OpenStax chapter on {source_title} for detailed information about {topic}.",
+ "category": "Biology"
+ },
+ {
+ "front": f"Why is {topic} important in biology?",
+ "back": f"Understanding {topic} is fundamental to biology. Check the source material for specific details.",
+ "category": "Biology"
+ },
+ ]
+
+ # Build proper A2UI structure programmatically
+ card_ids = [f"c{i+1}" for i in range(len(cards))]
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "row"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": f"Study Flashcards: {topic}"}, "usageHint": "h3"}}},
+ {"id": "row", "component": {"Row": {"children": {"explicitList": card_ids}, "distribution": "start", "alignment": "stretch"}}},
+ ]
+ for i, card in enumerate(cards):
+ components.append({
+ "id": card_ids[i],
+ "component": {
+ "Flashcard": {
+ "front": {"literalString": card.get("front", "")},
+ "back": {"literalString": card.get("back", "")},
+ "category": {"literalString": card.get("category", "Biochemistry")},
+ }
+ }
+ })
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+
+ # Include source citation (we already verified sources exist above)
+ source_info = {
+ "title": sources[0].get("title", ""),
+ "url": sources[0].get("url", ""),
+ "provider": sources[0].get("provider", "OpenStax Biology for AP Courses"),
+ }
+
+ return json.dumps({"format": "flashcards", "a2ui": a2ui, "surfaceId": SURFACE_ID, "source": source_info})
+
+ async def generate_quiz(
+ tool_context: ToolContext,
+ topic: str,
+ ) -> str:
+ """
+ Generate personalized quiz questions as A2UI JSON.
+
+ Args:
+ topic: The topic for quiz questions (e.g., "endocrine system", "photosynthesis")
+
+ Returns:
+ A2UI JSON string for QuizCard components
+ """
+ from google import genai
+ from google.genai import types
+
+ client = genai.Client(
+ vertexai=True,
+ project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
+ location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"),
+ )
+
+ # Fetch OpenStax content for context - REQUIRED
+ openstax_data = fetch_openstax_content(topic)
+ textbook_context = openstax_data.get("content", "")
+ sources = openstax_data.get("sources", [])
+
+ # If no OpenStax content found, return an error message instead of making up content
+ if not textbook_context or not sources:
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "message"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": f"No Content Available: {topic}"}, "usageHint": "h3"}}},
+ {"id": "message", "component": {"Text": {"text": {"literalString": f"Sorry, I couldn't find any OpenStax Biology content related to '{topic}'. This topic may not be covered in the AP Biology curriculum, or try rephrasing your request with more specific biology terms."}}}},
+ ]
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+ return json.dumps({"format": "quiz", "a2ui": a2ui, "surfaceId": SURFACE_ID, "source": {"title": "No content found", "url": "", "provider": "OpenStax Biology for AP Courses"}})
+
+ prompt = f'''Create 2 MCAT quiz questions about "{topic}" for Maria (pre-med, loves gym analogies).
+Each question should have 4 options (a, b, c, d) with exactly one correct answer.
+Use gym/sports analogies in explanations where appropriate.
+IMPORTANT: Base all content ONLY on the textbook content provided below. Do not add information not present in the source.
+
+Textbook source content:
+{textbook_context[:4000]}'''
+
+ quiz_schema = {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "question": {"type": "string", "description": "The MCAT-style question"},
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "label": {"type": "string", "description": "The option text"},
+ "value": {"type": "string", "description": "Option identifier (a, b, c, or d)"},
+ "isCorrect": {"type": "boolean", "description": "True if this is the correct answer"},
+ },
+ "required": ["label", "value", "isCorrect"],
+ },
+ },
+ "explanation": {"type": "string", "description": "Detailed explanation with gym analogy"},
+ "category": {"type": "string", "description": "Category like Biochemistry"},
+ },
+ "required": ["question", "options", "explanation", "category"],
+ },
+ }
+
+ response = client.models.generate_content(
+ model=model_id,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ response_mime_type="application/json",
+ response_schema=quiz_schema,
+ ),
+ )
+ quizzes = json.loads(response.text.strip())
+
+ # Handle case where LLM returns empty or invalid response
+ if not quizzes or not isinstance(quizzes, list) or len(quizzes) == 0:
+ logger.warning(f"LLM returned empty quiz for topic: {topic}")
+ # Create a default quiz question based on the source content
+ quizzes = [{
+ "question": f"Which of the following best describes {topic}?",
+ "options": [
+ {"label": f"A key concept in {sources[0].get('title', 'biology')}", "value": "a", "isCorrect": True},
+ {"label": "A topic not covered in AP Biology", "value": "b", "isCorrect": False},
+ {"label": "An unrelated scientific concept", "value": "c", "isCorrect": False},
+ {"label": "None of the above", "value": "d", "isCorrect": False},
+ ],
+ "explanation": f"Review the OpenStax chapter on {sources[0].get('title', topic)} for more details.",
+ "category": "Biology"
+ }]
+
+ # Build proper A2UI structure programmatically
+ quiz_ids = [f"q{i+1}" for i in range(len(quizzes))]
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "row"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": f"Quick Quiz: {topic}"}, "usageHint": "h3"}}},
+ {"id": "row", "component": {"Row": {"children": {"explicitList": quiz_ids}, "distribution": "start", "alignment": "stretch"}}},
+ ]
+ for i, quiz in enumerate(quizzes):
+ # Transform options to A2UI format
+ options = []
+ for opt in quiz.get("options", []):
+ options.append({
+ "label": {"literalString": opt.get("label", "")},
+ "value": opt.get("value", ""),
+ "isCorrect": opt.get("isCorrect", False),
+ })
+ components.append({
+ "id": quiz_ids[i],
+ "component": {
+ "QuizCard": {
+ "question": {"literalString": quiz.get("question", "")},
+ "options": options,
+ "explanation": {"literalString": quiz.get("explanation", "")},
+ "category": {"literalString": quiz.get("category", "Biochemistry")},
+ }
+ }
+ })
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+
+ # Include source citation (we already verified sources exist above)
+ source_info = {
+ "title": sources[0].get("title", ""),
+ "url": sources[0].get("url", ""),
+ "provider": sources[0].get("provider", "OpenStax Biology for AP Courses"),
+ }
+
+ return json.dumps({"format": "quiz", "a2ui": a2ui, "surfaceId": SURFACE_ID, "source": source_info})
+
+ async def get_textbook_content(
+ tool_context: ToolContext,
+ topic: str,
+ ) -> str:
+ """
+ Get textbook content from OpenStax Biology for AP Courses.
+
+ Args:
+ topic: The biology topic to look up (e.g., "ATP", "glycolysis", "thermodynamics")
+
+ Returns:
+ Textbook content with source citation
+ """
+ openstax_data = fetch_openstax_content(topic)
+ content = openstax_data.get("content", "")
+ sources = openstax_data.get("sources", [])
+
+ if not content:
+ return json.dumps({
+ "content": f"No specific textbook content found for '{topic}'. Please use general biology knowledge.",
+ "sources": []
+ })
+
+ # Format source citations
+ source_citations = []
+ for src in sources:
+ source_citations.append({
+ "title": src.get("title", ""),
+ "url": src.get("url", ""),
+ "provider": src.get("provider", "OpenStax Biology for AP Courses"),
+ })
+
+ return json.dumps({
+ "content": content[:4000], # Limit content length
+ "sources": source_citations
+ })
+
+ # GCS media URLs (publicly accessible)
+ # Media bucket follows pattern: {PROJECT_ID}-media
+ project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
+ if not project_id:
+ raise ValueError("GOOGLE_CLOUD_PROJECT environment variable must be set")
+ MEDIA_BUCKET = f"{project_id}-media"
+ PODCAST_URL = f"https://storage.googleapis.com/{MEDIA_BUCKET}/assets/podcast.m4a"
+ VIDEO_URL = f"https://storage.googleapis.com/{MEDIA_BUCKET}/assets/video.mp4"
+
+ async def get_audio_content(tool_context: ToolContext) -> str:
+ """
+ Get the personalized learning podcast as an A2UI AudioPlayer component.
+
+ Returns:
+ A2UI JSON string for AudioPlayer component
+ """
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "descText", "player"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": "Personalized Learning Podcast"}, "usageHint": "h3"}}},
+ {"id": "descText", "component": {"Text": {"text": {"literalString": "A podcast tailored for Maria covering ATP, bond energy, and common MCAT misconceptions. Uses gym and sports analogies to explain complex biochemistry concepts."}}}},
+ {"id": "player", "component": {"AudioPlayer": {"url": {"literalString": PODCAST_URL}, "description": {"literalString": "ATP & Bond Energy - MCAT Review"}}}},
+ ]
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+
+ return json.dumps({"format": "audio", "a2ui": a2ui, "surfaceId": SURFACE_ID})
+
+ async def get_video_content(tool_context: ToolContext) -> str:
+ """
+ Get the personalized learning video as an A2UI Video component.
+
+ Returns:
+ A2UI JSON string for Video component
+ """
+ components = [
+ {"id": "mainColumn", "component": {"Column": {"children": {"explicitList": ["header", "descText", "videoPlayer"]}, "distribution": "start", "alignment": "stretch"}}},
+ {"id": "header", "component": {"Text": {"text": {"literalString": "Video Lesson"}, "usageHint": "h3"}}},
+ {"id": "descText", "component": {"Text": {"text": {"literalString": "A visual explanation of ATP hydrolysis and bond energy concepts, designed for visual learners preparing for the MCAT."}}}},
+ {"id": "videoPlayer", "component": {"Video": {"url": {"literalString": VIDEO_URL}}}},
+ ]
+
+ a2ui = [
+ {"beginRendering": {"surfaceId": SURFACE_ID, "root": "mainColumn"}},
+ {"surfaceUpdate": {"surfaceId": SURFACE_ID, "components": components}},
+ ]
+
+ return json.dumps({"format": "video", "a2ui": a2ui, "surfaceId": SURFACE_ID})
+
+ # Create the agent WITH tools
+ agent = Agent(
+ name="personalized_learning_agent",
+ model=model_id,
+ instruction="""You are a personalized learning assistant for biology students. You help with ANY biology topic - from endocrine system to evolution, from genetics to ecology.
+
+TOOLS AVAILABLE:
+- generate_flashcards(topic) - Creates study flashcards. ALWAYS pass the user's exact topic.
+- generate_quiz(topic) - Creates quiz questions. ALWAYS pass the user's exact topic.
+- get_textbook_content(topic) - Gets textbook content from OpenStax for answering questions
+- get_audio_content() - Plays a pre-recorded podcast about metabolism concepts
+- get_video_content() - Shows a pre-recorded video lesson
+
+CRITICAL: When user asks for flashcards or quiz on a topic, you MUST pass that EXACT topic to the tool.
+Example: User says "quiz me on endocrine system" → call generate_quiz(topic="endocrine system")
+Example: User says "flashcards for photosynthesis" → call generate_flashcards(topic="photosynthesis")
+
+WHEN TO USE TOOLS - CALL IMMEDIATELY WITHOUT ASKING:
+- User asks for "flashcards" → IMMEDIATELY call generate_flashcards with the EXACT topic they mentioned
+- User asks for "quiz" or "test me" → IMMEDIATELY call generate_quiz with the EXACT topic they mentioned
+- User asks for "podcast", "audio", or "listen" → IMMEDIATELY call get_audio_content() - DO NOT ask for confirmation
+- User asks for "video", "watch", or "show me" → IMMEDIATELY call get_video_content() - DO NOT ask for confirmation
+
+IMPORTANT: For podcast and video requests, call the tool IMMEDIATELY. Do NOT ask "would you like to listen?" or "would you like to watch?". Just call the tool right away.
+
+LEARNER PROFILE (Maria):
+- Pre-med student preparing for MCAT
+- Loves sports/gym analogies
+- Visual-kinesthetic learner
+
+Always use gym/sports analogies where appropriate. Be encouraging and supportive.""",
+ tools=[generate_flashcards, generate_quiz, get_textbook_content, get_audio_content, get_video_content],
+ )
+
+ # Wrap agent in AdkApp for deployment (skip App wrapper to avoid version issues)
+ app = AdkApp(agent=agent, enable_tracing=True)
+
+ print("Starting deployment (this takes 2-5 minutes)...")
+
+ # Deploy using agent_engines.create() - the recommended API
+ remote_app = agent_engines.create(
+ agent_engine=app,
+ display_name="Personalized Learning Agent",
+ requirements=[
+ "google-cloud-aiplatform[agent_engines,adk]",
+ "google-genai>=1.0.0",
+ ],
+ )
+
+ print(f"\n{'='*60}")
+ print("DEPLOYMENT SUCCESSFUL!")
+ print(f"{'='*60}")
+ print(f"Resource Name: {remote_app.resource_name}")
+ resource_id = remote_app.resource_name.split("/")[-1]
+ print(f"Resource ID: {resource_id}")
+ print(f"Context Bucket: gs://{context_bucket}/learner_context/")
+ print()
+ print("Next steps:")
+ print(f" 1. Copy the Resource ID above")
+ print(f" 2. Paste it into the notebook's AGENT_RESOURCE_ID variable")
+ print(f" 3. Upload learner context files to gs://{context_bucket}/learner_context/")
+ print(f" 4. Run the remaining notebook cells to configure and start the demo")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/personalized_learning/deploy_hosting.py b/samples/personalized_learning/deploy_hosting.py
new file mode 100755
index 00000000..51529ccb
--- /dev/null
+++ b/samples/personalized_learning/deploy_hosting.py
@@ -0,0 +1,521 @@
+#!/usr/bin/env python3
+"""
+Personalized Learning Demo - Cloud Run + Firebase Hosting Deployment
+
+This script deploys the demo to Cloud Run and configures Firebase Hosting
+to route traffic to it, providing a clean URL for internal sharing.
+
+Authentication:
+ Uses Identity-Aware Proxy (IAP) for Google-managed authentication.
+ Users must sign in with their Google account to access the demo.
+
+Prerequisites:
+ - gcloud CLI installed and authenticated
+ - firebase CLI installed (npm install -g firebase-tools)
+ - Firebase project linked to your GCP project
+ - OAuth consent screen configured (script will guide you)
+
+Required environment variables:
+ GOOGLE_CLOUD_PROJECT - Your GCP project ID
+
+Optional environment variables:
+ AGENT_ENGINE_PROJECT_NUMBER - Project number for Agent Engine
+ AGENT_ENGINE_RESOURCE_ID - Resource ID of deployed agent
+ IAP_ALLOWED_USERS - Comma-separated list of allowed user emails
+ IAP_ALLOWED_DOMAIN - Domain to allow (e.g., "google.com")
+
+Usage:
+ python deploy_hosting.py # Deploy everything
+ python deploy_hosting.py --cloud-run-only # Deploy only Cloud Run
+ python deploy_hosting.py --firebase-only # Deploy only Firebase Hosting
+ python deploy_hosting.py --service-name NAME # Custom service name
+ python deploy_hosting.py --allow-domain google.com # Allow domain access
+"""
+
+import os
+import sys
+import json
+import shutil
+import argparse
+import subprocess
+import time
+from pathlib import Path
+
+# Configuration defaults
+DEFAULT_SERVICE_NAME = "personalized-learning-demo"
+DEFAULT_REGION = "us-central1"
+
+
+def run_command(cmd: list[str], check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
+ """Run a shell command and optionally capture output."""
+ print(f" → {' '.join(cmd)}")
+ return subprocess.run(
+ cmd,
+ check=check,
+ capture_output=capture,
+ text=True,
+ )
+
+
+def get_project_id() -> str:
+ """Get GCP project ID from environment or gcloud config."""
+ project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
+ if project_id:
+ return project_id
+
+ # Try to get from gcloud config
+ result = run_command(
+ ["gcloud", "config", "get-value", "project"],
+ check=False,
+ capture=True,
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ return result.stdout.strip()
+
+ return None
+
+
+def check_prerequisites() -> dict:
+ """Check that required tools are installed."""
+ tools = {}
+
+ # Check gcloud
+ result = run_command(["gcloud", "--version"], check=False, capture=True)
+ tools["gcloud"] = result.returncode == 0
+
+ # Check firebase
+ result = run_command(["firebase", "--version"], check=False, capture=True)
+ tools["firebase"] = result.returncode == 0
+
+ return tools
+
+
+def prepare_build_context(demo_dir: Path) -> Path:
+ """
+ Prepare the build context by copying the A2UI dependency.
+ Returns the path to the prepared directory.
+ """
+ print("\nPreparing build context...")
+
+ # The A2UI web-lib is at ../../renderers/lit relative to demo_dir
+ a2ui_source = demo_dir.parent.parent / "renderers" / "lit"
+ a2ui_dest = demo_dir / "a2ui-web-lib"
+
+ if not a2ui_source.exists():
+ print(f"ERROR: A2UI web-lib not found at {a2ui_source}")
+ sys.exit(1)
+
+ # Remove old copy if exists
+ if a2ui_dest.exists():
+ print(f" Removing old {a2ui_dest}")
+ shutil.rmtree(a2ui_dest)
+
+ # Copy the dependency (excluding node_modules, but keeping dist/ for pre-built output)
+ print(f" Copying {a2ui_source} → {a2ui_dest}")
+
+ shutil.copytree(a2ui_source, a2ui_dest, ignore=shutil.ignore_patterns("node_modules", ".git"))
+
+ print(" Build context ready")
+ return demo_dir
+
+
+def cleanup_build_context(demo_dir: Path):
+ """Remove the temporary A2UI copy after deployment."""
+ a2ui_dest = demo_dir / "a2ui-web-lib"
+ if a2ui_dest.exists():
+ print(f"\nCleaning up {a2ui_dest}")
+ shutil.rmtree(a2ui_dest)
+
+
+def deploy_cloud_run(project_id: str, service_name: str, region: str) -> str:
+ """Deploy the frontend + API server to Cloud Run."""
+ print("\n" + "=" * 60)
+ print("DEPLOYING TO CLOUD RUN")
+ print("=" * 60)
+
+ demo_dir = Path(__file__).parent.resolve()
+
+ print(f"\nProject: {project_id}")
+ print(f"Service: {service_name}")
+ print(f"Region: {region}")
+ print(f"Source: {demo_dir}")
+
+ # Enable required APIs first and wait for propagation
+ print("\nEnabling required APIs...")
+ run_command([
+ "gcloud", "services", "enable",
+ "run.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "artifactregistry.googleapis.com",
+ "iap.googleapis.com",
+ "aiplatform.googleapis.com", # For Gemini API access
+ "--project", project_id,
+ "--quiet",
+ ], check=False) # Don't fail if already enabled
+
+ # Get project number for IAM bindings
+ print("\nConfiguring IAM permissions for Cloud Build...")
+ result = run_command([
+ "gcloud", "projects", "describe", project_id,
+ "--format", "value(projectNumber)",
+ ], capture=True, check=False)
+ project_number = result.stdout.strip() if result.returncode == 0 else None
+
+ if project_number:
+ # Grant Cloud Build service account access to Cloud Storage
+ compute_sa = f"{project_number}-compute@developer.gserviceaccount.com"
+
+ # Grant storage admin to the compute service account
+ run_command([
+ "gcloud", "projects", "add-iam-policy-binding", project_id,
+ "--member", f"serviceAccount:{compute_sa}",
+ "--role", "roles/storage.objectViewer",
+ "--quiet",
+ ], check=False)
+
+ # Grant logging permissions so we can see build logs
+ run_command([
+ "gcloud", "projects", "add-iam-policy-binding", project_id,
+ "--member", f"serviceAccount:{compute_sa}",
+ "--role", "roles/logging.logWriter",
+ "--quiet",
+ ], check=False)
+
+ # Also grant Cloud Build service account permissions
+ cloudbuild_sa = f"{project_number}@cloudbuild.gserviceaccount.com"
+ run_command([
+ "gcloud", "projects", "add-iam-policy-binding", project_id,
+ "--member", f"serviceAccount:{cloudbuild_sa}",
+ "--role", "roles/storage.objectViewer",
+ "--quiet",
+ ], check=False)
+
+ # Grant Artifact Registry writer permission for pushing Docker images
+ # This is required for Cloud Run source deployments
+ run_command([
+ "gcloud", "projects", "add-iam-policy-binding", project_id,
+ "--member", f"serviceAccount:{cloudbuild_sa}",
+ "--role", "roles/artifactregistry.writer",
+ "--quiet",
+ ], check=False)
+
+ # Grant Vertex AI User permission to the compute service account
+ # This allows Cloud Run to call the Gemini API
+ print("\nGranting Vertex AI permissions to Cloud Run service account...")
+ run_command([
+ "gcloud", "projects", "add-iam-policy-binding", project_id,
+ "--member", f"serviceAccount:{compute_sa}",
+ "--role", "roles/aiplatform.user",
+ "--quiet",
+ ], check=False)
+
+ print("Waiting for API and IAM permissions to propagate (30 seconds)...")
+ time.sleep(30)
+
+ # Prepare build context (copy A2UI dependency)
+ prepare_build_context(demo_dir)
+
+ try:
+ # Get Agent Engine config from environment
+ agent_project_number = os.environ.get("AGENT_ENGINE_PROJECT_NUMBER", "")
+ agent_resource_id = os.environ.get("AGENT_ENGINE_RESOURCE_ID", "")
+
+ # Build env vars string
+ env_vars = [
+ f"GOOGLE_CLOUD_PROJECT={project_id}",
+ f"GOOGLE_CLOUD_LOCATION={region}",
+ ]
+ if agent_project_number:
+ env_vars.append(f"AGENT_ENGINE_PROJECT_NUMBER={agent_project_number}")
+ if agent_resource_id:
+ env_vars.append(f"AGENT_ENGINE_RESOURCE_ID={agent_resource_id}")
+
+ # Deploy using gcloud run deploy with --source
+ # We use --allow-unauthenticated since Firebase Auth handles access control.
+ # The app requires @google.com sign-in (configurable in src/firebase-auth.ts).
+ cmd = [
+ "gcloud", "run", "deploy", service_name,
+ "--source", str(demo_dir),
+ "--region", region,
+ "--project", project_id,
+ "--allow-unauthenticated", # Firebase Auth handles access control
+ "--memory", "1Gi",
+ "--timeout", "300",
+ "--quiet", # Auto-confirm prompts (e.g., enabling APIs)
+ ]
+
+ # Add environment variables
+ for env_var in env_vars:
+ cmd.extend(["--set-env-vars", env_var])
+
+ run_command(cmd)
+
+ # Get the service URL
+ result = run_command([
+ "gcloud", "run", "services", "describe", service_name,
+ "--region", region,
+ "--project", project_id,
+ "--format", "value(status.url)",
+ ], capture=True)
+
+ service_url = result.stdout.strip()
+ print(f"\nCloud Run URL: {service_url}")
+
+ return service_url
+
+ finally:
+ # Always clean up the temporary A2UI copy
+ cleanup_build_context(demo_dir)
+
+
+def configure_iap_access(
+ project_id: str,
+ service_name: str,
+ region: str,
+ allowed_users: list[str] = None,
+ allowed_domain: str = None,
+):
+ """
+ Configure IAP access for the Cloud Run service.
+
+ For Cloud Run, IAP works through Cloud Run's built-in IAM.
+ We grant the 'Cloud Run Invoker' role to allowed users/domains.
+
+ Args:
+ project_id: GCP project ID
+ service_name: Cloud Run service name
+ region: GCP region
+ allowed_users: List of user emails to grant access
+ allowed_domain: Domain to grant access (e.g., "google.com")
+ """
+ print("\n" + "=" * 60)
+ print("CONFIGURING IAP ACCESS")
+ print("=" * 60)
+
+ members_to_add = []
+
+ # Add individual users
+ if allowed_users:
+ for user in allowed_users:
+ members_to_add.append(f"user:{user}")
+ print(f" Adding user: {user}")
+
+ # Add domain
+ if allowed_domain:
+ members_to_add.append(f"domain:{allowed_domain}")
+ print(f" Adding domain: {allowed_domain}")
+
+ if not members_to_add:
+ print("\n No users or domain specified for IAP access.")
+ print(" The service will be protected but no one can access it yet.")
+ print("\n To grant access later, use:")
+ print(f" gcloud run services add-iam-policy-binding {service_name} \\")
+ print(f" --region={region} --member='user:EMAIL' --role='roles/run.invoker'")
+ print(f"\n Or for a domain:")
+ print(f" gcloud run services add-iam-policy-binding {service_name} \\")
+ print(f" --region={region} --member='domain:DOMAIN' --role='roles/run.invoker'")
+ return
+
+ # Grant Cloud Run Invoker role to each member
+ for member in members_to_add:
+ print(f"\n Granting Cloud Run Invoker to {member}...")
+ run_command([
+ "gcloud", "run", "services", "add-iam-policy-binding", service_name,
+ "--region", region,
+ "--project", project_id,
+ "--member", member,
+ "--role", "roles/run.invoker",
+ "--quiet",
+ ], check=False)
+
+ print("\n IAP access configured successfully.")
+
+
+def update_firebase_config(service_name: str, region: str):
+ """Update firebase.json with the correct service configuration."""
+ firebase_json_path = Path(__file__).parent / "firebase.json"
+
+ config = {
+ "hosting": {
+ "public": "public",
+ "ignore": [
+ "firebase.json",
+ "**/.*",
+ "**/node_modules/**"
+ ],
+ "rewrites": [
+ {
+ "source": "**",
+ "run": {
+ "serviceId": service_name,
+ "region": region
+ }
+ }
+ ]
+ }
+ }
+
+ with open(firebase_json_path, "w") as f:
+ json.dump(config, f, indent=2)
+ f.write("\n")
+
+ print(f"Updated {firebase_json_path}")
+
+
+def update_firebaserc(project_id: str):
+ """Update .firebaserc with the project ID."""
+ firebaserc_path = Path(__file__).parent / ".firebaserc"
+
+ config = {
+ "projects": {
+ "default": project_id
+ }
+ }
+
+ with open(firebaserc_path, "w") as f:
+ json.dump(config, f, indent=2)
+ f.write("\n")
+
+ print(f"Updated {firebaserc_path}")
+
+
+def deploy_firebase_hosting(project_id: str):
+ """Deploy Firebase Hosting configuration."""
+ print("\n" + "=" * 60)
+ print("DEPLOYING FIREBASE HOSTING")
+ print("=" * 60)
+
+ print(f"\nProject: {project_id}")
+ print()
+
+ # Deploy hosting only
+ run_command([
+ "firebase", "deploy",
+ "--only", "hosting",
+ "--project", project_id,
+ ], check=True)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Deploy the Personalized Learning Demo to Cloud Run + Firebase Hosting"
+ )
+ parser.add_argument(
+ "--project",
+ type=str,
+ default=None,
+ help="GCP project ID (defaults to GOOGLE_CLOUD_PROJECT or gcloud config)",
+ )
+ parser.add_argument(
+ "--service-name",
+ type=str,
+ default=DEFAULT_SERVICE_NAME,
+ help=f"Cloud Run service name (default: {DEFAULT_SERVICE_NAME})",
+ )
+ parser.add_argument(
+ "--region",
+ type=str,
+ default=DEFAULT_REGION,
+ help=f"GCP region (default: {DEFAULT_REGION})",
+ )
+ parser.add_argument(
+ "--cloud-run-only",
+ action="store_true",
+ help="Only deploy to Cloud Run, skip Firebase Hosting",
+ )
+ parser.add_argument(
+ "--firebase-only",
+ action="store_true",
+ help="Only deploy Firebase Hosting (assumes Cloud Run is already deployed)",
+ )
+ parser.add_argument(
+ "--allow-domain",
+ type=str,
+ default=os.environ.get("IAP_ALLOWED_DOMAIN"),
+ help="Domain to allow access (e.g., 'google.com'). Also reads from IAP_ALLOWED_DOMAIN env var.",
+ )
+ parser.add_argument(
+ "--allow-users",
+ type=str,
+ default=os.environ.get("IAP_ALLOWED_USERS"),
+ help="Comma-separated list of user emails to allow. Also reads from IAP_ALLOWED_USERS env var.",
+ )
+
+ args = parser.parse_args()
+
+ # Get project ID
+ project_id = args.project or get_project_id()
+ if not project_id:
+ print("ERROR: No project ID found.")
+ print("Set GOOGLE_CLOUD_PROJECT environment variable or use --project flag")
+ sys.exit(1)
+
+ # Check prerequisites
+ print("Checking prerequisites...")
+ tools = check_prerequisites()
+
+ if not args.firebase_only and not tools["gcloud"]:
+ print("ERROR: gcloud CLI not found. Install from https://cloud.google.com/sdk")
+ sys.exit(1)
+
+ if not args.cloud_run_only and not tools["firebase"]:
+ print("ERROR: firebase CLI not found. Install with: npm install -g firebase-tools")
+ sys.exit(1)
+
+ # Change to the demo directory
+ os.chdir(Path(__file__).parent)
+
+ # Deploy Cloud Run
+ if not args.firebase_only:
+ deploy_cloud_run(project_id, args.service_name, args.region)
+
+ # Only configure IAP access if explicitly requested AND not using Firebase Hosting
+ # When using Firebase Hosting, access is controlled by Firebase Auth instead
+ if args.cloud_run_only and (args.allow_users or args.allow_domain):
+ allowed_users = args.allow_users.split(",") if args.allow_users else None
+ configure_iap_access(
+ project_id,
+ args.service_name,
+ args.region,
+ allowed_users=allowed_users,
+ allowed_domain=args.allow_domain,
+ )
+
+ # Deploy Firebase Hosting
+ if not args.cloud_run_only:
+ # Update config files
+ update_firebase_config(args.service_name, args.region)
+ update_firebaserc(project_id)
+
+ # Deploy
+ deploy_firebase_hosting(project_id)
+
+ # Print summary
+ print("\n" + "=" * 60)
+ print("DEPLOYMENT COMPLETE")
+ print("=" * 60)
+
+ if not args.cloud_run_only:
+ print(f"\n✅ Demo is live at: https://{project_id}.web.app")
+ print(f"\nAccess is controlled by Firebase Authentication.")
+ print(f"Users must sign in with a @google.com account (configurable in src/firebase-auth.ts).")
+
+ if args.cloud_run_only:
+ print(f"\nCloud Run service: {args.service_name}")
+ print(f"Region: {args.region}")
+ if args.allow_domain or args.allow_users:
+ print(f"\nAuthentication: IAP-protected")
+ if args.allow_domain:
+ print(f" Allowed domain: {args.allow_domain}")
+ if args.allow_users:
+ print(f" Allowed users: {args.allow_users}")
+ else:
+ print(f"\n⚠️ Cloud Run deployed with --no-allow-unauthenticated.")
+ print(f" Grant access with: gcloud run services add-iam-policy-binding {args.service_name} \\")
+ print(f" --region={args.region} --member='user:EMAIL' --role='roles/run.invoker'")
+
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/personalized_learning/firebase.json b/samples/personalized_learning/firebase.json
new file mode 100644
index 00000000..a68fd535
--- /dev/null
+++ b/samples/personalized_learning/firebase.json
@@ -0,0 +1,19 @@
+{
+ "hosting": {
+ "public": "public",
+ "ignore": [
+ "firebase.json",
+ "**/.*",
+ "**/node_modules/**"
+ ],
+ "rewrites": [
+ {
+ "source": "**",
+ "run": {
+ "serviceId": "personalized-learning-demo",
+ "region": "us-central1"
+ }
+ }
+ ]
+ }
+}
diff --git a/samples/personalized_learning/index.html b/samples/personalized_learning/index.html
new file mode 100644
index 00000000..174f879d
--- /dev/null
+++ b/samples/personalized_learning/index.html
@@ -0,0 +1,1028 @@
+
+
+
+
+
+
+ A2UI for Learning
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ auto_awesome
+ Gemini 2.5 Flash
+ expand_more
+
+
+
+
+
+
+
+
+
+
+
+ auto_awesome
+
+
Hello, Maria
+
+ Personalized learning chat: Bite-sized smart study tools right in the chat, based on your specific learning journey
+
+
+
+ lightbulb
+ ATP energy concepts
+
+
+ style
+ Bond energy flashcards
+
+
+ podcasts
+ Listen to podcast
+
+
+ quiz
+ Practice quiz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/personalized_learning/learner_context/01_maria_learner_profile.txt b/samples/personalized_learning/learner_context/01_maria_learner_profile.txt
new file mode 100644
index 00000000..4342f6fd
--- /dev/null
+++ b/samples/personalized_learning/learner_context/01_maria_learner_profile.txt
@@ -0,0 +1,70 @@
+LEARNER PROFILE: MARIA
+=======================
+
+DEMOGRAPHICS
+- Name: Maria
+- Age: 20
+- Education: Pre-med student at Cymbal University
+- Goal: MCAT preparation, medical school admission
+
+LEARNING CONTEXT
+- Currently studying for the MCAT
+- Often studies while at the gym (audio learning preferred during workouts)
+- Visual-kinesthetic learner who benefits from real-world analogies
+- Athletic background - responds well to sports/fitness metaphors
+
+ACADEMIC STRENGTHS
+- AP Biology: 92% proficiency
+ - Strong understanding of cellular respiration
+ - Solid grasp of ATP's role as "energy currency"
+ - Understands metabolic pathways well
+
+ACADEMIC GAPS
+- Chemistry (Bond Energy): 65% proficiency
+ - Struggles with thermodynamic concepts
+ - Difficulty connecting chemistry to biology knowledge
+ - Needs help bridging disciplines
+
+THE MISCONCEPTION
+================
+
+What Maria learned (correctly):
+- "ATP is the energy currency of the cell"
+- "When ATP is hydrolyzed to ADP, energy is released"
+- "This energy powers cellular work like muscle contraction"
+
+How Maria interprets it (incorrectly):
+- "Energy must be STORED IN the phosphate bonds of ATP"
+- "Breaking the bond releases the stored energy, like a battery releasing charge"
+- "The bond itself contains energy that gets released"
+
+Why this matters:
+- This misconception will cause errors on MCAT chemistry questions
+- It prevents true understanding of thermodynamics
+- It creates confusion when learning about other exergonic reactions
+
+THE CORRECT UNDERSTANDING
+=========================
+
+Breaking ANY chemical bond requires energy input (bond dissociation energy).
+Energy is never "stored in" a bond.
+
+ATP hydrolysis releases energy because:
+1. The PRODUCTS (ADP + inorganic phosphate) are MORE STABLE than ATP
+2. Systems naturally move toward lower energy (more stable) states
+3. The energy difference between reactants and products is released as usable energy
+
+Think of it as: ATP is in a "strained" high-energy state. ADP + Pi is a "relaxed" low-energy state. The system releases energy when moving from strained to relaxed - not because the strain "stored" energy, but because relaxation is energetically favorable.
+
+LEARNING OBJECTIVES FOR THIS PODCAST
+====================================
+
+After listening, Maria should be able to:
+
+1. Explain why "energy stored in bonds" is misleading terminology
+2. State that breaking bonds requires energy input, not release
+3. Describe ATP hydrolysis in terms of product stability (not bond energy storage)
+4. Use Gibbs Free Energy (ΔG) to explain spontaneous reactions
+5. Apply the compressed spring analogy instead of the battery analogy
+6. Connect her biology knowledge to chemistry thermodynamics
+7. Correctly answer MCAT questions about reaction energetics
diff --git a/samples/personalized_learning/learner_context/02_chemistry_bond_energy.txt b/samples/personalized_learning/learner_context/02_chemistry_bond_energy.txt
new file mode 100644
index 00000000..b069258f
--- /dev/null
+++ b/samples/personalized_learning/learner_context/02_chemistry_bond_energy.txt
@@ -0,0 +1,83 @@
+CHEMISTRY: BOND ENERGY AND BOND STRENGTH
+Source: OpenStax Chemistry, Chapter 7 - Chemical Bonding and Molecular Geometry
+=========================================================================
+
+BOND ENERGY DEFINITION
+----------------------
+
+Bond energy (also called bond dissociation energy) is the energy REQUIRED to break a covalent bond in a gaseous substance.
+
+Key point: This is always a POSITIVE value. You must PUT IN energy to break a bond. Breaking bonds is an endothermic process.
+
+"The energy required to break a specific covalent bond in one mole of gaseous molecules is called the bond energy or the bond dissociation energy."
+
+CRITICAL INSIGHT: THE "ENERGY STORED IN BONDS" MYTH
+---------------------------------------------------
+
+The phrase "energy stored in bonds" is scientifically misleading.
+
+Bonds do NOT store energy like containers or batteries. Instead:
+- FORMING bonds RELEASES energy (exothermic)
+- BREAKING bonds REQUIRES energy (endothermic)
+
+When we say a molecule has "high energy bonds," we really mean that the molecule itself is in an unstable, high-energy configuration relative to its potential products.
+
+HOW REACTIONS RELEASE OR ABSORB ENERGY
+--------------------------------------
+
+In any chemical reaction:
+1. Energy is REQUIRED to break bonds in the reactants
+2. Energy is RELEASED when new bonds form in products
+
+The NET energy change depends on the balance:
+- If more energy is released forming products than required to break reactants → EXOTHERMIC (releases heat)
+- If more energy is required to break reactants than released forming products → ENDOTHERMIC (absorbs heat)
+
+EXAMPLE: Why does burning fuel release energy?
+
+When you burn methane (CH4 + 2O2 → CO2 + 2H2O):
+- Energy IN: Breaking C-H bonds and O=O bonds
+- Energy OUT: Forming C=O bonds and O-H bonds
+
+The C=O and O-H bonds that form are STRONGER (more stable) than the bonds that break. More energy is released forming products than consumed breaking reactants. The NET result is energy release.
+
+This is NOT because energy was "stored in" the methane bonds. It's because the products (CO2 and H2O) are more thermodynamically stable than the reactants.
+
+BOND STRENGTH AND STABILITY
+---------------------------
+
+Stronger bonds = more stable molecules = lower energy state
+
+When a reaction produces molecules with stronger bonds (more stable), energy is released because the system moves to a lower energy state.
+
+Think of it like a ball rolling downhill:
+- High position = high potential energy = less stable
+- Low position = low potential energy = more stable
+- The ball releases energy (kinetic) as it moves to the more stable position
+
+Similarly:
+- Reactants at higher energy = less stable configuration
+- Products at lower energy = more stable configuration
+- Energy is released as the system moves toward stability
+
+COMMON BOND ENERGIES (kJ/mol)
+-----------------------------
+- C-H: 414
+- C-C: 347
+- C=C: 611
+- C≡C: 837
+- O-H: 464
+- O=O: 498
+- C=O: 803
+- N≡N: 946 (very strong, very stable)
+
+Note: Triple bonds are strongest, then double, then single. This is why N2 gas is so unreactive - the triple bond is extremely stable.
+
+KEY TAKEAWAY FOR MARIA
+----------------------
+
+When your biology textbook says "ATP releases energy," it does NOT mean energy was stored in ATP's phosphate bonds like electricity in a battery.
+
+It means: The products of ATP hydrolysis (ADP + phosphate) are MORE STABLE than ATP. The system releases energy when moving to this more stable state.
+
+This is the same principle as burning fuel - it's all about relative stability of reactants vs. products, measured by Gibbs Free Energy (ΔG).
diff --git a/samples/personalized_learning/learner_context/03_chemistry_thermodynamics.txt b/samples/personalized_learning/learner_context/03_chemistry_thermodynamics.txt
new file mode 100644
index 00000000..5d4ce18f
--- /dev/null
+++ b/samples/personalized_learning/learner_context/03_chemistry_thermodynamics.txt
@@ -0,0 +1,165 @@
+CHEMISTRY: THERMODYNAMICS AND GIBBS FREE ENERGY
+Source: OpenStax Chemistry, Chapters 5 & 16 - Thermochemistry and Thermodynamics
+================================================================================
+
+WHAT IS THERMODYNAMICS?
+-----------------------
+
+Thermodynamics is the study of relationships between energy and work in chemical and physical processes. It tells us:
+- Whether a reaction will happen spontaneously
+- How much energy is involved
+- Which direction a reaction will go
+
+SPONTANEOUS PROCESSES
+---------------------
+
+A spontaneous process occurs naturally without continuous external energy input.
+
+Important: "Spontaneous" does NOT mean "fast." It means the process is thermodynamically favorable - the system naturally moves in that direction.
+
+Examples of spontaneous processes:
+- Ice melting at room temperature
+- A ball rolling downhill
+- Iron rusting
+- ATP hydrolysis to ADP + Pi
+
+What makes a process spontaneous? The system moves toward greater stability (lower free energy).
+
+ENTHALPY (ΔH)
+-------------
+
+Enthalpy change measures heat absorbed or released at constant pressure.
+
+- Negative ΔH = EXOTHERMIC = releases heat
+- Positive ΔH = ENDOTHERMIC = absorbs heat
+
+Calculation: ΔH = (Energy to break bonds in reactants) - (Energy released forming bonds in products)
+
+If products have stronger bonds than reactants, ΔH is negative (exothermic).
+
+ENTROPY (ΔS)
+------------
+
+Entropy is a measure of disorder or dispersal of energy and matter.
+
+- Systems tend toward higher entropy (more disorder)
+- Positive ΔS = increase in disorder = favorable
+- Negative ΔS = decrease in disorder = unfavorable
+
+Examples:
+- Gas expanding into vacuum → entropy increases (favorable)
+- Ice melting to liquid → entropy increases (favorable)
+- Organizing a messy room → entropy decreases (requires energy input)
+
+GIBBS FREE ENERGY (ΔG) - THE KEY CONCEPT
+----------------------------------------
+
+Gibbs Free Energy combines enthalpy and entropy to predict spontaneity:
+
+ ΔG = ΔH - TΔS
+
+Where:
+- ΔG = Gibbs free energy change
+- ΔH = enthalpy change
+- T = temperature (in Kelvin)
+- ΔS = entropy change
+
+THE RULE:
+- ΔG < 0 (negative) → SPONTANEOUS (favorable, releases free energy)
+- ΔG > 0 (positive) → NON-SPONTANEOUS (unfavorable, requires energy input)
+- ΔG = 0 → AT EQUILIBRIUM
+
+WHAT DOES NEGATIVE ΔG REALLY MEAN?
+----------------------------------
+
+When ΔG is negative, the PRODUCTS have LOWER FREE ENERGY than the REACTANTS.
+
+Lower free energy = more stable
+
+So a spontaneous reaction (negative ΔG) is one where the products are more stable than the reactants. Energy is released because the system moves "downhill" energetically to a more stable state.
+
+This is NOT because energy was "stored" somewhere and got released. It's because stability increased, and that stability difference manifests as released energy.
+
+THE SECOND LAW OF THERMODYNAMICS
+--------------------------------
+
+All spontaneous processes increase the entropy of the universe.
+
+For a process to be spontaneous:
+- Either it releases enough heat to compensate for any entropy decrease
+- Or it increases entropy enough to compensate for any heat absorbed
+- Or both (exothermic AND entropy increase = definitely spontaneous)
+
+ATP HYDROLYSIS IN THERMODYNAMIC TERMS
+-------------------------------------
+
+ATP + H2O → ADP + Pi
+
+ΔG° ≈ -30.5 kJ/mol (under cellular conditions)
+
+This negative ΔG tells us:
+- The reaction is SPONTANEOUS
+- Products (ADP + Pi) are MORE STABLE than reactants (ATP)
+- Energy is released because the system moves to a lower energy state
+
+The -30.5 kJ/mol is NOT "energy stored in the phosphate bond."
+It is the FREE ENERGY DIFFERENCE between ATP and its products.
+
+WHY IS ADP + Pi MORE STABLE THAN ATP?
+-------------------------------------
+
+Several factors make the products lower in free energy:
+
+1. ELECTROSTATIC REPULSION
+ - ATP has four negative charges crowded on three phosphate groups
+ - These negative charges repel each other
+ - Hydrolysis separates these charges, reducing repulsion
+ - Less repulsion = more stable = lower energy
+
+2. RESONANCE STABILIZATION
+ - Inorganic phosphate (Pi) has excellent resonance stabilization
+ - More resonance structures = more stable electron distribution
+ - ADP also has better resonance than ATP
+
+3. SOLVATION (HYDRATION)
+ - ADP and Pi are better stabilized by surrounding water molecules
+ - Better solvation = lower energy = more stable
+
+Combined, these factors make ADP + Pi about 30.5 kJ/mol more stable than ATP under cellular conditions.
+
+THE COMPRESSED SPRING ANALOGY
+-----------------------------
+
+Think of ATP like a COMPRESSED SPRING, not like a battery:
+
+Compressed spring:
+- High potential energy (strained state)
+- Unstable, wants to release
+- Energy not "stored in the metal"
+- Energy is in the CONFIGURATION
+
+When spring releases:
+- Moves to relaxed state
+- Lower potential energy (stable state)
+- Can do work during transition
+
+Similarly, ATP:
+- High free energy (strained molecular configuration)
+- Unstable due to charge repulsion
+- Energy not "stored in bonds"
+- Energy is in the unstable CONFIGURATION
+
+When ATP hydrolyzes:
+- Moves to stable state (ADP + Pi)
+- Lower free energy
+- Releases ~30.5 kJ/mol that can do cellular work
+
+KEY TAKEAWAYS
+-------------
+
+1. ΔG (Gibbs Free Energy) determines spontaneity
+2. Negative ΔG means products are MORE STABLE than reactants
+3. Energy is released when systems move toward stability
+4. ATP hydrolysis has ΔG = -30.5 kJ/mol because ADP + Pi is more stable
+5. This is NOT "energy stored in bonds" - it's a stability difference
+6. The compressed spring is a better analogy than a battery
diff --git a/samples/personalized_learning/learner_context/04_biology_atp_cellular_respiration.txt b/samples/personalized_learning/learner_context/04_biology_atp_cellular_respiration.txt
new file mode 100644
index 00000000..93879258
--- /dev/null
+++ b/samples/personalized_learning/learner_context/04_biology_atp_cellular_respiration.txt
@@ -0,0 +1,142 @@
+BIOLOGY: ATP AND CELLULAR RESPIRATION
+Source: OpenStax Biology, Chapter 6 - Metabolism & Chapter 7 - Cellular Respiration
+===================================================================================
+
+ATP: THE ENERGY CURRENCY OF THE CELL
+------------------------------------
+
+Adenosine triphosphate (ATP) is the primary energy carrier in all living cells.
+
+Structure:
+- Adenine (nitrogenous base)
+- Ribose (5-carbon sugar)
+- Three phosphate groups (α, β, γ)
+
+The phosphate groups are connected by phosphoanhydride bonds. The bond between the β and γ phosphates is the one typically hydrolyzed in energy-releasing reactions.
+
+ATP HYDROLYSIS
+--------------
+
+ATP + H2O → ADP + Pi + Energy
+
+When the terminal (γ) phosphate is removed:
+- ATP becomes ADP (adenosine diphosphate)
+- Inorganic phosphate (Pi) is released
+- Energy is released that can power cellular work
+
+This energy drives:
+- Muscle contraction
+- Active transport across membranes
+- Biosynthesis of molecules
+- Cell division
+- Nerve impulse transmission
+
+HOW MUCH ENERGY?
+----------------
+
+Under standard conditions: ΔG° = -30.5 kJ/mol
+Under cellular conditions: ΔG can be -50 to -65 kJ/mol (even more favorable!)
+
+This is significant - enough to drive many cellular processes.
+
+THE "ENERGY CURRENCY" METAPHOR
+------------------------------
+
+ATP is called "energy currency" because:
+- It's the common medium of energy exchange
+- Catabolic reactions (breaking down food) "deposit" energy by making ATP
+- Anabolic reactions (building molecules) "withdraw" energy by using ATP
+- It's constantly recycled (humans make ~40 kg of ATP per day!)
+
+However, the "currency" metaphor can be misleading if students think ATP "stores" energy like a wallet stores money. A better understanding:
+
+ATP is like a CHARGED BATTERY that powers devices, but the "charge" isn't stored IN the battery material - the battery is in a high-energy state that releases energy when it moves to a lower-energy state.
+
+Even better: ATP is like a COMPRESSED SPRING ready to release.
+
+CELLULAR RESPIRATION: MAKING ATP
+--------------------------------
+
+Cellular respiration harvests energy from glucose to make ATP:
+
+C6H12O6 + 6O2 → 6CO2 + 6H2O + ~30-32 ATP
+
+Three main stages:
+1. Glycolysis (cytoplasm): Glucose → 2 Pyruvate + 2 ATP
+2. Citric Acid Cycle (mitochondria): Pyruvate → CO2 + electron carriers
+3. Oxidative Phosphorylation (mitochondria): Electron carriers → ~26-28 ATP
+
+WHY DOES GLUCOSE OXIDATION RELEASE ENERGY?
+------------------------------------------
+
+The same principle as ATP hydrolysis:
+
+The products (CO2 + H2O) are MORE STABLE than the reactants (glucose + O2).
+
+- CO2 has very strong C=O double bonds (803 kJ/mol each)
+- H2O has strong O-H bonds (464 kJ/mol each)
+- These products are in a lower energy state than glucose
+
+The energy difference is captured by converting ADP + Pi → ATP.
+
+So glucose doesn't "contain" energy that gets released - rather, breaking down glucose produces more stable products, and that stability difference is captured as ATP.
+
+COUPLING REACTIONS
+------------------
+
+ATP hydrolysis is "coupled" to energy-requiring reactions:
+
+Example - Muscle contraction:
+- Myosin needs energy to change shape and pull actin
+- ATP binds to myosin
+- ATP hydrolysis releases energy
+- Energy drives the conformational change
+- Muscle contracts
+
+The spontaneous reaction (ATP → ADP + Pi, negative ΔG) drives the non-spontaneous reaction (muscle protein shape change, positive ΔG).
+
+Combined: ΔG_total = ΔG_ATP + ΔG_muscle = negative (spontaneous overall)
+
+ATP-ADP CYCLE
+-------------
+
+ATP is constantly recycled:
+
+ENERGY IN (from food):
+Cellular Respiration: ADP + Pi → ATP (requires energy)
+
+ENERGY OUT (for work):
+Cellular Work: ATP → ADP + Pi (releases energy)
+
+A typical cell turns over its entire ATP pool every 1-2 minutes!
+
+THE KEY INSIGHT FOR MARIA
+-------------------------
+
+Biology correctly teaches that:
+- ATP hydrolysis releases energy
+- This energy powers cellular work
+- ATP is constantly regenerated
+
+The chemistry clarification:
+- The energy isn't "stored IN" the phosphate bonds
+- ATP is in a high-energy, unstable molecular configuration
+- ADP + Pi is in a lower-energy, more stable configuration
+- The energy "released" is the difference between these states (ΔG)
+- This is measured by Gibbs Free Energy
+
+UNIFYING BIOLOGY AND CHEMISTRY
+------------------------------
+
+Both subjects teach the same fundamental principle:
+- Systems move toward lower energy (greater stability)
+- When they do, energy is released
+- This is true for glucose → CO2 + H2O
+- This is true for ATP → ADP + Pi
+- This is true for any spontaneous reaction
+
+The language differs:
+- Biology: "Energy is released from ATP"
+- Chemistry: "Products are more stable than reactants (negative ΔG)"
+
+Both are describing the same phenomenon. Maria already understands this from biology - she just needs to connect it to chemistry terminology.
diff --git a/samples/personalized_learning/learner_context/05_misconception_resolution.txt b/samples/personalized_learning/learner_context/05_misconception_resolution.txt
new file mode 100644
index 00000000..a767fa41
--- /dev/null
+++ b/samples/personalized_learning/learner_context/05_misconception_resolution.txt
@@ -0,0 +1,188 @@
+RESOLVING THE ATP BOND ENERGY MISCONCEPTION
+============================================
+
+THE MISCONCEPTION
+-----------------
+
+"Energy is stored in ATP's phosphate bonds, and when the bond breaks, that stored energy is released - like a battery releasing stored electrical charge."
+
+This is one of the most common misconceptions in biology education, reinforced by casual language in textbooks.
+
+WHY STUDENTS DEVELOP THIS MISCONCEPTION
+---------------------------------------
+
+1. Textbook language: "Energy stored in ATP" is a common shorthand
+2. Battery analogy: Students naturally compare ATP to batteries
+3. Intuition: It "feels right" that breaking something releases what was inside
+4. Incomplete explanation: Biology courses often skip the chemistry details
+
+WHY THE MISCONCEPTION IS WRONG
+------------------------------
+
+FACT: Breaking ANY chemical bond requires energy input.
+
+This is the definition of bond energy (bond dissociation energy) - the energy REQUIRED to break a bond. It's always positive. You must PUT energy INTO a system to break bonds.
+
+If breaking bonds released energy, molecules would spontaneously fall apart. They don't, because bonds represent stable, low-energy configurations of electrons.
+
+THE CORRECT UNDERSTANDING
+-------------------------
+
+ATP hydrolysis releases energy because:
+
+1. ATP is in an UNSTABLE, high-energy molecular configuration
+ - Four negative charges crowded on three phosphate groups
+ - Electrostatic repulsion makes this configuration strained
+
+2. ADP + Pi is a MORE STABLE, lower-energy configuration
+ - Charges are more separated
+ - Better resonance stabilization
+ - Better solvation by water
+
+3. Energy is released when the system moves from unstable → stable
+ - This is measured by Gibbs Free Energy (ΔG)
+ - ΔG = -30.5 kJ/mol for ATP hydrolysis
+ - Negative ΔG means products are more stable
+
+THE COMPRESSED SPRING ANALOGY (CORRECT)
+---------------------------------------
+
+Think of ATP as a COMPRESSED SPRING:
+
+Compressed spring:
+- High potential energy
+- In a strained, unstable state
+- Energy is NOT "stored in the metal"
+- Energy is in the CONFIGURATION
+
+When spring is released:
+- Moves to relaxed state
+- Releases energy
+- Can do work (push something)
+- Now in low-energy, stable state
+
+ATP similarly:
+- High free energy (strained configuration)
+- Phosphate charges repel each other
+- Energy is NOT "stored in the bond"
+- Energy is in the unstable CONFIGURATION
+
+When ATP hydrolyzes:
+- Moves to stable state (ADP + Pi)
+- Releases ~30.5 kJ/mol
+- Can do cellular work
+- Products are more stable
+
+WHY THE BATTERY ANALOGY IS WRONG
+--------------------------------
+
+Battery:
+- Stores charge in a concentrated form
+- Releasing charge provides energy
+- The stored entity (charge) IS the energy
+
+ATP:
+- Doesn't "store" energy in bonds
+- The phosphate bond itself is quite ordinary
+- Energy comes from RELATIVE STABILITY difference
+- Breaking the bond actually requires energy
+
+A better battery comparison: It's like saying a battery at the top of a hill has "more energy" than one at the bottom. The battery itself hasn't changed - its POSITION (configuration) determines its energy relative to surroundings.
+
+SPORTS/GYM ANALOGIES FOR MARIA
+------------------------------
+
+ANALOGY 1: The Plank Position
+
+Holding a plank:
+- Your body is in a high-energy, unstable position
+- It takes continuous effort to maintain
+- You feel the "strain" of the position
+
+Collapsing to the ground:
+- Your body moves to a low-energy, stable position
+- You release effort/energy
+- The ground didn't "store" energy - you just moved to stability
+
+ATP = your body in plank position (strained, unstable)
+ADP + Pi = your body on the ground (relaxed, stable)
+Energy released = the effort difference between positions
+
+ANALOGY 2: Stretching a Resistance Band
+
+Stretched band:
+- High potential energy
+- Unstable, wants to snap back
+- Energy in the stretched CONFIGURATION
+
+Released band:
+- Low potential energy
+- Stable, relaxed state
+- Can do work while releasing
+
+ATP = stretched resistance band
+ADP + Pi = relaxed band
+Energy = stored in the stretch, not "in the rubber"
+
+ANALOGY 3: Water Behind a Dam
+
+Water at high elevation:
+- High potential energy
+- Unstable relative to lower positions
+- Can do work (hydroelectric) when released
+
+Water at low elevation:
+- Low potential energy
+- More stable position
+- Energy was released during the fall
+
+ATP = water behind dam (high energy configuration)
+ADP + Pi = water in lower reservoir (stable configuration)
+Energy = the elevation difference, not "stored in water"
+
+CORRECTED LANGUAGE
+------------------
+
+INSTEAD OF: "ATP stores energy in its bonds"
+SAY: "ATP is in a high-energy, unstable configuration"
+
+INSTEAD OF: "Breaking ATP bonds releases stored energy"
+SAY: "ATP hydrolysis releases energy because the products are more stable"
+
+INSTEAD OF: "Energy comes from the phosphate bond"
+SAY: "Energy comes from the stability difference between ATP and ADP + Pi"
+
+MCAT IMPLICATIONS
+-----------------
+
+The MCAT tests this concept explicitly:
+
+Correct answers involve understanding:
+- Breaking bonds requires energy (endothermic)
+- Forming bonds releases energy (exothermic)
+- Net reaction energy depends on bond energies of reactants vs. products
+- ΔG determines spontaneity, not "stored energy"
+
+Wrong answers often include:
+- "Energy is released when bonds break"
+- "High-energy bonds store more energy"
+- "ATP releases energy because its bonds are weak"
+
+Maria's corrected understanding will help her avoid these traps.
+
+THE UNIFIED PRINCIPLE
+---------------------
+
+Both biology and chemistry teach the same core idea:
+
+SYSTEMS MOVE TOWARD LOWER ENERGY (GREATER STABILITY)
+
+When they do, energy is released that can do work.
+
+This applies to:
+- ATP → ADP + Pi (cellular energy)
+- Glucose → CO2 + H2O (respiration)
+- Food → smaller molecules (digestion)
+- Any spontaneous chemical reaction
+
+Maria already understands this intuitively from biology. The chemistry just gives it precise language (Gibbs Free Energy) and explains WHY ATP products are more stable (electrostatics, resonance, solvation).
diff --git a/samples/personalized_learning/learner_context/06_mcat_practice_concepts.txt b/samples/personalized_learning/learner_context/06_mcat_practice_concepts.txt
new file mode 100644
index 00000000..5e12639f
--- /dev/null
+++ b/samples/personalized_learning/learner_context/06_mcat_practice_concepts.txt
@@ -0,0 +1,157 @@
+MCAT PRACTICE: BOND ENERGY AND THERMODYNAMICS
+==============================================
+
+This document contains the key concepts and question patterns Maria should master for MCAT success.
+
+CORE MCAT CONCEPTS
+------------------
+
+1. BOND ENERGY IS ENERGY REQUIRED TO BREAK A BOND
+ - Always positive (endothermic)
+ - Higher bond energy = stronger bond = more stable
+ - Breaking bonds costs energy; forming bonds releases energy
+
+2. ENTHALPY OF REACTION (ΔH)
+ ΔH = Σ(bond energies broken) - Σ(bond energies formed)
+
+ - If ΔH < 0: Exothermic (more energy released forming bonds than consumed breaking bonds)
+ - If ΔH > 0: Endothermic (more energy consumed breaking bonds than released forming bonds)
+
+3. GIBBS FREE ENERGY (ΔG)
+ ΔG = ΔH - TΔS
+
+ - If ΔG < 0: Spontaneous (products more stable than reactants)
+ - If ΔG > 0: Non-spontaneous (products less stable than reactants)
+ - If ΔG = 0: At equilibrium
+
+4. RELATIONSHIP BETWEEN STABILITY AND ENERGY
+ - Lower energy = more stable
+ - Spontaneous reactions move toward stability (lower ΔG)
+ - Energy is released when systems become more stable
+
+COMMON MCAT QUESTION TYPES
+--------------------------
+
+TYPE 1: Calculate ΔH from bond energies
+
+Example:
+Given: C-H = 414 kJ/mol, O=O = 498 kJ/mol, C=O = 803 kJ/mol, O-H = 464 kJ/mol
+
+Calculate ΔH for: CH4 + 2O2 → CO2 + 2H2O
+
+Solution:
+Bonds broken: 4(C-H) + 2(O=O) = 4(414) + 2(498) = 2652 kJ
+Bonds formed: 2(C=O) + 4(O-H) = 2(803) + 4(464) = 3462 kJ
+ΔH = 2652 - 3462 = -810 kJ/mol (exothermic)
+
+Key insight: More energy released forming products than consumed breaking reactants.
+
+TYPE 2: Predict spontaneity
+
+Example:
+A reaction has ΔH = +50 kJ/mol and ΔS = +200 J/mol·K. At what temperature does it become spontaneous?
+
+Solution:
+Spontaneous when ΔG < 0
+ΔG = ΔH - TΔS < 0
+ΔH < TΔS
+T > ΔH/ΔS = 50,000 J / 200 J/K = 250 K
+
+Above 250 K, the entropy increase overcomes the positive enthalpy.
+
+TYPE 3: Compare stability
+
+Example:
+Why is ATP hydrolysis spontaneous?
+
+WRONG answer: "Because energy stored in the phosphate bond is released"
+CORRECT answer: "Because the products (ADP + Pi) are more thermodynamically stable than ATP due to reduced electrostatic repulsion, increased resonance stabilization, and improved solvation"
+
+TYPE 4: Coupled reactions
+
+Example:
+Given: ATP hydrolysis: ΔG = -30.5 kJ/mol
+ Glutamine synthesis: ΔG = +14.2 kJ/mol
+
+Is the coupled reaction spontaneous?
+
+Solution:
+ΔG_total = -30.5 + 14.2 = -16.3 kJ/mol
+Yes, spontaneous (negative ΔG)
+
+ATP hydrolysis drives the otherwise unfavorable synthesis.
+
+CRITICAL DISTINCTIONS
+---------------------
+
+BOND ENERGY vs. STABILITY
+- High bond energy = strong bond = stable molecule
+- It takes MORE energy to break strong bonds
+- Strong bonds don't "store" more energy - they're just harder to break
+
+EXOTHERMIC vs. SPONTANEOUS
+- Exothermic (ΔH < 0): Releases heat
+- Spontaneous (ΔG < 0): Thermodynamically favorable
+
+A reaction can be:
+- Exothermic AND spontaneous (most common for favorable reactions)
+- Endothermic AND spontaneous (if TΔS is large enough)
+- Exothermic AND non-spontaneous (if TΔS is negative enough)
+
+BREAKING vs. FORMING BONDS
+- Breaking bonds: ALWAYS requires energy (endothermic step)
+- Forming bonds: ALWAYS releases energy (exothermic step)
+- Net effect depends on the balance
+
+ATP-SPECIFIC MCAT POINTS
+------------------------
+
+1. ATP hydrolysis ΔG° ≈ -30.5 kJ/mol under standard conditions
+
+2. Why is ATP high-energy?
+ - Electrostatic repulsion between phosphate groups
+ - NOT because the P-O bond is "weak" or "stores energy"
+
+3. Why is ADP + Pi more stable?
+ - Charge separation reduces repulsion
+ - Better resonance in phosphate ion
+ - Better hydration of products
+
+4. ATP coupling
+ - Spontaneous ATP hydrolysis can drive non-spontaneous reactions
+ - Combined ΔG must be negative for the coupled reaction to proceed
+
+5. ATP is constantly recycled
+ - Humans produce ~40 kg ATP per day
+ - Same ATP molecules recycled thousands of times
+ - ATP ↔ ADP + Pi is a cycle, not a one-way consumption
+
+PRACTICE QUESTION FOR MARIA
+---------------------------
+
+Question:
+A student claims that "ATP is a high-energy molecule because it stores a lot of energy in its phosphate bonds, which is released when the bonds break."
+
+Which of the following best corrects this misconception?
+
+A) ATP bonds are actually weak and break easily, releasing stored energy.
+B) Breaking any chemical bond requires energy input; ATP hydrolysis releases energy because the products are more thermodynamically stable.
+C) ATP has more electrons than ADP, which is why breaking it releases energy.
+D) The energy comes from the adenine base, not the phosphate groups.
+
+Correct Answer: B
+
+Explanation: Breaking bonds ALWAYS requires energy input (positive bond dissociation energy). ATP hydrolysis releases energy because ADP + Pi (the products) are in a lower free energy state than ATP (more stable). This is due to reduced electrostatic repulsion, better resonance stabilization, and improved solvation - NOT because energy was "stored in" the bond.
+
+SUMMARY FOR MARIA
+-----------------
+
+Remember these key points for MCAT:
+
+1. Breaking bonds = costs energy (always)
+2. Forming bonds = releases energy (always)
+3. Net ΔH = bonds broken - bonds formed
+4. ΔG determines spontaneity (negative = spontaneous)
+5. ATP releases energy because products are MORE STABLE
+6. "High-energy bond" means the MOLECULE is unstable, not that the bond stores energy
+7. Compressed spring analogy > battery analogy
diff --git a/samples/personalized_learning/package.json b/samples/personalized_learning/package.json
new file mode 100644
index 00000000..17329382
--- /dev/null
+++ b/samples/personalized_learning/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "personalized-learning-demo",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A2UI sample demonstrating personalized educational content generation",
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently \"npm run dev:api\" \"npm run dev:vite\"",
+ "dev:vite": "vite",
+ "dev:api": "tsx api-server.ts",
+ "dev:agent": "cd agent && python server.py",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "start:all": "concurrently \"npm run dev:api\" \"npm run dev:vite\" \"npm run dev:agent\"",
+ "test": "node tests/unit-tests.mjs && node tests/integration-tests.mjs",
+ "test:unit": "node tests/unit-tests.mjs",
+ "test:integration": "node tests/integration-tests.mjs",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@a2ui/web-lib": "file:../../renderers/lit",
+ "@google/genai": "^1.22.0",
+ "@lit-labs/signals": "^0.1.3",
+ "@lit/context": "^1.1.4",
+ "firebase": "^10.14.1",
+ "google-auth-library": "^9.0.0",
+ "lit": "^3.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20.10.0",
+ "concurrently": "^8.2.2",
+ "dotenv": "^16.3.1",
+ "tsx": "^4.7.0",
+ "typescript": "^5.3.2",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/samples/personalized_learning/public/404.html b/samples/personalized_learning/public/404.html
new file mode 100644
index 00000000..f0c7f1bb
--- /dev/null
+++ b/samples/personalized_learning/public/404.html
@@ -0,0 +1,35 @@
+
+
+
+
+ Page Not Found
+
+
+
+
+
404
+
Page not found
+
+
+
diff --git a/samples/personalized_learning/public/assets/.gitkeep b/samples/personalized_learning/public/assets/.gitkeep
new file mode 100644
index 00000000..5c265b8f
--- /dev/null
+++ b/samples/personalized_learning/public/assets/.gitkeep
@@ -0,0 +1,8 @@
+# This directory holds audio/video assets for the demo.
+# See NOTEBOOKLM_GUIDE.md for instructions on generating these files.
+#
+# Expected files:
+# - podcast.m4a (Audio podcast generated from NotebookLM)
+# - video.mp4 (Educational video)
+#
+# These files are gitignored due to their size.
diff --git a/samples/personalized_learning/public/assets/openstax-bio-glossary.md b/samples/personalized_learning/public/assets/openstax-bio-glossary.md
new file mode 100644
index 00000000..dc3ba0ac
--- /dev/null
+++ b/samples/personalized_learning/public/assets/openstax-bio-glossary.md
@@ -0,0 +1,737 @@
+**Symbols**
+**3' UTR** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00006)
+**40S ribosomal subunit** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00005)
+**5' cap** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00001)
+**5' UTR** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00005)
+**60S ribosomal subunit** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00006)
+**7-methylguanosine cap** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00002)
+***α*****\-helix** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00009)
+***β*****\-pleated sheet** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00010)
+**A**
+**absorption spectrum** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00006)
+**abstract** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00020)
+**acetyl CoA** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00001)
+**acid** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00015)
+**activation energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00008)
+**activators** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00003)
+**active site** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00002)
+**Active transport** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00001)
+**adenosine triphosphate** [6.4 ATP: Adenosine Triphosphate](https://openstax.org/books/biology-ap-courses/pages/6-4-atp-adenosine-triphosphate#term-00001)
+**adhesion** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00012)
+**aerobic respiration** [7.2 Glycolysis](https://openstax.org/books/biology-ap-courses/pages/7-2-glycolysis#term-00005)
+**aliphatic hydrocarbons** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00006)
+**alleles** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00001)
+**allosteric inhibition** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00007)
+**alternation of generations** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00004)
+**Amino acids** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00005)
+**aminoacyl tRNA synthetases** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00002)
+**amphiphilic** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00006)
+**Anabolic** [6.1 Energy and Metabolism](https://openstax.org/books/biology-ap-courses/pages/6-1-energy-and-metabolism#term-00003)
+**anaerobic** [7.2 Glycolysis](https://openstax.org/books/biology-ap-courses/pages/7-2-glycolysis#term-00002)
+**anaerobic cellular respiration** [7.5 Metabolism without Oxygen](https://openstax.org/books/biology-ap-courses/pages/7-5-metabolism-without-oxygen#term-00002)
+**analyze** [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00008), [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00002), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00005), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000072), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00008), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00009), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00013)
+**anaphase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00017)
+**aneuploid** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00007)
+**Anions** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00034)
+**antenna pigments** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00011)
+**antibiotic resistance** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00013)
+**anticodon** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00007)
+**antiporter** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00009)
+**apoptosis** [9.3 Response to the Signal](https://openstax.org/books/biology-ap-courses/pages/9-3-response-to-the-signal#term-00003)
+**applied science** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00017)
+**Aquaporins** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00009)
+**aromatic hydrocarbons** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00007)
+**atom** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00002)
+**atomic number** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00008)
+**atomic weight** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00010)
+**Atoms** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00001)
+**ATP** [6.4 ATP: Adenosine Triphosphate](https://openstax.org/books/biology-ap-courses/pages/6-4-atp-adenosine-triphosphate#term-00002)
+**Autocrine signals** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00013)
+**Autoinducers** [9.4 Signaling in Single-Celled Organisms](https://openstax.org/books/biology-ap-courses/pages/9-4-signaling-in-single-celled-organisms#term-00003)
+**autosomes** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00014), [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00003)
+**Autotrophic** [22.3 Prokaryotic Metabolism](https://openstax.org/books/biology-ap-courses/pages/22-3-prokaryotic-metabolism#term-00001)
+**B**
+**balanced chemical equation** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00027)
+**base** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00016)
+**Basic science** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00016)
+**binary (prokaryotic) fission** [10.5 Prokaryotic Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-5-prokaryotic-cell-division#term-00001)
+**biochemistry** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00020)
+**bioenergetics** [6.1 Energy and Metabolism](https://openstax.org/books/biology-ap-courses/pages/6-1-energy-and-metabolism#term-00001)
+**biological macromolecules** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00001)
+**biology** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00003)
+**biomarker** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00006)
+**biosphere** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00016)
+**Biotechnology** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00001)
+**blending theory of inheritance** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00003)
+**blotting** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00008)
+**botany** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00025)
+**Buffers** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00018)
+**C**
+**CAAT box** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00003)
+**Calculate** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00004), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000012)
+**calorie** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00004)
+**Calvin cycle** [8.3 Using Light to Make Organic Molecules](https://openstax.org/books/biology-ap-courses/pages/8-3-using-light-to-make-organic-molecules#term-00001)
+**cAMP-dependent kinase (A-kinase)** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00009)
+**capillary action** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00013)
+**Carbohydrates** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00002)
+**carbon** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00001), [8.3 Using Light to Make Organic Molecules](https://openstax.org/books/biology-ap-courses/pages/8-3-using-light-to-make-organic-molecules#term-00002)
+**carotenoids** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00005)
+**carrier protein** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00010)
+**catabolic** [6.1 Energy and Metabolism](https://openstax.org/books/biology-ap-courses/pages/6-1-energy-and-metabolism#term-00004)
+**catabolite activator protein (CAP)** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00010)
+**Cations** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00033)
+**caveolin** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00005)
+**cell** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00006)
+**cell cycle** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00001)
+**cell cycle checkpoints** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00001)
+**cell plate** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00021)
+**cell wall** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00019)
+**Cell-surface receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00016)
+**cellular cloning** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00018)
+**Cellulose** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00008)
+**centimorgans (cM)** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00006)
+**Central Dogma** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00001)
+**central vacuole** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00022)
+**centrioles** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00007)
+**centromere** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00012)
+**centrosome** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00017)
+**Channel proteins** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00008)
+**chaperones** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00013)
+**chemical bonds** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00023)
+**chemical energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00003)
+**Chemical reactions** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00024)
+**chemical reactivity** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00013)
+**chemical synapses** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00010)
+**Chemiosmosis** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00005)
+**chemoautotrophs** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00003)
+**chiasmata** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00009)
+**chitin** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00009)
+**chlorophyll** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00021)
+**Chlorophyll *a*** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00003)
+**chlorophyll *b*** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00004)
+**chloroplast** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00006)
+**Chloroplasts** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00020)
+**chromatids** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00011)
+**chromatin** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00010)
+**Chromosomal Theory of Inheritance** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00001)
+**chromosome inversion** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00012)
+**Chromosomes** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00009), [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00006)
+**cilia** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00006)
+***cis*****\-acting element** [16.4 Eukaryotic Transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-4-eukaryotic-transcriptional-gene-regulation#term-00001)
+**citric acid cycle** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00002)
+**clathrin** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00002)
+**cleavage furrow** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00020)
+**codominance** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00011)
+**codons** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00004)
+**coenzymes** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00009)
+**cofactors** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00008)
+**cohesin** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00006)
+**cohesion** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00010)
+**colinear** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00002)
+**community** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00014)
+**Compare** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000031)
+**competitive inhibition** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00005)
+**complementary DNA (cDNA) libraries** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00015)
+**compounds** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00028)
+**concentration gradient** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00003)
+**conclusion** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00026)
+**condensin** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00012)
+**consensus** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00011)
+**construct a mathematical model** [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00006)
+**contig** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00005)
+**Continuous variation** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00002)
+**control group** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00015)
+**core enzyme** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00008)
+**covalent bonds** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00038)
+**crossover** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00011)
+**cyclic AMP (cAMP)** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00008)
+**cyclin-dependent kinases** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00003)
+**cyclins** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00002)
+**cytochrome complex** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00017)
+**Cytogenetic mapping** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00011)
+**Cytokinesis** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00019)
+**cytoplasm** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00004)
+**cytoskeleton** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00001)
+**cytosol** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00005)
+**D**
+**Deductive reasoning** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00002)
+**degenerate** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00003)
+**dehydration synthesis** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00004)
+**denaturation** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00004)
+**denature** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00003)
+**deoxynucleotide** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00002)
+**deoxyribonucleic acid (DNA)** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00002)
+**dephosphorylation** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00002)
+**Describe** [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00002), [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00004), [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00003), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000011), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000021), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00001), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-000010), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-000030)
+**Describe a model** [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00012)
+**Descriptive (or discovery) science** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00011)
+**Design a plan** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00002)
+**desmosomes** [4.6 Connections between Cells and Cellular Activities](https://openstax.org/books/biology-ap-courses/pages/4-6-connections-between-cells-and-cellular-activities#term-00004)
+**diacylglycerol (DAG)** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00011)
+**dicer** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00008)
+**dideoxynucleotides** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00003)
+**Diffusion** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00004)
+**dihybrid** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00004)
+**dimer** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00003)
+**dimerization** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00002)
+**diploid** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00003)
+**diploid-dominant** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00002)
+**Disaccharides** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00004)
+**discontinuous variation** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00004)
+**discussion** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00025)
+**dissociation** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00009)
+**DNA microarrays** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00010)
+**dominant lethal** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00018)
+**Dominant traits** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00011)
+**downstream** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00007)
+**E**
+**ecosystem** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00015)
+**electrochemical gradient** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00002)
+**electrogenic pump** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00010)
+**electrolytes** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00037)
+**electromagnetic spectrum** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00002)
+**electron configuration** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00022)
+**electron microscopes** [4.1 Studying Cells](https://openstax.org/books/biology-ap-courses/pages/4-1-studying-cells#term-00003)
+**electron orbitals** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00021)
+**electron transfer** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00035)
+**electron transport chain** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00016)
+**electronegativity** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00040)
+**Electrons** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00007)
+**electrophoresis** [14.2 DNA Structure and Sequencing](https://openstax.org/books/biology-ap-courses/pages/14-2-dna-structure-and-sequencing#term-00001)
+**Elements** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00003)
+**Enantiomers** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00010)
+**end-Cretaceous extinction** [38.1 The Biodiversity Crisis](https://openstax.org/books/biology-ap-courses/pages/38-1-the-biodiversity-crisis#term-00002)
+**endergonic reactions** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00007)
+**endocrine cells** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00012)
+**endocrine signals** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00011)
+**Endocytosis** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00001)
+**endoplasmic reticulum (ER)** [4.4 The Endomembrane System and Proteins](https://openstax.org/books/biology-ap-courses/pages/4-4-the-endomembrane-system-and-proteins#term-00002)
+**energy coupling** [6.4 ATP: Adenosine Triphosphate](https://openstax.org/books/biology-ap-courses/pages/6-4-atp-adenosine-triphosphate#term-00004)
+**enhancers** [16.4 Eukaryotic Transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-4-eukaryotic-transcriptional-gene-regulation#term-00003)
+**enthalpy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00005)
+**entropy** [6.3 The Laws of Thermodynamics](https://openstax.org/books/biology-ap-courses/pages/6-3-the-laws-of-thermodynamics#term-00003)
+**Enzyme-linked receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00020)
+**Enzymes** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00002)
+**epigenetic** [16.1 Regulation of Gene Expression](https://openstax.org/books/biology-ap-courses/pages/16-1-regulation-of-gene-expression#term-00002)
+**epistasis** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00007)
+**equilibrium** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00031)
+**eukaryotes** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00008)
+**eukaryotic cells** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00001)
+**eukaryotic initiation factor-2 (eIF-2)** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00002)
+**euploid** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00006)
+**Evaluate** [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00001), [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00004), [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00007)
+**evaporation** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00006)
+**evolution** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00017)
+**exergonic reactions** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00006)
+**Exocytosis** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00007)
+**exons** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00004)
+**exonuclease** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00012)
+**Explain** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000010), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00003), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00007)
+**exponential growth** [36.3 Environmental Limits to Population Growth](https://openstax.org/books/biology-ap-courses/pages/36-3-environmental-limits-to-population-growth#term-00001)
+**expressed sequence tag (EST)** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00016)
+**extracellular domain** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00017)
+**extracellular matrix** [4.6 Connections between Cells and Cellular Activities](https://openstax.org/books/biology-ap-courses/pages/4-6-connections-between-cells-and-cellular-activities#term-00001)
+**F**
+**F1** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00007)
+**F2** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00008)
+**facilitated transport** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00006)
+**FACT** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00007)
+**false negative** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00008)
+**falsifiable** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00013)
+**Feedback inhibition** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00010)
+**fermentation** [7.5 Metabolism without Oxygen](https://openstax.org/books/biology-ap-courses/pages/7-5-metabolism-without-oxygen#term-00001)
+**fertilization** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00001)
+**fixation** [8.3 Using Light to Make Organic Molecules](https://openstax.org/books/biology-ap-courses/pages/8-3-using-light-to-make-organic-molecules#term-00003)
+**flagella** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00005)
+**fluid mosaic model** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00001)
+**foreign DNA** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00011)
+**free energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00004)
+**FtsZ** [10.5 Prokaryotic Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-5-prokaryotic-cell-division#term-00003)
+**Functional groups** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00002)
+**G**
+**G-protein-linked receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00019)
+**G0 phase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00022)
+**G1 phase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00004)
+**G2 phase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00008)
+**gametes** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00002)
+**gametophytes** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00006)
+**Gap junctions** [4.6 Connections between Cells and Cellular Activities](https://openstax.org/books/biology-ap-courses/pages/4-6-connections-between-cells-and-cellular-activities#term-00005)
+**GC-rich boxes** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00004)
+**Gel electrophoresis** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00005)
+**gene expression** [16.1 Regulation of Gene Expression](https://openstax.org/books/biology-ap-courses/pages/16-1-regulation-of-gene-expression#term-00001)
+**Gene targeting** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00023)
+**Gene therapy** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00026)
+**genes** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00007)
+**genetic diagnosis** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00024)
+**Genetic engineering** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00020)
+**genetic map** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00003)
+**genetic marker** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00004)
+**genetic recombination** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00007)
+**genetic testing** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00025)
+**genetic variability** [19.2 Population Genetics](https://openstax.org/books/biology-ap-courses/pages/19-2-population-genetics#term-00009)
+**genetically modified organism** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00021)
+**genome** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00001)
+**genome annotation** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00009)
+**Genome mapping** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00002)
+**genomic libraries** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00014)
+**Genomics** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00001)
+**genotype** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00003)
+**Geometric isomers** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00009)
+**germ cells** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00005)
+**GLUT proteins** [7.7 Regulation of Cellular Respiration](https://openstax.org/books/biology-ap-courses/pages/7-7-regulation-of-cellular-respiration#term-00001)
+**Glycogen** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00007)
+**glycolipids** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00003)
+**Glycolysis** [7.2 Glycolysis](https://openstax.org/books/biology-ap-courses/pages/7-2-glycolysis#term-00001)
+**glycoproteins** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00002)
+**glycosidic bond** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00005)
+**Golgi apparatus** [4.4 The Endomembrane System and Proteins](https://openstax.org/books/biology-ap-courses/pages/4-4-the-endomembrane-system-and-proteins#term-00005)
+**granum** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00010)
+**growth factors** [9.3 Response to the Signal](https://openstax.org/books/biology-ap-courses/pages/9-3-response-to-the-signal#term-00002)
+**guanosine diphosphate (GDP)** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00004)
+**guanosine triphosphate (GTP)** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00003)
+**H**
+**hairpin** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00014)
+**haploid** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00004)
+**haploid-dominant** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00003)
+**Heat energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00010), [6.3 The Laws of Thermodynamics](https://openstax.org/books/biology-ap-courses/pages/6-3-the-laws-of-thermodynamics#term-00002)
+**heat of vaporization** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00005)
+**helicase** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00001)
+**hemizygous** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00016)
+**heterotrophic** [22.3 Prokaryotic Metabolism](https://openstax.org/books/biology-ap-courses/pages/22-3-prokaryotic-metabolism#term-00002)
+**heterotrophs** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00002)
+**heterozygous** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00005)
+**histone acetylation** [16.7 Cancer and Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-7-cancer-and-gene-regulation#term-00001)
+**histone proteins** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00009)
+**holoenzyme** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00009)
+**Homeostasis** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00001)
+**homologous** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00005)
+**homologous recombination** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00002)
+**homology** [20.2 Determining Evolutionary Relationships](https://openstax.org/books/biology-ap-courses/pages/20-2-determining-evolutionary-relationships#term-00001)
+**homozygous** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00004)
+**Hormones** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00003)
+**host DNA** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00012)
+**hybridizations** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00005)
+**Hydrocarbons** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00005)
+**hydrogen bond** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00042)
+**hydrolysis reactions** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00005)
+**hydrophilic** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00001), [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00004)
+**hydrophobic** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00002), [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00005)
+**hypertonic** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00016)
+**hypothesis** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00006)
+**hypothesis-based science** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00012)
+**hypotonic** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00015)
+**I**
+**identify** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00006)
+**Identify and justify the data** [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00005)
+**in vitro** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00011)
+**incomplete dominance** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00010)
+**induced fit** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00004)
+**Induced mutations** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00005)
+**inducers** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00013)
+**inducible operons** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00011)
+**Inductive reasoning** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00001)
+**inert gases** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00018)
+**inhibitor** [9.3 Response to the Signal](https://openstax.org/books/biology-ap-courses/pages/9-3-response-to-the-signal#term-00001)
+**initiation complex** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00001)
+**initiation site** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00005)
+**initiator tRNA** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00003)
+**inositol phospholipids** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00010)
+**inositol triphosphate (IP3)** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00012)
+**Integral proteins** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00007)
+**intercellular signaling** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00001)
+**interkinesis** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00013)
+**Intermediate filaments** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00003)
+**Internal receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00015)
+**interphase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00002)
+**intracellular mediators** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00014)
+**intracellular signaling** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00002)
+**introduction** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00021)
+**introns** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00005)
+**Ion channel-linked receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00018)
+**Ionic bonds** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00036)
+**ions** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00020)
+**irreversible** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00029)
+**isomerase** [7.2 Glycolysis](https://openstax.org/books/biology-ap-courses/pages/7-2-glycolysis#term-00004)
+**Isomers** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00003)
+**isotonic** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00017)
+**Isotopes** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00002), [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00011)
+**J**
+**Justify** [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00007), [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-000020), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00008), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00011)
+**Justify the claim** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00001)
+**K**
+**karyogram** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00002)
+**karyokinesis** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00009)
+**karyotype** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00001)
+**kinase** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00006)
+**kinetic energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00001)
+**kinetochore** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00014)
+**Kozak’s rules** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00006)
+**Krebs cycle** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00004)
+**L**
+***lac*** **operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00012)
+**lagging strand** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00008)
+**law of dominance** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00001)
+**law of independent assortment** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00003)
+**law of mass action** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00032)
+**law of segregation** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00002)
+**leading strand** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00006)
+**life cycles** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00001)
+**life sciences** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00009)
+**ligand** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00004)
+**ligase** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00011)
+**light microscopes** [4.1 Studying Cells](https://openstax.org/books/biology-ap-courses/pages/4-1-studying-cells#term-00002)
+**light-dependent reactions** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00012)
+**light-harvesting complex** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00013)
+**light-independent reactions** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00013)
+**linkage** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00005)
+**linkage analysis** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00006)
+**Lipids** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00001)
+**Litmus** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00014)
+**locus** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00008)
+**lysis buffer** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00002)
+**lysosomes** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00018)
+**M**
+**macromolecules** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00004)
+**mass number** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00009)
+**materials and methods** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00023)
+**mating factor** [9.4 Signaling in Single-Celled Organisms](https://openstax.org/books/biology-ap-courses/pages/9-4-signaling-in-single-celled-organisms#term-00001)
+**meiosis** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00003), [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00004)
+**Meiosis II** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00005)
+**mesophyll** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00004)
+**messenger RNA (mRNA)** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00004)
+**metabolism** [6.1 Energy and Metabolism](https://openstax.org/books/biology-ap-courses/pages/6-1-energy-and-metabolism#term-00002)
+**metabolome** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00004)
+**Metabolomics** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00003)
+**Metagenomics** [17.4 Applying Genomics](https://openstax.org/books/biology-ap-courses/pages/17-4-applying-genomics#term-00004)
+**metaphase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00015)
+**metaphase plate** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00016)
+**micelle** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00010)
+**Microbiology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00021)
+**microfilaments** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00002)
+**microRNAs** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00007)
+**microsatellite polymorphisms** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00009)
+**microscope** [4.1 Studying Cells](https://openstax.org/books/biology-ap-courses/pages/4-1-studying-cells#term-00001)
+**microtubules** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00004)
+**mismatch repair** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00002)
+**Mitochondria** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00013)
+**mitosis** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00010)
+**mitotic phase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00003)
+**mitotic spindle** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00006)
+**model** [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-000020)
+**model organism** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00008)
+**model system** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00001)
+**molecular biology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00019)
+**molecule** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00003)
+**Molecules** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00014)
+**monohybrid** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00006)
+**monomers** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00002)
+**Monosaccharides** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00003)
+**monosomy** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00008)
+**multiple cloning site (MCS)** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00014)
+**Mutations** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00004)
+**Myc** [16.7 Cancer and Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-7-cancer-and-gene-regulation#term-00002)
+**N**
+**natural sciences** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00008)
+**negative regulators** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00008)
+**neurobiology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00022)
+**neurotransmitters** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00009)
+**neutron** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00006)
+**next-generation sequencing** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00007)
+**noble gases** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00019)
+**noncompetitive inhibition** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00006)
+**nondisjunction** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00005)
+**nonparental types** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00003)
+**Nonpolar covalent bonds** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00041)
+**nonsense codons** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00006)
+**nontemplate strand** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00004)
+**northern blotting** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00010)
+**nuclear envelope** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00007)
+**Nucleic acids** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00001)
+**nucleoid** [4.2 Prokaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-2-prokaryotic-cells#term-00002)
+**nucleolus** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00011)
+**nucleoplasm** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00008)
+**nucleosome** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00010)
+**nucleotide excision repair** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00003)
+**nucleotides** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00005)
+**nucleus** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00004), [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00006)
+**O**
+**octamer boxes** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00005)
+**octet rule** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00016)
+**Okazaki fragments** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00007)
+**Omega** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00007)
+**oncogenes** [10.4 Cancer and the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-4-cancer-and-the-cell-cycle#term-00002)
+**operator** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00007)
+**operons** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00001)
+**organ system** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00011)
+**organelles** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00005), [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00002)
+**organic molecules** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00004)
+**Organisms** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00012)
+**Organs** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00010)
+**origin** [10.5 Prokaryotic Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-5-prokaryotic-cell-division#term-00002)
+**Osmolarity** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00014)
+**Osmosis** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00011)
+**oxidative phosphorylation** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00006)
+**P**
+**P0** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00006)
+**p21** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00006)
+**p53** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00005)
+**P680** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00018)
+**P700** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00020)
+**pairwise-end sequencing** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00006)
+**Paleontology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00023)
+**paracentric** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00014)
+**paracrine signals** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00007)
+**Parental types** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00004)
+**Passive transport** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00002)
+**pedigree analysis** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00009)
+**Peer-reviewed manuscripts** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00019)
+**peptide bond** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00006)
+**peptidyl transferase** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00007)
+**pericentric** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00013)
+**periodic table** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00012)
+**Peripheral proteins** [5.1 Components and Structure](https://openstax.org/books/biology-ap-courses/pages/5-1-components-and-structure#term-00008)
+**Peroxisomes** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00014)
+**pH scale** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00017)
+**Pharmacogenomics** [17.4 Applying Genomics](https://openstax.org/books/biology-ap-courses/pages/17-4-applying-genomics#term-00002)
+**phenotype** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00002)
+**phosphatases** [9.3 Response to the Signal](https://openstax.org/books/biology-ap-courses/pages/9-3-response-to-the-signal#term-00004)
+**phosphoanhydride bonds** [6.4 ATP: Adenosine Triphosphate](https://openstax.org/books/biology-ap-courses/pages/6-4-atp-adenosine-triphosphate#term-00003)
+**phosphodiester** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00009)
+**phosphodiesterase** [9.3 Response to the Signal](https://openstax.org/books/biology-ap-courses/pages/9-3-response-to-the-signal#term-00005)
+**Phospholipids** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00009)
+**Phosphorylation** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00003)
+**photoact** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00015)
+**photoautotrophs** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00001)
+**photon** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00014)
+**photosystem** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00008)
+**photosystem I** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00010)
+**photosystem II** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00009)
+**phylogenetic tree** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00018)
+**physical map** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00005)
+**physical sciences** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00010)
+**pigment** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00008)
+**pinocytosis** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00003)
+**plagiarism** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00022)
+**plasma membrane** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00003)
+**plasmids** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00001)
+**plasmodesmata** [4.6 Connections between Cells and Cellular Activities](https://openstax.org/books/biology-ap-courses/pages/4-6-connections-between-cells-and-cellular-activities#term-00002)
+**plasmolysis** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00018)
+**Point mutations** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00008)
+**polar covalent bond** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00039)
+**poly-A tail** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00003), [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00002)
+**polygenic** [17.4 Applying Genomics](https://openstax.org/books/biology-ap-courses/pages/17-4-applying-genomics#term-00001)
+**polymers** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00003)
+**polynucleotide** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00006)
+**polyploid** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00010)
+**polysaccharide** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00006)
+**polysome** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00001)
+**population** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00013)
+**Pose** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000052), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00003)
+**Pose three questions** [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00005)
+**Pose two questions** [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00008)
+**positive regulator** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00009)
+**Possible answers:** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00006)
+**post-transcriptional** [16.1 Regulation of Gene Expression](https://openstax.org/books/biology-ap-courses/pages/16-1-regulation-of-gene-expression#term-00003)
+**post-translational** [16.1 Regulation of Gene Expression](https://openstax.org/books/biology-ap-courses/pages/16-1-regulation-of-gene-expression#term-00004)
+**potential energy** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00002)
+**potocytosis** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00004)
+**predict** [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00001), [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00003), [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00006), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000020), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00007), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000022), [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000032), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00002), [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00010)
+**preinitiation complex** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00006)
+**Primary active transport** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00004)
+**primary electron acceptor** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00019)
+**primary structure** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00007)
+**primase** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00004)
+**primer** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00005)
+**probes** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00007)
+**product rule** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00013)
+**products** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00026)
+**prokaryote** [4.2 Prokaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-2-prokaryotic-cells#term-00001)
+**Prokaryotes** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00007)
+**prometaphase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00013)
+**promoter** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00010)
+**proofreading** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00001)
+**prophase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00011)
+**prosthetic group** [7.4 Oxidative Phosphorylation](https://openstax.org/books/biology-ap-courses/pages/7-4-oxidative-phosphorylation#term-00001)
+**proteases** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00003)
+**proteasome** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00007)
+**protein signature** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00007)
+**Proteins** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00001)
+**proteome** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00001)
+**proteomics** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00002)
+**proto-oncogenes** [10.4 Cancer and the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-4-cancer-and-the-cell-cycle#term-00001)
+**proton** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00005)
+**pumps** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00003)
+**Punnett square** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00007)
+**pure culture** [17.4 Applying Genomics](https://openstax.org/books/biology-ap-courses/pages/17-4-applying-genomics#term-00003)
+**purines** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00007)
+**pyrimidines** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00008)
+**pyruvate** [7.2 Glycolysis](https://openstax.org/books/biology-ap-courses/pages/7-2-glycolysis#term-00003)
+**Q**
+**quaternary structure** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00012)
+**questions** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000062)
+**quiescent** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00023)
+**quorum sensing** [9.4 Signaling in Single-Celled Organisms](https://openstax.org/books/biology-ap-courses/pages/9-4-signaling-in-single-celled-organisms#term-00002)
+**R**
+**Radiation hybrid mapping** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00012)
+**reactants** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00025)
+**reaction center** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00012)
+**reading frame** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00005)
+**receptor-mediated endocytosis** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00006)
+**receptors** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00006)
+**recessive lethal** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00017)
+**Recessive traits** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00012)
+**reciprocal cross** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00010)
+**recombinant DNA** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00016)
+**recombinant proteins** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00017)
+**recombination frequency** [13.1 Chromosomal Theory and Genetic Linkages](https://openstax.org/books/biology-ap-courses/pages/13-1-chromosomal-theory-and-genetic-linkages#term-00005)
+**recombination nodules** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00010)
+**redox reactions** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00001)
+**reduction** [8.3 Using Light to Make Organic Molecules](https://openstax.org/books/biology-ap-courses/pages/8-3-using-light-to-make-organic-molecules#term-00004)
+**reduction division** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00014)
+**replication forks** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00002)
+**Repressors** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00002)
+**Reproductive cloning** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00019)
+**Restriction endonucleases** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00015)
+**restriction fragment length polymorphisms** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00008)
+**results** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00024)
+**retinoblastoma protein (Rb)** [10.3 Control of the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-3-control-of-the-cell-cycle#term-00004)
+**reverse transcriptase PCR (RT-PCR)** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00006)
+**Reversible reactions** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00030)
+**Review articles** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00027)
+**Rho-dependent termination** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00012)
+**Rho-independent termination** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00013)
+**ribonucleases** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00004)
+**ribonucleic acid (RNA)** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00003)
+**Ribosomal RNA (rRNA)** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00010)
+**Ribosomes** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00012)
+**RNA editing** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00001)
+**RNA-binding proteins** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00003)
+**RNA-induced silencing complex (RISC)** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00009)
+**RNAs** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00002)
+**rough endoplasmic reticulum (RER)** [4.4 The Endomembrane System and Proteins](https://openstax.org/books/biology-ap-courses/pages/4-4-the-endomembrane-system-and-proteins#term-00003)
+**S**
+**S phase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00005)
+**saturated fatty acid** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00004)
+**Science** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00004)
+**scientific method** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00005)
+**scientific questions** [23 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/23-science-practice-challenge-questions#term-00004)
+**Second messengers** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00007)
+**Secondary active transport** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00005)
+**secondary structure** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00008)
+**Select** [19 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/19-science-practice-challenge-questions#term-00005)
+**selection** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-00009)
+**selectively permeable** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00001)
+**septum** [10.5 Prokaryotic Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-5-prokaryotic-cell-division#term-00004)
+**Sequence mapping** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00013)
+**serendipity** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00018)
+**Shine-Dalgarno sequence** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00005)
+**shotgun sequencing** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00004)
+**signal integration** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00005)
+**signal sequence** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00008)
+**signal transduction** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00001)
+**signaling cells** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00003)
+**signaling pathway** [9.2 Propagation of the Signal](https://openstax.org/books/biology-ap-courses/pages/9-2-propagation-of-the-signal#term-00004)
+**silent mutations** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00007)
+**single nucleotide polymorphisms** [17.2 Mapping Genomes](https://openstax.org/books/biology-ap-courses/pages/17-2-mapping-genomes#term-00010)
+**Single-strand binding proteins** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00003)
+**sliding clamp** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00009)
+**small nuclear** [15.3 Eukaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-3-eukaryotic-transcription#term-00001)
+**smooth endoplasmic reticulum (SER)** [4.4 The Endomembrane System and Proteins](https://openstax.org/books/biology-ap-courses/pages/4-4-the-endomembrane-system-and-proteins#term-00004)
+**solute** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00012)
+**solutes** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00005)
+**solvent** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00007)
+**somatic cells** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00002)
+**Southern blotting** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00009)
+**specific heat capacity** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00003)
+**spectrophotometer** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00007)
+**sphere of hydration** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00008)
+**splicing** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00006)
+**Spontaneous mutations** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00006)
+**sporophyte** [11.2 Sexual Reproduction](https://openstax.org/books/biology-ap-courses/pages/11-2-sexual-reproduction#term-00007)
+**starch** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00001)
+**start codon** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00004)
+**steroids** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00011)
+**stomata** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00005)
+**stroma** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00011)
+**Structural isomers** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00008)
+**substituted hydrocarbons** [2.3 Carbon](https://openstax.org/books/biology-ap-courses/pages/2-3-carbon#term-00011)
+**substrate-level phosphorylation** [7.1 Energy in Living Systems](https://openstax.org/books/biology-ap-courses/pages/7-1-energy-in-living-systems#term-00004)
+**substrates** [6.5 Enzymes](https://openstax.org/books/biology-ap-courses/pages/6-5-enzymes#term-00001)
+**sum rule** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00014)
+**summarize** [22 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/22-science-practice-challenge-questions#term-000042)
+**surface tension** [2.2 Water](https://openstax.org/books/biology-ap-courses/pages/2-2-water#term-00011)
+**symporter** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00008)
+**synapsis** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00008)
+**synaptic signal** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00008)
+**synaptonemal complex** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00007)
+**Systems biology** [17.5 Genomics and Proteomics](https://openstax.org/books/biology-ap-courses/pages/17-5-genomics-and-proteomics#term-00005)
+**T**
+**target cells** [9.1 Signaling Molecules and Cellular Receptors](https://openstax.org/books/biology-ap-courses/pages/9-1-signaling-molecules-and-cellular-receptors#term-00005)
+**TCA cycle** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00003)
+**telomerase** [14.5 DNA Replication in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/14-5-dna-replication-in-eukaryotes#term-00002)
+**telomeres** [14.5 DNA Replication in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/14-5-dna-replication-in-eukaryotes#term-00001)
+**telophase** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00018)
+**template strand** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00003)
+**tertiary structure** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00011)
+**test cross** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00008)
+**tetrads** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00012)
+**theory** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00007)
+**Thermodynamics** [6.3 The Laws of Thermodynamics](https://openstax.org/books/biology-ap-courses/pages/6-3-the-laws-of-thermodynamics#term-00001)
+**thylakoid lumen** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00009)
+**thylakoids** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00007)
+**Ti plasmids** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00027)
+**tight junction** [4.6 Connections between Cells and Cellular Activities](https://openstax.org/books/biology-ap-courses/pages/4-6-connections-between-cells-and-cellular-activities#term-00003)
+**tissues** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00009)
+**to construct a cladogram** [20 Science Practice Challenge Questions](https://openstax.org/books/biology-ap-courses/pages/20-science-practice-challenge-questions#term-00006)
+**Tonicity** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00013)
+**Topoisomerase** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00010)
+**trait** [12.1 Mendel’s Experiments and the Laws of Probability](https://openstax.org/books/biology-ap-courses/pages/12-1-mendels-experiments-and-the-laws-of-probability#term-00009)
+**trans** [4.4 The Endomembrane System and Proteins](https://openstax.org/books/biology-ap-courses/pages/4-4-the-endomembrane-system-and-proteins#term-00001)
+**trans fat** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00006)
+**transcription** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00012)
+**transcription bubble.** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00002)
+**transcription factor binding site** [16.4 Eukaryotic Transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-4-eukaryotic-transcriptional-gene-regulation#term-00002)
+**transcription factors** [16.3 Eukaryotic Epigenetic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-3-eukaryotic-epigenetic-gene-regulation#term-00001)
+**transcriptional start site** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00006)
+**Transfer RNA (tRNA)** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00011)
+**transformation** [14.1 Historical Basis of Modern Understanding](https://openstax.org/books/biology-ap-courses/pages/14-1-historical-basis-of-modern-understanding#term-00001)
+**transgenic** [17.1 Biotechnology](https://openstax.org/books/biology-ap-courses/pages/17-1-biotechnology#term-00022)
+**transition state** [6.2 Potential, Kinetic, Free, and Activation Energy](https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy#term-00009)
+**Transition substitution** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00009)
+**translation** [3.5 Nucleic Acids](https://openstax.org/books/biology-ap-courses/pages/3-5-nucleic-acids#term-00013)
+**translocation** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00015)
+**translocations** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00004)
+**transport proteins** [5.2 Passive Transport](https://openstax.org/books/biology-ap-courses/pages/5-2-passive-transport#term-00007)
+**transporters** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00006)
+**Transversion substitution** [14.6 DNA Repair](https://openstax.org/books/biology-ap-courses/pages/14-6-dna-repair#term-00010)
+**triacylglycerols** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00002)
+**Triassic–Jurassic extinction** [38.1 The Biodiversity Crisis](https://openstax.org/books/biology-ap-courses/pages/38-1-the-biodiversity-crisis#term-00001)
+**triglycerides** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00003)
+**trisomy** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00009)
+**Tryptophan** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00004)
+**tryptophan (*trp*) operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00005)
+**Tumor suppressor genes** [10.4 Cancer and the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-4-cancer-and-the-cell-cycle#term-00003)
+**U**
+**ubiquinone** [7.4 Oxidative Phosphorylation](https://openstax.org/books/biology-ap-courses/pages/7-4-oxidative-phosphorylation#term-00002)
+**unified cell theory** [4.1 Studying Cells](https://openstax.org/books/biology-ap-courses/pages/4-1-studying-cells#term-00004)
+**uniporter** [5.3 Active Transport](https://openstax.org/books/biology-ap-courses/pages/5-3-active-transport#term-00007)
+**unsaturated** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00005)
+**untranslated regions** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00004)
+**upstream** [15.2 Prokaryotic Transcription](https://openstax.org/books/biology-ap-courses/pages/15-2-prokaryotic-transcription#term-00006)
+**V**
+**vacuoles** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00016)
+**valence shell** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00017)
+**van der Waals interactions** [2.1 Atoms, Isotopes, Ions, and Molecules: The Building Blocks](https://openstax.org/books/biology-ap-courses/pages/2-1-atoms-isotopes-ions-and-molecules-the-building-blocks#term-00043)
+**variable** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00014)
+**variants** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00013)
+**Vesicles** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00015)
+**W**
+**wavelength** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00001)
+**Wax** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00008)
+**Whole-genome sequencing** [17.3 Whole-Genome Sequencing](https://openstax.org/books/biology-ap-courses/pages/17-3-whole-genome-sequencing#term-00001)
+**wild type** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00012)
+**X**
+**X inactivation** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00011)
+**X-linked** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00015)
+**Z**
+**Zoology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00024)
\ No newline at end of file
diff --git a/samples/personalized_learning/public/favicon.svg b/samples/personalized_learning/public/favicon.svg
new file mode 100644
index 00000000..dde03b96
--- /dev/null
+++ b/samples/personalized_learning/public/favicon.svg
@@ -0,0 +1,22 @@
+
diff --git a/samples/personalized_learning/public/maria-context.html b/samples/personalized_learning/public/maria-context.html
new file mode 100644
index 00000000..3cc88546
--- /dev/null
+++ b/samples/personalized_learning/public/maria-context.html
@@ -0,0 +1,467 @@
+
+
+
+
+
+ Learner Profile: Maria Thompson
+
+
+
+
+
+
+
+
+ psychology
+
+
+
Learner Context Profile
+
session_id: mcat-prep-2025-maria
+
+
+
+
+
+
MT
+
+
Maria Thompson
+
Pre-Med Student, Cymbal University
+
+ MCAT Prep
+ AP Biology: 92%
+ AP Chemistry: Struggling
+
+
+
+
+
+
Identified Misconception
+
+
+
+ warning
+
+
ATP & Bond Energy
+
+
+
+ Energy is stored in ATP bonds like a battery. When we break the phosphate bonds, that stored energy is released.
+
+
+ check_circle
+ Correct understanding: ATP releases energy because the products (ADP + Pi) are more thermodynamically stable, not because energy was "stored" in the bonds.
+
+
+
+
+
+
+
Learning Preferences
+
+
+
Learning Style
+
Visual-Kinesthetic
+
+
+
Preferred Analogies
+
Sports & Gym
+
+
+
Study Mode
+
Active recall, spaced repetition
+
+
+
Best Time
+
Morning sessions
+
+
+
+
+
+
Subject Proficiency
+
+
+ AP Biology
+
+
+
+ 92%
+
+
+ AP Chemistry
+
+
+
+ 68%
+
+
+ AP Physics
+
+
+
+ 85%
+
+
+
+
+
+
+
+
+
diff --git a/samples/personalized_learning/quickstart_setup.sh b/samples/personalized_learning/quickstart_setup.sh
new file mode 100755
index 00000000..b4cf51d6
--- /dev/null
+++ b/samples/personalized_learning/quickstart_setup.sh
@@ -0,0 +1,161 @@
+#!/bin/bash
+# Quickstart Setup Script for Personalized Learning Demo
+# This script handles all environment setup silently.
+# Run from: samples/personalized_learning/
+
+set -e # Exit on error
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Parse arguments
+PROJECT_ID=""
+SKIP_NPM=false
+SKIP_PIP=false
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --project)
+ PROJECT_ID="$2"
+ shift 2
+ ;;
+ --skip-npm)
+ SKIP_NPM=true
+ shift
+ ;;
+ --skip-pip)
+ SKIP_PIP=true
+ shift
+ ;;
+ -h|--help)
+ echo "Usage: ./quickstart_setup.sh --project YOUR_PROJECT_ID [options]"
+ echo ""
+ echo "Options:"
+ echo " --project ID Google Cloud project ID (required)"
+ echo " --skip-npm Skip npm install steps"
+ echo " --skip-pip Skip pip install steps"
+ echo " -h, --help Show this help message"
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+if [ -z "$PROJECT_ID" ]; then
+ echo -e "${RED}Error: --project is required${NC}"
+ echo "Usage: ./quickstart_setup.sh --project YOUR_PROJECT_ID"
+ exit 1
+fi
+
+echo "============================================================"
+echo "Personalized Learning Demo - Setup"
+echo "============================================================"
+echo "Project: $PROJECT_ID"
+echo ""
+
+# Step 1: Python Virtual Environment
+echo -e "${YELLOW}[1/6]${NC} Setting up Python environment..."
+if [ ! -d ".venv" ]; then
+ python3 -m venv .venv
+ echo " Created virtual environment"
+else
+ echo " Virtual environment already exists"
+fi
+
+# Activate venv for this script
+source .venv/bin/activate
+
+# Step 2: Install Python dependencies
+if [ "$SKIP_PIP" = false ]; then
+ echo -e "${YELLOW}[2/6]${NC} Installing Python dependencies..."
+ pip install -q \
+ --index-url https://pypi.org/simple/ \
+ --trusted-host pypi.org \
+ --trusted-host files.pythonhosted.org \
+ "google-adk>=0.3.0" \
+ "google-genai>=1.0.0" \
+ "google-cloud-storage>=2.10.0" \
+ "python-dotenv>=1.0.0" \
+ "litellm>=1.0.0" \
+ "vertexai" 2>/dev/null
+ echo " Python dependencies installed"
+else
+ echo -e "${YELLOW}[2/6]${NC} Skipping Python dependencies (--skip-pip)"
+fi
+
+# Step 3: Install Node.js dependencies
+if [ "$SKIP_NPM" = false ]; then
+ echo -e "${YELLOW}[3/6]${NC} Installing Node.js dependencies..."
+
+ # Build A2UI renderer
+ (cd ../../renderers/lit && npm install --registry https://registry.npmjs.org/ --silent 2>/dev/null && npm run build --silent 2>/dev/null)
+ echo " A2UI renderer built"
+
+ # Install demo dependencies
+ npm install --registry https://registry.npmjs.org/ --silent 2>/dev/null
+ echo " Demo dependencies installed"
+else
+ echo -e "${YELLOW}[3/6]${NC} Skipping Node.js dependencies (--skip-npm)"
+fi
+
+# Step 4: Enable GCP APIs
+echo -e "${YELLOW}[4/6]${NC} Enabling GCP APIs..."
+gcloud services enable aiplatform.googleapis.com --project="$PROJECT_ID" --quiet 2>/dev/null
+gcloud services enable cloudbuild.googleapis.com --project="$PROJECT_ID" --quiet 2>/dev/null
+gcloud services enable storage.googleapis.com --project="$PROJECT_ID" --quiet 2>/dev/null
+gcloud services enable cloudresourcemanager.googleapis.com --project="$PROJECT_ID" --quiet 2>/dev/null
+echo " APIs enabled"
+
+# Step 5: Create GCS buckets
+echo -e "${YELLOW}[5/6]${NC} Creating GCS buckets..."
+LOCATION="us-central1"
+
+# Staging bucket
+gsutil mb -l "$LOCATION" "gs://${PROJECT_ID}_cloudbuild" 2>/dev/null || true
+echo " Staging bucket: gs://${PROJECT_ID}_cloudbuild"
+
+# Learner context bucket
+CONTEXT_BUCKET="${PROJECT_ID}-learner-context"
+gsutil mb -l "$LOCATION" "gs://${CONTEXT_BUCKET}" 2>/dev/null || true
+echo " Context bucket: gs://${CONTEXT_BUCKET}"
+
+# OpenStax content bucket
+OPENSTAX_BUCKET="${PROJECT_ID}-openstax"
+gsutil mb -l "$LOCATION" "gs://${OPENSTAX_BUCKET}" 2>/dev/null || true
+echo " OpenStax bucket: gs://${OPENSTAX_BUCKET}"
+
+# Step 6: Upload learner context
+echo -e "${YELLOW}[6/6]${NC} Uploading learner context files..."
+# Note: Using -o flag to disable multiprocessing on macOS to avoid Python multiprocessing issues
+gsutil -o "GSUtil:parallel_process_count=1" -m cp -q learner_context/*.txt "gs://${CONTEXT_BUCKET}/learner_context/" 2>/dev/null || \
+ gsutil cp -q learner_context/*.txt "gs://${CONTEXT_BUCKET}/learner_context/" 2>/dev/null || true
+echo " Learner context uploaded to gs://${CONTEXT_BUCKET}/learner_context/"
+
+# Get project number for .env
+PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)" 2>/dev/null)
+
+echo ""
+echo -e "${GREEN}============================================================${NC}"
+echo -e "${GREEN}Setup Complete!${NC}"
+echo -e "${GREEN}============================================================${NC}"
+echo ""
+echo "Project ID: $PROJECT_ID"
+echo "Project Number: $PROJECT_NUMBER"
+echo "Context Bucket: gs://${CONTEXT_BUCKET}/learner_context/"
+echo ""
+echo "Next steps:"
+echo " 1. Run the 'Deploy Agent' cell in the notebook"
+echo " 2. Copy the Resource ID and paste it in the configuration cell"
+echo " 3. Run 'npm run dev' to start the demo"
+echo ""
+
+# Output values for notebook to capture
+echo "SETUP_PROJECT_ID=$PROJECT_ID"
+echo "SETUP_PROJECT_NUMBER=$PROJECT_NUMBER"
+echo "SETUP_CONTEXT_BUCKET=$CONTEXT_BUCKET"
diff --git a/samples/personalized_learning/src/a2a-client.ts b/samples/personalized_learning/src/a2a-client.ts
new file mode 100644
index 00000000..ab2712c7
--- /dev/null
+++ b/samples/personalized_learning/src/a2a-client.ts
@@ -0,0 +1,566 @@
+/*
+ * A2A Client
+ *
+ * Client for communicating with the A2A agent that generates A2UI content.
+ * This client talks to the remote agent (locally or on Agent Engine).
+ */
+
+import { getIdToken } from "./firebase-auth";
+
+// Helper to get auth headers for API requests
+async function getAuthHeaders(): Promise> {
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ const token = await getIdToken();
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ return headers;
+}
+
+export interface SourceInfo {
+ url: string;
+ title: string;
+ provider: string;
+}
+
+export interface A2UIResponse {
+ format: string;
+ a2ui: unknown[];
+ surfaceId: string;
+ source?: SourceInfo;
+ error?: string;
+}
+
+export class A2AClient {
+ private baseUrl: string;
+
+ constructor(baseUrl: string = "/a2ui-agent") {
+ this.baseUrl = baseUrl;
+ }
+
+ /**
+ * Generate A2UI content from the agent.
+ */
+ async generateContent(
+ format: string,
+ context: string = ""
+ ): Promise {
+ console.log(`[A2AClient] Requesting ${format} content`);
+
+ // For audio/video, always use local fallback content.
+ // The deployed agent returns GCS URLs which won't work locally,
+ // and we only have one pre-built podcast/video anyway.
+ const lowerFormat = format.toLowerCase();
+ if (lowerFormat === "podcast" || lowerFormat === "audio" || lowerFormat === "video") {
+ console.log(`[A2AClient] Using local fallback for ${format} (pre-built content)`);
+ return this.getFallbackContent(format);
+ }
+
+ try {
+ const response = await fetch(`${this.baseUrl}/a2a/query`, {
+ method: "POST",
+ headers: await getAuthHeaders(),
+ body: JSON.stringify({
+ message: context ? `${format}:${context}` : format,
+ session_id: this.getSessionId(),
+ extensions: ["https://a2ui.org/a2a-extension/a2ui/v0.8"],
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Agent error: ${response.status}`);
+ }
+
+ const data = await response.json();
+ console.log(`[A2AClient] Received response:`, data);
+
+ // Check if the response has valid A2UI content
+ // If empty, has an error, or agent couldn't fulfill request, use fallback
+ if (
+ !data.a2ui ||
+ data.a2ui.length === 0 ||
+ data.error ||
+ data.rawText?.includes("cannot fulfill") ||
+ data.rawText?.includes("do not have the functionality")
+ ) {
+ console.log(`[A2AClient] Agent returned empty/error, using fallback for ${format}`);
+ return this.getFallbackContent(format);
+ }
+
+ // Special case: if we requested a quiz but agent returned flashcards,
+ // use our quiz fallback instead (agent doesn't know about QuizCard)
+ if (format.toLowerCase() === "quiz") {
+ const a2uiStr = JSON.stringify(data.a2ui);
+ if (a2uiStr.includes("Flashcard") && !a2uiStr.includes("QuizCard")) {
+ console.log(`[A2AClient] Agent returned Flashcards for quiz request, using QuizCard fallback`);
+ return this.getFallbackContent(format);
+ }
+ }
+
+ return data as A2UIResponse;
+ } catch (error) {
+ console.error("[A2AClient] Error calling agent:", error);
+
+ // Return fallback content for demo purposes
+ return this.getFallbackContent(format);
+ }
+ }
+
+ /**
+ * Stream A2UI content from the agent (for long-running generation).
+ */
+ async *streamContent(
+ format: string,
+ context: string = ""
+ ): AsyncGenerator<{ status: string; data?: A2UIResponse }> {
+ console.log(`[A2AClient] Streaming ${format} content`);
+
+ try {
+ const response = await fetch(`${this.baseUrl}/a2a/stream`, {
+ method: "POST",
+ headers: await getAuthHeaders(),
+ body: JSON.stringify({
+ message: context ? `${format}:${context}` : format,
+ session_id: this.getSessionId(),
+ extensions: ["https://a2ui.org/a2a-extension/a2ui/v0.8"],
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Agent error: ${response.status}`);
+ }
+
+ const reader = response.body?.getReader();
+ if (!reader) {
+ throw new Error("No response body");
+ }
+
+ const decoder = new TextDecoder();
+ let buffer = "";
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+
+ // Parse SSE events
+ const lines = buffer.split("\n\n");
+ buffer = lines.pop() || "";
+
+ for (const line of lines) {
+ if (line.startsWith("data: ")) {
+ const data = JSON.parse(line.slice(6));
+ if (data.is_task_complete) {
+ yield { status: "complete", data: data.content };
+ } else {
+ yield { status: "processing" };
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error("[A2AClient] Stream error:", error);
+ yield { status: "complete", data: this.getFallbackContent(format) };
+ }
+ }
+
+ /**
+ * Get or create a session ID.
+ */
+ private getSessionId(): string {
+ let sessionId = sessionStorage.getItem("a2ui_session_id");
+ if (!sessionId) {
+ sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ sessionStorage.setItem("a2ui_session_id", sessionId);
+ }
+ return sessionId;
+ }
+
+ /**
+ * Get fallback content for demo purposes when agent is unavailable.
+ */
+ private getFallbackContent(format: string): A2UIResponse {
+ const surfaceId = "learningContent";
+
+ switch (format.toLowerCase()) {
+ case "flashcards":
+ return {
+ format: "flashcards",
+ surfaceId,
+ a2ui: [
+ { beginRendering: { surfaceId, root: "mainColumn" } },
+ {
+ surfaceUpdate: {
+ surfaceId,
+ components: [
+ {
+ id: "mainColumn",
+ component: {
+ Column: {
+ children: { explicitList: ["headerText", "flashcardRow"] },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "headerText",
+ component: {
+ Text: {
+ text: { literalString: "Study Flashcards: ATP & Bond Energy" },
+ usageHint: "h3",
+ },
+ },
+ },
+ {
+ id: "flashcardRow",
+ component: {
+ Row: {
+ children: { explicitList: ["card1", "card2", "card3"] },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "card1",
+ component: {
+ Flashcard: {
+ front: { literalString: "Why does ATP hydrolysis release energy?" },
+ back: {
+ literalString:
+ "ATP hydrolysis releases energy because the products (ADP + Pi) are MORE STABLE than ATP. The phosphate groups in ATP repel each other due to negative charges. When the terminal phosphate is removed, this electrostatic strain is relieved, and the products achieve better resonance stabilization. It's like releasing a compressed spring - the energy comes from moving to a lower-energy state.",
+ },
+ category: { literalString: "Biochemistry" },
+ },
+ },
+ },
+ {
+ id: "card2",
+ component: {
+ Flashcard: {
+ front: { literalString: "Does breaking a chemical bond release energy?" },
+ back: {
+ literalString:
+ "NO - this is a common MCAT trap! Breaking ANY bond REQUIRES energy input (it's endothermic). Energy is only released when NEW bonds FORM. In ATP hydrolysis, the energy released comes from forming more stable bonds in the products, not from 'breaking' the phosphate bond.",
+ },
+ category: { literalString: "Common Trap" },
+ },
+ },
+ },
+ {
+ id: "card3",
+ component: {
+ Flashcard: {
+ front: {
+ literalString:
+ "What's wrong with saying 'ATP stores energy in its bonds'?",
+ },
+ back: {
+ literalString:
+ "Bonds don't 'store' energy like batteries. Think of it like holding a plank position at the gym - you're not storing energy in your muscles, you're in a high-energy unstable state. When you release to rest (like ATP → ADP + Pi), you move to a more stable, lower-energy state. The 'energy release' is really about the stability difference between reactants and products.",
+ },
+ category: { literalString: "MCAT Concept" },
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ };
+
+ case "podcast":
+ case "audio":
+ return {
+ format: "audio",
+ surfaceId,
+ a2ui: [
+ { beginRendering: { surfaceId, root: "audioCard" } },
+ {
+ surfaceUpdate: {
+ surfaceId,
+ components: [
+ {
+ id: "audioCard",
+ component: { Card: { child: "audioContent" } },
+ },
+ {
+ id: "audioContent",
+ component: {
+ Column: {
+ children: {
+ explicitList: ["audioHeader", "audioPlayer", "audioDescription"],
+ },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "audioHeader",
+ component: {
+ Row: {
+ children: { explicitList: ["audioIcon", "audioTitle"] },
+ distribution: "start",
+ alignment: "center",
+ },
+ },
+ },
+ {
+ id: "audioIcon",
+ component: {
+ Icon: { name: { literalString: "podcasts" } },
+ },
+ },
+ {
+ id: "audioTitle",
+ component: {
+ Text: {
+ text: {
+ literalString: "ATP & Chemical Stability: Correcting the Misconception",
+ },
+ usageHint: "h3",
+ },
+ },
+ },
+ {
+ id: "audioPlayer",
+ component: {
+ AudioPlayer: {
+ url: { literalString: "/assets/podcast.m4a" },
+ audioTitle: { literalString: "Understanding ATP Energy Release" },
+ audioDescription: { literalString: "A personalized podcast about ATP and chemical stability" },
+ },
+ },
+ },
+ {
+ id: "audioDescription",
+ component: {
+ Text: {
+ text: {
+ literalString:
+ "This personalized podcast uses gym analogies to explain why 'energy stored in bonds' is a misconception. Perfect for your MCAT prep!",
+ },
+ usageHint: "body",
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ };
+
+ case "video":
+ return {
+ format: "video",
+ surfaceId,
+ a2ui: [
+ { beginRendering: { surfaceId, root: "videoCard" } },
+ {
+ surfaceUpdate: {
+ surfaceId,
+ components: [
+ {
+ id: "videoCard",
+ component: { Card: { child: "videoContent" } },
+ },
+ {
+ id: "videoContent",
+ component: {
+ Column: {
+ children: {
+ explicitList: ["videoTitle", "videoPlayer", "videoDescription"],
+ },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "videoTitle",
+ component: {
+ Text: {
+ text: { literalString: "Visual Guide: ATP Energy & Stability" },
+ usageHint: "h3",
+ },
+ },
+ },
+ {
+ id: "videoPlayer",
+ component: {
+ Video: {
+ url: { literalString: "/assets/video.mp4" },
+ },
+ },
+ },
+ {
+ id: "videoDescription",
+ component: {
+ Text: {
+ text: {
+ literalString:
+ "Watch the compressed spring analogy in action to understand why ATP releases energy through stability differences.",
+ },
+ usageHint: "body",
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ };
+
+ case "quiz":
+ return {
+ format: "quiz",
+ surfaceId,
+ a2ui: [
+ { beginRendering: { surfaceId, root: "mainColumn" } },
+ {
+ surfaceUpdate: {
+ surfaceId,
+ components: [
+ {
+ id: "mainColumn",
+ component: {
+ Column: {
+ children: { explicitList: ["headerText", "quizRow"] },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "headerText",
+ component: {
+ Text: {
+ text: { literalString: "Quick Quiz: ATP & Bond Energy" },
+ usageHint: "h3",
+ },
+ },
+ },
+ {
+ id: "quizRow",
+ component: {
+ Row: {
+ children: { explicitList: ["quiz1", "quiz2"] },
+ distribution: "start",
+ alignment: "stretch",
+ },
+ },
+ },
+ {
+ id: "quiz1",
+ component: {
+ QuizCard: {
+ question: {
+ literalString:
+ "What happens to the energy in bonds when ATP is hydrolyzed?",
+ },
+ options: [
+ {
+ label: {
+ literalString:
+ "Energy stored in the phosphate bond is released",
+ },
+ value: "a",
+ isCorrect: false,
+ },
+ {
+ label: {
+ literalString:
+ "Energy is released because products are more stable",
+ },
+ value: "b",
+ isCorrect: true,
+ },
+ {
+ label: {
+ literalString:
+ "The bond breaking itself releases energy",
+ },
+ value: "c",
+ isCorrect: false,
+ },
+ {
+ label: {
+ literalString: "ATP's special bonds contain more electrons",
+ },
+ value: "d",
+ isCorrect: false,
+ },
+ ],
+ explanation: {
+ literalString:
+ "ATP hydrolysis releases energy because the products (ADP + Pi) are MORE STABLE than ATP. The phosphate groups in ATP repel each other, creating strain. When this bond is broken, the products achieve better resonance stabilization - like releasing a compressed spring.",
+ },
+ category: { literalString: "Thermodynamics" },
+ },
+ },
+ },
+ {
+ id: "quiz2",
+ component: {
+ QuizCard: {
+ question: {
+ literalString:
+ "Breaking a chemical bond requires or releases energy?",
+ },
+ options: [
+ {
+ label: { literalString: "Always releases energy" },
+ value: "a",
+ isCorrect: false,
+ },
+ {
+ label: { literalString: "Always requires energy input" },
+ value: "b",
+ isCorrect: true,
+ },
+ {
+ label: {
+ literalString: "Depends on whether it's a high-energy bond",
+ },
+ value: "c",
+ isCorrect: false,
+ },
+ {
+ label: { literalString: "Neither - bonds are energy neutral" },
+ value: "d",
+ isCorrect: false,
+ },
+ ],
+ explanation: {
+ literalString:
+ "Breaking ANY bond REQUIRES energy (it's endothermic). This is a common MCAT trap! Energy is only released when NEW bonds FORM. Think of it like pulling apart magnets - you have to put in effort to separate them.",
+ },
+ category: { literalString: "Bond Energy" },
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ // Note: This is fallback content shown when Agent Engine fails.
+ // The actual topic-specific source comes from the Agent Engine response.
+ };
+
+ default:
+ return {
+ format: "error",
+ surfaceId,
+ a2ui: [],
+ error: `Unknown format: ${format}`,
+ };
+ }
+ }
+}
diff --git a/samples/personalized_learning/src/a2ui-renderer.ts b/samples/personalized_learning/src/a2ui-renderer.ts
new file mode 100644
index 00000000..8b2d5d5a
--- /dev/null
+++ b/samples/personalized_learning/src/a2ui-renderer.ts
@@ -0,0 +1,149 @@
+/*
+ Copyright 2025 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * A2UI Renderer
+ *
+ * Renders A2UI JSON payloads into the chat interface using the A2UI web components.
+ * Uses the signal-based model processor for proper reactivity.
+ */
+
+import { v0_8 } from "@a2ui/web-lib";
+import type { SourceInfo } from "./a2a-client";
+// Import the theme provider to register the custom element
+import "./theme-provider.js";
+
+// Type alias for the processor - use the actual exported class name
+type A2UIModelProcessorInstance = InstanceType;
+
+// Extended element type for the theme provider wrapper
+interface A2UIThemeProviderElement extends HTMLElement {
+ surfaceId: string;
+ surface: v0_8.Types.Surface;
+ processor: A2UIModelProcessorInstance;
+}
+
+export class A2UIRenderer {
+ private processors: Map = new Map();
+
+ /**
+ * Render A2UI JSON into a message element.
+ */
+ render(messageElement: HTMLDivElement, a2uiMessages: unknown[], source?: SourceInfo): void {
+ if (!a2uiMessages || a2uiMessages.length === 0) {
+ return;
+ }
+
+ // Create a container for the A2UI content
+ const container = document.createElement("div");
+ container.className = "a2ui-container";
+
+ // Find the message content element
+ const contentEl = messageElement.querySelector(".message-content");
+ if (!contentEl) {
+ console.error("[A2UIRenderer] Message content element not found");
+ return;
+ }
+
+ contentEl.appendChild(container);
+
+ // Create a model processor for this render
+ const processor = v0_8.Data.createSignalA2uiMessageProcessor();
+
+ // Process all A2UI messages to build the model
+ try {
+ processor.processMessages(a2uiMessages as v0_8.Types.ServerToClientMessage[]);
+ } catch (error) {
+ console.error("[A2UIRenderer] Error processing messages:", error);
+ }
+
+ // Render the surfaces
+ const surfaces = processor.getSurfaces();
+ for (const [surfaceId, surface] of surfaces.entries()) {
+ this.renderSurface(container, surfaceId, surface, processor);
+ this.processors.set(surfaceId, processor);
+ }
+
+ // Add source attribution if available
+ if (source && (source.url || source.provider)) {
+ this.renderSourceAttribution(container, source);
+ }
+ }
+
+ /**
+ * Render source attribution below the A2UI content.
+ */
+ private renderSourceAttribution(container: HTMLElement, source: SourceInfo): void {
+ const attribution = document.createElement("div");
+ attribution.className = "source-attribution";
+
+ // If we have a URL, make it a link; otherwise just show the text
+ if (source.url) {
+ attribution.innerHTML = `
+ Source:
+
+ ${source.title || source.provider}
+
+ — ${source.provider}
+ `;
+ } else {
+ attribution.innerHTML = `
+ Source:
+ ${source.title || source.provider}
+ — ${source.provider}
+ `;
+ }
+ container.appendChild(attribution);
+ }
+
+ /**
+ * Render a surface using the theme provider wrapper.
+ * The theme provider supplies the theme context required by A2UI components.
+ */
+ private renderSurface(
+ container: HTMLElement,
+ surfaceId: string,
+ surface: v0_8.Types.Surface,
+ processor: A2UIModelProcessorInstance
+ ): void {
+ // Create the theme provider wrapper which contains the a2ui-surface
+ const providerEl = document.createElement("a2ui-theme-provider") as A2UIThemeProviderElement;
+
+ providerEl.surfaceId = surfaceId;
+ providerEl.surface = surface;
+ providerEl.processor = processor;
+
+ // Add some styling for the container
+ providerEl.style.display = "block";
+ providerEl.style.marginTop = "16px";
+
+ container.appendChild(providerEl);
+ }
+
+ /**
+ * Get a processor for a surface ID.
+ */
+ getProcessor(surfaceId: string): A2UIModelProcessorInstance | undefined {
+ return this.processors.get(surfaceId);
+ }
+
+ /**
+ * Clear all rendered surfaces.
+ */
+ clear(): void {
+ this.processors.clear();
+ }
+}
diff --git a/samples/personalized_learning/src/chat-orchestrator.ts b/samples/personalized_learning/src/chat-orchestrator.ts
new file mode 100644
index 00000000..9cab982c
--- /dev/null
+++ b/samples/personalized_learning/src/chat-orchestrator.ts
@@ -0,0 +1,515 @@
+/*
+ * Chat Orchestrator
+ *
+ * Orchestrates the chat flow between the user, Gemini, and the A2A agent.
+ * Determines when to generate A2UI content and manages async artifact generation.
+ */
+
+import { A2UIRenderer } from "./a2ui-renderer";
+import { A2AClient } from "./a2a-client";
+import { getIdToken } from "./firebase-auth";
+
+// Helper to get auth headers for API requests
+async function getAuthHeaders(): Promise> {
+ const headers: Record = {
+ "Content-Type": "application/json",
+ };
+
+ const token = await getIdToken();
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ return headers;
+}
+
+// Types for conversation history
+interface Message {
+ role: "user" | "assistant";
+ content: string;
+ a2ui?: unknown[];
+}
+
+// Intent types the orchestrator can detect
+type Intent =
+ | "flashcards"
+ | "podcast"
+ | "audio"
+ | "video"
+ | "quiz"
+ | "general"
+ | "greeting";
+
+export class ChatOrchestrator {
+ private conversationHistory: Message[] = [];
+ private renderer: A2UIRenderer;
+ private a2aClient: A2AClient;
+
+ // System prompt for conversational responses.
+ // Note: Maria's profile also appears in agent/agent.py (for content generation) and
+ // learner_context/ files (for dynamic personalization). This duplication is intentional—
+ // the frontend and agent operate independently and both need learner context.
+ private systemPrompt = `You are a personalized MCAT tutor helping Maria, a pre-med student at Cymbal University.
+You have access to her learning profile and know she struggles with understanding ATP and bond energy concepts.
+
+CRITICAL RULE - KEEP RESPONSES SHORT:
+When Maria asks for flashcards, podcasts, or videos, respond with ONLY 1-2 sentences.
+The actual content (flashcards, audio player, video player) will be rendered separately by the UI system.
+DO NOT write out flashcard content, podcast transcripts, or video descriptions. Just briefly acknowledge the request.
+
+Example good responses:
+- "Here are some flashcards to help you review!" (flashcards render below)
+- "Great idea! Here's a podcast that explains this concept." (audio player renders below)
+- "Check out this video explanation!" (video player renders below)
+
+Example BAD responses (DO NOT DO THIS):
+- Writing out "Front: ... Back: ..." for flashcards
+- Writing a podcast script or transcript
+- Describing video content in detail
+
+When Maria asks a general question (not requesting materials):
+1. ANSWER it directly and helpfully first
+2. Use analogies she relates to (sports, gym, fitness)
+3. Only AFTER explaining, you may OFFER additional resources if relevant
+
+Key facts about Maria:
+- Visual-kinesthetic learner who responds well to sports/gym analogies
+- Common misconception: thinking "energy is stored in ATP bonds"
+- Correct understanding: ATP releases energy because products (ADP + Pi) are MORE STABLE
+
+CONTENT SOURCE ATTRIBUTION:
+If asked about sources, say materials come from OpenStax Biology for AP Courses, a free peer-reviewed college textbook.
+
+Keep ALL responses:
+- Short and conversational (1-2 sentences for material requests, 2-3 for explanations)
+- Friendly and encouraging
+- NEVER include the actual content of flashcards, podcasts, or videos in your text`;
+
+ constructor(renderer: A2UIRenderer) {
+ this.renderer = renderer;
+ this.a2aClient = new A2AClient();
+ }
+
+ /**
+ * Process a user message and generate a response.
+ * Uses combined intent+response endpoint to reduce latency.
+ */
+ async processMessage(
+ userMessage: string,
+ messageElement: HTMLDivElement
+ ): Promise {
+ // Add to history
+ this.conversationHistory.push({ role: "user", content: userMessage });
+
+ // Try combined endpoint first (single LLM call for intent + response + keywords)
+ let intent: Intent;
+ let responseText: string;
+ let keywords: string | undefined;
+
+ console.log("========================================");
+ console.log("[Orchestrator] PROCESSING USER MESSAGE");
+ console.log(`[Orchestrator] User said: "${userMessage}"`);
+ console.log("========================================");
+
+ try {
+ const combinedResult = await this.getCombinedIntentAndResponse(userMessage);
+ intent = combinedResult.intent as Intent;
+ responseText = combinedResult.text;
+ keywords = combinedResult.keywords;
+ console.log("========================================");
+ console.log("[Orchestrator] GEMINI RESPONSE RECEIVED");
+ console.log(`[Orchestrator] Detected intent: ${intent}`);
+ console.log(`[Orchestrator] Keywords: ${keywords || "(none)"}`);
+ console.log(`[Orchestrator] Response text: ${responseText.substring(0, 100)}...`);
+ console.log("========================================");
+ } catch (error) {
+ console.warn("[Orchestrator] Combined endpoint failed, falling back to separate calls");
+ // Fallback to separate calls if combined endpoint fails
+ intent = await this.detectIntentWithLLM(userMessage);
+ console.log(`[Orchestrator] Fallback detected intent: ${intent}`);
+ const response = await this.generateResponse(userMessage, intent);
+ responseText = response.text;
+ }
+
+ // Update the message element with the response text
+ this.setMessageText(messageElement, responseText);
+
+ // If we need A2UI content, fetch and render it
+ if (intent !== "general" && intent !== "greeting") {
+ // Add processing placeholder
+ const placeholder = this.addProcessingPlaceholder(
+ messageElement,
+ intent
+ );
+
+ try {
+ // Fetch A2UI content from the agent
+ // Use LLM-generated keywords if available (handles typos, adds related terms)
+ // Fall back to user message + response context if keywords not available
+ const topicContext = keywords
+ ? keywords // Keywords are already corrected and expanded by Gemini
+ : `User request: ${userMessage}\nAssistant context: ${responseText}`;
+
+ console.log("========================================");
+ console.log("[Orchestrator] CALLING AGENT ENGINE FOR A2UI CONTENT");
+ console.log(`[Orchestrator] Intent (format): ${intent}`);
+ console.log(`[Orchestrator] Topic context being sent:`);
+ console.log(`[Orchestrator] "${topicContext}"`);
+ console.log(`[Orchestrator] Keywords available: ${keywords ? "YES" : "NO (using fallback)"}`);
+ console.log("========================================");
+
+ const a2uiResult = await this.a2aClient.generateContent(
+ intent,
+ topicContext
+ );
+
+ console.log("========================================");
+ console.log("[Orchestrator] AGENT ENGINE RESPONSE RECEIVED");
+ console.log(`[Orchestrator] Format: ${a2uiResult?.format}`);
+ console.log(`[Orchestrator] Source: ${JSON.stringify(a2uiResult?.source)}`);
+ console.log(`[Orchestrator] A2UI messages: ${a2uiResult?.a2ui?.length || 0}`);
+ console.log("========================================");
+
+ // Remove placeholder
+ placeholder.remove();
+
+ // Render A2UI content with source attribution
+ if (a2uiResult && a2uiResult.a2ui) {
+ this.renderer.render(messageElement, a2uiResult.a2ui, a2uiResult.source);
+ this.conversationHistory[this.conversationHistory.length - 1].a2ui =
+ a2uiResult.a2ui;
+ }
+ } catch (error) {
+ console.error("[Orchestrator] Error fetching A2UI content:", error);
+ placeholder.innerHTML = `
+ error
+ Failed to load content. Please try again.
+ `;
+ }
+ }
+
+ // Add assistant response to history
+ this.conversationHistory.push({ role: "assistant", content: responseText });
+ }
+
+ /**
+ * Get combined intent and response in a single LLM call.
+ * This reduces latency by eliminating one round-trip.
+ * For content-generating intents, also returns keywords for better content retrieval.
+ */
+ private async getCombinedIntentAndResponse(message: string): Promise<{ intent: string; text: string; keywords?: string }> {
+ const recentContext = this.conversationHistory.slice(-4).map(m =>
+ `${m.role}: ${m.content}`
+ ).join("\n");
+
+ const response = await fetch("/api/chat-with-intent", {
+ method: "POST",
+ headers: await getAuthHeaders(),
+ body: JSON.stringify({
+ systemPrompt: this.systemPrompt,
+ messages: this.conversationHistory.slice(-10).map((m) => ({
+ role: m.role,
+ parts: [{ text: m.content }],
+ })),
+ userMessage: message,
+ recentContext: recentContext,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Combined API error: ${response.status}`);
+ }
+
+ return await response.json();
+ }
+
+ /**
+ * Detect the user's intent using Gemini LLM.
+ * Returns the detected intent for routing to appropriate content generation.
+ */
+ private async detectIntentWithLLM(message: string): Promise {
+ const recentContext = this.conversationHistory.slice(-4).map(m =>
+ `${m.role}: ${m.content}`
+ ).join("\n");
+
+ try {
+ const response = await fetch("/api/chat", {
+ method: "POST",
+ headers: await getAuthHeaders(),
+ body: JSON.stringify({
+ systemPrompt: `You are an intent classifier. Analyze the user's message and conversation context to determine their intent.
+
+IMPORTANT: Consider the CONVERSATION CONTEXT. If the user previously discussed flashcards/podcasts/videos and says things like "yes", "sure", "do it", "render them", "show me", "ya that works" - they are CONFIRMING a previous offer.
+
+Return ONLY ONE of these exact words (nothing else):
+- flashcards - if user wants study cards, review cards, flashcards, or is confirming a flashcard offer
+- podcast - if user wants audio content, podcast, or to listen to something
+- video - if user wants to watch something or see a video
+- quiz - if user wants to be tested or take a quiz
+- greeting - if user is just saying hello/hi
+- general - for questions, explanations, or general conversation
+
+Examples:
+- "make me some flashcards" → flashcards
+- "f'cards please" → flashcards
+- "ya that works" (after flashcard offer) → flashcards
+- "render them properly" (after flashcard discussion) → flashcards
+- "sure, show me" (after any content offer) → depends on what was offered
+- "explain ATP" → general
+- "hi there" → greeting`,
+ intentGuidance: "",
+ messages: [],
+ userMessage: `Recent conversation:\n${recentContext}\n\nCurrent message: "${message}"\n\nIntent:`,
+ }),
+ });
+
+ if (!response.ok) {
+ console.warn("[Orchestrator] Intent API failed, falling back to keyword detection");
+ return this.detectIntentKeyword(message);
+ }
+
+ const data = await response.json();
+ const intentText = (data.text || "general").toLowerCase().trim();
+
+ // Map response to valid intent
+ if (intentText.includes("flashcard")) return "flashcards";
+ if (intentText.includes("podcast") || intentText.includes("audio")) return "podcast";
+ if (intentText.includes("video")) return "video";
+ if (intentText.includes("quiz")) return "quiz";
+ if (intentText.includes("greeting")) return "greeting";
+
+ return "general";
+ } catch (error) {
+ console.error("[Orchestrator] Intent detection error:", error);
+ return this.detectIntentKeyword(message);
+ }
+ }
+
+ /**
+ * Fallback keyword-based intent detection.
+ */
+ private detectIntentKeyword(message: string): Intent {
+ const lower = message.toLowerCase();
+
+ if (lower.match(/^(hi|hello|hey|good morning|good afternoon|good evening)/i)) {
+ return "greeting";
+ }
+ if (lower.match(/flash\s*card|study\s*card|review\s*card|f'?card/i)) {
+ return "flashcards";
+ }
+ if (lower.match(/podcast|audio|listen/i)) {
+ return "podcast";
+ }
+ if (lower.match(/video|watch/i)) {
+ return "video";
+ }
+ if (lower.match(/quiz|test me/i)) {
+ return "quiz";
+ }
+ return "general";
+ }
+
+ /**
+ * Generate the main chat response using Gemini.
+ */
+ private async generateResponse(
+ userMessage: string,
+ intent: Intent
+ ): Promise<{ text: string }> {
+ // Build the conversation context
+ const messages = this.conversationHistory.slice(-10).map((m) => ({
+ role: m.role,
+ parts: [{ text: m.content }],
+ }));
+
+ // Add intent-specific guidance
+ let intentGuidance = "";
+ switch (intent) {
+ case "flashcards":
+ intentGuidance =
+ "The user wants flashcards. Respond with a SHORT (1-2 sentences) conversational acknowledgment. DO NOT include the flashcard content in your response - the flashcards will be rendered separately as interactive cards below your message. Just say something brief like 'Here are some flashcards to help you review!' or 'I've created some personalized flashcards for you.'";
+ break;
+ case "podcast":
+ case "audio":
+ intentGuidance =
+ "The user wants to listen to the podcast. Respond with a SHORT (1-2 sentences) introduction. DO NOT write out the podcast transcript or script - the audio player will be rendered separately below your message. Just say something brief like 'Here's a personalized podcast about ATP!' or 'I've got a podcast that explains this with gym analogies you'll love.'";
+ break;
+ case "video":
+ intentGuidance =
+ "The user wants to watch a video. Respond with a SHORT (1-2 sentences) introduction. DO NOT describe the video content in detail - the video player will be rendered separately below your message. Just say something brief like 'Here's a video that visualizes this concept!' or 'Check out this visual explanation.'";
+ break;
+ case "quiz":
+ intentGuidance =
+ "The user wants a quiz. Respond with a SHORT (1-2 sentences) introduction. DO NOT include the quiz questions or answers in your response - the interactive quiz cards will be rendered separately below your message. Just say something brief like 'Let's test your knowledge!' or 'Here's a quick quiz to check your understanding.'";
+ break;
+ case "greeting":
+ intentGuidance =
+ "The user is greeting you. Respond warmly in 1-2 sentences and briefly mention you can help with flashcards, podcasts, videos, and quizzes for their MCAT prep.";
+ break;
+ default:
+ intentGuidance =
+ "Respond helpfully but concisely (2-3 sentences max). If the question is about ATP, bond energy, or thermodynamics, provide a clear explanation and offer to create flashcards or play the podcast for deeper learning.";
+ }
+
+ try {
+ const response = await fetch("/api/chat", {
+ method: "POST",
+ headers: await getAuthHeaders(),
+ body: JSON.stringify({
+ systemPrompt: this.systemPrompt,
+ intentGuidance,
+ messages,
+ userMessage,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`API error: ${response.status}`);
+ }
+
+ const data = await response.json();
+ return { text: data.text || data.response || "I apologize, I couldn't generate a response." };
+ } catch (error) {
+ console.error("[Orchestrator] Error calling chat API:", error);
+
+ // Fallback responses based on intent
+ return this.getFallbackResponse(intent);
+ }
+ }
+
+ /**
+ * Get a fallback response if the API fails.
+ */
+ private getFallbackResponse(intent: Intent): { text: string } {
+ switch (intent) {
+ case "flashcards":
+ return {
+ text: "Here are some personalized flashcards to help you master these concepts!",
+ };
+ case "podcast":
+ case "audio":
+ return {
+ text: "Here's the personalized podcast I mentioned! It's designed specifically for you, Maria, using gym and fitness analogies to explain why 'energy stored in bonds' is a misconception. Perfect for listening during your workout!",
+ };
+ case "video":
+ return {
+ text: "Let me show you this visual explanation. It uses the compressed spring analogy to demonstrate how ATP releases energy through stability differences, not by 'releasing stored energy from bonds.'",
+ };
+ case "quiz":
+ return {
+ text: "Let's test your understanding! Here's a quick quiz on ATP and bond energy concepts.",
+ };
+ case "greeting":
+ return {
+ text: "Hey Maria! How's the MCAT studying going? What's on your mind today?",
+ };
+ default:
+ return {
+ text: "That's a great question! Let me help you think through this.",
+ };
+ }
+ }
+
+ /**
+ * Update the message element with text content.
+ */
+ private setMessageText(messageElement: HTMLDivElement, text: string): void {
+ const textEl = messageElement.querySelector(".message-text");
+ if (textEl) {
+ textEl.innerHTML = this.parseMarkdown(text);
+ }
+ }
+
+ /**
+ * Simple markdown parser for chat messages.
+ */
+ private parseMarkdown(text: string): string {
+ // Escape HTML first for security
+ let html = this.escapeHtml(text);
+
+ // Bold: **text** or __text__
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ html = html.replace(/__(.+?)__/g, '$1');
+
+ // Italic: *text* or _text_
+ html = html.replace(/\*([^*]+)\*/g, '$1');
+ html = html.replace(/(?$1');
+
+ // Inline code: `code`
+ html = html.replace(/`([^`]+)`/g, '$1');
+
+ // Convert bullet lists: lines starting with - or *
+ html = html.replace(/^[\-\*]\s+(.+)$/gm, '
$1
');
+ // Wrap consecutive
in
+ html = html.replace(/(
.*<\/li>\n?)+/g, '
$&
');
+
+ // Convert numbered lists: lines starting with 1. 2. etc
+ html = html.replace(/^\d+\.\s+(.+)$/gm, '
$1
');
+
+ // Convert double newlines to paragraph breaks
+ html = html.replace(/\n\n+/g, '
');
+
+ // Convert single newlines to (but not inside lists)
+ html = html.replace(/(?)\n(?!<)/g, ' ');
+
+ // Wrap in paragraph if not empty
+ if (html.trim()) {
+ html = `
${html}
`;
+ }
+
+ // Clean up empty paragraphs and fix list wrapping
+ html = html.replace(/