17 KiB
Feature Specification: Dynamic Prompt Management for OCR Extraction
Feature Branch: 229-dynamic-prompt-management
Created: 2026-05-25
Status: Draft
ADR Reference: ADR-029
Input: Dynamic, runtime-editable OCR extraction prompts shared by OCR Sandbox and Migration processor — replaces hardcoded prompts, adds versioning, timeout bug fix, Redis caching.
User Scenarios & Testing (mandatory)
User Story 1 - Prompt Version Management (Priority: P1)
Superadmin สามารถดูรายการ Prompt Versions ทั้งหมด, สร้าง Prompt Version ใหม่, activate version ที่ต้องการให้ระบบใช้จริง และลบ version ที่ไม่ต้องการ (ยกเว้น active version) ผ่าน AI Admin Console
Why this priority: นี่คือรากฐานของ feature ทั้งหมด — หากไม่มีโครงสร้างการจัดการ Prompt Version, ระบบยังคงใช้ prompt แบบ hardcoded และไม่สามารถแก้ไข runtime ได้
Independent Test: เปิด AI Admin Console → OCR Sandbox tab → จัดการ Prompt Versions ได้โดยไม่ต้อง upload PDF ใดๆ
Acceptance Scenarios:
- Given มี Prompt Versions ใน DB, When superadmin เปิด OCR Sandbox tab, Then เห็น Version History panel ทางขวามือพร้อม active version ที่มีเครื่องหมาย ✅
- Given superadmin กรอก Prompt Template ที่มี
{{ocr_text}}placeholder, When กด "บันทึก Version ใหม่", Then สร้าง version ใหม่ (inactive) และปรากฏใน Version History - Given superadmin กรอก Prompt Template ที่ไม่มี
{{ocr_text}}, When กด "บันทึก Version ใหม่", Then ระบบแสดง error "template ต้องมี {{ocr_text}} placeholder" และไม่บันทึก - Given มี inactive version, When superadmin กด "Activate", Then version นั้นกลายเป็น active, version เดิมกลายเป็น inactive, Redis cache ถูก invalidate ทันที
- Given มี active version, When superadmin พยายามกด Delete บน active version, Then ระบบปฏิเสธพร้อมข้อความ "ไม่สามารถลบ active version ได้"
- Given มี inactive version, When superadmin กด Delete, Then version ถูกลบออกจาก DB และหายจาก Version History
- Given superadmin กด Load บน version ใดๆ, When version โหลด, Then template ของ version นั้นปรากฏใน Prompt Editor textarea (ไม่ activate อัตโนมัติ)
User Story 2 - OCR Sandbox Testing with Prompt Evaluation (Priority: P2)
Superadmin สามารถ upload PDF และทดสอบ OCR + LLM extraction ด้วย Active Prompt ปัจจุบัน เพื่อประเมินคุณภาพผลลัพธ์ก่อนใช้งานจริงกับ Migration batch โดยผลลัพธ์ auto-save ลง active version และรองรับ Manual Note annotation
Why this priority: Sandbox testing ช่วยให้ admin ตรวจสอบและเปรียบเทียบ prompt ก่อน activate จริง — ลดความเสี่ยงที่ prompt ไม่ดีจะกระทบ Migration batch
Independent Test: upload PDF → กด "เริ่มทำ OCR Sandbox" → เห็นผล JSON ครบ 8 fields และผลลัพธ์ auto-save ลง active version
Acceptance Scenarios:
- Given มี Active Prompt, When superadmin upload PDF และกด "เริ่มทำ OCR Sandbox", Then ระบบรัน OCR + LLM extraction ด้วย active prompt และแสดงผล JSON ที่มี 8 fields (documentNumber, subject, discipline, date, confidence, category, tags, summary)
- Given sandbox run เสร็จสิ้น, When ผลลัพธ์ JSON ออกมา, Then ผลลัพธ์ auto-save ลงใน active version's
test_result_jsonและ updatelast_tested_atอัตโนมัติ - Given ผลลัพธ์ sandbox ปรากฏ, When superadmin พิมพ์ manual note และกด "บันทึก Manual Note", Then note ถูกบันทึกลงใน
manual_notefield ของ active version - Given OCR Sandbox ถูก trigger และ Ollama ต้องโหลด model ใหม่ (cold start), When ระบบรอผล, Then ระบบรอได้นานถึง 120 วินาที ไม่ timeout ก่อนกำหนด (แก้ bug จาก AI_TIMEOUT_MS = 30s)
- Given ไม่มี Active Prompt, When superadmin กด "เริ่มทำ OCR Sandbox", Then ระบบแสดง error ว่าไม่พบ active prompt และไม่รัน sandbox
User Story 3 - Runtime Prompt Resolution (Priority: P3)
ระบบ (processor layer) สามารถดึง Active Prompt จาก ai_prompts table ผ่าน Redis cache (TTL 60s) และใช้ใน processSandboxExtract กับ processMigrateDocument ได้โดยทั้งสองใช้ prompt เดียวกัน — ไม่มี hardcoded prompt ใน codebase
Why this priority: Backend plumbing ที่ทำให้ US1 และ US2 มีผลจริงในระบบ Production — ถ้าไม่มี US3, prompt ที่ admin set ผ่าน UI ไม่มีผลต่อ processor
Independent Test: activate prompt version ใหม่ → trigger sandbox job → ตรวจสอบ log ว่า job ใช้ prompt version ใหม่
Acceptance Scenarios:
- Given มี Active Prompt ใน DB, When processor เรียก
resolvePrompt('ocr_extraction', ocrText), Then ได้รับ resolved prompt ที่แทนที่{{ocr_text}}ด้วย OCR text จริง - Given Active Prompt ถูก cache ใน Redis TTL 60s, When processor เรียก
resolvePrompt()ภายใน 60s, Then ได้รับ prompt จาก Redis cache (ไม่ query DB ซ้ำ) - Given admin activate version ใหม่ (cache invalidated), When processor เรียก
resolvePrompt()ครั้งถัดไป, Then ได้รับ prompt version ล่าสุดจาก DB และ cache ถูก refresh - Given ไม่มี Active Prompt ใน DB (เช่น deploy ใหม่ก่อน seed), When processor พยายาม
resolveActive(), ThenBusinessExceptionthrow → BullMQ mark job failed →migrationService.createError()บันทึก error → batch หยุด (fail-fast — ไม่ใช้ skip เพราะทำให้เกิด silent data gap)
Edge Cases
- อะไรเกิดขึ้นถ้า admin สองคน activate พร้อมกัน? →
activate()ต้องใช้SELECT ... FOR UPDATEเพื่อ lock current active row ก่อน deactivate — serializes concurrent activations ต่อprompt_type; admin คนที่สองจะ block จนกว่าคนแรก COMMIT — ไม่มี double-active - อะไรเกิดขึ้นถ้า admin activate version ขณะที่ Migration batch กำลังรัน? → job ที่กำลัง run ใช้ prompt ที่ดึงมาแล้ว (per-job resolution); job ถัดไปจะใช้ prompt ใหม่ — acceptable tradeoff
- อะไรเกิดขึ้นถ้า Redis ล่มขณะที่ processor เรียก
resolvePrompt()? → fallback query DB โดยตรง; ระบบยังทำงานได้ แต่ performance ลด - อะไรเกิดขึ้นถ้า template ยาวเกิน 4,000 ตัวอักษร? →
create()reject ด้วยValidationException('Template exceeds 4,000 character limit')— ป้องกัน context window overflow ใน Ollama (FR-015) - อะไรเกิดขึ้นถ้า PDF ที่ upload ใน sandbox ไม่มี text (scanned image)? → OCR Service รัน PaddleOCR ตาม existing flow; ไม่ใช่ scope ของ feature นี้
- อะไรเกิดขึ้นถ้า Ollama timeout แม้จะตั้ง 120s? → sandbox job fail พร้อม error message; ไม่กระทบ Migration jobs อื่น
- อะไรเกิดขึ้นถ้า version 1 (seed data) ถูกลบก่อนที่จะมี version อื่น active? → ป้องกันได้ด้วย guard "ห้ามลบ active version"
- อะไรเกิดขึ้นถ้าผลลัพธ์ JSON จาก sandbox ไม่ครบ 8 fields? → save ทุกอย่างที่ได้ใน
test_result_json, UI แสดงตามที่มี - อะไรเกิดขึ้นถ้า admin ลบ v2 แล้วสร้าง version ใหม่ → version number กลายเป็น v4 ข้าม v2? → by design (monotonically increasing, ไม่ fill gaps per plan.md D6); UI MUST แสดง version numbers ตามที่เป็น ไม่ reindex — ป้องกัน admin สับสน "v2 หายไปไหน?"
- อะไรเกิดขึ้นถ้า processor crash หลัง Ollama return แต่ก่อน
saveTestResult()รัน? →test_result_jsonยังเป็นNULL(เหมือนยังไม่ทดสอบ) — acceptable; admin รัน sandbox ใหม่ได้ทันที; ไม่กระทบ Active Prompt หรือ migration batch
Requirements (mandatory)
Functional Requirements
- FR-001: ระบบ MUST บันทึก Prompt Template เป็น versioned records ใน
ai_promptstable — ทุกการบันทึกสร้าง version ใหม่เสมอ (immutable; ไม่มี update template เดิม) - FR-002: ระบบ MUST validate ว่า template มี
{{ocr_text}}placeholder ก่อน save — reject พร้อม user-friendly error message ถ้าไม่มี - FR-003: ระบบ MUST enforce single active version ต่อ
prompt_type— activate version ใหม่จะ deactivate version เดิมอัตโนมัติใน transaction เดียว - FR-004: ระบบ MUST ห้ามลบ active version — แสดง error ถ้าพยายามลบ active version
- FR-005: ระบบ MUST auto-save
test_result_jsonและ updatelast_tested_atลงใน Prompt Version ที่ active ณ เวลาที่ job เริ่มทำงาน (ไม่ใช่เวลาที่ result กลับมา) — กัน race condition ที่ admin activate version อื่นระหว่างที่ sandbox กำลังรันอยู่ - FR-006: ระบบ MUST รองรับ
manual_noteannotation จาก admin ต่อ Prompt Version ผ่าน PATCH endpoint - FR-007: ระบบ MUST invalidate Redis cache (
ai:prompt:active:ocr_extraction) ทันทีหลัง activate สำเร็จ - FR-008:
processSandboxExtractMUST ใช้ timeout 120000ms เพื่อรองรับ Ollama cold start (แก้ bug AI_TIMEOUT_MS = 30000ms) - FR-009: ทั้ง
processSandboxExtractและprocessMigrateDocumentMUST ใช้resolvePrompt()method เดียวกัน — ไม่มี hardcoded prompt ใน processor - FR-010: API endpoints ทั้งหมดสำหรับ Prompt Management MUST ป้องกันด้วย
system.manage_allCASL permission - FR-011: ระบบ MUST มี seed data (Prompt Version 1 ที่ migrate จาก hardcoded prompt ปัจจุบัน พร้อม
is_active = 1) ก่อน deploy - FR-012: Redis cache MUST fallback ไป DB query ถ้า Redis ไม่พร้อมใช้งาน (graceful degradation)
- FR-013: Prompt activation, creation, deletion events MUST be recorded in standard
audit_logstable (ไม่ใช่ai_audit_logs) - FR-014:
GET /ai/prompts/:typeMUST return all versions สำหรับ prompt_type นั้น (ไม่ paginate ใน v1) - FR-015:
templateMUST NOT exceed 4,000 characters — รักษา headroom ใน context window 8192 tokens ของ gemma4:e4b เพื่อให้ OCR text มีที่เหลือ — reject พร้อม user-friendly error ถ้าเกิน
Key Entities
- Prompt Version (
ai_prompts): Immutable snapshot ของ prompt template — มีprompt_type,version_number,template,is_active,test_result_json,manual_note,last_tested_at,activated_at,created_by - Active Prompt: Prompt Version ที่
is_active = 1ต่อprompt_type— cached ใน Redis keyai:prompt:active:{prompt_type}TTL 60s - Prompt Template: String ที่มี
{{ocr_text}}placeholder บังคับ — resolved เป็น final prompt โดย processor ก่อนส่งเข้า Ollama
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: Superadmin สามารถสร้าง, activate, และลบ Prompt Version ได้ภายใน 30 วินาที ต่อการดำเนินการหนึ่งครั้ง
- SC-002: OCR Sandbox รันได้สำเร็จสำหรับ PDF ที่ถูก upload โดยไม่ timeout ก่อน 120 วินาที
- SC-003: Processor ดึง Active Prompt จาก Redis cache ภายใน 5ms ในช่วง TTL 60s (ไม่ query DB ซ้ำ)
- SC-004: หลัง activate Prompt Version ใหม่, jobs ถัดไปทั้งหมดใช้ prompt version ใหม่ภายใน 60 วินาที (Redis TTL expiry)
- SC-005: ไม่มี hardcoded prompt template ใน codebase หลังจาก feature นี้ deploy — 100% DB-driven
- SC-006: Version History แสดง Prompt Versions ทั้งหมดพร้อม status (active/inactive) และ last_tested_at ได้อย่างถูกต้อง
Clarifications
Session 2026-05-25
- Q: Should prompt activation and sandbox run events be recorded in
ai_audit_logsor standardaudit_logs? → A: Standardaudit_logs— these are admin config actions, not AI inference results; keepsAiPromptsModuledecoupled fromAiAuditLogModule - Q: Should
GET /ai/prompts/:typeuse pagination? → A: Return all versions (no pagination in v1) — prompt versions are expected to be low-count (single digits to low tens); simplifies UI implementation
Assumptions
- Scope จำกัดที่
prompt_type = 'ocr_extraction'เดียว (8 fields: documentNumber, subject, discipline, date, confidence, category, tags, summary) ตาม ADR-029 core decisions - Admin ที่ใช้ feature นี้คือ Superadmin ที่มี
system.manage_allpermission เท่านั้น - OCR Service (PaddleOCR sidecar บน Desk-5439) ยังคงทำงานเหมือนเดิม — feature นี้เปลี่ยนเฉพาะ LLM prompt หลัง OCR
- Seed data (version 1 ที่ migrate จาก hardcoded prompt) ต้องถูก insert ก่อน first deploy ผ่าน SQL delta (ADR-009)
- Redis พร้อมใช้งาน; ถ้า Redis ล่ม ระบบ graceful degrade ไป DB query
- Existing
AiController/AiModuleสามารถ extend ได้โดยไม่ต้อง refactor โครงสร้างหลัก field_schema JSON NULLcolumn ในai_promptsเป็น system-managed metadata (ไม่ user-editable ใน v1) — ระบุ expected output fields สำหรับ validation- Timeout fix ใช้กับ
processSandboxExtractเท่านั้น;processMigrateDocumentใช้ default timeout ของ BullMQ job (queue-level timeout ต่างกัน)