Files
lcbp3/specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md
T
admin 190b9a3af5
CI / CD Pipeline / build (push) Successful in 4m7s
CI / CD Pipeline / deploy (push) Successful in 6m24s
690612:1407 ADR-035-235 #01
2026-06-12 14:07:14 +07:00

9.4 KiB

// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/quickstart.md // Change Log: // - 2026-06-11: Verification quickstart for AI Runtime Policy Refactor // - 2026-06-12: เพิ่ม PowerShell syntax และ environment variable setup

Quickstart: AI Runtime Policy Refactor — Verification Guide

Prerequisites

  • Backend running (pnpm run start:dev in backend/)
  • OCR sidecar running on Desk-5439 (docker compose up in ocr-sidecar/)
  • Ollama running with np-dms-ai and np-dms-ocr tags registered
  • Admin user token available

Environment Setup

การเข้าถึง Backend (สำคัญ)

จาก @/specs/04-Infrastructure-OPS/04-00-docker-compose/QNAP/app/docker-compose-app.yml:

Environment Backend URL ใช้เมื่อ
Production (QNAP + NPM) https://backend.np-dms.work/api ทดสอบจากเครื่องภายนอก (WSL, บ้าน)
QNAP Internal http://backend:3000 ทดสอบจากภายใน QNAP (docker network)
Local dev http://localhost:3001 รัน backend บนเครื่องตัวเอง

หมายเหตุ: Backend container ใช้ port 3000 (ไม่ใช่ 3001) และอยู่ behind nginx proxy manager

Bash (Linux/macOS/Git Bash on Windows)

# สำหรับ Production QNAP (ผ่าน HTTPS + NPM)
export BACKEND_URL="https://backend.np-dms.work/api"

# หรือถ้า SSH tunnel ไป QNAP แล้ว
# export BACKEND_URL="http://localhost:3000"

export TOKEN="your-jwt-token-here"

PowerShell (Windows)

# สำหรับ Production QNAP (ผ่าน HTTPS + NPM)
$env:BACKEND_URL = "https://backend.np-dms.work/api"

# หรือถ้า SSH tunnel ไป QNAP แล้ว
# $env:BACKEND_URL = "http://localhost:3000"

$env:TOKEN = "your-jwt-token-here"

วิธีหา TOKEN

วิธีที่ 1: Login ผ่าน API (Bash)

# Login แล้วดึง token จาก response
# หมายเหตุ: Backend ใช้ 'username' (ไม่ใช่ email) ใน login field
RESPONSE=$(curl -s -X POST "$BACKEND_URL/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "Center2025"}')

# วิธีดึง TOKEN (เลือก 1 จาก 3):

# วิธี 1: ใช้ jq (ถ้าติดตั้งแล้ว)
# export TOKEN=$(echo $RESPONSE | jq -r '.access_token')

# วิธี 2: ใช้ Python (ทั่วไปมีอยู่แล้ว) — แนะนำ
export TOKEN=$(echo $RESPONSE | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['access_token'])")

# วิธี 3: ดู response แล้ว copy เอง (ถ้าไม่มีทั้ง jq และ Python)
# echo $RESPONSE
# export TOKEN="paste_token_here"

วิธีที่ 2: ดึงจาก Browser DevTools

  1. เปิด browser ไปที่ http://192.168.10.8:3000 (frontend)
  2. Login ด้วย account ที่มีสิทธิ์ admin
  3. กด F12 → Network tab
  4. รีเฟรชหน้า หรือ ทำ action ใดก็ได้
  5. ดู request ที่ส่งไป backend → Headers → Authorization: Bearer eyJhbG...
  6. Copy ค่าหลัง Bearer มาใส่ใน $TOKEN

วิธีที่ 3: ถ้ามี Access ตรงกับ Database

-- ดู username ที่มี role = 'admin' (หลังจากนั้นต้อง login ผ่าน API เพื่อเอา token)
SELECT username FROM users WHERE role = 'admin' LIMIT 1;

Default Users (จาก Seed Data)

ถ้าใช้ seed data เริ่มต้น มี users นี้ให้ใช้:

Username Role Password
superadmin Superadmin Center2025
admin Org Admin Center2025
editor01 Editor Center2025
viewer01 Viewer Center2025

Gate 1: Policy Contract Verification

1A. Reject model.key (should return 400)

Bash:

curl -X POST "$BACKEND_URL/ai/jobs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); e=d.get('error', {}); print(e.get('statusCode'), e.get('message'))"
# Expected: 400, message about model.key not allowed

PowerShell:

$body = '{"type": "rag-query", "model": {"key": "typhoon2.5-np-dms:latest"}}'
Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
    "Authorization" = "Bearer $env:TOKEN"
    "Content-Type" = "application/json"
} -Body $body | Select-Object statusCode, message
# Expected: 400, message about model.key not allowed

1B. Reject parameter overrides (should return 400)

Bash:

curl -X POST "$BACKEND_URL/ai/jobs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "rag-query", "temperature": 0.9}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
# Expected: 400

PowerShell:

$body = '{"type": "rag-query", "temperature": 0.9}'
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
    "Authorization" = "Bearer $env:TOKEN"
    "Content-Type" = "application/json"
} -Body $body).statusCode
# Expected: 400

1C. Valid executionProfile (should return 201)

Bash:

curl -X POST "$BACKEND_URL/ai/jobs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid-here>"}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('modelUsed'))"
# Expected: "np-dms-ai"

PowerShell:

$body = '{"type": "rag-query", "executionProfile": "balanced", "documentPublicId": "<uuid-here>"}'
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
    "Authorization" = "Bearer $env:TOKEN"
    "Content-Type" = "application/json"
} -Body $body).data.modelUsed
# Expected: "np-dms-ai"

1D. large-context by non-admin (should return 403)

Bash:

curl -X POST "$BACKEND_URL/ai/jobs" \
  -H "Authorization: Bearer $NON_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "rag-query", "executionProfile": "large-context"}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
# Expected: 403

PowerShell:

$body = '{"type": "rag-query", "executionProfile": "large-context"}'
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
    "Authorization" = "Bearer $env:NON_ADMIN_TOKEN"
    "Content-Type" = "application/json"
} -Body $body).statusCode
# Expected: 403

Gate 2: Canonical Naming Verification

2A. Check audit log after job

SELECT metadata->>'$.modelUsed' FROM ai_audit_logs ORDER BY created_at DESC LIMIT 1;
-- Expected: "np-dms-ai" (ไม่ใช่ "typhoon2.5-np-dms:latest")

2B. Check Admin Console (Manual)

  1. เปิด /admin/ai ใน browser
  2. ตรวจว่า model labels ทั้งหมดแสดง np-dms-ai และ np-dms-ocr
  3. ตรวจว่าไม่มี typhoon* ปรากฏใน UI

Gate 3: Adaptive OCR Residency Verification

3A. OCR under large-context profile

# ส่ง OCR job ขณะที่มี large-context job active
# ดู sidecar log
docker logs ocr-sidecar --tail 20
# Expected log line: keep_alive=0 reason=large-context-active

3B. OCR with headroom sufficient

# ส่ง OCR job เมื่อ GPU headroom สูง (ไม่มี model loaded หนัก)
docker logs ocr-sidecar --tail 20
# Expected log line: keep_alive=120 reason=headroom-sufficient

Gate 4: Retrieval CPU Fallback Verification

4A. Force GPU pressure then run RAG

Step 1: Force load large model (Bash)

# ถ้า Ollama รันบน Desk-5439 (192.168.10.100)
curl http://192.168.10.100:11434/api/generate -d '{"model":"np-dms-ai","prompt":"warmup","keep_alive":-1}'

Step 2: Run RAG query

Bash:

curl -X POST "$BACKEND_URL/ai/jobs" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid>"}' \
  | jq '.data.status'
# Expected: "completed" (ไม่ fail)

PowerShell:

$body = '{"type":"rag-query","executionProfile":"balanced","documentPublicId":"<uuid-here>"}'
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/jobs" -Method POST -Headers @{
    "Authorization" = "Bearer $env:TOKEN"
} -Body $body).data.status
# Expected: "completed" (ไม่ fail)

Step 3: ตรวจ sidecar log

docker logs ocr-sidecar --tail 20
# Expected: device=cpu reason=gpu-headroom-below-threshold

Automated Test Suite

# Backend unit + integration tests
cd backend
pnpm test -- --testPathPattern="ai-policy|ocr-residency|execution-profile"

# Sidecar tests
cd specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/ocr-sidecar
pytest tests/ -v

All tests must pass before cutover gate is considered complete.