|
|
@@ -0,0 +1,810 @@
|
|
|
+# 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:
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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.
|
|
|
+
|
|
|
+```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 **{年级}**.
|
|
|
+"""
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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`:
|
|
|
+
|
|
|
+```python
|
|
|
+"""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:
|
|
|
+
|
|
|
+```python
|
|
|
+ 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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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:
|
|
|
+
|
|
|
+```python
|
|
|
+@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):
|
|
|
+
|
|
|
+```python
|
|
|
+@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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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):
|
|
|
+
|
|
|
+```ts
|
|
|
+// 话题讨论配置
|
|
|
+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):
|
|
|
+
|
|
|
+```ts
|
|
|
+// 对话会话配置
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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`:
|
|
|
+
|
|
|
+```ts
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+ // 年级
|
|
|
+ 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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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:
|
|
|
+
|
|
|
+```ts
|
|
|
+body: JSON.stringify({
|
|
|
+ topic: config.topic,
|
|
|
+ totalRounds: config.totalRounds,
|
|
|
+ roleId: config.roleId,
|
|
|
+}),
|
|
|
+```
|
|
|
+
|
|
|
+Replace with:
|
|
|
+
|
|
|
+```ts
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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:
|
|
|
+
|
|
|
+```sql
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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
|