From bb6b15931592ee518cb62c733008ec23425c6990 Mon Sep 17 00:00:00 2001 From: Woody Date: Mon, 27 Apr 2026 19:26:33 +0800 Subject: [PATCH] docs(plan): add Phase PX profile export/import feature plan --- .plans/package4_enhancement_plan.md | 429 +++++++++++++++++++++++++++- 1 file changed, 428 insertions(+), 1 deletion(-) diff --git a/.plans/package4_enhancement_plan.md b/.plans/package4_enhancement_plan.md index 29c078d..ad1b5c1 100644 --- a/.plans/package4_enhancement_plan.md +++ b/.plans/package4_enhancement_plan.md @@ -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 +} + +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 => { + const resp = await apiClient.get(`/prompts/profiles/${name}/export`) + return resp.data +} + +// Import a profile from JSON +export const importProfile = async (name: string, data: ProfileExportData): Promise => { + const resp = await apiClient.post(`/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` + `` 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 `` 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.