feat(ai): add ADR-036 unified OCR architecture and frontend test coverage
CI / CD Pipeline / build (push) Failing after 6m24s
CI / CD Pipeline / deploy (push) Has been skipped

- Add ADR-036 unified OCR architecture (typhoon-ocr via Ollama)
- Extend AI execution profiles for OCR sandbox configuration
- Add comprehensive frontend test coverage (components, hooks, services)
- Add backend test coverage for document-numbering services
- Update OCR sidecar with typhoon-ocr integration
- Add AI policy service and execution profile management
- Update AGENTS.md and architecture documentation
This commit is contained in:
2026-06-14 06:34:07 +07:00
parent e3503b6a77
commit 7e8f4859cd
108 changed files with 33914 additions and 339 deletions
@@ -37,8 +37,8 @@
| 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 ทันที) |
| `np-dms-ai:latest` | Main AI (General + OCR Post-processing + Extraction + RAG Q&A) | `scb10x/typhoon2.5-qwen3-4b:latest` | ~2.5GB | Stand by ตลอด (ไม่ใช่ 0) |
| `np-dms-ocr:latest` | OCR ภาษาไทย | `scb10x/typhoon-ocr1.5-3b:latest` | ~3.2GB | `0` (unload ทันที) |
### Key Parameters (Main Model)
@@ -60,7 +60,7 @@ PARAMETER repeat_penalty 1.15
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
# ollama create np-dms-ai -f ./np-dms-ai.model.md
FROM scb10x/typhoon2.5-qwen3-4b:latest
@@ -92,7 +92,7 @@ Guidelines:
---
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
# ollama create np-dms-ocr -f ./np-dms-ocr.model.md
# ใส่ชื่อ tag โมเดล 3B ที่คุณต้องการจูนตรงนี้ได้เลย
FROM scb10x/typhoon-ocr1.5-3b:latest
@@ -143,18 +143,18 @@ async function processJob(job: Job) {
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);
await ollama.unloadModel('np-dms-ai');
await ollama.loadModel('np-dms-ocr', { keep_alive: 0 });
const result = await ollama.generate('np-dms-ocr', prompt);
// keep_alive: 0 จะ unload อัตโนมัติหลังเสร็จ
// โหลด main model กลับเข้า VRAM สำหรับงานถัดไป
await ollama.loadModel('typhoon2.5-np-dms');
await ollama.loadModel('np-dms-ai');
return result;
}
// Main model jobs: extraction, rag-query, ai-suggest
const result = await ollama.generate('typhoon2.5-np-dms', prompt);
const result = await ollama.generate('np-dms-ai', prompt);
return result;
}
```
@@ -173,7 +173,7 @@ async function processJob(job: Job) {
| File | Change |
|------|--------|
| `backend/src/modules/ai/services/ai-settings.service.ts` | Hardcode `DEFAULT_MODEL = 'typhoon2.5-np-dms:latest'` |
| `backend/src/modules/ai/services/ai-settings.service.ts` | Hardcode `DEFAULT_MODEL = 'np-dms-ai: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 ด้านบน |
@@ -188,8 +188,8 @@ async function processJob(job: Job) {
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
ollama create np-dms-ai -f ./np-dms-ai.model.md
ollama create np-dms-ocr -f ./np-dms-ocr.model.md
```
2. **QNAP Backend:** Deploy ด้วย code changes (ADR-033 mechanism ยังคงใช้ได้)
@@ -206,7 +206,7 @@ async function processJob(job: Job) {
หากพบปัญหา:
1. สร้าง custom model ใหม่จาก base model ตัวอื่น (เช่น กลับไป `gemma4:e2b`)
2. หรือแก้ไข `typhoon2.5-np-dms.model.md` แล้วสร้าง version ใหม่ (`:v2`)
2. หรือแก้ไข `np-dms-ai.model.md` แล้วสร้าง version ใหม่ (`:v2`)
3. Update code ให้ชี้ไป model ใหม่ แล้ว redeploy
---
@@ -217,7 +217,7 @@ async function processJob(job: Job) {
|-----|---------|--------|
| **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` |
| **ADR-032** | Typhoon OCR Integration | OCR model ถูกแทนที่โดย `np-dms-ocr` |
---
@@ -0,0 +1,450 @@
# ADR-036: Unified AI Model Architecture — Sandbox-Production Parity for np-dms-ai and np-dms-ocr
**Status:** Proposed
**Date:** 2026-06-13
**Decision Makers:** Development Team, AI Integration Lead
**Supersedes:** — (New Architecture)
**Amends:** AI model testing and parameter management layer
**Related Documents:**
- [ADR-034: AI Model Change](./ADR-034-AI-model-change.md)
- [ADR-033: Active Model & OCR Management](./ADR-033-active-model-and-ocr-management.md)
- [ADR-029: Dynamic Prompt Management](./ADR-029-dynamic-prompt-management.md)
- [CONTEXT.md](../../../CONTEXT.md)
> **Grilling resolution (2026-06-13):** ADR นี้เป็น **enhance** ของ Profile-Only Parameter Governance ที่มีอยู่ (`AiPolicyService` + `ai_execution_profiles`) **ไม่ใช่** การสร้าง `system_settings` param store ใหม่ และ**ไม่** supersede ADR-029/033. การตัดสินที่ resolved แล้ว: (1) production setting store = `ai_execution_profiles`; (2) **draft (sandbox) store = `ai_sandbox_profiles`** (แยกต่างหาก) — admin iterate ลง draft แล้วกด **Apply** = UPSERT draft → production row + DEL cache; (3) คง **Snapshot semantics** (params แช่แข็งลง job payload ณ dispatch); (4) systemPrompt อยู่ใน `ai_prompts` (Active Prompt) เท่านั้น; (5) OCR params = row `ocr-extract` + column `canonical_model`; (6) "OCR Sandbox" = **Production Pipeline Sandbox** (รัน pipeline เดียวกับ production). ดู `CONTEXT.md` → Flagged ambiguities + Glossary (from ADR-036).
---
## Context and Problem Statement
ปัจจุบันระบบใช้งานโมเดล AI สองตัวบน Desk-5439:
- `np-dms-ai:latest` — โมเดลหลักสำหรับงานทั่วไป (แทน `typhoon2.5-np-dms` ที่ยกเลิกแล้ว)
- `np-dms-ocr:latest` — โมเดลสำหรับ OCR (แทน `typhoon-np-dms-ocr` ที่ยกเลิกแล้ว)
**ปัญหาหลัก:**
1. **ชื่อโมเดลไม่สอดคล้อง** — Repository ยังใช้ชื่อเก่า `typhoon2.5-np-dms` และ `typhoon-np-dms-ocr` แต่ Desk-5439 ใช้ `np-dms-ai` และ `np-dms-ocr`
2. **ไม่มีกลไกทดสอบและบันทึกค่า** — Admin ไม่สามารถทดสอบ parameters (temperature, system prompt, etc.) ใน sandbox แล้ว apply ไป production ได้
3. **Sandbox กับ Production ใช้ params คนละชุด** — แม้ "OCR Sandbox" (`processSandboxExtract`/`processSandboxAiExtract`) จะรันเส้น pipeline เดียวกับ production (`processMigrateDocument`: OCR → Active Prompt → Master Data → LLM) แต่ sandbox **hardcode** `{ num_ctx: 16384, num_predict: 4096 }` ส่วน production ใช้ `snapshotParams` จาก profile → ผลทดสอบไม่สะท้อน production จริง (parity gap)
> **Concept (grilling resolved):** "OCR Sandbox" จริงๆ คือ **Production Pipeline Sandbox** — sandbox ของ production pipeline ทั้งเส้น (ต่างแค่ไม่ commit DB) ไม่ใช่เครื่องมือทดสอบ OCR อย่างเดียว ดู `CONTEXT.md` glossary.
---
## Decision Drivers
- **Sandbox-Production Parity:** ผลการทดสอบ parameters ทั้ง `np-dms-ai` และ `np-dms-ocr` ใน sandbox ต้องสามารถนำไปใช้ใน production ได้ 100%
- **Unified Testing & Apply Mechanism:** กลไกเดียวกันสำหรับการทดสอบและบันทึกค่า parameters ไปใช้ใน production ทั้งสองโมเดล
- **Dynamic Parameter Control:** Admin สามารถแก้ไข parameters (temperature, system prompt, etc.) ใน sandbox แล้ว apply ไป production ได้ทันที ทั้ง `np-dms-ai` และ `np-dms-ocr`
- **Sidecar-Centric Architecture:** ทุก AI operation ผ่าน sidecar (จัดการ model lifecycle เอง) ไม่ว่าจะเป็น `np-dms-ai` หรือ `np-dms-ocr`
---
## Decision Outcome
### 1. Calibration บน Profile/Prompt Store ที่มีอยู่ (enhance)
**ไม่สร้าง `AiModelService` + `system_settings` store ใหม่** — เติม write/apply path บนกลไกที่มี:
```
Draft → Apply → Production (2-layer):
┌──────────────────────────────────────────────┐
│ Sandbox: admin แก้ draft → ai_sandbox_profiles │ → persisted (ไม่กระทบ production)
│ Production Pipeline Sandbox อ่าน draft รันทดสอบ│
└──────────────────────────────────────────────┘
↓ (พอใจ → กด Apply to Production)
┌──────────────────────────────────────────────┐
│ applyProfile(): UPSERT ai_sandbox_profiles │ → ai_execution_profiles row
│ (+ DEL redis cache) │
│ systemPrompt → AiPromptService.activate │ → ai_prompts (ADR-029)
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Production job → createJobPayload() snapshot │ → params แช่แข็ง ณ dispatch (คงเดิม)
│ → processor → sidecar (np-dms-ai / np-dms-ocr) │
└──────────────────────────────────────────────┘
```
**SoT:** production = `ai_execution_profiles` (รวม row `ocr-extract`); draft = `ai_sandbox_profiles`; systemPrompt = `ai_prompts` — ไม่มี param store ใน `system_settings`
### 2. Data Flow — Test & Apply Pattern
**สำหรับทั้ง `np-dms-ai` และ `np-dms-ocr`:**
```
[Sandbox UI Testing]
[เลือกโมเดล: np-dms-ai หรือ np-dms-ocr]
[ปรับ parameters: temperature, systemPrompt, etc.]
[ทดสอบ → ดูผลลัพธ์]
↓ (พอใจผล)
[กด "Apply to Production"]
[runtime params → ai_execution_profiles (row ตาม canonical_model) + DEL redis cache]
[systemPrompt → ai_prompts (activate version, ADR-029)]
[Production Job → createJobPayload() snapshot params ณ dispatch → ใช้ค่าที่แช่แข็ง]
```
### 3. Parameter Scope
| โมเดล | Runtime params → `ai_execution_profiles` | systemPrompt → `ai_prompts` (ADR-029) |
|-------|-------------------------------------------|----------------------------------------|
| `np-dms-ai` | temperature, topP, repeatPenalty, numCtx, maxTokens, keepAliveSeconds — ต่อ ExecutionProfile ที่ apply | Active Prompt ต่อ `prompt_type` |
| `np-dms-ocr` | temperature, topP, repeatPenalty, keepAliveSeconds (row `ocr-extract`; `numCtx`/`maxTokens` = NULL) | Active Prompt `ocr_extraction` (`{{ocr_text}}`) |
**ลบทิ้ง:** key `AI_MODEL_NP_DMS_AI_DEFAULTS` / `AI_MODEL_NP_DMS_OCR_DEFAULTS` / `OCR_PRODUCTION_DEFAULTS` — ไม่ใช้ `system_settings` เป็น param store (OCR param set อ้างอิง sidecar contract `app.py`: `temperature`/`top_p`/`repeat_penalty`/`keep_alive`)
### 4. Parameter Hierarchy (ทั้งสองโมเดล)
| Level | Source | ใช้เมื่อไหร |
|-------|--------|------------|
| **Runtime Override** | Job payload | ส่งค่าพิเศษเฉพาะ job |
| **Production Defaults** | DB (`ai_execution_profiles` row, snapshot ณ dispatch) | ค่าที่ admin apply จาก sandbox |
| **Service Defaults** | Hardcoded `AiPolicyService.defaultProfiles` / Modelfile | Fallback ถ้าไม่มี row/cache |
---
## Implementation Details
### 1. Backend — Enhance AiPolicyService (ไม่สร้าง AiModelService)
**File:** `backend/src/modules/ai/services/ai-policy.service.ts` (MODIFY)
เติม write/apply method ลงบน service เดิม (ที่มี `getProfileParameters()` read path + Redis cache อยู่แล้ว):
- `getSandboxParameters(profileName)` — อ่าน draft จาก `ai_sandbox_profiles`; **ถ้าไม่มี draft → seed (clone) จาก production row** ใน `ai_execution_profiles` แล้ว return (ไม่ fallback hardcoded ก่อน)
- `saveSandboxDraft(profileName, params, userId)` — UPSERT draft ลง `ai_sandbox_profiles`
- `resetSandboxToProduction(profileName, userId)` — overwrite draft ด้วยค่า production row ปัจจุบัน
- `applyProfile(profileName, userId)` — copy draft จาก `ai_sandbox_profiles` → UPSERT `ai_execution_profiles` + `DEL ai_execution_profiles:{profile}` cache (admin only)
- `getCanonicalModelName()` / `getProfileParameters()` / `createJobPayload()`**คงเดิม** (snapshot semantics, อ่าน production)
**File:** `backend/src/modules/ai/entities/ai-execution-profile.entity.ts` (MODIFY)
- เพิ่ม column `canonicalModel: 'np-dms-ai' | 'np-dms-ocr'`
- ทำ `numCtx`/`maxTokens` เป็น nullable (OCR ไม่ใช้)
**File:** `backend/src/modules/ai/entities/ai-sandbox-profile.entity.ts` (NEW)
- mirror columns ของ `ai_execution_profiles` — เป็น **Sandbox Draft Profile** (persisted) ที่ admin iterate ก่อน Apply
- ค่าตั้งต้น **seed จาก production row** เมื่อยังไม่มี draft (ดู `getSandboxParameters()`) — ไม่เริ่มจากค่าว่าง/hardcoded
`applyProfile(profileName, userId)` อ่าน draft จาก `ai_sandbox_profiles` → UPSERT ลง `ai_execution_profiles` + DEL cache; `SandboxOcrEngineService` ที่มีอยู่ **คงไว้** (รับ params ที่ resolve จาก draft); systemPrompt apply → `ai_prompts` ผ่าน prompt service (ADR-029) **ไม่** เก็บใน profiles
### 2. Backend — Processor Updates
**File:** `backend/src/modules/ai/processors/ai-batch.processor.ts` (MODIFY)
**คงพฤติกรรมเดิม:** processor ใช้ `payload.snapshotParams` ที่ถูกแช่แข็งไว้ตอน dispatch (ไม่ lazy-read setting ตอน process) — ส่งต่อไป sidecar
**สิ่งที่ต้องเพิ่ม:** ให้ `createJobPayload('ocr-extract')` ดึง params จาก row `ocr-extract` (canonical_model = np-dms-ocr) แทนการยืม profile `standard`
**ปิด parity gap (สำคัญ):** `processSandboxExtract` / `processSandboxAiExtract` ต้อง**เลิก hardcode** `{ num_ctx: 16384, num_predict: 4096 }` แล้วสร้าง `generateOptions` จาก **`ai_sandbox_profiles`** (Sandbox Draft Profile, schema เดียวกับ `ai_execution_profiles`) เพื่อให้ admin เห็นผลของค่าที่กำลังปรับก่อน Apply — ส่วน `processMigrateDocument` (production) อ่านจาก `ai_execution_profiles` ผ่าน snapshot เหมือนเดิม. หลัง Apply ค่าทั้งสองตารางจะตรงกัน → parity จริง
Sidecar จะรวม parameters จาก request เข้ากับ defaults ใน Modelfile
#### 2.1 Dual-Model Snapshot & OCR Param Flow (Gap 14 resolved)
`migrate-document`/`auto-fill-document` เป็น **dual-model job** (OCR `np-dms-ocr` + LLM `np-dms-ai`) แต่ `createJobPayload` เดิม snapshot params **ชุดเดียว** (LLM) → OCR step ไม่ได้รับ tunable params ที่ admin ปรับ. แก้ดังนี้:
- **Gap 4 — OCR row แยกจาก `ExecutionProfile`:** `ocr-extract` เป็น **model-defaults row** (key ด้วย `canonical_model`/`profile_name='ocr-extract'`) **ไม่ใช่** สมาชิกของ `ExecutionProfile` union (คง Canonical Profile Set = interactive/standard/quality/deep-analysis). เพิ่ม accessor `getModelDefaults('np-dms-ocr')` แยกจาก `getProfileParameters(profile)`
- **Gap 3 — snapshot 2 ชุด (backward-compat):** `AiJobPayload` คง `snapshotParams` (LLM, ไม่แตะ processor LLM path) + เพิ่ม **`ocrSnapshotParams?: OcrTyphoonOptions`** (reuse type ที่มีอยู่ = `{ temperature, topP, repeatPenalty }`). populate `ocrSnapshotParams` เมื่อ pipeline ของ job รัน OCR (`migrate-document`/`auto-fill-document`/`ocr-extract`)
- **Gap 1 — wire ไป production OCR:** `OcrDetectionInput` เพิ่ม `typhoonOptions?: OcrTyphoonOptions`; `OcrService.processWithTyphoon` append `temperature`/`topP`/`repeatPenalty` ลง form (sidecar `/ocr-upload` รับอยู่แล้ว); `processMigrateDocument` ส่ง `typhoonOptions: job.data.ocrSnapshotParams`
- **Gap 2 — keep_alive ไม่ freeze:** กฎ **quality params freeze / resource params lazy** — temperature/top_p/repeat/num_ctx/max_tokens แช่แข็ง ณ dispatch; **keep_alive มาจาก `calculateOcrResidency()` (Adaptive OCR Residency, ADR-033) ณ process time** ไม่อยู่ใน tunable set (สอดคล้อง `OcrTyphoonOptions` ที่ไม่มี keep_alive อยู่แล้ว)
- **Audit:** `snapshotParamsJson = { ...llmParams, ocr: ocrSnapshotParams }` ใน audit row เดียว (per-step error log คงเดิม)
#### 2.2 Master Data Context Parity (Gap 5 resolved)
`processSandboxExtract`/`processSandboxAiExtract` ปัจจุบันใช้ `projectPublicId='default'` → ส่ง `undefined` ไป `aiPromptsService.resolveContext`**skip master data lookup** (`ai-batch.processor.ts:552-557, 758-762`). ส่วน `processMigrateDocument` ส่ง `projectPublicId` + `contractPublicId` จริงเสมอ (`:973-978`).
`{{master_data_context}}` ใน prompt **ต่างกัน** แม้ params ถูกต้อง → Production Pipeline Sandbox **ไม่สมบูรณ์**
**แก้:**
- Sandbox UI ให้ admin เลือก `projectPublicId` (และ `contractPublicId` optional) ก่อนรันทดสอบ — ไม่อนุญาต `'default'`
- `processSandboxExtract`/`processSandboxAiExtract` ส่ง ID จริงไป `resolveContext` เสมอ — ไม่มี special case `'default'``undefined`
- `aiPromptsService.resolveContext` จะคืนค่า empty context (`{}`) ถ้า project/contract ไม่มี master data (production-ready behavior)
#### 2.3 Apply Guardrails (Gap 6 resolved)
Apply to Production เป็น **critical config change** (กระทบงานทั้งระบบ) ต้องมี guardrails ตาม AGENTS.md:
| Guardrail | Requirement | Implementation |
|-----------|-------------|----------------|
| **Idempotency** | `POST /api/ai/profiles/:profileName/apply` ต้อง validate `Idempotency-Key` header (mandatory per AGENTS.md) | `@Header('Idempotency-Key')` + Redis เก็บ key ที่ใช้แล้ว 5 นาที |
| **CASL Permission** | API ใหม่ต้องมี CASL guard + 4-Level RBAC | `@UseGuards(CaslGuard)` + action `ai.apply_profile` (subject: `SystemSettings`) — ใช้ permission `system.manage_ai` (admin) |
| **Param Validation** | class-validator (backend) + Zod (frontend) | DTO `ApplyProfileDto` ใช้ `@IsNumber()`, `@Min(0)`, `@Max(1)` สำหรับ temperature/topP; `@IsOptional()` สำหรับ nullable |
| **Audit Trail** | Log ใคร apply, อะไร, old→new | `ai_audit_logs` table (มีอยู่แล้ว) — เพิ่ม row `action='APPLY_PROFILE'`, `userPublicId`, `profileName`, `oldValuesJson`, `newValuesJson`, `appliedAt` |
| **Range Guard** | Temperature/topP ต้อง 01 | Service layer validation: `if (temp < 0 \|\| temp > 1) throw BusinessException` |
#### 2.4 Entity & Service Canonical Model (Gap 7 resolved)
`AiExecutionProfileEntity` ปัจจุบันไม่มี mapping สำหรับ `canonical_model` column (ที่จะเพิ่มใน SQL delta); `getProfileParameters` (`:125`) hardcode `canonicalModel: 'np-dms-ai'` แทนการอ่านจาก column → ถ้า row `ocr-extract` (canonical_model='np-dms-ocr') ถูกอ่านผ่าน path เดิม จะได้ค่าผิด
**แก้:**
- Entity เพิ่ม `@Column({ name: 'canonical_model', length: 20 }) canonicalModel!: string;`
- `getProfileParameters` เปลี่ยนเป็นอ่าน `dbProfile.canonicalModel` จาก column แทน hardcode (หรือ default เป็น `'np-dms-ai'` ถ้า column null)
- สร้าง accessor ใหม่ `getModelDefaults(canonicalModel: 'np-dms-ai' | 'np-dms-ocr')` ที่ query ตาม `canonical_model` column โดยตรง (สำหรับ model-defaults row ไม่ผ่าน ExecutionProfile)
**API signature:**
```typescript
@Post(':profileName/apply')
@UseGuards(CaslGuard)
async applyProfile(
@Param('profileName') profileName: string,
@Body() dto: ApplyProfileDto, // optional: { reason?: string }
@Headers('Idempotency-Key') idempotencyKey: string,
@CurrentUser() user: RequestWithUser,
): Promise<ApplyResultDto>
```
### 3. Backend — API Endpoints
**File:** `backend/src/modules/ai/controllers/ai.controller.ts` (ADD)
เพิ่ม endpoints สำหรับการทดสอบและบันทึกค่า parameters:
- `GET /api/ai/sandbox-profiles/:profileName` — ดึง draft; **ถ้าไม่มี → seed จาก production row** แล้ว return
- `PUT /api/ai/sandbox-profiles/:profileName` — บันทึก draft ลง `ai_sandbox_profiles` (admin only)
- `POST /api/ai/sandbox-profiles/:profileName/reset` — reset draft = ค่า production row ปัจจุบัน
- `POST /api/ai/profiles/:profileName/apply`**Apply to Production**: UPSERT draft → `ai_execution_profiles` + DEL cache (admin only, CASL-guarded)
- `GET /api/ai/profiles/:profileName` — ดึงค่า production defaults ปัจจุบัน (read-only panel)
systemPrompt apply → ใช้ endpoint ของ ADR-029 (`ai_prompts`) ที่มีอยู่ — **ไม่** สร้าง prompt endpoint ซ้ำ
**คงไว้:** API submit job ยังปฏิเสธ (400) ถ้า caller แนบ `executionProfile`/`model`/`temperature` (Profile-Only Parameter Governance)
### 4. Backend — Service Wiring (ไม่ consolidate/ลบ)
**Keep:** `backend/src/modules/ai/services/sandbox-ocr-engine.service.ts` — ยังใช้รับ ephemeral OCR override (temperature/topP/repeatPenalty) ไป sidecar; ไม่ลบ
**Modify:** `backend/src/modules/ai/ai.module.ts`
- ไม่ลบ provider เดิม; AiPolicyService มีอยู่แล้ว
**Modify:** `backend/src/modules/ai/controllers/ai-sandbox.controller.ts`
- เพิ่ม apply endpoint ที่เรียก `AiPolicyService.applyProfile()` (CASL admin) — ไม่ inject service ใหม่
### 5. Sidecar — Dynamic Params (คง endpoint เดิม)
**File:** `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` (MODIFY ถ้าจำเป็น)
sidecar รับ override อยู่แล้ว (`/ocr`, `/ocr-upload``temperature`/`top_p`/`repeat_penalty`/`keep_alive`) — ไม่ต้องสร้าง `/generate` ใหม่
- ถ้า np-dms-ai ต้องการ dynamic params เพิ่มเติม → ขยาย contract ของ endpoint ที่มี ไม่สร้างใหม่
- model lifecycle (unload/load) ตาม ADR-033 Adaptive OCR Residency **คงเดิม**
### 6. Frontend — Admin AI Console
**File:** `frontend/lib/services/admin-ai.service.ts` (ADD)
เพิ่ม functions สำหรับการทดสอบและบันทึกค่า parameters:
- `testModel(modelName, options)` — ทดสอบโมเดลด้วย parameters ที่กำหนด
- `saveModelDefaults(modelName, params)` — บันทึกค่า parameters ไปใช้ใน production
- `getModelDefaults(modelName)` — ดึงค่า parameters ปัจจุบันที่ใช้ใน production
**File:** `frontend/components/admin/ai/ModelTestingPanel.tsx` (NEW) หรือปรับ `OcrSandboxPromptManager.tsx`
สร้าง UI สำหรับทดสอบและบันทึกค่า parameters รองรับทั้งสองโมเดล:
- **Model Selector** — Dropdown เลือก `np-dms-ai` หรือ `np-dms-ocr`
- **Model Parameters** — Inputs สำหรับ temperature, topP, repeatPenalty
- **System Prompt** — Textarea สำหรับแก้ไข system prompt
- **Test Area** — พื้นที่ทดสอบ input และดูผลลัพธ์
- **Current Production Defaults** — Read-only panel แสดงค่าที่ใช้ใน production
- **Apply to Production** — Button สำหรับบันทึกค่าปัจจุบันไป production
### 7. Database — Extend ai_execution_profiles (ไม่ใช้ system_settings)
**Delta (ADR-009, edit SQL directly):** `specs/03-Data-and-Storage/deltas/2026-06-13-extend-ai-execution-profiles-ocr.sql`
```sql
-- ADR-036: ขยาย ai_execution_profiles → รองรับ np-dms-ocr (canonical_model) + OCR row
ALTER TABLE ai_execution_profiles
ADD COLUMN canonical_model VARCHAR(20) NOT NULL DEFAULT 'np-dms-ai' AFTER profile_name;
-- OCR ไม่ใช้ num_ctx/max_tokens → ทำเป็น nullable
ALTER TABLE ai_execution_profiles MODIFY COLUMN num_ctx INT NULL;
ALTER TABLE ai_execution_profiles MODIFY COLUMN max_tokens INT NULL;
-- seed row OCR (params ตาม sidecar contract: temperature/top_p/repeat_penalty/keep_alive)
INSERT INTO ai_execution_profiles
(profile_name, canonical_model, temperature, top_p, max_tokens, num_ctx, repeat_penalty, keep_alive_seconds, is_active)
VALUES
('ocr-extract', 'np-dms-ocr', 0.100, 0.100, NULL, NULL, 1.100, 0, 1)
ON DUPLICATE KEY UPDATE canonical_model = VALUES(canonical_model);
-- draft (sandbox) store — mirror columns ของ production; admin iterate ก่อน Apply
CREATE TABLE ai_sandbox_profiles (
id INT AUTO_INCREMENT PRIMARY KEY,
profile_name VARCHAR(50) NOT NULL UNIQUE,
canonical_model VARCHAR(20) NOT NULL DEFAULT 'np-dms-ai',
temperature DECIMAL(4,3) NOT NULL,
top_p DECIMAL(4,3) NOT NULL,
max_tokens INT NULL,
num_ctx INT NULL,
repeat_penalty DECIMAL(5,3) NOT NULL,
keep_alive_seconds INT NOT NULL,
updated_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
**ไม่มี** INSERT ลง `system_settings` สำหรับ parameter — systemPrompt จัดการผ่าน `ai_prompts` (ADR-029)
---
## Migration Plan
### Phase 1: Backend Enhance (ไม่มี breaking change)
1. รัน delta `2026-06-13-extend-ai-execution-profiles-ocr.sql` — เพิ่ม `canonical_model` + row `ocr-extract`
2. แก้ `ai-execution-profile.entity.ts` — เพิ่ม `canonicalModel`, ทำ `numCtx`/`maxTokens` nullable
3. เติม `AiPolicyService.applyProfile()` — write + invalidate cache
4. แก้ `createJobPayload('ocr-extract')` — ดึงจาก row `ocr-extract` (snapshot คงเดิม)
5. เพิ่ม apply/test/get endpoints ใน controller (CASL admin)
### Phase 2: Sidecar (ถ้าจำเป็น)
1. คง `/ocr`, `/ocr-upload` ที่รับ override อยู่แล้ว — ขยาย contract เฉพาะถ้า np-dms-ai ต้องการ
2. model lifecycle ตาม ADR-033 คงเดิม
### Phase 3: Frontend Update
1. เพิ่ม API functions ใน `admin-ai.service.ts` — apply/test/get profile
2. ปรับ `OcrSandboxPromptManager.tsx` หรือเพิ่ม panel — รองรับ apply runtime params (np-dms-ai + ocr-extract)
3. systemPrompt ใช้ Prompt Version UI เดิม (ADR-029)
### Phase 4: Data
1. row `ocr-extract` seed ผ่าน delta (Phase 1); ค่า np-dms-ai profiles เดิมไม่ต้อง migrate
2. ถ้าไม่มี row/cache → fallback `AiPolicyService.defaultProfiles`
---
## Rollback Strategy
หากพบปัญหา:
1. **Immediate:** Revert commit — กลับไปใช้การเรียก Ollama โดยตรง (แต่จะสูญเสียความสามารถในการปรับ parameters แบบ dynamic)
2. **Sidecar:** Rollback `app.py` ไป version เดิมที่ไม่รองรับ dynamic parameters
3. **Database:** rollback delta — ลบ column `canonical_model` + row `ocr-extract` (rollback SQL คู่กัน); ค่า np-dms-ai profiles เดิมไม่กระทบ
---
## Impact on Related ADRs
| ADR | Section | Impact |
|-----|---------|--------|
| **ADR-034** | Model Stack | **ต้องแก้** — canonical names `np-dms-ai`/`np-dms-ocr` (runtime tag เป็น ops detail ใน Modelfile/ENV) |
| **ADR-033** | Adaptive OCR Residency | **คงเดิม** — ไม่แตะ residency/model lifecycle; ADR-036 เติมแค่ write/apply path |
| **ADR-032** | Typhoon OCR Integration | **คงเดิม** — sidecar contract เดิม (`/ocr`, `/ocr-upload`) |
| **ADR-029** | Dynamic Prompt Management | **คงเดิม** — systemPrompt apply ผ่าน `ai_prompts` (Active Prompt) ที่มีอยู่ |
| **Profile-Only Governance** | `AiPolicyService` + `ai_execution_profiles` | **enhance** — เติม write path, ไม่ supersede |
---
## Glossary Updates (CONTEXT.md)
บันทึกแล้วใน `CONTEXT.md`**Glossary Updates (from ADR-036)** + **Flagged ambiguities**:
| Term | Definition |
|------|------------|
| **Apply to Production** | admin บันทึกค่าที่ทดสอบใน sandbox → runtime params ลง `ai_execution_profiles` (+invalidate Redis), systemPrompt ลง `ai_prompts`; มีผลกับงานใหม่เท่านั้น (snapshot) |
| **Sandbox Parameter Override** | ค่า ephemeral จาก testing ที่ไม่ persist จนกว่าจะกด Apply |
| **Tunable Production Defaults** | row ใน `ai_execution_profiles` (รวม `ocr-extract`) — ไม่ใช่ store แยกใน `system_settings` |
---
## Files to Modify
| File | Change Type |
|------|-------------|
| `backend/src/modules/ai/services/ai-policy.service.ts` | MODIFY (เพิ่ม `applyProfile()`) |
| `backend/src/modules/ai/entities/ai-execution-profile.entity.ts` | MODIFY (+`canonicalModel`, nullable numCtx/maxTokens) |
| `backend/src/modules/ai/entities/ai-sandbox-profile.entity.ts` | NEW (draft store) |
| `backend/src/modules/ai/interfaces/execution-policy.interface.ts` | MODIFY (+`ocrSnapshotParams?` ใน AiJobPayload — **ไม่** เพิ่ม `ocr-extract` ใน ExecutionProfile) |
| `backend/src/modules/ai/services/ocr.service.ts` | MODIFY (+`typhoonOptions` ใน OcrDetectionInput; processWithTyphoon ส่ง temp/topP/repeat) |
| `backend/src/modules/ai/processors/ai-batch.processor.ts` | MODIFY (createJobPayload OCR snapshot; processMigrateDocument ส่ง typhoonOptions; sandbox อ่าน draft) |
| `backend/src/modules/ai/controllers/ai.controller.ts` | MODIFY (apply/test/get endpoints, CASL admin, **Gap 6:** Idempotency-Key validation) |
| `backend/src/modules/ai/dto/apply-profile.dto.ts` | NEW (**Gap 6:** class-validator `@Min(0) @Max(1)` สำหรับ params) |
| `backend/src/modules/ai/dto/apply-result.dto.ts` | NEW (return applied profile + audit log id) |
| `backend/src/modules/ai/controllers/ai-sandbox.controller.ts` | MODIFY |
| `backend/src/modules/ai/services/sandbox-ocr-engine.service.ts` | **KEEP** (ephemeral override) |
| `backend/src/modules/ai/services/ollama.service.ts` | MODIFY (ENV/Modelfile tag เท่านั้น — runtime detail) |
| `frontend/lib/services/admin-ai.service.ts` | MODIFY |
| `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` | MODIFY (เพิ่ม apply runtime params; **Gap 5:** เพิ่ม project/contract selector ไม่อนุญาต 'default') |
| `specs/03-Data-and-Storage/deltas/2026-06-13-extend-ai-execution-profiles-ocr.sql` (+rollback) | NEW |
| `CONTEXT.md` | MODIFY (Glossary + Flagged ambiguities — **done**) |
| `specs/06-Decision-Records/ADR-034-AI-model-change.md` | MODIFY (canonical names) |
| `AGENTS.md` | MODIFY (canonical names) |
---
---
## Required Naming Alignment (Mandatory Before Implementation)
> **หมายเหตุ (grilling):** การเปลี่ยนชื่อในส่วนนี้คือการ sync **runtime tag** (Ollama model name บน Desk-5439 + ENV `OLLAMA_MODEL_MAIN`/`OLLAMA_MODEL_OCR`) ให้ตรงกับ **Canonical Model Identity** (`np-dms-ai`/`np-dms-ocr`) ตาม **Single-Name Canonical Model Policy** — ไม่ใช่การสร้าง canonical mapping ใหม่ เพราะ `AiPolicyService.getCanonicalModelName()` map tag → canonical อยู่แล้ว (รองรับทั้ง tag เก่า/ใหม่). Mock ใน test ที่ใช้ `typhoon2.5-np-dms:latest` เป็น runtime tag ของ `/api/ps` ไม่จำเป็นต้องแก้ (mapper รองรับอยู่).
ชื่อ model บน Desk-5439 (Ollama) ได้เปลี่ยนเป็น canonical names ใหม่แล้ว ต้องอัปเดต repository ให้สอดคล้อง:
### Model Names (New Canonical)
| Role | Old Name | New Name (Desk-5439) | Status |
|------|----------|----------------------|--------|
| Main AI | `typhoon2.5-np-dms:latest` | `np-dms-ai:latest` | **ต้องแก้** |
| OCR | `typhoon-np-dms-ocr:latest` | `np-dms-ocr:latest` | **ต้องแก้** |
| Embedding | `nomic-embed-text` | (ไม่เปลี่ยน) | OK |
### Files ที่ต้องแก้ไขชื่อ Model
#### Backend (Code)
| File | Line | แก้จาก | เป็น |
|------|------|--------|------|
| `backend/src/modules/ai/services/ollama.service.ts` | 58 | `'typhoon2.5-np-dms:latest'` | `'np-dms-ai:latest'` |
| `backend/src/modules/ai/services/ollama.service.ts` | 62 | `'typhoon-np-dms-ocr:latest'` | `'np-dms-ocr:latest'` |
| `backend/src/modules/ai/services/ocr.service.ts` | 86 | `engineName: 'typhoon-np-dms-ocr:latest'` | `engineName: 'np-dms-ocr:latest'` |
| `backend/src/modules/ai/services/ai-settings.service.ts` | (ค้นหา) | `typhoon2.5-np-dms` | `np-dms-ai` |
| `backend/src/modules/ai/processors/ai-batch.processor.spec.ts` | 70 | `'typhoon2.5-np-dms:latest'` | `'np-dms-ai:latest'` |
| `backend/src/modules/ai/processors/ai-batch.processor.spec.ts` | 70 | `'typhoon-np-dms-ocr:latest'` | `'np-dms-ocr:latest'` |
#### Frontend
| File | แก้จาก | เป็น |
|------|--------|------|
| `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` | `typhoon-np-dms-ocr` | `np-dms-ocr` |
| `frontend/app/(admin)/admin/ai/page.tsx` | `typhoon2.5-np-dms` | `np-dms-ai` |
#### Sidecar (OCR)
| File | แก้จาก | เป็น |
|------|--------|------|
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` | `typhoon-np-dms-ocr` | `np-dms-ocr` |
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/docker-compose.yml` | `typhoon-np-dms-ocr` | `np-dms-ocr` |
#### Documentation
| File | แก้จาก | เป็น |
|------|--------|------|
| `specs/06-Decision-Records/ADR-034-AI-model-change.md` | `typhoon2.5-np-dms` | `np-dms-ai` |
| `specs/06-Decision-Records/ADR-034-AI-model-change.md` | `typhoon-np-dms-ocr` | `np-dms-ocr` |
| `AGENTS.md` | `typhoon2.5-np-dms` | `np-dms-ai` |
| `AGENTS.md` | `typhoon-np-dms-ocr` | `np-dms-ocr` |
### Migration Steps (Naming Alignment)
1. **Desk-5439:** สร้างโมเดลใหม่ (ถ้ายังไม่มี)
```bash
# สร้างจาก Modelfile ที่มีอยู่
ollama create np-dms-ai -f ./np-dms-ai.model.md
ollama create np-dms-ocr -f ./np-dms-ocr.model.md
# ลบโมเดลเก่า (optional — รอ deploy สำเร็จก่อน)
ollama rm typhoon2.5-np-dms typhoon-np-dms-ocr
```
2. **Repository:** แก้ไขทุกไฟล์ที่ระบุในตารางด้านบน
3. **Deploy:** ทดสอบว่า API เรียกโมเดลใหม่ได้
4. **Cleanup:** ลบโมเดลเก่าบน Desk-5439 (หลัง verify สำเร็จ)
---
**สำหรับ Implementation:** ดูไฟล์ใน `specs/200-fullstacks/236-unified-ocr-architecture/` (สร้างเมื่อเริ่ม implement)