690611:1705 ADR-035-235 #00 [skip CI]
CI / CD Pipeline / build (push) Has been skipped
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-06-11 17:05:17 +07:00
parent cd7d20ccd4
commit 71c5e88181
14 changed files with 1422 additions and 682 deletions
@@ -0,0 +1,40 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/checklists/requirements.md
// Change Log:
// - 2026-06-11: Initial spec quality checklist
# Specification Quality Checklist: AI Runtime Policy Refactor
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-06-11
**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
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows (5 user stories covering all 5 workstreams)
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Spec draws from grilling session output (AI-Refactor.md) — all ambiguities resolved per CONTEXT.md flagged items
- Canonical terminology from CONTEXT.md Glossary Updates (ADR-034) used throughout
- Big bang cutover gate explicitly captured in US5
@@ -0,0 +1,51 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/contracts/create-ai-job.dto.md
// Change Log:
// - 2026-06-11: API contract for CreateAiJobDto
# Contract: POST /api/ai/jobs
## Request DTO
```typescript
interface CreateAiJobRequest {
type: 'auto-fill-document' | 'migrate-document' | 'rag-query';
documentPublicId?: string; // UUIDv7 — ADR-019
attachmentPublicId?: string; // UUIDv7 — ADR-019
executionProfile?: 'fast' | 'balanced' | 'thai-accurate' | 'large-context';
// [FORBIDDEN] model.key — HTTP 400 if present
// [FORBIDDEN] temperature, top_p, maxTokens — HTTP 400 if present
}
```
## Validation Rules
| Field | Rule |
|-------|------|
| `type` | Required; enum |
| `executionProfile` | Optional; enum; defaults to `balanced` |
| `large-context` | Requires admin role (CASL `ai.use_large_context`) — HTTP 403 if unauthorized |
| `model.*` | ANY model subfield → HTTP 400 |
| `temperature` | Present at root → HTTP 400 |
| `top_p` | Present at root → HTTP 400 |
| `maxTokens` | Present at root → HTTP 400 |
## Response DTO
```typescript
interface AiJobResponse {
jobId: string; // BullMQ job ID
status: 'queued' | 'completed' | 'failed';
modelUsed: 'np-dms-ai' | 'np-dms-ocr'; // Canonical name — never runtime tag
executionProfile: ExecutionProfile; // Effective profile (after backend override)
queueName: 'ai-realtime' | 'ai-batch';
}
```
## Error Responses
| Status | When |
|--------|------|
| 400 | `model.key` present, or parameter overrides present, or invalid `executionProfile` |
| 403 | `large-context` by non-admin |
| 422 | `documentPublicId` not found |
| 504 | CPU fallback retrieval timeout |
@@ -0,0 +1,131 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/data-model.md
// Change Log:
// - 2026-06-11: Data model for AI Runtime Policy Refactor
# Data Model: AI Runtime Policy Refactor
> หมายเหตุ: Feature นี้ไม่เพิ่ม schema DB ใหม่ (ADR-009 compliant) — เปลี่ยนเฉพาะ TypeScript interfaces, DTO shapes, และ Python data structures บน sidecar
---
## TypeScript Types (Backend)
### ExecutionProfile (enum)
```typescript
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
export type ExecutionProfile = 'fast' | 'balanced' | 'thai-accurate' | 'large-context';
```
### RuntimePolicy (interface)
```typescript
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
export interface RuntimePolicy {
canonicalModel: 'np-dms-ai' | 'np-dms-ocr'; // ชื่อ canonical เท่านั้น
temperature: number;
topP: number;
maxTokens: number;
keepAliveSeconds: number; // สำหรับ main model
}
```
### OcrResidencyDecision (interface)
```typescript
// File: backend/src/modules/ai/interfaces/ocr-residency.interface.ts
export interface OcrResidencyDecision {
keepAliveSeconds: number; // 0 = unload; > 0 = residency window
vramHeadroomMb: number; // หรือ -1 ถ้า query ล้มเหลว
activeProfile: ExecutionProfile | null;
reason: 'large-context-active' | 'high-pressure' | 'headroom-sufficient' | 'query-failed';
}
```
### VramHeadroom (interface)
```typescript
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
export interface VramHeadroom {
totalMb: number; // ค่า total VRAM (hardcoded จาก env)
usedMb: number; // ค่าจาก Ollama /api/ps
availableMb: number; // totalMb - usedMb
querySuccess: boolean; // false = ใช้ safe default
}
```
### CreateAiJobDto (updated)
```typescript
// File: backend/src/modules/ai/dto/create-ai-job.dto.ts
// [CHANGE] ลบ model field และ parameter overrides ออก
export class CreateAiJobDto {
@IsEnum(['auto-fill-document', 'migrate-document', 'rag-query'])
type: 'auto-fill-document' | 'migrate-document' | 'rag-query';
@IsOptional()
@IsUUID('all')
documentPublicId?: string;
@IsOptional()
@IsUUID('all')
attachmentPublicId?: string;
@IsOptional()
@IsEnum(['fast', 'balanced', 'thai-accurate', 'large-context'])
executionProfile?: ExecutionProfile;
// [REMOVED] model: { key, parameters } — ไม่อนุญาตแล้ว
}
```
---
## Python Types (OCR Sidecar)
### VramHeadroom (dataclass)
```python
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/vram_monitor.py
@dataclass
class VramHeadroom:
total_mb: float
used_mb: float
available_mb: float
query_success: bool
```
### OcrResidencyPolicy (dataclass)
```python
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py
@dataclass
class OcrResidencyDecision:
keep_alive_seconds: int # 0 = unload
vram_headroom_mb: float
reason: str # 'large-context-active' | 'high-pressure' | 'headroom-sufficient' | 'query-failed'
```
### EmbedRequest (updated)
```python
# ไม่มี model selection field — backend policy กำหนด model ทั้งหมด
class EmbedRequest(BaseModel):
texts: List[str]
# [NO model field] — device selection เป็น internal logic ของ sidecar
```
---
## ai_audit_logs — เพิ่ม Fields (ไม่เปลี่ยน schema, เปลี่ยน payload JSON)
```text
ai_audit_logs.metadata (JSON column ที่มีอยู่แล้ว) จะเพิ่ม fields:
- modelUsed: "np-dms-ai" | "np-dms-ocr" (canonical name เสมอ)
- executionProfile: ExecutionProfile
- ocrResidencyDecision: OcrResidencyDecision (สำหรับ OCR jobs)
- retrievalDevice: "gpu" | "cpu" (สำหรับ RAG jobs)
- vramHeadroomMb: number (ขณะ job เริ่มรัน)
```
> ใช้ JSON column ที่มีอยู่ — ไม่ต้อง ALTER TABLE (ADR-009 compliant)
@@ -0,0 +1,170 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/plan.md
// Change Log:
// - 2026-06-11: Initial implementation plan for AI Runtime Policy Refactor
# Implementation Plan: AI Runtime Policy Refactor
**Branch**: `235-ai-runtime-policy-refactor` | **Date**: 2026-06-11 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/200-fullstacks/235-ai-runtime-policy-refactor/spec.md`
## Summary
Refactor AI runtime ของ LCBP3-DMS ให้รองรับ GPU ใหม่ (RTX 5060 Ti 16GB) โดย: (A) เปลี่ยน API contract ให้ใช้ `executionProfile` แทน caller-driven model selection, (B) สร้าง backend policy mapping layer, (C) เพิ่ม adaptive OCR residency, (D) เพิ่ม CPU fallback สำหรับ retrieval acceleration, และ (E) ปรับ BullMQ queue concurrency พร้อม verification suite ครอบคลุม big bang cutover gate ทั้ง 4 แกน
---
## Technical Context
**Language/Version**: TypeScript 5.x (NestJS 10, Next.js 14), Python 3.11 (OCR sidecar FastAPI)
**Primary Dependencies**:
- Backend: NestJS, BullMQ, TypeORM, CASL, class-validator, class-transformer
- Frontend: Next.js, TanStack Query, Zod, shadcn/ui
- Sidecar: FastAPI, PyMuPDF (fitz), typhoon-ocr, httpx, FlagEmbedding
- Infrastructure: Ollama (Desk-5439), Redis, MariaDB
**Storage**: MariaDB (ai_audit_logs, ai_prompts, ai_intent_patterns), Redis (BullMQ, cache)
**Testing**: Jest (backend unit/integration), Vitest (frontend), Pytest (sidecar)
**Target Platform**: QNAP NAS (backend/frontend containers), Desk-5439 (Ollama + OCR sidecar)
**Performance Goals**: OCR cold start < 5s (with residency), retrieval CPU fallback < 30s timeout
**Constraints**: Big bang rollout — no legacy parallel path; LLM-First GPU ownership must be enforced
**Scale/Scope**: Single-server AI stack on Desk-5439; BullMQ concurrency max `ai-realtime: 2`, `ai-batch: 1`
---
## Constitution Check
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
| Rule | Status | Notes |
|------|--------|-------|
| ADR-019 UUID: no parseInt on UUID | ✅ Pass | No new UUID handling in this feature |
| ADR-009: No TypeORM migrations | ✅ Pass | No schema changes required |
| ADR-016 Security: CASL Guard on all API | ✅ Required | `large-context` profile must have CASL admin check |
| ADR-007 Error Handling: layered classification | ✅ Required | 400 (validation), 403 (profile auth), 504 (CPU timeout) |
| ADR-008 BullMQ: no inline jobs | ✅ Pass | Queue policy adjustment, not new inline processing |
| ADR-023/023A AI Boundary: no direct DB/storage | ✅ Pass | Policy layer stays in NestJS service |
| ADR-023A BullMQ 2-queue: ai-realtime + ai-batch | ✅ Required | concurrency adjustment within existing queues |
| ADR-002 Doc Numbering: Redis Redlock | ✅ N/A | Not applicable to this feature |
| TypeScript: no `any`, no `console.log` | ✅ Required | All new TypeScript code must comply |
| File headers: `// File: path/filename` | ✅ Required | All new files must have header |
**No constitution violations.** Proceeding to Phase 0.
---
## Project Structure
### Documentation (this feature)
```text
specs/200-fullstacks/235-ai-runtime-policy-refactor/
├── spec.md # Feature specification
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── tasks.md # Phase 2 output
├── checklists/
│ └── requirements.md
└── contracts/
├── create-ai-job.dto.ts.md
├── execution-policy.interface.ts.md
└── ocr-residency-policy.interface.ts.md
```
### Source Code (repository root)
```text
backend/src/modules/ai/
├── dto/
│ ├── create-ai-job.dto.ts # [MODIFY] เอา model.key ออก, เพิ่ม executionProfile
│ └── ai-job-response.dto.ts # [MODIFY] เพิ่ม modelUsed canonical name
├── services/
│ ├── ai.service.ts # [MODIFY] เพิ่ม profile validation + canonical name
│ ├── ai-policy.service.ts # [NEW] ExecutionProfile → RuntimePolicy mapping
│ ├── ocr.service.ts # [MODIFY] เพิ่ม adaptive residency calculation
│ └── vram-monitor.service.ts # [NEW] VRAM headroom query service
├── processors/
│ ├── ai-batch.processor.ts # [MODIFY] ใช้ policy จาก AiPolicyService
│ └── ai-realtime.processor.ts # [MODIFY] lightweight job classification + concurrency
├── interfaces/
│ ├── execution-policy.interface.ts # [NEW] RuntimePolicy type definition
│ └── ocr-residency.interface.ts # [NEW] OcrResidencyDecision type
├── guards/
│ └── execution-profile.guard.ts # [NEW] large-context profile admin check
└── ai.module.ts # [MODIFY] register new services + guard
backend/src/config/
└── bullmq.config.ts # [MODIFY] ai-realtime concurrency uplift config
specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/
├── app.py # [MODIFY] adaptive keep_alive, CPU fallback embed/rerank
├── services/
│ ├── vram_monitor.py # [NEW] VRAM headroom query via Ollama API
│ └── residency_policy.py # [NEW] keep_alive calculation policy
└── requirements.txt # [MODIFY] add nvidia-ml-py or pynvml if needed
frontend/
├── types/
│ └── ai.ts # [MODIFY] เอา model fields ออก, เพิ่ม executionProfile
├── lib/services/
│ └── admin-ai.service.ts # [MODIFY] update types + canonical name display
└── components/admin/ai/
└── OcrSandboxPromptManager.tsx # [MODIFY] แสดง canonical names ใน UI
backend/src/modules/ai/
└── tests/
├── ai-policy.service.spec.ts # [NEW] unit tests profile mapping
├── ocr-residency.spec.ts # [NEW] unit tests adaptive residency
└── execution-profile.guard.spec.ts # [NEW] unit tests CASL guard
```
---
## Phases
### Phase 1: Foundational — Policy Infrastructure
ต้องเสร็จก่อน workstream อื่นทั้งหมด:
1. สร้าง `VramMonitorService` — query VRAM headroom จาก Ollama `/api/ps` endpoint
2. สร้าง `AiPolicyService` — mapping `ExecutionProfile``RuntimePolicy`
3. สร้าง `ExecutionProfileGuard` — CASL check สำหรับ `large-context`
4. แก้ `CreateAiJobDto` — เอา `model.key` + parameter overrides ออก
5. แก้ `vram_monitor.py` บน sidecar — query GPU headroom
### Phase 2: Contract & Canonical Naming (Workstream A)
1. แก้ `AiService` — validate profile, override data-affecting jobs, log canonical names
2. แก้ `ai-job-response.dto.ts``modelUsed` เป็น canonical name
3. แก้ Frontend types และ Admin Console UI — แสดง canonical names
4. เพิ่ม rejection tests สำหรับ `model.key` และ parameter overrides
### Phase 3: Adaptive OCR Residency (Workstream B)
1. แก้ `OcrService` — inject `VramMonitorService`, คำนวณ `keep_alive` แบบ dynamic
2. แก้ `residency_policy.py` บน sidecar — รับ `keep_alive` จาก backend policy
3. เพิ่ม unit tests residency scenarios
### Phase 4: Retrieval Acceleration (Workstream C)
1. แก้ `app.py` — เพิ่ม GPU headroom check ใน `/embed` และ `/rerank`
2. เพิ่ม CPU fallback path พร้อม log
3. แก้ `ai-batch.processor.ts` สำหรับ RAG query fallback handling
### Phase 5: Queue Policy (Workstream D)
1. แก้ `bullmq.config.ts``ai-realtime` concurrency = 2 (configurable)
2. แก้ `ai-realtime.processor.ts` — classify lightweight vs generation-heavy jobs
3. ตรวจว่า `rag-query` ถูก route ไป `ai-batch` เท่านั้น
### Phase 6: Verification & Cutover (Workstream E)
1. รวม test suite ทั้ง 4 แกน
2. Manual validation checklist (Admin Console, OCR Sandbox)
3. Cutover gate verification
---
## Complexity Tracking
ไม่มี constitution violations ที่ต้องอธิบาย
@@ -0,0 +1,136 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md
// Change Log:
// - 2026-06-11: Verification quickstart for AI Runtime Policy Refactor
# Quickstart: AI Runtime Policy Refactor — Verification Guide
## Prerequisites
- Backend running (`pnpm run start:dev` in `backend/`)
- OCR sidecar running on Desk-5439 (`docker compose up` in ocr-sidecar/)
- Ollama running with `np-dms-ai` and `np-dms-ocr` tags registered
- Admin user token available
---
## Gate 1: Policy Contract Verification
### 1A. Reject model.key (should return 400)
```bash
curl -X POST http://localhost:3001/api/ai/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' \
| jq '.statusCode, .message'
# Expected: 400, message about model.key not allowed
```
### 1B. Reject parameter overrides (should return 400)
```bash
curl -X POST http://localhost:3001/api/ai/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "rag-query", "temperature": 0.9}' \
| jq '.statusCode'
# Expected: 400
```
### 1C. Valid executionProfile (should return 201)
```bash
curl -X POST http://localhost:3001/api/ai/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid>"}' \
| jq '.data.modelUsed'
# Expected: "np-dms-ai"
```
### 1D. large-context by non-admin (should return 403)
```bash
curl -X POST http://localhost:3001/api/ai/jobs \
-H "Authorization: Bearer $NON_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "rag-query", "executionProfile": "large-context"}' \
| jq '.statusCode'
# Expected: 403
```
---
## Gate 2: Canonical Naming Verification
### 2A. Check audit log after job
```sql
SELECT metadata->>'$.modelUsed' FROM ai_audit_logs ORDER BY created_at DESC LIMIT 1;
-- Expected: "np-dms-ai" (ไม่ใช่ "typhoon2.5-np-dms:latest")
```
### 2B. Check Admin Console (Manual)
1. เปิด `/admin/ai` ใน browser
2. ตรวจว่า model labels ทั้งหมดแสดง `np-dms-ai` และ `np-dms-ocr`
3. ตรวจว่าไม่มี `typhoon*` ปรากฏใน UI
---
## Gate 3: Adaptive OCR Residency Verification
### 3A. OCR under large-context profile
```bash
# ส่ง OCR job ขณะที่มี large-context job active
# ดู sidecar log
docker logs ocr-sidecar --tail 20
# Expected log line: keep_alive=0 reason=large-context-active
```
### 3B. OCR with headroom sufficient
```bash
# ส่ง OCR job เมื่อ GPU headroom สูง (ไม่มี model loaded หนัก)
docker logs ocr-sidecar --tail 20
# Expected log line: keep_alive=120 reason=headroom-sufficient
```
---
## Gate 4: Retrieval CPU Fallback Verification
### 4A. Force GPU pressure then run RAG
```bash
# 1. Force load large model
curl http://localhost:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}'
# 2. Run RAG query
curl -X POST http://localhost:3001/api/ai/jobs \
-H "Authorization: Bearer $TOKEN" \
-d '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid>"}' \
| jq '.data.status'
# Expected: "completed" (ไม่ fail)
# 3. ตรวจ sidecar log
docker logs ocr-sidecar --tail 20
# Expected: device=cpu reason=gpu-headroom-below-threshold
```
---
## Automated Test Suite
```bash
# Backend unit + integration tests
cd backend
pnpm test -- --testPathPattern="ai-policy|ocr-residency|execution-profile"
# Sidecar tests
cd specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar
pytest tests/ -v
```
**All tests must pass** before cutover gate is considered complete.
@@ -0,0 +1,149 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/research.md
// Change Log:
// - 2026-06-11: Phase 0 research for AI Runtime Policy Refactor
# Research: AI Runtime Policy Refactor
## 1. VRAM Headroom Query Strategy
**Decision**: ใช้ Ollama `/api/ps` endpoint เพื่อดู running models และ VRAM usage — คำนวณ headroom จาก total VRAM (16GB RTX 5060 Ti) หักด้วย loaded model VRAM
**Rationale**:
- Ollama `/api/ps` response มี `size_vram` สำหรับแต่ละ loaded model
- ไม่ต้องพึ่ง `pynvml` หรือ `nvidia-ml-py` ซึ่งเพิ่ม dependency และ platform coupling
- หาก `/api/ps` timeout หรือ error → safe default = 0 headroom (unload)
**Alternatives considered**:
- `pynvml` direct NVIDIA API: platform-specific, ต้อง CUDA toolkit, ไม่ต้อง
- `nvidia-smi` subprocess: fragile on container env, parsing overhead
- Hardcode threshold per model: ไม่ adaptive, ต้องอัปเดตทุกครั้งที่เปลี่ยน model
**Response shape จาก Ollama `/api/ps`**:
```json
{
"models": [
{
"name": "np-dms-ai:latest",
"model": "np-dms-ai:latest",
"size": 8192000000,
"size_vram": 7680000000,
"digest": "...",
"expires_at": "..."
}
]
}
```
---
## 2. ExecutionProfile → RuntimePolicy Mapping
**Decision**: Mapping table ใน `AiPolicyService` เป็น `readonly` constant — ไม่เก็บใน DB เพราะเป็น architecture decision ไม่ใช่ operational config
**Rationale**:
- Profile set เล็กและเสถียร (4 values) — DB overhead ไม่คุ้ม
- ถ้าต้องการเปลี่ยน profile behavior ต้องผ่าน code review (governance)
- Runtime parameters เป็น implementation detail ของ backend policy — ไม่ expose ใน API
**Policy mapping (draft)**:
| Profile | Canonical Model | Temperature | Top-P | Max Tokens | Notes |
|---------|----------------|-------------|-------|------------|-------|
| `fast` | `np-dms-ai` | 0.1 | 0.9 | 1024 | Quick suggestions |
| `balanced` | `np-dms-ai` | 0.3 | 0.9 | 2048 | Default RAG/suggest |
| `thai-accurate` | `np-dms-ai` | 0.1 | 0.8 | 2048 | Thai doc extraction |
| `large-context` | `np-dms-ai` | 0.3 | 0.9 | 8192 | Admin-only, long docs |
**Data-affecting overrides**:
- `migrate-document` → force `thai-accurate` profile parameters
- `auto-fill-document` → force `thai-accurate` profile parameters
- `ocr-extraction` → handled by OCR sidecar policy, not main LLM
---
## 3. Adaptive OCR Residency Calculation
**Decision**: Policy function ใน `OcrService` (backend) คำนวณ `keep_alive` แล้วส่งไปใน OCR request header/body — สidecar ใช้ค่านั้นตรงๆ
**Rationale**:
- Backend มี context ของ active job profile ที่ sidecar ไม่มี
- Central policy ง่ายกว่า distributed decision
**Algorithm**:
```
function calculateOcrKeepAlive(activeProfile, vramHeadroomMb):
if activeProfile == 'large-context': return 0
if vramHeadroomMb < VRAM_HEADROOM_THRESHOLD_MB: return 0
if vramHeadroomMb >= VRAM_HEADROOM_THRESHOLD_MB: return OCR_RESIDENCY_WINDOW_SECONDS (default: 120)
fallback (query error): return 0
```
**Default values**:
- `VRAM_HEADROOM_THRESHOLD_MB`: 3000 (3GB) — configurable env variable
- `OCR_RESIDENCY_WINDOW_SECONDS`: 120 (2 min) — configurable env variable
---
## 4. CPU Fallback for Retrieval (FlagEmbedding + BGE-Reranker)
**Decision**: `FlagEmbedding` รองรับ `use_fp16=False` และ device selection — pass `device="cpu"` เมื่อ headroom ไม่พอ
**Rationale**:
- FlagEmbedding (`BGE-M3`) รองรับ CPU inference โดย native — ไม่ต้อง rewrite
- `BGE-Reranker-Large` ก็รองรับ CPU เช่นกัน
- ต้องเพิ่ม timeout guard: CPU embed อาจใช้เวลา 10–30s สำหรับ long doc
**Pattern ใน sidecar**:
```python
async def embed_with_fallback(texts: list[str], vram_headroom_mb: float) -> EmbedResponse:
device = "cuda" if vram_headroom_mb >= settings.VRAM_HEADROOM_THRESHOLD_MB else "cpu"
# ใช้ FlagEmbedding พร้อม device parameter
# log fallback decision
return result
```
---
## 5. BullMQ Concurrency Uplift Pattern
**Decision**: ใช้ job-type classification ใน `ai-realtime.processor.ts` — ตรวจ `job.data.type` ก่อน process; lightweight jobs (intent-classify, tool-suggest) ทำงาน concurrently; generation-heavy jobs enforce semaphore
**Rationale**:
- BullMQ Worker รองรับ `concurrency: 2` ระดับ worker configuration
- Lightweight jobs ไม่เรียก Ollama → ไม่มี GPU contention จริง
- ไม่ต้องสร้าง queue ใหม่ — เปลี่ยน config + add guard ใน processor พอ
**Lightweight job types** (ที่อนุญาต concurrency = 2):
- `intent-classify` (Pattern Layer only)
- `tool-suggest` (no model switch)
**Generation-heavy** (ยังคง serialize):
- `rag-query`
- `auto-fill-document`
- `migrate-document`
- `ocr-extraction`
---
## 6. Canonical Name Enforcement Strategy
**Decision**: ใช้ `AiPolicyService.getCanonicalModelName(runtimeModelTag)` function ที่ map runtime tag → canonical — เรียกก่อน log/response ทุกครั้ง
**Pattern**:
```typescript
// ไม่ว่า Ollama จะตอบ runtime tag อะไร ให้ map ก่อน expose
const canonicalName = this.aiPolicyService.getCanonicalModelName(ollamaResponse.model);
// canonicalName = "np-dms-ai" หรือ "np-dms-ocr" เสมอ
```
**Mapping table**:
```typescript
const CANONICAL_MODEL_MAP: Record<string, string> = {
'typhoon2.5-np-dms:latest': 'np-dms-ai',
'np-dms-ai:latest': 'np-dms-ai',
'np-dms-ai': 'np-dms-ai',
'typhoon-np-dms-ocr:latest': 'np-dms-ocr',
'np-dms-ocr:latest': 'np-dms-ocr',
'np-dms-ocr': 'np-dms-ocr',
};
```
@@ -0,0 +1,193 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/spec.md
// Change Log:
// - 2026-06-11: Initial specification for AI Runtime Policy Refactor (RTX 5060 Ti 16GB)
# Feature Specification: AI Runtime Policy Refactor
**Feature Branch**: `235-ai-runtime-policy-refactor`
**Created**: 2026-06-11
**Status**: Draft
**Category**: 200-fullstacks
**Input**: User description from `docs/AI-Refactor.md` + `docs/0001-ai-runtime-policy-refactor.md`
## Overview
ปรับ AI runtime ของ LCBP3-DMS ให้รองรับ GPU ใหม่ (RTX 5060 Ti 16GB) โดยนำ canonical model identities (`np-dms-ai`, `np-dms-ocr`), policy-driven `executionProfile` contract, และ LLM-First GPU ownership มาใช้แทนระบบเดิมที่ caller เลือก model/parameter เองได้ และ keep_alive แบบ fixed ค่า
อ้างอิง: `docs/AI-Refactor.md`, `docs/0001-ai-runtime-policy-refactor.md`, ADR-033, ADR-034
---
## User Scenarios & Testing _(mandatory)_
### User Story 1 — Policy Contract & Canonical Naming (Priority: P1)
นักพัฒนาและ admin ที่ส่ง AI job request ผ่าน AI Gateway จะส่งได้แค่ `executionProfile` (`fast | balanced | thai-accurate | large-context`) โดยไม่สามารถระบุชื่อ model หรือ override runtime parameters ได้เอง — system แสดงและบันทึก model ในทุก layer ด้วยชื่อ canonical `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime เดิม
**Why this priority**: เป็นรากฐานของทุก workstream — ถ้า contract ยังเป็น caller-driven อยู่ workstream อื่นไม่มีความหมาย
**Independent Test**: ยิง POST ไปยัง AI Gateway endpoint ด้วย payload ที่มี `model.key` หรือ `temperature` แล้วตรวจว่า API reject 400 พร้อม error message; ยิงด้วย `executionProfile: "balanced"` แล้วตรวจว่าผ่านและ log/response แสดง `np-dms-ai`
**Acceptance Scenarios**:
1. **Given** AI job request ที่มี `model: { key: "typhoon2.5-np-dms:latest" }`, **When** ส่งไปยัง `POST /api/ai/jobs`, **Then** system ตอบ HTTP 400 พร้อมข้อความว่า field `model.key` ไม่อนุญาต
2. **Given** AI job request ที่มี `executionProfile: "balanced"`, **When** job ถูก dispatch ไปยัง `ai-batch` queue, **Then** job payload บันทึก `modelUsed: "np-dms-ai"` ใน audit log
3. **Given** admin เปิด AI Admin Console, **When** ดู model information panel, **Then** แสดงชื่อ `np-dms-ai` และ `np-dms-ocr` ไม่ใช่ชื่อ runtime จริง (เช่น `typhoon2.5-np-dms:latest`)
4. **Given** `auto-fill-document` job ถูกส่งมาพร้อม `executionProfile: "fast"`, **When** backend process job, **Then** backend override เป็น deterministic profile โดยไม่ใช้ค่า `fast` ที่ caller ส่งมา
5. **Given** `large-context` profile ถูกส่งโดย non-admin user, **When** backend validate, **Then** ตอบ HTTP 403 เพราะ profile นั้น restrict เฉพาะ admin/special workflows
---
### User Story 2 — Adaptive OCR Residency (Priority: P2)
Backend คำนวณ `keep_alive` value ของ `np-dms-ocr` แบบ dynamic ตาม VRAM headroom และ active workload ณ ขณะนั้น แทนการใช้ค่า fixed `keep_alive: 0` หรือ `keep_alive: 300` ตายตัว
**Why this priority**: แก้ปัญหา VRAM contention โดยตรง — ถ้า OCR ค้างอยู่ใน VRAM ตลอดจะบล็อก main model; ถ้า unload ทุกครั้งจะมี cold start penalty สูง 515 วินาที
**Independent Test**: รัน OCR job ขณะที่ `large-context` profile active และตรวจว่า `keep_alive: 0` ถูกส่งไป OCR sidecar; รัน OCR job ขณะที่ VRAM headroom สูงและตรวจว่าได้ residency window > 0
**Acceptance Scenarios**:
1. **Given** active job กำลังใช้ `large-context` profile, **When** OCR job เข้ามา, **Then** `keep_alive` ที่ส่งไป Ollama = `0`
2. **Given** ไม่มี active main model pressure และ VRAM headroom ≥ threshold, **When** OCR job เข้ามา, **Then** `keep_alive` ที่ส่งไป Ollama > `0` (residency window)
3. **Given** main model pressure สูง (high VRAM utilization), **When** OCR job เข้ามา, **Then** `keep_alive` = `0` เสมอ
4. **Given** OCR residency policy ทำงาน, **When** ดู trace/log ของ OCR request, **Then** log บันทึก residency decision พร้อม headroom value ที่ใช้ตัดสิน
---
### User Story 3 — Retrieval Acceleration with CPU Fallback (Priority: P3)
`/embed` และ `/rerank` endpoints บน OCR sidecar ตรวจสอบ VRAM headroom ก่อนใช้ GPU; ถ้า headroom ไม่ผ่าน policy threshold ให้ fallback ไป CPU ทันทีโดยไม่ fail และไม่รอ GPU queue
**Why this priority**: ป้องกัน RAG query ล้มเหลวในช่วงที่ GPU ถูกใช้งานสูง — retrieval ยังทำงานได้แค่ช้าลง
**Independent Test**: จำลอง GPU pressure สูงแล้วยิง RAG query — ตรวจว่า query ยังตอบได้ (อาจช้ากว่าปกติ) และ log บันทึก `"retrieval: cpu-fallback"`
**Acceptance Scenarios**:
1. **Given** GPU headroom < threshold, **When** `POST /embed` ถูกเรียก, **Then** ใช้ CPU compute โดยไม่ return error
2. **Given** GPU headroom < threshold, **When** `POST /rerank` ถูกเรียก, **Then** ใช้ CPU compute และ response ปกติ
3. **Given** fallback เกิดขึ้น, **When** ดู sidecar log, **Then** log entry มี `device: "cpu"` และ `reason: "gpu-headroom-below-threshold"`
4. **Given** `rag-query` job รัน, **When** GPU ถูก main model ใช้งานอยู่, **Then** RAG query ยังตอบ response ได้ (ไม่ timeout หรือ fail hard)
---
### User Story 4 — Queue Policy & Selective Realtime Concurrency (Priority: P4)
BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency = 2 ได้เฉพาะ lightweight realtime jobs (intent classification ที่ไม่เรียก OCR, tool-only suggestion ที่ไม่ต้อง model switching) โดยยังคง pause/resume coordination เดิม และ `rag-query` ยังถูก classify เป็น generation-centric job ที่อยู่ใน `ai-batch`
**Why this priority**: เพิ่ม throughput ให้ lightweight jobs โดยไม่กระทบ GPU safety
**Independent Test**: ส่ง intent classification job 2 อันพร้อมกัน ตรวจว่าทั้งสองรันพร้อมกันได้ใน `ai-realtime`; ส่ง `rag-query` ตรวจว่าไปอยู่ใน `ai-batch` ไม่ใช่ `ai-realtime`
**Acceptance Scenarios**:
1. **Given** 2 intent classification jobs เข้ามาพร้อมกัน, **When** ทั้งคู่ถูก dispatch, **Then** ทั้งคู่ process พร้อมกันใน `ai-realtime` queue (concurrency = 2)
2. **Given** `rag-query` job เข้ามา, **When** dispatch, **Then** job ถูกส่งไป `ai-batch` queue ไม่ใช่ `ai-realtime`
3. **Given** `ai-batch` ถูก pause เนื่องจาก realtime pressure, **When** pause/resume coordination ทำงาน, **Then** `ai-realtime` ยังคง concurrency = 2 ได้สำหรับ lightweight jobs
---
### User Story 5 — Verification & Cutover Gate (Priority: P5)
ระบบมี automated tests และ manual validation checklist ครบตามทั้ง 4 แกน (policy contract, canonical naming, adaptive OCR residency, retrieval fallback) ก่อนถือว่า big bang cutover สำเร็จ — ไม่อนุญาต partial success
**Why this priority**: เป็น safety net ของทั้ง refactor — partial cutover อาจทำให้ระบบ inconsistent
**Independent Test**: รัน test suite ที่ครอบคลุมทั้ง 4 แกนแล้วทุก test ผ่าน; admin สามารถเปิด AI Admin Console และ OCR Sandbox ตรวจ label/behavior จริงได้
**Acceptance Scenarios**:
1. **Given** test suite รัน, **When** ทุก test ผ่าน, **Then** cutover gate ถือว่าผ่านในส่วน executable verification
2. **Given** admin เปิด AI Admin Console, **When** ดู model labels ทั้งหมด, **Then** ไม่มีชื่อ `typhoon2.5-np-dms:latest` หรือ `typhoon-np-dms-ocr:latest` ปรากฏใน UI
3. **Given** admin รัน OCR Sandbox ซ้ำหลาย job ในเงื่อนไข headroom ต่างกัน, **When** ดู behavior, **Then** `keep_alive` ต่างกันตาม policy ที่ defined
---
### Edge Cases
- ถ้า VRAM headroom calculation service ล้มเหลว (timeout หรือ error) → ต้อง fallback เป็น `keep_alive: 0` เสมอ (safe default)
- ถ้า caller ส่ง `executionProfile` ที่ไม่อยู่ใน canonical set → ตอบ 400 validation error
- ถ้า `large-context` profile ถูก whitelist ให้ admin แต่ VRAM ไม่พอ → backend ต้อง reject พร้อม error ชัดเจน ไม่ใช่ silent fallback
- ถ้า OCR job เข้ามาพร้อมกับ main model generation job → LLM-First rule บังคับ: OCR ต้องรอหรือใช้ `keep_alive: 0`
- ถ้า `/embed` fallback ไป CPU แล้ว job ใช้เวลานานเกิน timeout → ต้อง return partial result หรือ error ที่ชัดเจน ไม่ใช่ hang
---
## Requirements _(mandatory)_
### Functional Requirements
**Workstream A: Contract & Canonical Naming**
- **FR-A01**: System MUST reject AI job requests ที่มี `model.key` field ใน payload (HTTP 400)
- **FR-A02**: System MUST reject AI job requests ที่มี direct `temperature`, `top_p`, หรือ `maxTokens` overrides (HTTP 400)
- **FR-A03**: `executionProfile` MUST รับค่าได้เฉพาะ `fast | balanced | thai-accurate | large-context`
- **FR-A04**: `large-context` profile MUST ถูก authorize เฉพาะ admin role หรือ backend-whitelisted workflows
- **FR-A05**: System MUST map `executionProfile` → canonical model name และ runtime parameters ใน backend policy layer
- **FR-A06**: งาน data-affecting (`migrate-document`, `auto-fill-document`) MUST ถูก backend override profile โดยไม่ใช้ค่าที่ caller ส่งมา
- **FR-A07**: ทุก layer (API response, audit log, Admin Console, OCR Sandbox) MUST แสดงชื่อ `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime จริง
**Workstream B: Runtime Policy**
- **FR-B01**: Backend MUST มี policy mapping: `executionProfile``{ canonicalModel, keep_alive, temperature, top_p, maxTokens }`
- **FR-B02**: OCR residency MUST คำนวณ `keep_alive` แบบ dynamic จาก VRAM headroom และ active profile
- **FR-B03**: ถ้า active profile = `large-context` หรือ main model pressure = high → OCR `keep_alive` MUST = `0`
- **FR-B04**: ถ้า VRAM headroom ≥ policy threshold → OCR สามารถใช้ residency window > 0
- **FR-B05**: VRAM headroom calculation ล้มเหลว → MUST fallback เป็น `keep_alive: 0` (safe default)
- **FR-B06**: OCR residency decision MUST ถูก log พร้อม headroom value ที่ใช้ตัดสิน
**Workstream C: Retrieval Acceleration**
- **FR-C01**: `/embed` endpoint MUST ตรวจ VRAM headroom ก่อน GPU compute; ถ้าไม่ผ่าน → fallback CPU
- **FR-C02**: `/rerank` endpoint MUST ตรวจ VRAM headroom ก่อน GPU compute; ถ้าไม่ผ่าน → fallback CPU
- **FR-C03**: CPU fallback MUST ไม่ hard fail และ MUST ไม่รอ GPU queue — ถ้า CPU compute timeout ต้อง return HTTP 504 พร้อม error message ชัดเจน (ไม่ return partial result)
- **FR-C04**: Fallback event MUST ถูก log พร้อม `device: "cpu"` และ `reason`
- **FR-C05**: `rag-query` job MUST ยังตอบได้เมื่อ GPU retrieval path ถูก fallback ไป CPU
- **FR-C06**: VRAM headroom threshold MUST เป็น configurable env variable (`VRAM_HEADROOM_THRESHOLD_MB`) — ถ้า VRAM query ล้มเหลว ให้ใช้ safe default = 0 MB (บังคับ fallback)
**Workstream D: Queue Policy**
- **FR-D01**: `ai-realtime` queue MUST รองรับ concurrency = 2 สำหรับ lightweight realtime jobs
- **FR-D02**: Lightweight realtime jobs ได้แก่: intent classification (ไม่เรียก OCR), tool-only suggestion (ไม่ต้อง model switching)
- **FR-D03**: `rag-query` MUST ถูก dispatch ไป `ai-batch` ไม่ใช่ `ai-realtime`
- **FR-D04**: pause/resume coordination ระหว่าง `ai-realtime` และ `ai-batch` MUST ยังคงทำงานได้ตามเดิม
### Key Entities
- **ExecutionProfile**: Enum value ที่ caller ส่งมา (`fast | balanced | thai-accurate | large-context`) — contract ระดับ API
- **RuntimePolicy**: Backend mapping จาก `ExecutionProfile``{ canonicalModel, keep_alive, temperature, top_p, maxTokens }` — ไม่ expose ใน API
- **VramHeadroom**: ค่า computed ณ เวลา request ที่ใช้ตัดสิน OCR residency และ retrieval acceleration — บันทึกใน log
- **CanonicalModelIdentity**: ชื่อ `np-dms-ai` หรือ `np-dms-ocr` — ใช้ทุกชั้นที่ผู้ใช้เห็น
- **OcrResidencyDecision**: ผลการคำนวณ `keep_alive` value สำหรับ OCR job แต่ละครั้ง — บันทึกใน log พร้อม input factors
---
## Success Criteria _(mandatory)_
### Measurable Outcomes
- **SC-001**: AI job requests ที่มี `model.key` หรือ parameter overrides ถูก reject 100% ในทุก environment
- **SC-002**: ทุก layer ที่ผู้ใช้และนักพัฒนาเห็น (API response, audit log, Admin Console, OCR Sandbox) แสดงชื่อ `np-dms-ai` / `np-dms-ocr` 100% โดยไม่มีชื่อ runtime รั่วออกมา
- **SC-003**: OCR cold start penalty ลดลงจากการใช้ adaptive residency ในสถานการณ์ที่ VRAM headroom เพียงพอ (วัดจาก average OCR latency ใน non-contention scenario)
- **SC-004**: RAG query ยังตอบ response ได้ 100% แม้ GPU retrieval path ถูก fallback ไป CPU (ไม่มี hard failure)
- **SC-005**: Automated test suite ครอบคลุมทั้ง 4 แกนของ cutover gate ผ่าน 100%
- **SC-006**: lightweight realtime job throughput เพิ่มขึ้น (สามารถ process 2 concurrent lightweight jobs) ขณะที่ pause/resume coordination ยังทำงานได้
---
## Clarifications
### Session 2026-06-11
- Q: ถ้า `/embed` fallback ไป CPU แล้ว job ใช้เวลานานเกิน timeout → ควร return partial result หรือ return error ที่ชัดเจน? → A: Return error ที่ชัดเจนพร้อม HTTP 504 timeout message — ไม่ return partial result เพราะ downstream LLM context จะ incomplete และทำให้ผลลัพธ์ผิดพลาดโดยไม่รู้ตัว
- Q: VRAM headroom threshold ระดับ spec ควรกำหนด default value ไหม? → A: ไม่กำหนดใน spec — threshold เป็น operational config (env variable `VRAM_HEADROOM_THRESHOLD_MB`) ที่ ops/admin ปรับได้ runtime; spec ระบุแค่ว่า "ต้องมี threshold ที่ configurable" และ "ต้องใช้ safe default = 0 (unload) เมื่อ query ล้มเหลว"
## Assumptions
- GPU ปัจจุบัน (RTX 5060 Ti 16GB) รองรับ VRAM monitoring API ที่ Ollama หรือ sidecar สามารถ query ได้
- VRAM headroom threshold ค่าเริ่มต้นจะถูกกำหนดใน config/env และปรับได้โดยไม่ต้อง redeploy
- Canonical model names (`np-dms-ai`, `np-dms-ocr`) ถูก tag ใน Ollama registry บน Desk-5439 ก่อน cutover
- OCR sidecar (`app.py`) บน Desk-5439 จะถูก update เป็นส่วนหนึ่งของ cutover
- Big bang rollout: ไม่มี parallel legacy path — ทุก change deploy พร้อมกันในรอบเดียว
- `ai-realtime` concurrency uplift เป็น configuration change ไม่ใช่ architectural change ใหม่
@@ -0,0 +1,204 @@
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/tasks.md
// Change Log:
// - 2026-06-11: Initial task list for AI Runtime Policy Refactor
# Tasks: AI Runtime Policy Refactor
**Input**: Design documents from `specs/200-fullstacks/235-ai-runtime-policy-refactor/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: US1=Contract&Naming, US2=OCR Residency, US3=Retrieval Fallback, US4=Queue Policy, US5=Verification
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: สร้าง foundational types และ interfaces ก่อน workstream ทุกอัน
- [ ] T001 สร้าง interface file `backend/src/modules/ai/interfaces/execution-policy.interface.ts` (ExecutionProfile type, RuntimePolicy interface, VramHeadroom interface)
- [ ] T002 สร้าง interface file `backend/src/modules/ai/interfaces/ocr-residency.interface.ts` (OcrResidencyDecision interface)
- [ ] T003 [P] สร้าง `backend/src/modules/ai/services/vram-monitor.service.ts` — query Ollama `/api/ps` เพื่อคำนวณ VRAM headroom
- [ ] T004 [P] สร้าง `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/vram_monitor.py` — Python VRAM headroom query via Ollama `/api/ps`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Policy infrastructure ที่ทุก workstream ต้องพึ่งพา — MUST complete ก่อนทุก user story
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
- [ ] T005 สร้าง `backend/src/modules/ai/services/ai-policy.service.ts` — ExecutionProfile → RuntimePolicy mapping, canonical model name mapping, data-affecting job override logic
- [ ] T006 สร้าง `backend/src/modules/ai/guards/execution-profile.guard.ts` — CASL check: `large-context` เฉพาะ admin role
- [ ] T007 [P] แก้ `backend/src/modules/ai/dto/create-ai-job.dto.ts` — เอา `model.key` และ parameter override fields ออก, เพิ่ม `executionProfile?: ExecutionProfile` พร้อม class-validator
- [ ] T008 สร้าง `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py` — OCR keep_alive calculation function
- [ ] T009 แก้ `backend/src/modules/ai/ai.module.ts` — register `AiPolicyService`, `VramMonitorService`, `ExecutionProfileGuard`
**Checkpoint**: Foundation ready — policy services, guard, and updated DTO available
---
## Phase 3: User Story 1 — Policy Contract & Canonical Naming (P1) 🎯 MVP
**Goal**: API reject `model.key`/parameter overrides; ทุก layer แสดง canonical names; data-affecting jobs ถูก override
**Independent Test**: ยิง POST ด้วย `model.key` → ต้องได้ 400; ยิงด้วย `executionProfile: "balanced"` → ต้องได้ 201 + `modelUsed: "np-dms-ai"`
### Implementation for User Story 1
- [ ] T010 [US1] แก้ `backend/src/modules/ai/ai.service.ts` — inject `AiPolicyService`, validate `executionProfile`, apply backend override สำหรับ `migrate-document` และ `auto-fill-document`, set `modelUsed` canonical name ใน audit log
- [ ] T011 [P] [US1] แก้ `backend/src/modules/ai/dto/ai-job-response.dto.ts` — เพิ่ม `modelUsed: 'np-dms-ai' | 'np-dms-ocr'` field, เพิ่ม `executionProfile` field (effective profile หลัง override)
- [ ] T012 [P] [US1] แก้ `backend/src/modules/ai/ai.controller.ts` — ใช้ `ExecutionProfileGuard` บน create-job endpoint, validate forbidden fields ใน pipe
- [ ] T013 [P] [US1] แก้ `frontend/types/ai.ts` — เอา `model` field ออก, เพิ่ม `executionProfile?: ExecutionProfile`, เพิ่ม `modelUsed?: string`
- [ ] T014 [US1] แก้ `frontend/lib/services/admin-ai.service.ts` — update request/response types ให้สอดคล้องกับ DTO ใหม่
- [ ] T015 [P] [US1] แก้ `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` — แสดง `np-dms-ai` / `np-dms-ocr` แทนชื่อ runtime ใน result cards และ model info
- [ ] T016 [US1] แก้ `frontend/app/(admin)/admin/ai/page.tsx` — แสดง canonical names ใน System Health panel และ model status cards
**Checkpoint**: US1 fully functional — policy contract enforced, canonical naming in all layers
---
## Phase 4: User Story 2 — Adaptive OCR Residency (P2)
**Goal**: `OcrService` คำนวณ `keep_alive` dynamic ตาม VRAM headroom + active profile; sidecar รับค่าและใช้
**Independent Test**: ดู log จาก OCR job ใน high-pressure scenario → `keep_alive=0`; ใน headroom-sufficient scenario → `keep_alive>0`
### Implementation for User Story 2
- [ ] T017 [US2] แก้ `backend/src/modules/ai/services/ocr.service.ts` — inject `VramMonitorService` และ `AiPolicyService`, เพิ่ม `calculateOcrResidency()` method, ส่ง `keep_alive` ที่คำนวณได้ไปใน OCR sidecar request, log `OcrResidencyDecision`
- [ ] T018 [P] [US2] แก้ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` — รับ `keep_alive` parameter จาก request body แทน hardcode `keep_alive=0`, ส่ง `keep_alive` ค่านั้นไปใน Ollama `/v1/chat/completions` call
- [ ] T019 [P] [US2] เพิ่ม env variables ใน docker-compose ของ Desk-5439 OCR sidecar — `VRAM_HEADROOM_THRESHOLD_MB`, `OCR_RESIDENCY_WINDOW_SECONDS`, `GPU_TOTAL_VRAM_MB`
- [ ] T020 [US2] เพิ่ม unit tests `backend/src/modules/ai/tests/ocr-residency.spec.ts` — scenarios: large-context-active, high-pressure, headroom-sufficient, query-failed fallback
**Checkpoint**: US2 functional — OCR keep_alive computed dynamically per policy
---
## Phase 5: User Story 3 — Retrieval Acceleration with CPU Fallback (P3)
**Goal**: `/embed` และ `/rerank` บน sidecar ตรวจ VRAM headroom; fallback CPU ถ้าไม่พอ; log fallback decision
**Independent Test**: จำลอง GPU pressure → ยิง `/embed` → ต้องได้ผลลัพธ์ (ไม่ fail) + log `device: "cpu"`
### Implementation for User Story 3
- [ ] T021 [P] [US3] แก้ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` — เพิ่ม VRAM headroom check ใน `POST /embed` endpoint; ถ้าผ่าน threshold ใช้ GPU, ถ้าไม่ผ่านหรือ query ล้มเหลว ใช้ CPU; log `device` และ `reason`
- [ ] T022 [P] [US3] แก้ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` — เพิ่ม VRAM headroom check ใน `POST /rerank` endpoint; CPU fallback logic เหมือน `/embed`; เพิ่ม timeout guard (504 response ถ้า CPU timeout)
- [ ] T023 [US3] แก้ `backend/src/modules/ai/processors/ai-batch.processor.ts` — รอง handle กรณีที่ `/embed` หรือ `/rerank` ตอบ `device: "cpu"` ใน response; log `retrievalDevice` ลง ai_audit_logs metadata
- [ ] T024 [P] [US3] สร้าง `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/tests/test_retrieval_fallback.py` — pytest tests สำหรับ CPU fallback behavior ของ `/embed` และ `/rerank`
**Checkpoint**: US3 functional — retrieval never hard-fails due to GPU pressure
---
## Phase 6: User Story 4 — Queue Policy & Selective Realtime Concurrency (P4)
**Goal**: `ai-realtime` concurrency = 2 สำหรับ lightweight jobs; `rag-query` route ไป `ai-batch`; pause/resume ยังทำงาน
**Independent Test**: ส่ง 2 intent-classify jobs พร้อมกัน → ทั้งสองรันพร้อมกัน; ส่ง rag-query → ไปอยู่ใน `ai-batch`
### Implementation for User Story 4
- [ ] T025 [US4] แก้ `backend/src/config/bullmq.config.ts` — เพิ่ม `REALTIME_CONCURRENCY` env variable (default: 2); ปรับ `ai-realtime` worker concurrency ให้ configurable
- [ ] T026 [US4] แก้ `backend/src/modules/ai/processors/ai-realtime.processor.ts` — เพิ่ม job type classification: `LIGHTWEIGHT_REALTIME_JOBS = ['intent-classify', 'tool-suggest']`; generation-heavy jobs ถูก redirect ไป `ai-batch` ถ้าเข้ามาผิด queue; เพิ่ม log สำหรับ classification decision
- [ ] T027 [P] [US4] ตรวจสอบ `backend/src/modules/ai/ai.service.ts` — ยืนยันว่า `rag-query` ถูก dispatch ไป `ai-batch` เสมอ (ไม่ใช่ `ai-realtime`); เพิ่ม explicit assertion ใน dispatch logic
- [ ] T028 [P] [US4] เพิ่ม unit tests `backend/src/modules/ai/tests/queue-policy.spec.ts` — ทดสอบ job classification, rag-query routing, lightweight job concurrency
**Checkpoint**: US4 functional — selective concurrency active, rag-query always in ai-batch
---
## Phase 7: User Story 5 — Verification & Cutover Gate (P5)
**Goal**: Test suite ครอบ 4 แกน cutover gate ทั้งหมด; manual validation checklist พร้อม; Admin Console / OCR Sandbox แสดงถูกต้อง
**Independent Test**: `pnpm test -- --testPathPattern="ai-policy|ocr-residency|execution-profile|queue-policy"` ทุก test ผ่าน 100%
### Implementation for User Story 5
- [ ] T029 [US5] สร้าง `backend/src/modules/ai/tests/ai-policy.service.spec.ts` — unit tests ครอบ: profile mapping ทุก 4 values, canonical name mapping, data-affecting override, `large-context` guard validation
- [ ] T030 [P] [US5] สร้าง `backend/src/modules/ai/tests/execution-profile.guard.spec.ts` — unit tests: admin passes, non-admin blocked, missing token blocked
- [ ] T031 [P] [US5] สร้าง `backend/src/modules/ai/tests/vram-monitor.service.spec.ts` — unit tests: successful query, Ollama timeout fallback, empty models response
- [ ] T032 [US5] ทดสอบ manual validation ตาม `quickstart.md` — รัน curl commands ทั้ง Gate 14, ตรวจ Admin Console labels, ตรวจ OCR Sandbox behavior; บันทึกผลใน checklist
- [ ] T033 [P] [US5] อัปเดต env template ไฟล์ `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/.env.template` — เพิ่ม `VRAM_HEADROOM_THRESHOLD_MB`, `OCR_RESIDENCY_WINDOW_SECONDS`, `GPU_TOTAL_VRAM_MB`, `REALTIME_CONCURRENCY`
- [ ] T034 [P] [US5] อัปเดต `backend/.env.example` — เพิ่ม `AI_VRAM_HEADROOM_THRESHOLD_MB`, `AI_REALTIME_CONCURRENCY`
**Checkpoint**: All 5 user stories complete — big bang cutover gate ready for validation
---
## Phase 8: Polish & Cross-Cutting Concerns
- [ ] T039 [US1] แก้ `backend/src/modules/ai/processors/ai-batch.processor.ts` — เปลี่ยน `ocrUsed` label value จาก `"Typhoon OCR"` / `"PaddleOCR"` เป็น `"np-dms-ocr"` ใน Redis completed result (ครอบคลุม FR-A07: canonical names ทุก layer รวมถึง OCR Sandbox badge)
- [ ] T035 [P] ตรวจสอบ i18n keys ที่ต้องเพิ่มใน `frontend/public/locales/` สำหรับ error messages ใหม่ (400 model.key, 403 large-context, 504 CPU timeout)
- [ ] T036 อัปเดต CONTEXT.md และ AGENTS.md — เพิ่ม `np-dms-ai` / `np-dms-ocr` เป็น canonical identity ใน System readiness summary; แก้ references เดิมที่ยังใช้ชื่อ runtime
- [ ] T037 [P] ตรวจสอบ ADR-034 references ทั้งหมดใน codebase ด้วย search — ไฟล์ไหนยังใช้ `typhoon2.5-np-dms:latest` หรือ `typhoon-np-dms-ocr:latest` ใน user-facing surfaces (ไม่ใช่ Modelfile/ops internals)
- [ ] T038 รัน `pnpm lint` และ `pnpm type-check` สำหรับ backend และ frontend — แก้ทุก error ก่อน cutover
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: ไม่มี dependency — เริ่มได้ทันที
- **Foundational (Phase 2)**: ต้องรอ Phase 1 (T001, T002) — BLOCKS ทุก user story
- **US1 (Phase 3)**: ต้องรอ Phase 2 complete — สำคัญสุด, ทำก่อน
- **US2 (Phase 4)**: ต้องรอ Phase 2 complete — ขึ้นกับ `VramMonitorService` จาก T003
- **US3 (Phase 5)**: ต้องรอ Phase 2 complete — ขึ้นกับ `vram_monitor.py` จาก T004
- **US4 (Phase 6)**: ต้องรอ Phase 2 complete — independent จาก US1/US2/US3
- **US5 (Phase 7)**: ต้องรอ US1+US2+US3+US4 complete (ทดสอบทุกแกน)
- **Polish (Phase 8)**: ต้องรอ US5 ผ่าน cutover gate
### User Story Dependencies
- **US1 (P1)**: ต้อง complete ก่อน — contract เป็น foundation ของ canonical naming ทุกชั้น
- **US2 (P2)**: ขึ้นกับ `VramMonitorService` (T003, Phase 1) เท่านั้น — parallel กับ US1 ได้
- **US3 (P3)**: ขึ้นกับ `vram_monitor.py` (T004, Phase 1) เท่านั้น — parallel กับ US1/US2 ได้
- **US4 (P4)**: Independent จาก US1/US2/US3 — parallel ได้หลัง Phase 2
- **US5 (P5)**: ต้องรอทุก US ก่อนหน้า
### Parallel Opportunities
- T001 + T002: parallel (different files)
- T003 + T004: parallel (different stacks)
- T005, T006, T007: T005 ทำก่อน (T006, T007 ขึ้นกับ types จาก T005)
- US1 + US2 + US3 + US4: parallel หลัง Phase 2 complete (ถ้ามีทีม)
- T029, T030, T031, T033, T034: parallel (different test files / env files)
---
## Implementation Strategy
### MVP First (US1 Only)
1. Phase 1: Setup (T001T004)
2. Phase 2: Foundational (T005T009)
3. Phase 3: US1 (T010T016)
4. **STOP & VALIDATE**: ยิง curl ตาม Gate 1 และ Gate 2 ใน quickstart.md
5. Deploy/validate canonical naming ใน Admin Console
### Incremental Delivery
1. Phase 1+2 → Foundation
2. US1 → Policy contract + canonical naming (MVP)
3. US2 → Adaptive OCR residency
4. US3 → Retrieval CPU fallback
5. US4 → Queue policy
6. US5 → Full cutover gate verification
### Total Task Count
- **Total**: 39 tasks
- **US1**: 7 tasks (T010T016)
- **US2**: 4 tasks (T017T020)
- **US3**: 4 tasks (T021T024)
- **US4**: 4 tasks (T025T028)
- **US5**: 6 tasks (T029T034)
- **Setup**: 4 tasks (T001T004)
- **Foundational**: 5 tasks (T005T009)
- **Polish**: 4 tasks (T035T038)