690618:1444 237 #02
CI / CD Pipeline / build (push) Successful in 7m5s
CI / CD Pipeline / deploy (push) Failing after 20m14s

This commit is contained in:
2026-06-18 14:44:46 +07:00
parent 037fbb65f5
commit 09e304de84
52 changed files with 4471 additions and 1038 deletions
@@ -0,0 +1,143 @@
# Research Findings: OCR & AI Extraction Prompt Management
**Date**: 2026-06-17
**Feature**: 238-ocr-ai-prompt-separation
## Research Areas
### 1. Concurrent Edit Handling Strategy
**Decision**: ใช้ optimistic locking ด้วย version/timestamp field ใน ai_prompts table
**Rationale**:
- ลด lock contention ใน database - ไม่ block admin คนอื่น
- User experience ดีกว่า - แจ้งเตือนแทนการ block
- ง่ายต่อการ implement กับ TypeORM @VersionColumn
- สอดคล้องกับ pattern ที่ใช้ใน LCBP3-DMS อยู่แล้ว (ADR-002 document numbering)
**Alternatives considered**:
- **Pessimistic locking (SELECT FOR UPDATE)**: ไม่เลือกเพราะอาจ block admin คนอื่นนาน, ไม่เหมาะกับ admin UI
- **Last-write-wins**: ไม่เลือกเพราะเสี่ยงสูญเสียการแก้ไขของ admin คนแรกโดยไม่รู้ตัว
- **Operational Transform (like Google Docs)**: ไม่เลือกเพราะ complex เกินความจำเป็น, prompt editing ไม่ต้องการ real-time collaboration
**⚠️ สถานะของจริง (codebase ปัจจุบัน)**:
- `@VersionColumn({ name: 'version' })` ถูกเพิ่มใน entity **แล้ว** (delta 2026-06-15) — คอลัมน์ DB มีแล้ว
- แต่ `activate()` ของจริง **ใช้ pessimistic_write lock** ใน transaction และ **ไม่รับ `expectedVersion`** จึงยังไม่มี HTTP 409 flow
- `@VersionColumn` ปัจจุบันทำงานตอน `save()` เท่านั้น (ดัก lost update ระหว่าง read→write)
**ถ้าจะทำ flow optimistic 409 ตาม spec ต้องแก้เพิ่ม**:
- แก้ signature `activate(promptType, versionNumber, userId, expectedVersion)` ให้รับ `expectedVersion`
- เทียบ `version` ก่อน save → ถ้าไม่ตรงโยน BusinessException/409 พร้อม current data
- หมายเหตุ: อ้าง ADR-002 จริงๆใช้ Redlock/pessimistic — คำว่า "สอดคล้อง pattern ADR-002" ใน rationale จึงคลาดเคลื่อน
### 2. Sidecar System Prompt Parameter Format
**Decision**: Sidecar รับ systemPrompt ผ่าน multipart/form-data field 'systemPrompt'
**Rationale**:
- สอดคล้องกับรูปแบบที่ sidecar รับ file upload อยู่แล้ว
- ไม่ต้องเปลี่ยน content-type หรือ endpoint structure
- ง่ายต่อการ integrate กับ existing form data
**Alternatives considered**:
- **JSON payload with base64 PDF**: ไม่เลือกเพราะต้องเปลี่ยน endpoint structure มาก, file size limit ของ JSON
- **Separate endpoint**: ไม่เลือกเพราะซับซ้อนเกินไป, ควรรวมอยู่ใน /ocr-upload
**✅ ยืนยันแล้ว (จาก app.py จริง)**: OCR engine ใช้ `prepare_ocr_messages(pdf_path, task_type="structure", page_num=N)` จาก typhoon_ocr ซึ่งคืน messages array ที่มี **user message เดียว** โดย `messages[0]["content"]` เป็น list (image + prompt). โค้ดปัจจุบันที่ `process_ocr()` (app.py:194-203) **inject ข้อความเพิ่มได้สำเร็จอยู่แล้ว** ด้วยการ `messages[0]["content"].append({"type": "text", "text": ...})` (DMS tags) → ใช้ pattern เดียวกันนี้กับ systemPrompt ได้ทันที
**Decision (ปรับให้ตรงของจริง)**: inject systemPrompt ด้วยการ **append text item เข้า `messages[0]["content"]`****ไม่** insert `{"role":"system"}` แยก (typhoon OCR เป็น single-message format; system role แยกยังไม่พิสูจน์ และเสี่ยงกระทบ structured extraction)
**Implementation approach (ยืนยันตาม app.py)**:
```python
# process_ocr() — เพิ่มพารามิเตอร์ system_prompt และ append ก่อน DMS tags
def process_ocr(pdf_path, page_num=1, options_override={}, system_prompt: Optional[str] = None) -> str:
messages = prepare_ocr_messages(pdf_path, task_type="structure", page_num=page_num)
if system_prompt:
messages[0]["content"].append({"type": "text", "text": system_prompt})
# DMS tags injection เดิม (ยังคงไว้)
messages[0]["content"].append({"type": "text", "text": "Additionally: ..."})
# ...payload เดิม → /v1/chat/completions
# /ocr-upload — รับ systemPrompt แล้วส่งต่อ (ต้อง X-API-Key)
@app.post("/ocr-upload", dependencies=[Depends(get_api_key)])
def ocr_upload(file=File(...), engine=Form("auto"), systemPrompt: Optional[str] = Form(None), ...):
# ต้อง thread systemPrompt → _process_pdf_doc(...) → process_ocr(..., system_prompt=systemPrompt)
...
```
> หมายเหตุ: ต้อง thread `systemPrompt` ผ่าน `_process_pdf_doc()` (เพิ่มพารามิเตอร์) ไปยัง `process_ocr()` ด้วย เพราะปัจจุบัน `_process_pdf_doc` ไม่รับ systemPrompt
### 3. Default OCR System Prompt Content
**Decision**: ใช้ hardcoded default minimal system prompt
**Content**: "Extract all text from this PDF page accurately."
**Rationale**:
- Simple and language-agnostic
- ทำงานได้กับทุกประเภทเอกสาร (Thai/English/mixed)
- ไม่มี bias ต่อ specific document type
- Vision model (np-dms-ocr) ถูกฝึกมาให้เข้าใจ instruction แบบนี้อยู่แล้ว
**Alternatives considered**:
- **No default (fail fast)**: ไม่เลือกเพราะจะทำให้ OCR ใช้ไม่ได้ถ้าลืมสร้าง prompt หรือ database error
- **Complex multi-language prompt**: ไม่เลือกเพราะอาจทำให้ model confused, minimal prompt มีประสิทธิภาพดีกว่า
- **Template with placeholders**: ไม่เลือกเพราะ OCR system prompt ไม่มี context อื่นให้ inject
### 4. Placeholder Validation for AI Extraction Prompt
**Decision**: ตรวจสอบ required placeholders ตอน save (backend validation)
**Required placeholders**:
- `{{ocr_text}}` - mandatory (OCR text to extract from)
- `{{master_data_context}}` - optional (project/contract context)
**Rationale**:
- ป้องกัน runtime error เมื่อ prompt ถูกใช้
- ให้ admin รู้ทันทีว่า template ไม่ถูกต้อง
- สอดคล้องกับ ADR-007 error handling strategy
**Validation approach**:
```typescript
validateTemplate(template: string, promptType: string): ValidationResult {
if (promptType === 'ocr_extraction') {
if (!template.includes('{{ocr_text}}')) {
return { valid: false, error: 'Template must include {{ocr_text}} placeholder' };
}
}
// ocr_system has no required placeholders
return { valid: true };
}
```
### 5. AI Prompts Table Schema Compatibility
**Decision**: ใช้ schema ที่มีอยู่แล้วจาก ADR-029, เพิ่มแค่ @VersionColumn
**Existing fields (sufficient)**:
- `prompt_type` (string): รองรับ 'ocr_system', 'ocr_extraction'
- `version_number` (int): Version tracking
- `template` (text): Prompt content
- `context_config` (json): Metadata
- `is_active` (tinyint(1)): Active flag (MariaDB ส่งกลับเป็น 0/1)
- `created_at` (datetime): Timestamp
- `created_by` (int FK → users.user_id): Creator — ไม่ใช่ created_by_public_id
**คอลัมน์จริงที่ต้องระวัง**:
- `version` (int, @VersionColumn): **มีอยู่แล้ว** (delta `2026-06-15-fix-ai-prompts-columns.sql`)
- `created_by` เป็น **INT FK → users(user_id)** — ไม่ใช่ `created_by_public_id`; seed ต้องใช้ `(SELECT user_id FROM users WHERE username='superadmin')`
**SQL delta (idempotent — version มีแล้ว)**:
```sql
ALTER TABLE ai_prompts ADD COLUMN IF NOT EXISTS `version` INT NOT NULL DEFAULT 1;
```
## Summary
ทุก research area มีทางเลือกที่ชัดเจนและสอดคล้องกับ:
1. LCBP3-DMS patterns (optimistic locking, backend validation)
2. ADR-007 error handling strategy
3. ADR-029 dynamic prompt management
4. ADR-037 unified prompt management UX
ไม่มี technical blockers พร้อม proceed ไป Phase 1 design