10 KiB
LCBP3 / NAP-DMS Context
ระบบจัดการเอกสารงานก่อสร้าง (DMS) สำหรับโครงการ LCBP3 — เน้นการควบคุม Correspondence, RFA, Transmittal, Drawing พร้อมผู้ช่วย AI แบบ on-premises ที่ทำงานภายใต้ Workflow Engine กลางและขอบเขต AI ที่เข้มงวด (ADR-023A)
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
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
Server-side Intent:
Enum ของคำขอที่ AI Gateway รองรับ (เช่น RAG_QUERY, CLASSIFY_DOCUMENT, EXTRACT_METADATA) — แทนที่ LLM function-calling
Avoid: Tool, LLM tool, LangChain tool
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 เอง)
Human-in-the-loop:
ทุก AI suggestion ต้องผ่านการ accept/reject โดย user ก่อนกลายเป็น state change — บันทึกใน ai_audit_logs
Avoid: Auto-apply, AI auto-execute
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) butworkflow_instancesis the source of truth - 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_logsrow; 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 byQdrantServicesignature
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 +projectPublicIdscope 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 }
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 —
specs/03-Data-and-Storage/03-07-OpenRAG.mdระบุ Elasticsearch + dense_vector ซึ่งขัดกับ ADR-023A (Qdrant + nomic-embed-text) — ยังไม่ resolve, ต้องตัดสินใจในรอบถัดไป