Files
lcbp3/specs/03-Data-and-Storage/03-07-OpenRAG.md
T
admin 81f9609ca2
Build and Deploy / deploy (push) Successful in 1m19s
260313:1701 20260313:1700 OpenRAG
2026-03-13 17:01:49 +07:00

932 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03-07: RAG (Retrieval-Augmented Generation) — Future Architecture Spec
**Document ID:** DMS-RAG-001
**Status:** Draft / Exploratory
**Version:** 1.8.1
**Date:** 2026-03-13
**Related Documents:**
- [ADR-017: Ollama Data Migration](../06-Decision-Records/ADR-017-ollama-data-migration.md)
- [ADR-018: AI Boundary Hardening](../06-Decision-Records/Patch%201.8.1.md)
- [n8n Migration Setup Guide](./03-05-n8n-migration-setup-guide.md)
- [Legacy Data Migration](./03-04-legacy-data-migration.md)
- [OpenRAG (openr.ag)](https://www.openr.ag/) — IBM open-source RAG: Docling + OpenSearch + Langflow
> ⚠️ **หมายเหตุ:** เอกสารนี้ออกแบบ RAG Pipeline **2 ส่วน**:
> 1. **OpenRAG (Extraction Phase)** — ทำหน้าที่ "พนักงานคัดกรองข้อมูล" อ่าน PDF ทั้ง Folder แล้วเขียน JSON ลง `rag-output/` บน Shared NAS
> 2. **n8n + Ollama + Elasticsearch (Integration & Search Phase)** — Poll ไฟล์ JSON จาก `rag-output/` ทีละไฟล์ แล้วนำเข้า DMS
>
> ทั้งหมดทำงาน **On-Premise** เท่านั้น — ไม่ส่งข้อมูลออกนอกเครือข่าย (ADR-018 AI Isolation)
>
> **Integration Model: File-based Queue (Pull)**
> - Admin Desktop mount `R:\` (Drive Letter) → QNAP NAS Shared Folder (`staging_ai`)
> - OpenRAG เขียน JSON ลง `R:\staging_ai\rag-output\` → n8n อ่านจาก `staging_ai/rag-output/`
> - **ไม่มี HTTP ระหว่าง OpenRAG กับ n8n** — NAS Folder เป็น Shared Queue
---
## 🎯 วัตถุประสงค์ (Objective)
เพิ่มความสามารถ **Semantic Search และ Document Q&A** ให้กับระบบ DMS โดยใช้ Infrastructure ที่มีอยู่แล้ว:
- ไม่ส่งข้อมูลออกนอกเครือข่ายองค์กร (Data Privacy)
- ไม่มีค่าใช้จ่ายต่อ Query (Zero Cost)
- ต่อยอดจากสถาปัตยกรรม Migration ที่ผ่าน Validate แล้ว (ADR-017/018)
---
## 🏗️ สถาปัตยกรรม Infrastructure (Binding)
ตาม Patch 1.8.1 (ADR-018) Infrastructure Layout ที่กำหนดไว้:
| Component | Host | บทบาทใน RAG Pipeline |
| ---------------------- | ------------- | ------------------------------------------------- |
| **OpenRAG** (Docling + OpenSearch + Langflow) | Admin Desktop | **Phase 0: Extraction** — สกัด Metadata + Text จาก PDF เป็น JSON |
| **Tika** (Fallback OCR) | QNAP | สกัดข้อความจาก PDF กรณีไม่ใช้ OpenRAG หรือ Fallback |
| **Elasticsearch 8.11** | QNAP | Vector Store + Full-text Index |
| **n8n** | QNAP | Orchestrator — Poll JSON จาก `rag-output/` (ทีละไฟล์) แล้วนำเข้า DMS |
| **DMS Backend (NestJS)**| QNAP | API Gateway — รับ Query / ส่งผล / บันทึก Metadata |
| **Ollama** | Admin Desktop | AI Inference (Embedding + Generate) บน RTX 2060 SUPER |
| **MariaDB 11.8** | QNAP | Document Metadata (Authoritative DB) |
| **Redis 7.2** | QNAP | Cache (Query Result Cache) |
> ⛔ **ข้อห้าม (ADR-018):** OpenRAG และ Ollama **ห้ามอยู่บน QNAP** และห้ามเข้า DB โดยตรง
> ✅ OpenRAG เขียนผล JSON ลง `rag-output/` บน Shared NAS (R:\ บน Admin Desktop = `staging_ai` บน QNAP)
---
## 🔄 RAG Data Flow (4 Phase)
---
### Phase 0: OpenRAG — Batch Extraction Phase ("พนักงานคัดกรองข้อมูล")
OpenRAG ทำงานบน **Admin Desktop** อ่าน PDF ทั้ง Folder แล้วเขียน JSON ทีละไฟล์ลง Shared NAS:
```
R:\staging_ai\*.pdf (Admin Desktop — Network Drive จาก QNAP)
┌───────────────────────────────────────────────────────────┐
│ OpenRAG Langflow Batch Runner (Admin Desktop) │
│ │
│ [Loop Folder Component] │
│ สำหรับแต่ละ .pdf ใน R:\staging_ai\ │
│ │ │
│ ▼ │
│ [Docling Component] ← Parse PDF Structure │
│ │ │
│ ▼ │
│ [Ollama LLM Component] ← Extract Metadata → JSON │
│ │ │
│ ▼ │
│ [File Write Component] │
│ เขียน JSON → R:\staging_ai\rag-output\<filename>.json │
│ (Skip ถ้า .json ไฟล์นั้นมีอยู่แล้ว — Idempotent) │
└───────────────────────────────────────────────────────────┘
────────────── Shared NAS Folder (staging_ai) ──────────────
staging_ai/
├── rag-output/
│ ├── TCC-COR-2024-001.json ← OpenRAG เขียน
│ ├── TCC-COR-2024-002.json
│ └── ... ← n8n อ่านทีละไฟล์
├── TCC-COR-2024-001.pdf
└── ...
────────────── n8n บน QNAP (Schedule Trigger) ──────────────
[n8n: Schedule Trigger ทุก 5 นาที]
→ อ่าน staging_ai/rag-output/*.json ทีละ 1 ไฟล์
→ Process → เปลี่ยนชื่อเป็น .done (หรือ ลบ)
→ Loop ต่อจนหมด Queue
```
**JSON Output Contract (เขียนลง `rag-output/<filename>.json`):**
```json
{
"source_file": "TCC-COR-2024-001.pdf",
"processed_at": "2026-03-13T10:00:00+07:00",
"is_valid": true,
"confidence": 0.91,
"extracted_text": "เนื้อหาเต็มของเอกสาร...",
"metadata": {
"correspondence_number": "TCC-COR-2024-001",
"title": "ส่งแบบ Shop Drawing งวดที่ 3",
"document_date": "2024-03-15",
"sender_org": "TCC",
"receiver_org": "LCB",
"project_code": "LCBP3",
"suggested_category": "Correspondence",
"detected_issues": []
},
"chunks": [
{ "chunk_index": 0, "page": 1, "text": "..." },
{ "chunk_index": 1, "page": 2, "text": "..." }
]
}
```
> ✅ **File Naming Convention:** `<original_pdf_basename>.json`
> ตัวอย่าง: `TCC-COR-2024-001.pdf` → `TCC-COR-2024-001.json`
>
> ✅ **Idempotency:** ถ้า `.json` ไฟล์นั้นมีอยู่แล้ว → Skip (ไม่ Process ซ้ำ)
> เพิ่ม field `processed_at` เพื่อ debug ว่า Extract เมื่อไหร่
>
> ⚠️ **Constraint (ADR-018):** OpenRAG ไม่มีสิทธิ์เข้า MariaDB
> เขียนได้เฉพาะใน `rag-output/` เท่านั้น — ไม่แตะ PDF ต้นฉบับ
---
### Phase 1: n8n Integration — Poll JSON จาก rag-output/ แล้ว Import เข้า DMS
n8n ทำงานแบบ **Pull (Schedule-based)** — ดึง JSON ทีละไฟล์จาก Shared NAS:
```
[n8n: Schedule Trigger ทุก 5 นาที]
├─── List Files: staging_ai/rag-output/*.json
│ (กรอง: ไม่รวม *.done, *.error)
├─── [ถ้าไม่มีไฟล์] → หยุด (รอรอบถัดไป)
└─── Loop ทีละ 1 ไฟล์:
├─── อ่านไฟล์ JSON
├─── Validate JSON Schema (is_valid, confidence, required fields)
├─── Confidence Router (ตาม ADR-017)
│ ≥ 0.85 → Auto Ingest via POST /api/migration/import
│ 0.600.84 → INSERT migration_review_queue (รอ Human Approve)
│ < 0.60 → Rename → .error (Log เหตุผล)
├─── [Auto Ingest Path]
│ POST /api/migration/import
│ Header: Idempotency-Key: {correspondence_number}:{batch_id}
│ Body: metadata + source_file_path
│ → Backend StorageService ย้ายไฟล์จาก staging_ai → uploads/YYYY/MM/
├─── [สำเร็จ] Rename: <filename>.json → <filename>.done
├─── [ล้มเหลว] Rename: <filename>.json → <filename>.error
└─── [Checkpoint] บันทึก migration_progress ทุก 10 records
```
> 📁 **File State Machine ใน `rag-output/`:**
>
> | สถานะ | Filename | ความหมาย |
> |-------|----------|----------|
> | Pending | `TCC-COR-001.json` | รอ n8n ดึงไป Process |
> | Done | `TCC-COR-001.done` | นำเข้า DMS สำเร็จ |
> | Error | `TCC-COR-001.error` | ล้มเหลว — รอ Manual Review |
### Phase 2: Indexing Pipeline — สร้าง Vector Index ใน Elasticsearch
```
PDF ที่ Import แล้ว (อยู่ใน uploads/)
[n8n Workflow: RAG Indexer]
├─── ใช้ Chunks จาก OpenRAG JSON โดยตรง (ไม่ต้อง OCR ซ้ำ)
│ หรือ Fallback: [Tika OCR] กรณีไม่มี chunks
├─── [Ollama: Embedding]
│ POST http://<OLLAMA_HOST>:11434/api/embeddings
│ Model: nomic-embed-text
└─── [Elasticsearch: Index Chunk]
index: dms_rag_chunks
fields: doc_id, chunk_text, embedding, page_num, metadata
```
### Phase 2: Query Pipeline (Real-time)
```
User Query (จาก DMS Frontend)
[DMS Backend: RAG Controller]
│ GET /api/rag/search?q=...&project_id=...
├─── Check Redis Cache (TTL 5 นาที)
└─── [n8n Webhook: RAG Query]
├─── [Ollama: Query Embedding] → Vector ของ Query
├─── [Elasticsearch: kNN Search]
│ └─ Top-5 Chunks ที่เกี่ยวข้อง + RBAC Filter (project_id, user_id)
├─── [Ollama: Generate Answer]
│ └─ Prompt: System + Context Chunks + User Query
│ Output: JSON { answer, sources, confidence }
└─── [DMS Backend] → ส่งผลกลับ + Cache ใน Redis
```
### Phase 3: Response Contract
```json
{
"answer": "เอกสาร RFA-2026-001 ถูก Approve โดย...",
"sources": [
{
"doc_id": "uuid-xxxx",
"doc_number": "RFA-2026-001",
"page": 3,
"excerpt": "...ข้อความที่ตัดมา...",
"confidence": 0.91
}
],
"confidence": 0.87,
"cached": false
}
```
---
## 📐 Elasticsearch Index Schema
```json
PUT /dms_rag_chunks
{
"mappings": {
"properties": {
"doc_id": { "type": "keyword" },
"doc_number": { "type": "keyword" },
"project_id": { "type": "keyword" },
"chunk_text": { "type": "text", "analyzer": "thai" },
"embedding": { "type": "dense_vector", "dims": 768 },
"page_num": { "type": "integer" },
"chunk_index": { "type": "integer" },
"created_at": { "type": "date" }
}
}
}
```
> ⚠️ **ขนาด Embedding Vector:** ขึ้นอยู่กับ Model ที่ใช้
> - `nomic-embed-text`: 768 dims
> - `llama3.2:3b` (ใช้ layer สุดท้าย): 3072 dims
> ต้องทดสอบ Performance บน RTX 2060 SUPER 8GB ก่อนเลือก
---
## 🛡️ Security & RBAC (สำคัญ)
- Query **ต้องผ่าน DMS API** — Ollama ไม่รับ Request โดยตรงจาก Frontend
- Elasticsearch Search **ต้องมี Filter** ด้วย `project_id` และ `permission_scope` เสมอ
- ผล RAG **ต้องไม่เปิดเผยเอกสาร** ที่ User ไม่มีสิทธิ์เห็น (CASL Enforcement ที่ Backend Layer)
- Cache Key ใน Redis ต้องรวม `user_id` หรือ `role` เพื่อป้องกัน Cross-user Cache Poisoning
---
## ⚙️ n8n Workflow: OpenRAG Ingestor (Node Overview)
Poll ไฟล์ JSON จาก Shared NAS ทีละไฟล์ แล้วนำข้อมูลเข้า DMS:
| Node | ชื่อ | หน้าที่ |
|------|------|----------|
| 0 | Schedule Trigger | ทำงานทุก 5 นาที (หรือ Manual Trigger) |
| 1 | List JSON Files | อ่านรายการ `staging_ai/rag-output/*.json` (กรอง .done/.error) |
| 2 | Loop Items | วนลูปทีละ 1 ไฟล์ |
| 3 | Read JSON File | อ่านเนื้อหา JSON จาก NAS |
| 4 | JSON Schema Validator | ตรวจสอบ field ครบ + ค่า is_valid |
| 5 | Confidence Router | แยก Auto / Review / Reject ตาม Threshold |
| 6A | Auto Ingest | POST `/api/migration/import` พร้อม Idempotency-Key |
| 6B | Review Queue | INSERT `migration_review_queue` เท่านั้น |
| 6C | Rename to .error | Rename ไฟล์ → `.error` + บันทึกเหตุผล |
| 7 | Rename to .done | Rename ไฟล์ → `.done` (กรณีสำเร็จ) |
| 8 | Save Checkpoint | UPDATE `migration_progress` ทุก 10 records |
---
## ⚙️ n8n Workflow: RAG Indexer (Node Overview)
Index Chunks (จาก OpenRAG JSON หรือ Tika Fallback) เข้า Elasticsearch:
| Node | ชื่อ | หน้าที่ |
|------|------|----------|
| 0 | Webhook / Schedule Trigger | รับ `doc_id` ที่นำเข้าแล้ว หรือ Batch รายคืน |
| 1 | Fetch Chunks | ดึง chunks จาก OpenRAG JSON หรือเรียก Tika Fallback |
| 2 | Tika OCR (Fallback) | POST `http://tika:9998/tika` กรณีไม่มี chunks จาก OpenRAG |
| 3 | Ollama Embeddings | POST `http://<OLLAMA_HOST>:11434/api/embeddings` |
| 4 | Elasticsearch Ingest | Bulk Index Chunks เข้า `dms_rag_chunks` |
| 5 | Update DMS Index Status | PATCH `/api/documents/{id}` ตั้ง `is_indexed: true` |
---
## ⚙️ n8n Workflow: RAG Query (Node Overview)
| Node | ชื่อ | หน้าที่ |
|------|------|----------|
| 0 | Webhook | รับ `{ query, project_id, user_id, top_k }` จาก Backend |
| 1 | Ollama: Embed Query | แปลง Query เป็น Vector |
| 2 | Elasticsearch: kNN Search | ค้นหา Top-k Chunks พร้อม RBAC Filter |
| 3 | Build RAG Prompt | รวม Context Chunks + System Prompt + User Query |
| 4 | Ollama: Generate | สร้างคำตอบ, Output JSON เท่านั้น |
| 5 | Return to Backend | Respond Webhook พร้อม `{ answer, sources, confidence }` |
---
## 📏 Confidence & Hallucination Guard
| ระดับ Confidence | การดำเนินการ |
|-----------------|--------------|
| `>= 0.80` | แสดงผลทันที พร้อม Sources |
| `0.60 0.79` | แสดงผลพร้อม Warning "โปรดตรวจสอบเอกสารต้นฉบับ" |
| `< 0.60` | ไม่แสดงคำตอบ — แสดงเฉพาะ Document Links ที่เกี่ยวข้อง |
> AI ไม่มีสิทธิ์ Write ข้อมูลใดๆ — Output เป็น JSON Read-only เสมอ (ADR-018)
---
## 🚧 ข้อจำกัดและความเสี่ยง
| ความเสี่ยง | ผลกระทบ | Mitigation |
|-----------|----------|------------|
| NAS Drive R: disconnect ขณะ OpenRAG รัน | เขียน JSON ไม่ได้ | Langflow ตรวจ Drive ก่อนเริ่ม Loop — แจ้งเตือนถ้า mount หาย |
| ไฟล์ JSON เขียนไม่สมบูรณ์ (crash กลางคัน) | n8n อ่าน JSON เสีย | n8n ตรวจ JSON valid ก่อน Process — Rename → .error |
| OpenRAG Process PDF ซ้ำ (Retry) | JSON เขียนทับ | Skip ถ้า `.json` มีอยู่แล้ว (Idempotent by filename) |
| n8n อ่านไฟล์ขณะ OpenRAG ยังเขียนไม่เสร็จ | JSON ไม่สมบูรณ์ | OpenRAG เขียนเป็น `.tmp` ก่อน → Rename เป็น `.json` เมื่อเสร็จ |
| rag-output/ เต็ม (เก่าสะสม) | Disk บน NAS เต็ม | ตั้ง Schedule ลบ `.done` ที่เกิน 30 วัน |
| OpenRAG Metadata ผิด | นำข้อมูลผิดเข้า DMS | Confidence < 0.85 → Human Review Queue (ADR-017 Policy) |
| Embedding Dim Mismatch | Index ใช้งานไม่ได้ | กำหนด Model + Dims ก่อน Index แรก ห้ามเปลี่ยน |
| RTX 2060 SUPER VRAM (8GB) | Timeout ถ้า Model ใหญ่เกินไป | ใช้ `nomic-embed-text` สำหรับ Embedding |
| AI Hallucination | คำตอบผิด | Confidence Threshold + Source Citation บังคับ |
| Cross-project Data Leak | Security Issue | RBAC Filter ทุก Query ที่ Elasticsearch Layer |
| Elasticsearch Storage | Disk Usage สูง | เปิด ILM Policy หรือจำกัดเฉพาะ Project สำคัญ |
| Ollama ไม่พร้อม | Query ล้มเหลว | Graceful Fallback: ใช้ Elasticsearch Full-text เท่านั้น |
---
## 📋 Implementation Gate (ก่อนพัฒนา)
> **หมายเหตุ:** Feature นี้เป็น Post-UAT / Post-Migration
> ต้องผ่าน Go-Live Gate ของ Migration (ADR-017) ก่อนเริ่มพัฒนา
**OpenRAG Setup (Admin Desktop):**
- [ ] ติดตั้ง OpenRAG บน Admin Desktop ตาม `## 🛠️ OpenRAG Setup Guide` ด้านล่าง
- [ ] กำหนด Langflow Workflow: PDF Input → Docling Parse → Ollama Extract → JSON Output
- [ ] ตั้งค่า System Prompt ใน Langflow ให้ Output ตรง JSON Contract ด้านบน
- [ ] ทดสอบ Extraction Accuracy กับตัวอย่างเอกสาร 20 ฉบับ (ไทย + อังกฤษ)
- [ ] ยืนยัน OpenRAG ไม่มี DB Credentials และ Mount `staging_ai` เป็น Read-only
**n8n Webhook Integration:**
- [ ] สร้าง n8n Webhook Endpoint: รับ JSON จาก OpenRAG (validate schema + route ตาม Confidence)
- [ ] ทดสอบ Idempotency-Key กรณี OpenRAG ส่ง Duplicate
- [ ] สร้าง n8n Workflow: RAG Indexer (Dry Run กับ 10 เอกสาร)
**Search & Query:**
- [ ] Migration v1.8.x เสร็จสมบูรณ์และ Stable (Prerequisite)
- [ ] ทดสอบ `nomic-embed-text` บน Admin Desktop — วัด VRAM + Speed
- [ ] กำหนด Elasticsearch Index Schema + Dims (lock ก่อน Index แรก)
- [ ] ออกแบบ RBAC Filter สำหรับ kNN Search
- [ ] สร้าง n8n Workflow: RAG Query (ทดสอบ Hallucination)
- [ ] เพิ่ม `/api/rag/search` Endpoint ใน DMS Backend
- [ ] เพิ่ม UI Component: RAG Search Panel ใน Frontend
- [ ] Load Test: Query Latency < 5 วินาที สำหรับ Top-5 Results
---
## 🛠️ OpenRAG Setup Guide (Admin Desktop — Step by Step)
> **สภาพแวดล้อม:** Windows 10/11, i9-9900K, 32GB RAM, RTX 2060 SUPER 8GB
> **ข้อกำหนด:** Ollama ต้องรันอยู่แล้วบน Admin Desktop (Port 11434)
> **เวลาติดตั้งประมาณ:** 20–40 นาที (ขึ้นอยู่กับความเร็ว Internet สำหรับ Pull Images)
---
### ขั้นตอนที่ 1: ติดตั้ง WSL 2 + Docker Desktop
OpenRAG บน Windows **ต้องรันผ่าน WSL 2** (ข้อกำหนดจาก OpenRAG Official Docs)
```powershell
# รันใน PowerShell (Admin) บน Admin Desktop
wsl --install -d Ubuntu
# รีสตาร์ท Windows หลังติดตั้งเสร็จ
```
จากนั้นติดตั้ง **Docker Desktop for Windows** พร้อม WSL 2 Integration:
1. ดาวน์โหลด Docker Desktop จาก [docs.docker.com](https://docs.docker.com/desktop/install/windows-install/)
2. ระหว่างติดตั้ง → เปิด **"Use WSL 2 based engine"**
3. หลังติดตั้ง → ไปที่ **Settings → Resources → WSL Integration**
4. เปิด Toggle สำหรับ Ubuntu distribution → **Apply & Restart**
> ✅ ตรวจสอบ: เปิด WSL Ubuntu แล้วรัน `docker ps` — ต้องไม่มี Error
---
### ขั้นตอนที่ 2: ติดตั้ง uv ใน WSL
> ️ **Ubuntu 24.04 (Noble) ไม่มี `python3.13` ใน Default Repository**
> ไม่ต้องติดตั้ง Python ด้วย `apt` — **`uv` จัดการ Python 3.13 เองได้โดยอัตโนมัติ**
```bash
# ติดตั้ง uv (ไม่ต้องการ Python ก่อน)
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc # โหลด PATH ใหม่
# ตรวจสอบ
uv --version # ต้องแสดง version เช่น uv 0.5.x
```
เมื่อรัน `uvx --python 3.13 openrag` ในขั้นตอนถัดไป `uv` จะ **ดาวน์โหลด Python 3.13 เองโดยอัตโนมัติ** ไม่ต้องติดตั้งแยก
> **ทางเลือก:** ถ้าต้องการ Python 3.13 ระดับ System จริงๆ (ไม่บังคับ):
> ```bash
> sudo add-apt-repository ppa:deadsnakes/ppa -y
> sudo apt update && sudo apt install -y python3.13 python3.13-venv
> ```
---
### ขั้นตอนที่ 3: ติดตั้ง OpenRAG
```bash
# ใน WSL Ubuntu:
mkdir ~/openrag-workspace
cd ~/openrag-workspace
# ⚠️ ติดตั้งแพ็กเกจระบบที่จำเป็นสำหรับ EasyOCR และ Docling
sudo apt update
sudo apt install -y libgl1 libglib2.0-0
# ติดตั้งและรัน OpenRAG (ครั้งแรกใช้เวลา 5–15 นาที)
# จะติดตั้ง easyocr ไปด้วยเพื่อรองรับ PDF ภาษาไทยผ่าน Docling
uvx --with easyocr --python 3.13 openrag
```
**ระหว่าง Interactive Setup ตอบดังนี้:**
| Prompt | คำตอบ (สำหรับระบบ LCBP3) |
|--------|--------------------------|
| OpenSearch Admin password | ตั้งรหัสผ่านแข็งแรง บันทึกไว้ |
| Langflow Admin password | ตั้งรหัสผ่านแข็งแรง บันทึกไว้ |
| OpenAI API key | **กด N / Skip** — เราใช้ Ollama แทน |
| Use custom LLM provider? | **Y** → เลือก **Ollama** |
| Ollama base URL | `http://192.168.20.100:11434` (Internal VLAN — Ollama รันบน Admin Desktop โดยตรง) |
| Configure Langfuse tracing? | **N** |
| Configure cloud connectors? | **N** |
| Start services now? | **Y** |
> ️ **Ollama รันบน Windows โดยตรง** (ไม่ใช่ใน Docker) ที่ IP `192.168.20.100` — ตรงกับ Config ใน `03-05-n8n-migration-setup-guide.md`
>
> ถ้าตั้งค่าผิดพลาด แก้ไขได้ที่:
> ```bash
> nano ~/.openrag/tui/.env
> # แก้บรรทัด OLLAMA_ENDPOINT=http://192.168.20.100:11434
> ```
---
### ขั้นตอนที่ 4: ตรวจสอบ Services ที่รัน
```bash
# ดู Containers ที่ OpenRAG สร้าง
docker ps
# ควรเห็น containers เหล่านี้:
# openrag-langflow (Langflow UI + API)
# openrag-opensearch (OpenSearch: Vector Store)
# openrag-opensearch-dashboards (Optional)
```
**URL ที่ใช้งานได้:**
| Service | URL | หมายเหตุ |
|---------|-----|----------|
| OpenRAG UI | `http://localhost:3000` | หน้าหลัก (เหมือน Chat UI) |
| Langflow | `http://localhost:7860` | สร้าง/แก้ไข Workflow |
| OpenSearch | `http://localhost:9200` | Vector Store API |
---
### ขั้นตอนที่ 5: ตรวจสอบ Ollama Connection
```bash
# ทดสอบว่า OpenRAG เชื่อม Ollama ได้ (รันใน WSL)
curl http://192.168.20.100:11434/api/tags
# ต้องแสดง JSON รายการ Models ที่มีใน Ollama
# ตรวจสอบว่ามี:
# - llama3.2:3b
# - mistral:7b-instruct-q4_K_M
# - nomic-embed-text (ถ้ายังไม่มี ให้ติดตั้ง)
```
```bash
# ติดตั้ง nomic-embed-text (สำหรับ Embedding)
# รันบน Windows Terminal (ไม่ใช่ WSL):
ollama pull nomic-embed-text
```
---
### ขั้นตอนที่ 6: กำหนด Langflow Workflow (Batch Extraction Pipeline)
เปิด Langflow ที่ `http://localhost:7860`**New Flow** → เพิ่ม Component ตามลำดับดังนี้:
#### ภาพรวม Node Connection
```
[Read File] ──▶ [Loop] ──▶ [Parser: Stringify] ──▶ [Prompt Template] ──▶ [Ollama]
│ │
│ (filename จาก Item) │
└──────────────────────────────────────────────────────────▶│
[Custom Code]
เขียน .tmp → rename .json
```
---
#### Node 1: Read File
> **Component:** `Read File` (หมวด Data / Helpers)
| Setting | ค่า |
|---------|-----|
| Files | อัปโหลด หรือ ชี้ไปที่ `/data/staging_ai/` |
| Advanced Parser | `OFF` (ปิด — อ่านเป็น raw text ธรรมดา) |
**การเชื่อมต่อ:**
- Output `Files` → Input `Inputs` ของ Loop
> ℹ️ Read File จะโหลดไฟล์ทั้งหมดมาเป็น list แล้วส่งให้ Loop วนลูปทีละไฟล์
> ถ้าต้องการเลือก folder แบบ dynamic ให้ใช้ **Directory** component แทน
---
#### Node 2: Loop
> **Component:** `Loop` (หมวด Logic)
| Setting | ค่า |
|---------|-----|
| Inputs | รับจาก `Read File → Files` |
**Output ที่ใช้:**
- `Item` → ส่งต่อให้ Parser และ Custom Code (filename)
- `Done` → ไม่ต้องเชื่อมไปไหน (สัญญาณสิ้นสุด Loop)
> ℹ️ Loop จะปล่อย `Item` ทีละ 1 ไฟล์ ผ่านทุก Node ก่อนวนรอบถัดไป
---
#### Node 3: Parser (Mode: Stringify)
> **Component:** `Parser` (หมวด Processing)
| Setting | ค่า |
|---------|-----|
| Mode | **`Stringify`** (ไม่ใช่ Parser) |
| Data or DataFrame | รับจาก `Loop → Item` |
**การเชื่อมต่อ:**
- Input `Data or DataFrame``Loop → Item`
- Output `Parsed Text` → Input `extracted_text` ของ Prompt Template
> ⚠️ **ใช้ Mode: Stringify เท่านั้น**
> Mode: Parser ใช้ template เป็น pattern สำหรับดึงค่า — ไม่เหมาะกับงานนี้
> Mode: Stringify แปลง file object เป็น text content ที่ Ollama อ่านได้
---
#### Node 4: Prompt Template
> **Component:** `Prompt Template` (หมวด Prompts)
| Setting | ค่า |
|---------|-----|
| Template | ใส่ System Prompt จากขั้นตอนที่ 7 ด้านล่าง |
| Variable `{extracted_text}` | เชื่อมกับ `Parser → Parsed Text` |
**การเชื่อมต่อ:**
- Variable `extracted_text``Parser → Parsed Text`
- Output `Prompt` → Input `Input` ของ Ollama
> ️ Prompt Template รองรับ `{variable_name}` สำหรับแทรกค่าแบบ dynamic
> ต้องตั้งชื่อ variable ให้ตรงกับที่ใช้ใน template (`{extracted_text}`)
---
#### Node 5: Ollama
> **Component:** `Ollama` (หมวด Models)
| Setting | ค่า |
|---------|-----|
| Ollama API URL | `http://localhost:11434` (ถ้ารันบน WSL ไม่ต้องใส่ IP) |
| Model Name | `scb10x/typhoon2.1-gemma3-4b` |
| Format | ไม่ต้องตั้ง — ใช้ Enable Structured Output แทน |
| Temperature | `0.1` |
| Enable Structured Output | `ON` |
| Tool Model Enabled | `ON` (เห็นใน screenshot) |
**การเชื่อมต่อ:**
- Input `Input``Prompt Template → Prompt`
- Input `System Message` ← ปล่อยว่าง (System Prompt อยู่ใน Prompt Template แล้ว)
- Output `Text` → Input ของ Custom Code (Node 6)
> ⚠️ **Ollama API URL:**
> ถ้า Langflow รันใน Docker (WSL) → ใช้ `http://host.docker.internal:11434`
> ถ้า Ollama bind บน VLAN IP → ใช้ `http://192.168.20.100:11434`
> ทดสอบด้วย: `curl http://host.docker.internal:11434/api/tags` ใน WSL
---
#### Node 6: Write JSON (Idempotent)
> **Component:** `Custom Component` (สร้างใหม่) — ทำหน้าที่รับ output JSON จาก Ollama และดึงชื่อไฟล์จาก Loop เพื่อเขียนเป็นไฟล์ `.json`
**Python Code:**
```python
from langflow.custom import Component
from langflow.io import StrInput, DataInput, Output
from langflow.schema import Data
import json
import os
from pathlib import Path
class WriteJsonIdempotent(Component):
display_name = "Write JSON (Idempotent)"
description = "Writes JSON to staging_ai dynamically based on loop item filename"
inputs = [
StrInput(name="json_content", display_name="JSON Content"),
DataInput(name="loop_item", display_name="Loop Item (PDF)"),
]
outputs = [
Output(display_name="Result Path", name="result_path", method="write_file")
]
def write_file(self) -> Data:
# Extract filename from loop_item
pdf_path = self.loop_item.data.get("file_path", "")
if not pdf_path:
return Data(data={"error": "No file_path in loop item"})
base_name = Path(pdf_path).stem
out_dir = Path("/data/staging_ai/rag-output")
out_dir.mkdir(parents=True, exist_ok=True)
json_path = out_dir / f"{base_name}.json"
# Idempotency check
if json_path.exists():
return Data(data={"status": "skipped", "path": str(json_path), "reason": "already exists"})
# Parse and write content to ensure it's valid JSON before saving
try:
parsed = json.loads(self.json_content)
# Inject source file name if missing
if not parsed.get("source_file"):
parsed["source_file"] = f"{base_name}.pdf"
tmp_path = out_dir / f"{base_name}.tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(parsed, f, ensure_ascii=False, indent=2)
# Atomic rename
os.replace(tmp_path, json_path)
return Data(data={"status": "written", "path": str(json_path)})
except Exception as e:
err_path = out_dir / f"{base_name}.error"
with open(err_path, "w", encoding="utf-8") as f:
f.write(f"Error parsing JSON from API: {str(e)}\\n\\nContent:\\n{self.json_content}")
return Data(data={"status": "error", "path": str(err_path), "error": str(e)})
```
**การเชื่อมต่อ:**
- Input `json_content``Ollama → Text`
- Input `loop_item``Loop → Item`
- Output `result_path``Loop → item` (Feedback loop กลับไปบอกว่ารอบนี้จบแล้ว)
> ✅ **Idempotency:** โค้ดมีการสั่ง `if json_path.exists(): return` เพื่อข้ามไฟล์ที่เขียนไปแล้ว
> ✅ **Atomic Write:** เขียนเป็น `.tmp` ก่อนแล้วใช้ `os.replace` เพื่อป้องกัน n8n มาอ่านตอนที่ยังเขียนไม่เสร็จ
> ✅ **Dynamic Filename:** อ่าน path ต้นฉบับจาก loop item ทำให้ได้ชื่อไฟล์ .json ตรงกับ pdf เสมอ
---
#### สรุปการ Wire ทั้ง Workflow
| From | Port | To | Port |
|------|------|----|------|
| Read File | Files | Loop | Inputs |
| Loop | Item | Parser | Data or DataFrame |
| Parser | Parsed Text | Prompt Template | extracted_text |
| Prompt Template | Prompt | Ollama | input_value (Input) |
| Ollama | Text | Write JSON (Idempotent) | json_content |
| Loop | Item | Write JSON (Idempotent) | loop_item |
| Write JSON | result_path | Loop | element |
**ตั้งค่า Ollama LLM Component:**
| ฟิลด์ | ค่า |
|-------|-----|
| Model Name | `scb10x/typhoon2.1-gemma3-4b` |
| Base URL | `http://192.168.20.100:11434` |
| Format | `json` (บังคับ JSON Output) |
| Temperature | `0.1` (ลด Hallucination) |
| Max Tokens | `2048` |
| Enable Structured Output | `ON` |
> ℹ️ **เหตุผลที่เลือก Typhoon 2.1:**
> `scb10x/typhoon2.1-gemma3-4b` โดย SCB10X เป็น Model ที่ออกแบบมาสำหรับภาษาไทยโดยเฉพาะ
> เหมาะกับเอกสารก่อสร้างที่มีทั้งไทยและอังกฤษปนกัน ดีกว่า `llama3.2:3b` มาก
> ต้องติดตั้งก่อน: `ollama pull scb10x/typhoon2.1-gemma3-4b` บน Admin Desktop
---
### ขั้นตอนที่ 7: System Prompt สำหรับ Metadata Extraction
คัดลอก Prompt นี้ใส่ใน **Prompt Template Component** ของ Langflow:
> ⚠️ **Langflow Escaping Rule:** ปีกกา `{` `}` ที่เป็น JSON literal ต้องเขียนเป็น `{{` `}}` (double)
> มิฉะนั้น Langflow จะตีความว่าเป็น variable → เกิด error "Invalid variables"
> **ข้อยกเว้น:** `{extracted_text}` ใช้ single เพราะเป็น variable จริงที่รับจาก Parser
```
คุณเป็นผู้ช่วย AI สำหรับระบบจัดการเอกสารก่อสร้าง Laem Chabang Port Phase 3 (LCBP3)
หน้าที่ของคุณคือดึงข้อมูล Metadata จากเอกสาร แล้วตอบกลับเป็น JSON ที่ valid เท่านั้น
ห้ามเพิ่มข้อความอื่นนอกจาก JSON
เอกสารอาจเป็นภาษาไทย อังกฤษ หรือผสมกัน
You are a document metadata extraction assistant for a construction document management system (LCBP3).
Extract the following fields and return ONLY a valid JSON object.
No explanation, no markdown, no text outside the JSON.
Documents may be in Thai, English, or mixed language.
Return ONLY this JSON structure:
{{
"source_file": "<ชื่อไฟล์ PDF ที่รับมา>",
"is_valid": true,
"confidence": 0.0,
"extracted_text": "<full extracted text, max 2000 chars>",
"metadata": {{
"correspondence_number": "<document number, e.g. TCC-COR-2024-001 or null>",
"title": "<document title or subject — ภาษาเดิมของเอกสาร>",
"document_date": "<date in YYYY-MM-DD format or null>",
"sender_org": "<sender organization abbreviation or null>",
"receiver_org": "<receiver organization abbreviation or null>",
"project_code": "<project code, e.g. LCBP3 or null>",
"suggested_category": "<one of: Correspondence, RFA, ContractDrawing, ShopDrawing>",
"detected_issues": []
}},
"chunks": [
{{"chunk_index": 0, "page": 1, "text": "<chunk text max 500 chars>"}}
]
}}
Document text to analyze:
{extracted_text}
```
> ️ **`{{` `}}` → แสดงเป็น `{` `}` จริงใน prompt ที่ส่งให้ LLM**
> ⚠️ **ห้าม Hardcode** รายการ Category — ดูจาก `GET /api/master/correspondence-types` ตาม ADR-017
---
### ขั้นตอนที่ 8: ตั้งค่า Volume Mount (AI Isolation — ADR-018)
แก้ไขไฟล์ `~/.openrag/tui/docker-compose.yml` ที่ OpenRAG สร้างขึ้น:
```yaml
services:
langflow:
volumes:
# staging_ai mount จาก NAS
# Windows R:\ drive จะปรากฏใน WSL เป็น /mnt/r/
- /mnt/r/staging_ai:/data/staging_ai # ← Read PDF + Write rag-output/
# หมายเหตุ: ต้องเขียนได้ที่ rag-output/ จึงไม่ใส่ :ro
opensearch:
# ไม่ต้อง mount staging_ai — OpenSearch ใช้ Vector Store เท่านั้น
```
> ⚠️ **ตรวจสอบ R:\ ใน WSL:**
> ```bash
> # ใน WSL Terminal ตรวจว่า mount อยู่ที่ไหน
> ls /mnt/r/staging_ai/
> # ต้องเห็นไฟล์ PDF ที่มีอยู่
> ```
>
> ✅ **สร้าง rag-output/ ก่อนรัน:**
> ```bash
> mkdir -p /mnt/r/staging_ai/rag-output
> ```
```bash
# หลังแก้ไข docker-compose.yml — รีสตาร์ท OpenRAG
cd ~/openrag-workspace
docker compose -f ~/.openrag/tui/docker-compose.yml restart langflow
```
---
### ขั้นตอนที่ 9: ตรวจสอบ File-based Queue
ทดสอบว่า OpenRAG เขียนไฟล์ลง NAS ได้ และ n8n อ่านไฟล์จาก NAS ได้:
**ทดสอบ OpenRAG เขียน (ใน WSL):**
```bash
# ตรวจสอบว่า mount ใช้งานได้
ls /mnt/r/staging_ai/*.pdf | head -5
# ทดสอบเขียนไฟล์
echo '{"test": true}' > /mnt/r/staging_ai/rag-output/test.json
ls /mnt/r/staging_ai/rag-output/
# ต้องเห็น test.json
```
**ทดสอบ n8n อ่าน (ใน n8n Workflow):**
สร้าง Test Workflow ใน n8n:
| Node | Type | Config |
|------|------|--------|
| Trigger | Manual | - |
| List Files | Read/Write Files from Disk | Path: `staging_ai/rag-output/*.json` |
| Read File | Read/Write Files from Disk | Dynamic path จาก List node |
| Parse JSON | Code | `JSON.parse(items[0].binary.data.toString())` |
```bash
# ตรวจสอบ path ใน n8n container
docker exec n8n ls /home/node/.n8n/staging_ai/rag-output/
# ต้องเห็น test.json ที่สร้างไว้
```
> 💡 **Path Mapping:**
> - Admin Desktop (WSL): `/mnt/r/staging_ai/rag-output/`
> - n8n บน QNAP: `staging_ai/rag-output/` (ตาม Volume Mount ใน docker-compose)
---
### ขั้นตอนที่ 10: Pre-Production Verification
| ลำดับ | รายการ | วิธีตรวจสอบ |
|-------|--------|-------------|
| 1 | Ollama เชื่อมต่อได้ | `curl http://192.168.20.100:11434/api/tags` จาก WSL |
| 2 | `nomic-embed-text` พร้อม | `ollama list` บน Windows Terminal |
| 3 | Langflow รันได้ | เปิด `http://localhost:7860` |
| 4 | R:\ mount เห็น PDF | `ls /mnt/r/staging_ai/*.pdf` ใน WSL |
| 5 | Langflow เขียน rag-output/ ได้ | ดู `/mnt/r/staging_ai/rag-output/` หลังรัน Test |
| 6 | ไม่มี DB Credentials ใน env | ตรวจ `~/.openrag/tui/docker-compose.yml` |
| 7 | Extraction ถูกต้อง ≥ 85% | รัน Batch กับเอกสาร 20 ฉบับ นับ field ที่ถูก |
| 8 | JSON ถูกต้อง (valid JSON) | `python3 -m json.tool rag-output/test.json` |
| 9 | n8n อ่าน JSON จาก NAS ได้ | รัน Test Workflow ใน n8n ดู Execution Log |
| 10 | GPU VRAM < 7.5GB ระหว่างรัน | `nvidia-smi --query-gpu=memory.used --format=csv` |
```bash
# ตรวจสอบ VRAM บน Admin Desktop (Windows Terminal)
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
```
---
## 📋 Implementation Gate (ก่อนพัฒนา)
> **หมายเหตุ:** Feature นี้เป็น Post-UAT / Post-Migration
> ต้องผ่าน Go-Live Gate ของ Migration (ADR-017) ก่อนเริ่มพัฒนา
**OpenRAG Setup (Admin Desktop):**
- [ ] WSL 2 + Docker Desktop ติดตั้งเสร็จ (ขั้นตอนที่ 1)
- [ ] OpenRAG ติดตั้งผ่าน `uvx --python 3.13 openrag` (ขั้นตอนที่ 23)
- [ ] Ollama เชื่อมต่อจาก Docker Container ได้ (ขั้นตอนที่ 5)
- [ ] `nomic-embed-text` พร้อมใช้งานใน Ollama
- [ ] Langflow Batch Workflow สร้างเสร็จพร้อม System Prompt (ขั้นตอนที่ 6–7)
- [ ] Volume Mount `R:\staging_ai``/data/staging_ai` (Read+Write) (ขั้นตอนที่ 8)
- [ ] สร้าง folder `staging_ai/rag-output/` บน NAS ก่อนรัน
- [ ] ตรวจสอบ Idempotent: Skip ถ้า `.json` ไฟล์มีอยู่แล้ว
- [ ] ทดสอบ Extraction Accuracy ≥ 85% กับ 20 เอกสารตัวอย่าง (ขั้นตอนที่ 10)
- [ ] ยืนยัน OpenRAG ไม่มี DB Credentials ใน docker-compose.yml
**n8n File-based Queue Integration:**
- [ ] ตรวจสอบ n8n Volume Mount เห็น `staging_ai/rag-output/` (ขั้นตอนที่ 9)
- [ ] สร้าง n8n Schedule Workflow: List JSON Files → Loop → Read → Validate → Route
- [ ] ทดสอบ Rename ไฟล์ `.json``.done` / `.error` ใน n8n
- [ ] n8n Workflow: OpenRAG Ingestor รัน Validation + Confidence Router ได้
- [ ] ทดสอบ Idempotency-Key กรณีรัน n8n ซ้ำ (ไฟล์ `.done` ไม่ถูก Process ซ้ำ)
**Search & Query (Post-Migration):**
- [ ] Migration v1.8.x เสร็จสมบูรณ์และ Stable (Prerequisite)
- [ ] กำหนด Elasticsearch Index Schema + Dims (lock ก่อน Index แรก)
- [ ] ออกแบบ RBAC Filter สำหรับ kNN Search
- [ ] สร้าง n8n Workflow: RAG Indexer + RAG Query
- [ ] เพิ่ม `/api/rag/search` Endpoint ใน DMS Backend
- [ ] เพิ่ม UI Component: RAG Search Panel ใน Frontend
- [ ] Load Test: Query Latency < 5 วินาที สำหรับ Top-5 Results
---
*เอกสารนี้เป็น Living Document — อัปเดตเมื่อมีการตัดสินใจ Architecture ใหม่*
**Version:** 1.8.1 | **Author:** Development Team | **Last Updated:** 2026-03-13