feat(ai): unify AI architecture, implement RAG and legacy migration
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Specification Quality Checklist: AI Model Revision (ADR-023A)
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-05-15
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs) — spec describes WHAT not HOW
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for both technical and non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain — all resolved in Clarifications session
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable (SC-001 through SC-008 with specific metrics)
|
||||
- [x] Success criteria are technology-agnostic (no framework specifics)
|
||||
- [x] All acceptance scenarios are defined (4 User Stories with scenarios)
|
||||
- [x] Edge cases are identified (6 edge cases documented)
|
||||
- [x] Scope is clearly bounded (AI pipeline only — DMS core workflow not in scope)
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User stories cover primary flows (Upload, RAG, Migration, Monitoring)
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Spec derived from ADR-023A grilling session (2026-05-15) — all decisions validated
|
||||
- Quality: PASS — ready for `/speckit-plan`
|
||||
@@ -0,0 +1,184 @@
|
||||
openapi: "3.1.0"
|
||||
info:
|
||||
title: AI Jobs API
|
||||
version: "1.0.0"
|
||||
description: BullMQ-based AI job submission endpoints (ADR-023A)
|
||||
|
||||
paths:
|
||||
/api/ai/suggest:
|
||||
post:
|
||||
summary: Queue AI Suggestion job (ai-realtime)
|
||||
description: |
|
||||
Triggered internally after document commit.
|
||||
Queues ai-suggest job to extract metadata from PDF (max 3 pages).
|
||||
Returns jobId for polling.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AiSuggestRequest'
|
||||
responses:
|
||||
"202":
|
||||
description: Job queued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JobQueuedResponse'
|
||||
"400":
|
||||
$ref: '#/components/responses/ValidationError'
|
||||
"503":
|
||||
description: AI Service unavailable (Desk-5439 offline) — document saved, AI skipped
|
||||
|
||||
/api/ai/jobs/{jobId}/status:
|
||||
get:
|
||||
summary: Poll job status
|
||||
parameters:
|
||||
- name: jobId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Job status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JobStatusResponse'
|
||||
|
||||
/api/ai/rag/query:
|
||||
post:
|
||||
summary: RAG Q&A query (ai-realtime)
|
||||
description: |
|
||||
Queues rag-query job. Results returned via polling or WebSocket event.
|
||||
projectPublicId REQUIRED — enforces multi-tenant isolation.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RagQueryRequest'
|
||||
responses:
|
||||
"202":
|
||||
description: Job queued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JobQueuedResponse'
|
||||
|
||||
/api/ai/embed:
|
||||
post:
|
||||
summary: Queue embed-document job (ai-batch)
|
||||
description: |
|
||||
Triggered internally after document commit (parallel with ai-suggest).
|
||||
Full-document chunked embedding — NOT limited to 3 pages.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EmbedDocumentRequest'
|
||||
responses:
|
||||
"202":
|
||||
$ref: '#/components/schemas/JobQueuedResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
AiSuggestRequest:
|
||||
type: object
|
||||
required: [documentPublicId, projectPublicId, idempotencyKey]
|
||||
properties:
|
||||
documentPublicId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUIDv7 of the committed document
|
||||
projectPublicId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUIDv7 of the project — REQUIRED for multi-tenancy
|
||||
idempotencyKey:
|
||||
type: string
|
||||
description: Prevents duplicate AI job on retry
|
||||
|
||||
RagQueryRequest:
|
||||
type: object
|
||||
required: [projectPublicId, question]
|
||||
properties:
|
||||
projectPublicId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUIDv7 — limits search to this project only
|
||||
question:
|
||||
type: string
|
||||
maxLength: 500
|
||||
description: Natural language question (Thai or English)
|
||||
topK:
|
||||
type: integer
|
||||
default: 5
|
||||
minimum: 1
|
||||
maximum: 20
|
||||
|
||||
EmbedDocumentRequest:
|
||||
type: object
|
||||
required: [documentPublicId, projectPublicId, idempotencyKey]
|
||||
properties:
|
||||
documentPublicId:
|
||||
type: string
|
||||
format: uuid
|
||||
projectPublicId:
|
||||
type: string
|
||||
format: uuid
|
||||
idempotencyKey:
|
||||
type: string
|
||||
|
||||
JobQueuedResponse:
|
||||
type: object
|
||||
properties:
|
||||
jobId:
|
||||
type: string
|
||||
queue:
|
||||
type: string
|
||||
enum: [ai-realtime, ai-batch]
|
||||
estimatedWaitSecs:
|
||||
type: integer
|
||||
|
||||
JobStatusResponse:
|
||||
type: object
|
||||
properties:
|
||||
jobId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [waiting, active, completed, failed]
|
||||
result:
|
||||
type: object
|
||||
nullable: true
|
||||
description: AI suggestion payload when completed
|
||||
|
||||
responses:
|
||||
ValidationError:
|
||||
description: Validation failed (class-validator)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: integer
|
||||
message:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
@@ -0,0 +1,206 @@
|
||||
openapi: "3.1.0"
|
||||
info:
|
||||
title: Migration Queue API
|
||||
version: "1.0.0"
|
||||
description: Legacy Document Migration pipeline endpoints (ADR-023A)
|
||||
|
||||
paths:
|
||||
/api/ai/migration/queue:
|
||||
post:
|
||||
summary: Submit legacy document for AI processing (n8n → DMS API)
|
||||
description: |
|
||||
Called by n8n during Legacy Migration Phase.
|
||||
Queues OCR + extract-metadata jobs (ai-batch).
|
||||
Result stored in migration_review_queue (status=PENDING).
|
||||
Requires Idempotency-Key header to prevent duplicates.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "SD-001-2026:batch-001"
|
||||
description: Format — <doc_number>:<batch_id>
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MigrationQueueRequest'
|
||||
responses:
|
||||
"202":
|
||||
description: Job queued for AI processing
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MigrationQueuedResponse'
|
||||
"409":
|
||||
description: Duplicate — idempotency_key already exists
|
||||
|
||||
get:
|
||||
summary: List migration_review_queue (Admin only)
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [PENDING, IMPORTED, REJECTED]
|
||||
- name: batchId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 20
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated queue list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MigrationQueueListResponse'
|
||||
|
||||
/api/ai/migration/queue/{publicId}/approve:
|
||||
post:
|
||||
summary: Admin approves migration item → imports document
|
||||
description: |
|
||||
Imports document from temp path to DMS.
|
||||
Automatically queues embed-document job after import.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: publicId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"200":
|
||||
description: Document imported, embed job queued
|
||||
"400":
|
||||
description: Item not in PENDING status
|
||||
"404":
|
||||
description: Queue item not found
|
||||
|
||||
/api/ai/migration/queue/{publicId}/reject:
|
||||
post:
|
||||
summary: Admin rejects migration item
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: publicId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [reason]
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
maxLength: 500
|
||||
responses:
|
||||
"200":
|
||||
description: Item rejected
|
||||
"400":
|
||||
description: Item not in PENDING status
|
||||
|
||||
components:
|
||||
schemas:
|
||||
MigrationQueueRequest:
|
||||
type: object
|
||||
required: [batchId, filename, tempPath]
|
||||
properties:
|
||||
batchId:
|
||||
type: string
|
||||
description: n8n batch identifier
|
||||
filename:
|
||||
type: string
|
||||
tempPath:
|
||||
type: string
|
||||
description: Absolute path in temp storage on server
|
||||
|
||||
MigrationQueuedResponse:
|
||||
type: object
|
||||
properties:
|
||||
publicId:
|
||||
type: string
|
||||
format: uuid
|
||||
status:
|
||||
type: string
|
||||
enum: [PENDING]
|
||||
jobId:
|
||||
type: string
|
||||
description: BullMQ job ID for tracking
|
||||
|
||||
MigrationQueueListResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MigrationQueueItem'
|
||||
total:
|
||||
type: integer
|
||||
page:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
|
||||
MigrationQueueItem:
|
||||
type: object
|
||||
properties:
|
||||
publicId:
|
||||
type: string
|
||||
format: uuid
|
||||
batchId:
|
||||
type: string
|
||||
originalFilename:
|
||||
type: string
|
||||
aiMetadata:
|
||||
type: object
|
||||
description: AI-extracted metadata suggestion
|
||||
confidenceScore:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
ocrUsed:
|
||||
type: boolean
|
||||
status:
|
||||
type: string
|
||||
enum: [PENDING, IMPORTED, REJECTED]
|
||||
reviewedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
rejectionReason:
|
||||
type: string
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
@@ -0,0 +1,167 @@
|
||||
# Data Model: AI Model Revision (ADR-023A)
|
||||
|
||||
**Feature**: `302-ai-model-revision`
|
||||
**Date**: 2026-05-15
|
||||
|
||||
---
|
||||
|
||||
## Entities & Schema Changes
|
||||
|
||||
### 1. `ai_audit_logs` (existing table — verify against schema)
|
||||
|
||||
ไม่มีการเพิ่ม column ใหม่ — ใช้ `model_name` column ที่มีอยู่แล้วบันทึก `gemma4:e4b` แทน `gemma4:9b`
|
||||
|
||||
**Key fields (existing)**:
|
||||
```
|
||||
id INT AUTO_INCREMENT
|
||||
public_id BINARY(16) → UUIDv7
|
||||
document_id INT FK documents.id
|
||||
project_id INT FK projects.id
|
||||
job_type VARCHAR(50) -- 'classification', 'tagging', 'rag', 'embed'
|
||||
model_name VARCHAR(100) -- 'gemma4:e4b', 'nomic-embed-text'
|
||||
confidence_score DECIMAL(5,4) -- 0.0000 – 1.0000
|
||||
ai_suggestion_json JSON
|
||||
human_override_json JSON NULL
|
||||
status ENUM('PENDING','PROCESSING','DONE','REJECTED')
|
||||
created_at DATETIME
|
||||
updated_at DATETIME
|
||||
```
|
||||
|
||||
**Index needed**: `(project_id, status, created_at)` สำหรับ Admin Dashboard query
|
||||
|
||||
---
|
||||
|
||||
### 2. `migration_review_queue` (new table — ADR-009: SQL delta)
|
||||
|
||||
staging table สำหรับ Legacy Migration — เก็บ AI output รอ Admin review
|
||||
|
||||
```sql
|
||||
-- Delta file: specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql
|
||||
|
||||
CREATE TABLE migration_review_queue (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
public_id BINARY(16) NOT NULL DEFAULT (UUID_TO_BIN(UUID(), TRUE)),
|
||||
batch_id VARCHAR(100) NOT NULL, -- n8n batch identifier
|
||||
idempotency_key VARCHAR(200) NOT NULL UNIQUE, -- '<doc_number>:<batch_id>'
|
||||
original_filename VARCHAR(500) NOT NULL,
|
||||
storage_temp_path VARCHAR(1000) NOT NULL, -- path ใน temp storage ก่อน import
|
||||
ai_metadata_json JSON NOT NULL, -- AI suggestion (full)
|
||||
confidence_score DECIMAL(5,4) NOT NULL,
|
||||
ocr_used TINYINT(1) NOT NULL DEFAULT 0,
|
||||
status ENUM('PENDING','IMPORTED','REJECTED') NOT NULL DEFAULT 'PENDING',
|
||||
reviewed_by INT NULL, -- FK users.id (Admin who reviewed)
|
||||
reviewed_at DATETIME NULL,
|
||||
rejection_reason VARCHAR(500) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_idempotency (idempotency_key),
|
||||
INDEX idx_status_created (status, created_at),
|
||||
INDEX idx_batch (batch_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Qdrant Vector Structure (no DB change — external store)
|
||||
|
||||
Collection name: `lcbp3_documents` (shared collection, separated by payload filter)
|
||||
|
||||
**Point structure**:
|
||||
```json
|
||||
{
|
||||
"id": "<uuid-v7>",
|
||||
"vector": [/* 768-dim from nomic-embed-text */],
|
||||
"payload": {
|
||||
"document_public_id": "019505a1-...",
|
||||
"project_public_id": "019505a2-...",
|
||||
"chunk_index": 3,
|
||||
"page_number": 2,
|
||||
"chunk_text": "...",
|
||||
"document_type": "SHOP_DRAWING",
|
||||
"embedded_at": "2026-05-15T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-tenancy filter (REQUIRED)**:
|
||||
```typescript
|
||||
filter: {
|
||||
must: [
|
||||
{ key: 'project_public_id', match: { value: projectPublicId } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. BullMQ Job Payload Interfaces
|
||||
|
||||
**ai-realtime queue** (RAG Q&A, AI Suggest):
|
||||
```typescript
|
||||
interface AiRealtimeJobData {
|
||||
jobType: 'rag-query' | 'ai-suggest';
|
||||
documentPublicId: string; // UUIDv7
|
||||
projectPublicId: string; // UUIDv7 — required
|
||||
userId: number; // INT internal ID (for audit only)
|
||||
payload: {
|
||||
// ai-suggest: { pdfPath: string; pages: 1-3 }
|
||||
// rag-query: { question: string; topK: number }
|
||||
};
|
||||
idempotencyKey: string;
|
||||
}
|
||||
```
|
||||
|
||||
**ai-batch queue** (OCR, Extract, Embed):
|
||||
```typescript
|
||||
interface AiBatchJobData {
|
||||
jobType: 'ocr' | 'extract-metadata' | 'embed-document';
|
||||
documentPublicId: string; // UUIDv7
|
||||
projectPublicId: string; // UUIDv7 — required
|
||||
payload: {
|
||||
// ocr: { pdfPath: string }
|
||||
// extract-metadata: { textContent: string; maxPages: 3 }
|
||||
// embed-document: { pdfPath: string; chunkSize: 512; overlap: 64 }
|
||||
};
|
||||
batchId?: string; // สำหรับ Legacy Migration เท่านั้น
|
||||
idempotencyKey: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. State Transitions
|
||||
|
||||
**Document Upload Flow** (new documents):
|
||||
```
|
||||
upload → temp → [ClamAV] → commit → [
|
||||
parallel:
|
||||
→ ai-realtime: ai-suggest → (USER: confirm/edit) → ai_audit_logs
|
||||
→ ai-batch: embed-document → Qdrant
|
||||
]
|
||||
```
|
||||
|
||||
**Legacy Migration Flow**:
|
||||
```
|
||||
n8n trigger → POST /api/ai/migration/queue → [
|
||||
ai-batch: ocr (if needed) + extract-metadata
|
||||
] → migration_review_queue (PENDING)
|
||||
↓
|
||||
Admin review (DMS UI)
|
||||
↓
|
||||
IMPORTED → document insert → ai-batch: embed-document → Qdrant
|
||||
REJECTED → rejection_reason saved
|
||||
```
|
||||
|
||||
**ai-realtime ↔ ai-batch Coordination**:
|
||||
```
|
||||
ai-realtime.active → ai-batch.pause()
|
||||
ai-realtime.completed/failed → ai-batch.resume()
|
||||
ai-batch concurrency=1 (no parallel GPU jobs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Delta File
|
||||
|
||||
จะสร้างใน `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` ตาม ADR-009
|
||||
@@ -0,0 +1,156 @@
|
||||
# Implementation Plan: AI Model Revision (ADR-023A)
|
||||
|
||||
**Branch**: `main` | **Date**: 2026-05-15 | **Spec**: [spec.md](./spec.md)
|
||||
**Feature Dir**: `specs/300-others/302-ai-model-revision/`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Implement ADR-023A AI Architecture Revision: เปลี่ยน model stack จาก 3-model (gemma4:9b + Typhoon + nomic-embed-text) เป็น 2-model (gemma4:e4b Q8_0 + nomic-embed-text), แยก BullMQ เป็น 2 queues (`ai-realtime`/`ai-batch`), เพิ่ม OCR auto-detection, enforce multi-tenant QdrantService, implement Legacy Migration pipeline และ migration_review_queue, และลบ Typhoon Cloud API ออกจาก codebase ทั้งหมด
|
||||
|
||||
---
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.x (strict mode)
|
||||
**Primary Dependencies**:
|
||||
- Backend: NestJS 10, BullMQ 5, TypeORM 0.3, ioredis (Redis 7), @qdrant/js-client-rest
|
||||
- AI Infrastructure: Ollama (Desk-5439), PaddleOCR, PyMuPDF (Python sidecar)
|
||||
- Queue: Redis 7 (same instance as existing BullMQ)
|
||||
**Storage**: MariaDB (existing) + Qdrant (external vector DB) + Local Storage (existing)
|
||||
**Testing**: Jest (NestJS unit/integration)
|
||||
**Target Platform**: QNAP NAS (NestJS container) + Admin Desktop Desk-5439 (Ollama)
|
||||
**Performance Goals**: ai-suggest < 30s; rag-query < 10s (p95 dequeue-to-response)
|
||||
**Constraints**: VRAM ≤ 5GB peak, concurrency=1 per queue (prevent GPU overflow)
|
||||
**Scale/Scope**: ~20,000 legacy docs (migration), ~50 new docs/day (production)
|
||||
|
||||
---
|
||||
|
||||
## Constitution Check
|
||||
|
||||
_GATE: Must pass before Phase 0 research._
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| ADR-019 UUID: no parseInt on UUID | ✅ PASS | BullMQ payloads ใช้ `publicId: string` เสมอ |
|
||||
| ADR-009: no TypeORM migrations | ✅ PASS | `migration_review_queue` ผ่าน SQL delta (#14) |
|
||||
| ADR-016: RBAC on all endpoints | ✅ PASS | AI endpoints จะมี CASL guard: `ai.manage` |
|
||||
| ADR-007: error handling layered | ✅ PASS | BullMQ failed jobs → dead-letter + log |
|
||||
| ADR-008: BullMQ for async | ✅ PASS | Inference ทั้งหมดผ่าน BullMQ (ไม่มี inline) |
|
||||
| ADR-023/023A: no direct Ollama | ✅ PASS | n8n → DMS API → BullMQ → Ollama เท่านั้น |
|
||||
| ADR-023A: QdrantService required projectPublicId | ✅ PASS | Enforce ที่ TypeScript compile-time |
|
||||
| TypeScript strict: no `any`, no `console.log` | ✅ PASS | Enforced ผ่าน eslint |
|
||||
| **Typhoon Cloud API removal** | ⚠️ PENDING | `rag/typhoon.service.ts` ต้อง delete (T002) |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/300-others/302-ai-model-revision/
|
||||
├── spec.md ✅ done
|
||||
├── plan.md ✅ this file
|
||||
├── research.md ✅ done
|
||||
├── data-model.md ✅ done
|
||||
├── quickstart.md (Phase 1)
|
||||
├── contracts/ (Phase 1)
|
||||
│ ├── ai-jobs.yaml
|
||||
│ └── migration-queue.yaml
|
||||
├── checklists/
|
||||
│ └── requirements.md ✅ done
|
||||
└── tasks.md (Phase 2 — speckit-tasks)
|
||||
```
|
||||
|
||||
### Schema Delta (ADR-009)
|
||||
|
||||
```text
|
||||
specs/03-Data-and-Storage/deltas/
|
||||
└── 14-add-migration-review-queue.sql # new
|
||||
```
|
||||
|
||||
### Source Code
|
||||
|
||||
```text
|
||||
backend/src/modules/ai/
|
||||
├── ai.module.ts # update: register 2 queues, remove Typhoon
|
||||
├── ai.controller.ts # update: add /migration/queue endpoint
|
||||
├── ai.service.ts # update: routing logic, queue selection
|
||||
├── processors/
|
||||
│ ├── ai-realtime.processor.ts # new: ai-realtime consumer
|
||||
│ └── ai-batch.processor.ts # new: ai-batch consumer (replaces existing)
|
||||
├── services/
|
||||
│ ├── ollama.service.ts # update: model → gemma4:e4b
|
||||
│ ├── qdrant.service.ts # update: enforce projectPublicId param
|
||||
│ ├── ocr.service.ts # new: OCR auto-detect + PaddleOCR routing
|
||||
│ ├── migration.service.ts # new: Legacy Migration pipeline
|
||||
│ └── embedding.service.ts # new: full-doc chunking + embed
|
||||
├── dto/
|
||||
│ ├── create-ai-job.dto.ts # update: queue discriminator field
|
||||
│ ├── migration-queue-item.dto.ts # new
|
||||
│ └── rag-query.dto.ts # new
|
||||
├── entities/
|
||||
│ └── migration-review-queue.entity.ts # new
|
||||
└── rag/
|
||||
├── rag.service.ts # update: remove typhoon ref, use QdrantService
|
||||
└── typhoon.service.ts # DELETE ← Tier 1 critical
|
||||
|
||||
backend/src/config/
|
||||
└── bullmq.config.ts # update: add ai-batch queue config
|
||||
|
||||
frontend/app/(dashboard)/ai-staging/
|
||||
├── page.tsx # update: add migration queue tab
|
||||
└── migration-review/
|
||||
└── page.tsx # new: Admin Migration Review UI
|
||||
|
||||
frontend/components/ai/
|
||||
├── ai-suggestion-field.tsx # update: confidence threshold display
|
||||
├── migration-queue-table.tsx # new: queue list + approve/reject
|
||||
└── AiStatusBanner.tsx # update: show queue status (ai-batch paused)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0: Cleanup & Foundation (Tier 1 Critical First)
|
||||
|
||||
**Goal**: ลบ Typhoon ออก, ตั้ง BullMQ 2-queue, สร้าง Schema Delta
|
||||
|
||||
Tasks: T001–T008
|
||||
|
||||
### Phase 1: Core AI Pipeline
|
||||
|
||||
**Goal**: OCR auto-detect, gemma4:e4b integration, ai-suggest + embed-document flows
|
||||
|
||||
Tasks: T009–T022
|
||||
|
||||
### Phase 2: RAG Pipeline
|
||||
|
||||
**Goal**: QdrantService multi-tenancy, chunking, rag-query endpoint
|
||||
|
||||
Tasks: T023–T030
|
||||
|
||||
### Phase 3: Legacy Migration Pipeline
|
||||
|
||||
**Goal**: migration_review_queue, n8n API endpoint, Admin Review UI
|
||||
|
||||
Tasks: T031–T042
|
||||
|
||||
### Phase 4: Monitoring & Threshold Management
|
||||
|
||||
**Goal**: Admin Dashboard AI metrics, threshold config, audit log delete permission
|
||||
|
||||
Tasks: T043–T050
|
||||
|
||||
---
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|-----------|-------------------------------------|
|
||||
| 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 |
|
||||
@@ -0,0 +1,113 @@
|
||||
# Quickstart: AI Model Revision (ADR-023A)
|
||||
|
||||
**Feature**: `302-ai-model-revision`
|
||||
**Date**: 2026-05-15
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Desk-5439 มี Ollama พร้อมใช้งาน: `ollama list` ต้องแสดง `gemma4:e4b` และ `nomic-embed-text`
|
||||
2. Qdrant instance running: `http://QDRANT_HOST:6333/health` → `{"status":"ok"}`
|
||||
3. Redis 7 running (ใช้ instance เดิมกับ existing BullMQ)
|
||||
4. `@qdrant/js-client-rest` ต้องติดตั้งใน backend: `npm ls @qdrant/js-client-rest`
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables (เพิ่มใน `backend/.env`)
|
||||
|
||||
```env
|
||||
# AI Infrastructure
|
||||
OLLAMA_HOST=http://192.168.10.XX:11434
|
||||
OLLAMA_MODEL_MAIN=gemma4:e4b
|
||||
OLLAMA_MODEL_EMBED=nomic-embed-text
|
||||
|
||||
# Qdrant
|
||||
QDRANT_HOST=http://192.168.10.XX:6333
|
||||
QDRANT_COLLECTION=lcbp3_documents
|
||||
|
||||
# OCR
|
||||
OCR_CHAR_THRESHOLD=100
|
||||
OCR_API_URL=http://localhost:8765 # PaddleOCR sidecar
|
||||
|
||||
# BullMQ Queue Names (for reference — hardcoded in code)
|
||||
# ai-realtime: RAG Q&A, AI Suggest
|
||||
# ai-batch: OCR, Extract, Embed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Scenarios (สำหรับ QA / UAT)
|
||||
|
||||
### Scenario 1: Digital PDF Classification (Fast Path)
|
||||
|
||||
```bash
|
||||
# 1. Upload a digital PDF via RFA form
|
||||
# 2. Monitor BullMQ dashboard (if Bull Board installed)
|
||||
# Expected: ai-realtime queue → ai-suggest job → completed within 30s
|
||||
# Expected: ai-batch queue → embed-document job → completed in background
|
||||
curl -H "Authorization: Bearer $TOKEN" http://localhost:3001/api/ai/jobs/status/$JOB_ID
|
||||
```
|
||||
|
||||
### Scenario 2: Scanned PDF (OCR Path)
|
||||
|
||||
```bash
|
||||
# 1. Upload a scanned (image-only) PDF
|
||||
# Expected: ai-batch queue → ocr job first → then ai-suggest
|
||||
# OCR detection: extracted_chars < 100 per page triggers slow path
|
||||
```
|
||||
|
||||
### Scenario 3: RAG Query (Multi-tenancy Check)
|
||||
|
||||
```bash
|
||||
# 1. Embed 2 docs in Project A, 1 doc in Project B
|
||||
# 2. Query from Project A context
|
||||
# Expected: results contain ONLY Project A docs
|
||||
curl -X POST http://localhost:3001/api/ai/rag/query \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"projectPublicId":"<project-a-uuid>","question":"ค้นหา shop drawing หมายเลข SD-001"}'
|
||||
```
|
||||
|
||||
### Scenario 4: Legacy Migration (Admin Only)
|
||||
|
||||
```bash
|
||||
# 1. Trigger via n8n (or direct API for testing):
|
||||
curl -X POST http://localhost:3001/api/ai/migration/queue \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Idempotency-Key: SD-001-2026:batch-001" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"batchId":"batch-001","filename":"SD-001.pdf","tempPath":"/uploads/temp/SD-001.pdf"}'
|
||||
# 2. Check migration_review_queue: status = PENDING
|
||||
# 3. Admin Approve from UI → status = IMPORTED
|
||||
```
|
||||
|
||||
### Scenario 5: Typhoon Removal Verification
|
||||
|
||||
```bash
|
||||
# After implementation, run:
|
||||
grep -r "typhoon" backend/src --include="*.ts"
|
||||
# Expected: NO results (file should be deleted)
|
||||
```
|
||||
|
||||
### Scenario 6: GPU Overload Prevention
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files to Modify (Priority Order)
|
||||
|
||||
| Priority | File | Change |
|
||||
|---------|------|--------|
|
||||
| 🔴 CRITICAL | `backend/src/modules/ai/rag/typhoon.service.ts` | DELETE |
|
||||
| 🔴 CRITICAL | `backend/src/config/bullmq.config.ts` | Add `ai-batch` queue |
|
||||
| 🔴 CRITICAL | `specs/03-Data-and-Storage/deltas/14-add-migration-review-queue.sql` | CREATE |
|
||||
| 🟡 HIGH | `backend/src/modules/ai/services/qdrant.service.ts` | Enforce `projectPublicId` param |
|
||||
| 🟡 HIGH | `backend/src/modules/ai/processors/ai-batch.processor.ts` | NEW — replace old processor |
|
||||
| 🟡 HIGH | `backend/src/modules/ai/services/ocr.service.ts` | NEW — auto-detect routing |
|
||||
| 🟢 NORMAL | `frontend/app/(dashboard)/ai-staging/page.tsx` | Add Migration Queue tab |
|
||||
@@ -0,0 +1,86 @@
|
||||
# Research: AI Model Revision (ADR-023A)
|
||||
|
||||
**Feature**: `302-ai-model-revision`
|
||||
**Date**: 2026-05-15
|
||||
**Status**: Complete — all decisions validated via Grilling Session
|
||||
|
||||
---
|
||||
|
||||
## Decision 1: Model Stack Reduction
|
||||
|
||||
- **Decision**: ใช้ 2-model stack: `gemma4:e4b Q8_0` + `nomic-embed-text` แทน 3-model stack เดิม
|
||||
- **Rationale**: VRAM budget RTX 2060 Super 8GB — 3-model stack (gemma4:9b + Typhoon + nomic-embed-text) ใช้ ~7.8GB ไม่มี headroom; 2-model stack ใช้ ~4.5GB peak มี headroom ~3.5GB
|
||||
- **Alternatives considered**:
|
||||
- gemma4:9b + nomic-embed-text (ไม่มี Typhoon): ยังเกิน budget ~6.8GB
|
||||
- gemma4:e4b Q4_K_M (quantize ต่ำกว่า): ประหยัด VRAM มากกว่าแต่คุณภาพต่ำกว่า Q8_0
|
||||
- ย้ายไปใช้ Cloud AI: ขัดกับ ADR-023 (INTERNAL data — ห้าม Cloud)
|
||||
- **VRAM Detail**: gemma4:e4b Q8_0 = ~4.0GB weights + ~0.2GB KV Cache (จำกัดโดย 3-page input limit) + nomic-embed-text ~0.3GB = **~4.5GB peak**
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: BullMQ 2-Queue Architecture
|
||||
|
||||
- **Decision**: แยกเป็น 2 Queues: `ai-realtime` (concurrency=1) + `ai-batch` (concurrency=1) พร้อม auto-pause mechanism
|
||||
- **Rationale**: Single queue ทำให้ RAG Q&A (interactive, p95 < 10s) ถูก block โดย OCR/Embed batch jobs (ไม่มี SLA); 2-queue ให้ priority separation โดยไม่เพิ่ม Worker ที่ทำให้ VRAM overflow
|
||||
- **Alternatives considered**:
|
||||
- Single queue + priority field: priority ใน BullMQ ไม่ป้องกัน long-running job ที่กำลังรันอยู่ block queue ถัดไป
|
||||
- 2 Queues + 2 Workers พร้อมกัน: VRAM overflow เมื่อทั้งคู่ใช้ gemma4:e4b พร้อมกัน
|
||||
- **Implementation**: BullMQ `active` event บน `ai-realtime` → pause `ai-batch`; `completed`/`failed` → resume `ai-batch`
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: PDF Input Strategy
|
||||
|
||||
- **Decision**: 3-page limit สำหรับ Classification/Tagging; Full-document chunking สำหรับ RAG Embedding
|
||||
- **Rationale**: Engineering docs มีข้อมูล metadata หลักในหน้าแรก 1-3 (Title Block, Drawing No., Revision); Full-doc embed ไม่กระทบ VRAM เพราะ nomic-embed-text ประมวลผล chunk ละ 512 tokens (stateless)
|
||||
- **Alternatives considered**:
|
||||
- Full-doc ทั้ง Classification และ Embed: กระทบ VRAM และ SLA ของ Classification
|
||||
- 3-page ทั้ง Classification และ Embed: RAG ไม่เจอเนื้อหาในหน้าท้าย — useless สำหรับเอกสาร 50+ หน้า
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: OCR Auto-Detection
|
||||
|
||||
- **Decision**: ตรวจสอบ `extracted_chars > OCR_CHAR_THRESHOLD (100)` ต่อหน้าด้วย PyMuPDF ก่อน route
|
||||
- **Rationale**: ทั้ง Legacy และ New Upload อาจมีทั้ง Scanned และ Digital — auto-detect ดีกว่า user-select เพราะ user ไม่รู้ว่า PDF ตัวเองเป็นแบบไหน; threshold 100 chars ป้องกัน watermark-only PDF ถูก classify ผิด
|
||||
- **Alternatives considered**:
|
||||
- User เลือก pipeline: UX แย่ + error prone
|
||||
- ใช้ OCR ทุกไฟล์เสมอ: ช้าเกินไปสำหรับ Digital PDF
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: n8n ↔ BullMQ Boundary
|
||||
|
||||
- **Decision**: n8n call `POST /api/ai/jobs` (DMS API) → BullMQ queue; ไม่เรียก Ollama โดยตรง
|
||||
- **Rationale**: ถ้า n8n bypass BullMQ → ai_audit_logs ไม่ถูกบันทึก + ไม่มี RBAC check + ไม่มี ADR-007 error handling; DMS API เป็น single gateway ที่ enforce ทุก cross-cutting concern
|
||||
- **Alternatives considered**:
|
||||
- n8n HTTP Request node → Ollama API: bypass ทั้ง audit, RBAC, error handling
|
||||
- n8n Execute Command → Python script → Ollama: อันตราย, ไม่มี audit trail
|
||||
|
||||
---
|
||||
|
||||
## Decision 6: QdrantService Multi-Tenancy Enforcement
|
||||
|
||||
- **Decision**: `QdrantService.search(projectPublicId: string, ...)` — required param, ห้าม expose `rawSearch()`
|
||||
- **Rationale**: ถ้า developer ลืม filter → ข้อมูลข้ามโครงการรั่วไหล (INTERNAL data sensitivity); compile-time enforcement ดีกว่า runtime guard
|
||||
- **Alternatives considered**:
|
||||
- Middleware filter: ยังต้องใช้ Service method — ป้องกันได้น้อยกว่า
|
||||
- Optional parameter with default: ยังมีโอกาส pass undefined ได้
|
||||
|
||||
---
|
||||
|
||||
## Decision 7: Threshold Recalibration Policy
|
||||
|
||||
- **Decision**: ใช้ค่าเริ่มต้น 0.85/0.60 สำหรับ Migration Phase แรก แล้ว recalibrate หลัง 100-500 ฉบับแรก
|
||||
- **Rationale**: ค่าเดิมถูกกำหนดในยุค gemma4:9b — distribution อาจเปลี่ยนไปกับ gemma4:e4b; recalibrate จาก real data ดีกว่า hardcode ค่าใหม่โดยไม่มีข้อมูล
|
||||
- **Trigger**: REJECTED rate > 30% หรือ Admin override rate > 40% → ปรับลด threshold
|
||||
|
||||
---
|
||||
|
||||
## Unvalidated Assumptions (Risk Register)
|
||||
|
||||
| Assumption | Risk | Mitigation |
|
||||
|-----------|------|-----------|
|
||||
| gemma4:e4b Q8_0 รองรับภาษาไทยได้ดีเพียงพอ | HIGH — ไม่มีหลักฐานเชิงคุณภาพ | ทดสอบ 50-100 ฉบับก่อน Go-live; เตรียม Prompt Engineering ชดเชย |
|
||||
| 3-page limit เพียงพอสำหรับ metadata extraction | MEDIUM — บางเอกสารอาจมี title block หน้า 4+ | ตรวจสอบตัวอย่างเอกสาร 20 ฉบับก่อน implementation |
|
||||
| RTX 2060 Super VRAM ใช้ได้ 8GB เต็ม | LOW — GPU อาจมี overhead จาก OS และ driver | monitor จริงด้วย `nvidia-smi` ระหว่าง UAT |
|
||||
@@ -0,0 +1,157 @@
|
||||
# Feature Specification: AI Model Revision (ADR-023A)
|
||||
|
||||
**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
|
||||
**ADR Source**: `specs/06-Decision-Records/ADR-023A-unified-ai-architecture.md`
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing _(mandatory)_
|
||||
|
||||
### User Story 1 — AI-Assisted Document Classification on Upload (Priority: P1)
|
||||
|
||||
เมื่อ User อัปโหลดเอกสาร (PDF) ผ่าน RFA หรือ Correspondence form ระบบต้องตรวจสอบอัตโนมัติว่าไฟล์เป็น Scanned หรือ Digital PDF จากนั้นสกัด Metadata (Document Type, Discipline, Project Code, Revision) โดยใช้ AI และนำเสนอผล Suggestion บนฟอร์ม เพื่อให้ผู้ใช้ยืนยันหรือแก้ไขก่อนบันทึก
|
||||
|
||||
**Why this priority**: เป็น Core User-facing Feature ที่สร้างคุณค่าหลักของระบบ AI — ผู้ใช้ทุกคนที่อัปโหลดเอกสารได้รับประโยชน์ทันที
|
||||
|
||||
**Independent Test**: อัปโหลด PDF → ระบบแสดง AI Suggestion ใน 30 วินาที → User กด Confirm → เอกสารบันทึกพร้อม Metadata
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** User อัปโหลด Digital PDF (มี text layer), **When** ระบบ commit ไฟล์, **Then** ระบบ route ไป Fast Path (ไม่ใช้ OCR) และแสดง AI Suggestion ภายใน 15 วินาที
|
||||
2. **Given** User อัปโหลด Scanned PDF (image-only), **When** ระบบ commit ไฟล์, **Then** ระบบ route ไป PaddleOCR และแสดง AI Suggestion ภายใน 60 วินาที
|
||||
3. **Given** AI Suggestion มี confidence ≥ 0.85, **When** แสดงบนฟอร์ม, **Then** Suggestion ถูก pre-fill และไฮไลต์สีเขียว พร้อมปุ่ม Confirm
|
||||
4. **Given** AI Suggestion มี confidence ระหว่าง 0.60–0.84, **When** แสดงบนฟอร์ม, **Then** Suggestion แสดงพร้อม badge ⚠️ "ตรวจสอบก่อนยืนยัน"
|
||||
5. **Given** AI Suggestion มี confidence < 0.60, **When** แสดงบนฟอร์ม, **Then** ฟิลด์ว่างเปล่า — ให้ User กรอกเอง
|
||||
6. **Given** AI Service ไม่พร้อมใช้งาน (Desk-5439 ออฟไลน์), **When** User อัปโหลด, **Then** ระบบ fallback — บันทึกเอกสารได้ปกติ แสดง warning "AI ไม่พร้อม กรอก Metadata เอง"
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 — RAG-based Document Q&A (Priority: P2)
|
||||
|
||||
User สามารถถามคำถามภาษาธรรมชาติ (ไทย/อังกฤษ) เกี่ยวกับเอกสารในโครงการ และได้รับคำตอบจาก AI พร้อม citation ว่าข้อมูลมาจากหน้าไหนของเอกสารใด โดยข้อมูลถูกจำกัดเฉพาะโครงการที่ User มีสิทธิ์เข้าถึง
|
||||
|
||||
**Why this priority**: เพิ่มประสิทธิภาพการค้นหาข้อมูลในเอกสาร เฉพาะกลุ่ม Power User ที่จำเป็น — รองจาก P1
|
||||
|
||||
**Independent Test**: ถามคำถาม → ได้คำตอบพร้อม citation ภายใน 10 วินาที → คำตอบมาจากเอกสารในโครงการเดียวกันเท่านั้น
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** User อยู่ในโครงการ A, **When** ส่งคำถาม RAG, **Then** คำตอบมาจากเอกสารในโครงการ A เท่านั้น (ไม่มีข้อมูลข้ามโครงการ)
|
||||
2. **Given** เอกสารเพิ่งถูก commit (< 5 นาที), **When** User ถาม RAG, **Then** ระบบแจ้ง "เอกสารใหม่อาจยังไม่อยู่ใน index — ค้นหาผ่านระบบปกติก่อน"
|
||||
3. **Given** RAG Q&A ใช้เวลา > 10 วินาที (p95), **When** ผ่าน SLA, **Then** Job ถูก flag ใน ai_audit_logs และ Admin รับแจ้งเตือน
|
||||
4. **Given** ไม่พบเนื้อหาที่เกี่ยวข้องใน Qdrant, **When** ค้นหา, **Then** ตอบ "ไม่พบข้อมูลในเอกสารที่อยู่ใน index — ลองค้นหาด้วยคำอื่น"
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 — Legacy Document Migration with AI Processing (Priority: P3)
|
||||
|
||||
Admin สามารถ trigger การนำเข้าเอกสาร Legacy (~20,000 ฉบับ) ผ่าน n8n โดย AI ประมวลผล OCR + Metadata อัตโนมัติ ผล Suggestion จะปรากฏใน Queue ที่ Admin Review ผ่าน DMS Frontend เพื่อ Approve หรือ Reject ก่อน Import
|
||||
|
||||
**Why this priority**: เป็น One-time Pre-launch activity — สำคัญแต่ไม่กระทบ Production User โดยตรง
|
||||
|
||||
**Independent Test**: trigger batch 10 ฉบับใน n8n → ดู migration_review_queue → Approve 5 ฉบับ → ตรวจสอบว่า 5 ฉบับ import สำเร็จและ embed ใน Qdrant
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Admin วางไฟล์ใน Folder และ trigger n8n, **When** Batch ประมวลผลเสร็จ, **Then** migration_review_queue มี record สถานะ PENDING สำหรับทุกไฟล์
|
||||
2. **Given** Admin Approve record ใน DMS Frontend, **When** กด Approve, **Then** เอกสาร Import เข้า DMS และ embed-document job ถูก queue อัตโนมัติ
|
||||
3. **Given** มีการส่ง batch เดิมซ้ำ (Idempotency), **When** n8n trigger อีกครั้งพร้อม Idempotency-Key เดิม, **Then** ไม่มี record ซ้ำใน migration_review_queue
|
||||
4. **Given** AI Confidence < 0.60 สำหรับ record, **When** แสดงใน Queue, **Then** record ถูก mark REJECTED อัตโนมัติ — Admin ต้อง Approve ด้วยตนเองหากต้องการ import
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 — AI Performance Monitoring and Threshold Management (Priority: P4)
|
||||
|
||||
Admin สามารถดู AI Performance metrics จาก ai_audit_logs (confidence distribution, override rate) และปรับ Confidence Threshold ผ่าน environment configuration เพื่อ recalibrate ระบบหลังจากได้ข้อมูลจริงจาก Migration Phase แรก
|
||||
|
||||
**Why this priority**: Operational concern — จำเป็นหลัง Go-live แต่ไม่บล็อก Launch
|
||||
|
||||
**Independent Test**: ดู Dashboard แสดง confidence score distribution → เปรียบเทียบกับ Admin override rate → ปรับ ENV → restart service → ตรวจสอบ threshold ใหม่มีผล
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** Admin เข้า /admin/ai-staging, **When** ดู dashboard, **Then** เห็น avg confidence, override rate, rejected rate แยกตาม document_type
|
||||
2. **Given** REJECTED rate > 30%, **When** Admin ต้องการปรับ threshold, **Then** ระบบแสดงคำแนะนำ threshold ใหม่พร้อม rationale
|
||||
3. **Given** Admin ลบ ai_audit_logs record (test data), **When** ลบ, **Then** การลบถูกบันทึกใน audit_logs ด้วย action: 'AI_AUDIT_LOG_DELETED'
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- ถ้า PDF > 50MB (upload limit) → reject ก่อนถึง AI pipeline
|
||||
- ถ้า 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 ยังทำงาน
|
||||
- เอกสารถูก delete จาก DMS → ต้อง delete chunks ออกจาก Qdrant ด้วย (document_public_id filter)
|
||||
|
||||
---
|
||||
|
||||
## Requirements _(mandatory)_
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: ระบบ MUST ตรวจจับประเภท PDF (Digital vs Scanned) อัตโนมัติโดยใช้ `extracted_chars > OCR_CHAR_THRESHOLD` โดยไม่ให้ User เลือก
|
||||
- **FR-002**: ระบบ MUST ส่ง PDF เข้า gemma4:e4b สูงสุด 3 หน้าแรกเท่านั้น สำหรับงาน Classification และ Tagging
|
||||
- **FR-003**: ระบบ MUST ฝัง Vector จากเอกสารทั้งฉบับ (full-document chunking) สำหรับ RAG — ไม่จำกัด 3 หน้า
|
||||
- **FR-004**: AI Inference ทั้งหมด MUST ผ่าน BullMQ Worker บน NestJS — ห้าม n8n เรียก Ollama โดยตรง
|
||||
- **FR-005**: `QdrantService.search()` MUST รับ `projectPublicId: string` เป็น required parameter เสมอ
|
||||
- **FR-006**: `embed-document` MUST ถูก queue อัตโนมัติหลัง document commit (parallel กับ AI Suggestion) — ห้าม manual trigger
|
||||
- **FR-007**: BullMQ MUST มี 2 queues แยกกัน: `ai-realtime` (RAG Q&A, AI Suggest) และ `ai-batch` (OCR, Extract, Embed) ทั้งคู่ concurrency=1
|
||||
- **FR-008**: ระบบ MUST pause `ai-batch` อัตโนมัติเมื่อ `ai-realtime` มี active job; MUST resume `ai-batch` เมื่อ `ai-realtime` job completed **หรือ** failed (ไม่ว่า outcome ใด) — ห้าม `ai-batch` ค้างสถานะ paused ตลอดไป (ผ่าน BullMQ Event hooks: `active`, `completed`, `failed`)
|
||||
- **FR-009**: Legacy Migration MUST ใช้ Idempotency-Key `<doc_number>:<batch_id>` ป้องกันบันทึกซ้ำ
|
||||
- **FR-010**: ระบบ MUST บันทึกทุก AI interaction ใน `ai_audit_logs` รวมถึง confidence_score, model_name, ai_suggestion_json, human_override_json
|
||||
- **FR-011**: การ Delete ai_audit_logs MUST บันทึกใน `audit_logs` (`action: 'AI_AUDIT_LOG_DELETED'`) และเฉพาะ SYSTEM_ADMIN เท่านั้น
|
||||
- **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-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)
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **AiJob**: Job ใน BullMQ (`ai-realtime` / `ai-batch`), มี jobType, documentPublicId, projectPublicId, status, result
|
||||
- **AiAuditLog**: บันทึก AI interaction รวม confidence_score, model_name, human_override_json (ดู Table `ai_audit_logs`)
|
||||
- **MigrationReviewQueue**: staging สำหรับ Legacy Migration (ดู Table `migration_review_queue`) — status: PENDING → IMPORTED | REJECTED
|
||||
- **QdrantChunk**: Vector chunk ใน Qdrant, มี payload: `{document_public_id, project_public_id, page_number, chunk_index}`
|
||||
- **DocumentEmbedding**: metadata ของ embedded document ใน DMS DB (linked กับ Qdrant collection)
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria _(mandatory)_
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **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-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 ฉบับแรก)
|
||||
- **SC-007**: ไม่มี Typhoon Cloud API ปรากฏใน codebase หลัง implementation (ตรวจสอบด้วย grep)
|
||||
- **SC-008**: ai_audit_logs ทุก record มี confidence_score และ model_name ไม่เป็น null
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Desk-5439 พร้อมใช้งานและมี Ollama ที่ติดตั้ง `gemma4:e4b Q8_0` และ `nomic-embed-text` แล้ว
|
||||
- Qdrant instance พร้อมใช้งานและ accessible จาก NestJS backend
|
||||
- n8n instance สามารถ call DMS API ผ่าน HTTP ได้
|
||||
- PaddleOCR ติดตั้งบน Desk-5439 พร้อมรองรับภาษาไทย
|
||||
- `OCR_CHAR_THRESHOLD` default = 100 chars ต่อหน้า (ปรับได้ผ่าน .env)
|
||||
- เอกสาร Legacy อยู่ใน Folder ที่ n8n เข้าถึงได้
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-05-15
|
||||
- Q: RAG embedding scope — embed ทั้งฉบับหรือแค่ 3 หน้า? → A: ทั้งฉบับ (chunked 512t/64t overlap) — 3-page limit ใช้เฉพาะ Classification/Tagging
|
||||
- Q: embed-document trigger timing → A: AUTO ทันทีหลัง commit (parallel กับ AI Suggestion), ไม่รอ Human confirm
|
||||
- Q: n8n role → A: n8n call DMS API เท่านั้น (`POST /api/ai/jobs`) — ไม่เรียก Ollama/Qdrant โดยตรง
|
||||
- Q: QdrantService enforcement → A: `projectPublicId: string` เป็น required param — ไม่มี optional fallback
|
||||
- Q: OCR scope → A: Auto-detect ทั้ง Legacy และ New Uploads ด้วย PyMuPDF
|
||||
@@ -0,0 +1,208 @@
|
||||
# Tasks: AI Model Revision (ADR-023A)
|
||||
|
||||
**Input**: `specs/300-others/302-ai-model-revision/` (spec.md, plan.md, data-model.md, contracts/)
|
||||
**Feature**: `302-ai-model-revision` | **Date**: 2026-05-15
|
||||
**Prerequisites**: plan.md ✅ | spec.md ✅ | research.md ✅ | data-model.md ✅ | contracts/ ✅
|
||||
|
||||
---
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: สามารถรันพร้อมกัน (คนละไฟล์, ไม่มี dependency กัน)
|
||||
- **[US1/US2/US3/US4]**: User Story ที่ task นี้ satisfy
|
||||
- ทุก task ต้องระบุ file path ที่แน่ชัด
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup & Cleanup (Tier 1 Critical First)
|
||||
|
||||
**Purpose**: ลบ Typhoon ออก, ตั้ง BullMQ 2-queue, สร้าง Schema Delta
|
||||
|
||||
**⚠️ 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"` ต้องไม่มีผล
|
||||
|
||||
**Checkpoint**: `grep -r "typhoon"` → 0 results; `bullmq.config.ts` มี 2 queues; delta file สร้างแล้ว
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Core AI infrastructure ที่ทุก User Story ต้องการ
|
||||
|
||||
**⚠️ 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`)
|
||||
|
||||
**Checkpoint**: NestJS compile สำเร็จ ไม่มี TypeScript error; QdrantService ไม่มี method ที่ไม่รับ projectPublicId
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 — AI-Assisted Document Classification (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Digital/Scanned PDF detection + AI Suggest metadata + frontend display
|
||||
|
||||
**Independent Test**: อัปโหลด PDF → AI Suggestion ปรากฏบนฟอร์มภายใน 30s
|
||||
|
||||
### 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
|
||||
|
||||
**Checkpoint**: อัปโหลด Digital PDF → AI Suggestion ใน 30s; อัปโหลด Scanned PDF → Suggestion ใน 90s; ai_audit_logs มี record ใหม่
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 — RAG-based Document Q&A (Priority: P2)
|
||||
|
||||
**Goal**: Full-document chunked embedding + projectPublicId isolation + RAG query endpoint
|
||||
|
||||
**Independent Test**: embed 2 docs ใน Project A, query → ได้เฉพาะ Project A; latency < 10s
|
||||
|
||||
### 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
|
||||
|
||||
**Checkpoint**: RAG query ตอบกลับ < 10s; ผล isolate ตาม projectPublicId; Qdrant ไม่มีข้อมูลข้ามโครงการ
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 — Legacy Migration Pipeline (Priority: P3)
|
||||
|
||||
**Goal**: n8n → DMS API → migration_review_queue → Admin Review UI
|
||||
|
||||
**Independent Test**: POST /api/ai/migration/queue → queue item PENDING → Admin Approve → document imported + embed queued
|
||||
|
||||
### 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 ครั้งที่สอง)
|
||||
|
||||
**Checkpoint**: n8n สามารถ POST และได้ 202; Admin เห็น queue ใน UI; Approve → document import + embed queued
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: User Story 4 — AI Monitoring & Threshold Management (Priority: P4)
|
||||
|
||||
**Goal**: Admin dashboard AI metrics + threshold recalibration + ai_audit_logs delete permission
|
||||
|
||||
**Independent Test**: Admin ดู dashboard → เห็น confidence distribution; Admin ลบ test log → audit_logs บันทึก
|
||||
|
||||
### 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'
|
||||
|
||||
**Checkpoint**: Admin dashboard แสดง metrics; delete audit log บันทึกใน audit_logs; threshold guidance แสดงถูกต้อง
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Polish & Cross-Cutting Concerns
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Phase 1 (Setup)**: ไม่มี dependency — เริ่มได้ทันที; **MUST** เสร็จก่อนทุก Phase
|
||||
- **Phase 2 (Foundation)**: ขึ้นอยู่กับ Phase 1 — BLOCKS ทุก User Story
|
||||
- **Phase 3 (US1 - P1)**: ขึ้นอยู่กับ Phase 2 — MVP สำคัญที่สุด
|
||||
- **Phase 4 (US2 - P2)**: ขึ้นอยู่กับ Phase 2; ใช้ `EmbeddingService` และ `QdrantService` จาก Phase 2
|
||||
- **Phase 5 (US3 - P3)**: ขึ้นอยู่กับ Phase 2; อาจรันพร้อม Phase 4 ได้ (คนละ service)
|
||||
- **Phase 6 (US4 - P4)**: ขึ้นอยู่กับ Phase 3 (ต้องมี ai_audit_logs data)
|
||||
- **Phase 7 (Polish)**: ขึ้นอยู่กับทุก Phase ก่อนหน้า
|
||||
|
||||
### User Story Dependencies (Internal)
|
||||
|
||||
- **US1**: T013 → T014 → T015, T016 (parallel); T019 ต้องหลัง T014
|
||||
- **US2**: T021 → T022; T023 → T024 → T025; T026, T027 parallel กับ T025
|
||||
- **US3**: T029 → T030 → T031; T032, T033 parallel กับ T031
|
||||
- **US4**: T036, T037 parallel; T038, T039 parallel กับ T036
|
||||
|
||||
### Parallel Opportunities per Phase
|
||||
|
||||
**Phase 1**: T002 ‖ T003 ‖ T004 (ทำพร้อมกันได้)
|
||||
**Phase 2**: T008 ‖ T009 ‖ T010 ‖ T012 (ทำพร้อมกันได้หลัง T006, T007, T011)
|
||||
**Phase 3**: T017 ‖ T018 พร้อมกัน; T019 ต้องหลัง T014
|
||||
**Phase 4**: T021 ‖ T023 พร้อมกัน (คนละ service); T026 ‖ T027 พร้อมกัน
|
||||
**Phase 5**: T032 ‖ T033 ‖ T034 พร้อมกัน (frontend tasks)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP Scope (Phase 1 + 2 + 3 เท่านั้น)
|
||||
|
||||
1. Phase 1: ลบ Typhoon, ตั้ง 2-queue, สร้าง delta → **Tier 1 Critical ✅**
|
||||
2. Phase 2: Foundation AI infrastructure → **Core engine ready**
|
||||
3. Phase 3: US1 - Document Classification → **User value delivered**
|
||||
4. **STOP และ VALIDATE**: ทดสอบ AI Suggestion flow end-to-end
|
||||
5. Deploy MVP ถ้าพร้อม
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
- Phase 3 done → MVP: Classification on Upload ✅
|
||||
- Phase 4 done → RAG Q&A ✅
|
||||
- Phase 5 done → Migration Pipeline ✅ (Pre-launch)
|
||||
- Phase 6 done → Admin Monitoring ✅
|
||||
- Phase 7 done → Complete ✅
|
||||
|
||||
---
|
||||
|
||||
## Metrics
|
||||
|
||||
- **Total Tasks**: 47 tasks (T001–T047)
|
||||
- **Phase 1 (Setup)**: 5 tasks
|
||||
- **Phase 2 (Foundation)**: 7 tasks
|
||||
- **Phase 3 (US1 P1)**: 8 tasks
|
||||
- **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%)
|
||||
- **MVP Scope**: 20 tasks (Phase 1+2+3)
|
||||
Reference in New Issue
Block a user