690603:2041 ADR-034-134 #01
CI / CD Pipeline / build (push) Failing after 4m28s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-06-03 20:41:42 +07:00
parent 754d609399
commit 3274dede7a
197 changed files with 1575 additions and 42 deletions
@@ -0,0 +1,18 @@
-- Rollback: Revert ai_available_models to gemma4 stack (undo ADR-034 delta)
-- Date: 2026-06-03
-- Pair: 2026-06-03-update-ai-available-models-typhoon.sql
-- 1. Remove Typhoon models
DELETE FROM ai_available_models
WHERE model_name IN ('typhoon2.5-np-dms:latest', 'typhoon-np-dms-ocr:latest');
-- 2. Restore gemma4:e2b as default
UPDATE ai_available_models
SET is_default = TRUE, updated_at = NOW()
WHERE model_name = 'gemma4:e2b';
-- 3. Revert system_settings active model
UPDATE system_settings
SET setting_value = 'gemma4:e2b',
updated_at = NOW()
WHERE setting_key = 'AI_ACTIVE_MODEL';
@@ -0,0 +1,49 @@
-- Delta: Update ai_available_models for Thai-Optimized Model Stack (ADR-034)
-- Date: 2026-06-03
-- Author: AI Assistant
-- Related: ADR-034 — Thai-Optimized AI Model Stack, supersedes ADR-023A Section 2.1
-- Rollback: 2026-06-03-update-ai-available-models-typhoon.rollback.sql
-- 1. Insert new main model (typhoon2.5-np-dms) as default, demote old defaults
INSERT INTO ai_available_models (model_name, model_version, description, vram_gb, is_active, is_default)
VALUES (
'typhoon2.5-np-dms:latest',
'latest',
'Thai-optimized main AI model based on typhoon2.5-qwen3-4b (~2.5GB VRAM, standby mode) — ADR-034',
2.50,
TRUE,
TRUE
)
ON DUPLICATE KEY UPDATE
description = VALUES(description),
vram_gb = VALUES(vram_gb),
is_active = TRUE,
is_default = TRUE,
updated_at = NOW();
-- Demote old gemma4 models from default status
UPDATE ai_available_models
SET is_default = FALSE, updated_at = NOW()
WHERE model_name IN ('gemma4:e2b', 'gemma4:e4b', 'typhoon2.1-gemma3-4b');
-- 2. Insert OCR model (typhoon-np-dms-ocr) — not default, keep_alive=0 (unload after each job)
INSERT INTO ai_available_models (model_name, model_version, description, vram_gb, is_active, is_default)
VALUES (
'typhoon-np-dms-ocr:latest',
'latest',
'Thai OCR model based on typhoon-ocr1.5-3b (~3.2GB VRAM, unloads after each job) — ADR-034',
3.20,
TRUE,
FALSE
)
ON DUPLICATE KEY UPDATE
description = VALUES(description),
vram_gb = VALUES(vram_gb),
is_active = TRUE,
updated_at = NOW();
-- 3. Update active model in system_settings to typhoon2.5-np-dms:latest
UPDATE system_settings
SET setting_value = 'typhoon2.5-np-dms:latest',
updated_at = NOW()
WHERE setting_key = 'AI_ACTIVE_MODEL';
@@ -0,0 +1,28 @@
FROM scb10x/typhoon-ocr1.5-3b:latest
PARAMETER num_ctx 8192
PARAMETER num_predict 4096
PARAMETER temperature 0.1
PARAMETER top_p 0.1
PARAMETER repeat_penalty 1.1
SYSTEM """You are an expert in structuring Thai documents.
Extract the information from the image in the most correct and organized format.
Instructions:
- Return ONLY clean Markdown output.
- Include ALL information visible on the page.
- Preserve document structure and hierarchy.
- Do NOT add explanations or interpretations.
Formatting Rules:
- Tables: Render tables using <table>...</table> in clean HTML format.
- Equations: Render equations using LaTeX syntax with inline ($...$) and block ($$...$$).
- Images/Charts/Diagrams: Wrap any clearly defined visual areas in:
<figure>
Describe the image's main elements, note contextual clues, mention visible text and meaning. Describe in Thai.
</figure>
- Page Numbers: Wrap page numbers in <page_number>...</page_number>.
- Checkboxes: Use ☐ for unchecked and ☑ for checked boxes.
- Signatures/Stamps: Describe location and context
- Unclear text: [unclear: context description]"""
@@ -0,0 +1,19 @@
FROM scb10x/typhoon2.5-qwen3-4b:latest
PARAMETER num_ctx 8192
PARAMETER num_predict 2048
PARAMETER temperature 0.1
PARAMETER top_p 0.85
PARAMETER repeat_penalty 1.15
PARAMETER stop "\n\n"
SYSTEM """คุณคือระบบ AI ผู้เชี่ยวชาญด้านการวิเคราะห์และจัดการเอกสารโครงการ (Document Management System)
หน้าที่ของคุณคืออ่านข้อความภาษาไทยที่ได้มาจากระบบ OCR อย่างละเอียด แล้วทำตามคำสั่งต่อไปนี้อย่างเคร่งครัด:
Guidelines:
1. ข้อมูลเข้าคือข้อความดิบจาก OCR ซึ่งอาจมีคำผิด บรรทัดขาดหาย หรือสัญลักษณ์รบกวน
2. ค้นหาและสกัด 'เลขที่เอกสาร' (Document Number) และ 'วันที่ของเอกสาร' ออกมาให้ถูกต้อง หากไม่พบให้ระบุว่า 'ไม่ระบุ'
3. สรุปเนื้อหาสำคัญของเอกสารนี้อย่างกระชับ เข้าใจง่าย โดยใช้บริบทโดยรวมในการตีความ หากไม่แน่ใจให้ระบุสถานะ "ไม่ชัดเจน"
4. ห้ามสร้างข้อมูล (hallucinate) ที่ไม่มีอยู่ในข้อความต้นฉบับ
5. ห้ามเดาตัวเลข วันที่ หรือเนื้อหาใดๆ ที่ไม่ได้ปรากฏอยู่ในข้อความดิบเด็ดขาด
6. หากข้อมูลไม่ครบ ให้เติม null พร้อมระบุ reason ในฟิลด์ _missing_fields
ตอบกลับเฉพาะ JSON ที่กำหนดเท่านั้น ห้ามเพิ่มข้อความนอกโครงสร้าง""”
@@ -150,7 +150,8 @@ graph TB
| ADR | Version | Dependency Type | Affected Version(s) | Implementation Status |
|-----|---------|-----------------|---------------------|----------------------|
| **ADR-023A** | 1.2 | Model Revision | v1.9.0+ | ✅ Active |
| **ADR-034** | 1.0 | Model Stack Revision | v1.9.0+ | ✅ Active (supersedes 023A Section 2.1) |
| **ADR-023A** | 1.2 | Model Revision | v1.9.0+ | ✅ Active (Section 2.1 superseded by ADR-034) |
| **ADR-023** | 1.1 | Base Architecture | v1.9.0+ | ✅ Active (superseded by 023A for model config) |
| **ADR-016** | 2.0 | Governs | v1.8.0+ | ✅ Active |
| **ADR-019** | 1.5 | Governs | v1.8.0+ | ✅ Active |
@@ -181,14 +182,16 @@ graph TB
#### 2.1 Model Stack & Dynamic Thai-Specialized Models (T041, US2, US3)
> ⚠️ **Update 2026-06-03:** Section นี้ถูก **superseded โดย [ADR-034](./ADR-034-AI-model-change.md)** — โมเดลหลักเปลี่ยนจาก `gemma4:e2b` เป็น `typhoon2.5-np-dms:latest` (Thai-optimized) พร้อม OCR model `typhoon-np-dms-ocr:latest`
ระบบประมวลผลพื้นฐานจะรันด้วยชุด 2-Model Stack ที่ประหยัด VRAM เป็นหลัก และเปิดให้โหลดสลับไปประมวลผลด้วยโมเดลภาษาไทยเฉพาะทางประสิทธิภาพสูง (High-Performance Thai Specialized Models) ได้แบบ Dynamic ภายใต้การควบคุมของ VRAM Monitor เพื่อไม่ให้เกิด VRAM OOM:
##### ชุดประมวลผลหลัก (Baseline 2-Model Stack):
##### ชุดประมวลผลหลัก (Baseline 2-Model Stack) — Superseded by ADR-034:
| โมเดล | Role | VRAM (โดยประมาณ) | หมายเหตุ |
|-------|------|-----------------|---------|
| `gemma4:e2b` | General Inference + OCR Post-processing + Extraction + RAG Q&A | ~2GB (Q4) + ~0.2GB (KV Cache) | Q4 quantization; Context window 8K tokens; Parameters 2.1B |
| `nomic-embed-text` | Embedding 768-dim → Qdrant | ~0.3GB | สร้าง Semantic Vector สำหรับ Hybrid Search |
| `gemma4:e2b` | ~~General Inference + OCR Post-processing + Extraction + RAG Q&A~~ | ~2GB (Q4) | ❌ ถูกแทนที่โดย `typhoon2.5-np-dms` (ADR-034) |
| `nomic-embed-text` | Embedding 768-dim → Qdrant | ~0.3GB | ✅ ยังใช้อยู่ |
| **รวม (peak)** | | **~2.5GB** | **เผื่อ headroom ~5.5GB — มั่นใจสูง เพราะ context window ขนาดใหญ่ (8K tokens)** |
##### โมเดลภาษาไทยเฉพาะทางที่เป็นทางเลือก (Dynamic Thai Specialized Models):
@@ -0,0 +1,236 @@
# ADR-034: AI Model Change — Thai-Optimized Model Stack
**Status:** Accepted
**Date:** 2026-06-03
**Decision Makers:** Development Team, AI Integration Lead
**Supersedes:** ADR-023A Section 2.1 (Model Stack & Configuration)
**Related Documents:**
- [ADR-023A: Unified AI Architecture — Model Revision](./ADR-023A-unified-ai-architecture.md)
- [ADR-033: Active Model & OCR Management](./ADR-033-active-model-and-ocr-management.md)
- [CONTEXT.md](../../../CONTEXT.md)
---
## Context and Problem Statement
การใช้งาน `gemma4:e2b` (~2GB) เป็นโมเดลหลักในสภาพแวดล้อมภาษาไทย พบว่าประสิทธิภาพด้าน OCR และการสกัดข้อมูลจากเอกสารภาษาไทยยังไม่เพียงพอ จึงต้องเปลี่ยนเป็นโมเดลที่ถูก fine-tune มาสำหรับภาษาไทยโดยเฉพาะ
**ข้อจำกัด:**
- VRAM Budget: RTX 2060 Super 8GB
- Main Model + OCR Model ไม่สามารถโหลดพร้อมกันได้ (รวม ~5.7GB ขณะประมวลผล แต่ peak อาจเกิน 8GB)
- ต้องรักษา mechanism `keep_alive` และ VRAM monitoring ตาม ADR-033
---
## Decision Drivers
- **Thai Language Optimization:** โมเดลต้องรองรับ OCR และการสกัดข้อมูลภาษาไทยได้ดีกว่า gemma4
- **VRAM Safety:** ไม่เกิน 8GB ในทุกสถานการณ์
- **Model Switching:** ใช้ BullMQ processor ควบคุมการสลับโมเดลเท่านั้น
- **No Direct n8n Access:** n8n ห้ามเรียก Ollama โดยตรง ต้องผ่าน DMS API → BullMQ
---
## Decision Outcome
### Selected Models
| Model | Role | Base Model | Size | Keep-Alive |
|-------|------|------------|------|------------|
| `typhoon2.5-np-dms:latest` | Main AI (General + OCR Post-processing + Extraction + RAG Q&A) | `scb10x/typhoon2.5-qwen3-4b:latest` | ~2.5GB | Stand by ตลอด (ไม่ใช่ 0) |
| `typhoon-np-dms-ocr:latest` | OCR ภาษาไทย | `scb10x/typhoon-ocr1.5-3b:latest` | ~3.2GB | `0` (unload ทันที) |
### Key Parameters (Main Model)
```
PARAMETER num_ctx 8192
PARAMETER num_predict 2048
PARAMETER temperature 0.1
PARAMETER top_p 0.85
PARAMETER repeat_penalty 1.15
```
---
## Implementation Details
### 1. Model Files (Desk-5439)
---
file: E:\np-dms\lcbp3\specs\04-Infrastructure-OPS\04-00-docker-compose\Desk-5439\typhoon2.5-np-dms.model.md
```t
# ollama create typhoon2.5-np-dms -f ./typhoon2.5-np-dms.model.md
FROM scb10x/typhoon2.5-qwen3-4b:latest
# 1. ปรับขนาดพื้นที่ประมวลผลข้อความ (Context Window)
# ตั้งไว้ที่ 16K ถึง 32K ถือว่าเหลือเฟือมากสำหรับเอกสารข้อความ OCR หลายสิบหน้า
PARAMETER num_ctx 8192
PARAMETER num_predict 2048
# 2. ปรับความนิ่งของคำตอบ (Determinism)
# บีบให้เป็น 0 เพื่อป้องกันโมเดล "คิดแทน" หรือเดาตัวเลข/วันที่ขึ้นมาเอง (สำคัญมากสำหรับเลขที่เอกสาร)
PARAMETER temperature 0.1
PARAMETER top_p 0.85
PARAMETER repeat_penalty 1.15
PARAMETER stop "\n\n"
# 3. ล็อกบทบาทและโครงสร้างผลลัพธ์ที่ต้องการ (System Prompt)
SYSTEM """คุณคือระบบ AI ผู้เชี่ยวชาญด้านการวิเคราะห์และจัดการเอกสารโครงการ (Document Management System)
หน้าที่ของคุณคืออ่านข้อความภาษาไทยที่ได้มาจากระบบ OCR อย่างละเอียด แล้วทำตามคำสั่งต่อไปนี้อย่างเคร่งครัด:
Guidelines:
1. ข้อมูลเข้าคือข้อความดิบจาก OCR ซึ่งอาจมีคำผิด บรรทัดขาดหาย หรือสัญลักษณ์รบกวน
2. ค้นหาและสกัด 'เลขที่เอกสาร' (Document Number) และ 'วันที่ของเอกสาร' ออกมาให้ถูกต้อง หากไม่พบให้ระบุว่า 'ไม่ระบุ'
3. สรุปเนื้อหาสำคัญของเอกสารนี้อย่างกระชับ เข้าใจง่าย โดยใช้บริบทโดยรวมในการตีความ หากไม่แน่ใจให้ระบุสถานะ "ไม่ชัดเจน"
4. ห้ามสร้างข้อมูล (hallucinate) ที่ไม่มีอยู่ในข้อความต้นฉบับ
5. ห้ามเดาตัวเลข วันที่ หรือเนื้อหาใดๆ ที่ไม่ได้ปรากฏอยู่ในข้อความดิบเด็ดขาด
6. หากข้อมูลไม่ครบ ให้เติม null พร้อมระบุ reason ในฟิลด์ _missing_fields
ตอบกลับเฉพาะ JSON ที่กำหนดเท่านั้น ห้ามเพิ่มข้อความนอกโครงสร้าง
""
```
---
file: E:\np-dms\lcbp3\specs\04-Infrastructure-OPS\04-00-docker-compose\Desk-5439\typhoon-np-dms-ocr.model.md
```t
# ollama create typhoon-np-dms-ocr -f ./typhoon-np-dms-ocr.model.md
# ใส่ชื่อ tag โมเดล 3B ที่คุณต้องการจูนตรงนี้ได้เลย
FROM scb10x/typhoon-ocr1.5-3b:latest
# ลดจาก 125k → 8k เพื่อประหยัด และ ล็อกให้ตัวโมเดล + KV Cache กิน VRAM ไม่เกิน 5GB (เหลือโควตาให้ Windows อีก 3GB)
PARAMETER num_ctx 8192
PARAMETER num_predict 4096
# งานดึงข้อความจากภาพสแกน สามารถปรับค่า temperature เป็น 0 เพื่อลดการเดา/มโนคำภาษาไทย
PARAMETER temperature 0.1
PARAMETER top_p 0.1
# ป้องกันไม่ให้โมเดลพิมพ์อักษรซ้ำซากเวลาเจอจุดที่ภาพเบลอหรือรอยเปื้อนบนกระดาษ
PARAMETER repeat_penalty 1.1
# ใส่คำสั่งหลักเพื่อให้โมเดลเข้าใจบทบาทและรูปแบบผลลัพธ์ที่ต้องการ (ตัวอย่าง)
SYSTEM """You are an expert in structuring Thai documents.
Extract the information from the image in the most correct and organized format.
Instructions:
- Return ONLY clean Markdown output.
- Include ALL information visible on the page.
- Preserve document structure and hierarchy.
- Do NOT add explanations or interpretations.
Formatting Rules:
- Tables: Render tables using <table>...</table> in clean HTML format.
- Equations: Render equations using LaTeX syntax with inline ($...$) and block ($$...$$).
- Images/Charts/Diagrams: Wrap any clearly defined visual areas in:
<figure>
Describe the image's main elements, note contextual clues, mention visible text and meaning. Describe in Thai.
</figure>
- Page Numbers: Wrap page numbers in <page_number>...</page_number>.
- Checkboxes: Use ☐ for unchecked and ☑ for checked boxes.
- Signatures/Stamps: Describe location and context
- Unclear text: [unclear: context description]
"""
```
---
### 2. Model Switching Logic (BullMQ Processor)
```typescript
// Pseudo-code for BullMQ processor (ai-batch queue)
async function processJob(job: Job) {
const { jobType, documentId } = job.data;
if (jobType === 'ocr-extract') {
// OCR job: unload main, load OCR, process, unload OCR
await ollama.unloadModel('typhoon2.5-np-dms');
await ollama.loadModel('typhoon-np-dms-ocr', { keep_alive: 0 });
const result = await ollama.generate('typhoon-np-dms-ocr', prompt);
// keep_alive: 0 จะ unload อัตโนมัติหลังเสร็จ
// โหลด main model กลับเข้า VRAM สำหรับงานถัดไป
await ollama.loadModel('typhoon2.5-np-dms');
return result;
}
// Main model jobs: extraction, rag-query, ai-suggest
const result = await ollama.generate('typhoon2.5-np-dms', prompt);
return result;
}
```
**กฎ:**
- **n8n ห้ามเรียก Ollama โดยตรง** — ต้องผ่าน `POST /api/ai/jobs` → BullMQ เท่านั้น
- **BullMQ concurrency = 1** — ป้องกัน VRAM overflow
- **Cold start OCR:** 30-60 วินาทีต่อ job ยอมรับได้
- **OCR job ซ้อนกัน 3-5 งาน:** รวม 2-5 นาที ยอมรับได้
---
### 3. Code Changes
**ไฟล์ที่ต้องแก้ไข:**
| File | Change |
|------|--------|
| `backend/src/modules/ai/services/ai-settings.service.ts` | Hardcode `DEFAULT_MODEL = 'typhoon2.5-np-dms:latest'` |
| `backend/src/modules/ai/services/ollama.service.ts` | เพิ่ม method `unloadModel()` และ `loadModel()` สำหรับ switching |
| `backend/src/modules/ai/processors/ai-batch.processor.ts` | Implement switching logic ตาม pseudo-code ด้านบน |
**Note:** ไม่ต้อง update `ai_settings` table — ใช้ hardcode value เพื่อความเร็วในการ deploy
---
### 4. Migration Plan
**ขั้นตอนการ deploy:**
1. **Desk-5439:** สร้าง custom models บน Ollama
```bash
cd /path/to/model/files
ollama create typhoon2.5-np-dms -f ./typhoon2.5-np-dms.model.md
ollama create typhoon-np-dms-ocr -f ./typhoon-np-dms-ocr.model.md
```
2. **QNAP Backend:** Deploy ด้วย code changes (ADR-033 mechanism ยังคงใช้ได้)
3. **Verification:**
- Test OCR job → ตรวจสอบว่า unload/load ทำงานถูกต้อง
- Test main model job → ตรวจสอบว่า main model พร้อมใช้หลัง OCR
---
### 5. Rollback Strategy
**ไม่มี automatic rollback mechanism**
หากพบปัญหา:
1. สร้าง custom model ใหม่จาก base model ตัวอื่น (เช่น กลับไป `gemma4:e2b`)
2. หรือแก้ไข `typhoon2.5-np-dms.model.md` แล้วสร้าง version ใหม่ (`:v2`)
3. Update code ให้ชี้ไป model ใหม่ แล้ว redeploy
---
## Impact on Related ADRs
| ADR | Section | Impact |
|-----|---------|--------|
| **ADR-023A** | Section 2.1 Model Stack | Superseded by ADR-034 — model config ใช้ค่าจากนี้ |
| **ADR-033** | VRAM Monitor + Model Switching | ยังใช้ได้ — mechanism เดิม เปลี่ยนแค่ชื่อ model |
| **ADR-032** | Typhoon OCR Integration | OCR model ถูกแทนที่โดย `typhoon-np-dms-ocr` |
---
## Glossary Updates (CONTEXT.md)
เพิ่มคำศัพท์ใหม่:
| Term | Definition |
|------|------------|
| **Thai-Optimized Model** | โมเดล AI ที่ถูก fine-tune มาสำหรับภาษาไทย (เช่น Typhoon series) |
| **Model Unload/Load** | กระบวนการยกเลิกโหลดโมเดลจาก VRAM และโหลดโมเดลใหม่เข้าไป |
| **Cold Start Penalty** | ความล่าช้า 5-15 วินาทีจากการโหลดโมเดล weights เข้า VRAM |
---
**สำหรับ Implementation:** ดูไฟล์ใน `specs/100-Infrastructures/134-AI-model-change` (สร้างเมื่อเริ่ม implement)
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Thai-Optimized AI Model Stack
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-06-03
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded (nomic-embed-text excluded; n8n boundary explicit)
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows (OCR, main AI, health check)
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- ADR-034 is the authoritative source; spec captures user-value perspective
- SC-001 OCR quality metric (>90% readable) is aspirational; verify with real docs post-deploy
- A1 (internet access on Desk-5439) should be confirmed before deploy
@@ -0,0 +1,142 @@
// File: specs/100-Infrastructures/134-ai-model-change/contracts/ollama-service-methods.md
// Change Log:
// - 2026-06-03: API contracts for OllamaService model management methods
# Contract: OllamaService Model Management Methods
---
## Method: unloadModel
**Signature**: `async unloadModel(modelName: string): Promise<void>`
**Location**: `backend/src/modules/ai/services/ollama.service.ts`
**Purpose**: Force unload a model from Ollama VRAM
### Ollama API Call
```http
POST http://{OLLAMA_BASE_URL}/api/generate
Content-Type: application/json
{
"model": "{modelName}",
"prompt": "",
"keep_alive": 0
}
```
### Success Behavior
- HTTP 200 → model unloaded from VRAM
- Model already unloaded / not found → log warn; no-op (not fatal)
### Error Handling
| Error | Behavior |
|-------|----------|
| HTTP timeout (> 5s) | Throw `Error('Failed to unload model {modelName}: timeout')` |
| Network error | Throw `Error('Failed to unload model {modelName}: {axiosError.message}')` |
| HTTP 404 (model not found) | Log warn; resolve without throwing |
---
## Method: loadModel
**Signature**: `async loadModel(modelName: string, keepAlive?: number | string): Promise<void>`
**Location**: `backend/src/modules/ai/services/ollama.service.ts`
**Purpose**: Load / warm a model into Ollama VRAM
### Ollama API Call
```http
POST http://{OLLAMA_BASE_URL}/api/generate
Content-Type: application/json
{
"model": "{modelName}",
"prompt": "",
"keep_alive": {keepAlive ?? -1}
}
```
### keepAlive Values
| Value | Behavior |
|-------|----------|
| `-1` | Model stays in VRAM indefinitely (main model) |
| `0` | Model unloads after request completes (OCR model) |
| `"5m"` | Model unloads after 5 minutes (alternative) |
### Success Behavior
- HTTP 200 → model loaded into VRAM
### Error Handling
| Error | Behavior |
|-------|----------|
| HTTP timeout (> 60s, cold start) | Throw `Error('Failed to load model {modelName}: timeout (cold start may take up to 60s)')` |
| HTTP 404 (model not found) | Throw `Error('Model {modelName} not found on Ollama. Run: ollama create {modelName} -f ./{modelName}.model.md')` |
| Network error | Throw `Error('Failed to load model {modelName}: {axiosError.message}')` |
---
## BullMQ Processor: Model Switching Contract
**Location**: `backend/src/modules/ai/processors/ai-batch.processor.ts`
### OCR Job Types Constant
```typescript
const OCR_JOB_TYPES: string[] = ['ocr-extract', 'sandbox-ocr-only'];
```
### Switching Sequence (Pseudocode)
```
processJob(job: Job):
if job.data.jobType ∈ OCR_JOB_TYPES:
log.log(`[ModelSwitch] Unloading ${DEFAULT_MODEL}`)
await ollamaService.unloadModel(DEFAULT_MODEL) // free ~2.5GB
log.log(`[ModelSwitch] Loading ${OCR_MODEL} (keep_alive: 0)`)
await ollamaService.loadModel(OCR_MODEL, 0) // load ~3.2GB
result = await runOcrJob(job)
// OCR model auto-unloads via keep_alive: 0
log.log(`[ModelSwitch] Reloading ${DEFAULT_MODEL} (keep_alive: -1)`)
await ollamaService.loadModel(DEFAULT_MODEL, -1) // restore main model
return result
else:
return await runMainModelJob(job)
```
### Concurrency Constraint
- BullMQ concurrency = 1 (ป้องกัน VRAM overflow)
- ไม่มี parallel OCR + main jobs
- ถ้า job fail ระหว่าง switching → BullMQ retry จาก ต้นใหม่ทั้ง sequence
---
## Health Response Contract Update
**Endpoint**: `GET /api/ai/health`
**Location**: `backend/src/modules/ai/ai.service.ts`
### Updated Response Shape
```typescript
interface SystemHealthResponse {
ollama: {
status: 'HEALTHY' | 'DOWN';
mainModel: string; // 'typhoon2.5-np-dms:latest'
ocrModel: string; // 'typhoon-np-dms-ocr:latest'
latencyMs?: number;
};
ocr: OcrHealthResult;
// ... existing fields
}
```
@@ -0,0 +1,116 @@
// File: specs/100-Infrastructures/134-ai-model-change/data-model.md
// Change Log:
// - 2026-06-03: Data model for Thai-Optimized AI Model Stack
# Data Model: Thai-Optimized AI Model Stack
---
## Database Schema Changes
**ไม่มี new tables หรือ column changes** — ADR-009 compliant; ไม่มี TypeORM migration
### Optional: ai_available_models Seed Update
ตาราง `ai_available_models` (จาก ADR-027 Session 6) ควร update เพื่อความสอดคล้อง:
**Update existing main model record:**
```sql
UPDATE ai_available_models
SET model_name = 'typhoon2.5-np-dms:latest',
display_name = 'Typhoon 2.5 NP-DMS (Thai)',
updated_at = NOW()
WHERE model_type = 'main' AND is_active = 1;
```
**Insert OCR model record (if column model_type supports 'ocr'):**
```sql
INSERT INTO ai_available_models (model_name, display_name, model_type, is_active, created_at, updated_at)
VALUES ('typhoon-np-dms-ocr:latest', 'Typhoon OCR 3B (Thai)', 'ocr', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE display_name = VALUES(display_name), updated_at = NOW();
```
> **Note**: ตรวจสอบ schema จริงใน `lcbp3-v1.9.0-schema-02-tables.sql` ก่อน run delta — column names และ model_type ENUM อาจแตกต่าง
---
## Code Configuration Model
### AiSettingsService Constants
| Constant | เดิม | ใหม่ |
|----------|------|------|
| `DEFAULT_MODEL` | `'gemma4:e2b'` | `'typhoon2.5-np-dms:latest'` |
| `OCR_MODEL` (เพิ่มใหม่) | — | `'typhoon-np-dms-ocr:latest'` |
**File**: `backend/src/modules/ai/services/ai-settings.service.ts`
---
## Custom Ollama Model Configurations
### typhoon2.5-np-dms (Main Model)
| Property | Value |
|----------|-------|
| Custom Name | `typhoon2.5-np-dms:latest` |
| Base Model | `scb10x/typhoon2.5-qwen3-4b:latest` |
| Size | ~2.5GB VRAM |
| keep_alive | Indefinite (`-1`) — standby ตลอด |
| num_ctx | 8192 |
| num_predict | 2048 |
| temperature | 0.1 |
| top_p | 0.85 |
| repeat_penalty | 1.15 |
| Role | Extraction, RAG Q&A, AI Suggestion, OCR Post-processing |
| Modelfile Path | `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/typhoon2.5-np-dms.model.md` |
### typhoon-np-dms-ocr (OCR Model)
| Property | Value |
|----------|-------|
| Custom Name | `typhoon-np-dms-ocr:latest` |
| Base Model | `scb10x/typhoon-ocr1.5-3b:latest` |
| Size | ~3.2GB VRAM |
| keep_alive | `0` — auto-unload immediately after job |
| num_ctx | 8192 |
| num_predict | 4096 |
| temperature | 0.1 |
| top_p | 0.1 |
| repeat_penalty | 1.1 |
| Role | Thai OCR extraction from PDF images |
| Modelfile Path | `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/typhoon-np-dms-ocr.model.md` |
---
## VRAM Budget Analysis
| State | Models in VRAM | Estimated VRAM Usage |
|-------|---------------|---------------------|
| Idle | typhoon2.5-np-dms (standby) | ~2.5GB |
| Main AI job | typhoon2.5-np-dms (active) | ~3.54GB peak |
| OCR transition | unloading main → loading OCR | ~3.2GB (OCR only) |
| OCR processing | typhoon-np-dms-ocr (active) | ~45GB peak |
| Post-OCR reload | loading main back | ~2.5GB |
| **Max peak** | OCR model active | **~5GB** (safe under 8GB limit) |
---
## OllamaService New Methods Signatures
```typescript
/**
* unloadModel: Force unload model จาก VRAM
* @param modelName - ชื่อ Ollama model ที่ต้องการ unload
*/
async unloadModel(modelName: string): Promise<void>
/**
* loadModel: Load / warm model เข้า VRAM
* @param modelName - ชื่อ Ollama model ที่ต้องการ load
* @param keepAlive - -1 = indefinite (default สำหรับ main model); 0 = auto-unload
*/
async loadModel(modelName: string, keepAlive?: number | string): Promise<void>
```
@@ -0,0 +1,133 @@
// File: specs/100-Infrastructures/134-ai-model-change/plan.md
// Change Log:
// - 2026-06-03: Initial implementation plan for Thai-Optimized AI Model Stack (ADR-034)
# Implementation Plan: Thai-Optimized AI Model Stack
**Branch**: `134-ai-model-change` | **Date**: 2026-06-03 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/100-Infrastructures/134-ai-model-change/spec.md`
---
## Summary
เปลี่ยน AI model stack จาก `gemma4:e2b` เป็น custom Typhoon models: `typhoon2.5-np-dms:latest` (main AI) + `typhoon-np-dms-ocr:latest` (Thai OCR) ตาม ADR-034. ต้อง implement:
1. Custom Modelfiles บน Desk-5439 (มีอยู่แล้วใน `specs/04-Infrastructure-OPS/...`)
2. `unloadModel()` + `loadModel()` ใน `OllamaService`
3. Model switching logic ใน `ai-batch.processor.ts`
4. อัปเดต `AiSettingsService.DEFAULT_MODEL`
5. SQL delta สำหรับ `ai_available_models` table
---
## Technical Context
**Language/Version**: TypeScript 5.x, NestJS 11, Node.js 20
**Primary Dependencies**: `@nestjs/bull`, `bullmq`, `axios` (Ollama HTTP client), Redis
**Storage**: ไม่มี schema changes — optional SQL delta สำหรับ `ai_available_models` seed
**Testing**: Jest (unit tests: OllamaService methods + ai-batch.processor switching)
**Target Platform**: QNAP NAS backend (Docker) + Desk-5439 (Ollama runtime, RTX 2060 Super 8GB)
**Performance Goals**: OCR cold start ≤ 60s; main model warm ≤ 5s; zero VRAM OOM
**Constraints**: VRAM ≤ 8GB; BullMQ concurrency=1; ADR-033 VRAM monitor preserved; ADR-023A AI boundary
**Scale/Scope**: 1 Desk-5439 Ollama instance; 1 concurrent AI job at a time (BullMQ)
---
## Constitution Check
_GATE: Must pass before Phase 0 research. Re-checked after Phase 1 design._
| Rule | Status | Notes |
|------|--------|-------|
| ADR-019 UUID: ไม่มี parseInt บน UUID | ✅ PASS | ไม่มี UUID changes ใน feature นี้ |
| ADR-009 Schema: ไม่มี TypeORM migration | ✅ PASS | ไม่มี table ใหม่; SQL delta สำหรับ seed update เท่านั้น |
| ADR-023/023A AI Boundary: Ollama บน Desk-5439 | ✅ PASS | ยังคง pattern เดิม; ไม่มี direct DB/storage access |
| ADR-033 VRAM Monitor: mechanism ยังใช้งาน | ✅ PASS | ชื่อ model เปลี่ยน; mechanism เดิม |
| ADR-008 BullMQ: Background jobs ผ่าน queue | ✅ PASS | ไม่มี inline AI call ใหม่ |
| ADR-007 Error Handling: Error classification | ✅ PASS | unload/load errors ต้องมี descriptive messages |
| ADR-016 Security: CASL Guard | ✅ PASS | ไม่มี new public endpoints |
| Forbidden: console.log, any type | ✅ PASS | ต้องตรวจสอบขณะ implement (ESLint enforce) |
---
## Project Structure
### Documentation (this feature)
```text
specs/100-Infrastructures/134-ai-model-change/
├── spec.md # Feature specification
├── plan.md # This file
├── research.md # Phase 0: decisions
├── data-model.md # Phase 1: config/model data
├── quickstart.md # Phase 1: verification guide
├── contracts/
│ └── ollama-service-methods.md # Method signatures + Ollama API
├── checklists/
│ └── requirements.md # Spec quality checklist
└── tasks.md # Phase 2: task list
```
### Infrastructure Files (already exist)
```text
specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/
├── typhoon2.5-np-dms.model.md # Main model Modelfile ✅ exists
└── typhoon-np-dms-ocr.model.md # OCR model Modelfile ✅ exists
```
### Source Code Changes
```text
backend/src/modules/ai/
├── services/
│ ├── ollama.service.ts # เพิ่ม unloadModel() + loadModel()
│ └── ai-settings.service.ts # อัปเดต DEFAULT_MODEL + เพิ่ม OCR_MODEL
└── processors/
└── ai-batch.processor.ts # เพิ่ม OCR model switching logic
```
### Optional Delta
```text
specs/03-Data-and-Storage/deltas/
└── 2026-06-03-update-ai-available-models-typhoon.sql
```
**Structure Decision**: Web application (backend only) — ไม่มี frontend changes ที่จำเป็น; health UI อาจ update model names เองจาก dynamic data
---
## Phase 0: Research
ผลการ research ดู [research.md](./research.md) — decisions ถูก resolve จาก ADR-034 แล้ว
---
## Phase 1: Design & Contracts
### Data Model
ดู [data-model.md](./data-model.md)
- ไม่มี new DB entities
- Code constants ใน `AiSettingsService` อัปเดต
- Optional: `ai_available_models` seed delta
### API Contracts
ดู [contracts/ollama-service-methods.md](./contracts/ollama-service-methods.md)
- `OllamaService.unloadModel(modelName: string): Promise<void>`
- `OllamaService.loadModel(modelName: string, keepAlive?: number | string): Promise<void>`
- BullMQ processor switching pseudocode
### Quickstart Verification
ดู [quickstart.md](./quickstart.md)
---
## Complexity Tracking
ไม่มี Constitution Check violations ที่ต้องจัดการ
@@ -0,0 +1,111 @@
// File: specs/100-Infrastructures/134-ai-model-change/quickstart.md
// Change Log:
// - 2026-06-03: Verification guide for Thai-Optimized AI Model Stack
# Quickstart: Thai-Optimized AI Model Stack Verification
---
## Prerequisites
- Desk-5439 รัน Ollama service (port 11434)
- Internet access บน Desk-5439 (สำหรับ pull base models จาก registry)
- QNAP backend container running (port 3001)
- Model files อยู่ที่ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/`
---
## Step 1: สร้าง Custom Models บน Desk-5439
```powershell
# บน Desk-5439 Windows — เปิด PowerShell ใน directory ที่มี Modelfiles
# Path: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/
ollama create typhoon2.5-np-dms -f .\typhoon2.5-np-dms.model.md
# คาดว่าใช้เวลา: 5-15 นาที (download base model ~2.5GB)
ollama create typhoon-np-dms-ocr -f .\typhoon-np-dms-ocr.model.md
# คาดว่าใช้เวลา: 5-15 นาที (download base model ~3.2GB)
# ตรวจสอบ
ollama list
# ต้องเห็น:
# typhoon2.5-np-dms:latest
# typhoon-np-dms-ocr:latest
# nomic-embed-text:latest (ยังคงอยู่ — embedding model ไม่เปลี่ยน)
```
---
## Step 2: Apply SQL Delta (ถ้า ai_available_models table มีอยู่)
```powershell
# รัน delta ผ่าน DB admin tool หรือ mysql client
# File: specs/03-Data-and-Storage/deltas/2026-06-03-update-ai-available-models-typhoon.sql
```
---
## Step 3: Deploy Backend
ปฏิบัติตาม `/deploy` workflow ปกติ (QNAP Container Station)
---
## Step 4: ตรวจสอบ Health Endpoint
```powershell
Invoke-RestMethod -Uri "http://localhost:3001/api/ai/health" -Method GET |
ConvertTo-Json -Depth 5
# ตรวจสอบ:
# ollama.mainModel = "typhoon2.5-np-dms:latest"
# ollama.status = "HEALTHY"
```
---
## Step 5: ทดสอบ OCR Job
```powershell
# ส่ง OCR job ผ่าน AI Admin Console → OCR Sandbox
# หรือ POST /api/ai/admin/sandbox/ocr ด้วย PDF ภาษาไทย
# ตรวจสอบ result:
# - ocrText มีภาษาไทยที่อ่านออกได้
# - ไม่มี VRAM OOM error ใน logs
```
---
## Step 6: ตรวจสอบ BullMQ Model Switching Logs
```powershell
# ดู backend logs ขณะ OCR job กำลังทำงาน
docker compose logs -f backend | Select-String "ModelSwitch"
# ต้องเห็น:
# [ModelSwitch] Unloading typhoon2.5-np-dms:latest
# [ModelSwitch] Loading typhoon-np-dms-ocr:latest (keep_alive: 0)
# [ModelSwitch] Reloading typhoon2.5-np-dms:latest (keep_alive: -1)
```
---
## Step 7: ตรวจสอบ VRAM ไม่เกิน 8GB
```powershell
# บน Desk-5439 ขณะ OCR job กำลังทำงาน
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
# ต้องไม่เกิน 8192 MiB
```
---
## Rollback
หากพบปัญหาหลัง deploy:
1. สร้าง custom model ใหม่จาก `gemma4:e2b`:
```powershell
ollama create typhoon2.5-np-dms -f .\gemma4-fallback.model.md
```
2. หรือ revert `AiSettingsService.DEFAULT_MODEL` กลับเป็น `'gemma4:e2b'` แล้ว redeploy
3. ดูรายละเอียดใน ADR-034 Section 5 Rollback Strategy
@@ -0,0 +1,70 @@
// File: specs/100-Infrastructures/134-ai-model-change/research.md
// Change Log:
// - 2026-06-03: Phase 0 research for Thai-Optimized AI Model Stack
# Research: Thai-Optimized AI Model Stack
**Status**: Complete — all decisions resolved from ADR-034
---
## Decision 1: Model Unloading via Ollama API
**Decision**: ใช้ `POST /api/generate` พร้อม `keep_alive: 0` และ `prompt: ""` เพื่อ unload model จาก VRAM
**Rationale**: Ollama ไม่มี dedicated unload endpoint; การส่ง `keep_alive: 0` จะทำให้ model unload ทันทีหลัง request complete; ส่ง empty prompt เพื่อเรียก endpoint โดยไม่ generate output
**Alternatives considered**:
- Restart Ollama service — disruptive; ใช้เวลานาน; ทำลาย in-flight jobs
- `/api/delete` — ลบ model ออกจาก registry ไม่ใช่แค่ unload จาก VRAM
---
## Decision 2: Model Loading via Ollama API
**Decision**: ใช้ `POST /api/generate` พร้อม `keep_alive: -1` สำหรับ main model; `keep_alive: 0` สำหรับ OCR model
**Rationale**: การส่ง request ไปยัง model ที่ยังไม่ได้ load จะ trigger Ollama auto-load; `keep_alive: -1` = infinite standby (ตาม ADR-034 "Stand by ตลอด"); `keep_alive: 0` = auto-unload หลัง job
**Alternatives considered**:
- `POST /api/chat` — ใช้ได้เช่นกัน แต่ `/api/generate` มี API surface เล็กกว่า เหมาะสำหรับ keepalive ping
---
## Decision 3: OCR Job Type List
**Decision**: OCR job types = `['ocr-extract', 'sandbox-ocr-only']`
**Rationale**: ทั้งสอง job types ต้องการ OCR model; `ocr-extract` = production; `sandbox-ocr-only` = admin sandbox; types อื่นทั้งหมดใช้ main model
**Alternatives considered**:
- Config-driven job types — over-engineering; ADR-034 กำหนดไว้แล้ว
- DB-driven routing — เพิ่ม complexity โดยไม่จำเป็น
---
## Decision 4: DEFAULT_MODEL Strategy — Hardcode
**Decision**: Hardcode `DEFAULT_MODEL = 'typhoon2.5-np-dms:latest'` ใน `AiSettingsService`
**Rationale**: ADR-034 ระบุชัดเจน "ไม่ต้อง update ai_settings table — ใช้ hardcode value เพื่อความเร็วในการ deploy"; การ hardcode ป้องกัน misconfiguration จาก DB
**Alternatives considered**:
- อ่านจาก `ai_settings` DB table — ถูก reject ใน ADR-034 เพื่อความง่ายใน deployment
- อ่านจาก ENV variable — อาจ conflict กับ ADR-027 model management; hardcode ชัดกว่า
---
## Decision 5: ai_available_models Table Update
**Decision**: สร้าง SQL delta `2026-06-03-update-ai-available-models-typhoon.sql` อัปเดต seed
**Rationale**: ADR-027 Admin Console อ่านจาก `ai_available_models` table; ถ้าไม่อัปเดตจะแสดงชื่อ model เก่า ทำให้ admin เห็น inconsistency
**Alternatives considered**:
- ไม่อัปเดต table — admin console แสดงข้อมูลเก่า; เสี่ยง confusion
---
## Decision 6: Error Handling for Model Switching Failure
**Decision**: Throw `Error` พร้อม descriptive message ที่ระบุ model name; NestJS Logger บันทึก context; ไม่ retry ภายใน switching logic (ปล่อยให้ BullMQ retry ตาม job policy)
**Rationale**: Model load failure ควร fail fast; BullMQ ที่มี retry config จะ re-attempt job ทั้งหมด รวมถึง switching sequence ด้วย; การ retry ภายใน switching เสี่ยง VRAM state inconsistency
**Alternatives considered**:
- Retry unload/load ภายใน service — อาจทำให้ VRAM state confused ถ้า partial load
---
## Unresolved Items
ไม่มี — decisions ทั้งหมดถูก resolve แล้ว
@@ -0,0 +1,127 @@
// File: specs/100-Infrastructures/134-ai-model-change/spec.md
// Change Log:
// - 2026-06-03: Initial specification for Thai-Optimized AI Model Stack (ADR-034)
# Feature Specification: Thai-Optimized AI Model Stack
**Feature Branch**: `134-ai-model-change`
**Created**: 2026-06-03
**Status**: Draft
**Category**: 100-Infrastructures
**ADR Reference**: ADR-034 (Supersedes ADR-023A Section 2.1)
**Input**: User description: "Implement ADR-034 — Switch AI model stack from gemma4:e2b to Thai-optimized Typhoon series: typhoon2.5-np-dms:latest (main AI) + typhoon-np-dms-ocr:latest (Thai OCR). Requires model switching logic in BullMQ processor, new unload/load methods in OllamaService, updated default model config. VRAM budget: RTX 2060 Super 8GB."
---
## User Scenarios & Testing _(mandatory)_
### User Story 1 — Thai OCR Extraction With New OCR Model (Priority: P1)
ในฐานะ DevOps Engineer ฉันต้องการให้งาน OCR ภาษาไทยใช้ Typhoon OCR-3B ที่ถูก fine-tune มาสำหรับภาษาไทย เพื่อให้ข้อความที่สกัดจาก PDF ภาษาไทยมีความแม่นยำสูงกว่า gemma4:e2b
**Why this priority**: งาน OCR ภาษาไทยคือจุดอ่อนหลักของระบบปัจจุบัน การเปลี่ยนเป็น Typhoon OCR model จะส่งผลโดยตรงต่อคุณภาพการ extract metadata และ text จากเอกสาร
**Independent Test**: ส่ง OCR job (`jobType: 'ocr-extract'`) เข้า BullMQ ด้วย PDF ภาษาไทย และตรวจสอบว่า (1) job สำเร็จโดยไม่มี VRAM OOM error (2) ข้อความ OCR อ่านออกได้ (3) หลัง OCR เสร็จ main model ยังพร้อมใช้งาน
**Acceptance Scenarios**:
1. **Given** `typhoon-np-dms-ocr:latest` ถูก create บน Desk-5439, **When** BullMQ ได้รับ job `ocr-extract` หรือ `sandbox-ocr-only`, **Then** processor unload main model → load OCR model (keep_alive:0) → ประมวลผล → OCR model unload อัตโนมัติ
2. **Given** OCR job เสร็จแล้ว, **When** main model job เข้ามาต่อ, **Then** processor reload main model และประมวลผลสำเร็จภายใน 60 วินาที
3. **Given** VRAM ถูกใช้งานสูงขณะมี OCR job, **When** job เข้าคิว, **Then** BullMQ concurrency=1 ป้องกัน VRAM overflow — ไม่เกิด OOM error
---
### User Story 2 — General AI Tasks Use Thai-Optimized Main Model (Priority: P2)
ในฐานะ DevOps Engineer ฉันต้องการให้ระบบ AI ใช้ `typhoon2.5-np-dms:latest` เป็น main model สำหรับงาน extraction, RAG Q&A, และ AI suggestion แทน gemma4:e2b เพื่อประสิทธิภาพภาษาไทยที่ดีขึ้น
**Why this priority**: เมื่อ OCR ทำงานได้แล้ว ต้องมั่นใจว่า AI extraction/RAG ที่ใช้ main model ทำงานได้กับโมเดลใหม่
**Independent Test**: ส่ง `ai-extract` job เข้า BullMQ → ตรวจสอบว่า backend เรียก `typhoon2.5-np-dms:latest` (ไม่ใช่ `gemma4:e2b`) และได้ผลลัพธ์ที่ถูกต้อง
**Acceptance Scenarios**:
1. **Given** `typhoon2.5-np-dms:latest` ถูก create บน Desk-5439, **When** backend start up, **Then** `AiSettingsService.DEFAULT_MODEL = 'typhoon2.5-np-dms:latest'`
2. **Given** main model พร้อม, **When** ส่ง extraction/RAG/suggestion job, **Then** ผลลัพธ์ return โดยไม่มี "model not found" error
3. **Given** การ reload main model หลัง OCR job, **When** warm reload, **Then** ใช้เวลา ≤ 60 วินาที (cold start acceptable)
---
### User Story 3 — System Health Reflects New Model Stack (Priority: P3)
ในฐานะ AI Admin ฉันต้องการเห็นสถานะของ model stack ใหม่ใน AI Admin Console เพื่อยืนยันว่าระบบทำงานถูกต้องหลัง deploy
**Why this priority**: Operations team ต้องการ observability เพื่อตรวจสอบหลัง deployment
**Independent Test**: เปิด AI Admin Console → System Health → เห็นชื่อ `typhoon2.5-np-dms` และ `typhoon-np-dms-ocr` แทน gemma4
**Acceptance Scenarios**:
1. **Given** backend รันกับ model stack ใหม่, **When** GET `/api/ai/health`, **Then** response แสดง main model = `typhoon2.5-np-dms:latest` และ ocr model = `typhoon-np-dms-ocr:latest`
2. **Given** มี model switching เกิดขึ้น, **When** ดู BullMQ job logs, **Then** เห็น log บันทึก unload/load พร้อม model name และ timestamp
---
### Edge Cases
- VRAM เต็ม (>8GB) ขณะ load OCR model → BullMQ retry; ไม่ crash; error log ชัดเจน
- Ollama API timeout ขณะ `unloadModel`/`loadModel` → throw descriptive error; ไม่ swallow silently
- โมเดล `typhoon2.5-np-dms` ไม่ถูก create บน Desk-5439 → backend start ได้; job fail พร้อม clear "model not found" error
- OCR job และ main job เข้าคิวพร้อมกัน → BullMQ concurrency=1 handle อยู่แล้ว; ไม่ต้องแก้
- Cold start OCR (3060s) ขณะ user รอ → ยอมรับได้; job polling แสดงสถานะ
---
## Requirements _(mandatory)_
### Functional Requirements
- **FR-001**: ระบบ MUST ใช้ `typhoon2.5-np-dms:latest` เป็น default main model สำหรับทุก AI job ที่ไม่ใช่ OCR
- **FR-002**: ระบบ MUST ใช้ `typhoon-np-dms-ocr:latest` เฉพาะ job type `ocr-extract` และ `sandbox-ocr-only`
- **FR-003**: BullMQ processor MUST unload main model ก่อน load OCR model ทุกครั้งที่มี OCR job
- **FR-004**: OCR model MUST unload อัตโนมัติหลัง job เสร็จ (keep_alive: 0)
- **FR-005**: หลัง OCR job เสร็จ processor MUST reload main model (keep_alive: -1 หรือ indefinite)
- **FR-006**: OllamaService MUST expose `unloadModel(modelName)` และ `loadModel(modelName, keepAlive?)` methods
- **FR-007**: `AiSettingsService.DEFAULT_MODEL` MUST hardcoded เป็น `'typhoon2.5-np-dms:latest'` (ตาม ADR-034)
- **FR-008**: Embedding model `nomic-embed-text` MUST ไม่เปลี่ยนแปลง (นอก scope)
- **FR-009**: n8n MUST ไม่เรียก Ollama โดยตรง — ยังคง pattern `POST /api/ai/jobs` → BullMQ (ADR-023A)
- **FR-010**: BullMQ concurrency MUST = 1 เพื่อป้องกัน VRAM overflow
### Key Entities
- **Custom Ollama Model `typhoon2.5-np-dms`**: Configuration ของ main AI model — base: typhoon2.5-qwen3-4b, keep_alive: indefinite, size ~2.5GB
- **Custom Ollama Model `typhoon-np-dms-ocr`**: Configuration ของ OCR model — base: typhoon-ocr1.5-3b, keep_alive: 0, size ~3.2GB
- **OllamaService.unloadModel / loadModel**: Methods สำหรับ explicit model management ผ่าน Ollama API
- **AiSettingsService.DEFAULT_MODEL**: Hardcoded constant ชี้ไปยัง main model ใหม่
---
## Success Criteria _(mandatory)_
### Measurable Outcomes
- **SC-001**: OCR job ประมวลผล PDF ภาษาไทยได้ข้อความที่อ่านออกได้ (ไม่มี garbled Thai characters) > 90% ของ pages
- **SC-002**: ไม่มี VRAM OOM error ตลอดการ process OCR + main jobs ต่อเนื่อง 10 jobs
- **SC-003**: OCR cold start ≤ 60 วินาที; main model warm response ≤ 5 วินาที
- **SC-004**: Deploy custom models บน Desk-5439 (`ollama create`) ใช้เวลา ≤ 15 นาทีต่อ model (download ขึ้นกับ network)
- **SC-005**: ไม่มี regression ใน AI extraction/RAG functionality ที่เดิมทำงานได้
---
## Assumptions
- **A1**: Desk-5439 มี internet access สำหรับ pull base models (`scb10x/typhoon*`) จาก Ollama registry
- **A2**: ADR-033 VRAM monitoring mechanism ใช้งานได้กับ Typhoon models
- **A3**: Custom Modelfiles ถูก define ใน `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/` แล้ว
- **A4**: Embedding model `nomic-embed-text` ไม่เปลี่ยนแปลง
- **A5**: `ai_available_models` table (ADR-027) จำเป็นต้อง update seed เพื่อแสดง model ใหม่ใน admin console
---
## Clarifications
### Session 2026-06-03
- Q: nomic-embed-text embedding model เปลี่ยนหรือไม่? → A: ไม่เปลี่ยน — ADR-034 supersedes LLM stack เท่านั้น
- Q: Keep-alive value สำหรับ main model หลัง reload คือเท่าไร? → A: -1 (indefinite) ตาม ADR-034 "Stand by ตลอด (ไม่ใช่ 0)"
- Q: `sandbox-ocr-only` ต้องใช้ OCR model switching ด้วยหรือไม่? → A: ใช่ — เป็น OCR job type เหมือน `ocr-extract`
@@ -0,0 +1,156 @@
// File: specs/100-Infrastructures/134-ai-model-change/tasks.md
// Change Log:
// - 2026-06-03: Initial task list for Thai-Optimized AI Model Stack
# Tasks: Thai-Optimized AI Model Stack
**Input**: Design documents from `specs/100-Infrastructures/134-ai-model-change/`
**ADR Reference**: ADR-034 (Supersedes ADR-023A Section 2.1)
**Prerequisites**: plan.md ✅ spec.md ✅ research.md ✅ data-model.md ✅ contracts/ ✅
## Format: `[ID] [P?] [Story?] Description`
- **[P]**: Can run in parallel (different files, no shared state)
- **[Story]**: User story label (US1/US2/US3)
---
## Phase 1: Setup (Infrastructure Verification)
**Purpose**: ยืนยัน Model files และ directory structure พร้อม; ไม่ต้องสร้างใหม่เพราะมีอยู่แล้ว
- [X] T001 [P] ตรวจสอบ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/typhoon2.5-np-dms.model.md` ว่า content ตรงกับ ADR-034 Section 1
- [X] T002 [P] ตรวจสอบ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/typhoon-np-dms-ocr.model.md` ว่า content ตรงกับ ADR-034 Section 1
- [X] T003 [P] สร้าง SQL delta `specs/03-Data-and-Storage/deltas/2026-06-03-update-ai-available-models-typhoon.sql` — UPDATE main model + INSERT ocr model ใน `ai_available_models` table (ตรวจสอบ schema ก่อน run)
- [X] T003b [P] สร้าง rollback `specs/03-Data-and-Storage/deltas/2026-06-03-update-ai-available-models-typhoon.rollback.sql` — revert model_name กลับเป็น gemma4:e2b + DELETE ocr model record (ADR-009: ทุก delta ต้องมี rollback)
---
## Phase 2: Foundational (OllamaService + AiSettingsService)
**Purpose**: Backend services support model ใหม่ — BLOCKS Phases 3, 4, 5
**⚠️ CRITICAL**: ต้องเสร็จก่อนเริ่ม US phases
- [X] T004 เพิ่ม method `unloadModel(modelName: string): Promise<void>` ใน `backend/src/modules/ai/services/ollama.service.ts` — POST /api/generate keep_alive:0 + error handling ตาม contract
- [X] T005 เพิ่ม method `loadModel(modelName: string, keepAlive?: number | string): Promise<void>` ใน `backend/src/modules/ai/services/ollama.service.ts` — POST /api/generate keep_alive:{keepAlive ?? -1} + timeout 60s + "model not found" error
- [X] T006 [P] อัปเดต `DEFAULT_MODEL = 'typhoon2.5-np-dms:latest'` และเพิ่ม `OCR_MODEL = 'typhoon-np-dms-ocr:latest'` ใน `backend/src/modules/ai/services/ai-settings.service.ts`
- [X] T007 [P] เขียน unit tests สำหรับ `unloadModel()` และ `loadModel()` ใน `backend/src/modules/ai/services/ollama.service.spec.ts` — mock axios; test timeout, 404, และ success cases
**Checkpoint**: Foundation พร้อม — processor switching สามารถ implement ได้
---
## Phase 3: User Story 1 — Thai OCR Processing (Priority: P1) 🎯 MVP
**Goal**: BullMQ processor switch models สำหรับ OCR jobs; ไม่มี VRAM OOM
**Independent Test**: ส่ง `ocr-extract` job → ตรวจสอบ log มี ModelSwitch entries + job สำเร็จ
- [X] T008 [US1] เพิ่ม constant `const OCR_JOB_TYPES = ['ocr-extract', 'sandbox-ocr-only'] as const` ใน `backend/src/modules/ai/processors/ai-batch.processor.ts`
- [X] T009 [US1] Implement model switching block ใน `processJob()`: unload main → load OCR (keep_alive:0) → process → reload main (keep_alive:-1) ใน `backend/src/modules/ai/processors/ai-batch.processor.ts`
- [X] T010 [US1] เพิ่ม NestJS `Logger` log สำหรับ model switch events (model name + timestamp) ใน `backend/src/modules/ai/processors/ai-batch.processor.ts`
- [X] T011 [P] [US1] เขียน unit tests สำหรับ OCR model switching logic ใน `backend/src/modules/ai/processors/ai-batch.processor.spec.ts` — mock OllamaService methods; test switching sequence + non-OCR bypass
**Checkpoint**: OCR job ทำงานกับ Typhoon OCR model; main model reload หลัง OCR สำเร็จ
---
## Phase 4: User Story 2 — Main AI Tasks With New Model (Priority: P2)
**Goal**: ทุก non-OCR AI job ใช้ `typhoon2.5-np-dms:latest`
**Independent Test**: ส่ง `ai-extract` job → ตรวจสอบ Ollama call ใช้ model ใหม่ (ไม่ใช่ gemma4)
- [X] T012 [US2] ตรวจสอบและอัปเดต `OllamaService.generate()` ให้อ้างอิง `AiSettingsService.DEFAULT_MODEL` แทน hardcoded model name ใน `backend/src/modules/ai/services/ollama.service.ts`
- [X] T013 [P] [US2] grep codebase ด้วย pattern `gemma4|OLLAMA_MODEL_MAIN|OLLAMA_RAG_MODEL` ใน `backend/src/` หา hardcoded model references เก่าทั้งหมด → อัปเดตให้ใช้ `AiSettingsService.DEFAULT_MODEL` หรือ `AiSettingsService.OCR_MODEL`
- [X] T014 [US2] อัปเดต comment/note ใน `backend/.env.example` — อธิบายว่า model names ถูก hardcode ใน `AiSettingsService` ตาม ADR-034
**Checkpoint**: ทุก non-OCR AI job ใช้ model ใหม่
---
## Phase 5: User Story 3 — System Health Visibility (Priority: P3)
**Goal**: Admin Console แสดงชื่อ model ที่ถูกต้อง
**Independent Test**: GET /api/ai/health → JSON มี `mainModel` = typhoon2.5-np-dms, `ocrModel` = typhoon-np-dms-ocr
- [X] T015 [P] [US3] อัปเดต health response structure ใน `backend/src/modules/ai/ai.service.ts` — เพิ่ม `mainModel` และ `ocrModel` fields ที่อ่านจาก `AiSettingsService`
- [X] T016 [P] [US3] ตรวจสอบ `frontend/app/(admin)/admin/ai/page.tsx` ว่า health card แสดง model names ใหม่ (ถ้า dynamic จาก API ไม่ต้องแก้; ถ้า hardcoded ให้แก้)
- [ ] T017 [US3] รัน SQL delta `2026-06-03-update-ai-available-models-typhoon.sql` บน production DB (manual step — ผ่าน DB admin tool)
**Checkpoint**: Admin Console แสดง model stack ใหม่ถูกต้อง
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Documentation update + compliance verification
- [X] T018 [P] อัปเดต `AGENTS.md` — Current Decisions D10: เปลี่ยน `gemma4:e4b Q8_0` เป็น `typhoon2.5-np-dms:latest (main) + typhoon-np-dms-ocr:latest (OCR)`; อัปเดต version เป็น v1.9.9 และ sync date
- [X] T019 [P] อัปเดต `memory/agent-memory.md` — Section 2.5 model names + Section 5 D10 + Section 7 Ollama row + Section 8 Recent Rollouts entry
- [X] T020 [P] อัปเดต `.agents/rules/11-ai-integration.md` — 2-model stack: `gemma4:e2b → typhoon2.5-np-dms:latest`
- [ ] T021 [P] รัน type check: `pnpm --filter backend build` — ต้องผ่าน 0 errors
- [ ] T022 [P] รัน lint: `pnpm --filter backend lint` — ตรวจสอบ no console.log, no any
- [ ] T023 [P] รัน unit tests ที่เพิ่มใหม่: `pnpm --filter backend test -- --testPathPattern="ollama.service|ai-batch.processor"`
- [ ] T024 รัน quickstart.md verification บน Desk-5439 + QNAP ตามขั้นตอนใน `quickstart.md` — รวมถึงตรวจสอบ BullMQ concurrency=1 ใน `ai-batch.processor.ts` (FR-010)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Setup)**: ไม่มี dependency — เริ่มทันที
- **Phase 2 (Foundational)**: ไม่มี dependency — เริ่มทันที; **BLOCKS Phase 3, 4, 5**
- **Phase 3 (US1)**: ต้องรอ Phase 2 สมบูรณ์
- **Phase 4 (US2)**: ต้องรอ Phase 2; สามารถ parallel กับ Phase 3
- **Phase 5 (US3)**: ต้องรอ Phase 3 + Phase 4
- **Phase 6 (Polish)**: ต้องรอทุก phase
### Within Phase 2
- T004 → T005 (sequential — เป็น paired methods)
- T006, T007 → parallel ได้
### Parallel Opportunities Per Phase
```
Phase 1: T001 ∥ T002 ∥ T003
Phase 2: (T004→T005) ∥ T006 ∥ T007
Phase 3: (T008→T009→T010) + T011 parallel กับ T010
Phase 4: T012 → T013 → T014
Phase 5: T015 ∥ T016; T017 manual last
Phase 6: T018 ∥ T019 ∥ T020 ∥ T021 ∥ T022 ∥ T023 → T024 last
```
---
## Implementation Strategy
### MVP First (US1 — OCR Model Switching)
1. Phase 1: Verify Modelfiles (T001-T002)
2. Phase 2: OllamaService + AiSettings (T004-T007)
3. Phase 3: OCR switching (T008-T011)
4. **Validate**: Test OCR job + confirm main reload
### Incremental Delivery
1. MVP (Phase 1-3) → OCR ทำงานกับ Typhoon
2. US2 (Phase 4) → Main model jobs ใช้ model ใหม่
3. US3 (Phase 5) → Admin health visibility
4. Polish (Phase 6) → Docs + type check + deploy
---
## Notes
- T001-T002: Model files มีอยู่แล้วใน `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/` — เป็น verification task ไม่ใช่ creation
- FR-008 (nomic-embed-text) + FR-009 (n8n boundary): Preserved by existing architecture — ไม่มี task เพราะ "do nothing" คือ correct action
- T018: ตรวจสอบ AGENTS.md version จริงก่อน bump เป็น v1.9.9
- T003: ตรวจสอบ `lcbp3-v1.9.0-schema-02-tables.sql` ก่อน write delta — ai_available_models schema อาจต่างจากที่คาด
- T013: ใช้ grep/code_search เพื่อหา references ทั้งหมด — อย่า hardcode path ไปเอง
- T017: Manual step; ต้องทำผ่าน DBA หรือ migration pipeline
- T024: Real-app verification ตาม `/check-real-app` workflow