7e8f4859cd
- 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
8.2 KiB
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:devinbackend/) - Admin user token with
system.manage_aipermission - 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