2026-04-25-english-speaking-dialogue-prompt.md 31 KB

English Speaking Dialogue System Prompt — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the minimal dialogue system prompt with a structured, grade-aware template; wire the existing curriculum-grade dropdown + learning-goal vocabulary/sentences into session creation so the backend can render the full prompt at session creation time.

Architecture: Frontend persists grade (= grade.id from curriculum.json, e.g. 'grade5-1') into the Pinia speaking store and sends it along with vocabulary[] and sentences[] to the backend's POST /session. Backend renders a full multi-tier prompt template into dialogue_session.system_prompt (no schema change). The LLM reads the literal {年级} value and self-selects the correct tier from the three Grade-Specific Adaptation Rules sections in the prompt.

Tech Stack:

  • Frontend: Vue 3 + TypeScript, Pinia (/Users/buoy/Development/gitrepo/PPT)
  • Backend: FastAPI + SQLAlchemy async + Pydantic (/Users/buoy/Development/gitrepo/cococlass-english-speaking-api)
  • Backend tests: pytest + httpx ASGITransport + in-memory aiosqlite

Spec: docs/superpowers/specs/2026-04-25-english-speaking-dialogue-prompt-design.md


File Map

Backend (cococlass-english-speaking-api):

  • Modify: app/api/dialogue.py:44-49 — extend CreateSessionRequest
  • Modify: app/api/dialogue.py:52-68 — pass new fields to service
  • Modify: app/service/speaking/dialogue_service.py:27-34 — replace SYSTEM_PROMPT_TEMPLATE
  • Modify: app/service/speaking/dialogue_service.py:52-90 — extend create_session_only
  • Test: tests/api/test_dialogue_greeting.py — extend existing test or add a new file
  • Test: tests/service/speaking/test_dialogue_service_prompt.py — new file for prompt rendering

Frontend (PPT):

  • Modify: src/types/englishSpeaking.ts:51-57 — add grade to TopicDiscussionConfig
  • Modify: src/types/englishSpeaking.ts:240-246 — add grade to SessionConfig
  • Modify: src/store/speaking.ts:10-42 — add grade to DEFAULT_CONFIG, add updateGrade
  • Modify: src/views/Editor/EnglishSpeaking/SpeakingPanel.vue:80-82 — bind selectedGrade to store
  • Modify: src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:213-218 — pass grade/sentences
  • Modify: src/views/Editor/EnglishSpeaking/services/llmService.ts:155-176 — send grade/vocabulary/sentences

Untouched:

  • app/models/dialogue.py (no schema change)
  • src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue (already emits grade.id)
  • src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue
  • src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts

Order

Backend first (Tasks 1–4) so the API accepts new fields before frontend sends them. Frontend after (Tasks 5–9). Then integration verification (Task 10).


Task 1: Backend — Extend CreateSessionRequest Pydantic model

Repo: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Files:

  • Modify: app/api/dialogue.py:44-49

  • [ ] Step 1: Update the Pydantic model

Edit app/api/dialogue.py, replace the existing CreateSessionRequest definition:

class CreateSessionRequest(BaseModel):
    topic: str
    grade: str
    vocabulary: list[str] = []
    sentences: list[str] = []
    totalRounds: int = 3
    durationSeconds: int | None = None
    roleId: str | None = None
    userId: str | None = None
  • Step 2: Verify import is fine

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && python -c "from app.api.dialogue import CreateSessionRequest; r = CreateSessionRequest(topic='t', grade='g'); print(r.vocabulary, r.sentences)"

Expected output: [] []

  • Step 3: Commit
cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api
git add app/api/dialogue.py
git commit -m "feat(speaking): add grade/vocabulary/sentences to CreateSessionRequest"

Task 2: Backend — Replace SYSTEM_PROMPT_TEMPLATE

Repo: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Files:

  • Modify: app/service/speaking/dialogue_service.py:27-34

  • [ ] Step 1: Replace the template constant

In app/service/speaking/dialogue_service.py, replace the SYSTEM_PROMPT_TEMPLATE = """...""" block (currently lines 27–34) with the full multi-tier template. Use Python .format() named-substitution style — placeholders are {年级}, {话题主题}, {重点词汇}, {重点句型}. Note: any literal { / } in the template body (none expected) would need doubling.

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 **{年级}**.
"""
  • Step 2: Smoke check the template renders

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && python -c "from app.service.speaking.dialogue_service import SYSTEM_PROMPT_TEMPLATE; print(SYSTEM_PROMPT_TEMPLATE.format(年级='grade5-1', 话题主题='How I get to school', 重点词汇='by bus, on foot', 重点句型='- How do you come to school?')[:200])"

Expected: First 200 chars of the rendered prompt with grade5-1 and How I get to school substituted in. No KeyError.

  • Step 3: Commit
git add app/service/speaking/dialogue_service.py
git commit -m "feat(speaking): replace system prompt with grade-aware multi-tier template"

Task 3: Backend — Extend create_session_only and add unit test

Repo: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Files:

  • Modify: app/service/speaking/dialogue_service.py:52-90
  • Create: tests/service/speaking/test_dialogue_service_prompt.py

  • [ ] Step 1: Write the failing test

Create tests/service/speaking/test_dialogue_service_prompt.py:

"""Tests for system_prompt rendering in create_session_only."""

from unittest.mock import MagicMock

import pytest
import pytest_asyncio
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool

from app.models.dialogue import Base, DialogueSession
from app.service.speaking.dialogue_service import DialogueService


@pytest_asyncio.fixture
async def db():
    engine = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        poolclass=StaticPool,
        connect_args={"check_same_thread": False},
    )
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
    async with SessionLocal() as s:
        yield s


@pytest.fixture
def service():
    return DialogueService(
        asr=MagicMock(), llm=MagicMock(), assessor=MagicMock(), storage=MagicMock(),
    )


@pytest.mark.asyncio
async def test_create_session_renders_full_prompt(db, service):
    await service.create_session_only(
        db=db,
        topic="How I get to school",
        grade="grade5-1",
        vocabulary=["by bus", "on foot"],
        sentences=["How do you come to school?", "I come by bus."],
    )

    session = (await db.execute(select(DialogueSession))).scalar_one()
    prompt = session.system_prompt

    assert "grade5-1" in prompt
    assert "How I get to school" in prompt
    assert "by bus, on foot" in prompt
    assert "- How do you come to school?" in prompt
    assert "- I come by bus." in prompt
    assert "Grade-Specific Adaptation Rules" in prompt
    assert "For Grades 1-3" in prompt
    assert "For Grades 4-6" in prompt
    assert "For Middle School" in prompt


@pytest.mark.asyncio
async def test_create_session_handles_empty_arrays(db, service):
    await service.create_session_only(
        db=db,
        topic="X",
        grade="grade5-1",
        vocabulary=[],
        sentences=[],
    )
    session = (await db.execute(select(DialogueSession))).scalar_one()
    prompt = session.system_prompt
    # Placeholders are substituted (with empty strings) — no leftover braces
    assert "{重点词汇}" not in prompt
    assert "{重点句型}" not in prompt


@pytest.mark.asyncio
async def test_create_session_rejects_blank_grade(db, service):
    with pytest.raises(HTTPException) as exc:
        await service.create_session_only(
            db=db,
            topic="X",
            grade="   ",
            vocabulary=[],
            sentences=[],
        )
    assert exc.value.status_code == 400
  • Step 2: Run test, expect failure

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/service/speaking/test_dialogue_service_prompt.py -v

Expected: FAIL — create_session_only() doesn't accept grade/vocabulary/sentences yet (TypeError on unexpected kwarg).

  • Step 3: Update create_session_only signature and rendering

In app/service/speaking/dialogue_service.py, replace the existing create_session_only method (lines 52–90) with:

    async def create_session_only(
        self,
        db: AsyncSession,
        topic: str,
        grade: str,
        vocabulary: list[str],
        sentences: list[str],
        total_rounds: int = 3,
        duration_seconds: int | None = None,
        role_config: dict | None = None,
        user_id: str | None = None,
    ) -> dict:
        """只建会话记录,不调 LLM / Insert a session row without invoking the LLM.

        Returns:
            dict: {sessionId, totalRounds, currentRound, expiresAt}
        """
        from fastapi import HTTPException

        if not grade.strip():
            raise HTTPException(status_code=400, detail="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,
        )
        expires_at = None
        if duration_seconds:
            expires_at = datetime.now() + timedelta(seconds=duration_seconds)

        session = DialogueSession(
            user_id=user_id,
            topic=topic,
            role_config=role_config,
            total_rounds=total_rounds,
            current_round=1,
            status="active",
            system_prompt=system_prompt,
            expires_at=expires_at,
        )
        db.add(session)
        await db.commit()
        await db.refresh(session)

        return {
            "sessionId": session.uuid,
            "totalRounds": total_rounds,
            "currentRound": session.current_round,
            "expiresAt": expires_at.isoformat() if expires_at else None,
        }
  • Step 4: Run tests, expect pass

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/service/speaking/test_dialogue_service_prompt.py -v

Expected: All 3 tests PASS.

  • Step 5: Run full backend suite to make sure nothing else broke

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest -v 2>&1 | tail -40

Expected: Existing tests may fail because they call create_session_only without the new required grade/vocabulary/sentences arguments. Note the failures — they're addressed in Task 4 (route handler update) and may also need fixture updates. If only the route's E2E test is broken, that's expected; we'll fix it in Task 4. If a service-level test breaks, fix the call site to pass the new required args (use defaults like grade='grade5-1', vocabulary=[], sentences=[]).

  • Step 6: Commit
git add app/service/speaking/dialogue_service.py tests/service/speaking/test_dialogue_service_prompt.py
git commit -m "feat(speaking): render grade-aware system_prompt in create_session_only"

Task 4: Backend — Wire route handler to pass new fields

Repo: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Files:

  • Modify: app/api/dialogue.py:52-68 (the /session POST handler)
  • Modify: tests/api/test_dialogue_greeting.py (existing E2E test sends old payload)

  • [ ] Step 1: Update the route handler to pass new fields

In app/api/dialogue.py, replace the body of the create_session route handler:

@router.post("/session")
async def create_session(
    req: CreateSessionRequest,
    db: AsyncSession = Depends(get_db),
    service: DialogueService = Depends(get_dialogue_service),
):
    """创建对话 session(不生成开场白)。开场白由 POST /session/{id}/greeting 触发。"""
    logger.info(f"Creating session: topic={req.topic}, grade={req.grade}, rounds={req.totalRounds}")
    result = await service.create_session_only(
        db=db,
        topic=req.topic,
        grade=req.grade,
        vocabulary=req.vocabulary,
        sentences=req.sentences,
        total_rounds=req.totalRounds,
        duration_seconds=req.durationSeconds,
        user_id=req.userId,
    )
    logger.info(f"Session created: {result['sessionId']}")
    return result
  • Step 2: Update the existing E2E test in tests/api/test_dialogue_greeting.py

Find every client.post("/api/speaking/dialogue/session", json=...) call site (or whatever path your test uses — search the file). For each one, add "grade": "grade5-1" to the JSON body. If vocabulary / sentences aren't sent, they default to [] per the Pydantic model — no change needed.

Concretely, run: grep -n '"topic"' /Users/buoy/Development/gitrepo/cococlass-english-speaking-api/tests/api/test_dialogue_greeting.py

Then for each match, ensure the JSON dict includes "grade": "grade5-1".

  • Step 3: Add a new E2E test asserting required-field validation

Append to tests/api/test_dialogue_greeting.py (inside the existing test module — reuse the test_env fixture):

@pytest.mark.asyncio
async def test_create_session_requires_grade(test_env):
    client: AsyncClient = test_env["client"]
    res = await client.post(
        "/api/speaking/dialogue/session",
        json={"topic": "Animals"},  # missing 'grade'
    )
    assert res.status_code == 422  # Pydantic validation error


@pytest.mark.asyncio
async def test_create_session_persists_grade_in_prompt(test_env):
    client: AsyncClient = test_env["client"]
    res = await client.post(
        "/api/speaking/dialogue/session",
        json={
            "topic": "How I get to school",
            "grade": "grade5-1",
            "vocabulary": ["by bus", "on foot"],
            "sentences": ["How do you come to school?"],
        },
    )
    assert res.status_code == 200
    session_id = res.json()["sessionId"]

    # Inspect persisted prompt
    SessionLocal = test_env["SessionLocal"]
    from sqlalchemy import select
    from app.models.dialogue import DialogueSession
    async with SessionLocal() as s:
        row = (await s.execute(select(DialogueSession).where(DialogueSession.uuid == session_id))).scalar_one()
    assert "grade5-1" in row.system_prompt
    assert "by bus, on foot" in row.system_prompt
    assert "- How do you come to school?" in row.system_prompt

If the existing test_env fixture doesn't expose client and SessionLocal as a dict, adapt to whatever shape it returns (read the fixture body around lines 22–60). The point: have client for HTTP calls and a way to query the DB.

  • Step 4: Run tests

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/api/test_dialogue_greeting.py -v

Expected: All tests PASS, including the new ones.

  • Step 5: Run full suite once more

Run: cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest -v 2>&1 | tail -20

Expected: All tests PASS.

  • Step 6: Commit
git add app/api/dialogue.py tests/api/test_dialogue_greeting.py
git commit -m "feat(speaking): wire grade/vocabulary/sentences through /session route"

Task 5: Frontend — Extend TopicDiscussionConfig and SessionConfig types

Repo: /Users/buoy/Development/gitrepo/PPT

Files:

  • Modify: src/types/englishSpeaking.ts:51-57
  • Modify: src/types/englishSpeaking.ts:240-246

  • [ ] Step 1: Add grade to TopicDiscussionConfig

In src/types/englishSpeaking.ts, replace the TopicDiscussionConfig interface (lines 51–57):

// 话题讨论配置
export interface TopicDiscussionConfig {
  topic: string
  grade: string          // grade.id from curriculum.json (e.g., 'grade5-1')
  selectedRole: string
  learningGoals: LearningGoals
  practice: PracticeSettings
  evaluation: EvaluationSettings
}
  • Step 2: Add grade to SessionConfig

In the same file, replace the SessionConfig interface (lines 240–246):

// 对话会话配置
export interface SessionConfig {
  topic: string
  grade: string
  roleId: string
  totalRounds: number
  vocabulary?: string[]
  sentences?: string[]
}
  • Step 3: Type-check (will fail at consumer sites — that's expected)

Run: cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | head -30

Expected: Errors at sites where TopicDiscussionConfig is constructed without grade (DEFAULT_CONFIG) and where SessionConfig is constructed without grade (TopicDiscussionPreview's createSession call). These are addressed in Tasks 6 and 8.

  • Step 4: Commit
cd /Users/buoy/Development/gitrepo/PPT
git add src/types/englishSpeaking.ts
git commit -m "feat(speaking): add grade to TopicDiscussionConfig and SessionConfig types"

Task 6: Frontend — Update Pinia store (DEFAULT_CONFIG + updateGrade)

Repo: /Users/buoy/Development/gitrepo/PPT

Files:

  • Modify: src/store/speaking.ts:10-42 (DEFAULT_CONFIG)
  • Modify: src/store/speaking.ts:78-82 (actions, near updateTopic)

  • [ ] Step 1: Add grade to DEFAULT_CONFIG

In src/store/speaking.ts, the DEFAULT_CONFIG object — add grade: 'grade5-1' right after topic:

const DEFAULT_CONFIG: TopicDiscussionConfig = {
  topic: 'How I get to school',
  grade: 'grade5-1',
  selectedRole: 'tom',
  learningGoals: {
    // …unchanged
  • Step 2: Add updateGrade action

In the actions block, immediately after the updateTopic action:

    // 年级
    updateGrade(grade: string) {
      this.config.grade = grade
    },
  • Step 3: Type-check

Run: cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep -E "speaking\.ts|englishSpeaking\.ts" | head -10

Expected: No errors from speaking.ts or englishSpeaking.ts. Errors elsewhere (TopicDiscussionPreview) are still expected.

  • Step 4: Commit
git add src/store/speaking.ts
git commit -m "feat(speaking): persist grade in store, add updateGrade action"

Task 7: Frontend — Bind SpeakingPanel selectedGrade to store

Repo: /Users/buoy/Development/gitrepo/PPT

Files:

  • Modify: src/views/Editor/EnglishSpeaking/SpeakingPanel.vue:67-110

  • [ ] Step 1: Replace local selectedGrade ref with store binding

In src/views/Editor/EnglishSpeaking/SpeakingPanel.vue, the <script setup> section currently has at lines 80–82:

const selectedTextbook = ref('shep')
const selectedGrade = ref('grade5-1')
const selectedUnit = ref('SHEP-5-上-Unit2')

The store is already imported (line 71: import { useSpeakingStore } from '@/store/speaking') and instantiated as speakingStore (line 106).

Move the useSpeakingStore call up (right after the const pageMode line so it's before our binding), and replace the three refs block with:

const speakingStore = useSpeakingStore()

// 课本筛选条件的默认值(上海英语·五年级上·Unit2),供开发/演示用
const selectedTextbook = ref('shep')
const selectedUnit = ref('SHEP-5-上-Unit2')
// selectedGrade 持久化到 store(用于发送到后端 createSession 渲染 system_prompt)
const selectedGrade = computed({
  get: () => speakingStore.config.grade,
  set: (val: string) => speakingStore.updateGrade(val),
})

Add computed to the Vue import at line 68:

import { ref, computed, watch } from 'vue'

Remove the now-duplicate const speakingStore = useSpeakingStore() from line 106 (the watch block can use the same instance).

  • Step 2: Verify the <Layer1Home> v-bind/v-on still works

The template binding :selectedGrade="selectedGrade" and @update:selectedGrade="selectedGrade = $event" will continue to work because computed with a setter is assignable. No template change needed.

  • Step 3: Type-check

Run: cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep "SpeakingPanel" | head -5

Expected: No errors from SpeakingPanel.vue.

  • Step 4: Manual smoke test

Run: cd /Users/buoy/Development/gitrepo/PPT && npm run dev (or whatever dev command this project uses — check package.json if unsure).

Open the English Speaking panel. Change the grade in the dropdown. Open Vue devtools (or add a temporary console.log(speakingStore.config.grade) in the panel) and confirm store.config.grade updates.

Stop the dev server.

  • Step 5: Commit
git add src/views/Editor/EnglishSpeaking/SpeakingPanel.vue
git commit -m "feat(speaking): bind SpeakingPanel grade selector to speakingStore"

Task 8: Frontend — TopicDiscussionPreview passes grade/sentences in createSession

Repo: /Users/buoy/Development/gitrepo/PPT

Files:

  • Modify: src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:213-218

  • [ ] Step 1: Extend the createSession call

In TopicDiscussionPreview.vue, the startDialogue function currently calls:

const info = await api.createSession({
  topic: speakingStore.config.topic || props.topic,
  totalRounds: speakingStore.config.practice.rounds ?? props.totalRounds,
  roleId: mockRole.id,
  vocabulary: speakingStore.config.learningGoals.vocabulary,
})

Replace with:

const info = await api.createSession({
  topic: speakingStore.config.topic || props.topic,
  grade: speakingStore.config.grade,
  totalRounds: speakingStore.config.practice.rounds ?? props.totalRounds,
  roleId: mockRole.id,
  vocabulary: speakingStore.config.learningGoals.vocabulary,
  sentences: speakingStore.config.learningGoals.sentences,
})
  • Step 2: Type-check

Run: cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep "TopicDiscussionPreview" | head -5

Expected: No errors. (Earlier grade missing error from Task 5 is now resolved.)

  • Step 3: Commit
git add src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
git commit -m "feat(speaking): pass grade and sentences to createSession"

Task 9: Frontend — RealDialogueAPI.createSession sends new fields on the wire

Repo: /Users/buoy/Development/gitrepo/PPT

Files:

  • Modify: src/views/Editor/EnglishSpeaking/services/llmService.ts:155-176

  • [ ] Step 1: Extend the request body

In src/views/Editor/EnglishSpeaking/services/llmService.ts, the RealDialogueAPI.createSession method currently has:

body: JSON.stringify({
  topic: config.topic,
  totalRounds: config.totalRounds,
  roleId: config.roleId,
}),

Replace with:

body: JSON.stringify({
  topic: config.topic,
  grade: config.grade,
  vocabulary: config.vocabulary ?? [],
  sentences: config.sentences ?? [],
  totalRounds: config.totalRounds,
  roleId: config.roleId,
}),

Also fix the MockDialogueAPI.createSession if it uses the rename — actually it doesn't read these fields, no change needed there.

  • Step 2: Type-check the whole frontend

Run: cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | tail -20

Expected: No errors related to this change. (Pre-existing unrelated errors, if any, are out of scope.)

  • Step 3: Commit
git add src/views/Editor/EnglishSpeaking/services/llmService.ts
git commit -m "feat(speaking): send grade/vocabulary/sentences to backend /session"

Task 10: Integration smoke test

Both repos.

  • Step 1: Start backend

Run in a terminal (or background): cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && uvicorn app.main:app --reload --port 8000 (use whatever startup command the backend uses — check pyproject.toml / README if unsure).

  • Step 2: Hit the API directly with curl
curl -i -X POST http://localhost:8000/api/speaking/dialogue/session \
  -H 'Content-Type: application/json' \
  -d '{
    "topic": "How I get to school",
    "grade": "grade5-1",
    "vocabulary": ["by bus", "on foot"],
    "sentences": ["How do you come to school?", "I come by bus."],
    "totalRounds": 3
  }'

Expected: HTTP 200, response body has sessionId.

  • Step 3: Inspect the persisted prompt

Connect to the backend's database (the connection string is in app/config.py or env). Query the row:

SELECT system_prompt FROM dialogue_session ORDER BY id DESC LIMIT 1;

(Or use sqlite3 path/to/db.sqlite "SELECT system_prompt FROM dialogue_session ORDER BY id DESC LIMIT 1;" if SQLite.)

Expected: Output contains grade5-1, How I get to school, by bus, on foot, - How do you come to school?, and the three Grade-Specific Adaptation Rules sections.

  • Step 4: Validation negative test
curl -i -X POST http://localhost:8000/api/speaking/dialogue/session \
  -H 'Content-Type: application/json' \
  -d '{"topic": "X"}'

Expected: HTTP 422 (Pydantic validation, missing grade).

  • Step 5: Frontend integration

Run: cd /Users/buoy/Development/gitrepo/PPT && npm run dev

Open the editor, navigate to English Speaking → 话题讨论 config page. Change the grade in SpeakingPanel, edit vocabulary or sentences, click "开始对话". Watch the network tab: the POST /session request body should include grade, vocabulary, sentences matching the UI state.

In the backend DB (or via repeating Step 3), confirm the latest session row's system_prompt reflects what the UI just sent.

  • Step 6: LLM behavior check (optional)

Continue the dialogue flow in the UI — the AI's first turn should match the Grade 4–6 tier rules (10–15 word sentences, mostly simple/compound). If wildly off-tier, that's a prompt-engineering issue, not an integration issue — note for follow-up but doesn't block this plan.

  • Step 7: Stop the dev server and backend

No commit for Task 10 itself (it's verification).


Done criteria

  • All backend tests pass (pytest -v)
  • vue-tsc --noEmit reports no new errors
  • curl POST /session with {topic, grade, vocabulary, sentences} returns 200 and the persisted system_prompt contains the rendered template
  • Missing grade returns 422
  • UI can change grade, send a session-create request, and see the prompt reflect the UI state in the DB