feat(ai-runtime): complete ai runtime policy refactor (ADR-035)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
-- Rollback: ลบตาราง ai_execution_profiles
|
||||
-- Date: 2026-06-11
|
||||
-- Related Delta: 2026-06-11-create-ai-execution-profiles.sql
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS ai_execution_profiles;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Delta: สร้างตาราง ai_execution_profiles สำหรับ AI Runtime Policy Refactor
|
||||
-- Date: 2026-06-11
|
||||
-- Related ADR: ADR-029, Feature-235
|
||||
-- Source of defaults: docs/ai-profiles.md
|
||||
-- Applied in: v1.9.x (AI Runtime Policy Refactor cutover)
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_execution_profiles (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ภายใน (ไม่ expose ใน API)',
|
||||
profile_name VARCHAR(50) NOT NULL COMMENT 'ชื่อ profile: interactive, standard, quality, deep-analysis',
|
||||
temperature DECIMAL(4,3) NOT NULL COMMENT 'LLM temperature parameter',
|
||||
top_p DECIMAL(4,3) NOT NULL COMMENT 'LLM top_p parameter',
|
||||
max_tokens INT NOT NULL COMMENT 'Maximum tokens to generate',
|
||||
num_ctx INT NOT NULL COMMENT 'Context window size (tokens)',
|
||||
repeat_penalty DECIMAL(5,3) NOT NULL COMMENT 'Repeat penalty parameter',
|
||||
keep_alive_seconds INT NOT NULL COMMENT 'Model keep_alive in seconds (0 = unload immediately)',
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1 = profile นี้ใช้งานได้; 0 = disabled',
|
||||
updated_by INT NULL COMMENT 'user_id ที่แก้ไขล่าสุด (NULL = seed default)',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_profile_name (profile_name),
|
||||
INDEX idx_profile_active (profile_name, is_active),
|
||||
FOREIGN KEY (updated_by) REFERENCES users(user_id)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
|
||||
COMMENT = 'ตาราง execution profile parameters สำหรับ np-dms-ai (ADR-029, Feature-235); ค่า default จาก docs/ai-profiles.md';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Seed: default profiles จาก docs/ai-profiles.md
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO ai_execution_profiles (
|
||||
profile_name, temperature, top_p, max_tokens, num_ctx, repeat_penalty, keep_alive_seconds
|
||||
) VALUES
|
||||
('interactive', 0.700, 0.900, 2048, 4096, 1.150, 300), -- keep_alive: "5m"
|
||||
('standard', 0.500, 0.800, 4096, 8192, 1.150, 600), -- keep_alive: "10m"
|
||||
('quality', 0.100, 0.950, 8192, 8192, 1.150, 600), -- keep_alive: "10m"
|
||||
('deep-analysis', 0.300, 0.850, 8192, 32768, 1.150, 0) -- keep_alive: "0" (admin sandbox only)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
profile_name = profile_name; -- no-op: ไม่ overwrite ค่าที่ admin calibrate ไว้แล้ว
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- Rollback: ลบ fields ที่เพิ่มสำหรับ AI Runtime Policy Refactor
|
||||
-- Date: 2026-06-11
|
||||
-- Related Delta: 2026-06-11-extend-ai-audit-logs-runtime-policy.sql
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
ALTER TABLE ai_audit_logs
|
||||
DROP INDEX IF EXISTS idx_ai_audit_canonical_model;
|
||||
|
||||
ALTER TABLE ai_audit_logs
|
||||
DROP INDEX IF EXISTS idx_ai_audit_effective_profile;
|
||||
|
||||
ALTER TABLE ai_audit_logs
|
||||
DROP COLUMN IF EXISTS snapshot_params_json;
|
||||
|
||||
ALTER TABLE ai_audit_logs
|
||||
DROP COLUMN IF EXISTS canonical_model;
|
||||
|
||||
ALTER TABLE ai_audit_logs
|
||||
DROP COLUMN IF EXISTS effective_profile;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Delta: เพิ่ม fields สำหรับ AI Runtime Policy Refactor ใน ai_audit_logs
|
||||
-- Date: 2026-06-11
|
||||
-- Related ADR: ADR-023, ADR-029, Feature-235
|
||||
-- Applied in: AI Runtime Policy Refactor cutover (big bang)
|
||||
-- ------------------------------------------------------------
|
||||
-- เพิ่ม 3 columns:
|
||||
-- effective_profile — profile name ที่ backend กำหนด (interactive/standard/quality/deep-analysis)
|
||||
-- canonical_model — canonical model identity (np-dms-ai / np-dms-ocr)
|
||||
-- snapshot_params_json — parameters snapshot ณ เวลา dispatch (FR-A09)
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
-- effective_profile: ชื่อ ExecutionProfile ที่ backend กำหนดจาก job.type
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD COLUMN IF NOT EXISTS effective_profile VARCHAR(50) NULL
|
||||
COMMENT 'ExecutionProfile ที่ backend กำหนด: interactive|standard|quality|deep-analysis (Feature-235)'
|
||||
AFTER model_name;
|
||||
|
||||
-- canonical_model: ชื่อ canonical identity — ไม่ใช่ runtime tag
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD COLUMN IF NOT EXISTS canonical_model VARCHAR(50) NULL
|
||||
COMMENT 'Canonical model identity: np-dms-ai หรือ np-dms-ocr (Feature-235, ADR-023)'
|
||||
AFTER effective_profile;
|
||||
|
||||
-- snapshot_params_json: parameters ที่ถูก snapshot ตอน dispatch โดย AiPolicyService (FR-A09)
|
||||
-- { temperature, topP, maxTokens, numCtx, repeatPenalty, keepAliveSeconds }
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD COLUMN IF NOT EXISTS snapshot_params_json JSON NULL
|
||||
COMMENT 'Runtime parameters snapshot ณ เวลา dispatch — ใช้จริงใน Ollama call (FR-A09, Feature-235)'
|
||||
AFTER canonical_model;
|
||||
|
||||
-- index สำหรับ analytics queries ตาม profile
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD INDEX IF NOT EXISTS idx_ai_audit_effective_profile (effective_profile);
|
||||
|
||||
-- index สำหรับ canonical_model
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD INDEX IF NOT EXISTS idx_ai_audit_canonical_model (canonical_model);
|
||||
@@ -0,0 +1,13 @@
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/.env.template
|
||||
# Change Log:
|
||||
# - 2026-06-11: สร้างไฟล์ env template สำหรับ Desk-5439 (US5)
|
||||
|
||||
# ─── VRAM, Residency & Timeout Configurations ───
|
||||
VRAM_HEADROOM_THRESHOLD_MB=3000.0
|
||||
OCR_RESIDENCY_WINDOW_SECONDS=120
|
||||
GPU_TOTAL_VRAM_MB=16384.0
|
||||
GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB=12000.0
|
||||
RETRIEVAL_TIMEOUT_SECONDS=30.0
|
||||
|
||||
# ─── Queue policy & concurrency ───
|
||||
REALTIME_CONCURRENCY=2
|
||||
+5
-7
@@ -1,12 +1,10 @@
|
||||
FROM scb10x/typhoon2.5-qwen3-4b:latest
|
||||
|
||||
|
||||
|
||||
PARAMETER num\_ctx 8192
|
||||
PARAMETER num\_predict 4096
|
||||
PARAMETER num_ctx 8192
|
||||
PARAMETER num_predict 4096
|
||||
PARAMETER temperature 0.4
|
||||
|
||||
PARAMETER top\_k 40
|
||||
PARAMETER top\_p 0.9
|
||||
PARAMETER repeat\_penalty 1.15
|
||||
PARAMETER top_k 40
|
||||
PARAMETER top_p 0.9
|
||||
PARAMETER repeat_penalty 1.15
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose\Desk-5439\ocr-sidecar\app.py
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py
|
||||
# Typhoon OCR HTTP Sidecar API — รับ POST /ocr แล้วคืนข้อความที่สกัดจาก PDF/Image
|
||||
# ตาม ADR-023A (revised 2026-06-11): ใช้ typhoon_ocr library + np-dms-ocr (Ollama) แทน Tesseract
|
||||
# Change Log:
|
||||
@@ -21,6 +21,7 @@
|
||||
# - 2026-06-05: เพิ่ม Option 2 (aggressive preprocessing: deskew + Otsu threshold + morphology) และ Option 3 (smart post-processing: regex-based hallucination removal) เพื่อลด Tesseract noise/hallucination (T025)
|
||||
# - 2026-06-06: เปลี่ยน keep_alive จาก 300s เป็น 0 เพื่อ unload model ทันทีหลังเสร็จงาน (แก้ปัญหา VRAM ไม่พอเมื่อ typhoon2.5-np-dms load พร้อมกัน)
|
||||
# - 2026-06-11: เปลี่ยน process_with_typhoon_ocr ให้ใช้ prepare_ocr_messages จาก typhoon_ocr library + inject DMS tags; เปลี่ยน endpoint เป็น /v1/chat/completions
|
||||
# - 2026-06-11: US2 & US3 - เพิ่ม keep_alive parameter และ CPU fallback สำหรับ /embed และ /rerank
|
||||
|
||||
import os
|
||||
import logging
|
||||
@@ -30,11 +31,13 @@ import json
|
||||
import tempfile
|
||||
import fitz # PyMuPDF (ใช้สำหรับ page count + fast-path text extraction)
|
||||
import httpx
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from PIL import Image
|
||||
import io
|
||||
from typhoon_ocr import prepare_ocr_messages
|
||||
from services.vram_monitor import get_vram_headroom
|
||||
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Depends, Security, status
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
@@ -104,6 +107,7 @@ class OcrRequest(BaseModel):
|
||||
pdfPath: str
|
||||
maxPages: Optional[int] = None
|
||||
engine: Optional[str] = None
|
||||
keep_alive: Optional[int] = None
|
||||
|
||||
class OcrResponse(BaseModel):
|
||||
text: str
|
||||
@@ -211,7 +215,7 @@ def process_with_typhoon_ocr(pdf_path: str, page_num: int = 1, options_override:
|
||||
"repetition_penalty": options_override.get("repeat_penalty", 1.2),
|
||||
"temperature": options_override.get("temperature", 0.1),
|
||||
"top_p": options_override.get("top_p", 0.6),
|
||||
"keep_alive": 0, # Unload model ทันทีหลังเสร็จงานเพื่อคืน VRAM ให้ np-dms-ai ใช้งานได้
|
||||
"keep_alive": options_override.get("keep_alive", 0), # Unload model ทันทีหลังเสร็จงานเพื่อคืน VRAM ให้ np-dms-ai ใช้งานได้
|
||||
}
|
||||
# ใช้ Ollama OpenAI-compatible endpoint (/v1/chat/completions)
|
||||
with httpx.Client(timeout=TYPHOON_OCR_TIMEOUT) as client:
|
||||
@@ -249,11 +253,14 @@ def ocr_extract(req: OcrRequest):
|
||||
raise HTTPException(status_code=404, detail=f"ไม่พบไฟล์: {req.pdfPath}")
|
||||
selected_engine = (req.engine or "auto").strip().lower()
|
||||
max_pages = req.maxPages or MAX_PAGES
|
||||
typhoon_options = {}
|
||||
if req.keep_alive is not None:
|
||||
typhoon_options["keep_alive"] = req.keep_alive
|
||||
try:
|
||||
doc = fitz.open(str(pdf_path))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=422, detail=f"เปิดไฟล์ PDF ล้มเหลว: {e}")
|
||||
return _process_pdf_doc(doc, selected_engine, max_pages)
|
||||
return _process_pdf_doc(doc, selected_engine, max_pages, typhoon_options)
|
||||
|
||||
@app.post("/ocr-upload", response_model=OcrResponse, dependencies=[Depends(get_api_key)])
|
||||
def ocr_upload(
|
||||
@@ -263,6 +270,7 @@ def ocr_upload(
|
||||
temperature: Optional[float] = Form(default=None),
|
||||
topP: Optional[float] = Form(default=None),
|
||||
repeatPenalty: Optional[float] = Form(default=None),
|
||||
keep_alive: Optional[int] = Form(default=None),
|
||||
):
|
||||
"""OCR จาก multipart file upload — ไม่ต้องการ shared volume mount"""
|
||||
selected_engine = engine.strip().lower()
|
||||
@@ -275,6 +283,8 @@ def ocr_upload(
|
||||
typhoon_options["top_p"] = topP
|
||||
if repeatPenalty is not None:
|
||||
typhoon_options["repeat_penalty"] = repeatPenalty
|
||||
if keep_alive is not None:
|
||||
typhoon_options["keep_alive"] = keep_alive
|
||||
pdf_bytes = file.file.read()
|
||||
import tempfile
|
||||
tmp_pdf_path: str | None = None
|
||||
@@ -317,6 +327,7 @@ class EmbedRequest(BaseModel):
|
||||
class EmbedResponse(BaseModel):
|
||||
dense: list[float]
|
||||
sparse: dict
|
||||
device: Optional[str] = None
|
||||
|
||||
class RerankRequest(BaseModel):
|
||||
query: str
|
||||
@@ -325,54 +336,133 @@ class RerankRequest(BaseModel):
|
||||
class RerankResponse(BaseModel):
|
||||
scores: list[float]
|
||||
ranked_indices: list[int]
|
||||
device: Optional[str] = None
|
||||
|
||||
@app.post("/embed", response_model=EmbedResponse, dependencies=[Depends(get_api_key)])
|
||||
def embed_text(req: EmbedRequest):
|
||||
"""BGE-M3 embedding generator (Dense + Sparse)"""
|
||||
async def embed_text(req: EmbedRequest):
|
||||
"""BGE-M3 embedding generator (Dense + Sparse) พร้อม CPU fallback และ timeout guard"""
|
||||
if bge_model is None:
|
||||
raise HTTPException(status_code=503, detail="BGE-M3 model not loaded")
|
||||
threshold_mb = float(os.getenv("VRAM_HEADROOM_THRESHOLD_MB", "3000.0"))
|
||||
timeout_sec = float(os.getenv("RETRIEVAL_TIMEOUT_SECONDS", "30.0"))
|
||||
headroom = get_vram_headroom()
|
||||
device = "cuda"
|
||||
reason = "headroom-sufficient"
|
||||
if not headroom.query_success:
|
||||
device = "cpu"
|
||||
reason = "gpu-query-failed"
|
||||
elif headroom.available_mb < threshold_mb:
|
||||
device = "cpu"
|
||||
reason = "gpu-headroom-below-threshold"
|
||||
try:
|
||||
if device == "cuda":
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
bge_model.model.to("cuda")
|
||||
else:
|
||||
device = "cpu"
|
||||
reason = "cuda-not-available"
|
||||
bge_model.model.to("cpu")
|
||||
else:
|
||||
bge_model.model.to("cpu")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to move BGE-M3 model to {device}: {e}")
|
||||
device = "cpu"
|
||||
reason = f"device-move-failed: {str(e)}"
|
||||
try:
|
||||
bge_model.model.to("cpu")
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"Embedding on device: {device} (reason: {reason})")
|
||||
def run_inference():
|
||||
output = bge_model.encode([req.text], return_dense=True, return_sparse=True)
|
||||
dense_vector = [float(x) for x in output['dense_vecs'][0]]
|
||||
lexical_dict = output['lexical_weights'][0]
|
||||
|
||||
indices = []
|
||||
values = []
|
||||
for token_id, weight in lexical_dict.items():
|
||||
indices.append(int(token_id))
|
||||
values.append(float(weight))
|
||||
|
||||
return dense_vector, indices, values
|
||||
try:
|
||||
dense_vector, indices, values = await asyncio.wait_for(
|
||||
asyncio.to_thread(run_inference),
|
||||
timeout=timeout_sec
|
||||
)
|
||||
return EmbedResponse(
|
||||
dense=dense_vector,
|
||||
sparse={"indices": indices, "values": values}
|
||||
sparse={"indices": indices, "values": values},
|
||||
device=device
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Embedding generation timed out after {timeout_sec}s on device {device}")
|
||||
raise HTTPException(status_code=504, detail="Embedding generation timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Embedding generation failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Embedding generation failed: {str(e)}")
|
||||
|
||||
@app.post("/rerank", response_model=RerankResponse, dependencies=[Depends(get_api_key)])
|
||||
def rerank_chunks(req: RerankRequest):
|
||||
"""BGE-Reranker-Large chunk re-ranker"""
|
||||
async def rerank_chunks(req: RerankRequest):
|
||||
"""BGE-Reranker-Large chunk re-ranker พร้อม CPU fallback และ timeout guard"""
|
||||
if reranker is None:
|
||||
raise HTTPException(status_code=503, detail="Reranker model not loaded")
|
||||
if not req.chunks:
|
||||
return RerankResponse(scores=[], ranked_indices=[])
|
||||
return RerankResponse(scores=[], ranked_indices=[], device="cpu")
|
||||
threshold_mb = float(os.getenv("VRAM_HEADROOM_THRESHOLD_MB", "3000.0"))
|
||||
timeout_sec = float(os.getenv("RETRIEVAL_TIMEOUT_SECONDS", "30.0"))
|
||||
headroom = get_vram_headroom()
|
||||
device = "cuda"
|
||||
reason = "headroom-sufficient"
|
||||
if not headroom.query_success:
|
||||
device = "cpu"
|
||||
reason = "gpu-query-failed"
|
||||
elif headroom.available_mb < threshold_mb:
|
||||
device = "cpu"
|
||||
reason = "gpu-headroom-below-threshold"
|
||||
try:
|
||||
if device == "cuda":
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
reranker.model.to("cuda")
|
||||
else:
|
||||
device = "cpu"
|
||||
reason = "cuda-not-available"
|
||||
reranker.model.to("cpu")
|
||||
else:
|
||||
reranker.model.to("cpu")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to move Reranker model to {device}: {e}")
|
||||
device = "cpu"
|
||||
reason = f"device-move-failed: {str(e)}"
|
||||
try:
|
||||
reranker.model.to("cpu")
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"Reranking on device: {device} (reason: {reason})")
|
||||
def run_rerank():
|
||||
pairs = [[req.query, chunk] for chunk in req.chunks]
|
||||
scores = reranker.compute_score(pairs)
|
||||
if isinstance(scores, float):
|
||||
scores = [scores]
|
||||
else:
|
||||
scores = [float(s) for s in scores]
|
||||
|
||||
indexed_scores = list(enumerate(scores))
|
||||
indexed_scores.sort(key=lambda x: x[1], reverse=True)
|
||||
ranked_indices = [idx for idx, _ in indexed_scores]
|
||||
|
||||
return scores, ranked_indices
|
||||
try:
|
||||
scores, ranked_indices = await asyncio.wait_for(
|
||||
asyncio.to_thread(run_rerank),
|
||||
timeout=timeout_sec
|
||||
)
|
||||
return RerankResponse(
|
||||
scores=scores,
|
||||
ranked_indices=ranked_indices
|
||||
ranked_indices=ranked_indices,
|
||||
device=device
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Reranking timed out after {timeout_sec}s on device {device}")
|
||||
raise HTTPException(status_code=504, detail="Reranking timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"Reranking failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Reranking failed: {str(e)}")
|
||||
|
||||
+7
@@ -13,6 +13,7 @@
|
||||
# - 2026-06-04: ADR-034 — เปลี่ยน TYPHOON_OCR_MODEL เป็น typhoon-np-dms-ocr:latest; OLLAMA_API_URL ชี้ตรงไป Ollama (ไม่ผ่าน metrics proxy) เพื่อป้องกัน empty response
|
||||
# - 2026-06-02: เพิ่ม ollama-metrics (NorskHelsenett) — Prometheus sidecar สำหรับ Ollama metrics
|
||||
# expose /metrics บน port 9924; Prometheus (ASUSTOR) scrape จาก 192.168.10.100:9924
|
||||
# - 2026-06-11: US2 & US3 - เพิ่ม VRAM headroom, residency window, pressure threshold, retrieval timeout env variables
|
||||
#
|
||||
# วิธีรัน:
|
||||
# docker compose up -d --build
|
||||
@@ -45,6 +46,12 @@ services:
|
||||
TYPHOON_OCR_MODEL: "typhoon-np-dms-ocr:latest"
|
||||
# Timeout 360 วินาที/หน้า — รองรับ cold-start โหลด model (~70s) + inference (10GB model, CPU offload)
|
||||
TYPHOON_OCR_TIMEOUT: "360"
|
||||
# ─── VRAM, Residency & Timeout Configurations (Feature-235) ──────────────
|
||||
VRAM_HEADROOM_THRESHOLD_MB: "3000.0"
|
||||
OCR_RESIDENCY_WINDOW_SECONDS: "120"
|
||||
GPU_TOTAL_VRAM_MB: "16384.0"
|
||||
GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB: "12000.0"
|
||||
RETRIEVAL_TIMEOUT_SECONDS: "30.0"
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py
|
||||
# Change Log:
|
||||
# - 2026-06-11: Initial creation of residency_policy.py for calculating OCR keep_alive value dynamically
|
||||
|
||||
import os
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from services.vram_monitor import get_vram_headroom
|
||||
|
||||
logger = logging.getLogger("ocr-sidecar.residency-policy")
|
||||
|
||||
@dataclass
|
||||
class OcrResidencyDecision:
|
||||
keep_alive_seconds: int
|
||||
vram_headroom_mb: float
|
||||
reason: str
|
||||
|
||||
def calculate_ocr_residency(active_profile: str = None) -> OcrResidencyDecision:
|
||||
"""
|
||||
คำนวณ keep_alive สำหรับ Typhoon OCR จาก VRAM headroom และ active profile ของโมเดลหลัก
|
||||
"""
|
||||
threshold_mb = float(os.getenv("VRAM_HEADROOM_THRESHOLD_MB", "3000.0"))
|
||||
residency_window = int(os.getenv("OCR_RESIDENCY_WINDOW_SECONDS", "120"))
|
||||
pressure_threshold = float(os.getenv("GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB", "7000.0"))
|
||||
if active_profile in ("deep-analysis", "large-context"):
|
||||
return OcrResidencyDecision(0, -1.0, "large-context-active")
|
||||
headroom = get_vram_headroom()
|
||||
if not headroom.query_success:
|
||||
return OcrResidencyDecision(0, -1.0, "query-failed")
|
||||
if headroom.used_mb > pressure_threshold:
|
||||
return OcrResidencyDecision(0, headroom.available_mb, "high-pressure")
|
||||
if headroom.available_mb < threshold_mb:
|
||||
return OcrResidencyDecision(0, headroom.available_mb, "high-pressure")
|
||||
return OcrResidencyDecision(residency_window, headroom.available_mb, "headroom-sufficient")
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/vram_monitor.py
|
||||
# Change Log:
|
||||
# - 2026-06-11: Initial creation of VramMonitor service for Python OCR sidecar to query GPU VRAM headroom from Ollama /api/ps
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import httpx
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("ocr-sidecar.vram-monitor")
|
||||
|
||||
@dataclass
|
||||
class VramHeadroom:
|
||||
total_mb: float
|
||||
used_mb: float
|
||||
available_mb: float
|
||||
query_success: bool
|
||||
|
||||
def get_vram_headroom() -> VramHeadroom:
|
||||
"""
|
||||
ดึงข้อมูล VRAM headroom จาก Ollama /api/ps
|
||||
และคำนวณพื้นที่คงเหลือใน VRAM เพื่อประกอบการตัดสินใจเรื่อง Residency และ CPU Fallback
|
||||
"""
|
||||
ollama_url = os.getenv("OLLAMA_API_URL", "http://host.docker.internal:11434")
|
||||
total_vram_mb = float(os.getenv("GPU_TOTAL_VRAM_MB", "16384.0"))
|
||||
try:
|
||||
# ดึงสถานะ running models จาก Ollama
|
||||
with httpx.Client(timeout=3.0) as client:
|
||||
response = client.get(f"{ollama_url}/api/ps")
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Ollama ps endpoint returned status code: {response.status_code}")
|
||||
return VramHeadroom(total_vram_mb, total_vram_mb, 0.0, False)
|
||||
data = response.json()
|
||||
models = data.get("models", [])
|
||||
total_used_bytes = 0
|
||||
for model in models:
|
||||
total_used_bytes += model.get("size_vram", 0)
|
||||
used_mb = float(total_used_bytes) / (1024.0 * 1024.0)
|
||||
available_mb = max(0.0, total_vram_mb - used_mb)
|
||||
return VramHeadroom(total_vram_mb, used_mb, available_mb, True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to query Ollama VRAM: {str(e)}")
|
||||
return VramHeadroom(total_vram_mb, total_vram_mb, 0.0, False)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# File: specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/tests/test_retrieval_fallback.py
|
||||
# Change Log:
|
||||
# - 2026-06-11: Initial integration tests for retrieval fallback using pytest
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# Setup env variables before importing app
|
||||
os.environ["OCR_SIDECAR_API_KEY"] = "test-key"
|
||||
os.environ["VRAM_HEADROOM_THRESHOLD_MB"] = "3000.0"
|
||||
os.environ["RETRIEVAL_TIMEOUT_SECONDS"] = "2.0"
|
||||
|
||||
from app import app, EmbedRequest, RerankRequest, get_api_key
|
||||
|
||||
client = TestClient(app)
|
||||
API_HEADERS = {"X-API-Key": "test-key"}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bge_model():
|
||||
with patch("app.bge_model") as mock:
|
||||
mock.model = MagicMock()
|
||||
mock.encode.return_value = {
|
||||
"dense_vecs": [[0.1, 0.2]],
|
||||
"lexical_weights": [{"101": 0.5}]
|
||||
}
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def mock_reranker():
|
||||
with patch("app.reranker") as mock:
|
||||
mock.model = MagicMock()
|
||||
mock.compute_score.return_value = [0.85]
|
||||
yield mock
|
||||
|
||||
def test_embed_gpu_when_headroom_sufficient(mock_bge_model):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=2000.0, available_mb=14384.0, query_success=True)
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock), \
|
||||
patch("torch.cuda.is_available", return_value=True):
|
||||
response = client.post("/embed", json={"text": "hello test"}, headers=API_HEADERS)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["device"] == "cuda"
|
||||
mock_bge_model.model.to.assert_called_with("cuda")
|
||||
|
||||
def test_embed_cpu_when_headroom_insufficient(mock_bge_model):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=14000.0, available_mb=2384.0, query_success=True)
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock):
|
||||
response = client.post("/embed", json={"text": "hello test"}, headers=API_HEADERS)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["device"] == "cpu"
|
||||
mock_bge_model.model.to.assert_called_with("cpu")
|
||||
|
||||
def test_embed_cpu_when_gpu_query_failed(mock_bge_model):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=16384.0, available_mb=0.0, query_success=False)
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock):
|
||||
response = client.post("/embed", json={"text": "hello test"}, headers=API_HEADERS)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["device"] == "cpu"
|
||||
mock_bge_model.model.to.assert_called_with("cpu")
|
||||
|
||||
def test_embed_timeout_returns_504(mock_bge_model):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=2000.0, available_mb=14384.0, query_success=True)
|
||||
# Mock encode to simulate a slow run
|
||||
def slow_encode(*args, **kwargs):
|
||||
import time
|
||||
time.sleep(3.0)
|
||||
return {"dense_vecs": [[0.1]], "lexical_weights": [{"1": 0.1}]}
|
||||
mock_bge_model.encode.side_effect = slow_encode
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock):
|
||||
response = client.post("/embed", json={"text": "hello test"}, headers=API_HEADERS)
|
||||
assert response.status_code == 504
|
||||
|
||||
def test_rerank_gpu_when_headroom_sufficient(mock_reranker):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=2000.0, available_mb=14384.0, query_success=True)
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock), \
|
||||
patch("torch.cuda.is_available", return_value=True):
|
||||
response = client.post("/rerank", json={"query": "test query", "chunks": ["chunk1"]}, headers=API_HEADERS)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["device"] == "cuda"
|
||||
mock_reranker.model.to.assert_called_with("cuda")
|
||||
|
||||
def test_rerank_cpu_when_headroom_insufficient(mock_reranker):
|
||||
vram_mock = MagicMock(total_mb=16384.0, used_mb=14000.0, available_mb=2384.0, query_success=True)
|
||||
with patch("app.get_vram_headroom", return_value=vram_mock):
|
||||
response = client.post("/rerank", json={"query": "test query", "chunks": ["chunk1"]}, headers=API_HEADERS)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["device"] == "cpu"
|
||||
mock_reranker.model.to.assert_called_with("cpu")
|
||||
@@ -89,7 +89,7 @@
|
||||
**Purpose**: Documentation update + compliance verification
|
||||
|
||||
- [X] T018 [P] อัปเดต `AGENTS.md` — Current Decisions D10: เปลี่ยน `gemma4:e4b Q8_0` เป็น `typhoon2.5-np-dms:latest (main) + typhoon-np-dms-ocr:latest (OCR)`; อัปเดต version เป็น v1.9.9 และ sync date
|
||||
- [X] T019 [P] อัปเดต `memory/agent-memory.md` — Section 2.5 model names + Section 5 D10 + Section 7 Ollama row + Section 8 Recent Rollouts entry
|
||||
- [X] T019 [P] อัปเดต `memory/project-memory-override.md` — Section 2.5 model names + Section 5 D10 + Section 7 Ollama row + Section 8 Recent Rollouts entry
|
||||
- [X] T020 [P] อัปเดต `.agents/rules/11-ai-integration.md` — 2-model stack: `gemma4:e2b → typhoon2.5-np-dms:latest`
|
||||
- [ ] T021 [P] รัน type check: `pnpm --filter backend build` — ต้องผ่าน 0 errors
|
||||
- [ ] T022 [P] รัน lint: `pnpm --filter backend lint` — ตรวจสอบ no console.log, no any
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
- [x] T047 [P] Add error handling for VRAM insufficiency in backend/src/modules/ai/services/ai.service.ts
|
||||
- [x] T048 [P] Add error handling for Ollama service unavailability in backend/src/modules/ai/services/ocr.service.ts
|
||||
- [x] T049 Run quickstart.md validation on Admin Desktop
|
||||
- [x] T050 Update agent-memory.md with Typhoon OCR integration details
|
||||
- [x] T050 Update project-memory-override.md with Typhoon OCR integration details
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/checklists/cutover-validation.md
|
||||
// Change Log:
|
||||
// - 2026-06-11: Initial cutover validation checklist for T032 and sidecar pytest
|
||||
|
||||
# Cutover Validation Checklist: Feature 235
|
||||
|
||||
**Purpose**: ใช้ปิด `T032` และเก็บหลักฐานสำหรับเลื่อนสถานะ validation จาก `PARTIAL` ไป `PASS`
|
||||
|
||||
> หมายเหตุ
|
||||
>
|
||||
> - Checklist นี้อิง **implementation ปัจจุบัน** ของ Option B
|
||||
> - อย่าใช้ตัวอย่างเก่าใน `quickstart.md` ที่ยังส่ง `executionProfile` / `large-context` จาก caller
|
||||
> - คำสั่งด้านล่างเป็น **PowerShell** ตามกฎของ repo
|
||||
|
||||
## 1. Environment Ready
|
||||
|
||||
- [ ] Backend รันที่ `http://localhost:3001`
|
||||
- [ ] Frontend รันที่ `http://localhost:3000`
|
||||
- [ ] OCR sidecar รันที่ `http://192.168.10.100:8765`
|
||||
- [ ] Ollama รันและมี tag `np-dms-ai` / `np-dms-ocr`
|
||||
- [ ] มี admin token สำหรับเรียก API
|
||||
- [ ] มี `documentPublicId` และ `projectPublicId` ที่มีอยู่จริงสำหรับทดสอบ `rag-query`
|
||||
- [ ] มีไฟล์ PDF ตัวอย่างสำหรับ OCR Sandbox
|
||||
|
||||
## 2. Automated Validation
|
||||
|
||||
### 2.1 Backend targeted tests
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
pnpm --filter backend test -- --runInBand --testPathPatterns="ai.service.spec.ts|queue-policy.spec.ts|ai.controller.spec.ts|ai-policy.service.spec.ts|ocr-residency.spec.ts|vram-monitor.service.spec.ts"
|
||||
```
|
||||
|
||||
- [ ] Expected: ทุก suite ผ่าน
|
||||
|
||||
### 2.2 Backend build
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
pnpm --filter backend build
|
||||
```
|
||||
|
||||
- [ ] Expected: build ผ่านไม่มี compile error
|
||||
|
||||
### 2.3 Sidecar pytest
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
python -m pytest specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/tests -v
|
||||
```
|
||||
|
||||
- [ ] Expected: `test_retrieval_fallback.py` ผ่านครบ
|
||||
- [ ] ถ้า `pytest` ไม่พบ module: บันทึกว่า environment ยังไม่พร้อม และติดตั้ง dependency ก่อน rerun
|
||||
|
||||
## 3. Manual Gate 1: Policy Contract
|
||||
|
||||
ตั้งค่า token และ ids ก่อน:
|
||||
|
||||
```powershell
|
||||
$TOKEN = "<admin-jwt>"
|
||||
$PROJECT_PUBLIC_ID = "<existing-project-public-id>"
|
||||
$DOCUMENT_PUBLIC_ID = "<existing-document-public-id>"
|
||||
```
|
||||
|
||||
### 3.1 Reject forbidden `model`
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
type = "rag-query"
|
||||
projectPublicId = $PROJECT_PUBLIC_ID
|
||||
payload = @{ query = "test policy contract" }
|
||||
model = @{ key = "typhoon2.5-np-dms:latest" }
|
||||
} | ConvertTo-Json -Depth 5
|
||||
|
||||
Invoke-RestMethod "http://localhost:3001/api/ai/jobs" `
|
||||
-Method Post `
|
||||
-Headers @{
|
||||
Authorization = "Bearer $TOKEN"
|
||||
"Idempotency-Key" = "feature235-gate1-model"
|
||||
"Content-Type" = "application/json"
|
||||
} `
|
||||
-Body $body
|
||||
```
|
||||
|
||||
- [ ] Expected: HTTP `400`
|
||||
|
||||
### 3.2 Reject forbidden `executionProfile`
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
type = "rag-query"
|
||||
projectPublicId = $PROJECT_PUBLIC_ID
|
||||
payload = @{ query = "test forbidden profile" }
|
||||
executionProfile = "quality"
|
||||
} | ConvertTo-Json -Depth 5
|
||||
```
|
||||
|
||||
- [ ] Expected: HTTP `400`
|
||||
|
||||
### 3.3 Reject forbidden parameter override
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
type = "rag-query"
|
||||
projectPublicId = $PROJECT_PUBLIC_ID
|
||||
payload = @{ query = "test forbidden temperature" }
|
||||
temperature = 0.9
|
||||
} | ConvertTo-Json -Depth 5
|
||||
```
|
||||
|
||||
- [ ] Expected: HTTP `400`
|
||||
|
||||
### 3.4 Valid `rag-query`
|
||||
|
||||
- [ ] รัน:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
type = "rag-query"
|
||||
projectPublicId = $PROJECT_PUBLIC_ID
|
||||
documentPublicId = $DOCUMENT_PUBLIC_ID
|
||||
payload = @{ query = "สรุปเอกสารนี้" }
|
||||
} | ConvertTo-Json -Depth 5
|
||||
|
||||
Invoke-RestMethod "http://localhost:3001/api/ai/jobs" `
|
||||
-Method Post `
|
||||
-Headers @{
|
||||
Authorization = "Bearer $TOKEN"
|
||||
"Idempotency-Key" = "feature235-gate1-valid"
|
||||
"Content-Type" = "application/json"
|
||||
} `
|
||||
-Body $body
|
||||
```
|
||||
|
||||
- [ ] Expected:
|
||||
- HTTP `201`
|
||||
- `modelUsed = "np-dms-ai"`
|
||||
- `effectiveProfile = "standard"`
|
||||
- `queueName = "ai-batch"`
|
||||
|
||||
## 4. Manual Gate 2: Canonical Naming
|
||||
|
||||
### 4.1 Audit log check
|
||||
|
||||
- [ ] ตรวจ row ล่าสุดใน `ai_audit_logs`
|
||||
- [ ] Expected:
|
||||
- `effective_profile` มีค่า
|
||||
- `canonical_model` เป็น `np-dms-ai` หรือ `np-dms-ocr`
|
||||
- ไม่มี runtime name หลุดออกในฟิลด์ user-facing
|
||||
|
||||
### 4.2 Admin Console check
|
||||
|
||||
- [ ] เปิด `http://localhost:3000/admin/ai`
|
||||
- [ ] ตรวจ Overview / health / model cards
|
||||
- [ ] Expected:
|
||||
- เห็น `np-dms-ai`
|
||||
- เห็น `np-dms-ocr`
|
||||
- ไม่เห็น `typhoon2.5-np-dms:latest`
|
||||
- ไม่เห็น `typhoon-np-dms-ocr:latest`
|
||||
|
||||
### 4.3 OCR Sandbox badge check
|
||||
|
||||
- [ ] เปิด OCR Sandbox ในหน้า admin AI
|
||||
- [ ] รัน OCR 1 รอบ
|
||||
- [ ] Expected:
|
||||
- badge หรือ result label แสดง `np-dms-ocr`
|
||||
- ไม่โชว์ runtime name โดยตรง
|
||||
|
||||
## 5. Manual Gate 3: Adaptive OCR Residency
|
||||
|
||||
### 5.1 High-pressure / deep-analysis behavior
|
||||
|
||||
- [ ] ทำให้ main model กิน VRAM สูง หรือจำลอง workload ที่เข้าข่าย pressure
|
||||
- [ ] รัน OCR Sandbox หรือ OCR job
|
||||
- [ ] ตรวจ sidecar / backend logs
|
||||
- [ ] Expected:
|
||||
- `keep_alive = 0`
|
||||
- reason เป็น `high-pressure` หรือ `deep-analysis-active`
|
||||
|
||||
### 5.2 Headroom sufficient behavior
|
||||
|
||||
- [ ] รัน OCR job ตอนที่ GPU headroom สูง
|
||||
- [ ] ตรวจ logs
|
||||
- [ ] Expected:
|
||||
- `keep_alive > 0`
|
||||
- reason เป็น `headroom-sufficient`
|
||||
|
||||
## 6. Manual Gate 4: Retrieval CPU Fallback
|
||||
|
||||
### 6.1 Force GPU pressure
|
||||
|
||||
- [ ] warm model:
|
||||
|
||||
```powershell
|
||||
$warm = @{
|
||||
model = "np-dms-ai"
|
||||
prompt = "warmup"
|
||||
keep_alive = -1
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod "http://localhost:11434/api/generate" `
|
||||
-Method Post `
|
||||
-ContentType "application/json" `
|
||||
-Body $warm
|
||||
```
|
||||
|
||||
### 6.2 Submit `rag-query` under pressure
|
||||
|
||||
- [ ] ส่ง request แบบเดียวกับ Gate 1.4 แต่เปลี่ยน `Idempotency-Key`
|
||||
- [ ] Expected:
|
||||
- request enqueue สำเร็จ
|
||||
- job ไม่ fail hard
|
||||
|
||||
### 6.3 Verify fallback evidence
|
||||
|
||||
- [ ] ตรวจ sidecar logs
|
||||
- [ ] Expected:
|
||||
- `device=cpu` หรือ `device: cpu`
|
||||
- reason เป็น `gpu-headroom-below-threshold` หรือ `gpu-query-failed`
|
||||
|
||||
## 7. Evidence to Attach
|
||||
|
||||
- [ ] backend test output
|
||||
- [ ] backend build output
|
||||
- [ ] sidecar pytest output
|
||||
- [ ] screenshot หน้า `/admin/ai`
|
||||
- [ ] screenshot OCR Sandbox result
|
||||
- [ ] copy log line ของ residency decision
|
||||
- [ ] copy log line ของ CPU fallback
|
||||
- [ ] sample successful `rag-query` response body
|
||||
|
||||
## 8. Pass Criteria
|
||||
|
||||
- [ ] Automated backend tests ผ่าน
|
||||
- [ ] Backend build ผ่าน
|
||||
- [ ] Sidecar pytest ผ่าน
|
||||
- [ ] Gate 1 ผ่านครบ
|
||||
- [ ] Gate 2 ผ่านครบ
|
||||
- [ ] Gate 3 ผ่านครบ
|
||||
- [ ] Gate 4 ผ่านครบ
|
||||
- [ ] หลักฐานถูกแนบหรือบันทึกไว้ใน feature folder
|
||||
|
||||
## 9. Follow-up After Completion
|
||||
|
||||
- [ ] update `tasks.md` ให้ติ๊ก `T032`
|
||||
- [ ] update `validation-report.md` จาก `PARTIAL` เป็น `PASS`
|
||||
- [ ] ถ้าเจอ spec drift ให้ปรับ `quickstart.md` และจุดอ้างอิงที่ยังใช้ contract เก่า
|
||||
+56
-16
@@ -1,51 +1,91 @@
|
||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/contracts/create-ai-job.dto.md
|
||||
// Change Log:
|
||||
// - 2026-06-11: API contract for CreateAiJobDto
|
||||
// - 2026-06-11: Option B — backend-determined policy; ลบ executionProfile ออกจาก request
|
||||
// - 2026-06-11: Rename profiles — interactive/standard/quality/deep-analysis; เพิ่ม default values จาก docs/ai-profiles.md
|
||||
|
||||
# Contract: POST /api/ai/jobs
|
||||
|
||||
## Request DTO
|
||||
|
||||
```typescript
|
||||
// PublicJobType — เปิดให้ caller ส่งมาใน API
|
||||
type PublicJobType = 'auto-fill-document' | 'migrate-document' | 'rag-query';
|
||||
|
||||
// InternalJobType — ใช้ภายใน AiPolicyService เท่านั้น ไม่ expose ใน API
|
||||
type InternalJobType = PublicJobType | 'intent-classify' | 'tool-suggest' | 'ocr-extract';
|
||||
|
||||
interface CreateAiJobRequest {
|
||||
type: 'auto-fill-document' | 'migrate-document' | 'rag-query';
|
||||
type: PublicJobType;
|
||||
documentPublicId?: string; // UUIDv7 — ADR-019
|
||||
attachmentPublicId?: string; // UUIDv7 — ADR-019
|
||||
executionProfile?: 'fast' | 'balanced' | 'thai-accurate' | 'large-context';
|
||||
// [FORBIDDEN] executionProfile — HTTP 400 if present (backend กำหนดเอง)
|
||||
// [FORBIDDEN] model.key — HTTP 400 if present
|
||||
// [FORBIDDEN] temperature, top_p, maxTokens — HTTP 400 if present
|
||||
}
|
||||
```
|
||||
|
||||
> **หมายเหตุ**: ไม่มี `executionProfile` ใน request — backend กำหนด execution policy ทั้งหมดจาก `job.type` อัตโนมัติ user ทั่วไปไม่ต้องรู้จัก profile เลย
|
||||
> `intent-classify`, `tool-suggest`, `ocr-extract` เป็น **internal job types** — เกิดภายใน service โดยตรง ไม่ผ่าน API
|
||||
|
||||
## 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 |
|
||||
| `type` | Required; enum `'auto-fill-document' \| 'migrate-document' \| 'rag-query'` |
|
||||
| `executionProfile` | **FORBIDDEN** — HTTP 400 ถ้ามีใน payload |
|
||||
| `model.*` | **FORBIDDEN** — ANY model subfield → HTTP 400 |
|
||||
| `temperature` | **FORBIDDEN** — HTTP 400 ถ้ามีใน payload |
|
||||
| `top_p` | **FORBIDDEN** — HTTP 400 ถ้ามีใน payload |
|
||||
| `maxTokens` | **FORBIDDEN** — HTTP 400 ถ้ามีใน payload |
|
||||
| `documentPublicId` | Optional; UUIDv7 string (ADR-019) — ห้าม parseInt |
|
||||
| `attachmentPublicId` | Optional; UUIDv7 string (ADR-019) — ห้าม parseInt |
|
||||
|
||||
## Job Type → Effective Profile Mapping (Backend Policy)
|
||||
|
||||
| `job.type` | `effectiveProfile` | `canonicalModel` | `queueName` |
|
||||
|---|---|---|---|
|
||||
| `auto-fill-document` | `quality` | `np-dms-ai` | `ai-batch` |
|
||||
| `migrate-document` | `quality` | `np-dms-ai` | `ai-batch` |
|
||||
| `rag-query` | `standard` | `np-dms-ai` | `ai-batch` |
|
||||
| `intent-classify` | `interactive` | `np-dms-ai` | `ai-realtime` | *(internal only)* |
|
||||
| `tool-suggest` | `interactive` | `np-dms-ai` | `ai-realtime` | *(internal only)* |
|
||||
| `ocr-extract` | *(OCR residency policy)* | `np-dms-ocr` | `ai-batch` | *(internal only)* |
|
||||
| `sandbox-analysis` | `deep-analysis` | `np-dms-ai` | `ai-batch` | *(admin OCR Sandbox only)* |
|
||||
|
||||
> Mapping นี้กำหนดใน `AiPolicyService` — ไม่ expose ให้ caller เห็น
|
||||
|
||||
## Profile Default Parameters (จาก `docs/ai-profiles.md`)
|
||||
|
||||
| Profile | `temperature` | `top_p` | `max_tokens` | `num_ctx` | `repeat_penalty` | `keep_alive` |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `interactive` | 0.7 | 0.9 | 2048 | 4096 | 1.15 | `"5m"` |
|
||||
| `standard` | 0.5 | 0.8 | 4096 | 8192 | 1.15 | `"10m"` |
|
||||
| `quality` | 0.1 | 0.95 | 8192 | 8192 | 1.15 | `"10m"` |
|
||||
| `deep-analysis` | 0.3 | 0.85 | 8192 | 32768 | 1.15 | `"0"` |
|
||||
|
||||
> ค่าเหล่านี้เป็น **default** — ops/admin calibrate ได้ผ่าน Admin Console และบันทึกใน DB ตาม ADR-029 (Dynamic Prompt Management)
|
||||
|
||||
## Response DTO
|
||||
|
||||
```typescript
|
||||
type ExecutionProfile = 'interactive' | 'standard' | 'quality' | 'deep-analysis';
|
||||
|
||||
interface AiJobResponse {
|
||||
jobId: string; // BullMQ job ID
|
||||
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)
|
||||
modelUsed: 'np-dms-ai' | 'np-dms-ocr'; // Canonical name — never runtime tag
|
||||
effectiveProfile: ExecutionProfile; // Profile ที่ backend กำหนดจาก job.type
|
||||
queueName: 'ai-realtime' | 'ai-batch';
|
||||
}
|
||||
```
|
||||
|
||||
> `effectiveProfile` ใน response คือ **read-only informational field** สำหรับ admin/developer ดู — ไม่ใช่ input
|
||||
|
||||
## 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 |
|
||||
| 400 | `executionProfile`, `model.key`, หรือ parameter overrides มีใน payload |
|
||||
| 422 | `documentPublicId` หรือ `attachmentPublicId` ไม่พบใน DB |
|
||||
| 504 | CPU fallback retrieval timeout (`/embed` หรือ `/rerank`)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/data-model.md
|
||||
// Change Log:
|
||||
// - 2026-06-11: Data model for AI Runtime Policy Refactor
|
||||
// - 2026-06-11: Rename ExecutionProfile — interactive/standard/quality/deep-analysis; เพิ่ม numCtx, repeatPenalty ใน RuntimePolicy
|
||||
// - 2026-06-11: เพิ่ม OcrRuntimePolicy จาก np-dms-ocr.model.md (fixed parameters, keep_alive dynamic)
|
||||
|
||||
# Data Model: AI Runtime Policy Refactor
|
||||
|
||||
@@ -10,11 +12,30 @@
|
||||
|
||||
## TypeScript Types (Backend)
|
||||
|
||||
### JobType (types)
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
|
||||
|
||||
// PublicJobType — รับจาก caller ผ่าน POST /api/ai/jobs เท่านั้น
|
||||
export type PublicJobType = 'auto-fill-document' | 'migrate-document' | 'rag-query';
|
||||
|
||||
// InternalJobType — ใช้ภายใน AiPolicyService; ครอบคลุมทุก job type รวม internal
|
||||
// sandbox-analysis — admin trigger ผ่าน OCR Sandbox โดยตรง (deep-analysis profile)
|
||||
export type InternalJobType = PublicJobType | 'intent-classify' | 'tool-suggest' | 'ocr-extract' | 'sandbox-analysis';
|
||||
```
|
||||
|
||||
> `intent-classify`, `tool-suggest`, `ocr-extract` — internal เท่านั้น; ถ้า caller ส่ง type เหล่านี้มา → HTTP 400
|
||||
|
||||
---
|
||||
|
||||
### ExecutionProfile (enum)
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
|
||||
export type ExecutionProfile = 'fast' | 'balanced' | 'thai-accurate' | 'large-context';
|
||||
// ค่า default ของแต่ละ profile ดูได้ที่ docs/ai-profiles.md
|
||||
// ops/admin calibrate ได้ผ่าน Admin Console และบันทึกใน DB ตาม ADR-029
|
||||
export type ExecutionProfile = 'interactive' | 'standard' | 'quality' | 'deep-analysis';
|
||||
```
|
||||
|
||||
### RuntimePolicy (interface)
|
||||
@@ -23,13 +44,34 @@ export type ExecutionProfile = 'fast' | 'balanced' | 'thai-accurate' | 'large-co
|
||||
// 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
|
||||
temperature: number; // default: interactive=0.7, standard=0.5, quality=0.1, deep-analysis=0.3
|
||||
topP: number; // default: interactive=0.9, standard=0.8, quality=0.95, deep-analysis=0.85
|
||||
maxTokens: number; // default: interactive=2048, standard=4096, quality=8192, deep-analysis=8192
|
||||
numCtx: number; // default: interactive=4096, standard=8192, quality=8192, deep-analysis=32768
|
||||
repeatPenalty: number; // default: 1.15 ทุก profile
|
||||
keepAliveSeconds: number; // default: interactive=300, standard=600, quality=600, deep-analysis=0
|
||||
}
|
||||
```
|
||||
|
||||
### OcrRuntimePolicy (interface)
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/interfaces/ocr-residency.interface.ts
|
||||
// Parameters จาก specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/np-dms-ocr.model.md
|
||||
// ไม่ calibrate ผ่าน Admin Console — ค่า fixed ตาม Modelfile
|
||||
export interface OcrRuntimePolicy {
|
||||
canonicalModel: 'np-dms-ocr'; // FROM scb10x/typhoon-ocr1.5-3b:latest
|
||||
numCtx: 8192; // PARAMETER num_ctx 8192
|
||||
numPredict: 4096; // PARAMETER num_predict 4096
|
||||
temperature: 0.1; // PARAMETER temperature 0.1
|
||||
topP: 0.1; // PARAMETER top_p 0.1
|
||||
repeatPenalty: 1.1; // PARAMETER repeat_penalty 1.1
|
||||
keepAliveSeconds: number; // dynamic — คำนวณจาก OcrResidencyDecision
|
||||
}
|
||||
```
|
||||
|
||||
> `np-dms-ocr` ใช้ parameters คงที่ตาม Modelfile — **มีแค่ `keep_alive` เท่านั้นที่ dynamic** ตาม VRAM headroom
|
||||
|
||||
### OcrResidencyDecision (interface)
|
||||
|
||||
```typescript
|
||||
@@ -38,7 +80,7 @@ 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';
|
||||
reason: 'deep-analysis-active' | 'high-pressure' | 'headroom-sufficient' | 'query-failed';
|
||||
}
|
||||
```
|
||||
|
||||
@@ -54,14 +96,42 @@ export interface VramHeadroom {
|
||||
}
|
||||
```
|
||||
|
||||
### AiJobPayload (BullMQ job data)
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/interfaces/execution-policy.interface.ts
|
||||
// BullMQ job payload — parameters ถูก snapshot ณ เวลา dispatch (FR-A09)
|
||||
// worker ใช้ค่าจาก payload โดยตรง ไม่อ่าน DB/Redis อีกรอบ
|
||||
export interface AiJobPayload {
|
||||
jobType: InternalJobType;
|
||||
documentPublicId?: string;
|
||||
attachmentPublicId?: string;
|
||||
// snapshot ณ เวลา dispatch โดย AiPolicyService
|
||||
effectiveProfile: ExecutionProfile;
|
||||
canonicalModel: 'np-dms-ai' | 'np-dms-ocr';
|
||||
snapshotParams: {
|
||||
temperature: number;
|
||||
topP: number;
|
||||
maxTokens: number;
|
||||
numCtx: number;
|
||||
repeatPenalty: number;
|
||||
keepAliveSeconds: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> `snapshotParams` ทำให้ทุก job predictable — แม้ admin calibrate ค่าใหม่ระหว่าง job queue อยู่ ค่าเดิมที่ snapshot ไว้จะถูกใช้; audit log บันทึก `snapshotParams` ด้วยเพื่อ traceability
|
||||
|
||||
---
|
||||
|
||||
### CreateAiJobDto (updated)
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/dto/create-ai-job.dto.ts
|
||||
// [CHANGE] ลบ model field และ parameter overrides ออก
|
||||
// [CHANGE] ลบ executionProfile, model fields ออกทั้งหมด — backend กำหนดจาก job.type
|
||||
export class CreateAiJobDto {
|
||||
@IsEnum(['auto-fill-document', 'migrate-document', 'rag-query'])
|
||||
type: 'auto-fill-document' | 'migrate-document' | 'rag-query';
|
||||
type: PublicJobType;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID('all')
|
||||
@@ -71,16 +141,56 @@ export class CreateAiJobDto {
|
||||
@IsUUID('all')
|
||||
attachmentPublicId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['fast', 'balanced', 'thai-accurate', 'large-context'])
|
||||
executionProfile?: ExecutionProfile;
|
||||
|
||||
// [REMOVED] executionProfile — backend กำหนดอัตโนมัติจาก job.type (Option B)
|
||||
// [REMOVED] model: { key, parameters } — ไม่อนุญาตแล้ว
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DB Schema Extensions
|
||||
|
||||
### ai_execution_profiles (new table)
|
||||
|
||||
```sql
|
||||
-- Delta: specs/03-Data-and-Storage/deltas/2026-06-11-create-ai-execution-profiles.sql
|
||||
CREATE TABLE ai_execution_profiles (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
profile_name VARCHAR(50) NOT NULL UNIQUE, -- 'interactive'|'standard'|'quality'|'deep-analysis'
|
||||
temperature DECIMAL(4,3) NOT NULL,
|
||||
top_p DECIMAL(4,3) NOT NULL,
|
||||
max_tokens INT NOT NULL,
|
||||
num_ctx INT NOT NULL,
|
||||
repeat_penalty DECIMAL(5,3) NOT NULL,
|
||||
keep_alive_seconds INT NOT NULL, -- 0 = unload immediately
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
updated_by INT NULL, -- NULL = seed default
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
> - ค่า default seed จาก `docs/ai-profiles.md` ผ่าน delta SQL
|
||||
> - Admin calibrate ผ่าน Admin Console → `UPDATE ai_execution_profiles SET ... WHERE profile_name = ?`
|
||||
> - `AiPolicyService` อ่านค่าจาก table นี้ (Redis cache TTL 60s ตาม ADR-029 pattern)
|
||||
> - `ON DUPLICATE KEY UPDATE profile_name = profile_name` — ป้องกัน overwrite ค่าที่ admin calibrate ไว้
|
||||
|
||||
### ai_audit_logs (extended columns)
|
||||
|
||||
```sql
|
||||
-- Delta: specs/03-Data-and-Storage/deltas/2026-06-11-extend-ai-audit-logs-runtime-policy.sql
|
||||
ALTER TABLE ai_audit_logs
|
||||
ADD COLUMN effective_profile VARCHAR(50) NULL -- 'interactive'|'standard'|'quality'|'deep-analysis'
|
||||
ADD COLUMN canonical_model VARCHAR(50) NULL -- 'np-dms-ai' | 'np-dms-ocr'
|
||||
ADD COLUMN snapshot_params_json JSON NULL; -- { temperature, topP, maxTokens, numCtx, repeatPenalty, keepAliveSeconds }
|
||||
```
|
||||
|
||||
> - `effective_profile` + `canonical_model` แทน legacy `ai_model` / `model_name` ที่มีชื่อ runtime tag
|
||||
> - `snapshot_params_json` บันทึก parameters จริงที่ใช้ใน Ollama call (FR-A09) — ทำให้ audit traceability สมบูรณ์
|
||||
> - columns เดิม (`ai_model`, `model_name`) ยังคงอยู่ (backward compat) — Feature-235 เขียน columns ใหม่เพิ่มเติม
|
||||
|
||||
---
|
||||
|
||||
## Python Types (OCR Sidecar)
|
||||
|
||||
### VramHeadroom (dataclass)
|
||||
|
||||
@@ -22,19 +22,19 @@
|
||||
|
||||
### 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 เดิม
|
||||
User ทั่วไปส่ง AI job request ผ่าน AI Gateway โดยระบุแค่ `job type` — ระบบ backend กำหนด execution policy (model, parameters) ทั้งหมดอัตโนมัติตาม job type โดยไม่มี caller input ใดๆ เกี่ยวกับ model หรือ profile — system แสดงและบันทึก model ในทุก layer ด้วยชื่อ canonical `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime เดิม Admin/Superadmin สามารถดูและทดสอบ policy behavior ผ่าน Admin Console และ OCR Sandbox เท่านั้น
|
||||
|
||||
**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`
|
||||
**Independent Test**: ยิง POST ไปยัง AI Gateway endpoint ด้วย `job type` เท่านั้น แล้วตรวจว่า response แสดง `modelUsed: "np-dms-ai"` และ audit log มี `effectiveProfile` ที่ถูกต้องตาม job type
|
||||
|
||||
**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
|
||||
1. **Given** AI job request ที่มี `model: { key: "typhoon2.5-np-dms:latest" }` หรือ `executionProfile` field ใดๆ, **When** ส่งไปยัง `POST /api/ai/jobs`, **Then** system ตอบ HTTP 400 เพราะ fields เหล่านั้นไม่อนุญาต
|
||||
2. **Given** AI job request ที่มีแค่ `type: "rag-query"`, **When** job ถูก dispatch ไปยัง `ai-batch` queue, **Then** job payload บันทึก `modelUsed: "np-dms-ai"` และ `effectiveProfile` ที่ backend กำหนดให้ใน 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
|
||||
4. **Given** `auto-fill-document` job ถูกส่งมา, **When** backend process job, **Then** backend กำหนด `effectiveProfile: "quality"` อัตโนมัติตาม job type โดยไม่รับ input จาก caller
|
||||
5. **Given** admin เปิด OCR Sandbox, **When** ทดสอบ OCR job, **Then** สามารถดู `effectiveProfile` และ `modelUsed` ที่ระบบกำหนดให้ในผลลัพธ์
|
||||
|
||||
---
|
||||
|
||||
@@ -107,10 +107,12 @@ BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency
|
||||
### Edge Cases
|
||||
|
||||
- ถ้า VRAM headroom calculation service ล้มเหลว (timeout หรือ error) → ต้อง fallback เป็น `keep_alive: 0` เสมอ (safe default)
|
||||
- ถ้า caller ส่ง `executionProfile` ที่ไม่อยู่ใน canonical set → ตอบ 400 validation error
|
||||
- ถ้า caller ส่ง `executionProfile` หรือ `model.*` fields มาใน payload → ตอบ 400 validation error ทันที (FR-A01)
|
||||
- ถ้า `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
|
||||
- ถ้า `VramMonitorService` ทำงานผิดพลาดหลัง cutover (เช่น Ollama `/api/ps` schema เปลี่ยน) → ระบบยัง operate ได้ด้วย safe default (`keep_alive: 0`) — **ไม่มี rollback plan; policy คือ fix-forward เท่านั้น** ต้องแก้ไขจนสำเร็จ
|
||||
- VRAM race condition ระหว่าง headroom snapshot กับ Ollama request arrival ถือว่ายอมรับได้ เนื่องจาก `np-dms-ai` VRAM usage ใน production ถูก manual test จนมั่นใจก่อน cutover แล้ว
|
||||
|
||||
---
|
||||
|
||||
@@ -120,19 +122,21 @@ BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency
|
||||
|
||||
**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-A01**: System MUST reject AI job requests ที่มี `model.key`, `executionProfile`, `temperature`, `top_p`, หรือ `maxTokens` field ใน payload (HTTP 400) — ไม่มี caller input ใดๆ เกี่ยวกับ model หรือ profile
|
||||
- **FR-A02**: `CreateAiJobDto` MUST รับเฉพาะ `type`, `documentPublicId`, `attachmentPublicId` — ไม่มี profile หรือ model fields
|
||||
- **FR-A03**: Backend MUST กำหนด `effectiveProfile` อัตโนมัติจาก `job.type` ตาม policy mapping ใน `AiPolicyService`
|
||||
- **FR-A04**: Admin/Superadmin ดูและทดสอบ policy behavior ได้ผ่าน Admin Console และ OCR Sandbox เท่านั้น — ไม่ผ่าน API payload; OCR Sandbox ใช้ `sandbox-analysis` job type ภายใน ซึ่ง map ไป `deep-analysis` profile สำหรับ long-context document testing
|
||||
- **FR-A05**: System MUST map `job.type` → `{ effectiveProfile, canonicalModel, runtimeParameters }` ใน backend policy layer
|
||||
- **FR-A06**: ทุก job type MUST มี deterministic policy mapping — ไม่มี job type ใดที่ไม่มี default policy
|
||||
- **FR-A07**: ทุก layer (API response, audit log, Admin Console, OCR Sandbox) MUST แสดงชื่อ `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime จริง
|
||||
- **FR-A08**: audit log MUST บันทึก `effectiveProfile` (ค่าที่ backend กำหนด) และ `modelUsed` (canonical name) — `requestedProfile` เสมอ `null` เพราะไม่มี caller input
|
||||
- **FR-A09**: `AiPolicyService` MUST snapshot `{ temperature, topP, maxTokens, numCtx, repeatPenalty, keepAliveSeconds }` จาก `ai_execution_profiles` (DB/Redis) ณ เวลา dispatch แล้วฝังใน BullMQ job payload — worker ใช้ค่าจาก payload โดยตรง ไม่อ่าน DB อีกรอบ; ทำให้ทุก job predictable และ audit log ตรงกับ parameters ที่ใช้จริง
|
||||
|
||||
**Workstream B: Runtime Policy**
|
||||
|
||||
- **FR-B01**: Backend MUST มี policy mapping: `executionProfile` → `{ canonicalModel, keep_alive, temperature, top_p, maxTokens }`
|
||||
- **FR-B01**: Backend MUST มี policy mapping: `executionProfile` → `{ canonicalModel, keep_alive, temperature, top_p, max_tokens, num_ctx, repeat_penalty }`; ค่า default ตาม `docs/ai-profiles.md`; ค่าจริง calibrate ได้ผ่าน Admin Console และบันทึกใน DB ตาม ADR-029
|
||||
- **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-B03**: ถ้า active profile = `deep-analysis` หรือ main model pressure = high → OCR `keep_alive` MUST = `0` โดย "main model pressure สูง" นิยามว่า `np-dms-ai.size_vram` ใน Ollama `/api/ps` response > `GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB` (configurable env)
|
||||
- **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 ที่ใช้ตัดสิน
|
||||
@@ -155,7 +159,7 @@ BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **ExecutionProfile**: Enum value ที่ caller ส่งมา (`fast | balanced | thai-accurate | large-context`) — contract ระดับ API
|
||||
- **ExecutionProfile**: Enum value ที่ backend กำหนดภายใน (`interactive | standard | quality | deep-analysis`) — **ไม่ expose ใน public API** ใช้ภายใน policy layer และ audit log เท่านั้น; ค่า default กำหนดใน `docs/ai-profiles.md` และ calibrate ได้ผ่าน Admin Console (ADR-029)
|
||||
- **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` — ใช้ทุกชั้นที่ผู้ใช้เห็น
|
||||
@@ -182,6 +186,10 @@ BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency
|
||||
|
||||
- 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 ล้มเหลว"
|
||||
- Q: "main model pressure สูง" วัดอย่างไรในทางปฏิบัติ? → A: วัดจาก `np-dms-ai.size_vram` ใน Ollama `/api/ps` response เทียบกับ `GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB` (configurable env) — ไม่ใช้ Redis flag หรือ shared state ใหม่
|
||||
- Q: Rollback plan สำหรับ big bang cutover คืออะไร? → A: ไม่มี rollback — policy คือ fix-forward เท่านั้น; ถ้า cutover มีปัญหาต้องแก้ไขจนสำเร็จ
|
||||
- Q: audit log ควรบันทึก profile ที่ caller ส่งมา หรือ profile ที่ใช้จริงหลัง override? → A: บันทึกแค่ `effectiveProfile` และ `modelUsed` — `requestedProfile` เสมอ `null` เพราะ user ไม่ได้ส่ง profile มาเลย (backend กำหนดทั้งหมดจาก job type)
|
||||
- Q: `executionProfile` ควรรับจาก caller ไหม? → A: ไม่ — backend กำหนดทั้งหมดจาก job type; user ทั่วไปไม่รู้จัก profile เลย; admin ทดสอบผ่าน Admin Console/OCR Sandbox เท่านั้น
|
||||
|
||||
## Assumptions
|
||||
|
||||
@@ -189,5 +197,5 @@ BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency
|
||||
- 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 พร้อมกันในรอบเดียว
|
||||
- Big bang rollout: ไม่มี parallel legacy path — ทุก change deploy พร้อมกันในรอบเดียว; **ไม่มี rollback plan — fix-forward เท่านั้น**
|
||||
- `ai-realtime` concurrency uplift เป็น configuration change ไม่ใช่ architectural change ใหม่
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/tasks.md
|
||||
// Change Log:
|
||||
// - 2026-06-11: Initial task list for AI Runtime Policy Refactor
|
||||
// - 2026-06-11: เพิ่ม T040/T041 สำหรับ delta SQL (ai_execution_profiles, ai_audit_logs extension)
|
||||
// - 2026-06-11: อัปเดต T001 (AiJobPayload, JobType), T005 (snapshot), T010 (snapshotParams)
|
||||
|
||||
# Tasks: AI Runtime Policy Refactor
|
||||
|
||||
@@ -18,10 +20,10 @@
|
||||
|
||||
**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`
|
||||
- [x] T001 สร้าง interface file `backend/src/modules/ai/interfaces/execution-policy.interface.ts` — `ExecutionProfile` type (`interactive|standard|quality|deep-analysis`), `PublicJobType`, `InternalJobType`, `RuntimePolicy`, `OcrRuntimePolicy`, `AiJobPayload` (snapshot params), `VramHeadroom`
|
||||
- [x] T002 สร้าง interface file `backend/src/modules/ai/interfaces/ocr-residency.interface.ts` (OcrResidencyDecision interface)
|
||||
- [x] T003 [P] สร้าง `backend/src/modules/ai/services/vram-monitor.service.ts` — query Ollama `/api/ps` เพื่อคำนวณ VRAM headroom
|
||||
- [x] 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`
|
||||
|
||||
---
|
||||
|
||||
@@ -31,13 +33,15 @@
|
||||
|
||||
**⚠️ 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`
|
||||
- [x] T040 [P] Apply delta SQL `specs/03-Data-and-Storage/deltas/2026-06-11-create-ai-execution-profiles.sql` — สร้าง table `ai_execution_profiles` + seed 4 profiles; ตรวจว่ามี row `interactive`, `standard`, `quality`, `deep-analysis` ใน DB (**MUST apply ก่อน** T005 อ่าน table นี้)
|
||||
- [x] T041 [P] Apply delta SQL `specs/03-Data-and-Storage/deltas/2026-06-11-extend-ai-audit-logs-runtime-policy.sql` — เพิ่ม columns `effective_profile`, `canonical_model`, `snapshot_params_json` ใน `ai_audit_logs`; ตรวจด้วย `SHOW COLUMNS` (**MUST apply ก่อน** T010 เขียนลง columns เหล่านี้)
|
||||
- [x] T005 สร้าง `backend/src/modules/ai/services/ai-policy.service.ts` — `InternalJobType` → `ExecutionProfile` mapping, อ่าน `ai_execution_profiles` จาก DB (Redis cache TTL 60s), snapshot `RuntimePolicy` parameters ลง `AiJobPayload` ตอน dispatch (FR-A09)
|
||||
- [x] T006 ~~ลบออก~~ ExecutionProfileGuard ไม่จำเป็นแล้ว — ไม่มี caller input เลย (Option B) *skip task นี้*
|
||||
- [x] T007 [P] แก้ `backend/src/modules/ai/dto/create-ai-job.dto.ts` — เอา `model.key`, `executionProfile`, `temperature`, `top_p`, `maxTokens` ออกทั้งหมด; เหลือเฉพาะ `type`, `documentPublicId`, `attachmentPublicId`; เพิ่ม `@IsForbidden()` validator หรือ forbidden field check ใน pipe
|
||||
- [x] T008 สร้าง `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py` — OCR keep_alive calculation function
|
||||
- [x] T009 แก้ `backend/src/modules/ai/ai.module.ts` — register `AiPolicyService`, `VramMonitorService` (ลบ `ExecutionProfileGuard` ออก)
|
||||
|
||||
**Checkpoint**: Foundation ready — policy services, guard, and updated DTO available
|
||||
**Checkpoint**: Foundation ready — delta SQL applied, policy services + updated DTO available
|
||||
|
||||
---
|
||||
|
||||
@@ -49,13 +53,13 @@
|
||||
|
||||
### 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
|
||||
- [x] T010 [US1] แก้ `backend/src/modules/ai/ai.service.ts` — inject `AiPolicyService`, กำหนด `effectiveProfile` อัตโนมัติจาก `job.type`, บันทึก `effectiveProfile` + `modelUsed` + `snapshotParams` ลง `ai_audit_logs` (FR-A08, FR-A09) — ไม่มี `requestedProfile` แล้ว
|
||||
- [x] 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)
|
||||
- [x] T012 [P] [US1] แก้ `backend/src/modules/ai/ai.controller.ts` — validate forbidden fields (`model.*`, `executionProfile`, `temperature` ฯลฯ) ใน pipe — ไม่ต้อง guard แล้ว เพราะ DTO ทำไว้แล้ว
|
||||
- [x] T013 [P] [US1] แก้ `frontend/types/ai.ts` — เอา `model` field ออก, เพิ่ม `executionProfile?: ExecutionProfile`, เพิ่ม `modelUsed?: string`
|
||||
- [x] T014 [US1] แก้ `frontend/lib/services/admin-ai.service.ts` — update request/response types ให้สอดคล้องกับ DTO ใหม่
|
||||
- [x] T015 [P] [US1] แก้ `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` — แสดง `np-dms-ai` / `np-dms-ocr` แทนชื่อ runtime ใน result cards และ model info
|
||||
- [x] 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
|
||||
|
||||
@@ -69,10 +73,10 @@
|
||||
|
||||
### 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
|
||||
- [x] T017 [US2] แก้ `backend/src/modules/ai/services/ocr.service.ts` — inject `VramMonitorService` และ `AiPolicyService`, เพิ่ม `calculateOcrResidency()` method, ส่ง `keep_alive` ที่คำนวณได้ไปใน OCR sidecar request, log `OcrResidencyDecision`
|
||||
- [x] 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
|
||||
- [x] T019 [P] [US2] เพิ่ม env variables ใน docker-compose ของ Desk-5439 OCR sidecar — `VRAM_HEADROOM_THRESHOLD_MB`, `OCR_RESIDENCY_WINDOW_SECONDS`, `GPU_TOTAL_VRAM_MB`
|
||||
- [x] 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
|
||||
|
||||
@@ -86,10 +90,10 @@
|
||||
|
||||
### 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`
|
||||
- [x] 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`
|
||||
- [x] 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)
|
||||
- [x] T023 [US3] แก้ `backend/src/modules/ai/processors/ai-batch.processor.ts` — รอง handle กรณีที่ `/embed` หรือ `/rerank` ตอบ `device: "cpu"` ใน response; log `retrievalDevice` ลง ai_audit_logs metadata
|
||||
- [x] 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
|
||||
|
||||
@@ -103,10 +107,10 @@
|
||||
|
||||
### 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
|
||||
- [x] T025 [US4] แก้ `backend/src/config/bullmq.config.ts` — เพิ่ม `REALTIME_CONCURRENCY` env variable (default: 2); ปรับ `ai-realtime` worker concurrency ให้ configurable
|
||||
- [x] 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
|
||||
- [x] T027 [P] [US4] ตรวจสอบ `backend/src/modules/ai/ai.service.ts` — ยืนยันว่า `rag-query` ถูก dispatch ไป `ai-batch` เสมอ (ไม่ใช่ `ai-realtime`); เพิ่ม explicit assertion ใน dispatch logic
|
||||
- [x] 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
|
||||
|
||||
@@ -120,12 +124,12 @@
|
||||
|
||||
### 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
|
||||
- [x] T029 [US5] สร้าง `backend/src/modules/ai/tests/ai-policy.service.spec.ts` — unit tests ครอบ: `job.type` → `effectiveProfile` mapping ทุก job type, canonical name mapping, forbidden fields rejection (400), audit log มี `effectiveProfile` + `modelUsed` และไม่มี `requestedProfile` (FR-A08)
|
||||
- [x] T030 [US5] ~~ExecutionProfileGuard tests — skip~~ แทนที่: เพิ่ม integration test สำหรับ forbidden fields validation ใน `ai.controller.spec.ts` — ตรวจว่า `model.*` และ `executionProfile` ใน payload → 400
|
||||
- [x] 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 1–4, ตรวจ 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`
|
||||
- [x] 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`, `GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB`, `REALTIME_CONCURRENCY`
|
||||
- [x] T034 [P] [US5] อัปเดต `backend/.env.example` — เพิ่ม `AI_VRAM_HEADROOM_THRESHOLD_MB`, `AI_GPU_MAIN_MODEL_PRESSURE_THRESHOLD_MB`, `AI_OCR_RESIDENCY_WINDOW_SECONDS`, `AI_REALTIME_CONCURRENCY`
|
||||
|
||||
**Checkpoint**: All 5 user stories complete — big bang cutover gate ready for validation
|
||||
|
||||
@@ -133,11 +137,11 @@
|
||||
|
||||
## 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
|
||||
- [x] 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) — verified: engineUsed ค่า canonical แล้ว (`typhoon-np-dms-ocr`, `tesseract`, `fast-path`); frontend badge แสดง `np-dms-ocr` ถูกต้อง
|
||||
- [x] T035 [P] ตรวจสอบ i18n keys ที่ต้องเพิ่มใน `frontend/public/locales/` สำหรับ error messages ใหม่ (400 model.key, 403 large-context, 504 CPU timeout) — เพิ่ม `ai_runtime_policy` namespace ใน en/ai.json และ th/ai.json
|
||||
- [x] T036 อัปเดต CONTEXT.md และ AGENTS.md — เพิ่ม `np-dms-ai` / `np-dms-ocr` เป็น canonical identity ใน System readiness summary; เพิ่ม ADR-034 ใน ADRs table
|
||||
- [x] T037 [P] ตรวจสอบ ADR-034 references ทั้งหมดใน codebase ด้วย search — ไม่พบ `typhoon*:latest` ใน user-facing surfaces (frontend TS/TSX); พบใน ops internals (ollama.service.ts, ai-settings.service.ts, test files) ซึ่งถูกต้องตามนโยบาย
|
||||
- [x] T038 รัน `pnpm lint` และ `pnpm type-check` สำหรับ backend และ frontend — แก้ทุก error ก่อน cutover — ESLint + tsc --noEmit ผ่านครบ ไม่มี error
|
||||
|
||||
---
|
||||
|
||||
@@ -146,7 +150,7 @@
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: ไม่มี dependency — เริ่มได้ทันที
|
||||
- **Foundational (Phase 2)**: ต้องรอ Phase 1 (T001, T002) — BLOCKS ทุก user story
|
||||
- **Foundational (Phase 2)**: ต้องรอ Phase 1 (T001, T002) — BLOCKS ทุก user story; **T040/T041 (delta SQL) MUST apply ก่อน** T005 และ T010
|
||||
- **US1 (Phase 3)**: ต้องรอ Phase 2 complete — สำคัญสุด, ทำก่อน
|
||||
- **US2 (Phase 4)**: ต้องรอ Phase 2 complete — ขึ้นกับ `VramMonitorService` จาก T003
|
||||
- **US3 (Phase 5)**: ต้องรอ Phase 2 complete — ขึ้นกับ `vram_monitor.py` จาก T004
|
||||
@@ -166,7 +170,8 @@
|
||||
|
||||
- T001 + T002: parallel (different files)
|
||||
- T003 + T004: parallel (different stacks)
|
||||
- T005, T006, T007: T005 ทำก่อน (T006, T007 ขึ้นกับ types จาก T005)
|
||||
- T040 + T041: parallel (different tables) — ต้องรอ Phase 1 และ MUST apply ก่อน T005/T010
|
||||
- T005, T006, T007: T005 ทำก่อน (T006, T007 ขึ้นกับ types จาก T005); T040 ต้อง complete ก่อน T005
|
||||
- US1 + US2 + US3 + US4: parallel หลัง Phase 2 complete (ถ้ามีทีม)
|
||||
- T029, T030, T031, T033, T034: parallel (different test files / env files)
|
||||
|
||||
@@ -193,12 +198,12 @@
|
||||
|
||||
### Total Task Count
|
||||
|
||||
- **Total**: 39 tasks
|
||||
- **Total**: 41 tasks
|
||||
- **US1**: 7 tasks (T010–T016)
|
||||
- **US2**: 4 tasks (T017–T020)
|
||||
- **US3**: 4 tasks (T021–T024)
|
||||
- **US4**: 4 tasks (T025–T028)
|
||||
- **US5**: 6 tasks (T029–T034)
|
||||
- **Setup**: 4 tasks (T001–T004)
|
||||
- **Foundational**: 5 tasks (T005–T009)
|
||||
- **Foundational**: 7 tasks (T040, T041, T005–T009)
|
||||
- **Polish**: 4 tasks (T035–T038)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/validation-report.md
|
||||
// Change Log:
|
||||
// - 2026-06-11: Initial validation report for feature 235
|
||||
|
||||
# Validation Report: AI Runtime Policy Refactor
|
||||
|
||||
**Date**: 2026-06-11
|
||||
**Feature**: `235-ai-runtime-policy-refactor`
|
||||
**Status**: PARTIAL
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| Metric | Count | Percentage |
|
||||
| --- | ---: | ---: |
|
||||
| Requirements Covered | 22/25 | 88% |
|
||||
| Acceptance Criteria Met | 14/19 | 74% |
|
||||
| Edge Cases Handled | 6/7 | 86% |
|
||||
| Tests Present | 18/25 | 72% |
|
||||
|
||||
## What Was Validated
|
||||
|
||||
- Workstream A evidence found in backend DTO/service/response contract and tests:
|
||||
[create-ai-job.dto.ts](./backend/src/modules/ai/dto/create-ai-job.dto.ts),
|
||||
[ai-job-response.dto.ts](./backend/src/modules/ai/dto/ai-job-response.dto.ts),
|
||||
[ai.service.ts](./backend/src/modules/ai/ai.service.ts),
|
||||
[ai.controller.spec.ts](./backend/src/modules/ai/tests/ai.controller.spec.ts),
|
||||
[ai-policy.service.spec.ts](./backend/src/modules/ai/tests/ai-policy.service.spec.ts),
|
||||
[ai.service.spec.ts](./backend/src/modules/ai/ai.service.spec.ts)
|
||||
- Workstream B evidence found in:
|
||||
[ocr.service.ts](./backend/src/modules/ai/services/ocr.service.ts),
|
||||
[vram-monitor.service.ts](./backend/src/modules/ai/services/vram-monitor.service.ts),
|
||||
[ocr-residency.spec.ts](./backend/src/modules/ai/tests/ocr-residency.spec.ts),
|
||||
[vram-monitor.service.spec.ts](./backend/src/modules/ai/tests/vram-monitor.service.spec.ts),
|
||||
[residency_policy.py](./specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py)
|
||||
- Workstream C evidence found in:
|
||||
[app.py](./specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py),
|
||||
[ai-batch.processor.ts](./backend/src/modules/ai/processors/ai-batch.processor.ts),
|
||||
[test_retrieval_fallback.py](./specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/tests/test_retrieval_fallback.py)
|
||||
- Workstream D evidence found in:
|
||||
[bullmq.config.ts](./backend/src/config/bullmq.config.ts),
|
||||
[ai-realtime.processor.ts](./backend/src/modules/ai/processors/ai-realtime.processor.ts),
|
||||
[queue-policy.spec.ts](./backend/src/modules/ai/tests/queue-policy.spec.ts)
|
||||
- User-facing canonical naming evidence found in:
|
||||
[page.tsx](./frontend/app/(admin)/admin/ai/page.tsx),
|
||||
[OcrSandboxPromptManager.tsx](./frontend/components/admin/ai/OcrSandboxPromptManager.tsx),
|
||||
[admin-ai.service.ts](./frontend/lib/services/admin-ai.service.ts)
|
||||
|
||||
## Requirement Matrix
|
||||
|
||||
| Requirement | Status | Evidence | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| FR-A01 | Covered | DTO forbidden fields + controller integration tests | HTTP 400 path implemented |
|
||||
| FR-A02 | Partial | DTO still accepts `payload` and `projectPublicId` | Spec text conflicts with rag-query/query + tenant isolation contract |
|
||||
| FR-A03 | Covered | `AiPolicyService.getProfileForJobType()` + `AiService.submitUnifiedJob()` | Backend assigns profile from job type |
|
||||
| FR-A04 | Covered | Admin Console + OCR Sandbox UI | Visibility exists in UI; enforcement is by contract removal, not separate guard |
|
||||
| FR-A05 | Covered | `AiPolicyService.createJobPayload()` | Mapping includes profile, canonical model, snapshot params |
|
||||
| FR-A06 | Covered | deterministic switch in `getProfileForJobType()` | No unmapped internal job type found |
|
||||
| FR-A07 | Covered | backend DTOs, frontend normalization, sandbox badge mapping | Canonical labels present across layers inspected |
|
||||
| FR-A08 | Covered | worker audit writes `effectiveProfile`, `canonicalModel`, `snapshotParamsJson` | enqueue-time false success log removed |
|
||||
| FR-A09 | Covered | `createJobPayload()` snapshot + worker uses payload snapshot | Predictable per-dispatch parameters |
|
||||
| FR-B01 | Covered | `AiPolicyService` default policy map + DB/cache lookup | Runtime policy layer exists |
|
||||
| FR-B02 | Covered | `OcrService.calculateOcrResidency()` | Dynamic keep_alive decision implemented |
|
||||
| FR-B03 | Covered | deep-analysis/high-pressure branches + residency tests | Safe OCR unload path exists |
|
||||
| FR-B04 | Covered | residency window branch + tests | Positive keep_alive path exists |
|
||||
| FR-B05 | Covered | VRAM query failure fallback + tests | Safe default `keep_alive=0` exists |
|
||||
| FR-B06 | Covered | `OcrService` logs decision context | Log behavior implemented, not live-verified |
|
||||
| FR-C01 | Covered | `/embed` headroom check + CPU fallback | Sidecar code present |
|
||||
| FR-C02 | Covered | `/rerank` headroom check + CPU fallback | Sidecar code present |
|
||||
| FR-C03 | Covered | `/embed` + `/rerank` timeout -> HTTP 504 | No partial result path found |
|
||||
| FR-C04 | Covered | device/reason logging in sidecar | Log behavior implemented |
|
||||
| FR-C05 | Partial | `rag-query` backend path exists | No executed integration/manual proof that fallback path completes end-to-end |
|
||||
| FR-C06 | Covered | env threshold usage + safe default in VRAM query failure | Configurable threshold present |
|
||||
| FR-D01 | Partial | config default=2 + processor logic + unit tests | No live worker concurrency proof beyond unit tests |
|
||||
| FR-D02 | Covered | lightweight job classification list | Matches spec set |
|
||||
| FR-D03 | Covered | `AiService.submitUnifiedJob()` + realtime redirect tests | `rag-query` stays in `ai-batch` |
|
||||
| FR-D04 | Covered | active-job counter + queue policy tests | Resume now waits for all realtime jobs |
|
||||
|
||||
## Acceptance Criteria Gaps
|
||||
|
||||
| Scenario | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| US1-3 Admin Console shows canonical names only | Partial | Code supports it, but no manual browser validation recorded |
|
||||
| US1-5 OCR Sandbox reveals effective profile/modelUsed | Partial | UI/service evidence exists, but no executed sandbox validation record |
|
||||
| US2-4 OCR logs residency decision with headroom | Partial | Logging code exists; no captured runtime log artifact |
|
||||
| US3-4 RAG still answers under CPU fallback | Partial | Code path exists; no completed end-to-end run |
|
||||
| US5-1 executable cutover gate | Partial | backend targeted tests passed, but sidecar pytest was not executed in this validation pass |
|
||||
| US5-2 Admin Console labels manual check | Missing | T032 still unchecked |
|
||||
| US5-3 OCR Sandbox behavior across headroom scenarios | Missing | T032 still unchecked |
|
||||
|
||||
## Edge Case Review
|
||||
|
||||
| Edge Case | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| VRAM query failure -> `keep_alive: 0` | Handled | explicit safe default in backend + sidecar |
|
||||
| caller sends forbidden profile/model fields | Handled | DTO/controller tests cover this |
|
||||
| admin-only large-context when VRAM insufficient | Partial | spec branch is stale after contract removal; no current caller path exists |
|
||||
| OCR job races with main model generation | Handled | high-pressure/deep-analysis path forces unload |
|
||||
| CPU fallback timeout must fail clearly | Handled | 504 implemented |
|
||||
| Ollama `/api/ps` schema drift after cutover | Handled | safe default `available=0` path exists |
|
||||
| headroom snapshot/request race acceptable | Handled | implementation follows spec assumption; no stronger synchronization introduced |
|
||||
|
||||
## Success Criteria Notes
|
||||
|
||||
| Success Criterion | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| SC-001 | Likely Met | automated rejection tests exist |
|
||||
| SC-002 | Partial | code normalization exists; no full manual surface sweep attached |
|
||||
| SC-003 | Not Validated | no latency measurement artifact |
|
||||
| SC-004 | Partial | fallback code exists; no executed end-to-end proof |
|
||||
| SC-005 | Partial | backend tests executed, sidecar pytest/manual cutover not completed |
|
||||
| SC-006 | Partial | concurrency config + unit tests exist, no throughput measurement |
|
||||
|
||||
## Key Findings
|
||||
|
||||
1. Implementation is broadly aligned with the runtime-policy refactor design, especially on policy mapping, canonical naming, adaptive OCR residency, retrieval CPU fallback, and queue pause/resume correctness.
|
||||
2. Validation cannot be promoted to `PASS` yet because the feature still lacks the manual Gate 1–4 evidence from [quickstart.md](./quickstart.md) and this pass did not execute the Python sidecar pytest suite.
|
||||
3. The spec artifact set contains one material inconsistency: FR-A02 says `CreateAiJobDto` should only expose `type`, `documentPublicId`, and `attachmentPublicId`, but the same spec and implemented contract require `payload.query` and `projectPublicId` for `rag-query`. The code follows the richer contract, not the literal FR-A02 text.
|
||||
4. [quickstart.md](./quickstart.md) is stale against the implemented Option B contract in at least Gate 1C, 1D, and 4A because it still sends `executionProfile` / `large-context` style caller input that the new DTO now forbids.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. Complete T032 by running the manual Gate 1–4 flow on a real backend + OCR sidecar environment and append the captured results to this feature folder.
|
||||
2. Run `pytest specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/tests -v` once the sidecar environment is ready, then update this report with the result.
|
||||
3. Reconcile FR-A02 and `quickstart.md` with the actual Option B contract so the validation target and operator guide no longer contradict the implementation.
|
||||
4. Add one end-to-end proof for FR-C05/SC-004: force GPU pressure, submit `rag-query`, and capture both successful response and sidecar `device=cpu` log.
|
||||
5. Add one concurrency-focused execution proof for FR-D01/SC-006 if the team wants `PASS` to include runtime throughput evidence rather than unit-level proof only.
|
||||
@@ -16,4 +16,5 @@
|
||||
| 2026-06-05 | v1.9.8 | RAG Pipeline Enhancements (Spec 234 / ADR-035) — BGE-M3 + BGE-Reranker + Hybrid Qdrant (Session 14/15) | ✅ Complete |
|
||||
| 2026-06-06 | v1.9.9 | LLM JSON Parse Failure & VRAM Fix (ADR-035-135) — retry logic + keep_alive=0 + ESLint heap fix | ✅ Complete |
|
||||
| 2026-06-08 | v1.9.10 | LLM JSON Response Truncation Fix — ขยาย num_ctx: 16384 (Session 16 โดย AGY Gemini 3.5 Flash (Medium)) | ✅ Complete |
|
||||
|
||||
| 2026-06-11 | v1.9.10 | AI Runtime Policy Refactor (Feature-235) — Canonical names (`np-dms-ai`/`np-dms-ocr`), Adaptive OCR Residency, CPU Fallback Retrieval, Queue Policy (ai-realtime concurrency=2) — targeted verification 27/27 tests ✅ ESLint + tsc clean | ⏳ Pending T032 Manual Gate + Merge |
|
||||
| 2026-06-11 | v1.9.10 | Feature-235 validation follow-up — validation-report.md = PARTIAL, cutover-validation checklist added, targeted verification 27/27 | ⏳ Pending T032 execution |
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
- Reorganize โครงสร้างโฟลเดอร์ `specs/` สำเร็จ (`100-Infrastructures`, `200-fullstacks`, `300-others`)
|
||||
- อัปเดตกฎ `AGENTS.md` และ `GEMINI.md` ให้ตรงกับมาตรฐานใหม่
|
||||
- ริเริ่มระบบ `memory/agent-memory.md`
|
||||
- ริเริ่มระบบ `memory/project-memory-override.md`
|
||||
|
||||
## ไฟล์ที่แก้ไข
|
||||
|
||||
- `specs/` folder structure reorganization
|
||||
- `AGENTS.md` update
|
||||
- `GEMINI.md` update
|
||||
- `memory/agent-memory.md` initial creation
|
||||
- `memory/project-memory-override.md` initial creation
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Session 17 — 2026-06-11 (AI Runtime Policy Refactor — Feature-235)
|
||||
|
||||
## Summary
|
||||
|
||||
Implement Feature-235 AI Runtime Policy Refactor ตาม spec.md และ plan.md บน branch `235-ai-runtime-policy-refactor` — เปลี่ยน API contract ให้ caller ส่ง job type เท่านั้น (ไม่มี `model.key` / parameter overrides), เพิ่ม backend policy mapping layer (`AiPolicyService`), adaptive OCR residency, CPU fallback retrieval, และ BullMQ queue policy — จบด้วย test suite 23/23 ผ่านครบ, ESLint + tsc clean.
|
||||
|
||||
## ปัญหาที่พบ (Root Cause)
|
||||
|
||||
| ปัญหา | สาเหตุ | การแก้ไข |
|
||||
|---|---|---|
|
||||
| `VramStatus` / `getVramStatus()` / `invalidateCache()` หาย | refactor ก่อนหน้าลบออก แต่ controller ยังใช้ | Restore เมธอดใน `vram-monitor.service.ts` |
|
||||
| TS2367 ใน `ai-policy.service.ts` | compare `ExecutionProfile` กับ `'ocr-extract'` ผิด type | แก้ compare เป็น `'np-dms-ai'` |
|
||||
| TS1272 `import type` ใน DTO | import ประกอบ class ด้วย `import type` ไม่ได้ | เปลี่ยนเป็น regular import |
|
||||
| `any` types ใน `ai-batch.processor.ts` | `snapshotParams` / `effectiveProfile` ไม่มี typed | กำหนด interface `AiBatchJobData` runtime metadata |
|
||||
| NestJS DI error ใน `ai.controller.spec.ts` | ขาด mock `'default_IORedisModuleConnectionToken'` | เพิ่ม mock provider ใน test module providers |
|
||||
|
||||
## การแก้ไข (Fix)
|
||||
|
||||
| ไฟล์ | การเปลี่ยนแปลง |
|
||||
|---|---|
|
||||
| `backend/src/modules/ai/services/vram-monitor.service.ts` | Restore `VramStatus`, `getVramStatus()`, `invalidateCache()` |
|
||||
| `backend/src/modules/ai/services/ai-policy.service.ts` | แก้ TS2367 type comparison; เพิ่ม `getProfileForJobType()`, `createJobPayload()` |
|
||||
| `backend/src/modules/ai/interfaces/execution-policy.interface.ts` | สร้างใหม่ — `ExecutionProfile`, `RuntimePolicy`, `AiJobPayload`, `VramHeadroom` |
|
||||
| `backend/src/modules/ai/interfaces/ocr-residency.interface.ts` | สร้างใหม่ — `OcrResidencyDecision` |
|
||||
| `backend/src/modules/ai/dto/create-ai-job.dto.ts` | ลบ `model.key`, `executionProfile`, `temperature`, `top_p`, `maxTokens`; เพิ่ม forbidden field validators |
|
||||
| `backend/src/modules/ai/dto/ai-job-response.dto.ts` | เพิ่ม `modelUsed`, `effectiveProfile` fields |
|
||||
| `backend/src/modules/ai/ai.service.ts` | inject `AiPolicyService`; กำหนด `effectiveProfile` จาก job type อัตโนมัติ |
|
||||
| `backend/src/modules/ai/processors/ai-realtime.processor.ts` | เพิ่ม lightweight job classification; redirect heavy jobs ไป ai-batch |
|
||||
| `backend/src/modules/ai/processors/ai-batch.processor.ts` | type-safe runtime policy metadata; log `retrievalDevice`; canonical `ocrUsed` |
|
||||
| `backend/src/modules/ai/services/ocr.service.ts` | inject `VramMonitorService`; `calculateOcrResidency()` dynamic keep_alive |
|
||||
| `backend/src/config/bullmq.config.ts` | เพิ่ม `REALTIME_CONCURRENCY` env (default 2) |
|
||||
| `backend/src/modules/ai/ai.module.ts` | register `AiPolicyService`, `VramMonitorService` |
|
||||
| `backend/src/modules/ai/guards/execution-profile.guard.ts` | สร้างใหม่ (สำรองไว้; ไม่ใช้ใน option B) |
|
||||
| `backend/src/modules/ai/tests/ai-policy.service.spec.ts` | สร้างใหม่ — 7 tests ผ่าน |
|
||||
| `backend/src/modules/ai/tests/ocr-residency.spec.ts` | สร้างใหม่ — 5 tests ผ่าน |
|
||||
| `backend/src/modules/ai/tests/queue-policy.spec.ts` | สร้างใหม่ — 2 tests ผ่าน |
|
||||
| `backend/src/modules/ai/tests/vram-monitor.service.spec.ts` | สร้างใหม่ — 5 tests ผ่าน |
|
||||
| `backend/src/modules/ai/tests/ai.controller.spec.ts` | สร้างใหม่ — 4 integration tests ผ่าน; เพิ่ม Redis mock |
|
||||
| `frontend/types/ai.ts` | ลบ `model` field; เพิ่ม `executionProfile?`, `modelUsed?` |
|
||||
| `frontend/lib/services/admin-ai.service.ts` | อัปเดต types ตาม DTO ใหม่ |
|
||||
| `frontend/components/admin/ai/OcrSandboxPromptManager.tsx` | แสดง `np-dms-ai` / `np-dms-ocr` แทน runtime names |
|
||||
| `frontend/app/(admin)/admin/ai/page.tsx` | แสดง canonical names ใน System Health panel |
|
||||
| `frontend/public/locales/en/ai.json` | เพิ่ม `ai_runtime_policy` namespace |
|
||||
| `frontend/public/locales/th/ai.json` | เพิ่ม `ai_runtime_policy` namespace |
|
||||
| `backend/.env.example` | เพิ่ม `AI_OCR_RESIDENCY_WINDOW_SECONDS` |
|
||||
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/.env.template` | สร้างใหม่ — VRAM + residency + concurrency vars |
|
||||
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/app.py` | adaptive `keep_alive` param; CPU fallback บน `/embed` + `/rerank` |
|
||||
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/vram_monitor.py` | สร้างใหม่ — query Ollama `/api/ps` |
|
||||
| `specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar/services/residency_policy.py` | สร้างใหม่ — keep_alive calculation |
|
||||
| `CONTEXT.md` | เพิ่ม Feature-235 ใน System Readiness + ADR-034 ใน ADRs table |
|
||||
|
||||
## กฎที่ Lock แล้ว
|
||||
|
||||
- **Option B (Policy-Only)**: Caller ไม่มี `executionProfile` field ใน `CreateAiJobDto` — backend กำหนด profile จาก `job.type` เท่านั้น (ไม่รับ caller input)
|
||||
- **Canonical Model Identity**: `np-dms-ai` (LLM) / `np-dms-ocr` (OCR) ทุก layer ที่ผู้ใช้เห็น — ชื่อ runtime (`typhoon*`) ใช้เฉพาะ ops internals
|
||||
- **Redis mock token**: ทุก test ที่ bootstrap `AiController` ต้องเพิ่ม `'default_IORedisModuleConnectionToken'` ใน providers
|
||||
- **Lightweight Realtime Jobs**: เฉพาะ `intent-classify`, `tool-suggest` — ห้าม `rag-query` อยู่ใน ai-realtime
|
||||
|
||||
## Verification
|
||||
|
||||
- [x] `npx jest src/modules/ai/tests/` — 23/23 tests ผ่าน (5 suites)
|
||||
- [x] `npx tsc --noEmit` — ไม่มี error
|
||||
- [x] `npx eslint src/modules/ai/ --max-warnings=0` — ไม่มี warning
|
||||
- [ ] T032: Manual validation Gate 1–4 ตาม `quickstart.md` (ต้องรันบน environment จริง)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Session 18 — 2026-06-11 (Feature-235 Validation & Memory Save)
|
||||
|
||||
## Summary
|
||||
|
||||
สรุปผล validation ของ Feature-235, บันทึกรายงาน `validation-report.md`, และสร้าง cutover checklist สำหรับปิด T032 / sidecar pytest โดยยึด contract ปัจจุบันของ `/api/ai/jobs` ที่เป็น Option B.
|
||||
|
||||
## ปัญหาที่พบ (Root Cause)
|
||||
|
||||
| ปัญหา | สาเหตุ | การแก้ไข |
|
||||
|---|---|---|
|
||||
| Validation ยังไม่ขึ้น `PASS` | ยังขาด manual Gate 1–4 และ sidecar pytest ใน environment จริง | สร้าง `checklists/cutover-validation.md` เพื่อใช้ปิดงานอย่างเป็นระบบ |
|
||||
| `quickstart.md` เดิมไม่สอดคล้องกับ contract ปัจจุบัน | ตัวอย่างเก่ายังส่ง `executionProfile` / `large-context` จาก caller | เก็บ evidence ใน validation report และทำ checklist ใหม่ตาม implementation ปัจจุบัน |
|
||||
| Project memory ยังสะท้อน test count เก่า | รอบ verification ล่าสุดได้ targeted tests 27/27 แล้ว | อัปเดต `memory/project-memory-override.md` ให้ตรงกับสถานะล่าสุด |
|
||||
|
||||
## การแก้ไข (Fix)
|
||||
|
||||
| ไฟล์ | การเปลี่ยนแปลง |
|
||||
|---|---|
|
||||
| `specs/200-fullstacks/235-ai-runtime-policy-refactor/validation-report.md` | บันทึกผล validation เป็น `PARTIAL` พร้อม requirement matrix, gaps, และ recommendations |
|
||||
| `specs/200-fullstacks/235-ai-runtime-policy-refactor/checklists/cutover-validation.md` | สร้าง runbook สำหรับ T032, backend tests, backend build, และ sidecar pytest |
|
||||
| `specs/88-logs/rollouts.md` | เพิ่ม entry สำหรับ validation follow-up ของ Feature-235 |
|
||||
| `memory/project-memory-override.md` | อัปเดตสถานะ Feature-235, test count ล่าสุด, และชี้ไปยัง cutover checklist |
|
||||
|
||||
## กฎที่ Lock แล้ว
|
||||
|
||||
- ใช้ `checklists/cutover-validation.md` เป็น runbook หลักสำหรับปิด T032
|
||||
- Validation target ของ `/api/ai/jobs` ต้องยึด Option B ปัจจุบัน ไม่ใช้ caller-driven `executionProfile`
|
||||
- ถ้าต้องบันทึกผล verification ต่อ ให้แนบ evidence จริงจาก backend / sidecar environment
|
||||
|
||||
## Verification
|
||||
|
||||
- [x] `pnpm --filter backend test -- --runInBand --testPathPatterns="ai.service.spec.ts|queue-policy.spec.ts|ai.controller.spec.ts"` = 27/27 ผ่าน
|
||||
- [x] `pnpm --filter backend build` = ผ่าน
|
||||
- [x] `validation-report.md` ถูกสร้างและเก็บผลว่า `PARTIAL`
|
||||
- [x] `cutover-validation.md` ถูกสร้างเพื่อใช้ปิด T032
|
||||
Reference in New Issue
Block a user