Files
lcbp3/specs/200-fullstacks/236-unified-ocr-architecture/quickstart.md
T
admin 7e8f4859cd
CI / CD Pipeline / build (push) Failing after 6m24s
CI / CD Pipeline / deploy (push) Has been skipped
feat(ai): add ADR-036 unified OCR architecture and frontend test coverage
- Add ADR-036 unified OCR architecture (typhoon-ocr via Ollama)
- Extend AI execution profiles for OCR sandbox configuration
- Add comprehensive frontend test coverage (components, hooks, services)
- Add backend test coverage for document-numbering services
- Update OCR sidecar with typhoon-ocr integration
- Add AI policy service and execution profile management
- Update AGENTS.md and architecture documentation
2026-06-14 06:34:07 +07:00

8.2 KiB

// File: specs/200-fullstacks/236-unified-ocr-architecture/quickstart.md // Change Log: // - 2026-06-13: Verification quickstart for Unified AI Model Architecture — Sandbox-Production Parity

Quickstart: Unified AI Model Architecture — Verification Guide

Prerequisites

  • Backend running (pnpm run start:dev in backend/)
  • Admin user token with system.manage_ai permission
  • SQL delta applied: specs/03-Data-and-Storage/deltas/2026-06-13-extend-ai-execution-profiles-ocr.sql
  • OCR sidecar running on Desk-5439 (for OCR-related tests)

Environment Setup

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

Bash

export BACKEND_URL="https://backend.np-dms.work/api"
export TOKEN="your-jwt-token-here"
export IDEMPOTENCY_KEY="test-$(date +%s)"

PowerShell

$env:BACKEND_URL = "https://backend.np-dms.work/api"
$env:TOKEN = "your-jwt-token-here"
$env:IDEMPOTENCY_KEY = "test-$(Get-Date -UFormat %s)"

Gate 1: Sandbox Parameter Testing (US1)

1A. Get sandbox draft (should auto-seed from production if absent)

Bash:

curl -s "$BACKEND_URL/ai/sandbox-profiles/standard" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import sys, json; d=json.load(sys.stdin)['data']; print(d.get('profileName'), d.get('temperature'))"
# Expected: "standard" 0.5 (or current production value)

PowerShell:

(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/sandbox-profiles/standard" -Headers @{
  "Authorization" = "Bearer $env:TOKEN"
}).data | Select-Object profileName, temperature
# Expected: profileName=standard, temperature=0.5

1B. Save sandbox draft (should not affect production)

Bash:

curl -s -X PUT "$BACKEND_URL/ai/sandbox-profiles/standard" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY-save" \
  -d '{"temperature": 0.8, "topP": 0.9, "repeatPenalty": 1.15, "keepAliveSeconds": 300}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('temperature'))"
# Expected: 0.8

1C. Verify production unchanged after sandbox save

curl -s "$BACKEND_URL/ai/profiles/standard" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import sys, json; d=json.load(sys.stdin)['data']; print('production temperature:', d.get('temperature'))"
# Expected: original production value (not 0.8)

1D. Reset sandbox to production values

Bash:

curl -s -X POST "$BACKEND_URL/ai/sandbox-profiles/standard/reset" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('temperature'))"
# Expected: original production temperature (e.g. 0.5)

Gate 2: Apply to Production (US2)

2A. Apply with valid Idempotency-Key (should succeed)

Bash:

APPLY_KEY="apply-standard-$(date +%s)"
curl -s -X POST "$BACKEND_URL/ai/profiles/standard/apply" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $APPLY_KEY" \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('appliedAt'), d.get('data', {}).get('cacheInvalidated'))"
# Expected: ISO timestamp, True

PowerShell:

$applyKey = "apply-standard-$(Get-Date -UFormat %s)"
(Invoke-RestMethod -Uri "$env:BACKEND_URL/ai/profiles/standard/apply" -Method POST -Headers @{
  "Authorization"  = "Bearer $env:TOKEN"
  "Content-Type"   = "application/json"
  "Idempotency-Key" = $applyKey
}).data | Select-Object appliedAt, cacheInvalidated
# Expected: appliedAt = ISO timestamp, cacheInvalidated = True

2B. Duplicate apply with same Idempotency-Key (should return cached result)

# Run same apply again with same key — should return 200 with cached result, not re-apply
curl -s -X POST "$BACKEND_URL/ai/profiles/standard/apply" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $APPLY_KEY" \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print('idempotent:', 'appliedAt' in d.get('data', {}))"
# Expected: idempotent: True

2C. Apply with invalid temperature (should return 400)

curl -s -X PUT "$BACKEND_URL/ai/sandbox-profiles/standard" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"temperature": 1.5, "topP": 0.9, "repeatPenalty": 1.1, "keepAliveSeconds": 300}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
# Expected: 400

2D. Verify audit log created

SELECT
  action, metadata->>'$.profileName', metadata->>'$.newValues',
  created_at
FROM ai_audit_logs
WHERE action = 'APPLY_PROFILE'
ORDER BY created_at DESC
LIMIT 1;
-- Expected: action='APPLY_PROFILE', profileName='standard', newValues with applied params

Gate 3: Dual-Model Parameter Management (US3)

3A. Get OCR sandbox profile (ocr-extract row)

curl -s "$BACKEND_URL/ai/sandbox-profiles/ocr-extract" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import sys, json; d=json.load(sys.stdin)['data']; print(d.get('canonicalModel'), d.get('numCtx'), d.get('maxTokens'))"
# Expected: "np-dms-ocr" None None  (numCtx/maxTokens null for OCR)

3B. Verify np-dms-ai and np-dms-ocr are independent

-- ตรวจสอบว่ามี 2 rows ที่แยกกันใน ai_execution_profiles
SELECT profile_name, canonical_model, temperature, num_ctx, max_tokens
FROM ai_execution_profiles
WHERE profile_name IN ('standard', 'ocr-extract');
-- Expected: standard → np-dms-ai (num_ctx populated), ocr-extract → np-dms-ocr (num_ctx NULL)

Gate 4: Master Data Context Parity (US4)

4A. Sandbox test requires project selection

# ส่ง sandbox test โดยไม่ระบุ projectPublicId — ควร return 400
curl -s -X POST "$BACKEND_URL/ai/sandbox/test" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filePublicId": "<uuid>"}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('error', {}).get('statusCode'))"
# Expected: 400

4B. Sandbox test with real project context

# สมมติว่ามี projectPublicId จริง
curl -s -X POST "$BACKEND_URL/ai/sandbox/test" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filePublicId": "<file-uuid>", "projectPublicId": "<project-uuid>"}' \
  | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('data', {}).get('status'))"
# Expected: "processing" or "completed"

Gate 5: System Prompt Integration (US5)

5A. Verify apply endpoint does not touch ai_prompts

-- Apply parameter → ตรวจว่า ai_prompts ไม่ถูกแตะต้อง
-- Run before apply:
SELECT updated_at FROM ai_prompts WHERE prompt_type = 'ocr_extraction' ORDER BY updated_at DESC LIMIT 1;
-- Apply parameters via API...
-- Run after apply — timestamp should be unchanged:
SELECT updated_at FROM ai_prompts WHERE prompt_type = 'ocr_extraction' ORDER BY updated_at DESC LIMIT 1;

Automated Test Suite

# Backend unit tests (sandbox + apply + dual-model)
cd backend
pnpm test -- --testPathPattern="ai-policy.service"

# Backend unit tests (processor dual-model snapshot)
pnpm test -- --testPathPattern="ai-batch.processor"

# Backend unit tests (OCR parameter wiring)
pnpm test -- --testPathPattern="ocr.service"

# Backend integration tests (apply flow end-to-end)
pnpm test -- --testPathPattern="ai-policy.service.integration"

# Run all AI-related tests
pnpm test -- --testPathPattern="(ai-policy|ai-batch|ocr.service)"

All tests must pass before deployment.


Model Name Verification

# ตรวจสอบว่าไม่มี typhoon* ใน codebase (ควรเป็น 0)
grep -r "typhoon2\.5-np-dms\|typhoon-np-dms-ocr" backend/src/ frontend/ --include="*.ts" --include="*.tsx" | wc -l
# Expected: 0