Files
lcbp3/specs/200-fullstacks/229-dynamic-prompt-management/tasks.md
T

14 KiB
Raw Blame History

Tasks: Dynamic Prompt Management for OCR Extraction

Input: Design documents from specs/200-fullstacks/229-dynamic-prompt-management/ Prerequisites: plan.md , spec.md , research.md , data-model.md , contracts/prompts.yaml , quickstart.md Branch: 229-dynamic-prompt-management

Format: [ID] [P?] [Story?] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [US#]: User Story mapping (US1 = Version Management, US2 = Sandbox Testing, US3 = Runtime Resolution)

Phase 1: Setup (Shared Infrastructure)

Purpose: Schema, entity, and module scaffolding that all user stories depend on

  • T001 Read hardcoded prompt from backend/src/modules/ai/processors/ai-batch.processor.ts (both processSandboxExtract and processMigrateDocument) and capture exact template text for seed data
  • T002 Create SQL delta specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sql with CREATE TABLE ai_prompts + seed data INSERT (use exact template from T001)
  • T002b [P] Create rollback delta specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.rollback.sql with DROP TABLE IF EXISTS ai_prompts — follows existing delta rollback convention
  • T003 [P] Create TypeORM entity backend/src/modules/ai/prompts/ai-prompts.entity.ts per data-model.md (INT PK with @Exclude(), all columns, no publicId needed)
  • T004 [P] Create backend/src/modules/ai/prompts/dto/create-ai-prompt.dto.ts with template: string (class-validator @IsNotEmpty())
  • T005 [P] Create backend/src/modules/ai/prompts/dto/update-prompt-note.dto.ts with manualNote: string | null
  • T006 [P] Create backend/src/modules/ai/prompts/dto/ai-prompt-response.dto.ts with @Expose() fields per data-model.md API Response Shape

Checkpoint: Schema applied, entity and DTOs compile — T007 can begin


Phase 2: Foundational (Blocking Backend Prerequisites)

Purpose: Core service and module wiring — MUST complete before US1 frontend or US3 processor work

⚠️ CRITICAL: All user story implementation depends on this phase

  • T007 Create backend/src/modules/ai/prompts/ai-prompts.service.ts with:
    • findAll(promptType: string): Promise<AiPrompt[]> — ORDER BY version_number DESC
    • getActive(promptType: string): Promise<AiPrompt | null> — Redis cache first, DB fallback
    • create(promptType, dto, userId): Promise<AiPrompt> — validate {{ocr_text}} present (FR-002); validate template.length <= 4000 (FR-015, reject with ValidationException); assign MAX(version_number)+1 FOR UPDATE
    • activate(promptType, versionNumber, userId): Promise<AiPrompt> — transaction: SELECT id FROM ai_prompts WHERE prompt_type=? AND is_active=1 FOR UPDATE first (serializes concurrent activations) → deactivate old → activate new → COMMIT → Redis DEL + audit_logs
    • delete(promptType, versionNumber, userId): Promise<void> — guard active version + audit_logs
    • updateNote(promptType, versionNumber, note): Promise<AiPrompt> — PATCH manual_note only
    • saveTestResult(promptType, versionNumber, resultJson): Promise<void> — auto-save from sandbox
  • T008 Create backend/src/modules/ai/prompts/ai-prompts.controller.ts — 5 endpoints per contracts/prompts.yaml with @UseGuards(JwtAuthGuard, CaslAbilityGuard) + @Audit() on mutating endpoints
  • T009 Create backend/src/modules/ai/prompts/ai-prompts.module.ts — import TypeOrmModule.forFeature([AiPrompt]), RedisModule, export AiPromptsService
  • T010 Register AiPromptsModule in backend/src/modules/ai/ai.module.ts imports array
  • T011 Add AiPrompt entity to backend/src/database/ or TypeORM config entities array so it is picked up by the DB connection

Checkpoint: pnpm --filter backend build passes; GET /api/ai/prompts/ocr_extraction returns seed data — US1 frontend and US3 processor can proceed in parallel


Phase 3: User Story 1 — Prompt Version Management UI (Priority: P1) 🎯 MVP

Goal: Superadmin จัดการ Prompt Versions ได้ผ่าน AI Admin Console (ไม่ต้อง upload PDF)

Independent Test: เปิด AI Admin Console → OCR Sandbox tab → เห็น Version History, สร้าง version ใหม่, activate, ลบ inactive version ได้

Implementation for User Story 1

  • T012 [P] [US1] Create frontend/types/ai-prompts.tsAiPrompt interface พร้อม promptType, versionNumber, template, isActive, testResultJson, manualNote, lastTestedAt, activatedAt, createdAt; and SandboxResult interface พร้อม promptVersionUsed: number, answer: string, completedAt: string
  • T013 [P] [US1] Create frontend/lib/services/ai-prompts.service.tslistVersions(promptType), createVersion(promptType, template), activateVersion(promptType, versionNumber), deleteVersion(promptType, versionNumber), updateNote(promptType, versionNumber, note) — calls /api/ai/prompts/... via apiClient
  • T014 [US1] Create frontend/hooks/use-ai-prompts.ts — TanStack Query useQuery (listVersions) + useMutation (create, activate, delete, updateNote) with invalidateQueries on success
  • T015 [US1] Create frontend/components/admin/ai/PromptVersionHistory.tsx — Version list panel ทางขวา แสดง version_number, is_active badge (), last_tested_at, buttons: Load / Activate / Delete (Delete ปิดถ้า isActive)
  • T016 [US1] Create frontend/components/admin/ai/OcrSandboxPromptManager.tsx — 2-column layout: left (Prompt Editor textarea + "บันทึก Version ใหม่" button) + right (PromptVersionHistory) — โหลด active version template เข้า textarea เมื่อ mount
  • T017 [US1] Wire OcrSandboxPromptManager into existing AI Admin Console OCR Sandbox tab (replace or extend existing component in frontend/app/(admin)/admin/...)
  • T018 [P] [US1] Add i18n keys to frontend/public/locales/th/ai-admin.json and frontend/public/locales/en/ai-admin.json — keys: prompt.saveVersion, prompt.activate, prompt.delete, prompt.load, prompt.activeLabel, prompt.placeholderError, prompt.deleteActiveError

Checkpoint: US1 fully functional — version list, create, activate, delete work in UI without PDF upload


Phase 4: User Story 2 — OCR Sandbox Testing with Prompt Evaluation (Priority: P2)

Goal: Superadmin upload PDF → ทดสอบ active prompt → ผลลัพธ์ auto-save ลง active version

Independent Test: upload PDF → รัน sandbox → เห็นผล 8 fields; test_result_json updated; manual note saved

Implementation for User Story 2

  • T019 [US2] Extend OcrSandboxPromptManager.tsx — เพิ่ม File Upload section (PDF only) + "เริ่มทำ OCR Sandbox" button + ผลลัพธ์ JSON display panel + "บันทึก Manual Note" field (textarea + button)
  • T020 [US2] Add runSandbox(promptType, file) to frontend/lib/services/ai-prompts.service.ts — calls existing sandbox endpoint (POST /api/ai/ocr-sandbox or existing BullMQ trigger endpoint) passing PDF file
  • T021 [US2] Add useSandboxRun mutation to frontend/hooks/use-ai-prompts.ts — triggers sandbox run, polls for result, updates version list on completion (to reflect new testResultJson); read promptVersionUsed from job result and display as "ผลจาก Prompt Version X" badge in the result panel
  • T022 [US2] Add i18n keys: prompt.runSandbox, prompt.sandboxResult, prompt.saveNote, prompt.noActivePrompt, prompt.timeoutInfo, prompt.versionUsed to both locale files

Checkpoint: US2 fully functional — upload PDF, run sandbox, see results, save note


Phase 5: User Story 3 — Runtime Prompt Resolution in Processor (Priority: P3)

Goal: processSandboxExtract + processMigrateDocument ใช้ resolvePrompt() จาก DB — ไม่มี hardcoded prompt

Independent Test: activate version 2 → trigger sandbox job → log shows "Using prompt version 2" — no hardcoded string in processor

Implementation for User Story 3

  • T023 [US3] Inject AiPromptsService into backend/src/modules/ai/processors/ai-batch.processor.ts constructor
  • T024 [US3] Add resolveActive(promptType: string, ocrText: string): Promise<{ resolvedPrompt: string; versionNumber: number }> method to AiPromptsService — calls getActive(promptType), replaces {{ocr_text}} with ocrText; if no active prompt: throw BusinessException('No active prompt for type: ocr_extraction') → caller (processor) lets it propagate → BullMQ marks job failed (fail-fast; do NOT catch-and-skip as that creates silent data gaps); returns both resolved string and versionNumber on success (FR-005)
  • T025 [US3] Replace hardcoded prompt string in processSandboxExtract with const { resolvedPrompt, versionNumber } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text) — pass resolvedPrompt to OllamaService.generate() with timeoutMs: 120000 (fix AI_TIMEOUT_MS bug); carry versionNumber forward to T027; add promptVersionUsed: versionNumber to the completed Redis result shape so frontend can display which version produced the result
  • T026 [US3] Replace hardcoded prompt string in processMigrateDocument with const { resolvedPrompt } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text) — pass resolvedPrompt to OllamaService.generate(); keep existing timeout (no change); also fix pre-existing bug: add discipline?: string to MigrateDocumentMetadata interface and add discipline: readString(source.discipline) to parseMigrateDocumentMetadata() (see data-model.md "Pre-existing Bug"); NOTE: BullMQ job-level timeout is adequate for batch workloads — no change needed per ADR-029 D3
  • T027 [US3] In processSandboxExtractversionNumber is captured at job start via resolveActive() (T025); after run completes call this.aiPromptsService.saveTestResult('ocr_extraction', versionNumber, resultJson) — no re-query needed (FR-005 race condition already guarded)

Checkpoint: US3 complete — pnpm --filter backend build passes; no hardcoded prompt strings in processor; resolvePrompt() uses DB/Redis


Phase 6: Polish & Cross-Cutting Concerns

Purpose: Seed data verification, error boundary, tests

  • T028 [P] Write unit tests for AiPromptsService in backend/src/modules/ai/prompts/ai-prompts.service.spec.ts:
    • create(): rejects template without {{ocr_text}}; assigns correct version_number
    • activate(): deactivates old version; invalidates Redis cache; calls AuditLogService (or @Audit() decorator) — verifies audit record created (FR-013)
    • delete(): throws BusinessException when deleting active version; calls AuditLogService on successful delete (FR-013)
    • delete(): succeeds for inactive version
    • getActive(): returns from Redis cache when cache hit
    • getActive(): falls back to DB query when Redis unavailable (mock Redis to throw connection error)
  • T029 [P] Verify SQL delta syntax — run against a local MariaDB copy and confirm seed data is present with correct is_active = 1
  • T030 [P] Run pnpm --filter backend lint and pnpm --filter frontend lint — fix any issues in new files
  • T031 Run quickstart.md acceptance checklist end-to-end and confirm all checkboxes pass
  • T032 Update CONTEXT.md "System readiness summary" table — change ADR-029 row status from "🟡 ADR Accepted" to " พร้อม" (if applicable)

Dependencies & Execution Order

Phase Dependencies

  • Phase 1 (Setup): No dependencies — start immediately; T002b/T003/T004/T005/T006 parallel after T002
  • Phase 2 (Foundational): Depends on T002 (SQL delta) + T003/T004/T005/T006; BLOCKS all user stories
  • Phase 3 (US1): Depends on Phase 2 complete; T012/T013 parallel with T014
  • Phase 4 (US2): Depends on Phase 2 + most of Phase 3 (needs OcrSandboxPromptManager base)
  • Phase 5 (US3): Depends on Phase 2 (AiPromptsService); can run in parallel with Phases 3 & 4
  • Phase 6 (Polish): Depends on Phases 3, 4, 5 complete

User Story Dependencies

  • US1 (P1): Needs Phase 2 complete — independently testable
  • US2 (P2): Needs Phase 2 + US1 UI base — builds on US1 component
  • US3 (P3): Needs Phase 2 only — fully independent from US1/US2 frontend; can be done in parallel

Within Each Phase

  • Phase 1: T001 → T002 (sequential, T001 informs seed data); T002b/T003/T004/T005/T006 parallel after T002
  • Phase 2: T007 first → T008 (needs service) → T009 → T010/T011 parallel
  • Phase 3: T012/T013 parallel → T014 → T015 → T016 → T017; T018 anytime
  • Phase 4: T019 → T020 → T021; T022 anytime
  • Phase 5: T023 → T024 → T025 → T026 → T027 (sequential — each builds on previous)

Parallel Example: Phase 2 Backend + Phase 5 Processor

# Once Phase 1 complete, these can run in parallel:

# Developer A — Phase 2 (backend service/controller/module)
T007: AiPromptsService
T008: AiPromptsController
T009: AiPromptsModule
T010: Register in AiModule

# Developer B — Phase 5 (processor changes)
# NOTE: Developer B needs AiPromptsService injectable (T007 done)
# so Phase 5 starts after T007 completes
T023: Inject AiPromptsService
T024: resolvePrompt() method
T025: fix sandbox timeout + replace hardcoded prompt
T026: replace migrate-document hardcoded prompt
T027: auto-save test_result_json

Implementation Strategy

MVP First (User Story 1 — Version Management)

  1. Complete Phase 1: Setup (T001T006)
  2. Complete Phase 2: Foundational (T007T011)
  3. Complete Phase 3: US1 (T012T018)
  4. STOP and VALIDATE: Admin can manage prompt versions without PDF upload
  5. Deploy/demo US1 independently

Incremental Delivery

  1. Phase 1 + 2 → Foundation ready
  2. Phase 3 (US1) → Admin can manage versions (MVP delivered)
  3. Phase 5 (US3) in parallel with Phase 4 (US2) → Runtime resolution + Sandbox testing
  4. Phase 6 → Polish and release

Notes

  • T001 is critical: Must read exact hardcoded prompt before deleting it — seed data depends on it
  • T025 + T026: The hardcoded prompt removal is the "100% DB-driven" success criterion (SC-005)
  • T028: Service unit tests should mock Redis to test cache hit + fallback scenarios
  • [P] tasks: T003/T004/T005/T006 can all run in parallel (separate files)
  • Avoid: modifying ai-batch.processor.ts (T025/T026) before AiPromptsService (T007) is injectable