Files
admin 1139e54086
CI / CD Pipeline / build (push) Successful in 4m29s
CI / CD Pipeline / deploy (push) Successful in 1m50s
690525:1720 ADR-028-228-migration-OCR #06 dynamic prompt
2026-05-25 17:20:48 +07:00

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:

  1. Given มี Prompt Versions ใน DB, When superadmin เปิด OCR Sandbox tab, Then เห็น Version History panel ทางขวามือพร้อม active version ที่มีเครื่องหมาย
  2. Given superadmin กรอก Prompt Template ที่มี {{ocr_text}} placeholder, When กด "บันทึก Version ใหม่", Then สร้าง version ใหม่ (inactive) และปรากฏใน Version History
  3. Given superadmin กรอก Prompt Template ที่ไม่มี {{ocr_text}}, When กด "บันทึก Version ใหม่", Then ระบบแสดง error "template ต้องมี {{ocr_text}} placeholder" และไม่บันทึก
  4. Given มี inactive version, When superadmin กด "Activate", Then version นั้นกลายเป็น active, version เดิมกลายเป็น inactive, Redis cache ถูก invalidate ทันที
  5. Given มี active version, When superadmin พยายามกด Delete บน active version, Then ระบบปฏิเสธพร้อมข้อความ "ไม่สามารถลบ active version ได้"
  6. Given มี inactive version, When superadmin กด Delete, Then version ถูกลบออกจาก DB และหายจาก Version History
  7. 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:

  1. 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)
  2. Given sandbox run เสร็จสิ้น, When ผลลัพธ์ JSON ออกมา, Then ผลลัพธ์ auto-save ลงใน active version's test_result_json และ update last_tested_at อัตโนมัติ
  3. Given ผลลัพธ์ sandbox ปรากฏ, When superadmin พิมพ์ manual note และกด "บันทึก Manual Note", Then note ถูกบันทึกลงใน manual_note field ของ active version
  4. Given OCR Sandbox ถูก trigger และ Ollama ต้องโหลด model ใหม่ (cold start), When ระบบรอผล, Then ระบบรอได้นานถึง 120 วินาที ไม่ timeout ก่อนกำหนด (แก้ bug จาก AI_TIMEOUT_MS = 30s)
  5. 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:

  1. Given มี Active Prompt ใน DB, When processor เรียก resolvePrompt('ocr_extraction', ocrText), Then ได้รับ resolved prompt ที่แทนที่ {{ocr_text}} ด้วย OCR text จริง
  2. Given Active Prompt ถูก cache ใน Redis TTL 60s, When processor เรียก resolvePrompt() ภายใน 60s, Then ได้รับ prompt จาก Redis cache (ไม่ query DB ซ้ำ)
  3. Given admin activate version ใหม่ (cache invalidated), When processor เรียก resolvePrompt() ครั้งถัดไป, Then ได้รับ prompt version ล่าสุดจาก DB และ cache ถูก refresh
  4. Given ไม่มี Active Prompt ใน DB (เช่น deploy ใหม่ก่อน seed), When processor พยายาม resolveActive(), Then BusinessException throw → BullMQ mark job failedmigrationService.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_prompts table — ทุกการบันทึกสร้าง 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 และ update last_tested_at ลงใน Prompt Version ที่ active ณ เวลาที่ job เริ่มทำงาน (ไม่ใช่เวลาที่ result กลับมา) — กัน race condition ที่ admin activate version อื่นระหว่างที่ sandbox กำลังรันอยู่
  • FR-006: ระบบ MUST รองรับ manual_note annotation จาก admin ต่อ Prompt Version ผ่าน PATCH endpoint
  • FR-007: ระบบ MUST invalidate Redis cache (ai:prompt:active:ocr_extraction) ทันทีหลัง activate สำเร็จ
  • FR-008: processSandboxExtract MUST ใช้ timeout 120000ms เพื่อรองรับ Ollama cold start (แก้ bug AI_TIMEOUT_MS = 30000ms)
  • FR-009: ทั้ง processSandboxExtract และ processMigrateDocument MUST ใช้ resolvePrompt() method เดียวกัน — ไม่มี hardcoded prompt ใน processor
  • FR-010: API endpoints ทั้งหมดสำหรับ Prompt Management MUST ป้องกันด้วย system.manage_all CASL 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_logs table (ไม่ใช่ ai_audit_logs)
  • FR-014: GET /ai/prompts/:type MUST return all versions สำหรับ prompt_type นั้น (ไม่ paginate ใน v1)
  • FR-015: template MUST 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 key ai: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_logs or standard audit_logs? → A: Standard audit_logs — these are admin config actions, not AI inference results; keeps AiPromptsModule decoupled from AiAuditLogModule
  • Q: Should GET /ai/prompts/:type use 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_all permission เท่านั้น
  • 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 NULL column ใน ai_prompts เป็น system-managed metadata (ไม่ user-editable ใน v1) — ระบุ expected output fields สำหรับ validation
  • Timeout fix ใช้กับ processSandboxExtract เท่านั้น; processMigrateDocument ใช้ default timeout ของ BullMQ job (queue-level timeout ต่างกัน)