2026-04-25-english-speaking-dialogue-prompt-design.md 15 KB

English Speaking Dialogue System Prompt — Design

Date: 2026-04-25 Branch: feat/english-speaking Repos affected:

  • Frontend: /Users/buoy/Development/gitrepo/PPT
  • Backend: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Goal

Replace the current minimal system prompt with a structured, grade-aware template that adapts language complexity to the student's grade level. Wire the existing curriculum-selector grade dropdown + learning-goal vocabulary/sentence lists into the session-creation request so the backend can render the full prompt at session-creation time.

Non-goals

  • No grade.id → label mapping in frontend
  • No tier inference (grade1-3 / 4-6 / 初中) anywhere — the LLM reads all three tier sections in the prompt and self-selects based on the literal {年级} value
  • No new DB columns — reuse existing dialogue_session.system_prompt
  • No prompt versioning / A-B testing infrastructure
  • No alembic migration

Architecture

Frontend (PPT)                        Backend (cococlass-english-speaking-api)
─────────────────                     ──────────────────────────────────────────
SpeakingPanel.vue                     POST /session/create
  CurriculumSelector                    ↓
   selectedGrade ──┐                  CreateSessionRequest (Pydantic)
                   ↓                    ↓
   store.config.grade                 dialogue_service.create_session_only()
   store.config.learningGoals.{       ↓
     vocabulary, sentences }          render SYSTEM_PROMPT_TEMPLATE
                   ↓                    ↓
   llmService.createSession()        DialogueSession.system_prompt (Text)
   POST { topic, grade,                ↓
          vocabulary, sentences,     LLM (greeting / round)
          totalRounds, ... }           ↓
                                      reads system_prompt → adapts
                                      output to {年级} tier

The prompt template is hardcoded in the backend service. The frontend ships only data, not prompt fragments.


Section 1 — Frontend config: persist grade

src/types/englishSpeaking.ts

Add grade: string to TopicDiscussionConfig:

export interface TopicDiscussionConfig {
  topic: string
  grade: string          // NEW — grade.id from curriculum.json
  selectedRole: string
  learningGoals: LearningGoals
  practice: PracticeSettings
  evaluation: EvaluationSettings
}

src/store/speaking.ts

Add to DEFAULT_CONFIG:

const DEFAULT_CONFIG: TopicDiscussionConfig = {
  topic: 'How I get to school',
  grade: 'grade5-1',     // NEW — must match a grade.id in curriculum.json
  selectedRole: 'tom',
  // …rest unchanged
}

Add action:

updateGrade(id: string) {
  this.config.grade = id
},

src/views/Editor/EnglishSpeaking/SpeakingPanel.vue

Replace the local selectedGrade ref with a binding to the store:

  • Before: const selectedGrade = ref('grade5-1')
  • After: bind <CurriculumSelector>'s selectedGrade prop to store.config.grade and its update:selectedGrade event to store.updateGrade()

selectedTextbook and selectedUnit stay as local refs — they're not part of the prompt contract.

src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue

No changes. Already emits update:selectedGrade with grade.id.


Section 2 — Frontend → Backend request contract

POST /session/create body:

{
  topic: string
  grade: string          // REQUIRED — dropdown's selected value (= grade.id, e.g. 'grade5-1')
  vocabulary: string[]   // array (may be empty)
  sentences: string[]    // array (may be empty)
  totalRounds?: number
  durationSeconds?: number
  roleId?: string
  userId?: string
}

src/views/Editor/EnglishSpeaking/services/llmService.ts

In RealDialogueAPI.createSession(), extend the request body:

body: JSON.stringify({
  topic: store.config.topic,
  grade: store.config.grade,                        // NEW
  vocabulary: store.config.learningGoals.vocabulary, // NEW
  sentences: store.config.learningGoals.sentences,   // NEW
  totalRounds: store.config.practice.rounds,
  // …existing fields
})

Frontend does not translate grade.id → label. Whatever the dropdown selected goes on the wire.


Section 3 — Backend changes

app/api/dialogue.py

Extend CreateSessionRequest:

class CreateSessionRequest(BaseModel):
    topic: str
    grade: str                                # NEW, required
    vocabulary: list[str] = []                # NEW
    sentences: list[str] = []                 # NEW
    totalRounds: int = 3
    durationSeconds: int | None = None
    roleId: str | None = None
    userId: str | None = None

In the route handler, pass the new fields into create_session_only().

app/service/speaking/dialogue_service.py

1. Replace SYSTEM_PROMPT_TEMPLATE with the full template (4 placeholders: {年级}, {话题主题}, {重点词汇}, {重点句型}):

SYSTEM_PROMPT_TEMPLATE = """You are a professional English speaking practice partner designed for students at the {年级}. Your goal is to create a relaxed and encouraging learning environment to help students improve their spoken English skills. All output must be in English.

## Core Configuration
- **Practice Level**: {年级}
- **Conversation Topic**: {话题主题}
- **Key Vocabulary**: {重点词汇}
- **Key Sentence Patterns**: {重点句型}

## Grade-Specific Adaptation Rules
**The system must dynamically adjust all language output based on the following parameters for the specified {年级}:**

### For Grades 1-3 (e.g., Grade 2)
*   **Vocabulary Selection**: 100% CEFR A1-A2 level words. Use only the most common, concrete terms related to the topic.
*   **Sentence Complexity**: 100% simple sentences (single subject + predicate). Use no compound or complex sentences.
*   **Sentence Length**: Aim for 4 to 8 words per sentence.
*   **Utterances per Turn**: As the teacher, you should speak 1 sentence per turn. Encourage the student to respond with 1 simple sentence.
*   **Pacing & Scaffolding**: Speak slowly. Use repetition. Ask one simple, direct question at a time.

### For Grades 4-6 (e.g., Grade 5)
*   **Vocabulary Selection**: Approximately 80% CEFR A1-A2 and 20% B1-B2 level words. You may introduce a few more descriptive or topic-specific adjectives/adverbs.
*   **Sentence Complexity**: Approximately 90% simple sentences and 10% compound sentences (using 'and', 'but', 'so', 'because').
*   **Sentence Length**: Aim for 10 to 15 words per sentence.
*   **Utterances per Turn**: As the teacher, you can use 1-2 sentences per turn. Encourage the student to form slightly longer responses.
*   **Pacing & Scaffolding**: Use natural pacing. You can connect ideas within a turn using basic conjunctions.

### For Middle School Grades 1-3 (e.g., Grade 8)
*   **Vocabulary Selection**: Approximately 70% CEFR A1-A2 and 30% B1-B2 level words. Confidently use a wider range of vocabulary, including more abstract or general academic terms where appropriate.
*   **Sentence Complexity**: Approximately 60% simple sentences and 40% compound/complex sentences. Use relative clauses, participial phrases, and a variety of conjunctions.
*   **Sentence Length**: Aim for 15 to 25 words per sentence.
*   **Utterances per Turn**: As the teacher, you can use 2 sentences to model richer language. Encourage the student to express opinions, reasons, and sequences of events.
*   **Pacing & Scaffolding**: Use natural, conversational pacing. Prompt for elaboration (e.g., "Why?", "Can you tell me more?").

## Conversation Rules
1.  **Role & Tone**: You are the friendly tutor. Maintain a positive, patient, and encouraging tone. Correct errors gently and implicitly by modeling the correct form in your following response.
2.  **Topic Adherence**: All dialogue must naturally revolve around the topic: **{话题主题}**. Gently guide the conversation back if it drifts.
3.  **Language Focus Integration**:
    *   Seamlessly incorporate the **{重点词汇}** into the conversation. Aim to use and elicit each target word multiple times.
    *   Create scenarios that naturally prompt the use of the **{重点句型}**.
    *   Provide positive, specific feedback when the student successfully uses a target word or pattern (e.g., "Great! You said 'by bus.'").
4.  **Conversation Flow**:
    *   **Start**: Greet, introduce yourself, and state the topic.
    *   **Core**: Engage in 4-6 conversational turns. Adapt your follow-up questions and prompts based on the student's responses and the **Grade-Specific Rules** above.
    *   **End**: Conclude the practice naturally with encouraging feedback.

## Example Dialogue Styles
*   **Grade 2 Example**: T: "Hello. Let's talk about school. How do you come to school?" S: "I come by car." T: "Good! By car. Is it fun?"
*   **Grade 5 Example**: T: "Hello! Today, let's discuss how we get to school. How do you usually come to school?" S: "I usually come by bus with my friend." T: "That's nice. Do you live near the bus stop?"
*   **Grade 8 Example**: T: "Hello there. Let's talk about our commutes to school. What's your typical way of getting to school, and how long does it usually take?" S: "I typically ride my bike because I live quite near, so it only takes about ten minutes." T: "That's efficient. Do you ever use other means of transport, like the subway?"

## Start the Conversation Now
Initiate the conversation. Your first utterance must:
1.  Be in English.
2.  Start with a greeting like "Hello" or "Hi".
3.  Mention the topic **{话题主题}**.
4.  Be crafted according to the **Grade-Specific Adaptation Rules** for the specified **{年级}**.
"""

2. Update create_session_only() signature and rendering:

async def create_session_only(
    db: AsyncSession,
    topic: str,
    grade: str,                            # NEW, required
    vocabulary: list[str],                 # NEW
    sentences: list[str],                  # NEW
    total_rounds: int = 3,
    duration_seconds: int | None = None,
    role_config: dict | None = None,
    user_id: str | None = None,
) -> DialogueSession:
    if not grade.strip():
        raise HTTPException(400, "grade is required")

    rendered_vocab = ", ".join(vocabulary)
    rendered_sentences = "\n".join(f"- {s}" for s in sentences)

    system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
        年级=grade,
        话题主题=topic,
        重点词汇=rendered_vocab,
        重点句型=rendered_sentences,
    )
    # …existing persistence logic, store system_prompt on session

app/models/dialogue.py

No changes. DialogueSession.system_prompt: Text already exists.


Section 4 — Serialization & edge cases

Vocabulary string[] → {重点词汇}

  • Render: ", ".join(vocabulary)
  • Empty array → empty string (placeholder line stays in template, value is blank)
  • No dedup, no trim, no length cap (frontend store already manages)

Sentences string[] → {重点句型}

  • Render: "\n".join(f"- {s}" for s in sentences)
  • Empty array → empty string
  • Same: no preprocessing

grade missing / empty

  • grade: str has no default → Pydantic 422 if missing
  • Empty string "" passes Pydantic but is semantically invalid → backend raises HTTPException(400, "grade is required")

topic missing

  • Status quo, existing validation applies.

Frontend defensives

  • store.config.grade defaults to 'grade5-1' from DEFAULT_CONFIG — normal path never sends empty
  • vocabulary / sentences come from store.config.learningGoals.{vocabulary, sentences} — defaults are populated

Explicitly NOT done

  • No grade.id → label conversion (LLM reads {年级} literal, matches against the three tier sections it sees in the prompt)
  • No tier inference logic in code
  • No persistence of vocab/sentences as separate DB columns (they're already inside the rendered system_prompt)

Section 5 — Compatibility & verification

DB compatibility

  • No schema change.
  • Historical dialogue_session rows have system_prompt rendered from the old short template — left as-is, only new sessions use the new template.
  • If the backend has no real session history yet, the table can be cleared without consequence.

Caller compatibility

  • create_session_only() signature changes (new required grade). Only one caller: POST /session/create route handler in app/api/dialogue.py. Update it in the same change.

Deployment order

  1. Backend first — accepts new fields, renders new template
  2. Frontend after — sends new fields
  3. Window: old frontend + new backend → missing grade → 422 (visible error in dev). Acceptable for solo-dev environment.
  4. Reverse (new frontend + old backend) → extra fields ignored, old template renders without grade adaptation. Won't crash, just won't have desired effect.

Verification

  1. Backend smoke test: curl POST /session/create with {topic, grade, vocabulary, sentences} → query DB for the row's system_prompt column → confirm it contains the three tier sections and all 4 placeholders are substituted.
  2. Frontend integration: change grade in SpeakingPanel, edit vocab/sentences, click start → DB row's system_prompt reflects current store state.
  3. LLM behavior: run a real round with grade='grade5-1' and inspect output — sentence length / vocabulary should track the Grade 4-6 tier rules. Repeat with a hypothetical grade='grade2' to confirm tier switches.

Rollback

Revert the two commits (frontend, backend). DB stays clean since no migration. Old short prompt template returns.


Files touched

Frontend (/Users/buoy/Development/gitrepo/PPT)

  • src/types/englishSpeaking.ts — add grade to TopicDiscussionConfig
  • src/store/speaking.ts — add grade default + updateGrade action
  • src/views/Editor/EnglishSpeaking/SpeakingPanel.vue — bind grade to store
  • src/views/Editor/EnglishSpeaking/services/llmService.ts — extend createSession body

Backend (/Users/buoy/Development/gitrepo/cococlass-english-speaking-api)

  • app/api/dialogue.py — extend CreateSessionRequest, pass new fields to service
  • app/service/speaking/dialogue_service.py — replace SYSTEM_PROMPT_TEMPLATE, extend create_session_only signature

Untouched

  • app/models/dialogue.py
  • src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue
  • src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue
  • src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts