Files
lcbp3/specs/06-Decision-Records/ADR-036-unified-ocr-architecture.md
T
admin 7e8f4859cd
CI / CD Pipeline / build (push) Failing after 6m24s
CI / CD Pipeline / deploy (push) Has been skipped
feat(ai): add ADR-036 unified OCR architecture and frontend test coverage
- 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
2026-06-14 06:34:07 +07:00

451 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)