14 KiB
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(bothprocessSandboxExtractandprocessMigrateDocument) and capture exact template text for seed data - T002 Create SQL delta
specs/03-Data-and-Storage/deltas/2026-05-25-create-ai-prompts.sqlwithCREATE 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.sqlwithDROP TABLE IF EXISTS ai_prompts— follows existing delta rollback convention - T003 [P] Create TypeORM entity
backend/src/modules/ai/prompts/ai-prompts.entity.tsper 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.tswithtemplate: string(class-validator@IsNotEmpty()) - T005 [P] Create
backend/src/modules/ai/prompts/dto/update-prompt-note.dto.tswithmanualNote: string | null - T006 [P] Create
backend/src/modules/ai/prompts/dto/ai-prompt-response.dto.tswith@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.tswith:findAll(promptType: string): Promise<AiPrompt[]>— ORDER BY version_number DESCgetActive(promptType: string): Promise<AiPrompt | null>— Redis cache first, DB fallbackcreate(promptType, dto, userId): Promise<AiPrompt>— validate{{ocr_text}}present (FR-002); validatetemplate.length <= 4000(FR-015, reject withValidationException); assignMAX(version_number)+1 FOR UPDATEactivate(promptType, versionNumber, userId): Promise<AiPrompt>— transaction:SELECT id FROM ai_prompts WHERE prompt_type=? AND is_active=1 FOR UPDATEfirst (serializes concurrent activations) → deactivate old → activate new → COMMIT → Redis DEL + audit_logsdelete(promptType, versionNumber, userId): Promise<void>— guard active version + audit_logsupdateNote(promptType, versionNumber, note): Promise<AiPrompt>— PATCH manual_note onlysaveTestResult(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— importTypeOrmModule.forFeature([AiPrompt]),RedisModule, exportAiPromptsService - T010 Register
AiPromptsModuleinbackend/src/modules/ai/ai.module.tsimports array - T011 Add
AiPromptentity tobackend/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.ts—AiPromptinterface พร้อมpromptType,versionNumber,template,isActive,testResultJson,manualNote,lastTestedAt,activatedAt,createdAt; andSandboxResultinterface พร้อมpromptVersionUsed: number,answer: string,completedAt: string - T013 [P] [US1] Create
frontend/lib/services/ai-prompts.service.ts—listVersions(promptType),createVersion(promptType, template),activateVersion(promptType, versionNumber),deleteVersion(promptType, versionNumber),updateNote(promptType, versionNumber, note)— calls/api/ai/prompts/...viaapiClient - T014 [US1] Create
frontend/hooks/use-ai-prompts.ts— TanStack QueryuseQuery(listVersions) +useMutation(create, activate, delete, updateNote) withinvalidateQuerieson 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
OcrSandboxPromptManagerinto existing AI Admin Console OCR Sandbox tab (replace or extend existing component infrontend/app/(admin)/admin/...) - T018 [P] [US1] Add i18n keys to
frontend/public/locales/th/ai-admin.jsonandfrontend/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)tofrontend/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
useSandboxRunmutation tofrontend/hooks/use-ai-prompts.ts— triggers sandbox run, polls for result, updates version list on completion (to reflect newtestResultJson); readpromptVersionUsedfrom 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.versionUsedto 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
AiPromptsServiceintobackend/src/modules/ai/processors/ai-batch.processor.tsconstructor - T024 [US3] Add
resolveActive(promptType: string, ocrText: string): Promise<{ resolvedPrompt: string; versionNumber: number }>method toAiPromptsService— callsgetActive(promptType), replaces{{ocr_text}}withocrText; if no active prompt: throwBusinessException('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
processSandboxExtractwithconst { resolvedPrompt, versionNumber } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text)— passresolvedPrompttoOllamaService.generate()withtimeoutMs: 120000(fix AI_TIMEOUT_MS bug); carryversionNumberforward to T027; addpromptVersionUsed: versionNumberto the completed Redis result shape so frontend can display which version produced the result - T026 [US3] Replace hardcoded prompt string in
processMigrateDocumentwithconst { resolvedPrompt } = await this.aiPromptsService.resolveActive('ocr_extraction', ocrResult.text)— passresolvedPrompttoOllamaService.generate(); keep existing timeout (no change); also fix pre-existing bug: adddiscipline?: stringtoMigrateDocumentMetadatainterface and adddiscipline: readString(source.discipline)toparseMigrateDocumentMetadata()(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
processSandboxExtract—versionNumberis captured at job start viaresolveActive()(T025); after run completes callthis.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
AiPromptsServiceinbackend/src/modules/ai/prompts/ai-prompts.service.spec.ts:create(): rejects template without{{ocr_text}}; assigns correct version_numberactivate(): deactivates old version; invalidates Redis cache; callsAuditLogService(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 versiongetActive(): returns from Redis cache when cache hitgetActive(): 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 lintandpnpm --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
OcrSandboxPromptManagerbase) - 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)
- Complete Phase 1: Setup (T001–T006)
- Complete Phase 2: Foundational (T007–T011)
- Complete Phase 3: US1 (T012–T018)
- STOP and VALIDATE: Admin can manage prompt versions without PDF upload
- Deploy/demo US1 independently
Incremental Delivery
- Phase 1 + 2 → Foundation ready
- Phase 3 (US1) → Admin can manage versions (MVP delivered)
- Phase 5 (US3) in parallel with Phase 4 (US2) → Runtime resolution + Sandbox testing
- 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) beforeAiPromptsService(T007) is injectable