diff --git a/backend/src/modules/ai/ai.controller.ts b/backend/src/modules/ai/ai.controller.ts index c41d7c7d..14cb3e36 100644 --- a/backend/src/modules/ai/ai.controller.ts +++ b/backend/src/modules/ai/ai.controller.ts @@ -12,6 +12,7 @@ // - 2026-05-30: เพิ่ม endpoints GET/POST/PATCH models และ GET vram/status สำหรับ dynamic AI model management และ VRAM monitoring (T031-T034, US2) // - 2026-06-01: [BUGFIX] submitSandboxOcr: เพิ่ม @ApiBearerAuth(), @HttpCode(ACCEPTED), Body({ engineType }) และส่ง engineType ไปยัง enqueueSandboxJob // - 2026-06-02: เพิ่ม REST endpoints GET /ai/ocr-engines และ POST /ai/ocr-engines/:engineId/select (T003, T004, ADR-033) และนำเข้า SystemException เพื่อป้องกันความเสียหายในการคอมไพล์ +// - 2026-06-06: [BUGFIX] เพิ่ม @Throttle({ default: { limit: 300, ttl: 60000 } }) บน GET admin/sandbox/job/:id เพื่อแก้ ThrottlerException spam จาก frontend polling // Controller สำหรับ AI Gateway Endpoints (ADR-023) import { @@ -452,6 +453,7 @@ export class AiController { @UseGuards(JwtAuthGuard, RbacGuard) @ApiBearerAuth() @RequirePermission('system.manage_all') + @Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min — รองรับ admin polling ทุก 200ms @ApiOperation({ summary: 'AI Admin Sandbox Job Status — ตรวจสอบสถานะ RAG sandbox job (T036)', diff --git a/backend/src/modules/ai/processors/ai-batch.processor.ts b/backend/src/modules/ai/processors/ai-batch.processor.ts index 5cd15667..7d8ac636 100644 --- a/backend/src/modules/ai/processors/ai-batch.processor.ts +++ b/backend/src/modules/ai/processors/ai-batch.processor.ts @@ -11,6 +11,7 @@ // - 2026-05-28: EC-001 ใช้ findOrSuggestTags เพื่อตรวจจับ Tag ใหม่และบันทึก aiIssues; EC-002 ตรวจสอบ UUID ของผู้ส่ง/ผู้รับ และ Flag เมื่อหาไม่พบ // - 2026-06-03: ADR-034 — เพิ่ม 'ocr-extract' job type + OCR_JOB_TYPES constant + processOcrExtract() ที่มี model switching logic (unload main → load OCR → generate → reload main) // - 2026-06-06: แก้ไข bug LLM JSON parse failure — เพิ่ม retry logic (2 attempts), debug log raw response, และปรับปรุง error message ให้แสดงทั้ง raw และ cleaned response +// - 2026-06-06: เพิ่ม OCR text truncation (MAX_OCR_TEXT_CHARS=15000) เพื่อป้องกัน context overflow เมื่อเอกสารยาวมากชน num_ctx 8192 import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Logger } from '@nestjs/common'; @@ -75,6 +76,9 @@ export interface AiBatchJobData { idempotencyKey: string; } +/** OCR text สูงสุดที่ส่งเข้า LLM prompt — ป้องกัน context overflow (num_ctx 8192, Thai ~3 chars/token) */ +const MAX_OCR_TEXT_CHARS = 15000; + const readString = (value: unknown): string | undefined => typeof value === 'string' && value.trim().length > 0 ? value : undefined; @@ -421,8 +425,16 @@ export class AiBatchProcessor extends WorkerHost { overrideProjPublicId === 'default' ? undefined : overrideProjPublicId ); + const ocrTextSafe = + ocrResult.text.length > MAX_OCR_TEXT_CHARS + ? (this.logger.warn( + `OCR text truncated: ${ocrResult.text.length} chars > ${MAX_OCR_TEXT_CHARS} limit (context overflow protection)` + ), + ocrResult.text.substring(0, MAX_OCR_TEXT_CHARS)) + : ocrResult.text; + const resolvedPrompt = activePrompt.template - .replace('{{ocr_text}}', ocrResult.text) + .replace('{{ocr_text}}', ocrTextSafe) .replace( '{{master_data_context}}', JSON.stringify(masterDataContext, null, 2) @@ -636,8 +648,16 @@ export class AiBatchProcessor extends WorkerHost { projectPublicId === 'default' ? undefined : projectPublicId ); + const ocrTextSafe = + ocrText.length > MAX_OCR_TEXT_CHARS + ? (this.logger.warn( + `OCR text truncated: ${ocrText.length} chars > ${MAX_OCR_TEXT_CHARS} limit (context overflow protection)` + ), + ocrText.substring(0, MAX_OCR_TEXT_CHARS)) + : ocrText; + const resolvedPrompt = targetPrompt.template - .replace('{{ocr_text}}', ocrText) + .replace('{{ocr_text}}', ocrTextSafe) .replace( '{{master_data_context}}', JSON.stringify(masterDataContext, null, 2) diff --git a/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test.md b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test.md new file mode 100644 index 00000000..29b35e1d --- /dev/null +++ b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test.md @@ -0,0 +1,62 @@ +HEC CHEC(THAI) CO.,LTD บริษัท ซีเอชบี (ไทย) จำกัด +Bangkok Liaison Office: 87/2 CRC Tower, 20 FL Wireless Rd, Lumphini, Pathumwan, Bangkok 10330 +สำนักงานใหญ่ : 87/2 อาคารเซียาร์ซี พาวเวอร์ ชั้น 20 ถนนวิทยุ แขวงลุมพินี เขตปทุมวัน กรุงเทพมหานคร 10330 Tel.02-002-8683 Fax. 02-002-8685 +Project Site Office: Thung Sukhla,Si Racha District Chon Buri 20230 สำนักงานโครงการ : ตำบลธงสุขลา สันทวีศรีราชา จังหวัดชลบุรี 20230 +ต้นฉบับ +Project of Laem Chabang Port Phase 3, Infrastructure Works (Contract 2) +สัญญาจ้างเหมาก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบังระยะที่ 3 ( ส่วนที่ 2 ) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และสาธารณูปโภค +PSLCP3 +ได้รับเอกสารฉบับนี้แล้ว +เลขที่รับเอกสาร.................... +วันที่ 19 ก.พ. 2568 +ผู้รับ ลายเซ็น +10 กุมภาพันธ์ 2568 +เรื่อง ส่งมอบพื้นที่ถมทะเลพื้นที่ 1 และ 2 +โครงการพัฒนาท่าเรือแหลมฉบังระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และสาธารณูปโภค +เรียน คุณสุวัฒน์ พิพัฒนปัญญกูล +ผู้จัดการโครงการงานควบคุมงานก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4) +อ้างถึง 1) สัญญาจ้างเหมาก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบังระยะที่ 3 (ส่วนที่2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนนและสาธารณูปโภค สัญญาเลขที่ ทลฉ.จ.19/2567 ลงวันที่ 31 กรกฎาคม 2567 +2) หนังสือการท่าเรือแหลมฉบัง เลขที่ สคฉ.03/0036 ลงวันที่ 4 กุมภาพันธ์ 2568 เรื่อง ขอส่งมอบ พื้นที่ โครงการพัฒนาท่าเรือแหลมฉบังระยะที่3 (ส่วนที่2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบ ถนนและสาธารณูปโภค +3) หนังสือขอข้อคิดเห็นและข้อมูลเพิ่มเติม เลขที่ LCBP3-C2-RFI-BUD-0001-A ลงวันที่ 4 มกราคม 2568 เรื่อง ขอข้อคิดเห็นงานก่อสร้างอาคารบริเวณพื้นที่ 1 (Area 1) เมื่อเทียบกับระดับดินเดิม ณ ปัจจุบัน จากการสำรวจลักษณะภูมิประเทศ (Topographic Survey) +4) หนังสือขออนุมัติเอกสาร เลขที่ LCBP3-C2-RFA-ROW-RPT-0007-A ลงวันที่ 22 มกราคม 2568 เรื่อง ขออนุมัติรายงานการสำรวจเพื่อการก่อสร้างอาคารประตูตรวจสอบ 5 และถนน RN-2 +สิ่งที่ส่งมาด้วย +1) รูปแสดง การร่วมตรวจสอบพื้นที่ระหว่างผู้ควบคุมงาน/คคง. และผู้รับจ้าง/ผรม.2 และ รูปแสดงอุปสรรคในพื้นที่ก่อสร้างที่ได้รับมอบพื้นที่ถมทะเลพื้นที่ 1 และ 2 +ตามสัญญาที่อ้างถึง +1) สัญญาจ้างเหมาก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบังระยะที่ 3 (ส่วนที่2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนนและสาธารณูปโภค สัญญาเลขที่ ทลฉ.จ.19/2567 ลงวันที่ 31 กรกฎาคม 2567 หนังสือการท่าเรือแหลมฉบัง ที่อ้างถึง +2) เรื่องขอส่งมอบพื้นที่ โครงการพัฒนาท่าเรือแหลม ฉบังระยะที่3 (ส่วนที่2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนนและระบบสาธารณูปโภค +ผู้รับจ้าง/ผรม.2 รับทราบการส่งมอบพื้นที่ถมทะเลพื้นที่ 1 และ 2 และขอแจ้งยืนยันวันเริ่ม Keydate ตามรายละเอียดดังนี้ +เริ่มใช้เมื่อวันที่ 20 กันยายน 2567 +3 diff --git a/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.ps1 b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.ps1 new file mode 100644 index 00000000..a1053420 --- /dev/null +++ b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.ps1 @@ -0,0 +1,157 @@ +# ทดสอบ typhoon2.5-np-dms โดยใช้ prompt template จริง + OCR text จาก test.md + master data context จาก DB +# รันด้วย: powershell -ExecutionPolicy Bypass -File test_llm.ps1 + +$ErrorActionPreference = "Stop" + +$OLLAMA_URL = "http://192.168.10.100:11434" +$MODEL = "typhoon2.5-np-dms:latest" +$OCR_TEXT_FILE = Join-Path $PSScriptRoot "test.md" + +# --- Prompt Template (ดึงจาก DB: ai_prompts version=2, prompt_type=ocr_extraction) --- +$TEMPLATE = @" +คุณคือเอนจิ้นสกัดข้อมูลอัจฉริยะ (Document Intelligence Engine) +วิเคราะห์ข้อความ OCR ที่ได้รับจากเอกสารของโครงการ Laem Chabang Port Phase 3 และสกัดข้อมูลเมตาดาต้าให้ออกมาเป็น JSON object ที่ถูกต้องตามโครงสร้างที่กำหนด + +ข้อความ OCR ที่สกัดได้: +{{ocr_text}} + +ข้อมูลอ้างอิงของระบบ (Master Data Context): +{{master_data_context}} + +กฎการสกัดข้อมูล: +1. วิเคราะห์และจับคู่ข้อมูลจากข้อความ OCR กับข้อมูลอ้างอิงที่ระบุใน Master Data Context เสมอ +2. สำหรับโครงการ (project) ให้ค้นหาและสกัดส่งกลับเป็น UUID ของโครงการ (projectPublicId) +3. สำหรับประเภทเอกสารโต้ตอบ (correspondence type) ให้สกัดรหัสส่งกลับมา (correspondenceTypeCode) เช่น RFA, Transmittal +4. สำหรับสาขางาน (discipline) ให้ส่งคืนรหัสส่งกลับมา (disciplineCode) เช่น GEN, STR +5. สำหรับหน่วยงานผู้ส่ง (originator) ค้นหาจาก availableOrganizations และส่งกลับมาเป็น UUID (originatorOrganizationPublicId) +6. สำหรับหน่วยงานผู้รับ (recipients) ให้ส่งกลับมาเป็นรายการ Array ของออบเจกต์ ซึ่งมี UUID ขององค์กร (organizationPublicId) และประเภทผู้รับ (recipientType: "TO" หรือ "CC") เสมอ +7. สำหรับหัวข้อเอกสาร (subject) ให้สกัดหัวข้อหรือชื่อเรื่องของเอกสารภาษาไทยหรือภาษาอังกฤษ +8. วันที่ของเอกสาร (documentDate) ให้ส่งคืนในรูปแบบ YYYY-MM-DD +9. รายการแท็ก (tags) สกัดคำสำคัญหรือคำแนะนำ Tags (สอดคล้องกับ availableTags หากมี) +10. สรุปความเนื้อหา (summary) เขียนสรุปรายละเอียดเอกสารสั้นกระชับ 4-5 ประโยคเป็นภาษาไทยอย่างสละสลวย +11. confidence: ค่าความมั่นใจในการสกัดข้อมูลนี้ (ทศนิยมระหว่าง 0.0 ถึง 1.0) + +ส่งคืนคำตอบเฉพาะ JSON Object ที่ถูกต้องเท่านั้น ห้ามใส่บล็อกโค้ด markdown หรือคำอธิบายเพิ่มเติมใดๆ +โครงสร้าง JSON ผลลัพธ์: +{ + "projectPublicId": "string หรือ null", + "correspondenceTypeCode": "string หรือ null", + "disciplineCode": "string หรือ null", + "originatorOrganizationPublicId": "string หรือ null", + "recipients": [ + { + "organizationPublicId": "string", + "recipientType": "TO หรือ CC" + } + ], + "subject": "string หรือ null", + "documentDate": "string:YYYY-MM-DD หรือ null", + "tags": ["string"], + "summary": "string หรือ null", + "confidence": 0.95 +} +"@ + +# --- Master Data Context (ดึงจาก DB จริง) --- +$MASTER_DATA = @{ + availableProjects = @( + @{code = "LCBP3"; uuid = "c957f1e3-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)"} + @{code = "LCBP3-C1"; uuid = "c957f44b-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล"} + @{code = "LCBP3-C2"; uuid = "c957f523-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค"} + @{code = "LCBP3-C3"; uuid = "c957f57c-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง"} + @{code = "LCBP3-C4"; uuid = "c957f5cc-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง"} + ) + availableOrganizations = @( + @{code = "กทท."; uuid = "c94cb0b4-538b-11f1-8c7d-0242ac1d0007"; name = "การท่าเรือแห่งประเทศไทย"} + @{code = "สคฉ.3"; uuid = "c94cb3f6-538b-11f1-8c7d-0242ac1d0007"; name = "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3"} + @{code = "สคฉ.3-01"; uuid = "c94cb532-538b-11f1-8c7d-0242ac1d0007"; name = "ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน"} + @{code = "สคฉ.3-02"; uuid = "c94cb5ab-538b-11f1-8c7d-0242ac1d0007"; name = "ตรวจรับพัสดุ งานทางทะเล"} + @{code = "สคฉ.3-03"; uuid = "c94cb616-538b-11f1-8c7d-0242ac1d0007"; name = "ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค"} + @{code = "คคง."; uuid = "c94cb8ac-538b-11f1-8c7d-0242ac1d0007"; name = "Construction Supervision Ltd."} + @{code = "ผรม.1"; uuid = "c94cb907-538b-11f1-8c7d-0242ac1d0007"; name = "Contractor งานทางทะเล"} + @{code = "ผรม.2"; uuid = "c94cb95e-538b-11f1-8c7d-0242ac1d0007"; name = "Contractor งานก่อสร้าง"} + @{code = "ผรม.3"; uuid = "c94cb9b3-538b-11f1-8c7d-0242ac1d0007"; name = "Contractor งานก่อสร้าง ส่วนที่ 3"} + @{code = "ผรม.4"; uuid = "c94cba0c-538b-11f1-8c7d-0242ac1d0007"; name = "Contractor งานก่อสร้าง ส่วนที่ 4"} + ) + availableDisciplines = @( + @{code = "GEN"; name = "งานบริหารโครงการ"} + @{code = "COD"; name = "สัญญาและข้อโต้แย้ง"} + @{code = "QSB"; name = "สำรวจปริมาณและควบคุมงบประมาณ"} + @{code = "PPG"; name = "บริหารแผนและความก้าวหน้า"} + @{code = "BST"; name = "งานโครงสร้างอาคาร"} + @{code = "UTL"; name = "งานระบบสาธารณูปโภค"} + @{code = "EPW"; name = "งานระบบไฟฟ้า"} + @{code = "SRV"; name = "งานสำรวจ"} + @{code = "ODC"; name = "สำนักงาน-ควบคุมเอกสาร"} + ) + availableCorrespondenceTypes = @( + @{code = "RFA"; name = "Request for Approval"} + @{code = "RFI"; name = "Request for Information"} + @{code = "TRANSMITTAL"; name = "Transmittal"} + @{code = "LETTER"; name = "Letter"} + @{code = "MEMO"; name = "Memorandum"} + @{code = "MOM"; name = "Minutes of Meeting"} + @{code = "NOTICE"; name = "Notice"} + @{code = "OTHER"; name = "Other"} + ) + availableTags = @() +} + +function Check-Models { + try { + $resp = Invoke-RestMethod -Uri "$OLLAMA_URL/api/ps" -Method Get -TimeoutSec 5 + if ($resp.models) { + Write-Host " Models in VRAM: $($resp.models.name -join ', ')" + } else { + Write-Host " VRAM: ไม่มี model โหลดอยู่ (ว่าง)" + } + } catch { + Write-Host " Error checking models: $_" + } +} + +Write-Host "=" * 60 +Write-Host "🔍 ตรวจสอบ VRAM ก่อนรัน..." +Check-Models + +$ocrText = Get-Content $OCR_TEXT_FILE -Raw -Encoding UTF8 +Write-Host "`n📄 OCR text: $($ocrText.Length) chars" + +$masterDataJson = $MASTER_DATA | ConvertTo-Json -Depth 10 -Compress:$false +$prompt = $TEMPLATE -replace "{{ocr_text}}", $ocrText -replace "{{master_data_context}}", $masterDataJson +Write-Host "📝 Total prompt: $($prompt.Length) chars" +Write-Host "=" * 60 +Write-Host "⏳ กำลังส่งไปยัง Ollama..." + +$body = @{ + model = $MODEL + prompt = $prompt + stream = $false +} | ConvertTo-Json -Depth 10 + +$start = Get-Date +try { + $resp = Invoke-RestMethod -Uri "$OLLAMA_URL/api/generate" -Method Post -Body $body -ContentType "application/json" -TimeoutSec 180 + $elapsed = (Get-Date - $start).TotalSeconds + $rawResponse = $resp.response + + Write-Host "`n✅ Response ใน $([math]::Round($elapsed, 1))s" + Write-Host "-" * 60 + Write-Host $rawResponse + Write-Host "-" * 60 + + $cleaned = $rawResponse -replace "```json", "" -replace "```", "" -replace "^\s+", "" -replace "\s+$", "" + try { + $parsed = $cleaned | ConvertFrom-Json + Write-Host "`n✅ JSON parse สำเร็จ!" + $parsed | ConvertTo-Json -Depth 10 + } catch { + Write-Host "`n❌ JSON parse ล้มเหลว: $_" + Write-Host " Raw (200 chars): $($rawResponse.Substring(0, [Math]::Min(200, $rawResponse.Length)))" + } +} catch { + Write-Host "`n❌ Error: $_" +} + +Write-Host "`n🔍 ตรวจสอบ VRAM หลังรัน..." +Check-Models diff --git a/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.py b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.py new file mode 100644 index 00000000..8e89d9a8 --- /dev/null +++ b/specs/04-Infrastructure-OPS/04-00-docker-compose/Desk-5439/test_llm.py @@ -0,0 +1,173 @@ +""" +ทดสอบ typhoon2.5-np-dms โดยใช้ prompt template จริง + OCR text จาก test.md + master data context จาก DB +รันด้วย: python test_llm.py +""" + +import json +import time +import urllib.request +import urllib.error +from pathlib import Path + +OLLAMA_URL = "http://192.168.10.100:11434" +MODEL = "typhoon2.5-np-dms:latest" +OCR_TEXT_FILE = Path(__file__).parent / "test.md" + +# --- Prompt Template (ดึงจาก DB: ai_prompts version=2, prompt_type=ocr_extraction) --- +TEMPLATE = """คุณคือเอนจิ้นสกัดข้อมูลอัจฉริยะ (Document Intelligence Engine) +วิเคราะห์ข้อความ OCR ที่ได้รับจากเอกสารของโครงการ Laem Chabang Port Phase 3 และสกัดข้อมูลเมตาดาต้าให้ออกมาเป็น JSON object ที่ถูกต้องตามโครงสร้างที่กำหนด + +ข้อความ OCR ที่สกัดได้: +{{ocr_text}} + +ข้อมูลอ้างอิงของระบบ (Master Data Context): +{{master_data_context}} + +กฎการสกัดข้อมูล: +1. วิเคราะห์และจับคู่ข้อมูลจากข้อความ OCR กับข้อมูลอ้างอิงที่ระบุใน Master Data Context เสมอ +2. สำหรับโครงการ (project) ให้ค้นหาและสกัดส่งกลับเป็น UUID ของโครงการ (projectPublicId) +3. สำหรับประเภทเอกสารโต้ตอบ (correspondence type) ให้สกัดรหัสส่งกลับมา (correspondenceTypeCode) เช่น RFA, Transmittal +4. สำหรับสาขางาน (discipline) ให้ส่งคืนรหัสส่งกลับมา (disciplineCode) เช่น GEN, STR +5. สำหรับหน่วยงานผู้ส่ง (originator) ค้นหาจาก availableOrganizations และส่งกลับมาเป็น UUID (originatorOrganizationPublicId) +6. สำหรับหน่วยงานผู้รับ (recipients) ให้ส่งกลับมาเป็นรายการ Array ของออบเจกต์ ซึ่งมี UUID ขององค์กร (organizationPublicId) และประเภทผู้รับ (recipientType: "TO" หรือ "CC") เสมอ +7. สำหรับหัวข้อเอกสาร (subject) ให้สกัดหัวข้อหรือชื่อเรื่องของเอกสารภาษาไทยหรือภาษาอังกฤษ +8. วันที่ของเอกสาร (documentDate) ให้ส่งคืนในรูปแบบ YYYY-MM-DD +9. รายการแท็ก (tags) สกัดคำสำคัญหรือคำแนะนำ Tags (สอดคล้องกับ availableTags หากมี) +10. สรุปความเนื้อหา (summary) เขียนสรุปรายละเอียดเอกสารสั้นกระชับ 4-5 ประโยคเป็นภาษาไทยอย่างสละสลวย +11. confidence: ค่าความมั่นใจในการสกัดข้อมูลนี้ (ทศนิยมระหว่าง 0.0 ถึง 1.0) + +ส่งคืนคำตอบเฉพาะ JSON Object ที่ถูกต้องเท่านั้น ห้ามใส่บล็อกโค้ด markdown หรือคำอธิบายเพิ่มเติมใดๆ +โครงสร้าง JSON ผลลัพธ์: +{ + "projectPublicId": "string หรือ null", + "correspondenceTypeCode": "string หรือ null", + "disciplineCode": "string หรือ null", + "originatorOrganizationPublicId": "string หรือ null", + "recipients": [ + { + "organizationPublicId": "string", + "recipientType": "TO หรือ CC" + } + ], + "subject": "string หรือ null", + "documentDate": "string:YYYY-MM-DD หรือ null", + "tags": ["string"], + "summary": "string หรือ null", + "confidence": 0.95 +}""" + +# --- Master Data Context (ดึงจาก DB จริง) --- +MASTER_DATA = { + "availableProjects": [ + {"code": "LCBP3", "uuid": "c957f1e3-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)"}, + {"code": "LCBP3-C1", "uuid": "c957f44b-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล"}, + {"code": "LCBP3-C2", "uuid": "c957f523-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค"}, + {"code": "LCBP3-C3", "uuid": "c957f57c-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง"}, + {"code": "LCBP3-C4", "uuid": "c957f5cc-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง"}, + ], + "availableOrganizations": [ + {"code": "กทท.", "uuid": "c94cb0b4-538b-11f1-8c7d-0242ac1d0007", "name": "การท่าเรือแห่งประเทศไทย"}, + {"code": "สคฉ.3", "uuid": "c94cb3f6-538b-11f1-8c7d-0242ac1d0007", "name": "โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3"}, + {"code": "สคฉ.3-01", "uuid": "c94cb532-538b-11f1-8c7d-0242ac1d0007", "name": "ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน"}, + {"code": "สคฉ.3-02", "uuid": "c94cb5ab-538b-11f1-8c7d-0242ac1d0007", "name": "ตรวจรับพัสดุ งานทางทะเล"}, + {"code": "สคฉ.3-03", "uuid": "c94cb616-538b-11f1-8c7d-0242ac1d0007", "name": "ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค"}, + {"code": "คคง.", "uuid": "c94cb8ac-538b-11f1-8c7d-0242ac1d0007", "name": "Construction Supervision Ltd."}, + {"code": "ผรม.1", "uuid": "c94cb907-538b-11f1-8c7d-0242ac1d0007", "name": "Contractor งานทางทะเล"}, + {"code": "ผรม.2", "uuid": "c94cb95e-538b-11f1-8c7d-0242ac1d0007", "name": "Contractor งานก่อสร้าง"}, + {"code": "ผรม.3", "uuid": "c94cb9b3-538b-11f1-8c7d-0242ac1d0007", "name": "Contractor งานก่อสร้าง ส่วนที่ 3"}, + {"code": "ผรม.4", "uuid": "c94cba0c-538b-11f1-8c7d-0242ac1d0007", "name": "Contractor งานก่อสร้าง ส่วนที่ 4"}, + ], + "availableDisciplines": [ + {"code": "GEN", "name": "งานบริหารโครงการ"}, + {"code": "COD", "name": "สัญญาและข้อโต้แย้ง"}, + {"code": "QSB", "name": "สำรวจปริมาณและควบคุมงบประมาณ"}, + {"code": "PPG", "name": "บริหารแผนและความก้าวหน้า"}, + {"code": "BST", "name": "งานโครงสร้างอาคาร"}, + {"code": "UTL", "name": "งานระบบสาธารณูปโภค"}, + {"code": "EPW", "name": "งานระบบไฟฟ้า"}, + {"code": "SRV", "name": "งานสำรวจ"}, + {"code": "ODC", "name": "สำนักงาน-ควบคุมเอกสาร"}, + ], + "availableCorrespondenceTypes": [ + {"code": "RFA", "name": "Request for Approval"}, + {"code": "RFI", "name": "Request for Information"}, + {"code": "TRANSMITTAL", "name": "Transmittal"}, + {"code": "LETTER", "name": "Letter"}, + {"code": "MEMO", "name": "Memorandum"}, + {"code": "MOM", "name": "Minutes of Meeting"}, + {"code": "NOTICE", "name": "Notice"}, + {"code": "OTHER", "name": "Other"}, + ], + "availableTags": [], +} + + +def check_models(): + req = urllib.request.Request(f"{OLLAMA_URL}/api/ps") + with urllib.request.urlopen(req, timeout=5) as resp: + data = json.loads(resp.read()) + models = data.get("models", []) + if models: + print(f" Models in VRAM: {[m['name'] for m in models]}") + else: + print(" VRAM: ไม่มี model โหลดอยู่ (ว่าง)") + + +def main(): + print("=" * 60) + print("🔍 ตรวจสอบ VRAM ก่อนรัน...") + check_models() + + ocr_text = OCR_TEXT_FILE.read_text(encoding="utf-8") + print(f"\n📄 OCR text: {len(ocr_text)} chars") + + prompt = TEMPLATE.replace("{{ocr_text}}", ocr_text).replace( + "{{master_data_context}}", json.dumps(MASTER_DATA, ensure_ascii=False, indent=2) + ) + print(f"📝 Total prompt: {len(prompt)} chars") + print("=" * 60) + print("⏳ กำลังส่งไปยัง Ollama...") + + body = json.dumps({ + "model": MODEL, + "prompt": prompt, + "stream": False, + }).encode("utf-8") + + start = time.time() + req = urllib.request.Request( + f"{OLLAMA_URL}/api/generate", + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=180) as resp: + result = json.loads(resp.read()) + elapsed = time.time() - start + raw_response = result.get("response", "") + + print(f"\n✅ Response ใน {elapsed:.1f}s") + print("-" * 60) + print(raw_response) + print("-" * 60) + + # พยายาม parse JSON + cleaned = raw_response.replace("```json", "").replace("```", "").strip() + try: + parsed = json.loads(cleaned) + print("\n✅ JSON parse สำเร็จ!") + print(json.dumps(parsed, ensure_ascii=False, indent=2)) + except json.JSONDecodeError as e: + print(f"\n❌ JSON parse ล้มเหลว: {e}") + print(f" Raw (200 chars): {raw_response[:200]!r}") + + except urllib.error.URLError as e: + print(f"\n❌ Connection error: {e}") + + print("\n🔍 ตรวจสอบ VRAM หลังรัน...") + check_models() + + +if __name__ == "__main__": + main() diff --git a/specs/88-logs/_backend_logs.md b/specs/88-logs/_backend_logs.md index 440f14ea..49c8af6f 100644 Binary files a/specs/88-logs/_backend_logs.md and b/specs/88-logs/_backend_logs.md differ