docs(plan): add Phase PX profile export/import feature plan

This commit is contained in:
Woody 2026-04-27 19:26:33 +08:00
parent 05af86f5d2
commit bb6b159315
1 changed files with 428 additions and 1 deletions

View File

@ -2,7 +2,7 @@
**Source**: User request (2026-04-26)
**Scope**: Refactor the 3-step RAG query pipeline so retrieval, filtering, and response generation are organized per sub-question instead of batch-flattened.
**Status**: ✅ Complete — All 7 sub-phases implemented (2026-04-26). Phase 4a Prompt Integration added (2026-04-27).
**Status**: ✅ Complete — All 7 sub-phases implemented (2026-04-26). Phase 4a Prompt Integration added (2026-04-27). Phase PX Profile Export/Import planned (2026-04-27) — see end of file.
---
@ -986,3 +986,430 @@ None — all resolved.
| `test_phase4_response_panel.test.tsx` | ~6 | Section rendering, source grouping, copy, loading |
| `test_phase4_citation_parser.test.ts` | ~4 | Per-sub-q lookup, cross-section isolation |
| `test_phase4_e2e_query_flow.test.tsx` | ~3 | Full SSE flow with mocked stream |
---
## Phase PX: Profile Export/Import (2026-04-27)
**Source**: User request — "add an export and import function for setting a profile. The format is json."
**Scope**: Add JSON export/import capability to the System Prompts page. Users can download a profile's prompt configuration as a `.json` file and import it into another profile (or the same one) to transfer or back up their prompt settings.
**Status**: 🟡 Planned — not yet implemented.
---
### Objective
Let users:
1. **Export** a single profile's prompt templates as a downloadable JSON file
2. **Import** a previously exported JSON file to overwrite a profile's prompt templates
3. Optionally, **export all** profiles at once for full configuration backup
---
### Decision Register
| # | Decision | Rationale |
|---|----------|-----------|
| P1 | Export single profiles, not all-at-once by default | User asked "for setting a profile" — per-profile export/import is more practical for sharing individual configurations. Add "Export All" as secondary option. |
| P2 | Import overwrites ALL prompt steps for target profile | Simplest mental model. Import = full replace (not merge). User gets confirmation dialog before proceeding. |
| P3 | Export JSON includes all 7 steps (including legacy `filter`, `generate`) | Even though UI hides these, the DB stores them. Export should be a complete snapshot — import restores all 7. |
| P4 | Do NOT export auto-increment IDs | `id` fields are not portable between databases. Import inserts new rows; joins on `(name, step_name)` uniqueness. |
| P5 | `created_at`/`updated_at` reset on import | Imported profiles get fresh timestamps (`datetime('now')`). Original export timestamp preserved in file metadata only. |
| P6 | Active profile state NOT imported | `is_active` is deployment-specific. The user sets active profile separately via the existing dropdown. Import only touches `prompt_template` content. |
| P7 | Validate profile name on import | Only A, B, C allowed. Import into non-existent name = rejected. |
| P8 | JSON schema versioned | `"format": "legco-reranker-profile/v1"` for future-proofing. Reject unknown versions on import. |
---
### JSON Format Specification
#### Single Profile Export
```json
{
"format": "legco-reranker-profile/v1",
"profile_name": "A",
"exported_at": "2026-04-27T12:00:00Z",
"prompts": {
"decompose": "Given this question: '{question}'\n\nBreak it down into 2-5 simplified sub-questions...",
"filter": "Given question '{question}' and these document chunks:\n\n{chunks}\n\n...",
"generate": "Question: {question}\n\nContext:\n{context}\n\n...",
"generate_per_subq": "Answer each sub-question using ONLY its document chunks...",
"filter_intro": "Evaluate each chunk for relevance to its associated sub-question only.",
"filter_section": "\nSub-question {subq_idx}: \"{subq_question}\"\n{chunks}",
"filter_outro": "\nFor each chunk, rate its relevance 0-10..."
}
}
```
#### Full Backup Export (All Profiles)
```json
{
"format": "legco-reranker-profile/v1",
"exported_at": "2026-04-27T12:00:00Z",
"active_profile": "A",
"profiles": {
"A": {
"prompts": { ... }
},
"B": {
"prompts": { ... }
},
"C": {
"prompts": { ... }
}
}
}
```
#### Import Request Format
```json
POST /api/v1/prompts/profiles/{name}/import
Content-Type: application/json
{
"format": "legco-reranker-profile/v1",
"profile_name": "A",
"exported_at": "2026-04-27T12:00:00Z",
"prompts": {
"decompose": "...",
...
}
}
```
**Response**:
```json
{
"status": "ok",
"profile": "B",
"imported_steps": 7,
"source_profile": "A"
}
```
---
### Sub-Phase Structure
| Sub-Phase | Scope | Components | Test Files |
|-----------|-------|------------|------------|
| PX.1 | Backend — Export endpoint | `routers/prompts.py`, `models/prompts.py` | `test_phaseX_export.py` |
| PX.2 | Backend — Import endpoint | `routers/prompts.py`, `models/prompts.py`, `prompt_service.py` | `test_phaseX_import.py` |
| PX.3 | Frontend — Export/Import UI | `SystemPromptsPage.tsx`, `ProfileList.tsx`, `lib/api.ts`, `lib/queries.tsx`, `types/index.ts` | `test_phaseX_export_import.test.tsx` |
| PX.4 | Testing & Polish | All affected files | Integration + acceptance tests |
---
### Sub-Phase PX.1: Backend — Single Profile Export Endpoint
**Test files to write first:**
- `backend/app/test/test_phaseX_export.py` — Tests export endpoint, JSON schema validation, empty profile handling
**Task PX.1.1: Add Pydantic models**
File: `backend/app/models/prompts.py`
```python
class ProfileExportResponse(BaseModel):
format: str = "legco-reranker-profile/v1"
profile_name: str
exported_at: str
prompts: dict[str, str]
class AllProfilesExportResponse(BaseModel):
format: str = "legco-reranker-profile/v1"
exported_at: str
active_profile: str
profiles: dict[str, dict[str, dict[str, str]]] # profile_name -> {"prompts": {step: text}}
```
**Task PX.1.2: Add `GET /api/v1/prompts/profiles/{name}/export` endpoint**
File: `backend/app/routers/prompts.py`
- Reads all 7 `system_prompts` rows for the given profile
- Returns `ProfileExportResponse` with `Content-Disposition: attachment; filename="legco-profile-{name}.json"`
- Uses `application/json` content type
**Task PX.1.3: Add `GET /api/v1/prompts/export/all` endpoint (optional)**
- Reads all 3 profiles + all 21 prompt rows
- Returns `AllProfilesExportResponse`
- For full backup/restore scenarios
**Commit**: `"feat(prompts): add single-profile and full JSON export endpoints"`
---
### Sub-Phase PX.2: Backend — Single Profile Import Endpoint
**Test files to write first:**
- `backend/app/test/test_phaseX_import.py` — Tests import endpoint, validation, error cases
**Task PX.2.1: Add request model**
File: `backend/app/models/prompts.py`
```python
class ProfileImportRequest(BaseModel):
format: str # must be "legco-reranker-profile/v1"
profile_name: str # source profile name (informational)
exported_at: str | None = None # informational timestamp
prompts: dict[str, str] # step_name -> template_text
```
**Task PX.2.2: Add `POST /api/v1/prompts/profiles/{name}/import` endpoint**
File: `backend/app/routers/prompts.py`
Validation steps:
1. Check target `{name}` is A, B, or C → 400 if not
2. Check `request.format == "legco-reranker-profile/v1"` → 400 if not
3. Validate that all 7 required step keys (`decompose`, `filter`, `generate`, `generate_per_subq`, `filter_intro`, `filter_section`, `filter_outro`) are present in `request.prompts` → 400 with list of missing keys if not
4. Validate no extra/unknown step keys → reject (or warn? → decision: reject with 400, listing unknown keys)
Implementation:
- Uses `PromptService._update_all_prompts()` (existing batch-update internally) to overwrite all 7 steps
- Each step gets fresh `created_at`/`updated_at` timestamps (DB defaults)
- Returns `{"status": "ok", "profile": name, "imported_steps": len(prompts), "source_profile": request.profile_name}`
**Task PX.2.3: Add `POST /api/v1/prompts/import/all` endpoint (optional)**
- Accepts `AllProfilesExportResponse` format
- Imports all 3 profiles at once
- Does NOT change active profile (only if explicitly included)
**Commit**: `"feat(prompts): add single-profile JSON import endpoint with full validation"`
---
### Sub-Phase PX.3: Frontend — Export/Import UI
**Test files to write first:**
- `frontend/src/test/components/test_phaseX_export_import.test.tsx` — Tests export/import buttons, file download, file upload
**Task PX.3.1: Add TypeScript types**
File: `frontend/src/types/index.ts`
```typescript
interface ProfileExportData {
format: string
profile_name: string
exported_at: string
prompts: Record<string, string>
}
interface ProfileImportResponse {
status: string
profile: string
imported_steps: number
source_profile: string
}
```
**Task PX.3.2: Add API client functions**
File: `frontend/src/lib/api.ts`
```typescript
// Download a profile as JSON blob for browser-side save
export const exportProfile = async (name: string): Promise<ProfileExportData> => {
const resp = await apiClient.get<ProfileExportData>(`/prompts/profiles/${name}/export`)
return resp.data
}
// Import a profile from JSON
export const importProfile = async (name: string, data: ProfileExportData): Promise<ProfileImportResponse> => {
const resp = await apiClient.post<ProfileImportResponse>(`/prompts/profiles/${name}/import`, data)
return resp.data
}
```
**Task PX.3.3: Add TanStack Query mutation for import**
File: `frontend/src/lib/queries.tsx`
```typescript
export const useImportProfile = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ name, data }: { name: string; data: ProfileExportData }) =>
importProfile(name, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
})
}
```
**Task PX.3.4: Add Export button to ProfileList cards**
File: `frontend/src/components/ProfileList.tsx`
- Add export icon button (e.g., `Download` from lucide-react) next to the "Edit" button on each card
- On click: calls `exportProfile(name)` via `fetch` → creates blob → triggers browser download via `URL.createObjectURL` + `<a>` click
- Filename: `legco-profile-{name}-{date}.json`
**Task PX.3.5: Add Import button and dialog to SystemPromptsPage**
File: `frontend/src/pages/SystemPromptsPage.tsx`
- Add "Import" button in the top bar (next to "Active Profile" dropdown)
- On click: opens a modal/dialog with:
- File input (accept `.json`) — hidden `<input type="file">` triggered by styled button
- After file selected: parse JSON client-side, show preview (source profile name, export date, step count)
- Target profile selector (dropdown: A, B, C) — defaults to source profile name if valid
- "Import" button → confirmation dialog ("This will overwrite all prompts for Profile {target}. Continue?")
- On confirm: calls `importProfileMutation.mutate()`
- Success: show toast "Profile {target} imported successfully ({n} steps from Profile {source})"
- Error: show inline error message with details
**Task PX.3.6: Add Export All button (optional)**
File: `frontend/src/pages/SystemPromptsPage.tsx`
- "Export All" button in top bar
- Downloads all 3 profiles as `legco-profiles-{date}.json`
**Commit**: `"feat(prompts): add export/import UI with file download, upload dialog, and validation"`
---
### Sub-Phase PX.4: Testing & Polish
**Test files:**
- `backend/app/test/test_phaseX_export.py` — Export endpoint: valid profile, invalid name, JSON schema validation
- `backend/app/test/test_phaseX_import.py` — Import endpoint: valid import, missing steps, extra steps, invalid format version, invalid target name
- `frontend/src/test/components/test_phaseX_export_import.test.tsx` — Export button click → download, Import dialog flow → file upload → preview → confirm → success/error
**Task PX.4.1: Backend unit tests**
- `test_export_profile_valid` — GET export/A returns all 7 steps with correct format version
- `test_export_profile_invalid_name` — GET export/X returns 400
- `test_export_all` — GET export/all returns 3 profiles, 21 prompts total
- `test_import_valid` — POST import/B with valid JSON → 200, verify all 7 steps updated
- `test_import_overwrites_existing` — POST import/B → verify old content replaced
- `test_import_missing_required_step` — POST import with only 6 steps → 400 with missing key listed
- `test_import_unknown_step_key` — POST import with extra step → 400
- `test_import_invalid_format_version` — POST import with format: "v2" → 400
- `test_import_invalid_target_name` — POST import/X → 400
- `test_import_does_not_change_active` — import into inactive profile → active profile unchanged
**Task PX.4.2: Frontend tests**
- Export button visible on each profile card
- Click export → fetch called, download triggered
- Import dialog opens on button click
- File selection → JSON parsed, preview shown
- Invalid JSON file → error message shown
- Target profile selector defaults to source profile
- Confirm import → mutation called, success toast
- Import error → inline error message
- Export All downloads all profiles
**Task PX.4.3: Integration verification**
- `npm run build` — no TypeScript errors
- `npm test` — all frontend tests pass
- `pytest backend/app/test/test_phaseX_*.py -v` — all backend tests pass
- Manual flow: export Profile A → edit Profile B → import exported file into B → verify B's prompts match A's original
**Commit**: `"test(prompts): add unit, integration tests for export/import"`
---
### Files Affected — Complete Inventory
#### Backend — New Files
| File | Sub-Phase | Purpose |
|------|-----------|---------|
| `backend/app/test/test_phaseX_export.py` | PX.4 | Unit tests for export endpoint |
| `backend/app/test/test_phaseX_import.py` | PX.4 | Unit tests for import endpoint |
#### Backend — Modified Files
| File | Sub-Phase | Changes |
|------|-----------|---------|
| `backend/app/models/prompts.py` | PX.1, PX.2 | Add `ProfileExportResponse`, `AllProfilesExportResponse`, `ProfileImportRequest`, `ProfileImportResponse` |
| `backend/app/routers/prompts.py` | PX.1, PX.2 | Add `GET /export`, `GET /export/all`, `POST /import` endpoints |
#### Frontend — New Files
| File | Sub-Phase | Purpose |
|------|-----------|---------|
| `frontend/src/test/components/test_phaseX_export_import.test.tsx` | PX.4 | Component tests for export/import UI |
#### Frontend — Modified Files
| File | Sub-Phase | Changes |
|------|-----------|---------|
| `frontend/src/types/index.ts` | PX.3 | Add `ProfileExportData`, `ProfileImportResponse` types |
| `frontend/src/lib/api.ts` | PX.3 | Add `exportProfile()`, `importProfile()` API functions |
| `frontend/src/lib/queries.tsx` | PX.3 | Add `useImportProfile()` mutation hook |
| `frontend/src/components/ProfileList.tsx` | PX.3 | Add Export button per profile card |
| `frontend/src/pages/SystemPromptsPage.tsx` | PX.3 | Add Import/Export All buttons, import dialog/modal |
---
### Acceptance Criteria
#### Backend
- [ ] `GET /api/v1/prompts/profiles/A/export` returns JSON with all 7 steps, correct format version
- [ ] `GET /api/v1/prompts/profiles/X/export` returns 400 (invalid profile name)
- [ ] `GET /api/v1/prompts/export/all` returns all 3 profiles, active profile marker
- [ ] `POST /api/v1/prompts/profiles/B/import` with valid payload overwrites all 7 steps for Profile B
- [ ] Import rejects payload with missing required step keys (400 + key names)
- [ ] Import rejects payload with unknown step keys (400 + key names)
- [ ] Import rejects payload with unknown format version (400)
- [ ] Import does NOT change `is_active` flag on target profile
- [ ] Exported JSON does NOT contain internal DB IDs (`id`/`profile_id`)
- [ ] All existing prompt API endpoints still work unchanged
#### Frontend
- [ ] Export button visible on each profile card in ProfileList
- [ ] Clicking Export downloads a `.json` file with correct naming (`legco-profile-A-2026-04-27.json`)
- [ ] Import button visible on SystemPromptsPage top bar
- [ ] Clicking Import opens a modal with: file input, JSON preview, target profile selector, confirm button
- [ ] Selecting invalid JSON file shows error message
- [ ] Importing into a valid profile shows success confirmation with step count
- [ ] Import error from backend shows inline error message
- [ ] After successful import, profile data refreshes (query invalidation)
- [ ] All existing System Prompts functionality still works unchanged
---
### Risk Register
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| JSON file too large to upload | Low | Low — 7 prompts × ~2KB = ~14KB | Add 1MB limit on import endpoint (`FastAPI` `Body(max_length=...)`) |
| User imports into wrong profile by mistake | Medium | Medium — overwrites their existing config | Confirmation dialog with source/target profile names clearly displayed before import |
| Exported file missing legacy `filter`/`generate` steps | Medium | Medium — import would fail validation | Always export all 7 steps (even hidden ones). Import validates all 7 are present. |
| Browser download API differences | Low | Low | Use standard `Blob` + `URL.createObjectURL` approach, tested across Chrome/Firefox |
| Import endpoint receives malformed JSON | Low | Low — Pydantic validation catches this | `ProfileImportRequest` model validates format string, dict keys, value types |
| User exports from one deployment and imports into another with different profile names | Low | Low — only 3 names (A/B/C) | Import only into A/B/C — if source was "D", user must choose target manually |
---
### New Dependencies
None. All changes use existing libraries (FastAPI, Pydantic, React, TanStack Query, lucide-react icons).
---
### Implementation Sequence
```
PX.1 (Backend Export) ──► PX.2 (Backend Import)
PX.3 (Frontend UI)
PX.4 (Testing)
```
PX.1 and PX.2 can be done together (both in `routers/prompts.py`). PX.3 depends on knowing the exact API contracts from PX.1/PX.2. PX.4 runs after everything is wired.