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
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
// 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
|
||||
|
||||
```bash
|
||||
export BACKEND_URL="https://backend.np-dms.work/api"
|
||||
export TOKEN="your-jwt-token-here"
|
||||
export IDEMPOTENCY_KEY="test-$(date +%s)"
|
||||
```
|
||||
|
||||
### PowerShell
|
||||
|
||||
```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:**
|
||||
```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:**
|
||||
```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:**
|
||||
```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
|
||||
|
||||
```bash
|
||||
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:**
|
||||
```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:**
|
||||
```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:**
|
||||
```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)
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```sql
|
||||
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)
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```sql
|
||||
-- ตรวจสอบว่ามี 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
|
||||
|
||||
```bash
|
||||
# ส่ง 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
|
||||
|
||||
```bash
|
||||
# สมมติว่ามี 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
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# ตรวจสอบว่าไม่มี typhoon* ใน codebase (ควรเป็น 0)
|
||||
grep -r "typhoon2\.5-np-dms\|typhoon-np-dms-ocr" backend/src/ frontend/ --include="*.ts" --include="*.tsx" | wc -l
|
||||
# Expected: 0
|
||||
```
|
||||
Reference in New Issue
Block a user