# 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`:

```ts
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`:

```ts
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:

```ts
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:

```ts
{
  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:

```ts
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`:

```python
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: `{年级}`, `{话题主题}`, `{重点词汇}`, `{重点句型}`):

```python
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:

```python
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
- Window: old frontend + new backend → missing `grade` → 422 (visible error in dev). Acceptable for solo-dev environment.
- 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`
