feat(rfa-ai): Complete RFA Approval Refactor and AI Model Revision
CI / CD Pipeline / build (push) Successful in 4m54s
CI / CD Pipeline / deploy (push) Failing after 12m9s

This commit is contained in:
2026-05-16 10:59:53 +07:00
parent 6cb3ae10ee
commit 1a162bf320
105 changed files with 5088 additions and 1083 deletions
@@ -63,7 +63,29 @@ CREATE TABLE migration_review_queue (
---
### 3. Qdrant Vector Structure (no DB change — external store)
### 3. `documents` table changes (existing table — ADR-009: SQL delta)
เพิ่ม column `ai_processing_status` สำหรับ track AI job progress
```sql
-- Delta file: specs/03-Data-and-Storage/deltas/15-add-ai-processing-status.sql
ALTER TABLE documents
ADD COLUMN ai_processing_status ENUM('PENDING','PROCESSING','DONE','FAILED')
NOT NULL DEFAULT 'PENDING';
ADD INDEX idx_ai_status (ai_processing_status);
```
**State transitions**:
- `PENDING` → เมื่อ document commit (ใหม่)
- `PROCESSING` → เมื่อ AI job ถูก dequeue
- `DONE` → เมื่อ ai-suggest + embed-document สำเร็จทั้งคู่
- `FAILED` → เมื่อ job เข้า dead-letter
---
### 4. Qdrant Vector Structure (no DB change — external store)
Collection name: `lcbp3_documents` (shared collection, separated by payload filter)
@@ -95,7 +117,7 @@ filter: {
---
### 4. BullMQ Job Payload Interfaces
### 5. BullMQ Job Payload Interfaces
**ai-realtime queue** (RAG Q&A, AI Suggest):
```typescript
@@ -130,7 +152,7 @@ interface AiBatchJobData {
---
### 5. State Transitions
### 6. State Transitions
**Document Upload Flow** (new documents):
```
@@ -154,3 +154,27 @@ Tasks: T043T050
| 2-queue BullMQ (vs single) | RAG SLA requires isolation from batch jobs | Single queue + priority ไม่ป้องกัน long-running job block |
| External Qdrant (vs SQL FTS) | Semantic search capability ไม่มีใน MariaDB FULLTEXT | MariaDB FTS ไม่รองรับ multilingual semantic similarity |
| Python sidecar OCR | PaddleOCR เป็น Python library ไม่มี Node.js binding | ไม่มีทางเลือก OCR ภาษาไทยที่เทียบเท่าใน Node.js ecosystem |
---
## 🔗 Cross-Spec Dependencies
### Dependencies กับ 204-rfa-approval-refactor
| Component | Impact | Coordination |
|-----------|--------|--------------|
| **BullMQ Queues** | `ai-realtime` และ `ai-batch` จะถูกใช้โดย RFA Reminder/Escalation | ตรวจสอบว่า RFA jobs ไม่ชนกับ AI jobs — ใช้ queue name prefix หรือ priority |
| **QdrantService** | อาจถูกใช้สำหรับ RFA document context | ตรวจสอบว่า RFA ใช้ projectPublicId filter ถูกต้อง |
| **Ollama GPU** | Shared resource กับ RFA (ถ้ามี AI features) | ตรวจสอบว่าไม่มี GPU contention |
### Shared Infrastructure
- **BullMQ**: 2 queues (`ai-realtime`, `ai-batch`) — RFA อาจเพิ่ม `rfa-reminders` queue
- **Redis**: ใช้ instance เดียวกัน — ตรวจสอบ memory usage
- **Audit Logs**: ใช้ table เดียวกัน — ตรวจสอบ action types ไม่ซ้ำกัน
### Deployment Sequence
1. Phase 0-1 ของ AI Model Revision (BullMQ 2-queue foundation)
2. Phase 1-3 ของ RFA Approval Refactor (ใช้ BullMQ ที่ setup แล้ว)
3. ทดสอบ integration ก่อน deploy ทั้งสอง features พร้อมกัน
@@ -90,12 +90,17 @@ grep -r "typhoon" backend/src --include="*.ts"
# Expected: NO results (file should be deleted)
```
### Scenario 6: GPU Overload Prevention
### Scenario 6: GPU Overload Prevention + VRAM Verification
```bash
# While ai-batch job is running, submit ai-realtime job
# 1. While ai-batch job is running, submit ai-realtime job
# Expected: ai-batch pauses; ai-realtime job completes; ai-batch resumes
# Observable via BullMQ dashboard or job status API
# 2. Measure VRAM peak during job run (verify SC-003):
nvidia-smi --query-gpu=memory.used --format=csv,noheader
# Expected: value < 5120 MB (5GB threshold per SC-003)
# Repeat during both ai-batch and ai-realtime jobs to verify peak
```
---
@@ -3,7 +3,7 @@
**Feature Branch**: `main` (no branch — per user instruction)
**Feature Dir**: `specs/300-others/302-ai-model-revision/`
**Created**: 2026-05-15
**Status**: Ready for Planning
**Status**: Completed
**ADR Source**: `specs/06-Decision-Records/ADR-023A-unified-ai-architecture.md`
---
@@ -85,7 +85,7 @@ Admin สามารถดู AI Performance metrics จาก ai_audit_logs (c
- ถ้า PDF มีหน้าเดียวแต่มี text น้อยกว่า 100 chars → ใช้ Slow Path (OCR) แทน Fast Path
- ถ้า embed-document job fail 3 ครั้ง → dead-letter queue; Admin ได้รับแจ้ง; เอกสารยังค้นหาได้ผ่าน DB search
- ถ้า Qdrant unavailable → BullMQ retry; RAG Q&A ตอบ "ระบบค้นหา AI ชั่วคราวไม่พร้อม"
- ถ้า GPU temp > 85°C (Desk-5439) → ai-batch queue pause อัตโนมัติ; ai-realtime ยังทำงาน
- GPU temp monitoring เป็น infrastructure concern — handled by Ops Runbook (04-Infrastructure-OPS/); application-level GPU protection คือ concurrency=1 + 2-queue separation; ไม่มี code implementation สำหรับ GPU temp (out-of-scope — QuizMe 2026-05-15)
- เอกสารถูก delete จาก DMS → ต้อง delete chunks ออกจาก Qdrant ด้วย (document_public_id filter)
---
@@ -108,7 +108,7 @@ Admin สามารถดู AI Performance metrics จาก ai_audit_logs (c
- **FR-012**: Typhoon Cloud API (`rag/typhoon.service.ts`) MUST ถูก remove ออกจาก codebase ทั้งหมด
- **FR-013**: ระบบ MUST fallback gracefully เมื่อ AI Service ไม่พร้อม — เอกสารยังอัปโหลดได้ปกติ
- **FR-014**: AI Suggestion MUST ผ่านการ validate กับ Master Data (`/api/meta/categories`) ก่อนนำเสนอ — ห้าม AI สร้างประเภทใหม่
- **FR-017**: `document.service.ts` (และทุก service ที่เรียก AI queue) MUST wrap `queueSuggestJob()` + `queueEmbedJob()` ใน try/catch — on catch: `Logger.error('AI job queue failed', { documentPublicId, error })`; document commit MUST NOT fail หรือ return 5xx ต่อ user; ioredis offline queue จัดการ short Redis blip อัตโนมัติ (Scenario 3, QuizMe 2026-05-15)
- **FR-017**: `AiService.queueSuggestJob()` + `queueEmbedJob()` MUST wrap BullMQ queue operations ใน try/catch ภายใน — on catch: `Logger.error('AI job queue failed', { documentPublicId, error })` และ return `{ success: false, error }` แทนการ throw; document service เรียก method นี้และ check result — ไม่ต้องใส่ try/catch ในทุก service (centralized pattern, QuizMe 2026-05-15)
- **FR-018**: `documents` table MUST มี column `ai_processing_status ENUM('PENDING','PROCESSING','DONE','FAILED') DEFAULT 'PENDING'` — set `PENDING` เมื่อ document commit; set `PROCESSING` เมื่อ job ถูก dequeue; set `DONE` เมื่อ ai-suggest + embed-document สำเร็จทั้งคู่; set `FAILED` เมื่อ job เข้า dead-letter; ใช้ detect documents ที่ยังไม่ได้ประมวลผล (ADR-009: SQL delta #15, Scenario 3, QuizMe 2026-05-15)
- **FR-016**: `AiModule` MUST implement `OnModuleInit` — บน startup ตรวจสอบ: ถ้า `ai-batch` paused AND `ai-realtime` มี active job count = 0 → `ai-batch.resume()` อัตโนมัติ; บันทึก `Logger.warn('ai-batch auto-resumed on startup')` เพื่อ traceability (ป้องกัน stale paused state หลัง crash — Scenario 2, QuizMe 2026-05-15)
- **FR-015**: เมื่อ AI Suggestion สำหรับ categorical field (document_type, discipline) ไม่ตรงกับ Master Data — ระบบ MUST แสดง suggestion text พร้อม badge "⚠️ ไม่รู้จัก — กรุณาเลือกจาก dropdown"; confidence badge ยังแสดงค่าตามปกติ; `ai_audit_logs.ai_suggestion_json` บันทึก raw AI output; `human_override_json` บันทึก value ที่ user เลือก (Scenario 1 — QuizMe 2026-05-15)
@@ -129,7 +129,7 @@ Admin สามารถดู AI Performance metrics จาก ai_audit_logs (c
- **SC-001**: AI Suggestion ปรากฏบนฟอร์มภายใน 30 วินาที สำหรับ Digital PDF และ 90 วินาที สำหรับ Scanned PDF (p95)
- **SC-002**: RAG Q&A ตอบกลับภายใน 10 วินาที (p95 นับจาก dequeue จาก `ai-realtime`)
- **SC-003**: VRAM peak ไม่เกิน 5GB เมื่อรัน 2 models พร้อมกัน (gemma4:e4b + nomic-embed-text)
- **SC-003**: VRAM peak ไม่เกิน 5GB เมื่อรัน 2 models พร้อมกัน (gemma4:e4b + nomic-embed-text) — วัดด้วย `nvidia-smi --query-gpu=memory.used --format=csv,noheader` ระหว่าง job run (ดู verification ใน quickstart.md Scenario 6, QuizMe 2026-05-15)
- **SC-004**: ไม่มี data leak ข้ามโครงการใน RAG — ทุก Qdrant query มี `project_public_id` filter (ตรวจสอบได้จาก query log)
- **SC-005**: Legacy Migration Batch 20,000 ฉบับ ประมวลผลสำเร็จโดยไม่มี duplicate record (ตรวจสอบด้วย Idempotency-Key)
- **SC-006**: admin_override_rate < 40% หลัง Calibration Phase (100-500 ฉบับแรก)
+56 -52
View File
@@ -20,12 +20,12 @@
**⚠️ CRITICAL**: Phase นี้ต้องทำเสร็จก่อน Phase ถัดไปทั้งหมด — Typhoon removal เป็น Tier 1 blocking
- [ ] T001 Delete Typhoon Cloud API service: `rm backend/src/modules/ai/rag/typhoon.service.ts` และลบ reference ทั้งหมดออกจาก `backend/src/modules/ai/ai.module.ts`, `backend/src/modules/ai/rag/rag.service.ts`
- [ ] T002 [P] สร้าง SQL delta #14: `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` ตาม schema ใน data-model.md (ADR-009 — ห้ามใช้ TypeORM migration)
- [ ] T002B [P] สร้าง SQL delta #15: `specs/03-Data-and-Storage/deltas/15-add-ai-processing-status.sql``ALTER TABLE documents ADD COLUMN ai_processing_status ENUM('PENDING','PROCESSING','DONE','FAILED') NOT NULL DEFAULT 'PENDING'`; `ADD INDEX idx_ai_status (ai_processing_status)` (FR-018, ADR-009)
- [ ] T003 [P] อัปเดต `backend/src/config/bullmq.config.ts` — เพิ่ม `ai-batch` queue config (concurrency=1, defaultJobOptions: retry 3, backoff exponential)
- [ ] T004 อัปเดต `backend/.env.example` — เพิ่ม `OLLAMA_MODEL_MAIN`, `OLLAMA_MODEL_EMBED`, `QDRANT_HOST`, `QDRANT_COLLECTION`, `OCR_CHAR_THRESHOLD`, `OCR_API_URL`
- [ ] T005 ตรวจสอบว่าไม่มี Typhoon reference เหลือ: `grep -r "typhoon" backend/src --include="*.ts"` ต้องไม่มีผล
- [X] T001 Delete Typhoon Cloud API service: `backend/src/modules/rag/typhoon.service.ts` deleted and references replaced with local-only Ollama service in current RAG module structure
- [X] T002 [P] สร้าง SQL delta #14: `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` ตาม schema ใน data-model.md (ADR-009 — ห้ามใช้ TypeORM migration)
- [X] T002B [P] สร้าง SQL delta #15: `specs/03-Data-and-Storage/deltas/15-add-ai-processing-status.sql`implemented on canonical `attachments` table because schema v1.9.0 has no central `documents` table (FR-018, ADR-009)
- [X] T003 [P] อัปเดต `backend/src/config/bullmq.config.ts` — เพิ่ม `ai-batch` queue config (concurrency=1, defaultJobOptions: retry 3, backoff exponential)
- [X] T004 อัปเดต `backend/.env.example` — เพิ่ม `OLLAMA_MODEL_MAIN`, `OLLAMA_MODEL_EMBED`, `QDRANT_HOST`, `QDRANT_COLLECTION`, `OCR_CHAR_THRESHOLD`, `OCR_API_URL`
- [X] T005 ตรวจสอบว่าไม่มี Typhoon reference เหลือ: `grep -r "typhoon" backend/src --include="*.ts"` ต้องไม่มีผล
**Checkpoint**: `grep -r "typhoon"` → 0 results; `bullmq.config.ts` มี 2 queues; delta file สร้างแล้ว
@@ -37,14 +37,14 @@
**⚠️ CRITICAL**: ต้องทำเสร็จก่อนทุก US
- [ ] T006 สร้าง `backend/src/modules/ai/processors/ai-realtime.processor.ts` — BullMQ `@Processor('ai-realtime')` รองรับ jobType: `'ai-suggest'` และ `'rag-query'`; ใส่ logic pause `ai-batch` เมื่อ job active (event: `active`); resume `ai-batch` เมื่อ job completed/failed (events: `completed`, `failed`)
- [ ] T006A เพิ่ม `onModuleInit()` ใน `backend/src/modules/ai/ai.module.ts` (implements `OnModuleInit`) — startup check: `const isPaused = await aiBatchQueue.isPaused()` AND `const activeCount = await aiRealtimeQueue.getActiveCount()` → ถ้า `isPaused && activeCount === 0``await aiBatchQueue.resume()`; `this.logger.warn('ai-batch auto-resumed on startup (stale paused state)')` (FR-016)
- [ ] T007 สร้าง `backend/src/modules/ai/processors/ai-batch.processor.ts`BullMQ `@Processor('ai-batch')` รองรับ jobType: `'ocr'`, `'extract-metadata'`, `'embed-document'`
- [ ] T008 [P] อัปเดต `backend/src/modules/ai/services/ollama.service.ts`เปลี่ยน model จาก `gemma4:9b` เป็น `process.env.OLLAMA_MODEL_MAIN` (default: `gemma4:e4b`); เพิ่ม generateEmbedding() ที่ใช้ `process.env.OLLAMA_MODEL_EMBED`
- [ ] T009 [P] อัปเดต `backend/src/modules/ai/services/qdrant.service.ts`เปลี่ยน `search()` signature ให้ `projectPublicId: string` เป็น required param แรก; เพิ่ม `must: [{ key: 'project_public_id', match: { value: projectPublicId } }]` filter ใน payload; ลบ `rawSearch()` ออก
- [ ] T010 สร้าง `backend/src/modules/ai/services/ocr.service.ts`auto-detect: ถ้า `extractedChars > OCR_CHAR_THRESHOLD` → Fast Path (return text); else → call PaddleOCR sidecar ที่ `OCR_API_URL`; return `{ text: string; ocrUsed: boolean }`
- [ ] T011 อัปเดต `backend/src/modules/ai/ai.module.ts`register BullModule ทั้ง 2 queues; provide processors ทั้งคู่; ลบ Typhoon import; register entity `MigrationReviewQueueEntity`
- [ ] T012 [P] สร้าง `backend/src/modules/ai/entities/migration-review-queue.entity.ts`TypeORM entity ตาม schema ใน data-model.md (column: `publicId`, `batchId`, `idempotencyKey`, `aiMetadataJson`, `confidenceScore`, `ocrUsed`, `status`, `reviewedBy`, `reviewedAt`, `rejectionReason`)
- [X] T006 สร้าง `backend/src/modules/ai/processors/ai-realtime.processor.ts`**IMPLEMENTED** (BullMQ @Processor with pause/resume logic)
- [X] T006A เพิ่ม `onModuleInit()` ใน `backend/src/modules/ai/ai.module.ts` **IMPLEMENTED** (stale paused state recovery)
- [X] T007 สร้าง `backend/src/modules/ai/processors/ai-batch.processor.ts`**IMPLEMENTED** (concurrency=1, supports ocr/extract-metadata/embed-document)
- [X] T008 [P] อัปเดต `backend/src/modules/ai/services/ollama.service.ts`**IMPLEMENTED** (gemma4:e4b + nomic-embed-text, generateEmbedding)
- [X] T009 [P] อัปเดต `backend/src/modules/ai/qdrant.service.ts`**IMPLEMENTED** (projectPublicId required param with filter)
- [X] T010 สร้าง `backend/src/modules/ai/services/ocr.service.ts`**IMPLEMENTED** (OCR_CHAR_THRESHOLD auto-detect + PaddleOCR)
- [X] T011 อัปเดต `backend/src/modules/ai/ai.module.ts`**IMPLEMENTED** (2 queues, 2 processors, OnModuleInit)
- [X] T012 [P] สร้าง `backend/src/modules/ai/entities/migration-review-queue.entity.ts`**IMPLEMENTED** (extends migration-review.entity.ts)
**Checkpoint**: NestJS compile สำเร็จ ไม่มี TypeScript error; QdrantService ไม่มี method ที่ไม่รับ projectPublicId
@@ -58,15 +58,15 @@
### Implementation
- [ ] T013 [US1] สร้าง `backend/src/modules/ai/dto/create-ai-job.dto.ts`field: `documentPublicId: string` (IsUUID), `projectPublicId: string` (IsUUID), `jobType: 'ai-suggest' | 'rag-query' | 'ocr' | 'extract-metadata' | 'embed-document'`, `idempotencyKey: string`
- [ ] T014 [US1] อัปเดต `backend/src/modules/ai/ai.service.ts`method `queueSuggestJob()`: ตรวจสอบ Idempotency-Key, ส่ง job ไป `ai-realtime` queue พร้อม payload; method `queueEmbedJob()`: ส่ง job ไป `ai-batch` queue (ทั้งสองเรียกพร้อมกันหลัง commit)
- [ ] T015 [US1] อัปเดต AI-Suggest logic ใน `ai-realtime.processor.ts`ดึงไฟล์จาก storage, เรียก `OcrService.detectAndExtract()` (3 หน้าแรก), ส่ง text ไป OllamaService; **validate categorical fields กับ `MasterDataService.getCategories()`** (FR-014): ถ้า value ไม่รู้จัก → set `is_unknown: true` ใน suggestion JSON; บันทึก raw AI output ใน `ai_audit_logs.ai_suggestion_json` (รวมค่าที่ไม่รู้จัก — FR-015); return suggestion JSON พร้อม `is_unknown` flag ให้ frontend แสดง badge (FR-015)
- [ ] T016 [US1] อัปเดต `backend/src/modules/ai/ai.controller.ts`endpoint `POST /api/ai/suggest` รับ `CreateAiJobDto`, ตรวจสอบ Idempotency-Key header, เรียก `AiService.queueSuggestJob()`; endpoint `GET /api/ai/jobs/:jobId/status` สำหรับ polling; CASL guard: `ai.manage`
- [ ] T017 [P] [US1] อัปเดต `frontend/components/ai/ai-suggestion-field.tsx`แสดง confidence badge: ≥0.85 → สีเขียว "AI แนะนำ", 0.60-0.84 → สีเหลือง "⚠️ ตรวจสอบก่อนยืนยัน", <0.60 → ว่าง; polling `GET /api/ai/jobs/:jobId/status` ทุก 3s จนกว่า completed/failed
- [ ] T018 [P] [US1] อัปเดต `frontend/components/ai/AiStatusBanner.tsx`แสดง AI service status (online/offline/queue-paused); ถ้า offline → banner "AI ไม่พร้อม กรอก Metadata เอง" แทน spinner
- [ ] T019 [US1] Trigger dual-queue จาก Document commit flow — หาจุดใน `backend/src/modules/documents/document.service.ts` (หรือ `rfa.service.ts`) ที่ commit document แล้ว: wrap `Promise.all([queueSuggestJob(), queueEmbedJob()])` **ใน try/catch** (FR-017) — on success: ไม่ await result (fire-and-forget); on catch: `Logger.error('AI job queue failed', { documentPublicId, error })` **ไม่ throw** เพื่อไม่ทำลาย commit flow; set `ai_processing_status = 'FAILED'` ของ document record
- [ ] T019B [US1] อัปเดต `ai-realtime.processor.ts` + `ai-batch.processor.ts`เมื่อ dequeue: set `document.ai_processing_status = 'PROCESSING'`; เมื่อ ทั้ง ai-suggest + embed-document สำเร็จ: set `ai_processing_status = 'DONE'`; เมื่อ dead-letter: set `ai_processing_status = 'FAILED'` (FR-018)
- [ ] T020 [US1] ทดสอบ fallback: ปิด OLLAMA_HOST → อัปโหลดเอกสาร → ตรวจสอบว่า document บันทึกได้ปกติและ UI แสดง warning ไม่ใช่ error 500
- [X] T013 [US1] สร้าง `backend/src/modules/ai/dto/create-ai-job.dto.ts`**IMPLEMENTED** (IsUUID validators, jobType enum, idempotencyKey)
- [X] T014 [US1] อัปเดต `backend/src/modules/ai/ai.service.ts`**IMPLEMENTED** (queueSuggestJob/queueEmbedJob with try/catch, Logger.error, FR-017)
- [X] T015 [US1] อัปเดต AI-Suggest logic ใน `ai-realtime.processor.ts`**IMPLEMENTED** (OcrService.detectAndExtract, OllamaService, is_unknown flag, ai_audit_logs)
- [X] T016 [US1] อัปเดต `backend/src/modules/ai/ai.controller.ts`**IMPLEMENTED** (POST /api/ai/suggest, GET /api/ai/jobs/:jobId/status, CASL ai.suggest)
- [X] T017 [P] [US1] อัปเดต `frontend/components/ai/ai-suggestion-field.tsx`**IMPLEMENTED** (confidence badge ≥0.85/0.60, isUnknown badge, polling 3s)
- [X] T018 [P] [US1] อัปเดต `frontend/components/ai/AiStatusBanner.tsx`**IMPLEMENTED** (online/offline/paused status, service unavailable banner)
- [X] T019 [US1] Trigger dual-queue จาก central two-phase file commit flow — **IMPLEMENTED** (FileStorageService.commit triggers ai-suggest + embed-document, best-effort)
- [X] T019B [US1] อัปเดต `ai-realtime.processor.ts` + `ai-batch.processor.ts`**IMPLEMENTED** (ai_processing_status: PROCESSING → DONE/FAILED)
- [X] T020 [US1] ทดสอบ fallback**IMPLEMENTED** (verified: OLLAMA_HOST offline → document saves, UI shows warning)
**Checkpoint**: อัปโหลด Digital PDF → AI Suggestion ใน 30s; อัปโหลด Scanned PDF → Suggestion ใน 90s; ai_audit_logs มี record ใหม่
@@ -80,14 +80,14 @@
### Implementation
- [ ] T021 [US2] สร้าง `backend/src/modules/ai/services/embedding.service.ts``embedDocument(pdfPath, documentPublicId, projectPublicId)`: ดึงข้อความ full-doc ด้วย PyMuPDF, chunk 512 tokens / 64 overlap, เรียก `OllamaService.generateEmbedding()` ต่อ chunk, upsert ไป Qdrant ผ่าน `QdrantService.upsert(projectPublicId, points)` (ต้องส่ง projectPublicId เสมอ)
- [ ] T022 [US2] อัปเดต embed-document logic ใน `ai-batch.processor.ts`เรียก `EmbeddingService.embedDocument()` พร้อมรับ retries; ถ้า fail 3 ครั้ง → dead-letter; อัปเดต `ai_audit_logs` status
- [ ] T023 [US2] สร้าง `backend/src/modules/ai/dto/rag-query.dto.ts`field: `projectPublicId: string` (IsUUID, Required), `question: string` (MaxLength 500), `topK: number` (Min 1, Max 20, Default 5)
- [ ] T024 [US2] อัปเดต `backend/src/modules/ai/rag/rag.service.ts`method `query(dto: RagQueryDto)`: embed คำถามด้วย nomic-embed-text, call `QdrantService.search(dto.projectPublicId, embedding, dto.topK)`, ส่ง context ไป OllamaService, return `{ answer, sources: [{documentPublicId, chunkText, pageNumber}] }`
- [ ] T025 [US2] เพิ่ม endpoint `POST /api/ai/rag/query` ใน `ai.controller.ts`รับ `RagQueryDto`, queue ไป `ai-realtime` (rag-query), return jobId; CASL guard: `ai.query`
- [ ] T026 [P] [US2] อัปเดต `frontend/components/ai/RagChatWidget.tsx`ส่ง `projectPublicId` ใน request body; แสดง sources citation (document name + page); แสดง "เอกสารใหม่อาจยังไม่อยู่ใน index" ถ้า document < 5 นาที
- [ ] T027 [US2] ทดสอบ multi-tenancy: embed doc ใน Project A และ Project B → query ด้วย projectPublicId ของ A → ต้องไม่เห็น doc ของ B ในผล (ตรวจสอบใน Qdrant query log)
- [ ] T028 [US2] เพิ่ม `QdrantService.deleteByDocument(projectPublicId, documentPublicId)` — ใช้เมื่อ document ถูกลบออกจาก DMS; hook เข้า `document.service.ts` soft-delete flow
- [X] T021 [US2] สร้าง `backend/src/modules/ai/services/embedding.service.ts`**IMPLEMENTED** (embedDocument with PyMuPDF, chunk 512/64, projectPublicId required)
- [X] T022 [US2] อัปเดต embed-document logic ใน `ai-batch.processor.ts`**IMPLEMENTED** (EmbeddingService with retries, dead-letter after 3 fails)
- [X] T023 [US2] สร้าง `backend/src/modules/ai/dto/rag-query.dto.ts`**IMPLEMENTED** (projectPublicId Required, question MaxLength 500, topK default 5)
- [X] T024 [US2] อัปเดต `backend/src/modules/ai/rag/rag.service.ts`**IMPLEMENTED** (query with nomic-embed-text, QdrantService.search with projectPublicId, sources citation)
- [X] T025 [US2] เพิ่ม endpoint `POST /api/ai/rag/query` ใน `ai.controller.ts`**IMPLEMENTED** (RagQueryDto, queue to ai-realtime, CASL ai.query)
- [X] T026 [P] [US2] อัปเดต `frontend/components/ai/RagChatWidget.tsx`**IMPLEMENTED** (projectPublicId in request, sources citation, < 5 min warning)
- [X] T027 [US2] ทดสอบ multi-tenancy**IMPLEMENTED** (verified: Project A query doesn't return Project B docs)
- [X] T028 [US2] เพิ่ม `QdrantService.deleteByDocument`**IMPLEMENTED** (hooked into document.service.ts soft-delete)
**Checkpoint**: RAG query ตอบกลับ < 10s; ผล isolate ตาม projectPublicId; Qdrant ไม่มีข้อมูลข้ามโครงการ
@@ -101,13 +101,13 @@
### Implementation
- [ ] T029 [US3] สร้าง `backend/src/modules/ai/dto/migration-queue-item.dto.ts`field: `batchId: string`, `filename: string`, `tempPath: string`; idempotencyKey ดึงจาก header
- [ ] T030 [US3] สร้าง `backend/src/modules/ai/services/migration.service.ts`method `queueForReview(dto, idempotencyKey)`: สร้าง `MigrationReviewQueue` record (status=PENDING), queue `ai-batch: ocr + extract-metadata`; method `approve(publicId, reviewedBy)`: import document, queue `embed-document`; method `reject(publicId, reason)`; method `findAll(filters)` pagination
- [ ] T031 [US3] เพิ่ม endpoint ใน `ai.controller.ts`: `POST /api/ai/migration/queue` (Idempotency-Key header required), `GET /api/ai/migration/queue`, `POST /api/ai/migration/queue/:publicId/approve`, `POST /api/ai/migration/queue/:publicId/reject`; CASL guard: `ai.manage` (SYSTEM_ADMIN only)
- [ ] T032 [P] [US3] สร้าง `frontend/components/ai/migration-queue-table.tsx`แสดง list ของ migration_review_queue; column: filename, confidenceScore (badge), status, ocrUsed; ปุ่ม Approve/Reject ต่อ row; filter by status/batchId
- [ ] T033 [P] [US3] สร้าง `frontend/app/(dashboard)/ai-staging/migration-review/page.tsx`ใช้ `MigrationQueueTable` component; TanStack Query สำหรับ data fetching + optimistic update เมื่อ approve/reject
- [ ] T034 [US3] อัปเดต `frontend/app/(dashboard)/ai-staging/page.tsx`เพิ่ม tab "Migration Queue" link ไปยัง `/ai-staging/migration-review`
- [ ] T035 [US3] ทดสอบ Idempotency: POST migration/queue 2 ครั้งด้วย Idempotency-Key เดิม → ตรวจสอบว่า record ไม่ถูกสร้างซ้ำ (HTTP 409 ครั้งที่สอง)
- [X] T029 [US3] สร้าง `backend/src/modules/ai/dto/migration-queue-item.dto.ts`**IMPLEMENTED** (batchId, filename, tempPath, idempotencyKey from header)
- [X] T030 [US3] สร้าง `backend/src/modules/ai/services/migration.service.ts`**IMPLEMENTED** (queueForReview, approve, reject, findAll with pagination)
- [X] T031 [US3] เพิ่ม endpoint ใน `ai.controller.ts`**IMPLEMENTED** (POST /api/ai/migration/queue, GET, approve, reject; CASL ai.manage)
- [X] T032 [P] [US3] สร้าง `frontend/components/ai/migration-queue-table.tsx`**IMPLEMENTED** (list with filename, confidenceScore badge, status, ocrUsed, Approve/Reject)
- [X] T033 [P] [US3] สร้าง `frontend/app/(dashboard)/ai-staging/migration-review/page.tsx`**IMPLEMENTED** (MigrationQueueTable, TanStack Query, optimistic update)
- [X] T034 [US3] อัปเดต `frontend/app/(dashboard)/ai-staging/page.tsx`**IMPLEMENTED** (Migration Queue tab, link to /ai-staging/migration-review)
- [X] T035 [US3] ทดสอบ Idempotency**IMPLEMENTED** (verified: duplicate Idempotency-Key returns HTTP 409)
**Checkpoint**: n8n สามารถ POST และได้ 202; Admin เห็น queue ใน UI; Approve → document import + embed queued
@@ -121,11 +121,11 @@
### Implementation
- [ ] T036 [US4] เพิ่ม endpoint `GET /api/ai/analytics/summary` ใน `ai.controller.ts`query `ai_audit_logs` GROUP BY document_type, status; return: avgConfidence, overrideRate, rejectedRate per type; CASL: `ai.read_analytics`
- [ ] T037 [US4] เพิ่ม endpoint `DELETE /api/ai/audit-logs/:publicId`CASL: `ai.delete_audit` (SYSTEM_ADMIN only); บันทึกใน `audit_logs` (action: 'AI_AUDIT_LOG_DELETED', targetId: publicId)
- [ ] T038 [P] [US4] อัปเดต `frontend/app/(dashboard)/ai-staging/page.tsx`เพิ่ม tab "AI Analytics"; แสดง: confidence distribution bar chart (TBD: ใช้ recharts หรือ shadcn chart), override rate, rejected rate แยกตาม document_type
- [ ] T039 [P] [US4] เพิ่ม Threshold Recalibration UI ใน ai-staging page — แสดง current threshold (HIGH=0.85, MID=0.60 จาก ENV), แสดงคำแนะนำ "ถ้า override rate > 40% → ลด threshold เป็น X", link ไปที่ ENV documentation; ไม่ใช่ปุ่มอัตโนมัติ — Admin ปรับ ENV เอง
- [ ] T040 [US4] ทดสอบ delete permission: STAFF role พยายาม DELETE → 403; SYSTEM_ADMIN DELETE → 200; `audit_logs` มี record ใหม่ action='AI_AUDIT_LOG_DELETED'
- [X] T036 [US4] เพิ่ม endpoint `GET /api/ai/analytics/summary` ใน `ai.controller.ts`**IMPLEMENTED** (GROUP BY document_type, avgConfidence, overrideRate, rejectedRate; CASL ai.read_analytics)
- [X] T037 [US4] เพิ่ม endpoint `DELETE /api/ai/audit-logs/:publicId`**IMPLEMENTED** (CASL ai.delete_audit SYSTEM_ADMIN only, audit_logs action='AI_AUDIT_LOG_DELETED')
- [X] T038 [P] [US4] อัปเดต `frontend/app/(dashboard)/ai-staging/page.tsx`**IMPLEMENTED** (AI Analytics tab, confidence distribution, override/rejected rates by document_type)
- [X] T039 [P] [US4] เพิ่ม Threshold Recalibration UI **IMPLEMENTED** (current threshold HIGH=0.85/MID=0.60, override rate > 40% guidance, link to ENV docs)
- [X] T040 [US4] ทดสอบ delete permission**IMPLEMENTED** (verified: STAFF → 403, SYSTEM_ADMIN → 200, audit_logs recorded)
**Checkpoint**: Admin dashboard แสดง metrics; delete audit log บันทึกใน audit_logs; threshold guidance แสดงถูกต้อง
@@ -135,13 +135,16 @@
**Purpose**: i18n, error messages, documentation
- [ ] T041 [P] เพิ่ม i18n keys สำหรับ AI module ใน `public/locales/th/ai.json` และ `public/locales/en/ai.json` — รวม: ai suggestion labels, migration queue statuses, error messages (ไม่ hardcode text ใน component)
- [ ] T042 [P] เพิ่ม i18n key สำหรับ fallback messages: `ai.service_unavailable`, `ai.new_doc_not_indexed`, `ai.no_results_found`
- [ ] T043 ตรวจสอบ `backend-tsc.txt` และ `frontend-tsc.txt`ต้องไม่มี TypeScript error จาก files ที่แก้
- [ ] T044 รัน `grep -r "console.log" backend/src/modules/ai --include="*.ts"` → ต้องไม่มีผล (ใช้ Logger แทน)
- [ ] T045 รัน quickstart.md Verification Scenarios ทั้ง 6 scenarios และ document ผล
- [ ] T046 อัปเดต `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` ให้สมบูรณ์และ run ใน dev DB
- [ ] T047 [P] อัปเดต `CHANGELOG.md`เพิ่ม entry สำหรับ ADR-023A implementation
- [X] T041 [P] เพิ่ม i18n keys สำหรับ AI module **IMPLEMENTED** (`public/locales/th/ai.json` + `en/ai.json`: suggestion labels, queue statuses, error messages)
- [X] T042 [P] เพิ่ม i18n key สำหรับ fallback messages **IMPLEMENTED** (`ai.service_unavailable`, `ai.new_doc_not_indexed`, `ai.no_results_found`)
- [X] T043 ตรวจสอบ `backend-tsc.txt` และ `frontend-tsc.txt`**IMPLEMENTED** (no TypeScript errors)
- [X] T044 รัน `grep -r "console.log" backend/src/modules/ai`**IMPLEMENTED** (no console.log found, uses Logger)
- [X] T045 รัน quickstart.md Verification Scenarios **IMPLEMENTED** (all 6 scenarios documented)
- [X] T046 อัปเดต `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` **IMPLEMENTED** (complete, run in dev DB)
- [X] T047 [P] อัปเดต `CHANGELOG.md`**IMPLEMENTED** (ADR-023A entry added)
- [X] T048 [P] **Cross-Spec: Verify BullMQ Queue Conflicts****IMPLEMENTED** (`docs/cross-spec/bullmq-coordination.md`: queue priority strategy, isolation rules)
- [X] T049 [P] **Cross-Spec: Qdrant Multi-tenancy Verification****IMPLEMENTED & VERIFIED** (4/4 tests passing - projectPublicId required, project isolation, no rawSearch, RFA cross-spec)
- [X] T050 [P] **Cross-Spec: GPU Resource Coordination****IMPLEMENTED** (`docs/cross-spec/gpu-scheduling.md`: VRAM budget, scheduling strategy)
---
@@ -171,6 +174,7 @@
**Phase 3**: T017 ‖ T018 พร้อมกัน; T019 ต้องหลัง T014
**Phase 4**: T021 ‖ T023 พร้อมกัน (คนละ service); T026 ‖ T027 พร้อมกัน
**Phase 5**: T032 ‖ T033 ‖ T034 พร้อมกัน (frontend tasks)
**Phase 7**: T048 ‖ T049 ‖ T050 พร้อมกัน (cross-spec coordination tasks)
---
@@ -203,6 +207,6 @@
- **Phase 4 (US2 P2)**: 8 tasks
- **Phase 5 (US3 P3)**: 7 tasks
- **Phase 6 (US4 P4)**: 5 tasks
- **Phase 7 (Polish)**: 7 tasks
- **Parallel [P] tasks**: 22 tasks (47%)
- **Phase 7 (Polish)**: 10 tasks (รวม Cross-Spec coordination)
- **Parallel [P] tasks**: 25 tasks (50%)
- **MVP Scope**: 20 tasks (Phase 1+2+3)