Files
lcbp3/CONTEXT.md
T
admin ae1b1f35e1
CI / CD Pipeline / build (push) Successful in 4m51s
CI / CD Pipeline / deploy (push) Successful in 12m7s
feat(ai): ADR-032 Typhoon OCR integration - models, processors, cache, VRAM monitor, sandbox UI
2026-05-30 22:18:51 +07:00

24 KiB

LCBP3 / NAP-DMS Context

ระบบจัดการเอกสารงานก่อสร้าง (DMS) สำหรับโครงการ LCBP3 — เน้นการควบคุม Correspondence, RFA, Transmittal, Drawing พร้อมผู้ช่วย AI แบบ on-premises ที่ทำงานภายใต้ Workflow Engine กลางและขอบเขต AI ที่เข้มงวด (ADR-023A)

Agent/ tooling context: สำหรับ Hermes Agent, Telegram Bridge, และ DevOps tooling → ดู specs/06-Decision-Records/CONTEXT-ADR-031.md Typhoon OCR context: สำหรับ Typhoon OCR-3B และ typhoon2.1-gemma3-4b integration → ดู specs/06-Decision-Records/ADR-032-typhoon-ocr-integration.md

Language

Documents

Correspondence: ซองจดหมาย/เอกสารทุกประเภทที่หมุนเวียนในโครงการ เป็น parent ของ RFA / Transmittal / Memo Avoid: Letter, Communication, Document (generic)

RFA (Request For Approval): Correspondence ประเภทขออนุมัติ มี revision และอ้างอิง Drawing Revision ผ่าน rfa_items Avoid: Approval Request, Submit for Approval

Transmittal: Correspondence ที่ใช้ส่งมอบเอกสาร/แบบ ไม่ใช่จดหมายปะหน้า Avoid: Delivery Note, Cover Letter

Shop Drawing: แบบที่ผู้รับเหมาจัดทำเพื่อขออนุมัติก่อนก่อสร้าง Avoid: Construction Drawing

Contract Drawing: แบบต้นฉบับตามสัญญา ไม่ใช่ Shop Drawing Avoid: Design Drawing, Blueprint

Workflow

Workflow Engine: State machine กลาง DSL-based (ADR-001) — authority เดียวของการเปลี่ยน state ของทุก Correspondence Avoid: Approval Flow, Process Engine, RFA status flow (เป็นเพียง definition หนึ่ง)

Workflow Definition: Row ใน workflow_definitions ระบุ DSL ของ flow เช่น RFA_FLOW_V1, CORRESPONDENCE_FLOW_V1 Avoid: Approval logic, Hardcoded flow

Workflow Instance: Row ใน workflow_instances = สถานะปัจจุบันของเอกสารหนึ่งฉบับ — source of truth ของ state Avoid: Status, Stage (ใช้ภายใน DSL ได้แต่ห้ามแทน instance)

Workflow Transition: การเปลี่ยน state ที่บันทึกใน workflow_histories พร้อม actor_user_id (มนุษย์เท่านั้น) Avoid: Auto-execute, AI-driven approval

Intent Classification

Intent Classifier: Service ที่แปลงคำถามธรรมชาติ (ไทย/อังกฤษปน) → Server-side Intent enum ใช้ Hybrid strategy: Pattern First → LLM Fallback (ADR-024) Avoid: NLU, NLP router, LangChain router

Server-side Intent: Enum ของคำขอที่ AI Gateway รองรับ — สร้างจาก ai_intent_definitions table ไม่ใช่ hardcode Avoid: Tool, LLM tool, LangChain tool

Pattern Layer: ชั้นแรกของ Intent Classifier — keyword/regex match จาก ai_intent_patterns table, cache ใน Redis TTL 5 min Avoid: Rule engine, NLU pipeline

LLM Fallback: ชั้นที่สองของ Intent Classifier — synchronous Ollama call (gemma4:e4b Q80) เมื่อ Pattern Layer ไม่ match, ใช้ semaphore max=3 _Avoid: BullMQ-based classification, async intent routing

AI

AI Document Assistant: ผู้ช่วยที่ให้ Insight + Suggest + Notify โดยไม่เปลี่ยน state ของเอกสารเอง (ADR-023A) Avoid: AI Document Controller, AI Agent, Autonomous Agent

AI Gateway: NestJS module ที่เป็นจุดเข้าเดียวของทุกคำขอ AI — enforce CASL + tenant scope ก่อนส่งงานเข้า BullMQ Avoid: AI Service (generic), Tool Layer

Document Chunk: Row ใน ai_document_chunks (MariaDB) เก็บ chunk text + metadata, ground truth สำหรับ re-embed Avoid: ai_embeddings, embedding row

Vector Point: Point ใน Qdrant — เก็บแค่ chunk_public_id, vector, และ payload { project_public_id, document_public_id, chunk_index } Avoid: Embedding (ambiguous), Vector record

RAG Query: Pipeline: embed query → QdrantService.search(projectPublicId, vector) → ดึง chunk_text จาก MariaDB → ส่งเข้า LLM พร้อม context Avoid: Semantic search (overloaded), Vector search (incomplete)

OCR Service: Container สำเร็จรูป (opaque black box) เปิด HTTP API ให้ NestJS เรียก — ไม่มีโค้ด Python ใน repo, ทีมไม่ maintain runtime ภายใน Avoid: Python sidecar, OCR microservice (ที่เรา maintain เอง)

Prompt Version: Immutable snapshot ของ prompt template ใน ai_prompts table — ทุกครั้งที่ admin กด "บันทึก" จะสร้าง version ใหม่ (versionnumber เพิ่มทีละ 1) version เก่ายังอยู่ใน history ลบได้ยกเว้น active version (ADR-029) _Avoid: Prompt config, Prompt setting, Editable prompt

Active Prompt: Prompt Version ที่มี is_active = 1 ต่อ prompt_type — ใช้โดยทั้ง OCR Sandbox และ processMigrateDocument พร้อมกัน, cached ใน Redis TTL 60s; invalidated เมื่อ admin activate version อื่น (ADR-029) Avoid: Production prompt (ทั้ง sandbox และ migrate-document ใช้อันเดียวกัน)

Prompt Template: String ที่มี {{ocr_text}} placeholder บังคับ — backend validate ก่อน save; processor แทนที่ด้วย OCR output ก่อนส่งเข้า Ollama (ADR-029) Avoid: Prompt string, Prompt text (ambiguous — อาจหมายถึง resolved prompt ที่มี OCR text แล้ว)

Human-in-the-loop: ทุก AI suggestion ต้องผ่านการ accept/reject โดย user ก่อนกลายเป็น state change — บันทึกใน ai_audit_logs Avoid: Auto-apply, AI auto-execute

AI Tool Layer: Bridge layer ระหว่าง AI Gateway กับ business modules — dispatch โดย AI Gateway หลังได้ Server-side Intent, enforce CASL ภายใน tool เอง (ADR-025) Avoid: LLM function calling, Tool plugin, LangChain tool

Tool Registry: Static map ใน AiToolRegistryService ที่ map ServerIntent → tool handler — Intent ที่ไม่มีใน registry route ไป RAG หรือ FALLBACK Avoid: Dynamic plugin registry, Runtime-loaded tools

ToolResult DTO: LLM-friendly response object จาก tool — มีเฉพาะ publicId + business codes, ไม่มี INT id (ADR-019), ไม่มี TypeORM relations Avoid: Raw entity, Full entity response

ToolCallResult: Result wrapper ที่ tool คืนให้ Gateway: { ok: true, data } หรือ { ok: false, reason, message } — ไม่ throw exception Avoid: Throw exception from tool, Untyped error

Relationships

  • A Correspondence has a 1:1 specialization to RFA / Transmittal / etc. (table inheritance)
  • A RFA has 1:N RFA Revisions, each linking to one or more Shop Drawing Revisions via rfa_items
  • A Workflow Instance governs exactly one Correspondence; its current state is projected into entity columns (e.g. rfa_revisions.rfa_status_code_id) but workflow_instances is the source of truth
  • A Prompt Version lives in ai_prompts; exactly one per prompt_type has is_active = 1 — this is the Active Prompt consumed by both OCR Sandbox and processMigrateDocument; cached in Redis TTL 60s
  • A Document Chunk (MariaDB) has a 1:1 Vector Point in Qdrant via shared chunk_public_id (UUIDv7)
  • An AI Document Assistant suggestion produces an ai_audit_logs row; if user accepts, it triggers a normal Workflow Transition (AI never writes the transition itself)
  • Qdrant queries MUST be filtered by project_public_id — enforced at compile time by QdrantService signature
  • An Intent Classifier receives user query → returns Server-side Intent + confidence; Pattern Layer (DB table) checked first, LLM Fallback (Ollama sync) used only when pattern miss
  • An Intent Definition (ai_intent_definitions) has 1:N Intent Patterns (ai_intent_patterns); Admin จัดการได้ runtime
  • AI Gateway dispatches to AI Tool Layer directly (server-side) after receiving Intent — LLM never calls tools itself; Tool Registry maps Intent → handler; each handler returns ToolCallResult wrapper
  • A ToolResult DTO contains only publicId + business codes — injected into LLM prompt as JSON context (v1, max 500 tokens); hybrid RAG+Tool deferred to Phase 4

AI authority scope (resolved)

Scope Allowed? Mechanism
Read-only insight (summarise, explain) AI Gateway → service → CASL-guarded query
Suggest action (UI shows button) Response shape { suggestedAction, confidence, reasoning }
Auto-trigger side-effects (notify, alert, comment) BullMQ job (ADR-008); MUST NOT change workflow state
Auto-execute workflow transition Forbidden Tier 1 — every transition needs human actor_user_id

Upload pipeline (resolved)

Stage Mode Queue Notes
1. Upload → temp + return tempUploadId Sync <1s
2. ClamAV scan + MIME whitelist Sync block ก่อน commit (ADR-016)
3. User commit (metadata + ย้าย permanent) Sync สร้าง documents row, ใช้ Idempotency-Key
4. Classification/Tagging (3 pages แรก) Async ai-realtime suggest metadata; user accept/reject (human-in-the-loop)
5. RAG Embedding (full doc; OCR ถ้า text-layer < 100 chars/page) Async ai-batch trigger AUTO หลัง commit, parallel กับ stage 4
6. Qdrant upsert + ai_document_chunks.embedded_at = NOW() Async (worker) gap = DB full-text fallback

กฎ:

  • ห้าม OCR/embed ใน HTTP request handler
  • BullMQ jobId = chunk_public_id (UUIDv7) กัน duplicate
  • Embed fail → graceful degrade (เอกสารยังใช้งานได้, AI feature ลด)
  • Revision ใหม่ → chunks เก่า mark superseded_at, ไม่ลบ vector
  • Frontend ใช้ AiStatusBanner แสดง progress

Example dialogue

Dev: "AI สรุป RFA revision นี้ให้หน่อย แล้วเปลี่ยน status เป็น approved เลย" Domain expert: "ไม่ได้ — AI สรุปได้ (read-only insight) และเสนอ 'ควร approve เพราะ…' ได้ (suggest action) แต่การเปลี่ยน state ต้องผ่าน user กดปุ่มเอง ระบบจะเรียก WorkflowService.transition() ซึ่งบันทึก actor_user_id เป็นมนุษย์ใน workflow_histories"

Dev: "งั้น Tool Layer ใน plan เก่าที่ให้ LLM เรียก get_rfa(id) ใช้ได้ไหม" Domain expert: "ไม่ใช่ tool ของ LLM — เป็น Server-side Intent ที่ AI Gateway แปลงเป็น service call ภายใต้ CASL + projectPublicId scope LLM แค่รับ context ที่ pre-fetched มาแล้ว"

Identifier rules (ADR-019, AI subsystem)

Boundary Identifier ที่ใช้
API (FE ↔ AI Gateway) publicId (UUIDv7 string) เท่านั้น; INT id มี @Exclude()
Server-side Intent payload *PublicId strings; service แปลงเป็น INT FK ภายใน
LLM context (prompt) publicId + business code (rfa_number, drawing_code) ห้ามเห็น INT
Qdrant payload project_public_id, document_public_id, chunk_public_id
ai_document_chunks internals INT FK ใช้ได้ภายใน DB; identity ที่ expose = chunk_public_id BINARY(16)
Business codes (e.g. drawing_code = "A-101") รับเป็น input ได้ แต่ resolve → publicId ก่อน query

Forbidden (Tier 1 CI blocker):

  • parseInt(<*PublicId>), Number(<*PublicId>), +<*PublicId>
  • publicId ?? id ?? '' fallback chain
  • DTO ที่มีทั้ง { id, uuid, publicId }

AI integration architecture (resolved)

มีแล้ว (Infrastructure):

  • AI Gateway — NestJS module, CASL-guarded, enqueue jobs ไป BullMQ
  • n8n — Workflow orchestrator บน QNAP (Migration Phase + simple routing)
  • Ollama — Local LLM inference บน Admin Desktop (gemma4:e4b Q8_0 + nomic-embed-text)
  • QdrantService — Vector search แบบ project-isolated
  • AiRagService — RAG pipeline (embed query → Qdrant → LLM context)

ยังขาด (Runtime Layer):

  • Intent Router — แปลงคำถามธรรมชาติ → Server-side Intent enum (เช่น RAG_QUERY, GET_RFA, GET_DRAWING_REVISIONS)
  • AI Tool Layer — Bridge functions ที่เรียก business modules (RFA, Drawing, Transmittal) ภายใต้ CASL scope
  • Document Chat UI — Side-panel component สำหรับคุยกับ AI ใน context ของเอกสาร

ความสัมพันธ์:

User Chat → Intent Router (ยังไม่มี) → Server-side Intent → AI Gateway → CASL Check → ├─→ BullMQ → n8n → Ollama → Response └─→ Tool Layer (ยังไม่มี) → Business Service → Response

System readiness summary (resolved)

Component สถานะ หมายเหตุ
Infrastructure พร้อม NestJS + Next.js + MariaDB + Redis + Elasticsearch
Workflow Engine พร้อม DSL-based, ADR-001/021
AI Boundary พร้อม ADR-023A — Ollama isolation, no direct DB access
RAG Pipeline 🟡 บางส่วน Qdrant service มีใน code, ต้องตรวจสอบ deployment
Intent Router 🟡 ADR Accepted ADR-024 Accepted — Intent Classifier (Pattern→LLM Fallback) รอ implementation
AI Tool Layer 🟡 ADR Accepted ADR-025 Accepted — Tool Layer Architecture รอ implementation
Document Chat UI 🟡 ADR Accepted ADR-026 Accepted — Side-panel Chat UI รอ implementation
AI Admin Console 🟡 ADR Accepted ADR-027 Accepted — Dynamic Control Panel รอ implementation
Dynamic Prompt Mgmt พร้อม ADR-029 Active — พัฒนาเสร็จสมบูรณ์ทั้ง Entity, API, Sandbox Runner, Cache และ UI Playgrounds

Flagged ambiguities

  • "approval logic" ในเอกสารเก่าใช้คาบเกี่ยวระหว่าง rfa_approve_codes (business outcome เช่น 1A/1B) กับ workflow_definitions (state transition rules) — resolved: เป็นคนละสิ่ง
  • "ai_embeddings" vs "ai_document_chunks" — resolved: ใช้ ai_document_chunks (metadata + text) + Qdrant (vector only); ห้ามเก็บ vector ใน MariaDB
  • "Tool Layer" ในเอกสาร AI — resolved: ไม่ใช่ LLM-callable tools, เป็น Server-side Intents ที่ NestJS controlใน AI Gateway
  • "AI = Document Controller" — resolved: ใช้ AI Document Assistant (Suggest + Insight) แทน เพื่อกัน scope creep ไปทาง autonomous agent
  • OpenRAG vs ADR-023A — resolved: ADR-023A เป็น canonical source — ใช้ Qdrant + nomic-embed-text สำหรับ vector search; Elasticsearch ใช้สำหรับ keyword/full-text เท่านั้น; specs/03-Data-and-Storage/03-07-OpenRAG.md เป็นเอกสาร reference แต่ไม่ใช่ active spec
  • ".agents/ กับ Production AI" — resolved: .agents/ คือ Dev AI toolkit (ช่วยเขียนโค้ด); Production AI คือ AI Gateway + n8n + Ollama — เป็นคนละ layer กัน

Roadmap: AI Runtime Layer (pending ADRs)

สร้างตามลำดับ dependency:

Phase 1 — Intent Router (2-3 สัปดาห์)

เป้าหมาย: แปลงคำถามธรรมชาติ → Server-side Intent enum

ขั้นตอน:

  1. สร้าง IntentClassifier service — ใช้ Ollama หรือ simple pattern matching เป็น v1
  2. กำหนด ServerIntent enum: RAG_QUERY, GET_RFA, GET_DRAWING, GET_TRANSMITTAL, SUMMARIZE_DOCUMENT
  3. เพิ่ม endpoint POST /ai/intent ที่รับ { query: string, context?: { type, publicId } } → คืน { intent, confidence, params }
  4. ทดสอบ: "RFA ล่าสุดของโครงการนี้คืออะไร" → GET_RFA with { sort: 'latest', limit: 1 }

ขึ้นกับ: ไม่มี (ใช้ Ollama ที่มีอยู่)


Phase 2 — AI Tool Layer (3-4 สัปดาห์)

เป้าหมาย: Bridge functions ที่เรียก business modules ภายใต้ CASL scope

ขั้นตอน:

  1. สร้าง AiToolService — registry สำหรับ tool functions
  2. สร้าง tool wrappers:
    • getRfa(params: { publicId?; rfaNumber?; contractPublicId?; status? })
    • getDrawing(params: { publicId?; drawingCode?; contractPublicId?; revision? })
    • getTransmittal(params: { publicId?; transmittalNumber?; purpose? })
    • getRfaDrawings(rfaPublicId: string) — ดึง drawings ที่ผูกกับ RFA
  3. ใส่ CASL guard ทุก tool — ตรวจสอบ projectPublicId scope
  4. เพิ่ม response formatter — แปลง entity → LLM-friendly context (publicId + business codes เท่านั้น)
  5. ทดสอบ: Intent Router → Tool Layer → RfaService → Response

ขึ้นกับ: Phase 1 (Intent Router ต้องรู้ว่าเรียก tool ไหน)


Phase 3 — Document Chat UI (2 สัปดาห์)

เป้าหมาย: Side-panel component สำหรับคุยกับ AI ใน context เอกสาร

ขั้นตอน:

  1. สร้าง AiChatPanel component — รับ context: { type: 'drawing'|'rfa'|'transmittal', publicId: string }
  2. เพิ่ม chat interface: user message + AI response + suggested actions
  3. สร้าง useAiChat() hook — TanStack Query, streaming response (optional)
  4. ฝังใน pages:
    • /drawings/[publicId] — context เป็น drawing นั้น
    • /rfas/[publicId] — context เป็น RFA นั้น
    • /transmittals/[publicId] — context เป็น transmittal นั้น
  5. ทดสอบ: เปิด drawing → ถาม "RFA ที่เกี่ยวข้องกับ drawing นี้คืออะไร" → AI ตอบถูก

ขึ้นกับ: Phase 1 + 2 (ต้องมี Intent Router + Tool Layer ก่อน)


Phase 4 — Integration & Polish (2 สัปดาห์)

ขั้นตอน:

  1. เพิ่ม RAG context ผสมกับ Tool results (hybrid response)
  2. เพิ่ม suggested actions ที่มาจาก AI ("ควรสร้าง RFA ใหม่ไหม?")
  3. ทดสอบ end-to-end ทุก flow
  4. ปรับ threshold / confidence scores ตามผลทดสอบ

ADRs ที่เกี่ยวข้องกับ AI Runtime Layer

ADR หัวข้อ ตัดสินใจอะไร สถานะ
ADR-024 Intent Classification Strategy Hybrid: Pattern First → LLM Fallback Accepted
ADR-025 AI Tool Layer Architecture Bridge pattern, CASL enforcement, response shape Accepted
ADR-026 Document Chat UI Pattern Side-panel vs modal vs separate page Accepted
ADR-027 AI Admin Console & Dynamic Control Admin Panel + dynamic model/prompt/intent control Accepted
ADR-028 Migration Architecture Refactor Staging Queue & post-migration cleanup Active
ADR-029 Dynamic Prompt Management ai_prompts table, versioned OCR extraction prompt shared by sandbox + migrate-document Active

หมายเหตุ: ADR-023A ยังคงเป็น canonical สำหรับ infrastructure — ADR-024/025/026/027 เพิ่ม runtime layer; ADR-028 ปรับ Migration Pipeline

สิ่งที่ควรทำในอนาคต (Future Maintenance & Security Tasks)

  • Axios Dependency: เสนอให้อัปเดตเวอร์ชันของ axios ใน package.json เป็น >=1.16.0 เพื่อแก้ไขช่องโหว่ Prototype Pollution (High Severity - CVE-2026-44494) เพื่อป้องกันช่องทางความเสี่ยงในการถูกโจมตีผ่าน Prototype Pollution Gadget ใน config.proxy ของ Axios API client