260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -9,37 +9,41 @@ try {
|
||||
if (!fs.existsSync(config.SOURCE_PDF_DIR)) {
|
||||
throw new Error(`PDF Source directory not found at: ${config.SOURCE_PDF_DIR}`);
|
||||
}
|
||||
|
||||
|
||||
const files = fs.readdirSync(config.SOURCE_PDF_DIR);
|
||||
|
||||
|
||||
// Check write permission to log path
|
||||
fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());
|
||||
|
||||
|
||||
// Grab categories out of the previous node (Fetch Categories) if available
|
||||
// otherwise use fallback array
|
||||
let categories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];
|
||||
let categories = ['Correspondence', 'RFA', 'Drawing', 'Transmittal', 'Report', 'Other'];
|
||||
try {
|
||||
const upstreamData = $('Fetch Categories').first()?.json?.data;
|
||||
if (upstreamData && Array.isArray(upstreamData)) {
|
||||
categories = upstreamData.map(c => c.name || c.type || c); // very loose mapping depending on API response
|
||||
categories = upstreamData.map((c) => c.name || c.type || c); // very loose mapping depending on API response
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
} catch (e) {}
|
||||
|
||||
// Grab existing tags from Fetch Tags node
|
||||
let existingTags = [];
|
||||
try {
|
||||
const tagData = $('Fetch Tags').first()?.json?.data || [];
|
||||
existingTags = Array.isArray(tagData) ? tagData.map(t => t.tag_name || t.name || '').filter(Boolean) : [];
|
||||
} catch(e) {}
|
||||
|
||||
return [{ json: {
|
||||
preflight_ok: true,
|
||||
pdf_count_in_source: files.length,
|
||||
excel_target: config.EXCEL_FILE,
|
||||
system_categories: categories,
|
||||
existing_tags: existingTags,
|
||||
timestamp: new Date().toISOString()
|
||||
}}];
|
||||
existingTags = Array.isArray(tagData) ? tagData.map((t) => t.tag_name || t.name || '').filter(Boolean) : [];
|
||||
} catch (e) {}
|
||||
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
preflight_ok: true,
|
||||
pdf_count_in_source: files.length,
|
||||
excel_target: config.EXCEL_FILE,
|
||||
system_categories: categories,
|
||||
existing_tags: existingTags,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
throw new Error(`Pre-flight check failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,56 @@
|
||||
ตารางสรุปหน้าที่และผลลัพธ์ (Output) ของแต่ละ Node ใน LCBP3 Migration Workflow v1.8.1 คแบ่งกลุ่มตามขั้นตอนการทำงานเพื่อให้เข้าใจได้ง่ายขึ้นครับ:
|
||||
|
||||
## 🚀 กลุ่มที่ 1: จุดเริ่มต้นและเตรียมการ (Initialization & Preflight)
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| -------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------- |
|
||||
| Form Trigger | จุดเริ่มต้นของ Workflow แสดงฟอร์มให้ผู้ใช้เลือกโมเดล AI, ขนาด Batch และระบุตำแหน่งไฟล์ Excel | ข้อมูลจากผู้ใช้ (Model, Batch Size, Excel Path) |
|
||||
| Set Configuration | ตั้งค่าตัวแปรระบบ (Config) เช่น URL, Token, โฟลเดอร์ทำงาน และเกณฑ์การตัดสินใจของ AI | ชุดตัวแปร config ไว้ใช้ตลอด Workflow |
|
||||
| Check Backend Health | เรียก API ทดสอบว่าระบบ Backend พร้อมทำงานหรือไม่ | สถานะ HTTP 200 (OK) |
|
||||
| Fetch Categories | ดึงข้อมูล Master Data หมวดหมู่เอกสารจาก Backend | รายการหมวดหมู่เอกสารทั้งหมดในระบบ |
|
||||
| Fetch Tags | ดึงข้อมูล Master Data แท็กที่มีอยู่จาก Backend | รายชื่อแท็กทั้งหมดในระบบ |
|
||||
| File Mount Check | ตรวจสอบว่าไฟล์ Excel และโฟลเดอร์ PDF มีอยู่จริง และเช็คสิทธิ์การเขียนไฟล์ Log | สถานะ preflight_ok: true พร้อมรายชื่อหมวดหมู่/แท็กที่ดึงมาได้ |
|
||||
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| Form Trigger | จุดเริ่มต้นของ Workflow แสดงฟอร์มให้ผู้ใช้เลือกโมเดล AI, ขนาด Batch และระบุตำแหน่งไฟล์ Excel | ข้อมูลจากผู้ใช้ (Model, Batch Size, Excel Path) |
|
||||
| Set Configuration | ตั้งค่าตัวแปรระบบ (Config) เช่น URL, Token, โฟลเดอร์ทำงาน และเกณฑ์การตัดสินใจของ AI | ชุดตัวแปร config ไว้ใช้ตลอด Workflow |
|
||||
| Check Backend Health | เรียก API ทดสอบว่าระบบ Backend พร้อมทำงานหรือไม่ | สถานะ HTTP 200 (OK) |
|
||||
| Fetch Categories | ดึงข้อมูล Master Data หมวดหมู่เอกสารจาก Backend | รายการหมวดหมู่เอกสารทั้งหมดในระบบ |
|
||||
| Fetch Tags | ดึงข้อมูล Master Data แท็กที่มีอยู่จาก Backend | รายชื่อแท็กทั้งหมดในระบบ |
|
||||
| File Mount Check | ตรวจสอบว่าไฟล์ Excel และโฟลเดอร์ PDF มีอยู่จริง และเช็คสิทธิ์การเขียนไฟล์ Log | สถานะ preflight_ok: true พร้อมรายชื่อหมวดหมู่/แท็กที่ดึงมาได้ |
|
||||
|
||||
## 📂 กลุ่มที่ 2: เตรียมข้อมูลและการแบ่งชุด (Data Ingestion & Batching)
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| Read Checkpoint | อ่านฐานข้อมูลว่าครั้งที่แล้วประมวลผล Excel ถึงบรรทัดที่เท่าไหร่ (Resume capability) | ตัวเลข last_processed_index ล่าสุด |
|
||||
| Read Excel Binary | อ่านไฟล์ Excel ต้นฉบับขึ้นมาเป็นข้อมูลไบนารี | ข้อมูล Binary ของ Excel |
|
||||
| Read Excel | แปลงข้อมูล Binary ให้เป็นตารางข้อมูล JSON | JSON Array ของข้อมูลเอกสารทุกแถว |
|
||||
| Process Batch + Encoding | ตัดแบ่งแถวตามจำนวน BATCH_SIZE เริ่มจากจุด Checkpoint และแปลง Encoding ให้รองรับภาษาไทย (UTF-8) | ข้อมูลเอกสาร 1 ชุด (เช่น 2 รายการ) ที่พร้อมทำงาน |
|
||||
| File Validator | ตรวจสอบว่าไฟล์ PDF ที่ระบุใน Excel มีอยู่จริงในโฟลเดอร์ ป้องกัน Path Traversal | เฉพาะรายการที่มีไฟล์ PDF อยู่จริง (รายการ Error จะถูกตัดและส่งไป Log) |
|
||||
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Read Checkpoint | อ่านฐานข้อมูลว่าครั้งที่แล้วประมวลผล Excel ถึงบรรทัดที่เท่าไหร่ (Resume capability) | ตัวเลข last_processed_index ล่าสุด |
|
||||
| Read Excel Binary | อ่านไฟล์ Excel ต้นฉบับขึ้นมาเป็นข้อมูลไบนารี | ข้อมูล Binary ของ Excel |
|
||||
| Read Excel | แปลงข้อมูล Binary ให้เป็นตารางข้อมูล JSON | JSON Array ของข้อมูลเอกสารทุกแถว |
|
||||
| Process Batch + Encoding | ตัดแบ่งแถวตามจำนวน BATCH_SIZE เริ่มจากจุด Checkpoint และแปลง Encoding ให้รองรับภาษาไทย (UTF-8) | ข้อมูลเอกสาร 1 ชุด (เช่น 2 รายการ) ที่พร้อมทำงาน |
|
||||
| File Validator | ตรวจสอบว่าไฟล์ PDF ที่ระบุใน Excel มีอยู่จริงในโฟลเดอร์ ป้องกัน Path Traversal | เฉพาะรายการที่มีไฟล์ PDF อยู่จริง (รายการ Error จะถูกตัดและส่งไป Log) |
|
||||
|
||||
## 🧠 กลุ่มที่ 3: สกัดข้อความและวิเคราะห์ด้วย AI (Text Extraction & AI Analysis)
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Read PDF File | อ่านไฟล์ PDF ของรายการที่ผ่านเข้ามาเป็นข้อมูลไบนารี | ข้อมูล Binary ของไฟล์ PDF |
|
||||
| Extract PDF Text | ส่งไฟล์ PDF ให้ Apache Tika ทำ OCR / สกัดตัวอักษร | ข้อความดิบ (Text) ที่อ่านได้จากหน้า PDF |
|
||||
| Check Fallback State | ตรวจสอบใน DB ว่าระบบกำลังอยู่ในโหมดใช้โมเดล AI สำรองหรือไม่ | สถานะ is_fallback_active |
|
||||
| Fetch DB Context | ดึงข้อมูลโปรเจกต์ แผนก และองค์กร เพื่อใช้เป็นบริบทให้ AI อ้างอิง | ข้อมูลอ้างอิงรหัสและชื่อต่างๆ จากระบบเก่า |
|
||||
| Build AI Prompt | ประกอบร่างข้อความ (Prompt) โดยรวมข้อมูลจาก Excel, ข้อความใน PDF และบริบท เพื่อสั่งงาน AI | คำสั่งในฟิลด์ ollama_payload |
|
||||
| Ollama AI Analysis | ส่ง Prompt ยิงเข้า Server Ollama เพื่อให้ AI วิเคราะห์ จัดหมวดหมู่ และสรุปข้อมูล | ข้อความอธิบายหรือ JSON ที่ AI ตอบกลับมา |
|
||||
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||
| Read PDF File | อ่านไฟล์ PDF ของรายการที่ผ่านเข้ามาเป็นข้อมูลไบนารี | ข้อมูล Binary ของไฟล์ PDF |
|
||||
| Extract PDF Text | ส่งไฟล์ PDF ให้ Apache Tika ทำ OCR / สกัดตัวอักษร | ข้อความดิบ (Text) ที่อ่านได้จากหน้า PDF |
|
||||
| Check Fallback State | ตรวจสอบใน DB ว่าระบบกำลังอยู่ในโหมดใช้โมเดล AI สำรองหรือไม่ | สถานะ is_fallback_active |
|
||||
| Fetch DB Context | ดึงข้อมูลโปรเจกต์ แผนก และองค์กร เพื่อใช้เป็นบริบทให้ AI อ้างอิง | ข้อมูลอ้างอิงรหัสและชื่อต่างๆ จากระบบเก่า |
|
||||
| Build AI Prompt | ประกอบร่างข้อความ (Prompt) โดยรวมข้อมูลจาก Excel, ข้อความใน PDF และบริบท เพื่อสั่งงาน AI | คำสั่งในฟิลด์ ollama_payload |
|
||||
| Ollama AI Analysis | ส่ง Prompt ยิงเข้า Server Ollama เพื่อให้ AI วิเคราะห์ จัดหมวดหมู่ และสรุปข้อมูล | ข้อความอธิบายหรือ JSON ที่ AI ตอบกลับมา |
|
||||
| Parse & Validate AI Response | แปลงคำตอบ AI เป็น JSON Object ตรวจสอบว่าโครงสร้างถูกต้อง และจัดรูปแบบให้ตรงกับ Backend | ข้อมูลเดิม + ผลลัพธ์ ai_result (หรือ parse_error ถ้า AI ตอบผิดรูปแบบ) |
|
||||
| Update Fallback State | นับจำนวน Error ลง DB หาก AI ทำงานพลาดหลายครั้ง ระบบจะสลับไปใช้ Fallback Model โดยอัตโนมัติ | อัปเดตตาราง migration_fallback_state สำเร็จ |
|
||||
| Update Fallback State | นับจำนวน Error ลง DB หาก AI ทำงานพลาดหลายครั้ง ระบบจะสลับไปใช้ Fallback Model โดยอัตโนมัติ | อัปเดตตาราง migration_fallback_state สำเร็จ |
|
||||
|
||||
## 🔀 กลุ่มที่ 4: การตัดสินใจและการนำเข้าระบบ (Routing & Ingestion)
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
|
||||
| Confidence Router | ตรวจประเมินคะแนนความมั่นใจ (Confidence) จาก AI และกำหนดเส้นทาง route_index ให้เอกสาร | สถานะชั่วคราวและค่า route_index (0, 1, 2, 3) |
|
||||
| Route by Confidence Switch Node | แบ่งเส้นทางข้อมูลออกเป็น 4 ขา ตามค่าจาก Router | กระจายข้อมูลไปทาง Staging(High), Staging(Review), Reject หรือ Error |
|
||||
| Restore Binary | (หลังแยกสาย) ดึงข้อมูล Binary ของ PDF กลับมาแนบกับข้อมูลอีกครั้งเตรียมอัปโหลด | JSON + Binary PDF ของไฟล์นั้นๆ |
|
||||
| Upload to Backend | ยิง API นำไฟล์ PDF ฝากไว้ที่ Temp Storage ของ Backend DMS | รหัสไฟล์ temp_attachment_id ของ Backend |
|
||||
| Build Enqueue Payload | ประกอบร่างข้อมูลผลวิเคราะห์ AI เข้ากับรหัสไฟล์ เพื่อเตรียมโยนเข้าคิว Migration | โครงสร้าง JSON ที่พร้อมส่งเข้า API Queue (enqueue_payload) |
|
||||
| Enqueue to Review Queue | ยิงข้อมูลเข้า API Backend เพื่อบันทึกเข้าสู่ Review Queue ระบบ DMS | สถานะสำเร็จจากการรับข้อมูลของ Backend |
|
||||
| Save Checkpoint | บันทึกประวัติลง Database ว่าประมวลผลผ่านเอกสารชุดนี้เรียบร้อยแล้ว | อัปเดต last_processed_index สำเร็จ |
|
||||
| Delay | หน่วงเวลา (เช่น 2 วินาที) ก่อนวนรอบขึ้นไปทำข้อมูล Batch ถัดไป | วนลูปกลับไปที่จุด Read Checkpoint |
|
||||
| Route by Confidence Switch Node | แบ่งเส้นทางข้อมูลออกเป็น 4 ขา ตามค่าจาก Router | กระจายข้อมูลไปทาง Staging(High), Staging(Review), Reject หรือ Error |
|
||||
| Restore Binary | (หลังแยกสาย) ดึงข้อมูล Binary ของ PDF กลับมาแนบกับข้อมูลอีกครั้งเตรียมอัปโหลด | JSON + Binary PDF ของไฟล์นั้นๆ |
|
||||
| Upload to Backend | ยิง API นำไฟล์ PDF ฝากไว้ที่ Temp Storage ของ Backend DMS | รหัสไฟล์ temp_attachment_id ของ Backend |
|
||||
| Build Enqueue Payload | ประกอบร่างข้อมูลผลวิเคราะห์ AI เข้ากับรหัสไฟล์ เพื่อเตรียมโยนเข้าคิว Migration | โครงสร้าง JSON ที่พร้อมส่งเข้า API Queue (enqueue_payload) |
|
||||
| Enqueue to Review Queue | ยิงข้อมูลเข้า API Backend เพื่อบันทึกเข้าสู่ Review Queue ระบบ DMS | สถานะสำเร็จจากการรับข้อมูลของ Backend |
|
||||
| Save Checkpoint | บันทึกประวัติลง Database ว่าประมวลผลผ่านเอกสารชุดนี้เรียบร้อยแล้ว | อัปเดต last_processed_index สำเร็จ |
|
||||
| Delay | หน่วงเวลา (เช่น 2 วินาที) ก่อนวนรอบขึ้นไปทำข้อมูล Batch ถัดไป | วนลูปกลับไปที่จุด Read Checkpoint |
|
||||
|
||||
## 🚨 กลุ่มที่ 5: การจัดการข้อผิดพลาด (Error Logging)
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ----------------- | ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| Log Reject to CSV | หาก AI ให้คะแนนต่ำกว่าเกณฑ์ จะบันทึกเหตุผลทิ้งไว้ในไฟล์ CSV | บรรทัดข้อมูลใน reject_log.csv |
|
||||
| Log Error to CSV | หากเกิดข้อผิดพลาดในการประมวลผล (เช่น หาไฟล์ไม่เจอ, AI หลอน) จะบันทึกลง CSV | บรรทัดข้อมูลใน error_log.csv |
|
||||
| Log Error to DB | ยิง API ของ Backend เพื่อบันทึก Error เข้าสู่ Database ส่วนกลาง | ข้อมูล Error ในตาราง migration_errors |
|
||||
|
||||
| ชือ Node | หน้าที่ (Function) | ผลลัพธ์ (Output) |
|
||||
| ----------------- | -------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| Log Reject to CSV | หาก AI ให้คะแนนต่ำกว่าเกณฑ์ จะบันทึกเหตุผลทิ้งไว้ในไฟล์ CSV | บรรทัดข้อมูลใน reject_log.csv |
|
||||
| Log Error to CSV | หากเกิดข้อผิดพลาดในการประมวลผล (เช่น หาไฟล์ไม่เจอ, AI หลอน) จะบันทึกลง CSV | บรรทัดข้อมูลใน error_log.csv |
|
||||
| Log Error to DB | ยิง API ของ Backend เพื่อบันทึก Error เข้าสู่ Database ส่วนกลาง | ข้อมูล Error ในตาราง migration_errors |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
||||
# Database Indexing & Performance Strategy
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Context:** Production-scale (100k+ documents, High Concurrency)
|
||||
**Database:** MySQL 8.x (On-Premise via Docker)
|
||||
|
||||
## 1. Core Principles (หลักการสำคัญ)
|
||||
|
||||
ในการออกแบบ Database Index สำหรับระบบ DMS ให้ยึดหลักการตัดสินใจดังนี้:
|
||||
|
||||
1. **Data Integrity First:** ใช้ `UNIQUE INDEX` เพื่อเป็นปราการด่านสุดท้ายป้องกันการเกิด Duplicate Document Number และ Revision ซ้ำซ้อน (แม้ Application Layer จะมี Logic ดักไว้แล้วก็ตาม)
|
||||
2. **Soft-Delete Awareness:** ทุก Index ที่เกี่ยวข้องกับความถูกต้องของข้อมูล ต้องคำนึงถึงคอลัมน์ `deleted_at` เพื่อไม่ให้เอกสารที่ถูกลบไปแล้ว มาขัดขวางการสร้างเอกสารใหม่ที่ใช้เลขเดิม
|
||||
3. **Foreign Key Performance:** สร้าง B-Tree Index ให้กับ Foreign Key (FK) ทุกตัว เพื่อรองรับการ JOIN ข้อมูลที่รวดเร็ว โดยเฉพาะการดึง Workflow และ Routing
|
||||
@@ -13,12 +16,15 @@
|
||||
---
|
||||
|
||||
## 2. Document Control Indexes (ป้องกัน Duplicate & Conflict)
|
||||
|
||||
หัวใจของ DMS คือห้ามมีเอกสารเลขซ้ำในระบบที่ Active อยู่
|
||||
|
||||
### 2.1 Unique Document Number & Revision
|
||||
|
||||
เพื่อรองรับระบบ Soft Delete (`deleted_at`) ใน MySQL การตั้ง Unique Index จำเป็นต้องมีเทคนิคเพื่อจัดการกับค่า `NULL` (เนื่องจาก MySQL มองว่า `NULL` ไม่เท่ากับ `NULL` จึงอาจทำให้เกิด Duplicate ได้ถ้าตั้งค่าไม่รัดกุม)
|
||||
|
||||
**SQL Recommendation (Functional Index - MySQL 8.0+):**
|
||||
|
||||
```sql
|
||||
-- ป้องกันการสร้าง Document No และ Revision ซ้ำ สำหรับเอกสารที่ยังไม่ถูกลบ (Active)
|
||||
ALTER TABLE `documents`
|
||||
@@ -30,7 +36,7 @@ ADD UNIQUE INDEX `idx_unique_active_doc_rev` (
|
||||
|
||||
```
|
||||
|
||||
*เหตุผล:* โครงสร้างนี้รับประกันว่าจะมี `document_no` + `revision` ที่ Active ได้เพียง 1 รายการเท่านั้น แต่สามารถมีรายการที่ถูกลบ (`deleted_at` มีค่า) ซ้ำกันได้
|
||||
_เหตุผล:_ โครงสร้างนี้รับประกันว่าจะมี `document_no` + `revision` ที่ Active ได้เพียง 1 รายการเท่านั้น แต่สามารถมีรายการที่ถูกลบ (`deleted_at` มีค่า) ซ้ำกันได้
|
||||
|
||||
### 2.2 Current/Superseded Flag Index
|
||||
|
||||
@@ -75,7 +81,7 @@ ADD FULLTEXT INDEX `ft_idx_doc_title` (`title`, `subject`);
|
||||
|
||||
```
|
||||
|
||||
*(หมายเหตุ: หากอนาคตมีระบบ OCR หรือค้นหาในเนื้อหาไฟล์ PDF ให้พิจารณาขยับไปใช้ Elasticsearch แยกต่างหาก ไม่ควรเก็บ Full-Text ขนาดใหญ่ไว้ใน MySQL)*
|
||||
_(หมายเหตุ: หากอนาคตมีระบบ OCR หรือค้นหาในเนื้อหาไฟล์ PDF ให้พิจารณาขยับไปใช้ Elasticsearch แยกต่างหาก ไม่ควรเก็บ Full-Text ขนาดใหญ่ไว้ใน MySQL)_
|
||||
|
||||
---
|
||||
|
||||
@@ -104,10 +110,9 @@ ADD INDEX `idx_audit_user_action` (`user_id`, `action`, `created_at`);
|
||||
|
||||
1. **Partitioning:** แนะนำให้ทำ Table Partitioning ตามเดือน (Monthly) หรือปี (Yearly) บนคอลัมน์ `created_at`
|
||||
2. **Minimal Indexing:** ห้ามสร้าง Index เยอะเกินความจำเป็นในตารางนี้ แนะนำแค่:
|
||||
* `INDEX(document_id, created_at)` สำหรับดู History ของเอกสารนั้นๆ
|
||||
* `INDEX(user_id, created_at)` สำหรับตรวจสอบพฤติกรรมผู้ใช้ต้องสงสัย (Security Audit)
|
||||
|
||||
|
||||
- `INDEX(document_id, created_at)` สำหรับดู History ของเอกสารนั้นๆ
|
||||
- `INDEX(user_id, created_at)` สำหรับตรวจสอบพฤติกรรมผู้ใช้ต้องสงสัย (Security Audit)
|
||||
|
||||
```sql
|
||||
-- ตัวอย่างการ Index สำหรับดูกระแสของเอกสาร
|
||||
@@ -122,10 +127,10 @@ ADD INDEX `idx_entity_history` (`entity_type`, `entity_id`, `created_at` DESC);
|
||||
|
||||
เนื่องจากระบบอยู่บน On-Prem NAS (QNAP/ASUSTOR) ทรัพยากร I/O ของดิสก์มีจำกัด (Disk IOPS)
|
||||
|
||||
* **Index Defragmentation:** ให้กำหนด Scheduled Task (ผ่าน Cronjob หรือ MySQL Event) มารัน `OPTIMIZE TABLE` ทุกๆ ไตรมาส สำหรับตารางที่มีการ Delete/Update บ่อย (ช่วยคืนพื้นที่ดิสก์และลด I/O)
|
||||
* **Slow Query Monitoring:** ใน `04-infrastructure-ops/04-01-docker-compose.md` ต้องเปิดใช้งาน `slow_query_log=1` และตั้ง `long_query_time=2` เพื่อตรวจสอบว่ามี Query ใดทำงานแบบ Full Table Scan (ไม่ใช้ Index) หรือไม่
|
||||
|
||||
- **Index Defragmentation:** ให้กำหนด Scheduled Task (ผ่าน Cronjob หรือ MySQL Event) มารัน `OPTIMIZE TABLE` ทุกๆ ไตรมาส สำหรับตารางที่มีการ Delete/Update บ่อย (ช่วยคืนพื้นที่ดิสก์และลด I/O)
|
||||
- **Slow Query Monitoring:** ใน `04-infrastructure-ops/04-01-docker-compose.md` ต้องเปิดใช้งาน `slow_query_log=1` และตั้ง `long_query_time=2` เพื่อตรวจสอบว่ามี Query ใดทำงานแบบ Full Table Scan (ไม่ใช้ Index) หรือไม่
|
||||
|
||||
## 💡 คำแนะนำเพิ่มเติมจาก Architect (Architect's Notes):
|
||||
|
||||
1. **เรื่อง Soft Delete กับ Unique Constraint:** เป็นจุดที่นักพัฒนาพลาดกันบ่อยที่สุด ถ้าระบบอนุญาตให้ลบ `DOC-001 Rev.0` แล้วสร้าง `DOC-001 Rev.0` ใหม่ได้ การจัดการ Unique Constraint บน MySQL ต้องใช้ Functional Index (ตามตัวอย่างในข้อ 2.1) เพื่อป้องกันการตีกันของค่า `NULL` ในฐานข้อมูล
|
||||
2. **ลดภาระ QNAP/ASUSTOR:** อุปกรณ์จำพวก NAS On-Premise มักจะมีปัญหาเรื่อง Random Read/Write Disk I/O การใช้ **Composite Index** แบบครบคลุม (Covering Index) จะช่วยให้ MySQL คืนค่าได้จาก Index Tree โดยตรง ไม่ต้องกระโดดไปอ่าน Data File จริง ซึ่งจะช่วยรีด Performance ของ NAS ได้สูงสุดครับ
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
# 3.3 File Storage and Handling
|
||||
|
||||
---
|
||||
|
||||
title: 'Data & Storage: File Storage and Handling (Two-Phase)'
|
||||
version: 1.8.0
|
||||
status: drafted
|
||||
owner: Nattanin Peancharoen
|
||||
last_updated: 2026-02-22
|
||||
related:
|
||||
|
||||
- specs/01-requirements/01-03.10-file-handling.md (Merged)
|
||||
- specs/03-Data-and-Storage/ADR-003-file-storage-approach.md (Merged)
|
||||
- specs/02-architecture/02-01-system-architecture.md
|
||||
- ADR-006-security-best-practices
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview and Core Infrastructure Requirements
|
||||
@@ -18,12 +21,15 @@ related:
|
||||
เอกสารฉบับนี้รวบรวมข้อกำหนดการจัดการไฟล์และการจัดเก็บไฟล์ (File Storage Approach) สำหรับ LCBP3-DMS โดยมีข้อบังคับด้าน Infrastructure และ Security ที่สำคัญมากดังต่อไปนี้:
|
||||
|
||||
### 1.1 Infrastructure Requirement (การจัดเก็บและ Mount Volume)
|
||||
|
||||
**สำคัญ (CRITICAL SPECIFICATION):**
|
||||
|
||||
1. **Outside Webroot:** ไฟล์รูปและเอกสารทั้งหมดต้องถูกจัดเก็บไว้ **ภายนอก Webroot ของ Application** ห้ามเก็บไฟล์รูปหรือเอกสารไว้ใน Container หรือโฟลเดอร์ Webroot เด็ดขาด เพื่อป้องกันการเข้าถึงไฟล์โดยตรงจากสาธารณะ (Direct Public Access)
|
||||
2. **QNAP Volume Mount:** ต้องใช้ **QNAP Volume Mount เข้า Docker** (Mount external volume from QNAP NAS to Docker container) สำหรับเป็นพื้นที่เก็บไฟล์ Storage ให้ Container ดึงไปใช้งาน
|
||||
3. **Authenticated Endpoint:** ไฟล์ต้องถูกเข้าถึงและให้บริการดาวน์โหลดผ่าน Authenticated Endpoint ในฝั่ง Backend เท่านั้น โดยต้องผ่านการตรวจสอบสิทธิ์ (RBAC / Junction Table) เสียก่อน
|
||||
|
||||
### 1.2 Access & Security Rules
|
||||
|
||||
- **Virus Scan:** ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party ก่อนการบันทึก
|
||||
- **Whitelist File Types:** อนุญาตเฉพาะเอกสารตามที่กำหนด: PDF, DWG, DOCX, XLSX, ZIP
|
||||
- **Max File Size:** ขนาดไฟล์สูงสุดไม่เกิน 50MB ต่อไฟล์ (Total max 500MB per form submission)
|
||||
@@ -36,7 +42,9 @@ related:
|
||||
## 2. Two-Phase File Storage Approach (ADR-003)
|
||||
|
||||
### 2.1 Context and Problem Statement
|
||||
|
||||
LCBP3-DMS ต้องจัดการ File Uploads สำหรับ Attachments ของเอกสาร (PDF, DWG, DOCX, etc.) โดยต้องรับมือกับปัญหา:
|
||||
|
||||
1. **Orphan Files:** User อัปโหลดไฟล์แล้วไม่ Submit Form ทำให้ไฟล์ค้างใน Storage
|
||||
2. **Transaction Integrity:** ถ้า Database Transaction Rollback ไฟล์ยังอยู่ใน Storage ต้องสอดคล้องกับ Database Record
|
||||
3. **Virus Scanning:** ต้อง Scan ไฟล์ก่อน Save เข้าระบบถาวร
|
||||
@@ -44,6 +52,7 @@ LCBP3-DMS ต้องจัดการ File Uploads สำหรับ Attachm
|
||||
5. **Storage Organization:** จัดเก็บไฟล์แยกเป็นสัดส่วน (เพื่อไม่ให้ QNAP Storage กระจัดกระจายและจำกัดขนาดได้)
|
||||
|
||||
### 2.2 Decision Drivers
|
||||
|
||||
- **Data Integrity:** File และ Database Record ต้อง Consistent
|
||||
- **Security:** ป้องกัน Virus และ Malicious Files
|
||||
- **User Experience:** Upload ต้องรวดเร็ว ไม่ Block UI (ถ้าอัปโหลดพร้อม Submit อาจทำให้ระบบดูค้าง)
|
||||
@@ -51,11 +60,13 @@ LCBP3-DMS ต้องจัดการ File Uploads สำหรับ Attachm
|
||||
- **Auditability:** ติดตามประวัติ File Operations ได้
|
||||
|
||||
### 2.3 Considered Options & Decision
|
||||
|
||||
- **Option 1:** Direct Upload to Permanent Storage (ทิ้งไฟล์ถ้า Transaction Fail / ได้ Orphan Files) - ❌
|
||||
- **Option 2:** Upload after Form Submission (UX แย่ ผู้ใช้ต้องรออัปโหลดรวดเดียวท้ายสุด) - ❌
|
||||
- **Option 3: Two-Phase Storage (Temp → Permanent) ⭐ (Selected Option)** - ✅
|
||||
|
||||
**แนวทาง Two-Phase Storage (Temp → Permanent):**
|
||||
|
||||
1. **Phase 1 (Upload):** ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ `temp/` ได้รับ `temp_id`
|
||||
2. **Phase 2 (Commit):** เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/` และบันทึกลง Database ใน Transaction เดียวกัน
|
||||
3. **Cleanup:** มี Cron Job ทำหน้าที่ลบไฟล์ใน `temp/` ที่ค้างเกินกำหนด (เช่น 24 ชั่วโมง)
|
||||
@@ -65,6 +76,7 @@ LCBP3-DMS ต้องจัดการ File Uploads สำหรับ Attachm
|
||||
## 3. Implementation Details
|
||||
|
||||
### 3.1 Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE attachments (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -89,6 +101,7 @@ CREATE TABLE attachments (
|
||||
```
|
||||
|
||||
### 3.2 Two-Phase Storage Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Client
|
||||
@@ -205,7 +218,12 @@ export class FileStorageService {
|
||||
}
|
||||
|
||||
// Phase 2: Commit to Permanent (within Transaction Manager)
|
||||
async commitFiles(tempIds: string[], entityId: number, entityType: string, manager: EntityManager): Promise<Attachment[]> {
|
||||
async commitFiles(
|
||||
tempIds: string[],
|
||||
entityId: number,
|
||||
entityType: string,
|
||||
manager: EntityManager
|
||||
): Promise<Attachment[]> {
|
||||
const attachments = [];
|
||||
|
||||
for (const tempId of tempIds) {
|
||||
@@ -227,13 +245,17 @@ export class FileStorageService {
|
||||
await fs.move(tempAttachment.file_path, permanentPath);
|
||||
|
||||
// Update Database record
|
||||
await manager.update(Attachment, { id: tempAttachment.id }, {
|
||||
file_path: permanentPath,
|
||||
stored_filename: permanentFilename,
|
||||
is_temporary: false,
|
||||
temp_id: null,
|
||||
expires_at: null,
|
||||
});
|
||||
await manager.update(
|
||||
Attachment,
|
||||
{ id: tempAttachment.id },
|
||||
{
|
||||
file_path: permanentPath,
|
||||
stored_filename: permanentFilename,
|
||||
is_temporary: false,
|
||||
temp_id: null,
|
||||
expires_at: null,
|
||||
}
|
||||
);
|
||||
|
||||
attachments.push(tempAttachment);
|
||||
}
|
||||
@@ -283,7 +305,9 @@ export class FileStorageService {
|
||||
```
|
||||
|
||||
### 3.4 API Controller Context
|
||||
|
||||
ในส่วนของตัว Controller ฝ่ายรับข้อมูลจะต้องแยกระหว่าง Uploading กับ Comit:
|
||||
|
||||
1. `POST /attachments/upload` ใช้เพื่อรับไฟล์และ Return `temp_id` แก่ User ทันที
|
||||
2. `POST /correspondences` หรือ Object อื่นๆ ใช้เพื่อ Commit Database โดยจะรับ `temp_file_ids: []` พ่วงมากับ Body form
|
||||
|
||||
@@ -292,6 +316,7 @@ export class FileStorageService {
|
||||
## 4. Consequences & Mitigation Strategies
|
||||
|
||||
### Positive Consequences
|
||||
|
||||
1. ✅ **Fast Upload UX:** User upload แบบ Async ก่อน Submit ดำเนินการลื่นไหล
|
||||
2. ✅ **No Orphan Files:** เกิดระบบ Auto-cleanup จัดการไฟล์หมดอายุโดยอัตโนมัติ ไม่เปลืองสเปซ QNAP
|
||||
3. ✅ **Transaction Safe:** Rollback ได้สมบูรณ์หากบันทึกฐานข้อมูลผิดพลาด ไฟล์จะถูก Cron จัดการให้ทีหลังไม่ตกค้างในระบบ
|
||||
@@ -300,16 +325,18 @@ export class FileStorageService {
|
||||
6. ✅ **Storage Organization:** จัดเก็บอย่างเป็นระเบียบ ด้วยรูปแบบ YYYY/MM ลดคอขวด IO Operations ในระบบ
|
||||
|
||||
### Negative Consequences & Mitigations
|
||||
|
||||
1. ❌ **Complexity:** ต้อง Implement 2 phases ซึ่งซับซ้อนขึ้น
|
||||
👉 *Mitigation:* รวบ Logic ทุกอย่างให้เป็น Service ชั้นเดียว (`FileStorageService`) เพื่อให้จัดการง่ายและเรียกใช้ง่ายที่สุด
|
||||
👉 _Mitigation:_ รวบ Logic ทุกอย่างให้เป็น Service ชั้นเดียว (`FileStorageService`) เพื่อให้จัดการง่ายและเรียกใช้ง่ายที่สุด
|
||||
2. ❌ **Extra Storage:** ต้องใช้พื้นที่ QNAP ในส่วน Temp directory ควบคู่ไปกับแบบ Permanent
|
||||
👉 *Mitigation:* คอย Monitor และปรับรอบความถี่ของการ Cleanup หากไฟล์มีปริมาณไหลเวียนเยอะมาก
|
||||
👉 _Mitigation:_ คอย Monitor และปรับรอบความถี่ของการ Cleanup หากไฟล์มีปริมาณไหลเวียนเยอะมาก
|
||||
3. ❌ **Edge Cases:** อาจเกิดประเด็นเรื่อง File lock หรือ missing temp files
|
||||
👉 *Mitigation:* ทำ Proper error handling พร้อม Logging ให้ตรวจสอบได้ง่าย
|
||||
👉 _Mitigation:_ ทำ Proper error handling พร้อม Logging ให้ตรวจสอบได้ง่าย
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Optimization Consideration
|
||||
|
||||
- **Streaming:** ใช้ multipart/form-data streaming เพิ่อลดภาระ Memory ของฝั่งเครื่องเซิฟเวอร์ (NestJS) ขณะสูบไฟล์ใหญ่ๆ
|
||||
- **Compression:** พิจารณาเรื่องการบีบอัดสำหรับไฟล์ขนาดใหญ่หรือบางประเภท
|
||||
- **Deduplication Check:** สามารถใช้งาน Field `checksum` ดักการ Commit ด้วยข้อมูลชุดเดิมที่เคยถูกอัปโหลดเพื่อประหยัดพื้นที่จัดเก็บ (Deduplicate)
|
||||
|
||||
@@ -35,25 +35,28 @@
|
||||
### Phase 1: การเตรียม Infrastructure และ Storage (สัปดาห์ที่ 1)
|
||||
|
||||
**File Migration:**
|
||||
|
||||
- ย้ายไฟล์ PDF ทั้งหมดจากแหล่งเก็บไปยัง Folder ชั่วคราวบน NAS (QNAP)
|
||||
- Target Path: `/share/np-dms/staging_ai/`
|
||||
|
||||
**Mount Folder:**
|
||||
|
||||
- Bind Mount `/share/np-dms/staging_ai/` เข้ากับ n8n Container แบบ **read-only**
|
||||
- สร้าง `/share/np-dms/n8n/migration_logs/` Volume แยกสำหรับเขียน Log แบบ **read-write**
|
||||
|
||||
**Ollama Config:**
|
||||
|
||||
- ติดตั้ง Ollama บน Desktop (Desk-5439, RTX 2060 SUPER 8GB)
|
||||
- No DB credentials, Internal network only
|
||||
|
||||
#### 🔍 เปรียบเทียบผลลัพธ์ที่คาดหวัง
|
||||
|
||||
| งาน | Typhoon2-4B | Qwen2.5-7B | OpenThaiGPT-7B |
|
||||
|-----|-------------|------------|----------------|
|
||||
| ความเร็ว (ток/วินาที) | ~35-45 | ~8-12 | ~10-15 |
|
||||
| ความเข้าใจบริบทไทย | ดีมาก | ดี | ดีมาก |
|
||||
| การสร้างแท็กแม่นยำ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| ความเสถียรบน 8GB | ✅ สูง | ⚠️ ปานกลาง | ⚠️ ปานกลาง |
|
||||
| งาน | Typhoon2-4B | Qwen2.5-7B | OpenThaiGPT-7B |
|
||||
| --------------------- | ----------- | ---------- | -------------- |
|
||||
| ความเร็ว (ток/วินาที) | ~35-45 | ~8-12 | ~10-15 |
|
||||
| ความเข้าใจบริบทไทย | ดีมาก | ดี | ดีมาก |
|
||||
| การสร้างแท็กแม่นยำ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| ความเสถียรบน 8GB | ✅ สูง | ⚠️ ปานกลาง | ⚠️ ปานกลาง |
|
||||
|
||||
```bash
|
||||
# แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) หรือ ollama run llama3.2:3b
|
||||
@@ -79,15 +82,18 @@ watch -n 1 nvidia-smi
|
||||
# Fallback: mistral:7b-instruct-q4_K_M (แม่นกว่า, VRAM ~5GB)
|
||||
# ollama pull mistral:7b-instruct-q4_K_M
|
||||
```
|
||||
|
||||
ใช้ ทางเลือกที่ 1
|
||||
|
||||
**ทดสอบ Ollama:**
|
||||
|
||||
```bash
|
||||
curl http://192.168.20.100:11434/api/generate \
|
||||
-d '{"model":"llama3.2:3b","prompt":"reply: ok","stream":false}'
|
||||
```
|
||||
|
||||
**Concurrency Configuration:**
|
||||
|
||||
- Sequential: Batch Size = 1, Delay ≥ 2 วินาที, ปิด Parallel Execution
|
||||
- เพิ่ม Health Check Node ก่อนเริ่ม Batch เพื่อป้องกัน Workflow ค้างหาก Desktop Sleep หรือ Overheat
|
||||
|
||||
@@ -96,6 +102,7 @@ curl http://192.168.20.100:11434/api/generate \
|
||||
### Phase 2: การเตรียม Target Database และ API (สัปดาห์ที่ 1)
|
||||
|
||||
**SQL Indexing:**
|
||||
|
||||
```sql
|
||||
ALTER TABLE correspondences ADD INDEX idx_doc_number (document_number);
|
||||
ALTER TABLE correspondences ADD INDEX idx_deleted_at (deleted_at);
|
||||
@@ -103,6 +110,7 @@ ALTER TABLE correspondences ADD INDEX idx_created_by (created_by);
|
||||
```
|
||||
|
||||
**Checkpoint Table:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS migration_progress (
|
||||
batch_id VARCHAR(50) PRIMARY KEY,
|
||||
@@ -113,6 +121,7 @@ CREATE TABLE IF NOT EXISTS migration_progress (
|
||||
```
|
||||
|
||||
**Tags Table (สำหรับ AI Tag Extraction):**
|
||||
|
||||
```sql
|
||||
-- ตาราง Master เก็บ Tags (Global หรือ Project-specific)
|
||||
CREATE TABLE tags (
|
||||
@@ -143,6 +152,7 @@ CREATE TABLE correspondence_tags (
|
||||
```
|
||||
|
||||
**Idempotency Table :**
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS import_transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -158,6 +168,7 @@ CREATE TABLE IF NOT EXISTS import_transactions (
|
||||
> **Idempotency Logic:** ถ้า `idempotency_key` ซ้ำ → Backend คืน HTTP 200 ทันที (ไม่สร้าง Revision ซ้ำ) ถ้าไม่ซ้ำ → ประมวลผลปกติ
|
||||
|
||||
**API Authentication — Migration Token:**
|
||||
|
||||
```sql
|
||||
INSERT INTO users (username, email, role, is_active)
|
||||
VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true);
|
||||
@@ -165,14 +176,14 @@ VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true);
|
||||
|
||||
**Scope ของ Migration Token (Patch — คำนิยามชัดเจน):**
|
||||
|
||||
| สิทธิ์ | ปกติ | Migration Token | หมายเหตุ |
|
||||
| ------------------------------------- | --- | --------------- | --------------------------------- |
|
||||
| Bypass File Virus Scan | ❌ | ✅ | ไฟล์ผ่าน Scan มาแล้วก่อน Import |
|
||||
| Bypass Duplicate **Validation Error** | ❌ | ✅ | **Revision Logic ยัง enforce ปกติ** |
|
||||
| Bypass Created-by User validation | ❌ | ✅ | |
|
||||
| Overwrite existing revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
| Delete previous revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
| ลบ / แก้ไข Record อื่น | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
| สิทธิ์ | ปกติ | Migration Token | หมายเหตุ |
|
||||
| ------------------------------------- | ---- | --------------- | ----------------------------------- |
|
||||
| Bypass File Virus Scan | ❌ | ✅ | ไฟล์ผ่าน Scan มาแล้วก่อน Import |
|
||||
| Bypass Duplicate **Validation Error** | ❌ | ✅ | **Revision Logic ยัง enforce ปกติ** |
|
||||
| Bypass Created-by User validation | ❌ | ✅ | |
|
||||
| Overwrite existing revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
| Delete previous revision | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
| ลบ / แก้ไข Record อื่น | ❌ | ❌ | **ห้ามโดยเด็ดขาด** |
|
||||
|
||||
> ⚠️ **Patch Clarification:** "Bypass Duplicate Number Check" ถูกแทนด้วย "Bypass Duplicate **Validation Error**" — Revision increment logic ยังทำงานตามปกติทุกกรณี
|
||||
|
||||
@@ -194,21 +205,25 @@ VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true);
|
||||
4. File Mount Check → `staging_ai` มีไฟล์, `migration_logs` เขียนได้
|
||||
|
||||
**Fetch System Categories (Patch — ห้าม hardcode):**
|
||||
|
||||
```http
|
||||
GET /api/meta/categories
|
||||
Authorization: Bearer <MIGRATION_TOKEN>
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "categories": ["Correspondence","RFA","Drawing","Transmittal","Report","Other"] }
|
||||
{ "categories": ["Correspondence", "RFA", "Drawing", "Transmittal", "Report", "Other"] }
|
||||
```
|
||||
|
||||
n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variable (`system_categories`) และ inject เข้า AI Prompt ทุก Request
|
||||
|
||||
#### Node 1: Data Reader & Checkpoint
|
||||
|
||||
#### Node 1: Data Reader & Checkpoint
|
||||
|
||||
- อ่าน Checkpoint จาก **MariaDB Node แยก**
|
||||
- อ่าน Checkpoint จาก **MariaDB Node แยก**
|
||||
- Batch ทีละ **50–100 แถว** ตาม `$env.MIGRATION_BATCH_SIZE` (ควรจำกัด Batch Size ป้องกัน DB Connection Overload)
|
||||
- ติด `original_index` ทุก Item และ Normalize Encoding (UTF-8 NFC) สำหรับ ชื่อไฟล์ และ เลขเอกสารเก่า
|
||||
|
||||
@@ -221,7 +236,7 @@ n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variab
|
||||
3. แปลง `receiver_code` -> `receiver_organization_id`
|
||||
4. หา Tags ที่มีอยู่ในโปรเจ็กต์: `SELECT * FROM tags WHERE project_id = {{project_id}}`
|
||||
- **Output:** n8n เก็บ `project_id`, `organization_ids` และ `existing_tags_json` ไว้ในแต่ละ item
|
||||
- *ถ้าหารหัสโปรเจ็กต์ไม่เจอ ให้ส่งเข้า Error Log ไม่ทำต่อ*
|
||||
- _ถ้าหารหัสโปรเจ็กต์ไม่เจอ ให้ส่งเข้า Error Log ไม่ทำต่อ_
|
||||
|
||||
#### Node 3: File Processor (Extract PDF Text & Temp Upload)
|
||||
|
||||
@@ -235,6 +250,7 @@ n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variab
|
||||
#### Node 4: AI Analysis (Sequential เท่านั้น)
|
||||
|
||||
**System Prompt:**
|
||||
|
||||
```text
|
||||
You are a Document Controller for a large construction project.
|
||||
Your task is to validate document metadata, summarize content, and suggest relevant tags.
|
||||
@@ -242,6 +258,7 @@ You MUST respond ONLY with valid JSON. No explanation, no markdown.
|
||||
```
|
||||
|
||||
**User Prompt:**
|
||||
|
||||
```text
|
||||
Validate and summarize this document. Respond in JSON.
|
||||
Document Number: {{$json.document_number}}
|
||||
@@ -273,12 +290,14 @@ Respond ONLY with this exact JSON structure:
|
||||
ข้อมูลทั้งหมดที่ผ่าน n8n และ AI Model **จะต้องไม่ถูกอัพเดทเข้าตารางหลักอัตโนมัติ** แต่จะถูกบังคับนำเข้าตาราง Staging `migration_review_queue` แทน เพื่อรอมนุษย์จัดการผ่าน Frontend UI
|
||||
|
||||
**Status Routing Policy:**
|
||||
|
||||
- `confidence >= 0.85` และ `is_valid = true` -> Status **`PENDING`** (พร้อมรับ Batch Import)
|
||||
- `confidence >= 0.60` และ `< 0.85` -> Status **`PENDING`** (ติด Flag ให้ระวัง)
|
||||
- `confidence < 0.60` หรือ `is_valid = false` -> Status **`REJECTED`**
|
||||
- Parse Error / AI ไม่ตอบ -> **Error Log** (Node ถัดไป)
|
||||
|
||||
**Insert into staging:**
|
||||
|
||||
```sql
|
||||
INSERT INTO migration_review_queue (
|
||||
document_number, title, project_id, sender_organization_id, receiver_organization_id,
|
||||
@@ -290,7 +309,7 @@ ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary)
|
||||
|
||||
#### Node 6: Error Log & Reject Log
|
||||
|
||||
- Parse Error → เขียนลงไฟล์ `/share/np-dms/n8n/migration_logs/error_log.csv`
|
||||
- Parse Error → เขียนลงไฟล์ `/share/np-dms/n8n/migration_logs/error_log.csv`
|
||||
- ทุก 10-50 ราบการอัพเดท MariaDB `migration_progress` เพื่อเป็น Checkpoint.
|
||||
|
||||
---
|
||||
@@ -308,10 +327,12 @@ ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary)
|
||||
### Phase 4: แผนการทดสอบ (Testing & QA)
|
||||
|
||||
**Dry Run Policy (Mandatory):**
|
||||
|
||||
- All migrations MUST run with `--dry-run`
|
||||
- No DB commit until validation approved
|
||||
|
||||
**Dry Run Validation (20–50 แถว):**
|
||||
|
||||
- JSON Parse Success Rate > 95%
|
||||
- Category ที่ AI ตอบตรงกับ System Enum ทุกรายการ
|
||||
- รัน Batch เดิมซ้ำ 2 รอบ → ต้องไม่สร้าง Duplicate หรือ Revision ซ้ำ (Idempotency Test)
|
||||
@@ -319,6 +340,7 @@ ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary)
|
||||
- Revision Drift ถูก route ไป Review Queue
|
||||
|
||||
**Integrity Check:**
|
||||
|
||||
```sql
|
||||
-- ตรวจยอด
|
||||
SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';
|
||||
@@ -351,11 +373,13 @@ WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT';
|
||||
## 4. Rollback Plan
|
||||
|
||||
**Step 1:** หยุด n8n และ Disable Token
|
||||
|
||||
```sql
|
||||
UPDATE users SET is_active = false WHERE username = 'migration_bot';
|
||||
```
|
||||
|
||||
**Step 2:** ลบ Records (Transaction)
|
||||
|
||||
```sql
|
||||
START TRANSACTION;
|
||||
DELETE FROM correspondence_files
|
||||
@@ -369,6 +393,7 @@ COMMIT;
|
||||
**Step 3:** ย้ายไฟล์กลับ `/share/np-dms/staging_ai/` ผ่าน Script แยก
|
||||
|
||||
**Step 4:** Reset State
|
||||
|
||||
```sql
|
||||
UPDATE migration_progress
|
||||
SET status = 'FAILED', last_processed_index = 0
|
||||
@@ -383,19 +408,19 @@ WHERE batch_id = 'migration_20260226';
|
||||
|
||||
## 5. แผนรับมือความเสี่ยง (Risk Management)
|
||||
|
||||
| ลำดับที่ | ความเสี่ยง | การจัดการ (Mitigation) |
|
||||
| ---- | -------------------------- | -------------------------------------------------- |
|
||||
| 1 | AI Node หรือ GPU ค้าง | Timeout 30 วินาที, Retry 3 รอบ, Delay 60 วินาที |
|
||||
| 2 | Ollama ตอบไม่ใช่ JSON | JSON Pre-processor + ส่ง Human Review Queue |
|
||||
| 3 | Category ไม่ตรง System Enum | Fetch `/api/meta/categories` ก่อน Batch ทุกครั้ง |
|
||||
| 4 | Idempotency ซ้ำ | `import_transactions` table + Backend คืน HTTP 200 |
|
||||
| 5 | Revision Drift | ตรวจ Excel revision column → Route ไป Review Queue |
|
||||
| 6 | Storage bypass | ห้าม move file โดยตรง — ผ่าน Backend API เท่านั้น |
|
||||
| 7 | GPU VRAM Overflow | ใช้เฉพาะ Quantized Model (q4_K_M) |
|
||||
| 8 | ดิสก์ NAS เต็ม | ปิด "Save Successful Executions" ใน n8n |
|
||||
| 9 | Migration Token ถูกขโมย | Token 7 วัน, IP Whitelist `<NAS_IP>` เท่านั้น |
|
||||
| 11 | AI Tag Extraction ผิดพลาด | Tag confidence < 0.6 → ส่งไป Review Queue / บันทึกใน metadata |
|
||||
| 12 | Tag ซ้ำ/คล้ายกัน | Normalization ก่อนบันทึก (lowercase, trim, deduplicate) |
|
||||
| ลำดับที่ | ความเสี่ยง | การจัดการ (Mitigation) |
|
||||
| -------- | --------------------------- | ------------------------------------------------------------- |
|
||||
| 1 | AI Node หรือ GPU ค้าง | Timeout 30 วินาที, Retry 3 รอบ, Delay 60 วินาที |
|
||||
| 2 | Ollama ตอบไม่ใช่ JSON | JSON Pre-processor + ส่ง Human Review Queue |
|
||||
| 3 | Category ไม่ตรง System Enum | Fetch `/api/meta/categories` ก่อน Batch ทุกครั้ง |
|
||||
| 4 | Idempotency ซ้ำ | `import_transactions` table + Backend คืน HTTP 200 |
|
||||
| 5 | Revision Drift | ตรวจ Excel revision column → Route ไป Review Queue |
|
||||
| 6 | Storage bypass | ห้าม move file โดยตรง — ผ่าน Backend API เท่านั้น |
|
||||
| 7 | GPU VRAM Overflow | ใช้เฉพาะ Quantized Model (q4_K_M) |
|
||||
| 8 | ดิสก์ NAS เต็ม | ปิด "Save Successful Executions" ใน n8n |
|
||||
| 9 | Migration Token ถูกขโมย | Token 7 วัน, IP Whitelist `<NAS_IP>` เท่านั้น |
|
||||
| 11 | AI Tag Extraction ผิดพลาด | Tag confidence < 0.6 → ส่งไป Review Queue / บันทึกใน metadata |
|
||||
| 12 | Tag ซ้ำ/คล้ายกัน | Normalization ก่อนบันทึก (lowercase, trim, deduplicate) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
|
||||
## ⚠️ ความแตกต่างจากเวอร์ชัน Enterprise
|
||||
|
||||
| ฟีเจอร์ | Enterprise | Free Plan (นี้) |
|
||||
| --------------------- | ----------------------- | ------------------------------ |
|
||||
| Environment Variables | ✅ ใช้ `$env` | ❌ ใช้ `Set Node` + `staticData` |
|
||||
| External Secrets | ✅ Vault/Secrets Manager | ❌ Hardcode ใน Set Node |
|
||||
| Multiple Workflows | ✅ Unlimited | ⚠️ รวมเป็น Workflow เดียว |
|
||||
| Error Handling | ✅ Advanced | ⚠️ Manual Retry |
|
||||
| Webhook Triggers | ✅ | ✅ ใช้ได้ |
|
||||
| ฟีเจอร์ | Enterprise | Free Plan (นี้) |
|
||||
| --------------------- | ------------------------ | -------------------------------- |
|
||||
| Environment Variables | ✅ ใช้ `$env` | ❌ ใช้ `Set Node` + `staticData` |
|
||||
| External Secrets | ✅ Vault/Secrets Manager | ❌ Hardcode ใน Set Node |
|
||||
| Multiple Workflows | ✅ Unlimited | ⚠️ รวมเป็น Workflow เดียว |
|
||||
| Error Handling | ✅ Advanced | ⚠️ Manual Retry |
|
||||
| Webhook Triggers | ✅ | ✅ ใช้ได้ |
|
||||
|
||||
---
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
**สิ่งสำคัญ:**
|
||||
|
||||
| Item | ค่า Production |
|
||||
| Item | ค่า Production |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| Image | `n8nio/n8n:latest` |
|
||||
| Container | `n8n` |
|
||||
@@ -134,7 +134,7 @@ const CONFIG = {
|
||||
|
||||
// Thresholds
|
||||
CONFIDENCE_HIGH: 0.85,
|
||||
CONFIDENCE_LOW: 0.60,
|
||||
CONFIDENCE_LOW: 0.6,
|
||||
MAX_RETRY: 3,
|
||||
FALLBACK_THRESHOLD: 5,
|
||||
|
||||
@@ -147,14 +147,14 @@ const CONFIG = {
|
||||
DB_PORT: 3306,
|
||||
DB_NAME: 'lcbp3_production',
|
||||
DB_USER: 'migration_bot',
|
||||
DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE' // 🔴 เปลี่ยน
|
||||
DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE', // 🔴 เปลี่ยน
|
||||
};
|
||||
|
||||
// อย่าแก้โค้ดด้านล่างนี้
|
||||
$workflow.staticData = $workflow.staticData || {};
|
||||
$workflow.staticData.config = CONFIG;
|
||||
|
||||
return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}];
|
||||
return [{ json: { config_loaded: true, timestamp: new Date().toISOString() } }];
|
||||
```
|
||||
|
||||
### ขั้นตอนที่ 2: ตั้งค่า Credentials ใน n8n UI
|
||||
@@ -166,7 +166,6 @@ return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}];
|
||||
3. **Rotate Token ทันทีหลัง Migration เสร็จ**
|
||||
4. **💡 หมายเหตุ:** Backend ระบบ DMS ได้ถูกตั้งค่าให้สร้าง Token แบบไม่มีวันหมดอายุ (100 ปี) สำหรับ User ชื่อ `migration_bot` โดยเฉพาะ เพื่อป้องกันปัญหา Token หมดอายุระหว่างที่ Workflow กำลังทำงานข้ามวัน
|
||||
|
||||
|
||||
**Credentials (ถ้าใช้):**
|
||||
|
||||
| Credential | Type | ใช้ใน Node |
|
||||
@@ -175,7 +174,6 @@ return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}];
|
||||
| LCBP3 Backend | HTTP Request | Import to Backend, Fetch Categories |
|
||||
| MariaDB | MySQL | ทุก Database Node |
|
||||
|
||||
|
||||
### ขั้นตอนที่ 3: วิธีการรับ MIGRATION_TOKEN
|
||||
|
||||
เนื่องจากหน้าเว็บ DMS ใช้ระบบ Session Cookies (Auth.js) จึงไม่สามารถคัดลอก JWT Token จาก Network Tab ในเบราว์เซอร์ได้โดยตรง
|
||||
@@ -183,6 +181,7 @@ return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}];
|
||||
ให้ใช้วิธี **เรียก API ตรงไปที่ Backend** ด้วยเครื่องมืออย่าง Postman, cURL หรือ Thunder Client แทน:
|
||||
|
||||
**ตัวอย่างคำสั่ง cURL:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.np-dms.work/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -190,6 +189,7 @@ curl -X POST https://api.np-dms.work/api/auth/login \
|
||||
```
|
||||
|
||||
**การนำไปใช้งาน:**
|
||||
|
||||
1. เปลี่ยน URL ให้ตรงกับ Backend ของคุณ (เช่น `http://localhost:3001/api/auth/login` สำหรับ Local)
|
||||
2. นำรหัสผ่านของบัญชี `migration_bot` มาใส่แทนที่ `YOUR_PASSWORD`
|
||||
3. ในผลลัพธ์ที่ได้ ให้คัดลอกเฉพาะค่าจากฟิลด์ `access_token` (ข้อความยาวๆ)
|
||||
@@ -211,59 +211,67 @@ mysql -h <DB_HOST> -u migration_bot -p lcbp3_production < lcbp3-v1.8.0-migration
|
||||
**ตารางที่สร้าง (6 ตาราง ชั่วคราว — ลบได้หลัง Migration เสร็จ):**
|
||||
|
||||
| ตาราง | วัตถุประสงค์ |
|
||||
| -------------------------- | ------------------------------- |
|
||||
| -------------------------- | ---------------------------------- |
|
||||
| `migration_progress` | Checkpoint ติดตามความคืบหน้า Batch |
|
||||
| `migration_review_queue` | รายการที่ต้องตรวจสอบโดยคน |
|
||||
| `migration_errors` | Error Log |
|
||||
| `migration_fallback_state` | สถานะ AI Model Fallback |
|
||||
| `import_transactions` | Idempotency ป้องกัน Import ซ้ำ |
|
||||
| `migration_daily_summary` | สรุปผลรายวัน |
|
||||
| `migration_errors` | Error Log |
|
||||
| `migration_fallback_state` | สถานะ AI Model Fallback |
|
||||
| `import_transactions` | Idempotency ป้องกัน Import ซ้ำ |
|
||||
| `migration_daily_summary` | สรุปผลรายวัน |
|
||||
|
||||
---
|
||||
|
||||
## 📌 ส่วนที่ 4: การทำงานของแต่ละ Node
|
||||
|
||||
### Node 0: Set Configuration
|
||||
|
||||
- เก็บค่า Config ทั้งหมดใน `$workflow.staticData.config`
|
||||
- อ่านผ่าน `$workflow.staticData.config.KEY` ใน Node อื่น
|
||||
|
||||
### Node 1: Pre-flight Checks & Data Reader
|
||||
|
||||
- ตรวจสอบ Backend Health และ Ollama Ping
|
||||
- อ่าน Checkpoint (`last_processed_index`) จาก `migration_progress`
|
||||
- Batch ข้อมูลจาก Excel ตามตาราง `BATCH_SIZE` ปกติ (50-100)
|
||||
- Normalize ข้อมูล UTF-8 (NFC) และสร้าง `original_index`
|
||||
|
||||
### Node 2: DB Lookup & Categories Fetch
|
||||
|
||||
- ดึง Categories จาก `/api/meta/categories` เพื่อเตรียม Prompt
|
||||
- Query ทะลวง DB: แปลงรหัสใน Excel (`project_code`, `sender`, `receiver`) ให้เป็น IDs จาก MariaDB
|
||||
- Query ดึง Master Tags ของโปรเจ็กต์: `SELECT tag_name, description FROM tags WHERE project_id = ...`
|
||||
- Output: แปลง ID เรียบร้อยและเตรียม `existing_tags_json` ให้ Ollama
|
||||
|
||||
### Node 3: Text Extraction & Temp Upload
|
||||
|
||||
- ใช้ **Apache Tika** (ผ่าน `Extract PDF Text` node หรือ HTTP Request) สกัดข้อความ (OCR/Text) ออกจาก PDF ใน staging
|
||||
- แนบไฟล์ไปยัง Backend: ยิง HTTP Request **`POST /api/storage/upload`** ของ Backend
|
||||
- รอรับผลลัพธ์เป็น `temp_attachment_id` (หมายความว่าไฟล์นี้เข้าข่าย Temporary ถูกเก็บจัดการใน NAS เรียบร้อยแล้ว)
|
||||
- Output: ไฟล์พร้อมใช้งาน, ได้เนื้อหา Text มาเตรียม prompt
|
||||
|
||||
### Node 4: AI Analysis
|
||||
|
||||
- วาง System Prompt บังคับ Output JSON
|
||||
- โยน Metadata (Title, Date, DB Lookups) พร้อม Extracted PDF Text คุยกับ **Ollama `llama3.2:3b`**
|
||||
- ให้ AI วิเคราะห์ และสรุปเป็น `ai_summary`
|
||||
- ให้ AI วิเคราะห์ และสรุปเป็น `ai_summary`
|
||||
- ให้ AI แนะนำ Tags ใหม่หรือเลือก Tags เดิมจาก `existing_tags_json`
|
||||
|
||||
### Node 5: Parse & Validate
|
||||
|
||||
- Schema Validation (ดูให้แน่ใจว่า AI ตอบ `is_valid`, `confidence`, `summary`, `suggested_tags`)
|
||||
- Normalizing categories, trimming tags (`is_new: true / false` flag สำคัญมาก)
|
||||
- จัดชุดค่า Status ใหม่
|
||||
|
||||
### Node 6: Confidence Router & Staging Ingest
|
||||
|
||||
**แยกสาย 4 สาย:**
|
||||
1. **PENDING (Auto Ready):** (`confidence ≥ 0.85` && `is_valid = true`) → INSERT เข้า `migration_review_queue`
|
||||
|
||||
1. **PENDING (Auto Ready):** (`confidence ≥ 0.85` && `is_valid = true`) → INSERT เข้า `migration_review_queue`
|
||||
2. **PENDING (Flagged):** (`confidence 0.60 - 0.84`) → INSERT เข้า `migration_review_queue` พร้อม Highlight/Remarks ให้ Admin ดูละเอียด
|
||||
3. **REJECTED:** (`confidence < 0.60` หรือ `is_valid = false`) → INSERT เข้า `migration_review_queue` สถานะรอแก้แบบ Manual
|
||||
4. **Error/Parse Fail:** ไปลง CSV Reject Log + DB `migration_errors`
|
||||
|
||||
**สำคัญมาก:** *n8n จะทำหน้าที่สูบข้อมูลและจัดเตรียมเข้า `migration_review_queue` เท่านั้น จะไม่มีการข้ามขั้นตอนไป Import ลงตารางหลัก `correspondences` อัตโนมัติ (Final Commit ต้องทำบน Frontend UI)*
|
||||
**สำคัญมาก:** _n8n จะทำหน้าที่สูบข้อมูลและจัดเตรียมเข้า `migration_review_queue` เท่านั้น จะไม่มีการข้ามขั้นตอนไป Import ลงตารางหลัก `correspondences` อัตโนมัติ (Final Commit ต้องทำบน Frontend UI)_
|
||||
|
||||
---
|
||||
|
||||
@@ -306,6 +314,7 @@ DELETE FROM migration_fallback_state WHERE batch_id = '<BATCH_ID>';
|
||||
```
|
||||
|
||||
**Confirmation Guard:**
|
||||
|
||||
```javascript
|
||||
if ($input.first().json.confirmation !== 'CONFIRM_ROLLBACK') {
|
||||
throw new Error('Rollback cancelled: type "CONFIRM_ROLLBACK" to proceed.');
|
||||
@@ -317,13 +326,13 @@ return $input.all();
|
||||
|
||||
## 📌 ส่วนที่ 6: Daily Operation
|
||||
|
||||
| เวลา | กิจกรรม | ผู้รับผิดชอบ |
|
||||
| เวลา | กิจกรรม | ผู้รับผิดชอบ |
|
||||
| ----- | ------------------------------ | ------------------- |
|
||||
| 08:00 | ตรวจสอบ Night Summary Email | Admin |
|
||||
| 09:00 | Approve/Reject ใน Review Queue | Document Controller |
|
||||
| 17:00 | ตรวจ Disk Space + GPU Temp | DevOps |
|
||||
| 22:00 | Workflow เริ่มรันอัตโนมัติ | System |
|
||||
| 06:30 | Night Summary Report ส่ง Email | System |
|
||||
| 22:00 | Workflow เริ่มรันอัตโนมัติ | System |
|
||||
| 06:30 | Night Summary Report ส่ง Email | System |
|
||||
|
||||
### Emergency Stop
|
||||
|
||||
@@ -349,16 +358,19 @@ mysql -h <DB_IP> -u root -p \
|
||||
## 🚨 ข้อควรระวังสำหรับ Free Plan
|
||||
|
||||
### 1. Security
|
||||
|
||||
- **อย่า Commit ไฟล์นี้เข้า Git** ถ้ามี Password/Token
|
||||
- ใช้ `.gitignore` สำหรับไฟล์ JSON ที่มี Config
|
||||
- Rotate Token ทันทีหลังใช้งาน
|
||||
|
||||
### 2. Limitations
|
||||
|
||||
- **Execution Timeout**: ตรวจสอบ n8n execution timeout (default 5 นาที)
|
||||
- **Memory**: จำกัดที่ 2GB (ตาม Docker Compose)
|
||||
- **Concurrent**: รัน Batch ต่อเนื่อง ไม่ parallel
|
||||
|
||||
### 3. Backup
|
||||
|
||||
- สำรอง PostgreSQL data ที่ `/share/np-dms/n8n/postgres-data`
|
||||
- สำรอง n8n data ที่ `/share/np-dms/n8n`
|
||||
- สำรอง Logs ที่ `/share/np-dms/n8n/migration_logs`
|
||||
@@ -367,67 +379,82 @@ mysql -h <DB_IP> -u root -p \
|
||||
|
||||
## ✅ Pre-Production Checklist (Free Plan)
|
||||
|
||||
| ลำดับ | รายการ | วิธีตรวจสอบ |
|
||||
| --- | ---------------------- | ----------------------------------------------------------------- |
|
||||
| 1 | Config ถูกต้อง | รัน Test Execution ดูผลลัพธ์ Node 0 |
|
||||
| 2 | Database Connect ได้ | Test Step ใน Node Read Checkpoint |
|
||||
| 3 | Ollama พร้อม | `curl http://<OLLAMA_HOST>/api/tags` |
|
||||
| 4 | Backend Token ใช้ได้ | Test Step ใน Node Fetch Categories |
|
||||
| 5 | File Mount RO ถูกต้อง | `docker exec n8n ls /home/node/.n8n-files/staging_ai` |
|
||||
| 6 | Log Mount RW ถูกต้อง | `docker exec n8n touch /home/node/.n8n-files/migration_logs/test` |
|
||||
| 7 | Categories ไม่ hardcode | ดูผลลัพธ์ Node Fetch Categories |
|
||||
| 8 | Tags โหลดถูกต้อง | ดูผลลัพธ์ Node Fetch Tags (ควรแสดงรายการ Tags ที่มีอยู่) |
|
||||
| 9 | AI Tag Extraction ทำงาน | ตรวจ `suggested_tags` ใน Response จาก Parse & Validate Node |
|
||||
| 10 | Idempotency Key ถูกต้อง | ตรวจ Header ใน Node Import |
|
||||
| 11 | Checkpoint บันทึก | ตรวจสอบ `migration_progress` หลังรัน |
|
||||
| 12 | Error Log สร้างไฟล์ | ตรวจสอบ `error_log.csv` |
|
||||
| ลำดับ | รายการ | วิธีตรวจสอบ |
|
||||
| ----- | ----------------------- | ----------------------------------------------------------------- |
|
||||
| 1 | Config ถูกต้อง | รัน Test Execution ดูผลลัพธ์ Node 0 |
|
||||
| 2 | Database Connect ได้ | Test Step ใน Node Read Checkpoint |
|
||||
| 3 | Ollama พร้อม | `curl http://<OLLAMA_HOST>/api/tags` |
|
||||
| 4 | Backend Token ใช้ได้ | Test Step ใน Node Fetch Categories |
|
||||
| 5 | File Mount RO ถูกต้อง | `docker exec n8n ls /home/node/.n8n-files/staging_ai` |
|
||||
| 6 | Log Mount RW ถูกต้อง | `docker exec n8n touch /home/node/.n8n-files/migration_logs/test` |
|
||||
| 7 | Categories ไม่ hardcode | ดูผลลัพธ์ Node Fetch Categories |
|
||||
| 8 | Tags โหลดถูกต้อง | ดูผลลัพธ์ Node Fetch Tags (ควรแสดงรายการ Tags ที่มีอยู่) |
|
||||
| 9 | AI Tag Extraction ทำงาน | ตรวจ `suggested_tags` ใน Response จาก Parse & Validate Node |
|
||||
| 10 | Idempotency Key ถูกต้อง | ตรวจ Header ใน Node Import |
|
||||
| 11 | Checkpoint บันทึก | ตรวจสอบ `migration_progress` หลังรัน |
|
||||
| 12 | Error Log สร้างไฟล์ | ตรวจสอบ `error_log.csv` |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 การแก้ไขปัญหาเฉพาะหน้า
|
||||
|
||||
### ปัญหา: Config ไม่ถูกต้อง
|
||||
|
||||
**แก้ไข:** แก้ที่ Node "Set Configuration" แล้ว Save → Execute Workflow ใหม่
|
||||
|
||||
### ปัญหา: Database Connection Error
|
||||
|
||||
**ตรวจสอบ:**
|
||||
|
||||
```javascript
|
||||
// ใส่ใน Code Node ชั่วคราวเพื่อ Debug
|
||||
const config = $workflow.staticData.config;
|
||||
return [{ json: {
|
||||
host: config.DB_HOST,
|
||||
port: config.DB_PORT,
|
||||
// อย่าแสดง password ใน Production!
|
||||
test: 'Config loaded: ' + (config ? 'YES' : 'NO')
|
||||
}}];
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
host: config.DB_HOST,
|
||||
port: config.DB_PORT,
|
||||
// อย่าแสดง password ใน Production!
|
||||
test: 'Config loaded: ' + (config ? 'YES' : 'NO'),
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### ปัญหา: AI Tag Extraction ไม่ทำงาน
|
||||
|
||||
**ตรวจสอบ:**
|
||||
|
||||
1. ดู Response ใน Node "Parse & Validate" ว่ามี field `suggested_tags` หรือไม่
|
||||
2. ถ้าไม่มี → ตรวจสอบ Prompt ใน "Build AI Prompt" ว่ารวม Tag Extraction Instructions แล้ว
|
||||
3. ถ้า AI ตอบแต่ Tags ไม่ถูกต้อง → ปรับ Threshold หรือส่งไป Review Queue
|
||||
|
||||
```javascript
|
||||
// Debug Code Node ชั่วคราว
|
||||
return [{
|
||||
json: {
|
||||
has_suggested_tags: !!$json.ai_result?.suggested_tags,
|
||||
tag_count: $json.ai_result?.suggested_tags?.length || 0,
|
||||
suggested_tags: $json.ai_result?.suggested_tags,
|
||||
tag_confidence: $json.ai_result?.tag_confidence
|
||||
}
|
||||
}];
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
has_suggested_tags: !!$json.ai_result?.suggested_tags,
|
||||
tag_count: $json.ai_result?.suggested_tags?.length || 0,
|
||||
suggested_tags: $json.ai_result?.suggested_tags,
|
||||
tag_confidence: $json.ai_result?.tag_confidence,
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### ปัญหา: Tags ซ้ำหรือผิดพลาด
|
||||
|
||||
**แก้ไข:**
|
||||
|
||||
- ใช้ SQL ตรวจสอบ Tags ที่ซ้ำ:
|
||||
|
||||
```sql
|
||||
SELECT tag_name, COUNT(*) as cnt FROM tags
|
||||
WHERE created_by = (SELECT user_id FROM users WHERE username = 'migration_bot')
|
||||
GROUP BY tag_name HAVING cnt > 1;
|
||||
```
|
||||
|
||||
- ถ้าพบซ้ำ → ใช้ Node Normalize ก่อนบันทึก (มีแล้วใน Parse & Validate)
|
||||
|
||||
---
|
||||
@@ -468,7 +495,7 @@ mysql -h <DB_HOST> -u migration_bot -p -e "SELECT COUNT(DISTINCT ct.corresponden
|
||||
|
||||
## 📞 การติดต่อสนับสนุน
|
||||
|
||||
| ปัญหา | ช่องทางติดต่อ |
|
||||
| ปัญหา | ช่องทางติดต่อ |
|
||||
| --------------- | ------------------------------------------- |
|
||||
| Technical Issue | DevOps Team (Slack: #migration-support) |
|
||||
| Data Issue | Document Controller (Email: dc@lcbp3.local) |
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
# 📦 Legacy Data Migration — Business Scope & Governance
|
||||
|
||||
---
|
||||
|
||||
title: 'Migration Business Scope, Data Governance, and Go/No-Go Gates'
|
||||
version: 1.0.0
|
||||
status: DRAFT — Awaiting Stakeholder Confirmation
|
||||
owner: Nattanin Peancharoen (PO + Migration Lead)
|
||||
last_updated: 2026-03-11
|
||||
related:
|
||||
- specs/03-Data-and-Storage/03-04-legacy-data-migration.md ← Technical Implementation
|
||||
- specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md
|
||||
- specs/06-Decision-Records/ADR-017-ollama-data-migration.md
|
||||
- specs/06-Decision-Records/ADR-018-ai-boundary.md
|
||||
- specs/00-Overview/00-04-stakeholder-signoff-and-risk.md ← Risk Register (RISK-002)
|
||||
|
||||
- specs/03-Data-and-Storage/03-04-legacy-data-migration.md ← Technical Implementation
|
||||
- specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md
|
||||
- specs/06-Decision-Records/ADR-017-ollama-data-migration.md
|
||||
- specs/06-Decision-Records/ADR-018-ai-boundary.md
|
||||
- specs/00-Overview/00-04-stakeholder-signoff-and-risk.md ← Risk Register (RISK-002)
|
||||
|
||||
---
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -26,12 +29,12 @@ related:
|
||||
|
||||
## 1. 🎯 Migration Objective
|
||||
|
||||
| วัตถุประสงค์ | รายละเอียด |
|
||||
|------------|-----------|
|
||||
| **Continuity** | ผู้ใช้สามารถค้นหาและอ้างอิงเอกสารเก่าในระบบใหม่ได้ทันที |
|
||||
| **Traceability** | Workflow ใหม่สามารถ Link กลับไปยัง Correspondence เก่าได้ |
|
||||
| วัตถุประสงค์ | รายละเอียด |
|
||||
| ----------------- | ------------------------------------------------------------- |
|
||||
| **Continuity** | ผู้ใช้สามารถค้นหาและอ้างอิงเอกสารเก่าในระบบใหม่ได้ทันที |
|
||||
| **Traceability** | Workflow ใหม่สามารถ Link กลับไปยัง Correspondence เก่าได้ |
|
||||
| **Searchability** | เอกสารเก่าถูก Index ใน Elasticsearch — ค้นหาได้ด้วย Full-text |
|
||||
| **Compliance** | Audit Trail ครบ: รู้ว่าใครนำเข้า เมื่อไหร่ จาก Batch ไหน |
|
||||
| **Compliance** | Audit Trail ครบ: รู้ว่าใครนำเข้า เมื่อไหร่ จาก Batch ไหน |
|
||||
|
||||
---
|
||||
|
||||
@@ -39,19 +42,21 @@ related:
|
||||
|
||||
### 2.1 ✅ IN SCOPE — นำเข้าระบบใหม่
|
||||
|
||||
| ประเภทเอกสาร | Subdirectory | Volume (ประมาณ) | Priority |
|
||||
|-------------|-------------|----------------|---------|
|
||||
| **Correspondence** (Letters, RFI) | `CORR/` | ~8,000 ไฟล์ | 🔴 High |
|
||||
| **RFA + Shop Drawings** | `RFA/` | ~5,000 ไฟล์ | 🔴 High |
|
||||
| **Contract Drawings** | `CD/` | ~3,000 ไฟล์ | 🟠 Medium |
|
||||
| **Transmittals** | `TRM/` | ~2,000 ไฟล์ | 🟠 Medium |
|
||||
| **Reports & Minutes** | `RPT/` | ~2,000 ไฟล์ | 🟡 Low |
|
||||
| ประเภทเอกสาร | Subdirectory | Volume (ประมาณ) | Priority |
|
||||
| --------------------------------- | ------------ | --------------- | --------- |
|
||||
| **Correspondence** (Letters, RFI) | `CORR/` | ~8,000 ไฟล์ | 🔴 High |
|
||||
| **RFA + Shop Drawings** | `RFA/` | ~5,000 ไฟล์ | 🔴 High |
|
||||
| **Contract Drawings** | `CD/` | ~3,000 ไฟล์ | 🟠 Medium |
|
||||
| **Transmittals** | `TRM/` | ~2,000 ไฟล์ | 🟠 Medium |
|
||||
| **Reports & Minutes** | `RPT/` | ~2,000 ไฟล์ | 🟡 Low |
|
||||
|
||||
**ช่วงเวลาที่ Include:**
|
||||
|
||||
- **เริ่มต้น:** 1 มกราคม 2564 (โครงการเริ่ม)
|
||||
- **สิ้นสุด:** วันก่อน Go-Live — 1 วัน (เอกสารหลังจากนั้นใช้ระบบใหม่)
|
||||
|
||||
**เงื่อนไข Include:**
|
||||
|
||||
- ไฟล์ต้องเป็น PDF (หรือ DWG สำหรับ Drawing)
|
||||
- ไฟล์ต้อง Readable โดย Tika/Ollama (ไม่ Corrupted)
|
||||
- มี Row ใน Excel Metadata ที่ตรงกัน (document_number ไม่ว่าง)
|
||||
@@ -60,16 +65,16 @@ related:
|
||||
|
||||
### 2.2 ❌ OUT OF SCOPE — ไม่นำเข้า
|
||||
|
||||
| รายการ | เหตุผล |
|
||||
|--------|-------|
|
||||
| **เอกสารก่อนปี 2564** | ก่อนเริ่มโครงการ LCBP3 Phase 3 |
|
||||
| **Email Body / Attachments ที่ไม่ใช่ PDF** | Format ไม่รองรับ |
|
||||
| **Draft ที่ไม่เคย Submit** | ไม่มีเลขเอกสารทางการ |
|
||||
| **ไฟล์ที่ Corrupted หรืออ่านไม่ได้** | ไปที่ Reject Log |
|
||||
| **ข้อมูล Financial / Cost Records** | ไม่อยู่ใน DMS Scope |
|
||||
| **Personal Communication (ไม่มีเลขทางการ)** | ไม่ใช่เอกสารทางการ |
|
||||
| **วิดีโอ / รูปภาพ Standalone** | ไม่ใช่ Document |
|
||||
| **ไฟล์ DWG ที่ไม่มี PDF คู่** | ออก PDF ก่อนนำเข้า (Admin Task) |
|
||||
| รายการ | เหตุผล |
|
||||
| ------------------------------------------- | ------------------------------- |
|
||||
| **เอกสารก่อนปี 2564** | ก่อนเริ่มโครงการ LCBP3 Phase 3 |
|
||||
| **Email Body / Attachments ที่ไม่ใช่ PDF** | Format ไม่รองรับ |
|
||||
| **Draft ที่ไม่เคย Submit** | ไม่มีเลขเอกสารทางการ |
|
||||
| **ไฟล์ที่ Corrupted หรืออ่านไม่ได้** | ไปที่ Reject Log |
|
||||
| **ข้อมูล Financial / Cost Records** | ไม่อยู่ใน DMS Scope |
|
||||
| **Personal Communication (ไม่มีเลขทางการ)** | ไม่ใช่เอกสารทางการ |
|
||||
| **วิดีโอ / รูปภาพ Standalone** | ไม่ใช่ Document |
|
||||
| **ไฟล์ DWG ที่ไม่มี PDF คู่** | ออก PDF ก่อนนำเข้า (Admin Task) |
|
||||
|
||||
---
|
||||
|
||||
@@ -107,31 +112,31 @@ Tier 3 — นำเข้าภายใน 1 เดือนหลัง Go-Li
|
||||
|
||||
### 3.1 Excel Metadata Schema (Legacy)
|
||||
|
||||
| Column | Field ใหม่ | บังคับ | หมายเหตุ |
|
||||
|--------|----------|-------|---------|
|
||||
| `DOC_NO` | `document_number` | ✅ | ใช้เป็น Idempotency Key |
|
||||
| `TITLE` | `title` | ✅ | AI จะ Suggest แก้ไขถ้าผิด Format |
|
||||
| `DATE` | `reference_date` | ✅ | วันที่เอกสาร (ไม่ใช่วันนำเข้า) |
|
||||
| `FROM_ORG` | `sender_org_id` | ✅ | Map ด้วย org_code lookup table |
|
||||
| `TO_ORG` | `receiver_org_id` | ✅ | Map ด้วย org_code lookup table |
|
||||
| `TYPE` | `category` | ✅ | AI ตรวจสอบ Enum ที่ถูกต้อง |
|
||||
| `DISCIPLINE` | `discipline` | ❌ | Optional — AI Extract จาก Title |
|
||||
| `CONTRACT_NO` | `contract_id` | ❌ | Map ด้วย contract lookup table |
|
||||
| `PROJECT_NO` | `project_id` | ✅ | ต้องมี (ทุกเอกสาร) |
|
||||
| `FILE_PATH` | `source_file_path` | ✅ | Path ใน NAS staging folder |
|
||||
| `REVISION` | `revision` | ❌ | Detect จากเลขเอกสาร |
|
||||
| Column | Field ใหม่ | บังคับ | หมายเหตุ |
|
||||
| ------------- | ------------------ | ------ | -------------------------------- |
|
||||
| `DOC_NO` | `document_number` | ✅ | ใช้เป็น Idempotency Key |
|
||||
| `TITLE` | `title` | ✅ | AI จะ Suggest แก้ไขถ้าผิด Format |
|
||||
| `DATE` | `reference_date` | ✅ | วันที่เอกสาร (ไม่ใช่วันนำเข้า) |
|
||||
| `FROM_ORG` | `sender_org_id` | ✅ | Map ด้วย org_code lookup table |
|
||||
| `TO_ORG` | `receiver_org_id` | ✅ | Map ด้วย org_code lookup table |
|
||||
| `TYPE` | `category` | ✅ | AI ตรวจสอบ Enum ที่ถูกต้อง |
|
||||
| `DISCIPLINE` | `discipline` | ❌ | Optional — AI Extract จาก Title |
|
||||
| `CONTRACT_NO` | `contract_id` | ❌ | Map ด้วย contract lookup table |
|
||||
| `PROJECT_NO` | `project_id` | ✅ | ต้องมี (ทุกเอกสาร) |
|
||||
| `FILE_PATH` | `source_file_path` | ✅ | Path ใน NAS staging folder |
|
||||
| `REVISION` | `revision` | ❌ | Detect จากเลขเอกสาร |
|
||||
|
||||
### 3.2 Organization Code Mapping
|
||||
|
||||
> ต้องสร้าง Lookup Table ก่อนเริ่ม Migration — Superadmin ทำใน Pre-migration Setup
|
||||
|
||||
| Legacy Code (Excel) | Organization ใหม่ | org_id (System) |
|
||||
|--------------------|-----------------|----------------|
|
||||
| กทท. | การท่าเรือแห่งประเทศไทย | TBD (ดูจาก DB) |
|
||||
| สค. | สำนักงานโครงการ | TBD |
|
||||
| TEAM | TEAM | TBD |
|
||||
| คคง. | คณะกรรมการตรวจงาน | TBD |
|
||||
| ผรม. | ผู้รับจ้างหลัก | TBD |
|
||||
| Legacy Code (Excel) | Organization ใหม่ | org_id (System) |
|
||||
| ------------------- | ----------------------- | --------------- |
|
||||
| กทท. | การท่าเรือแห่งประเทศไทย | TBD (ดูจาก DB) |
|
||||
| สค. | สำนักงานโครงการ | TBD |
|
||||
| TEAM | TEAM | TBD |
|
||||
| คคง. | คณะกรรมการตรวจงาน | TBD |
|
||||
| ผรม. | ผู้รับจ้างหลัก | TBD |
|
||||
|
||||
> **Action Item:** Superadmin ต้อง Fill in `org_id` ก่อน Migration เริ่ม
|
||||
|
||||
@@ -183,15 +188,15 @@ T+1 เดือน:
|
||||
|
||||
### Gate #1: Before Production Migration Starts (T-3 สัปดาห์)
|
||||
|
||||
| เกณฑ์ | ต้องผ่าน | วิธีวัด |
|
||||
|-------|---------|--------|
|
||||
| Dry Run 2 JSON Parse Success | ≥ 95% | n8n Execution Log |
|
||||
| Dry Run 2 AI Category Accuracy | ≥ 90% (Manual Spot-check 50 docs) | Human Review |
|
||||
| Idempotency Test: รัน Batch ซ้ำ | 0 Duplicate Records | SQL Count |
|
||||
| Organization Mapping ครบ | 100% | Lookup Table review |
|
||||
| Frontend Review UI พร้อมใช้งาน | ✅ | UAT Passed สำหรับหน้าจออนุมัติ |
|
||||
| Migration Bot Token Active + Whitelisted | ✅ | API Test |
|
||||
| Staging NAS Space: ≥ 500GB free | ✅ | QNAP Dashboard |
|
||||
| เกณฑ์ | ต้องผ่าน | วิธีวัด |
|
||||
| ---------------------------------------- | --------------------------------- | ------------------------------ |
|
||||
| Dry Run 2 JSON Parse Success | ≥ 95% | n8n Execution Log |
|
||||
| Dry Run 2 AI Category Accuracy | ≥ 90% (Manual Spot-check 50 docs) | Human Review |
|
||||
| Idempotency Test: รัน Batch ซ้ำ | 0 Duplicate Records | SQL Count |
|
||||
| Organization Mapping ครบ | 100% | Lookup Table review |
|
||||
| Frontend Review UI พร้อมใช้งาน | ✅ | UAT Passed สำหรับหน้าจออนุมัติ |
|
||||
| Migration Bot Token Active + Whitelisted | ✅ | API Test |
|
||||
| Staging NAS Space: ≥ 500GB free | ✅ | QNAP Dashboard |
|
||||
|
||||
**Owner:** Nattanin P. | **Approver:** Org Admin ทุกองค์กร
|
||||
|
||||
@@ -199,40 +204,40 @@ T+1 เดือน:
|
||||
|
||||
### Gate #2: Before Go-Live (T-1 วัน)
|
||||
|
||||
| เกณฑ์ | ต้องผ่าน |
|
||||
|-------|---------|
|
||||
| Tier 1 Migration: 100% เสร็จ + Verified | ✅ |
|
||||
| Tier 2 Migration: ≥ 90% เสร็จ + Verified | ✅ |
|
||||
| Review Queue (รวมการพิจารณา AI New Tags): ≤ 5% ค้างอยู่ (Critical Tier 1 = 0%) | ✅ |
|
||||
| Migration Bot Token: REVOKED | ✅ |
|
||||
| Integrity Queries ผ่านทั้งหมด | ✅ |
|
||||
| Legacy System ยังเข้าถึงได้ (Read-only Fallback) | ✅ |
|
||||
| เกณฑ์ | ต้องผ่าน |
|
||||
| ------------------------------------------------------------------------------ | -------- |
|
||||
| Tier 1 Migration: 100% เสร็จ + Verified | ✅ |
|
||||
| Tier 2 Migration: ≥ 90% เสร็จ + Verified | ✅ |
|
||||
| Review Queue (รวมการพิจารณา AI New Tags): ≤ 5% ค้างอยู่ (Critical Tier 1 = 0%) | ✅ |
|
||||
| Migration Bot Token: REVOKED | ✅ |
|
||||
| Integrity Queries ผ่านทั้งหมด | ✅ |
|
||||
| Legacy System ยังเข้าถึงได้ (Read-only Fallback) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### Gate #3: Post Go-Live (T+30 วัน)
|
||||
|
||||
| เกณฑ์ | ต้องผ่าน |
|
||||
|-------|---------|
|
||||
| Tier 3 Migration: 100% เสร็จ | ✅ |
|
||||
| User Search Test: สามารถค้นหา Legacy Doc ใน ES | ✅ |
|
||||
| Zero Orphan Files ใน Staging | ✅ |
|
||||
| Legacy System Archive เสร็จ (Compress + Store) | ✅ |
|
||||
| เกณฑ์ | ต้องผ่าน |
|
||||
| ---------------------------------------------- | -------- |
|
||||
| Tier 3 Migration: 100% เสร็จ | ✅ |
|
||||
| User Search Test: สามารถค้นหา Legacy Doc ใน ES | ✅ |
|
||||
| Zero Orphan Files ใน Staging | ✅ |
|
||||
| Legacy System Archive เสร็จ (Compress + Store) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 6. 🧑💼 Data Ownership & Responsibility
|
||||
|
||||
| Responsibility | Owner | Action |
|
||||
|---------------|-------|--------|
|
||||
| **Excel Metadata Quality** | Document Control (สค.) | ทำความสะอาดก่อน T-6 |
|
||||
| **File Organization บน NAS** | Nattanin P. + IT | จัด Folder structure |
|
||||
| **Organization Lookup Table** | Superadmin (NAP) | สร้างก่อน T-6 |
|
||||
| **Tier 1 Document List** | Document Control ทุก Org | ยืนยัน T-5 |
|
||||
| **Daily Monitoring (n8n Runs)** | Nattanin P. | T-3 ถึง Go-Live |
|
||||
| **Admin Review Queue & AI Tag Approval** | Document Control (สค.) | ทุกเช้าวันทำงาน (บังคับตรวจสอบ New Tags) |
|
||||
| **Post-migration Verification** | Nattanin P. | After each Gate |
|
||||
| **Legacy System Archival** | กทท. IT + NAP | T+30 |
|
||||
| Responsibility | Owner | Action |
|
||||
| ---------------------------------------- | ------------------------ | ---------------------------------------- |
|
||||
| **Excel Metadata Quality** | Document Control (สค.) | ทำความสะอาดก่อน T-6 |
|
||||
| **File Organization บน NAS** | Nattanin P. + IT | จัด Folder structure |
|
||||
| **Organization Lookup Table** | Superadmin (NAP) | สร้างก่อน T-6 |
|
||||
| **Tier 1 Document List** | Document Control ทุก Org | ยืนยัน T-5 |
|
||||
| **Daily Monitoring (n8n Runs)** | Nattanin P. | T-3 ถึง Go-Live |
|
||||
| **Admin Review Queue & AI Tag Approval** | Document Control (สค.) | ทุกเช้าวันทำงาน (บังคับตรวจสอบ New Tags) |
|
||||
| **Post-migration Verification** | Nattanin P. | After each Gate |
|
||||
| **Legacy System Archival** | กทท. IT + NAP | T+30 |
|
||||
|
||||
---
|
||||
|
||||
@@ -255,6 +260,7 @@ T+1 เดือน:
|
||||
- Output ของ AI ต้องผ่าน Backend Validation ก่อน Write
|
||||
|
||||
4. **Audit Log**: ทุก Record ที่ Import มี:
|
||||
|
||||
```json
|
||||
{ "created_by": "SYSTEM_IMPORT", "batch_id": "migration_YYYYMMDD", "action": "IMPORT" }
|
||||
```
|
||||
@@ -281,6 +287,7 @@ T+1 เดือน:
|
||||
### กรณี Go-Live โดย Tier 2 ไม่เสร็จ (Emergency)
|
||||
|
||||
**เปิดใช้ Parallel Operation:**
|
||||
|
||||
- ระบบใหม่: เอกสาร Tier 1 + เอกสารใหม่หลัง Go-Live
|
||||
- Legacy System: เปิด Read-only สำหรับเอกสาร Tier 2/3 ยังไม่ Migrate
|
||||
- Timeline Extension: Tier 2 ต้องเสร็จภายใน T+7 (ไม่เกิน 1 สัปดาห์หลัง Go-Live)
|
||||
@@ -289,15 +296,15 @@ T+1 เดือน:
|
||||
|
||||
## 9. 📊 Migration Success Metrics
|
||||
|
||||
| Metric | Target | วิธีวัด |
|
||||
|--------|--------|--------|
|
||||
| Total Records Imported | ≥ 95% ของ In-Scope | SQL COUNT vs Excel Row Count |
|
||||
| Auto-import Rate (confidence ≥ 0.85) | ≥ 70% | n8n Execution Report |
|
||||
| Review Queue Clearance | ≥ 95% ก่อน Go-Live | Review Queue Table |
|
||||
| Reject Rate (Corrupted/Unreadable) | < 5% | Reject Log |
|
||||
| Duplicate Records | 0 | SQL HAVING COUNT > 1 |
|
||||
| Tag Extraction Rate | ≥ 80% ของ Auto-imported docs มี ≥ 1 Tag | SQL |
|
||||
| Post-migration Search Hit Rate | ≥ 90% ของ Legacy doc numbers ค้นหาเจอ | Manual Test 100 samples |
|
||||
| Metric | Target | วิธีวัด |
|
||||
| ------------------------------------ | --------------------------------------- | ---------------------------- |
|
||||
| Total Records Imported | ≥ 95% ของ In-Scope | SQL COUNT vs Excel Row Count |
|
||||
| Auto-import Rate (confidence ≥ 0.85) | ≥ 70% | n8n Execution Report |
|
||||
| Review Queue Clearance | ≥ 95% ก่อน Go-Live | Review Queue Table |
|
||||
| Reject Rate (Corrupted/Unreadable) | < 5% | Reject Log |
|
||||
| Duplicate Records | 0 | SQL HAVING COUNT > 1 |
|
||||
| Tag Extraction Rate | ≥ 80% ของ Auto-imported docs มี ≥ 1 Tag | SQL |
|
||||
| Post-migration Search Hit Rate | ≥ 90% ของ Legacy doc numbers ค้นหาเจอ | Manual Test 100 samples |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
**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)
|
||||
@@ -12,12 +13,14 @@
|
||||
- [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
|
||||
@@ -27,6 +30,7 @@
|
||||
## 🎯 วัตถุประสงค์ (Objective)
|
||||
|
||||
เพิ่มความสามารถ **Semantic Search และ Document Q&A** ให้กับระบบ DMS โดยใช้ Infrastructure ที่มีอยู่แล้ว:
|
||||
|
||||
- ไม่ส่งข้อมูลออกนอกเครือข่ายองค์กร (Data Privacy)
|
||||
- ไม่มีค่าใช้จ่ายต่อ Query (Zero Cost)
|
||||
- ต่อยอดจากสถาปัตยกรรม Migration ที่ผ่าน Validate แล้ว (ADR-017/018)
|
||||
@@ -37,16 +41,16 @@
|
||||
|
||||
ตาม 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) |
|
||||
| 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)
|
||||
@@ -174,11 +178,11 @@ n8n ทำงานแบบ **Pull (Schedule-based)** — ดึง JSON ที
|
||||
|
||||
> 📁 **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 |
|
||||
> | สถานะ | 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
|
||||
|
||||
@@ -267,9 +271,10 @@ PUT /dms_rag_chunks
|
||||
```
|
||||
|
||||
> ⚠️ **ขนาด Embedding Vector:** ขึ้นอยู่กับ Model ที่ใช้
|
||||
>
|
||||
> - `nomic-embed-text`: 768 dims
|
||||
> - `llama3.2:3b` (ใช้ layer สุดท้าย): 3072 dims
|
||||
> ต้องทดสอบ Performance บน RTX 2060 SUPER 8GB ก่อนเลือก
|
||||
> ต้องทดสอบ Performance บน RTX 2060 SUPER 8GB ก่อนเลือก
|
||||
|
||||
---
|
||||
|
||||
@@ -286,19 +291,19 @@ PUT /dms_rag_chunks
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -306,37 +311,37 @@ Poll ไฟล์ JSON จาก Shared NAS ทีละไฟล์ แล้
|
||||
|
||||
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` |
|
||||
| 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 }` |
|
||||
| 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 ที่เกี่ยวข้อง |
|
||||
| ระดับ Confidence | การดำเนินการ |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| `>= 0.80` | แสดงผลทันที พร้อม Sources |
|
||||
| `0.60 – 0.79` | แสดงผลพร้อม Warning "โปรดตรวจสอบเอกสารต้นฉบับ" |
|
||||
| `< 0.60` | ไม่แสดงคำตอบ — แสดงเฉพาะ Document Links ที่เกี่ยวข้อง |
|
||||
|
||||
> AI ไม่มีสิทธิ์ Write ข้อมูลใดๆ — Output เป็น JSON Read-only เสมอ (ADR-018)
|
||||
|
||||
@@ -344,20 +349,20 @@ Index Chunks (จาก OpenRAG JSON หรือ Tika Fallback) เข้า El
|
||||
|
||||
## 🚧 ข้อจำกัดและความเสี่ยง
|
||||
|
||||
| ความเสี่ยง | ผลกระทบ | 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 เท่านั้น |
|
||||
| ความเสี่ยง | ผลกระทบ | 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 เท่านั้น |
|
||||
|
||||
---
|
||||
|
||||
@@ -367,6 +372,7 @@ Index Chunks (จาก OpenRAG JSON หรือ Tika Fallback) เข้า El
|
||||
> ต้องผ่าน 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 ด้านบน
|
||||
@@ -374,11 +380,13 @@ Index Chunks (จาก OpenRAG JSON หรือ Tika Fallback) เข้า El
|
||||
- [ ] ยืนยัน 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 แรก)
|
||||
@@ -436,6 +444,7 @@ 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
|
||||
@@ -461,20 +470,21 @@ 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** |
|
||||
| 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
|
||||
@@ -496,11 +506,11 @@ docker ps
|
||||
|
||||
**URL ที่ใช้งานได้:**
|
||||
|
||||
| Service | URL | หมายเหตุ |
|
||||
|---------|-----|----------|
|
||||
| Service | URL | หมายเหตุ |
|
||||
| ---------- | ----------------------- | ------------------------- |
|
||||
| OpenRAG UI | `http://localhost:3000` | หน้าหลัก (เหมือน Chat UI) |
|
||||
| Langflow | `http://localhost:7860` | สร้าง/แก้ไข Workflow |
|
||||
| OpenSearch | `http://localhost:9200` | Vector Store API |
|
||||
| Langflow | `http://localhost:7860` | สร้าง/แก้ไข Workflow |
|
||||
| OpenSearch | `http://localhost:9200` | Vector Store API |
|
||||
|
||||
---
|
||||
|
||||
@@ -547,12 +557,13 @@ ollama pull nomic-embed-text
|
||||
|
||||
> **Component:** `Read File` (หมวด Data / Helpers)
|
||||
|
||||
| Setting | ค่า |
|
||||
|---------|-----|
|
||||
| Files | อัปโหลด หรือ ชี้ไปที่ `/data/staging_ai/` |
|
||||
| Advanced Parser | `OFF` (ปิด — อ่านเป็น raw text ธรรมดา) |
|
||||
| Setting | ค่า |
|
||||
| --------------- | ----------------------------------------- |
|
||||
| Files | อัปโหลด หรือ ชี้ไปที่ `/data/staging_ai/` |
|
||||
| Advanced Parser | `OFF` (ปิด — อ่านเป็น raw text ธรรมดา) |
|
||||
|
||||
**การเชื่อมต่อ:**
|
||||
|
||||
- Output `Files` → Input `Inputs` ของ Loop
|
||||
|
||||
> ℹ️ Read File จะโหลดไฟล์ทั้งหมดมาเป็น list แล้วส่งให้ Loop วนลูปทีละไฟล์
|
||||
@@ -564,11 +575,12 @@ ollama pull nomic-embed-text
|
||||
|
||||
> **Component:** `Loop` (หมวด Logic)
|
||||
|
||||
| Setting | ค่า |
|
||||
|---------|-----|
|
||||
| Inputs | รับจาก `Read File → Files` |
|
||||
| Setting | ค่า |
|
||||
| ------- | -------------------------- |
|
||||
| Inputs | รับจาก `Read File → Files` |
|
||||
|
||||
**Output ที่ใช้:**
|
||||
|
||||
- `Item` → ส่งต่อให้ Parser และ Custom Code (filename)
|
||||
- `Done` → ไม่ต้องเชื่อมไปไหน (สัญญาณสิ้นสุด Loop)
|
||||
|
||||
@@ -580,12 +592,13 @@ ollama pull nomic-embed-text
|
||||
|
||||
> **Component:** `Parser` (หมวด Processing)
|
||||
|
||||
| Setting | ค่า |
|
||||
|---------|-----|
|
||||
| Mode | **`Stringify`** (ไม่ใช่ Parser) |
|
||||
| Data or DataFrame | รับจาก `Loop → Item` |
|
||||
| Setting | ค่า |
|
||||
| ----------------- | ------------------------------- |
|
||||
| Mode | **`Stringify`** (ไม่ใช่ Parser) |
|
||||
| Data or DataFrame | รับจาก `Loop → Item` |
|
||||
|
||||
**การเชื่อมต่อ:**
|
||||
|
||||
- Input `Data or DataFrame` ← `Loop → Item`
|
||||
- Output `Parsed Text` → Input `extracted_text` ของ Prompt Template
|
||||
|
||||
@@ -599,12 +612,13 @@ ollama pull nomic-embed-text
|
||||
|
||||
> **Component:** `Prompt Template` (หมวด Prompts)
|
||||
|
||||
| Setting | ค่า |
|
||||
|---------|-----|
|
||||
| Template | ใส่ System Prompt จากขั้นตอนที่ 7 ด้านล่าง |
|
||||
| Variable `{extracted_text}` | เชื่อมกับ `Parser → Parsed Text` |
|
||||
| Setting | ค่า |
|
||||
| --------------------------- | ------------------------------------------ |
|
||||
| Template | ใส่ System Prompt จากขั้นตอนที่ 7 ด้านล่าง |
|
||||
| Variable `{extracted_text}` | เชื่อมกับ `Parser → Parsed Text` |
|
||||
|
||||
**การเชื่อมต่อ:**
|
||||
|
||||
- Variable `extracted_text` ← `Parser → Parsed Text`
|
||||
- Output `Prompt` → Input `Input` ของ Ollama
|
||||
|
||||
@@ -617,16 +631,17 @@ ollama pull nomic-embed-text
|
||||
|
||||
> **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) |
|
||||
| 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)
|
||||
@@ -655,46 +670,46 @@ 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"
|
||||
@@ -704,6 +719,7 @@ class WriteJsonIdempotent(Component):
|
||||
```
|
||||
|
||||
**การเชื่อมต่อ:**
|
||||
|
||||
- Input `json_content` ← `Ollama → Text`
|
||||
- Input `loop_item` ← `Loop → Item`
|
||||
- Output `result_path` → `Loop → item` (Feedback loop กลับไปบอกว่ารอบนี้จบแล้ว)
|
||||
@@ -716,27 +732,26 @@ class WriteJsonIdempotent(Component):
|
||||
|
||||
#### สรุปการ 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 |
|
||||
|
||||
| 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` |
|
||||
| ฟิลด์ | ค่า |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| 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 ที่ออกแบบมาสำหรับภาษาไทยโดยเฉพาะ
|
||||
@@ -804,7 +819,7 @@ services:
|
||||
volumes:
|
||||
# staging_ai mount จาก NAS
|
||||
# Windows R:\ drive จะปรากฏใน WSL เป็น /mnt/r/
|
||||
- /mnt/r/staging_ai:/data/staging_ai # ← Read PDF + Write rag-output/
|
||||
- /mnt/r/staging_ai:/data/staging_ai # ← Read PDF + Write rag-output/
|
||||
# หมายเหตุ: ต้องเขียนได้ที่ rag-output/ จึงไม่ใส่ :ro
|
||||
|
||||
opensearch:
|
||||
@@ -812,6 +827,7 @@ services:
|
||||
```
|
||||
|
||||
> ⚠️ **ตรวจสอบ R:\ ใน WSL:**
|
||||
>
|
||||
> ```bash
|
||||
> # ใน WSL Terminal ตรวจว่า mount อยู่ที่ไหน
|
||||
> ls /mnt/r/staging_ai/
|
||||
@@ -819,6 +835,7 @@ services:
|
||||
> ```
|
||||
>
|
||||
> ✅ **สร้าง rag-output/ ก่อนรัน:**
|
||||
>
|
||||
> ```bash
|
||||
> mkdir -p /mnt/r/staging_ai/rag-output
|
||||
> ```
|
||||
@@ -851,12 +868,12 @@ ls /mnt/r/staging_ai/rag-output/
|
||||
|
||||
สร้าง 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())` |
|
||||
| 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
|
||||
@@ -865,6 +882,7 @@ docker exec n8n ls /home/node/.n8n/staging_ai/rag-output/
|
||||
```
|
||||
|
||||
> 💡 **Path Mapping:**
|
||||
>
|
||||
> - Admin Desktop (WSL): `/mnt/r/staging_ai/rag-output/`
|
||||
> - n8n บน QNAP: `staging_ai/rag-output/` (ตาม Volume Mount ใน docker-compose)
|
||||
|
||||
@@ -872,18 +890,18 @@ docker exec n8n ls /home/node/.n8n/staging_ai/rag-output/
|
||||
|
||||
### ขั้นตอนที่ 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` |
|
||||
| ลำดับ | รายการ | วิธีตรวจสอบ |
|
||||
| ----- | ------------------------------ | --------------------------------------------------- |
|
||||
| 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)
|
||||
@@ -898,6 +916,7 @@ nvidia-smi --query-gpu=memory.used,memory.total --format=csv
|
||||
> ต้องผ่าน Go-Live Gate ของ Migration (ADR-017) ก่อนเริ่มพัฒนา
|
||||
|
||||
**OpenRAG Setup (Admin Desktop):**
|
||||
|
||||
- [ ] WSL 2 + Docker Desktop ติดตั้งเสร็จ (ขั้นตอนที่ 1)
|
||||
- [ ] OpenRAG ติดตั้งผ่าน `uvx --python 3.13 openrag` (ขั้นตอนที่ 2–3)
|
||||
- [ ] Ollama เชื่อมต่อจาก Docker Container ได้ (ขั้นตอนที่ 5)
|
||||
@@ -910,6 +929,7 @@ nvidia-smi --query-gpu=memory.used,memory.total --format=csv
|
||||
- [ ] ยืนยัน 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
|
||||
@@ -917,6 +937,7 @@ nvidia-smi --query-gpu=memory.used,memory.total --format=csv
|
||||
- [ ] ทดสอบ 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
|
||||
@@ -927,5 +948,5 @@ nvidia-smi --query-gpu=memory.used,memory.total --format=csv
|
||||
|
||||
---
|
||||
|
||||
*เอกสารนี้เป็น Living Document — อัปเดตเมื่อมีการตัดสินใจ Architecture ใหม่*
|
||||
_เอกสารนี้เป็น Living Document — อัปเดตเมื่อมีการตัดสินใจ Architecture ใหม่_
|
||||
**Version:** 1.8.1 | **Author:** Development Team | **Last Updated:** 2026-03-13
|
||||
|
||||
@@ -4,15 +4,27 @@ const isFallback = fallbackState.is_fallback_active || false;
|
||||
const model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;
|
||||
|
||||
// Read DB Context
|
||||
const dbContext = $('Fetch DB Context').all().map(i => i.json);
|
||||
const dbProjects = dbContext.filter(d => d.type === 'projects').map(d => ({id: d.id, code: d.text1, name: d.text2}));
|
||||
const dbDisciplines = dbContext.filter(d => d.type === 'disciplines').map(d => ({id: d.id, th: d.text1, en: d.text2}));
|
||||
const dbOrgs = dbContext.filter(d => d.type === 'organizations').map(d => ({id: d.id, name: d.text1, code: d.text2}));
|
||||
const dbTags = dbContext.filter(d => d.type === 'tags').map(d => ({id: d.id, name: d.text1}));
|
||||
const dbCorrTypes = dbContext.filter(d => d.type === 'correspondence_types').map(d => ({id: d.id, code: d.text1, name: d.text2}));
|
||||
const dbContext = $('Fetch DB Context')
|
||||
.all()
|
||||
.map((i) => i.json);
|
||||
const dbProjects = dbContext
|
||||
.filter((d) => d.type === 'projects')
|
||||
.map((d) => ({ id: d.id, code: d.text1, name: d.text2 }));
|
||||
const dbDisciplines = dbContext
|
||||
.filter((d) => d.type === 'disciplines')
|
||||
.map((d) => ({ id: d.id, th: d.text1, en: d.text2 }));
|
||||
const dbOrgs = dbContext
|
||||
.filter((d) => d.type === 'organizations')
|
||||
.map((d) => ({ id: d.id, name: d.text1, code: d.text2 }));
|
||||
const dbTags = dbContext.filter((d) => d.type === 'tags').map((d) => ({ id: d.id, name: d.text1 }));
|
||||
const dbCorrTypes = dbContext
|
||||
.filter((d) => d.type === 'correspondence_types')
|
||||
.map((d) => ({ id: d.id, code: d.text1, name: d.text2 }));
|
||||
|
||||
let systemCategories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];
|
||||
try { systemCategories = $('File Mount Check').first().json.system_categories || systemCategories; } catch (e) {}
|
||||
let systemCategories = ['Correspondence', 'RFA', 'Drawing', 'Transmittal', 'Report', 'Other'];
|
||||
try {
|
||||
systemCategories = $('File Mount Check').first().json.system_categories || systemCategories;
|
||||
} catch (e) {}
|
||||
|
||||
const pdfItems = $('Extract PDF Text').all();
|
||||
// File Validator passes all original Excel JSON fields through (sender, receiver, project_code, etc.)
|
||||
@@ -35,13 +47,13 @@ return pdfItems.map((pdfItem, i) => {
|
||||
// JavaScript pre-mapping
|
||||
const findOrgId = (code) => {
|
||||
if (!code) return null;
|
||||
const match = dbOrgs.find(o => o.code === code || o.name === code);
|
||||
const match = dbOrgs.find((o) => o.code === code || o.name === code);
|
||||
return match ? match.id : null;
|
||||
};
|
||||
|
||||
const findProjectId = (code) => {
|
||||
if (!code) return config.PROJECT_ID; // Fallback to config
|
||||
const match = dbProjects.find(p => p.code === code || p.name === code);
|
||||
const match = dbProjects.find((p) => p.code === code || p.name === code);
|
||||
return match ? match.id : config.PROJECT_ID;
|
||||
};
|
||||
|
||||
@@ -49,8 +61,8 @@ return pdfItems.map((pdfItem, i) => {
|
||||
const receiverId = findOrgId(receiverCode);
|
||||
const projectId = findProjectId(projectCode);
|
||||
// Excel corrType is likely already the ID based on requirements, but fallback matching to ID if needed
|
||||
const corrMatch = dbCorrTypes.find(c => String(c.id) === corrType || c.code === corrType || c.name === corrType);
|
||||
const corrTypeId = corrMatch ? corrMatch.id : (isNaN(parseInt(corrType)) ? null : parseInt(corrType));
|
||||
const corrMatch = dbCorrTypes.find((c) => String(c.id) === corrType || c.code === corrType || c.name === corrType);
|
||||
const corrTypeId = corrMatch ? corrMatch.id : isNaN(parseInt(corrType)) ? null : parseInt(corrType);
|
||||
|
||||
const isRFA = docNum.includes('-RFA-') || title.toLowerCase().includes('rfa');
|
||||
|
||||
@@ -60,7 +72,9 @@ Your task is to classify documents and extract metadata from OCR text.
|
||||
Respond ONLY with valid JSON.`;
|
||||
|
||||
// Use pdfItem for the OCR extracted data, NOT the metaItem
|
||||
const pdfText = String(pdfItem.json.data || '').substring(0, 3500).replace(/[^a-zA-Z0-9ก-๙\s\.\/\-:\[\]\(\)]/g, ' ');
|
||||
const pdfText = String(pdfItem.json.data || '')
|
||||
.substring(0, 3500)
|
||||
.replace(/[^a-zA-Z0-9ก-๙\s\.\/\-:\[\]\(\)]/g, ' ');
|
||||
|
||||
const userPrompt = `Analyze this document:
|
||||
[EXCEL METADATA]
|
||||
@@ -117,15 +131,15 @@ Respond ONLY with this EXACT JSON structure:
|
||||
project_id: projectId,
|
||||
sender_id: senderId,
|
||||
receiver_id: receiverId,
|
||||
correspondence_type_id: corrTypeId
|
||||
correspondence_type_id: corrTypeId,
|
||||
},
|
||||
_debug_mapping: {
|
||||
excel_project_code: projectCode,
|
||||
excel_sender: senderCode,
|
||||
excel_receiver: receiverCode,
|
||||
excel_corr_type: corrType,
|
||||
matched_project: dbProjects.find(p => p.code === projectCode || p.name === projectCode) || null,
|
||||
first_org_sample: dbOrgs[0] || null
|
||||
matched_project: dbProjects.find((p) => p.code === projectCode || p.name === projectCode) || null,
|
||||
first_org_sample: dbOrgs[0] || null,
|
||||
},
|
||||
ollama_payload: {
|
||||
model: model,
|
||||
@@ -134,9 +148,9 @@ Respond ONLY with this EXACT JSON structure:
|
||||
format: 'json',
|
||||
options: {
|
||||
temperature: 0.1,
|
||||
num_ctx: 8192
|
||||
}
|
||||
}
|
||||
}
|
||||
num_ctx: 8192,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,16 +9,12 @@
|
||||
"dataType": "File",
|
||||
"id": "File-5V2fL",
|
||||
"name": "dataframe",
|
||||
"output_types": [
|
||||
"DataFrame"
|
||||
]
|
||||
"output_types": ["DataFrame"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "data",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"inputTypes": [
|
||||
"DataFrame"
|
||||
],
|
||||
"inputTypes": ["DataFrame"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -37,16 +33,12 @@
|
||||
"dataType": "Prompt Template",
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"name": "prompt",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "system_message",
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Message"],
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
@@ -64,17 +56,12 @@
|
||||
"dataType": "LoopComponent",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"name": "item",
|
||||
"output_types": [
|
||||
"Data"
|
||||
]
|
||||
"output_types": ["Data"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "input_data",
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"inputTypes": [
|
||||
"DataFrame",
|
||||
"Data"
|
||||
],
|
||||
"inputTypes": ["DataFrame", "Data"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -92,16 +79,12 @@
|
||||
"dataType": "ParserComponent",
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"name": "parsed_text",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "extracted_text",
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Message"],
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
@@ -119,18 +102,12 @@
|
||||
"dataType": "OllamaModel",
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"name": "text_output",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "input",
|
||||
"id": "SaveToFile-M0RUY",
|
||||
"inputTypes": [
|
||||
"Data",
|
||||
"DataFrame",
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Data", "DataFrame", "Message"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -148,18 +125,13 @@
|
||||
"dataType": "SaveToFile",
|
||||
"id": "SaveToFile-M0RUY",
|
||||
"name": "message",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"dataType": "LoopComponent",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"name": "item",
|
||||
"output_types": [
|
||||
"Data",
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Data", "Message"]
|
||||
}
|
||||
},
|
||||
"id": "xy-edge__SaveToFile-M0RUY{œdataTypeœ:œSaveToFileœ,œidœ:œSaveToFile-M0RUYœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-LoopComponent-5vFOr{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-5vFOrœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ,œMessageœ]}",
|
||||
@@ -175,9 +147,7 @@
|
||||
"data": {
|
||||
"id": "File-5V2fL",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -258,9 +228,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "DataFrame",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -438,9 +406,7 @@
|
||||
"display_name": "Doc Key",
|
||||
"dynamic": false,
|
||||
"info": "The key to use for the DoclingDocument column.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -484,10 +450,7 @@
|
||||
"display_name": "Server File Path",
|
||||
"dynamic": false,
|
||||
"info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.",
|
||||
"input_types": [
|
||||
"Data",
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Data", "Message"],
|
||||
"list": true,
|
||||
"list_add_label": "Add More",
|
||||
"name": "file_path",
|
||||
@@ -635,10 +598,7 @@
|
||||
"external_options": {},
|
||||
"info": "OCR engine to use. Only available when pipeline is set to 'standard'.",
|
||||
"name": "ocr_engine",
|
||||
"options": [
|
||||
"None",
|
||||
"easyocr"
|
||||
],
|
||||
"options": ["None", "easyocr"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -772,10 +732,7 @@
|
||||
"external_options": {},
|
||||
"info": "Docling pipeline to use",
|
||||
"name": "pipeline",
|
||||
"options": [
|
||||
"standard",
|
||||
"vlm"
|
||||
],
|
||||
"options": ["standard", "vlm"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -956,12 +913,7 @@
|
||||
"data": {
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Data",
|
||||
"DataFrame",
|
||||
"LanguageModel",
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Data", "DataFrame", "LanguageModel", "Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -1022,12 +974,7 @@
|
||||
],
|
||||
"total_dependencies": 3
|
||||
},
|
||||
"keywords": [
|
||||
"model",
|
||||
"llm",
|
||||
"language model",
|
||||
"large language model"
|
||||
],
|
||||
"keywords": ["model", "llm", "language model", "large language model"],
|
||||
"module": "lfx.components.ollama.ollama.ChatOllamaComponent"
|
||||
},
|
||||
"minimized": false,
|
||||
@@ -1045,9 +992,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1061,9 +1006,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"LanguageModel"
|
||||
],
|
||||
"types": ["LanguageModel"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1077,9 +1020,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"types": ["Data"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1093,9 +1034,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -1249,13 +1188,7 @@
|
||||
"edit_mode": "inline",
|
||||
"formatter": "text",
|
||||
"name": "type",
|
||||
"options": [
|
||||
"str",
|
||||
"int",
|
||||
"float",
|
||||
"bool",
|
||||
"dict"
|
||||
],
|
||||
"options": ["str", "int", "float", "bool", "dict"],
|
||||
"type": "str"
|
||||
},
|
||||
{
|
||||
@@ -1265,10 +1198,7 @@
|
||||
"edit_mode": "inline",
|
||||
"formatter": "text",
|
||||
"name": "multiple",
|
||||
"options": [
|
||||
"True",
|
||||
"False"
|
||||
],
|
||||
"options": ["True", "False"],
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
@@ -1294,9 +1224,7 @@
|
||||
"display_name": "Input",
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1344,11 +1272,7 @@
|
||||
"external_options": {},
|
||||
"info": "Enable/disable Mirostat sampling for controlling perplexity.",
|
||||
"name": "mirostat",
|
||||
"options": [
|
||||
"Disabled",
|
||||
"Mirostat",
|
||||
"Mirostat 2.0"
|
||||
],
|
||||
"options": ["Disabled", "Mirostat", "Mirostat 2.0"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -1413,10 +1337,7 @@
|
||||
"external_options": {},
|
||||
"info": "Refer to https://ollama.com/library for more models.",
|
||||
"name": "model_name",
|
||||
"options": [
|
||||
"scb10x/typhoon2.1-gemma3-4b:latest",
|
||||
"qwen2.5:7b-instruct-q4_K_M"
|
||||
],
|
||||
"options": ["scb10x/typhoon2.1-gemma3-4b:latest", "qwen2.5:7b-instruct-q4_K_M"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -1538,9 +1459,7 @@
|
||||
"display_name": "Stop Tokens",
|
||||
"dynamic": false,
|
||||
"info": "Comma-separated list of tokens to signal the model to stop generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1583,9 +1502,7 @@
|
||||
"display_name": "System",
|
||||
"dynamic": false,
|
||||
"info": "System to use for generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1610,9 +1527,7 @@
|
||||
"display_name": "System Message",
|
||||
"dynamic": false,
|
||||
"info": "System message to pass to the model.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1637,9 +1552,7 @@
|
||||
"display_name": "Tags",
|
||||
"dynamic": false,
|
||||
"info": "Comma-separated list of tags to add to the run trace.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1692,9 +1605,7 @@
|
||||
"display_name": "Template",
|
||||
"dynamic": false,
|
||||
"info": "Template to use for generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1836,10 +1747,7 @@
|
||||
"data": {
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Data",
|
||||
"DataFrame"
|
||||
],
|
||||
"base_classes": ["Data", "DataFrame"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -1847,9 +1755,7 @@
|
||||
"display_name": "Loop",
|
||||
"documentation": "https://docs.langflow.org/loop",
|
||||
"edited": false,
|
||||
"field_order": [
|
||||
"data"
|
||||
],
|
||||
"field_order": ["data"],
|
||||
"frozen": false,
|
||||
"icon": "infinity",
|
||||
"legacy": false,
|
||||
@@ -1874,16 +1780,12 @@
|
||||
"cache": true,
|
||||
"display_name": "Item",
|
||||
"group_outputs": true,
|
||||
"loop_types": [
|
||||
"Message"
|
||||
],
|
||||
"loop_types": ["Message"],
|
||||
"method": "item_output",
|
||||
"name": "item",
|
||||
"selected": "Data",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"types": ["Data"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1895,9 +1797,7 @@
|
||||
"name": "done",
|
||||
"selected": "DataFrame",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -1928,9 +1828,7 @@
|
||||
"display_name": "Inputs",
|
||||
"dynamic": false,
|
||||
"info": "The initial DataFrame to iterate over.",
|
||||
"input_types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"input_types": ["DataFrame"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "data",
|
||||
@@ -1967,26 +1865,18 @@
|
||||
"data": {
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {
|
||||
"template": [
|
||||
"extracted_text"
|
||||
]
|
||||
"template": ["extracted_text"]
|
||||
},
|
||||
"description": "Create a prompt template with dynamic variables.",
|
||||
"display_name": "Prompt Template",
|
||||
"documentation": "https://docs.langflow.org/components-prompts",
|
||||
"edited": false,
|
||||
"error": null,
|
||||
"field_order": [
|
||||
"template",
|
||||
"use_double_brackets",
|
||||
"tool_placeholder"
|
||||
],
|
||||
"field_order": ["template", "use_double_brackets", "tool_placeholder"],
|
||||
"frozen": false,
|
||||
"full_path": null,
|
||||
"icon": "prompts",
|
||||
@@ -2024,9 +1914,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2061,9 +1949,7 @@
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"info": "",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"load_from_db": false,
|
||||
"multiline": true,
|
||||
@@ -2101,9 +1987,7 @@
|
||||
"display_name": "Tool Placeholder",
|
||||
"dynamic": false,
|
||||
"info": "A placeholder input for tool mode.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2164,9 +2048,7 @@
|
||||
"data": {
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -2174,12 +2056,7 @@
|
||||
"display_name": "Parser",
|
||||
"documentation": "https://docs.langflow.org/parser",
|
||||
"edited": false,
|
||||
"field_order": [
|
||||
"input_data",
|
||||
"mode",
|
||||
"pattern",
|
||||
"sep"
|
||||
],
|
||||
"field_order": ["input_data", "mode", "pattern", "sep"],
|
||||
"frozen": false,
|
||||
"icon": "braces",
|
||||
"last_updated": "2026-03-13T08:19:27.565Z",
|
||||
@@ -2212,9 +2089,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2271,10 +2146,7 @@
|
||||
"display_name": "Data or DataFrame",
|
||||
"dynamic": false,
|
||||
"info": "Accepts either a DataFrame or a Data object.",
|
||||
"input_types": [
|
||||
"DataFrame",
|
||||
"Data"
|
||||
],
|
||||
"input_types": ["DataFrame", "Data"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "input_data",
|
||||
@@ -2296,10 +2168,7 @@
|
||||
"dynamic": false,
|
||||
"info": "Convert into raw string instead of using a template.",
|
||||
"name": "mode",
|
||||
"options": [
|
||||
"Parser",
|
||||
"Stringify"
|
||||
],
|
||||
"options": ["Parser", "Stringify"],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
"real_time_refresh": true,
|
||||
@@ -2320,9 +2189,7 @@
|
||||
"display_name": "Template",
|
||||
"dynamic": true,
|
||||
"info": "Use variables within curly brackets to extract column values for DataFrames or key values for Data.For example: `Name: {Name}, Age: {Age}, Country: {Country}`",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2347,9 +2214,7 @@
|
||||
"display_name": "Separator",
|
||||
"dynamic": false,
|
||||
"info": "String used to separate rows/items.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2389,9 +2254,7 @@
|
||||
"data": {
|
||||
"id": "SaveToFile-M0RUY",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -2472,9 +2335,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2695,16 +2556,7 @@
|
||||
"external_options": {},
|
||||
"info": "Select the file format for Google Drive storage.",
|
||||
"name": "gdrive_format",
|
||||
"options": [
|
||||
"txt",
|
||||
"json",
|
||||
"csv",
|
||||
"xlsx",
|
||||
"slides",
|
||||
"docs",
|
||||
"jpg",
|
||||
"mp3"
|
||||
],
|
||||
"options": ["txt", "json", "csv", "xlsx", "slides", "docs", "jpg", "mp3"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -2724,11 +2576,7 @@
|
||||
"display_name": "File Content",
|
||||
"dynamic": true,
|
||||
"info": "The input to save.",
|
||||
"input_types": [
|
||||
"Data",
|
||||
"DataFrame",
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Data", "DataFrame", "Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "input",
|
||||
@@ -2753,13 +2601,7 @@
|
||||
"external_options": {},
|
||||
"info": "Select the file format for local storage.",
|
||||
"name": "local_format",
|
||||
"options": [
|
||||
"csv",
|
||||
"excel",
|
||||
"json",
|
||||
"markdown",
|
||||
"txt"
|
||||
],
|
||||
"options": ["csv", "excel", "json", "markdown", "txt"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
|
||||
@@ -9,16 +9,12 @@
|
||||
"dataType": "File",
|
||||
"id": "File-5V2fL",
|
||||
"name": "dataframe",
|
||||
"output_types": [
|
||||
"DataFrame"
|
||||
]
|
||||
"output_types": ["DataFrame"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "data",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"inputTypes": [
|
||||
"DataFrame"
|
||||
],
|
||||
"inputTypes": ["DataFrame"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -37,16 +33,12 @@
|
||||
"dataType": "Prompt Template",
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"name": "prompt",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "input_value",
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Message"],
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
@@ -64,17 +56,12 @@
|
||||
"dataType": "LoopComponent",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"name": "item",
|
||||
"output_types": [
|
||||
"Data"
|
||||
]
|
||||
"output_types": ["Data"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "input_data",
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"inputTypes": [
|
||||
"DataFrame",
|
||||
"Data"
|
||||
],
|
||||
"inputTypes": ["DataFrame", "Data"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -92,16 +79,12 @@
|
||||
"dataType": "ParserComponent",
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"name": "parsed_text",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "extracted_text",
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Message"],
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
@@ -119,18 +102,12 @@
|
||||
"dataType": "OllamaModel",
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"name": "text_output",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "json_content",
|
||||
"id": "CustomComponent-WriteJsonIdempotent",
|
||||
"inputTypes": [
|
||||
"Data",
|
||||
"DataFrame",
|
||||
"Message"
|
||||
],
|
||||
"inputTypes": ["Data", "DataFrame", "Message"],
|
||||
"type": "other"
|
||||
}
|
||||
},
|
||||
@@ -148,18 +125,13 @@
|
||||
"dataType": "CustomComponent",
|
||||
"id": "CustomComponent-WriteJsonIdempotent",
|
||||
"name": "result_path",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Message"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"dataType": "LoopComponent",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"name": "item",
|
||||
"output_types": [
|
||||
"Data",
|
||||
"Message"
|
||||
]
|
||||
"output_types": ["Data", "Message"]
|
||||
}
|
||||
},
|
||||
"id": "xy-edge__CustomComponent-WriteJsonIdempotent{œdataTypeœ:œSaveToFileœ,œidœ:œCustomComponent-WriteJsonIdempotentœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-LoopComponent-5vFOr{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-5vFOrœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ,œMessageœ]}",
|
||||
@@ -176,16 +148,12 @@
|
||||
"dataType": "LoopComponent",
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"name": "item",
|
||||
"output_types": [
|
||||
"Data"
|
||||
]
|
||||
"output_types": ["Data"]
|
||||
},
|
||||
"targetHandle": {
|
||||
"fieldName": "loop_item",
|
||||
"id": "CustomComponent-WriteJsonIdempotent",
|
||||
"inputTypes": [
|
||||
"Data"
|
||||
],
|
||||
"inputTypes": ["Data"],
|
||||
"type": "Data"
|
||||
}
|
||||
},
|
||||
@@ -199,9 +167,7 @@
|
||||
"data": {
|
||||
"id": "File-5V2fL",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -282,9 +248,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "DataFrame",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -462,9 +426,7 @@
|
||||
"display_name": "Doc Key",
|
||||
"dynamic": false,
|
||||
"info": "The key to use for the DoclingDocument column.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -508,10 +470,7 @@
|
||||
"display_name": "Server File Path",
|
||||
"dynamic": false,
|
||||
"info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.",
|
||||
"input_types": [
|
||||
"Data",
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Data", "Message"],
|
||||
"list": true,
|
||||
"list_add_label": "Add More",
|
||||
"name": "file_path",
|
||||
@@ -659,10 +618,7 @@
|
||||
"external_options": {},
|
||||
"info": "OCR engine to use. Only available when pipeline is set to 'standard'.",
|
||||
"name": "ocr_engine",
|
||||
"options": [
|
||||
"None",
|
||||
"easyocr"
|
||||
],
|
||||
"options": ["None", "easyocr"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -796,10 +752,7 @@
|
||||
"external_options": {},
|
||||
"info": "Docling pipeline to use",
|
||||
"name": "pipeline",
|
||||
"options": [
|
||||
"standard",
|
||||
"vlm"
|
||||
],
|
||||
"options": ["standard", "vlm"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -980,12 +933,7 @@
|
||||
"data": {
|
||||
"id": "OllamaModel-xJSnu",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Data",
|
||||
"DataFrame",
|
||||
"LanguageModel",
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Data", "DataFrame", "LanguageModel", "Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -1046,12 +994,7 @@
|
||||
],
|
||||
"total_dependencies": 3
|
||||
},
|
||||
"keywords": [
|
||||
"model",
|
||||
"llm",
|
||||
"language model",
|
||||
"large language model"
|
||||
],
|
||||
"keywords": ["model", "llm", "language model", "large language model"],
|
||||
"module": "lfx.components.ollama.ollama.ChatOllamaComponent"
|
||||
},
|
||||
"minimized": false,
|
||||
@@ -1069,9 +1012,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1085,9 +1026,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"LanguageModel"
|
||||
],
|
||||
"types": ["LanguageModel"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1101,9 +1040,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"types": ["Data"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1117,9 +1054,7 @@
|
||||
"options": null,
|
||||
"required_inputs": null,
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -1273,13 +1208,7 @@
|
||||
"edit_mode": "inline",
|
||||
"formatter": "text",
|
||||
"name": "type",
|
||||
"options": [
|
||||
"str",
|
||||
"int",
|
||||
"float",
|
||||
"bool",
|
||||
"dict"
|
||||
],
|
||||
"options": ["str", "int", "float", "bool", "dict"],
|
||||
"type": "str"
|
||||
},
|
||||
{
|
||||
@@ -1289,10 +1218,7 @@
|
||||
"edit_mode": "inline",
|
||||
"formatter": "text",
|
||||
"name": "multiple",
|
||||
"options": [
|
||||
"True",
|
||||
"False"
|
||||
],
|
||||
"options": ["True", "False"],
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
@@ -1318,9 +1244,7 @@
|
||||
"display_name": "Input",
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1368,11 +1292,7 @@
|
||||
"external_options": {},
|
||||
"info": "Enable/disable Mirostat sampling for controlling perplexity.",
|
||||
"name": "mirostat",
|
||||
"options": [
|
||||
"Disabled",
|
||||
"Mirostat",
|
||||
"Mirostat 2.0"
|
||||
],
|
||||
"options": ["Disabled", "Mirostat", "Mirostat 2.0"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -1437,10 +1357,7 @@
|
||||
"external_options": {},
|
||||
"info": "Refer to https://ollama.com/library for more models.",
|
||||
"name": "model_name",
|
||||
"options": [
|
||||
"scb10x/typhoon2.1-gemma3-4b:latest",
|
||||
"qwen2.5:7b-instruct-q4_K_M"
|
||||
],
|
||||
"options": ["scb10x/typhoon2.1-gemma3-4b:latest", "qwen2.5:7b-instruct-q4_K_M"],
|
||||
"options_metadata": [],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
@@ -1562,9 +1479,7 @@
|
||||
"display_name": "Stop Tokens",
|
||||
"dynamic": false,
|
||||
"info": "Comma-separated list of tokens to signal the model to stop generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1607,9 +1522,7 @@
|
||||
"display_name": "System",
|
||||
"dynamic": false,
|
||||
"info": "System to use for generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1635,9 +1548,7 @@
|
||||
"display_name": "Tags",
|
||||
"dynamic": false,
|
||||
"info": "Comma-separated list of tags to add to the run trace.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1690,9 +1601,7 @@
|
||||
"display_name": "Template",
|
||||
"dynamic": false,
|
||||
"info": "Template to use for generating text.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -1834,10 +1743,7 @@
|
||||
"data": {
|
||||
"id": "LoopComponent-5vFOr",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Data",
|
||||
"DataFrame"
|
||||
],
|
||||
"base_classes": ["Data", "DataFrame"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -1845,9 +1751,7 @@
|
||||
"display_name": "Loop",
|
||||
"documentation": "https://docs.langflow.org/loop",
|
||||
"edited": false,
|
||||
"field_order": [
|
||||
"data"
|
||||
],
|
||||
"field_order": ["data"],
|
||||
"frozen": false,
|
||||
"icon": "infinity",
|
||||
"legacy": false,
|
||||
@@ -1872,16 +1776,12 @@
|
||||
"cache": true,
|
||||
"display_name": "Item",
|
||||
"group_outputs": true,
|
||||
"loop_types": [
|
||||
"Message"
|
||||
],
|
||||
"loop_types": ["Message"],
|
||||
"method": "item_output",
|
||||
"name": "item",
|
||||
"selected": "Data",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"types": ["Data"],
|
||||
"value": "__UNDEFINED__"
|
||||
},
|
||||
{
|
||||
@@ -1893,9 +1793,7 @@
|
||||
"name": "done",
|
||||
"selected": "DataFrame",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"types": ["DataFrame"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -1926,9 +1824,7 @@
|
||||
"display_name": "Inputs",
|
||||
"dynamic": false,
|
||||
"info": "The initial DataFrame to iterate over.",
|
||||
"input_types": [
|
||||
"DataFrame"
|
||||
],
|
||||
"input_types": ["DataFrame"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "data",
|
||||
@@ -1965,26 +1861,18 @@
|
||||
"data": {
|
||||
"id": "Prompt Template-dKwcS",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {
|
||||
"template": [
|
||||
"extracted_text"
|
||||
]
|
||||
"template": ["extracted_text"]
|
||||
},
|
||||
"description": "Create a prompt template with dynamic variables.",
|
||||
"display_name": "Prompt Template",
|
||||
"documentation": "https://docs.langflow.org/components-prompts",
|
||||
"edited": false,
|
||||
"error": null,
|
||||
"field_order": [
|
||||
"template",
|
||||
"use_double_brackets",
|
||||
"tool_placeholder"
|
||||
],
|
||||
"field_order": ["template", "use_double_brackets", "tool_placeholder"],
|
||||
"frozen": false,
|
||||
"full_path": null,
|
||||
"icon": "prompts",
|
||||
@@ -2022,9 +1910,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2059,9 +1945,7 @@
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"info": "",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"load_from_db": false,
|
||||
"multiline": true,
|
||||
@@ -2099,9 +1983,7 @@
|
||||
"display_name": "Tool Placeholder",
|
||||
"dynamic": false,
|
||||
"info": "A placeholder input for tool mode.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2162,9 +2044,7 @@
|
||||
"data": {
|
||||
"id": "ParserComponent-Xspgr",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"base_classes": ["Message"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -2172,12 +2052,7 @@
|
||||
"display_name": "Parser",
|
||||
"documentation": "https://docs.langflow.org/parser",
|
||||
"edited": false,
|
||||
"field_order": [
|
||||
"input_data",
|
||||
"mode",
|
||||
"pattern",
|
||||
"sep"
|
||||
],
|
||||
"field_order": ["input_data", "mode", "pattern", "sep"],
|
||||
"frozen": false,
|
||||
"icon": "braces",
|
||||
"last_updated": "2026-03-13T08:19:27.565Z",
|
||||
@@ -2210,9 +2085,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2269,10 +2142,7 @@
|
||||
"display_name": "Data or DataFrame",
|
||||
"dynamic": false,
|
||||
"info": "Accepts either a DataFrame or a Data object.",
|
||||
"input_types": [
|
||||
"DataFrame",
|
||||
"Data"
|
||||
],
|
||||
"input_types": ["DataFrame", "Data"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"name": "input_data",
|
||||
@@ -2294,10 +2164,7 @@
|
||||
"dynamic": false,
|
||||
"info": "Convert into raw string instead of using a template.",
|
||||
"name": "mode",
|
||||
"options": [
|
||||
"Parser",
|
||||
"Stringify"
|
||||
],
|
||||
"options": ["Parser", "Stringify"],
|
||||
"override_skip": false,
|
||||
"placeholder": "",
|
||||
"real_time_refresh": true,
|
||||
@@ -2318,9 +2185,7 @@
|
||||
"display_name": "Template",
|
||||
"dynamic": true,
|
||||
"info": "Use variables within curly brackets to extract column values for DataFrames or key values for Data.For example: `Name: {Name}, Age: {Age}, Country: {Country}`",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2345,9 +2210,7 @@
|
||||
"display_name": "Separator",
|
||||
"dynamic": false,
|
||||
"info": "String used to separate rows/items.",
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"input_types": ["Message"],
|
||||
"list": false,
|
||||
"list_add_label": "Add More",
|
||||
"load_from_db": false,
|
||||
@@ -2387,10 +2250,7 @@
|
||||
"data": {
|
||||
"id": "CustomComponent-WriteJsonIdempotent",
|
||||
"node": {
|
||||
"base_classes": [
|
||||
"CustomComponent",
|
||||
"Component"
|
||||
],
|
||||
"base_classes": ["CustomComponent", "Component"],
|
||||
"beta": false,
|
||||
"conditional_paths": [],
|
||||
"custom_fields": {},
|
||||
@@ -2471,9 +2331,7 @@
|
||||
"required_inputs": null,
|
||||
"selected": "Message",
|
||||
"tool_mode": true,
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"types": ["Message"],
|
||||
"value": "__UNDEFINED__"
|
||||
}
|
||||
],
|
||||
@@ -2518,4 +2376,4 @@
|
||||
"locked": false,
|
||||
"name": "OpenRAG V0.1",
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ const fs = require('fs');
|
||||
let lines = fs.readFileSync('n8n.workflow.json', 'utf8').split('\n');
|
||||
|
||||
const toRemove = [];
|
||||
for(let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith(" // Ollama Settings\\n OLLAMA_HOST:")) toRemove.push(i);
|
||||
if (lines[i].startsWith("const model = isFallback ? config.OLLAMA_MODEL_FALLBACK")) toRemove.push(i);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith(' // Ollama Settings\\n OLLAMA_HOST:')) toRemove.push(i);
|
||||
if (lines[i].startsWith('const model = isFallback ? config.OLLAMA_MODEL_FALLBACK')) toRemove.push(i);
|
||||
if (lines[i].startsWith(" response_to: String(meta.response_to || '').trim() || null,\\n")) toRemove.push(i);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- ==========================================================
|
||||
-- ==========================================================
|
||||
-- DMS v1.8.0 Schema Part 2/3: CREATE TABLE Statements
|
||||
-- รัน: mysql < 01-schema-drop.sql แล้วจึงรัน 02-schema-tables.sql
|
||||
-- ==========================================================
|
||||
@@ -105,6 +105,7 @@ CREATE TABLE refresh_tokens (
|
||||
expires_at DATETIME NOT NULL COMMENT 'วันหมดอายุ',
|
||||
is_revoked TINYINT(1) DEFAULT 0 COMMENT 'สถานะยกเลิก (1=Revoked)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
replaced_by_token VARCHAR(255) NULL COMMENT 'Token ใหม่ที่มาแทนที่ (Rotation)',
|
||||
FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บ Refresh Tokens สำหรับ Authentication';
|
||||
|
||||
@@ -42,10 +42,7 @@
|
||||
"name": "Form Trigger",
|
||||
"type": "n8n-nodes-base.formTrigger",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
31024,
|
||||
13504
|
||||
],
|
||||
"position": [31024, 13504],
|
||||
"webhookId": "e164a362-0c6b-4243-a5ad-b325aa943f4f",
|
||||
"notes": "เปิด URL เพื่อเลือก Model ก่อนรัน"
|
||||
},
|
||||
@@ -57,10 +54,7 @@
|
||||
"name": "Set Configuration",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
31216,
|
||||
13504
|
||||
],
|
||||
"position": [31216, 13504],
|
||||
"notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน"
|
||||
},
|
||||
{
|
||||
@@ -83,10 +77,7 @@
|
||||
"name": "Fetch Categories",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
31216,
|
||||
13696
|
||||
],
|
||||
"position": [31216, 13696],
|
||||
"notes": "ดึง Categories จาก Backend"
|
||||
},
|
||||
{
|
||||
@@ -109,10 +100,7 @@
|
||||
"name": "Fetch Tags",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
31392,
|
||||
13696
|
||||
],
|
||||
"position": [31392, 13696],
|
||||
"notes": "ดึง Tags ที่มีอยู่แล้วจาก Backend"
|
||||
},
|
||||
{
|
||||
@@ -126,10 +114,7 @@
|
||||
"name": "Check Backend Health",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
31392,
|
||||
13504
|
||||
],
|
||||
"position": [31392, 13504],
|
||||
"onError": "continueErrorOutput",
|
||||
"notes": "ตรวจสอบ Backend พร้อมใช้งาน"
|
||||
},
|
||||
@@ -141,10 +126,7 @@
|
||||
"name": "File Mount Check",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
31216,
|
||||
13904
|
||||
],
|
||||
"position": [31216, 13904],
|
||||
"notes": "ตรวจสอบ File System มีไฟล์ Excel และ Folder ตามตั้งค่า"
|
||||
},
|
||||
{
|
||||
@@ -157,10 +139,7 @@
|
||||
"name": "Read Checkpoint",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
31632,
|
||||
13744
|
||||
],
|
||||
"position": [31632, 13744],
|
||||
"alwaysOutputData": true,
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
@@ -180,10 +159,7 @@
|
||||
"name": "Read Excel Binary",
|
||||
"type": "n8n-nodes-base.readWriteFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
31392,
|
||||
13904
|
||||
],
|
||||
"position": [31392, 13904],
|
||||
"notes": "ดึงไฟล์ Excel ขึ้นมาไว้ในหน่วยความจำ"
|
||||
},
|
||||
{
|
||||
@@ -194,10 +170,7 @@
|
||||
"name": "Read Excel",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
31392,
|
||||
14112
|
||||
],
|
||||
"position": [31392, 14112],
|
||||
"notes": "แปลงข้อมูล Excel เป็น JSON Data"
|
||||
},
|
||||
{
|
||||
@@ -208,10 +181,7 @@
|
||||
"name": "Process Batch + Encoding",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
31808,
|
||||
13488
|
||||
],
|
||||
"position": [31808, 13488],
|
||||
"alwaysOutputData": true,
|
||||
"notes": "ตัด Batch + Normalize UTF-8"
|
||||
},
|
||||
@@ -223,10 +193,7 @@
|
||||
"name": "File Validator",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
31984,
|
||||
13488
|
||||
],
|
||||
"position": [31984, 13488],
|
||||
"notes": "ตรวจสอบไฟล์ PDF ตัวชี้ใน Directory จาก Config"
|
||||
},
|
||||
{
|
||||
@@ -239,10 +206,7 @@
|
||||
"name": "Check Fallback State",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
31792,
|
||||
13888
|
||||
],
|
||||
"position": [31792, 13888],
|
||||
"alwaysOutputData": true,
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
@@ -261,10 +225,7 @@
|
||||
"name": "Build AI Prompt",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32144,
|
||||
13872
|
||||
],
|
||||
"position": [32144, 13872],
|
||||
"notes": "สร้าง Prompt โดยใช้ Categories จาก System"
|
||||
},
|
||||
{
|
||||
@@ -282,10 +243,7 @@
|
||||
"name": "Ollama AI Analysis",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
31792,
|
||||
14096
|
||||
],
|
||||
"position": [31792, 14096],
|
||||
"notes": "เรียก Ollama วิเคราะห์เอกสาร"
|
||||
},
|
||||
{
|
||||
@@ -296,10 +254,7 @@
|
||||
"name": "Parse & Validate AI Response",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32000,
|
||||
14096
|
||||
],
|
||||
"position": [32000, 14096],
|
||||
"notes": "Parse JSON + Validate Schema + Enum Check"
|
||||
},
|
||||
{
|
||||
@@ -312,10 +267,7 @@
|
||||
"name": "Update Fallback State",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
32464,
|
||||
13472
|
||||
],
|
||||
"position": [32464, 13472],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "CHHfbKhMacNo03V4",
|
||||
@@ -332,10 +284,7 @@
|
||||
"name": "Confidence Router",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32160,
|
||||
14096
|
||||
],
|
||||
"position": [32160, 14096],
|
||||
"notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)"
|
||||
},
|
||||
{
|
||||
@@ -366,10 +315,7 @@
|
||||
"name": "Import to Backend",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
32704,
|
||||
13664
|
||||
],
|
||||
"position": [32704, 13664],
|
||||
"notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key"
|
||||
},
|
||||
{
|
||||
@@ -380,10 +326,7 @@
|
||||
"name": "Flag Checkpoint",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32880,
|
||||
13664
|
||||
],
|
||||
"position": [32880, 13664],
|
||||
"notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)"
|
||||
},
|
||||
{
|
||||
@@ -396,10 +339,7 @@
|
||||
"name": "Save Checkpoint",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
32928,
|
||||
13856
|
||||
],
|
||||
"position": [32928, 13856],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "CHHfbKhMacNo03V4",
|
||||
@@ -418,10 +358,7 @@
|
||||
"name": "Insert Review Queue",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
32896,
|
||||
14016
|
||||
],
|
||||
"position": [32896, 14016],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "CHHfbKhMacNo03V4",
|
||||
@@ -438,10 +375,7 @@
|
||||
"name": "Log Reject to CSV",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32624,
|
||||
14032
|
||||
],
|
||||
"position": [32624, 14032],
|
||||
"notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV"
|
||||
},
|
||||
{
|
||||
@@ -452,10 +386,7 @@
|
||||
"name": "Log Error to CSV",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
32448,
|
||||
14128
|
||||
],
|
||||
"position": [32448, 14128],
|
||||
"notes": "บันทึก Error ลง CSV (จาก File Validator)"
|
||||
},
|
||||
{
|
||||
@@ -468,10 +399,7 @@
|
||||
"name": "Log Error to DB",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
32752,
|
||||
14128
|
||||
],
|
||||
"position": [32752, 14128],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "CHHfbKhMacNo03V4",
|
||||
@@ -489,10 +417,7 @@
|
||||
"name": "Delay",
|
||||
"type": "n8n-nodes-base.wait",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
33104,
|
||||
14080
|
||||
],
|
||||
"position": [33104, 14080],
|
||||
"webhookId": "38e97a99-4dcc-4b63-977a-a02945a1c369",
|
||||
"notes": "หน่วงเวลาระหว่าง Batches"
|
||||
},
|
||||
@@ -604,10 +529,7 @@
|
||||
"name": "Route by Confidence",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
32336,
|
||||
13744
|
||||
]
|
||||
"position": [32336, 13744]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -618,10 +540,7 @@
|
||||
"name": "Read PDF File",
|
||||
"type": "n8n-nodes-base.readWriteFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
31824,
|
||||
13680
|
||||
],
|
||||
"position": [31824, 13680],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
@@ -665,10 +584,7 @@
|
||||
"name": "Extract PDF Text",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
32096,
|
||||
13664
|
||||
],
|
||||
"position": [32096, 13664],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
@@ -681,10 +597,7 @@
|
||||
"name": "Fetch DB Context",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
32000,
|
||||
13872
|
||||
],
|
||||
"position": [32000, 13872],
|
||||
"alwaysOutputData": true,
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
@@ -702,10 +615,7 @@
|
||||
"name": "Build Import Payload",
|
||||
"typeVersion": 2,
|
||||
"type": "n8n-nodes-base.code",
|
||||
"position": [
|
||||
32544,
|
||||
13664
|
||||
],
|
||||
"position": [32544, 13664],
|
||||
"notes": "สร้าง payload สำหรับ Import to Backend"
|
||||
},
|
||||
{
|
||||
@@ -716,10 +626,7 @@
|
||||
"name": "Upsert Tags",
|
||||
"typeVersion": 2,
|
||||
"type": "n8n-nodes-base.code",
|
||||
"position": [
|
||||
32592,
|
||||
13856
|
||||
],
|
||||
"position": [32592, 13856],
|
||||
"notes": "Upsert tags หลัง import สำเร็จ"
|
||||
},
|
||||
{
|
||||
@@ -732,10 +639,7 @@
|
||||
"name": "Link Tags to Correspondence",
|
||||
"typeVersion": 2.4,
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"position": [
|
||||
32768,
|
||||
13856
|
||||
],
|
||||
"position": [32768, 13856],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "CHHfbKhMacNo03V4",
|
||||
|
||||
@@ -6,7 +6,7 @@ async function test() {
|
||||
port: 3306,
|
||||
user: 'migration_bot',
|
||||
password: 'Center2025',
|
||||
database: 'lcbp3'
|
||||
database: 'lcbp3',
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user