690519:1631 224 to 226 AI #01
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
# ADR-024: Intent Classification Strategy
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-19
|
||||
**Decision Makers:** Development Team, System Architect, AI Integration Lead
|
||||
**Related Documents:**
|
||||
- [ADR-023A: Unified AI Architecture — Model Revision](./ADR-023A-unified-ai-architecture.md)
|
||||
- [ADR-019: Hybrid Identifier Strategy](./ADR-019-hybrid-identifier-strategy.md)
|
||||
- [ADR-016: Security & Authentication](./ADR-016-security-authentication.md)
|
||||
- [CONTEXT.md](../../CONTEXT.md)
|
||||
|
||||
> **หมายเหตุ:** ADR นี้กำหนดกลยุทธ์การจำแนก Intent สำหรับ AI Runtime Layer — เป็น layer เพิ่มเติมจาก ADR-023A (Infrastructure) โดยทำหน้าที่แปลงคำถามธรรมชาติ (ไทย/อังกฤษปน) เป็น Server-side Intent enum ก่อน route ไปยัง AI Tool Layer
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
ระบบ AI Gateway (ADR-023A) รองรับ job types เช่น `ai-suggest`, `rag-query`, `ocr` ผ่าน BullMQ แล้ว แต่ยังไม่มีกลไกแปลงคำถามธรรมชาติจาก user → Server-side Intent ที่ระบบเข้าใจ
|
||||
|
||||
ความท้าทาย:
|
||||
1. **Bilingual input** — user พิมพ์ภาษาไทย/อังกฤษปนกันอย่างอิสระ
|
||||
2. **GPU budget จำกัด** — RTX 2060 Super 8GB ใช้ร่วมกับ RAG, OCR, Embedding
|
||||
3. **Latency** — Intent classification เป็น prerequisite ก่อน route → ต้องเร็ว
|
||||
4. **Extensibility** — Intent เพิ่มทุก quarter ต้องไม่ต้อง deploy code ใหม่ทุกครั้ง
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Low latency for common queries** — 70-80% ของ queries เป็น pattern ที่ชัดเจน
|
||||
- **Bilingual tolerance** — ภาษาไทย+อังกฤษปน, typo ต้อง handle ได้
|
||||
- **GPU conservation** — ลด LLM calls ให้น้อยที่สุด เพราะ VRAM ใช้ร่วมกับงาน AI อื่น
|
||||
- **Runtime configurability** — Admin จัดการ pattern ได้โดยไม่ต้อง deploy
|
||||
- **Graceful degradation** — ถ้า LLM ไม่ว่าง/ล่ม ระบบยังตอบ user ได้
|
||||
|
||||
---
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Option A: Pure Pattern Matching (Keyword + Regex)
|
||||
|
||||
**Pros:**
|
||||
- Deterministic, latency < 5ms, ไม่ใช้ GPU
|
||||
- Testable 100%
|
||||
|
||||
**Cons:**
|
||||
- ❌ ภาษาไทย+อังกฤษปน → regex ซับซ้อนมาก
|
||||
- ❌ Typo = miss ทุกครั้ง
|
||||
- ❌ ต้อง maintain rule set ที่โตขึ้นทุก quarter
|
||||
|
||||
### Option B: Pure LLM-based (Ollama Classify ทุก request)
|
||||
|
||||
**Pros:**
|
||||
- เข้าใจ bilingual, typo-tolerant, ขยาย intent ง่าย (แก้ system prompt)
|
||||
|
||||
**Cons:**
|
||||
- ❌ Latency 500ms–2s ทุก request
|
||||
- ❌ GPU load ทุก chat message → แย่ง resource กับ RAG/OCR
|
||||
- ❌ Non-deterministic → ต้อง validate ทุกครั้ง
|
||||
|
||||
### Option C: Hybrid — Pattern First, LLM Fallback ✅ (เลือก)
|
||||
|
||||
**Pros:**
|
||||
- Common queries (70-80%) จับได้ที่ pattern layer < 10ms
|
||||
- Bilingual + typo handle ได้ผ่าน LLM fallback
|
||||
- GPU load ลดลง 70-80% เทียบกับ Pure LLM
|
||||
- Pattern เก็บใน DB → Admin แก้ได้ runtime
|
||||
|
||||
**Cons:**
|
||||
- Maintain 2 layers (แต่ pattern layer เป็น DB records, ไม่ใช่ code)
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
**เลือก Option C: Hybrid (Pattern First → LLM Fallback)**
|
||||
|
||||
---
|
||||
|
||||
## Classification Flow
|
||||
|
||||
```
|
||||
User Query
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ 1. Load patterns from Redis │ (cache TTL 5 min)
|
||||
│ (fallback: query DB) │
|
||||
└─────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ 2. Pattern Match Loop │ priority ASC
|
||||
│ - keyword: includes() │
|
||||
│ - regex: RegExp.test() │
|
||||
└─────────────┬───────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ Match? │
|
||||
▼ ▼
|
||||
┌─────┐ ┌──────────────────────────────┐
|
||||
│ YES │ │ 3. LLM Fallback (Ollama) │
|
||||
│ │ │ - Synchronous call │
|
||||
│ │ │ - Semaphore max=3 │
|
||||
│ │ │ - Dynamic system prompt │
|
||||
└──┬──┘ └──────────────┬───────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────────────────┐
|
||||
│ confidence │ │ Confidence Threshold │
|
||||
│ = 1.0 │ │ ≥ 0.7 → use │
|
||||
│ │ │ 0.4–0.69 → use + log │
|
||||
│ │ │ < 0.4 → FALLBACK │
|
||||
└──────┬──────┘ └────────────┬────────────┘
|
||||
│ │
|
||||
└────────────┬────────────┘
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Return Intent + │
|
||||
│ confidence + params│
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v1 Intent Enum (12 intents)
|
||||
|
||||
### Read-only (ดึงข้อมูล)
|
||||
|
||||
| Intent Code | คำอธิบาย | ตัวอย่าง Query |
|
||||
|-------------|----------|---------------|
|
||||
| `RAG_QUERY` | ถามคำถามธรรมชาติ ตอบจาก vector + doc context | "สรุปเนื้อหา RFA-0042 ให้หน่อย" |
|
||||
| `GET_RFA` | ดึง RFA ตาม filter | "RFA ล่าสุดของ contract A" |
|
||||
| `GET_DRAWING` | ดึง Drawing revision | "drawing A-101 rev ล่าสุด" |
|
||||
| `GET_TRANSMITTAL` | ดึง Transmittal | "transmittal เลขที่ TR-0015" |
|
||||
| `GET_CORRESPONDENCE` | ดึง Correspondence ทั่วไป | "จดหมาย NAP-OUT-0233" |
|
||||
| `GET_CIRCULATION` | ดึง Circulation | "circulation ที่ส่งให้ฉัน" |
|
||||
| `GET_RFA_DRAWINGS` | ดึง Drawings ที่ผูกกับ RFA | "drawings ใน RFA-0042" |
|
||||
| `SUMMARIZE_DOCUMENT` | สรุปเอกสารที่เปิดอยู่ | "สรุปเอกสารนี้" |
|
||||
| `LIST_OVERDUE` | รายการ cross-entity ที่เกินกำหนด | "อะไรเกินกำหนดบ้าง" |
|
||||
|
||||
### Suggest (แจ้งเตือน)
|
||||
|
||||
| Intent Code | คำอธิบาย | ตัวอย่าง Query |
|
||||
|-------------|----------|---------------|
|
||||
| `SUGGEST_METADATA` | แนะนำ metadata สำหรับเอกสารที่อัปโหลด | "ช่วยแนะนำ metadata" |
|
||||
| `SUGGEST_ACTION` | แจ้งเตือนว่าควรทำอะไรต่อ (notification-grade) | "มีอะไรที่ควรทำบ้าง" |
|
||||
|
||||
### Utility
|
||||
|
||||
| Intent Code | คำอธิบาย |
|
||||
|-------------|----------|
|
||||
| `FALLBACK` | ไม่เข้า intent ไหน / ไม่เกี่ยวกับระบบ → ตอบว่าไม่เข้าใจ + แนะนำตัวอย่าง |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### `ai_intent_definitions`
|
||||
|
||||
```sql
|
||||
CREATE TABLE ai_intent_definitions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
public_id UUID NOT NULL DEFAULT UUID(),
|
||||
intent_code VARCHAR(50) NOT NULL UNIQUE,
|
||||
description_th VARCHAR(255) NOT NULL,
|
||||
description_en VARCHAR(255) NOT NULL,
|
||||
category ENUM('read','suggest','utility') NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
### `ai_intent_patterns`
|
||||
|
||||
```sql
|
||||
CREATE TABLE ai_intent_patterns (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
public_id UUID NOT NULL DEFAULT UUID(),
|
||||
intent_code VARCHAR(50) NOT NULL,
|
||||
language ENUM('th','en','any') NOT NULL DEFAULT 'any',
|
||||
pattern_type ENUM('keyword','regex') NOT NULL DEFAULT 'keyword',
|
||||
pattern_value VARCHAR(255) NOT NULL,
|
||||
priority INT NOT NULL DEFAULT 100,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_intent_active (is_active, priority),
|
||||
INDEX idx_intent_code (intent_code),
|
||||
CONSTRAINT fk_intent_pattern_definition
|
||||
FOREIGN KEY (intent_code) REFERENCES ai_intent_definitions(intent_code)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LLM Fallback Specification
|
||||
|
||||
### System Prompt (Dynamic)
|
||||
|
||||
```
|
||||
คุณเป็นตัวจำแนกคำสั่ง (Intent Classifier) สำหรับระบบจัดการเอกสารก่อสร้าง
|
||||
จงวิเคราะห์คำถามของผู้ใช้ แล้วตอบเป็น JSON เท่านั้น:
|
||||
{"intent":"<INTENT_CODE>","confidence":<0.0-1.0>}
|
||||
|
||||
Intent ที่รองรับ:
|
||||
{{DYNAMIC_INTENT_LIST_FROM_DB}}
|
||||
|
||||
กฎ:
|
||||
- ตอบ JSON บรรทัดเดียว ห้ามมีข้อความอื่น
|
||||
- ถ้าไม่มั่นใจ ให้ confidence ต่ำ
|
||||
- ถ้าไม่เกี่ยวกับระบบเอกสาร ให้ intent=FALLBACK
|
||||
```
|
||||
|
||||
`{{DYNAMIC_INTENT_LIST_FROM_DB}}` สร้างจาก `SELECT intent_code, description_th FROM ai_intent_definitions WHERE is_active = TRUE`
|
||||
|
||||
### Confidence Thresholds
|
||||
|
||||
| Range | Action |
|
||||
|-------|--------|
|
||||
| ≥ 0.7 | ใช้ intent ที่ LLM ตอบ |
|
||||
| 0.4–0.69 | ใช้ intent + log warning ใน `ai_audit_logs` |
|
||||
| < 0.4 | Override เป็น `FALLBACK` |
|
||||
|
||||
### Recalibration
|
||||
|
||||
หลังรวบรวม 100-500 queries ใน `ai_audit_logs` → วิเคราะห์:
|
||||
- Intent ไหนที่ LLM classify ถูก/ผิดบ่อย
|
||||
- Threshold ควรปรับขึ้น/ลง
|
||||
- Pattern ไหนควรเพิ่มเพื่อลด LLM calls
|
||||
|
||||
---
|
||||
|
||||
## Performance Budget
|
||||
|
||||
| Step | Target Latency | Notes |
|
||||
|------|---------------|-------|
|
||||
| Pattern match (cache hit) | < 10ms | regex loop over cached patterns |
|
||||
| Pattern match (cache miss → DB) | < 50ms | query + cache write |
|
||||
| LLM fallback (Ollama) | < 2000ms | synchronous, gemma4:e4b Q8_0, prompt ~200 tokens |
|
||||
| **Total worst case** | < 2100ms | pattern miss + LLM |
|
||||
| **Total best case** | < 10ms | pattern hit |
|
||||
|
||||
---
|
||||
|
||||
## Concurrency Protection
|
||||
|
||||
- **Semaphore**: max 3 concurrent LLM classify calls
|
||||
- **Overflow behavior**: เกิน semaphore → return `FALLBACK` intent + confidence 0 + log warning
|
||||
- **เหตุผล**: prompt สั้น (~200 tokens) จึงใช้ 3 concurrent ได้บน RTX 2060 Super 8GB โดยไม่กระทบ RAG/OCR ที่ใช้ `ai-batch` queue
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
- **Key**: `ai:intent:patterns:active`
|
||||
- **Format**: JSON array ของ patterns sorted by priority
|
||||
- **TTL**: 300 seconds (5 นาที)
|
||||
- **Invalidation**: TTL-based เท่านั้น (v1) — Admin แก้ pattern แล้วรอไม่เกิน 5 นาที
|
||||
- **Cache miss**: query `ai_intent_patterns WHERE is_active = TRUE ORDER BY priority ASC` → write cache
|
||||
|
||||
---
|
||||
|
||||
## Admin UI (v1 Scope)
|
||||
|
||||
Admin page สำหรับจัดการ Intent Classification:
|
||||
|
||||
1. **Intent Definitions** — CRUD intent codes + descriptions
|
||||
2. **Intent Patterns** — CRUD patterns per intent (keyword/regex, language, priority)
|
||||
3. **Test Console** — input query → แสดงผล classification result (pattern hit / LLM fallback + confidence)
|
||||
4. **Analytics** — แสดง hit rate (pattern vs LLM), confidence distribution จาก `ai_audit_logs`
|
||||
|
||||
---
|
||||
|
||||
## Audit & Observability
|
||||
|
||||
ทุก classification request บันทึกใน `ai_audit_logs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "intent_classification",
|
||||
"input": "<user query>",
|
||||
"output": { "intent": "GET_RFA", "confidence": 0.85 },
|
||||
"method": "pattern" | "llm_fallback" | "semaphore_overflow",
|
||||
"latencyMs": 8,
|
||||
"projectPublicId": "...",
|
||||
"userPublicId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- 70-80% ของ queries ตอบได้ < 10ms โดยไม่ใช้ GPU
|
||||
- ขยาย intent ได้ runtime ผ่าน Admin UI (ไม่ต้อง deploy)
|
||||
- Bilingual + typo tolerance ผ่าน LLM fallback
|
||||
- Audit trail ครบทุก classification → recalibrate ได้
|
||||
|
||||
### Negative
|
||||
- 2 layers to maintain (DB patterns + LLM prompt) — แต่ทั้งคู่ configurable ไม่ใช่ hardcode
|
||||
- LLM fallback ไม่ deterministic → ต้องมี threshold + audit
|
||||
- Admin UI เพิ่ม scope ใน v1
|
||||
|
||||
### Risks
|
||||
- gemma4:e4b Q8_0 classify ผิดสำหรับ query ที่กำกวม → mitigate ด้วย threshold + FALLBACK + recalibration
|
||||
- Pattern ที่กว้างเกินไป (เช่น keyword "เอกสาร" match ทุก intent) → mitigate ด้วย priority ordering + regex specificity
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (ADR-009)
|
||||
|
||||
- เพิ่มตาราง `ai_intent_definitions` และ `ai_intent_patterns` ผ่าน SQL delta file
|
||||
- Seed ข้อมูล 12 intents + initial patterns
|
||||
- ไม่ใช้ TypeORM migration
|
||||
@@ -0,0 +1,329 @@
|
||||
# ADR-025: AI Tool Layer Architecture
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-19
|
||||
**Decision Makers:** Development Team, System Architect, AI Integration Lead
|
||||
**Related Documents:**
|
||||
- [ADR-024: Intent Classification Strategy](./ADR-024-intent-classification-strategy.md)
|
||||
- [ADR-023A: Unified AI Architecture — Model Revision](./ADR-023A-unified-ai-architecture.md)
|
||||
- [ADR-019: Hybrid Identifier Strategy](./ADR-019-hybrid-identifier-strategy.md)
|
||||
- [ADR-007: Error Handling Strategy](./ADR-007-error-handling-strategy.md)
|
||||
- [ADR-016: Security & Authentication](./ADR-016-security-authentication.md)
|
||||
- [CONTEXT.md](../../CONTEXT.md)
|
||||
|
||||
> **หมายเหตุ:** ADR นี้กำหนดสถาปัตยกรรมของ AI Tool Layer — bridge ระหว่าง AI Gateway (ADR-023A) กับ business modules (RFA, Drawing, Transmittal ฯลฯ) หลังจาก Intent Classifier (ADR-024) คืน Server-side Intent แล้ว
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
เมื่อ Intent Classifier (ADR-024) คืน Server-side Intent เช่น `GET_RFA`, `GET_DRAWING` แล้ว ระบบต้องการ layer ที่:
|
||||
|
||||
1. **Map Intent → business service call** ภายใต้ CASL authorization
|
||||
2. **คืน response ที่ LLM ใช้ได้** โดยไม่ expose INT primary key (ADR-019)
|
||||
3. **Handle error อย่างมีโครงสร้าง** เพื่อ AI Gateway ตัดสินใจ graceful degrade ได้
|
||||
4. **Extensible** — เพิ่ม tool ใหม่โดยไม่กระทบ Gateway logic
|
||||
|
||||
ความท้าทาย:
|
||||
- Business services มี CASL guard ที่ controller layer — Tool Layer ต้อง enforce permission เองโดยไม่ bypass
|
||||
- LLM prompt token budget จำกัด — ห้ามส่ง raw entity ที่มี field ไม่จำเป็น
|
||||
- Tool call อาจ fail ด้วยเหตุผลต่างกัน (permission, not found, service error) — Gateway ต้องรู้ reason เพื่อ route ถูก
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **CASL enforcement ต้องครบทุก tool call** — ห้ามมี unguarded path เข้า business data
|
||||
- **ADR-019 compliance** — ห้าม expose INT `id` ใน LLM context
|
||||
- **Predictable error handling** — ADR-007 layered classification
|
||||
- **Type safety** — TypeScript strict, ไม่มี `any`
|
||||
- **Testability** — แต่ละ tool test แยกได้โดยไม่ต้องมี full Gateway
|
||||
|
||||
---
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Option 1: LLM Function Calling (LLM เรียก tool เอง)
|
||||
|
||||
ให้ LLM ตัดสินใจว่าจะเรียก tool ไหน ผ่าน function calling protocol
|
||||
|
||||
**Cons:**
|
||||
- ❌ CASL enforcement เกิดที่ LLM runtime — ไม่ controllable
|
||||
- ❌ gemma4:e4b Q8_0 ไม่รองรับ function calling อย่างน่าเชื่อถือ
|
||||
- ❌ Non-deterministic — LLM อาจเรียกผิด tool
|
||||
|
||||
### Option 2: AI Gateway เรียก Tool Layer ตรง (Server-side dispatch) ✅ (เลือก)
|
||||
|
||||
Intent Classifier คืน Intent → AI Gateway map กับ static registry → เรียก tool โดยตรง → inject result ใน LLM prompt
|
||||
|
||||
**Pros:**
|
||||
- ✅ CASL check อยู่ใน server code ทั้งหมด
|
||||
- ✅ Deterministic — Intent enum map กับ tool function แบบ 1:1
|
||||
- ✅ Testable, type-safe
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
**เลือก Option 2: Server-side dispatch ผ่าน Static Tool Registry**
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
User Query
|
||||
│
|
||||
▼
|
||||
Intent Classifier (ADR-024)
|
||||
│ returns { intent, confidence, params }
|
||||
▼
|
||||
AI Gateway
|
||||
│
|
||||
├─ lookup AiToolRegistryService.getHandler(intent)
|
||||
│
|
||||
▼
|
||||
AiToolRegistryService (Static Map)
|
||||
│ TOOL_REGISTRY[intent] → tool function
|
||||
▼
|
||||
Tool Function (e.g. RfaToolService.getRfa)
|
||||
│ receives (params, requestUser: RequestUser)
|
||||
│ enforce CASL internally
|
||||
│ call business service
|
||||
│ map entity → *ToolResult DTO
|
||||
▼
|
||||
ToolCallResult<T>
|
||||
{ ok: true, data: T }
|
||||
| { ok: false, reason, message }
|
||||
│
|
||||
▼
|
||||
AI Gateway
|
||||
│ ok=true → inject data ใน LLM prompt → Ollama → response
|
||||
│ ok=false → handle by reason (log, fallback, error message)
|
||||
▼
|
||||
User Response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Registry
|
||||
|
||||
### Static Map
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/tool/ai-tool-registry.service.ts
|
||||
|
||||
type ToolHandler = (
|
||||
params: Record<string, unknown>,
|
||||
user: RequestUser,
|
||||
) => Promise<ToolCallResult<unknown>>;
|
||||
|
||||
const TOOL_REGISTRY: Partial<Record<ServerIntent, ToolHandler>> = {
|
||||
[ServerIntent.GET_RFA]: (p, u) => rfaToolService.getRfa(p, u),
|
||||
[ServerIntent.GET_DRAWING]: (p, u) => drawingToolService.getDrawing(p, u),
|
||||
[ServerIntent.GET_TRANSMITTAL]: (p, u) => transmittalToolService.getTransmittal(p, u),
|
||||
[ServerIntent.GET_CORRESPONDENCE]:(p, u) => correspondenceToolService.getCorrespondence(p, u),
|
||||
[ServerIntent.GET_CIRCULATION]: (p, u) => circulationToolService.getCirculation(p, u),
|
||||
[ServerIntent.GET_RFA_DRAWINGS]: (p, u) => rfaToolService.getRfaDrawings(p, u),
|
||||
[ServerIntent.SUMMARIZE_DOCUMENT]:(p, u) => documentToolService.summarize(p, u),
|
||||
[ServerIntent.LIST_OVERDUE]: (p, u) => documentToolService.listOverdue(p, u),
|
||||
};
|
||||
```
|
||||
|
||||
Intent ที่ไม่มีใน registry (เช่น `RAG_QUERY`, `SUGGEST_*`, `FALLBACK`) → AI Gateway route ไปยัง pipeline อื่น (RAG หรือ error) โดยไม่ผ่าน Tool Layer
|
||||
|
||||
---
|
||||
|
||||
## CASL Enforcement Pattern
|
||||
|
||||
แต่ละ tool รับ `RequestUser` และ check permission ก่อน query:
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/tool/rfa-tool.service.ts
|
||||
|
||||
async getRfa(
|
||||
params: GetRfaToolParams,
|
||||
user: RequestUser,
|
||||
): Promise<ToolCallResult<RfaToolResult[]>> {
|
||||
// 1. CASL check
|
||||
const ability = this.caslFactory.createForUser(user);
|
||||
if (ability.cannot('read', 'Rfa')) {
|
||||
return { ok: false, reason: 'FORBIDDEN', message: 'ไม่มีสิทธิ์เข้าถึง RFA' };
|
||||
}
|
||||
|
||||
// 2. projectPublicId scope (ADR-019, ADR-023A)
|
||||
if (!params.projectPublicId) {
|
||||
return { ok: false, reason: 'INVALID_PARAMS', message: 'ต้องระบุ projectPublicId' };
|
||||
}
|
||||
|
||||
// 3. query business service
|
||||
try {
|
||||
const rfas = await this.rfaService.findByTool(params, user);
|
||||
return { ok: true, data: rfas.map(toRfaToolResult) };
|
||||
} catch (err) {
|
||||
this.logger.error('RfaToolService.getRfa failed', err);
|
||||
return { ok: false, reason: 'SERVICE_ERROR', message: 'เกิดข้อผิดพลาดในการดึงข้อมูล RFA' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ToolCallResult Type
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/tool/types/tool-call-result.type.ts
|
||||
|
||||
export type ToolCallReason = 'FORBIDDEN' | 'NOT_FOUND' | 'INVALID_PARAMS' | 'SERVICE_ERROR';
|
||||
|
||||
export type ToolCallResult<T> =
|
||||
| { ok: true; data: T }
|
||||
| { ok: false; reason: ToolCallReason; message: string };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LLM-Friendly ToolResult DTOs
|
||||
|
||||
Tool คืน `*ToolResult` DTO แทน raw entity — มีเฉพาะ fields ที่ LLM ต้องการ ไม่มี INT `id`:
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/ai/tool/types/rfa-tool-result.type.ts
|
||||
|
||||
export interface RfaToolResult {
|
||||
publicId: string;
|
||||
rfaNumber: string;
|
||||
revisionCode: string;
|
||||
statusCode: string; // business code: "1A", "1B", "PENDING" ฯลฯ
|
||||
drawingCount: number;
|
||||
submittedAt: string | null; // ISO string
|
||||
respondedAt: string | null;
|
||||
contractPublicId: string;
|
||||
}
|
||||
|
||||
// File: backend/src/modules/ai/tool/types/drawing-tool-result.type.ts
|
||||
|
||||
export interface DrawingToolResult {
|
||||
publicId: string;
|
||||
drawingCode: string;
|
||||
drawingTitle: string;
|
||||
discipline: string;
|
||||
currentRevision: string;
|
||||
latestRfaPublicId: string | null;
|
||||
latestRfaStatus: string | null;
|
||||
contractPublicId: string;
|
||||
}
|
||||
```
|
||||
|
||||
**กฎ:**
|
||||
- ❌ ห้ามมี `id: number` ในทุก ToolResult type
|
||||
- ❌ ห้ามมี TypeORM entity relation objects
|
||||
- ✅ ใช้ `publicId` + business codes เท่านั้น
|
||||
- ✅ Date fields เป็น ISO string (ไม่ใช่ Date object)
|
||||
|
||||
---
|
||||
|
||||
## LLM Prompt Integration (v1)
|
||||
|
||||
v1 ใช้ Tool result inject ใน prompt ตรง — ไม่ผสม RAG chunks (Phase 4):
|
||||
|
||||
```
|
||||
[System]
|
||||
คุณเป็น AI ผู้ช่วยระบบจัดการเอกสารก่อสร้าง NAP-DMS
|
||||
ตอบโดยใช้ข้อมูลใน Context เท่านั้น ห้ามคาดเดา
|
||||
|
||||
[Context — Tool Result]
|
||||
{{TOOL_RESULT_JSON}}
|
||||
|
||||
[User]
|
||||
{{USER_QUERY}}
|
||||
```
|
||||
|
||||
- `{{TOOL_RESULT_JSON}}` = `JSON.stringify(toolResult.data)` (compact, ไม่มี indent)
|
||||
- Token budget สำหรับ tool result: สูงสุด **500 tokens**
|
||||
- ถ้าผล tool เกิน 500 tokens → truncate ด้วย `slice(0, N)` + append `"... (แสดงผลบางส่วน)"`
|
||||
|
||||
---
|
||||
|
||||
## Error Handling by Reason
|
||||
|
||||
AI Gateway handle `ok=false` ตาม reason:
|
||||
|
||||
| reason | action |
|
||||
|--------|--------|
|
||||
| `FORBIDDEN` | คืน error message ให้ user + บันทึก audit log (security event) |
|
||||
| `NOT_FOUND` | คืน "ไม่พบข้อมูล" + แนะนำให้ตรวจสอบ parameter |
|
||||
| `INVALID_PARAMS` | คืน error พร้อมบอก field ที่ขาด |
|
||||
| `SERVICE_ERROR` | log error + คืน "ขณะนี้ระบบไม่สามารถดึงข้อมูลได้ กรุณาลองใหม่" |
|
||||
|
||||
---
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
backend/src/modules/ai/
|
||||
├── tool/
|
||||
│ ├── ai-tool-registry.service.ts ← static map + dispatch
|
||||
│ ├── rfa-tool.service.ts
|
||||
│ ├── drawing-tool.service.ts
|
||||
│ ├── transmittal-tool.service.ts
|
||||
│ ├── correspondence-tool.service.ts
|
||||
│ ├── circulation-tool.service.ts
|
||||
│ ├── document-tool.service.ts
|
||||
│ └── types/
|
||||
│ ├── tool-call-result.type.ts
|
||||
│ ├── rfa-tool-result.type.ts
|
||||
│ ├── drawing-tool-result.type.ts
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit & Observability
|
||||
|
||||
ทุก tool call บันทึกใน `ai_audit_logs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "tool_call",
|
||||
"intent": "GET_RFA",
|
||||
"params": { "projectPublicId": "...", "limit": 5 },
|
||||
"result": "ok" | "forbidden" | "not_found" | "service_error",
|
||||
"latencyMs": 45,
|
||||
"projectPublicId": "...",
|
||||
"userPublicId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- CASL enforcement อยู่ใน server code ทั้งหมด — ไม่มี unguarded path
|
||||
- ADR-019 compliance — INT id ไม่เคยปรากฏใน LLM context
|
||||
- Static registry อ่านง่าย, type-safe, test แยก tool ได้
|
||||
- Result wrapper ทำให้ Gateway handle error ได้อย่างมีโครงสร้าง
|
||||
|
||||
### Negative
|
||||
- Tool ใหม่ต้องเพิ่ม entry ใน static registry (code deploy) — ต่างจาก Intent Pattern ที่ admin แก้ได้ runtime
|
||||
- `*ToolResult` DTO เพิ่ม maintenance surface (แต่ต้องมีเพื่อ ADR-019)
|
||||
|
||||
### Risks
|
||||
- Tool result เกิน token budget → truncation อาจทำให้ LLM ตอบไม่ครบ → mitigate ด้วย pagination parameter ใน tool params (limit default = 5)
|
||||
- CASL factory ใน tool service อาจ duplicate กับ controller layer → mitigate ด้วย shared `CaslAbilityFactory` injection
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Phase 4)
|
||||
|
||||
- **Hybrid RAG + Tool** — ผสม RAG chunks กับ tool result ใน prompt เดียว (CONTEXT.md Phase 4)
|
||||
- **Streaming tool response** — v1 คืนครั้งเดียว
|
||||
- **Tool chaining** — tool เรียก tool อื่น (ไม่รองรับ v1)
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (ADR-009)
|
||||
|
||||
- ไม่มี schema เพิ่มใน ADR-025 — ใช้ตาราง `ai_audit_logs` ที่มีอยู่แล้ว
|
||||
- สร้าง NestJS module `AiToolModule` แยกจาก `AiModule` หลัก และ import เข้า `AiModule`
|
||||
@@ -0,0 +1,369 @@
|
||||
# ADR-026: Document Chat UI Pattern
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-19
|
||||
**Decision Makers:** Development Team, UX Lead, System Architect
|
||||
**Related Documents:**
|
||||
- [ADR-025: AI Tool Layer Architecture](./ADR-025-ai-tool-layer-architecture.md)
|
||||
- [ADR-024: Intent Classification Strategy](./ADR-024-intent-classification-strategy.md)
|
||||
- [ADR-023A: Unified AI Architecture — Model Revision](./ADR-023A-unified-ai-architecture.md)
|
||||
- [CONTEXT.md](../../CONTEXT.md)
|
||||
|
||||
> **หมายเหตุ:** ADR นี้กำหนดรูปแบบ UI สำหรับ Document Chat — ช่องสนทนากับ AI ใน context ของเอกสารที่กำลังดูอยู่ เป็นการเชื่อมต่อระหว่างผู้ใช้กับ AI Runtime Layer (ADR-024/025)
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
เมื่อ Intent Classifier (ADR-024) และ AI Tool Layer (ADR-025) พร้อมใช้งาน ผู้ใช้ต้องการช่องทางโต้ตอบกับ AI ใน context ของเอกสารที่กำลังดูอยู่
|
||||
|
||||
ความท้าทาย:
|
||||
1. **Context switching** — ถ้าเปิดหน้าใหม่ ผู้ใช้จะเสีย context ของเอกสารที่กำลังดู
|
||||
2. **Screen real estate** — หน้าจอต้องแสดงทั้งเอกสารและ chat พร้อมกัน
|
||||
3. **Multi-device** — ต้องรองรับ desktop (จอใหญ่) และ tablet (ไซต์ก่อสร้าง)
|
||||
4. **Workflow continuity** — ผู้ใช้อาจสลับไปมาระหว่างดูเอกสารและถาม AI หลายครั้ง
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
- **Preserve document context** — ผู้ใช้ต้องเห็นเอกสารและ chat พร้อมกัน
|
||||
- **Minimize interruption** — ไม่ควร block หน้าจอ หรือพาไปหน้าใหม่
|
||||
- **Responsive design** — รองรับ 1920×1080 (office) ถึง 768×1024 (tablet ไซต์)
|
||||
- **Collapsible** — ผู้ใช้ควบคุมได้ว่าจะเปิด/ปิด chat หรือไม่
|
||||
- **Consistent placement** — อยู่ตำแหน่งเดียวกันทุกหน้า เพื่อ muscle memory
|
||||
|
||||
---
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Option A: Modal (Overlay Dialog)
|
||||
|
||||
Chat แสดงเป็น modal กลางจอ พร้อม backdrop มืด
|
||||
|
||||
**Pros:**
|
||||
- Implement ง่าย ใช้ shadcn Dialog ได้เลย
|
||||
- โฟกัสที่ chat สุดๆ ไม่มีสิ่งรบกวน
|
||||
|
||||
**Cons:**
|
||||
- ❌ บดบังเอกสารทั้งหมด — เสีย context
|
||||
- ❌ ปิด modal = สนทนาหาย — ไม่เหมาะกับการสลับไปมา
|
||||
- ❌ ไม่เห็นข้อมูลเอกสารระหว่างพิมพ์คำถาม
|
||||
|
||||
### Option B: Separate Page (/documents/[id]/chat)
|
||||
|
||||
Chat เป็นหน้าแยกต่างหาก มี URL ของตัวเอง
|
||||
|
||||
**Pros:**
|
||||
- URL shareable — ส่งลิงก์สนทนาให้คนอื่นได้
|
||||
- หน้าจอเต็มที่สำหรับ chat history ยาวๆ
|
||||
|
||||
**Cons:**
|
||||
- ❌ Context switching รุนแรง — ต้องกด Back เพื่อดูเอกสาร
|
||||
- ❌ ต้องโหลดเอกสารซ้ำในหน้า chat (หรือเก็บ state ซับซ้อน)
|
||||
- ❌ ไม่สามารถดูเอกสารและ chat พร้อมกัน
|
||||
|
||||
### Option C: Side-panel (Right side, collapsible) ✅ (เลือก)
|
||||
|
||||
Chat แสดงเป็น panel ทางขวา กด toggle เปิด/ปิดได้
|
||||
|
||||
**Pros:**
|
||||
- ✅ เอกสารยังเห็นอยู่ — context ไม่หาย
|
||||
- ✅ สลับไปมาง่าย — toggle เปิด/ปิดไม่กระทบเอกสาร
|
||||
- ✅ รองรับ responsive — desktop (fixed 400px), tablet (30% width), mobile (bottom sheet)
|
||||
- ✅ ตำแหน่ง consistent — อยู่ขวาทุกหน้า
|
||||
|
||||
**Cons:**
|
||||
- เอกสารหลักเหลือพื้นที่น้อยลงเมื่อเปิด chat
|
||||
- Implement resizable ซับซ้อนกว่า modal (แต่ v1 ใช้ fixed width)
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
**เลือก Option C: Right-side collapsible side-panel**
|
||||
|
||||
---
|
||||
|
||||
## Layout Specification
|
||||
|
||||
### Desktop (≥ 1024px)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Header │
|
||||
├─────────────────────────────────────┬───────────────────────┤
|
||||
│ │ │
|
||||
│ Document Content │ [Toggle] AI Chat │
|
||||
│ (remaining width) │ ───────────────── │
|
||||
│ │ User: สรุป RFA นี้ │
|
||||
│ • Drawing A-101 │ ───────────────── │
|
||||
│ • Revision 3 │ AI: ตอบ... │
|
||||
│ • Status: Approved │ ───────────────── │
|
||||
│ │ [Suggested Actions] │
|
||||
│ │ • View latest RFA │
|
||||
│ │ • Create new RFA │
|
||||
│ │ │
|
||||
└─────────────────────────────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
- **Panel width:** 400px (fixed)
|
||||
- **Toggle button:** มุมขวาบนของเอกสาร (แยกจาก panel)
|
||||
- **Animation:** slide in/out 200ms ease-out
|
||||
- **Z-index:** 40 (สูงกว่าเอกสาร แต่ต่ำกว่า modal/dialog อื่น)
|
||||
|
||||
### Tablet (768px – 1023px)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Header │
|
||||
├────────────────────┬───────────────────┤
|
||||
│ │ │
|
||||
│ Document │ AI Chat (30%) │
|
||||
│ (70%) │ │
|
||||
│ │ │
|
||||
└────────────────────┴───────────────────┘
|
||||
```
|
||||
|
||||
- **Panel width:** 30% of viewport
|
||||
- **Min-width:** 320px (ถ้าน้อยกว่า ให้เป็น overlay แทน)
|
||||
|
||||
### Mobile (< 768px)
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ Header │
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ Document Content │
|
||||
│ (เต็มจอ) │
|
||||
│ │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ [💬] Toggle Chat │ ← floating button
|
||||
└─────────────────────────────┘
|
||||
|
||||
เมื่อกด Toggle:
|
||||
┌─────────────────────────────┐
|
||||
│ Header │
|
||||
├─────────────────────────────┤
|
||||
│ AI Chat (Bottom Sheet) │
|
||||
│ ───────────────────── │
|
||||
│ สูง 60% ของจอ │
|
||||
│ │
|
||||
│ [Drag handle] │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Pattern:** Bottom sheet (shadcn Sheet with side="bottom")
|
||||
- **Height:** 60% of viewport (expandable to 90%)
|
||||
- **Backdrop:** มี overlay มืดบางๆ บนเอกสาร
|
||||
|
||||
---
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
frontend/components/ai/
|
||||
├── ai-chat-panel.tsx ← หลัก (รวมทุก breakpoint)
|
||||
├── ai-chat-toggle.tsx ← ปุ่มเปิด/ปิด (floating บน mobile)
|
||||
├── ai-chat-messages.tsx ← message list + bubble
|
||||
├── ai-chat-input.tsx ← input + send button
|
||||
├── ai-suggested-actions.tsx ← action chips
|
||||
└── hooks/
|
||||
└── use-ai-chat.ts ← TanStack Query + state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State (per page)
|
||||
|
||||
```typescript
|
||||
// useAiChat hook
|
||||
interface AiChatState {
|
||||
isOpen: boolean; // panel เปิดอยู่หรือไม่
|
||||
messages: ChatMessage[]; // สนทนาใน session นี้
|
||||
isLoading: boolean; // AI กำลังตอบ
|
||||
suggestedActions: SuggestedAction[]; // actions ล่าสุดจาก AI
|
||||
}
|
||||
```
|
||||
|
||||
### Persistence
|
||||
|
||||
- **Session storage:** เก็บ `messages` ต่อ session (refresh = หาย)
|
||||
- **No server persistence:** v1 ไม่เก็บ chat history บน server (ลด scope)
|
||||
- **Context preservation:** `documentPublicId` + `documentType` ส่งทุก request เพื่อให้ AI รู้ context
|
||||
|
||||
---
|
||||
|
||||
## Integration with AI Runtime Layer
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
User พิมพ์ใน Chat Input
|
||||
│
|
||||
▼
|
||||
AiChatInput → useAiChat.sendMessage()
|
||||
│
|
||||
▼
|
||||
POST /api/ai/chat
|
||||
{
|
||||
"query": "สรุปเอกสารนี้",
|
||||
"context": {
|
||||
"type": "drawing",
|
||||
"publicId": "0195..."
|
||||
}
|
||||
}
|
||||
│
|
||||
▼
|
||||
AI Gateway (ADR-023A)
|
||||
│
|
||||
├─→ Intent Classifier (ADR-024)
|
||||
│
|
||||
├─→ AI Tool Layer (ADR-025) ถ้าเป็น GET_* intent
|
||||
│
|
||||
└─→ RAG Pipeline ถ้าเป็น RAG_QUERY
|
||||
│
|
||||
▼
|
||||
Ollama (gemma4:e4b Q8_0)
|
||||
│
|
||||
▼
|
||||
Response → Stream/Chunk → UI
|
||||
```
|
||||
|
||||
### Context Injection
|
||||
|
||||
ทุก request อัตโนมัติแนบ `context` จากหน้าปัจจุบัน:
|
||||
|
||||
| Page | context.type | context.publicId |
|
||||
|------|--------------|------------------|
|
||||
| /drawings/[id] | "drawing" | drawing.publicId |
|
||||
| /rfas/[id] | "rfa" | rfa.publicId |
|
||||
| /transmittals/[id] | "transmittal" | transmittal.publicId |
|
||||
| /correspondences/[id] | "correspondence" | correspondence.publicId |
|
||||
|
||||
---
|
||||
|
||||
## UX Patterns
|
||||
|
||||
### Initial State
|
||||
|
||||
- **Default:** Panel ปิด (ผู้ใช้ต้องกดเปิดเอง)
|
||||
- **First visit:** แสดง subtle hint (pulse animation ที่ toggle button) ครั้งเดียว
|
||||
- **Returning user:** จำ state จาก session storage (ถ้าเคยเปิดไว้ → เปิดต่อ)
|
||||
|
||||
### Message Types
|
||||
|
||||
| Type | ลักษณะ | ตัวอย่าง |
|
||||
|------|--------|----------|
|
||||
| **User** | ขวา, primary color | "สรุป RFA นี้ให้หน่อย" |
|
||||
| **AI Text** | ซ้าย, สีธรรมดา | ข้อความตอบ |
|
||||
| **AI Tool Result** | ซ้าย, card style | รายการ RFA 3 รายการ |
|
||||
| **AI Suggestion** | ซ้ายล่าง, chip buttons | "ควรสร้าง RFA ใหม่?" |
|
||||
| **System** | กลาง, จางๆ | "กำลังเชื่อมต่อ AI..." |
|
||||
|
||||
### Suggested Actions
|
||||
|
||||
AI ตอบพร้อม suggested actions (ถ้ามี):
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ AI: สรุป RFA-0042 │
|
||||
│ • ส่ง 2024-05-10 │
|
||||
│ • สถานะ: รอตอบกลับ │
|
||||
│ • มี 3 drawings │
|
||||
├─────────────────────────────────┤
|
||||
│ [ดู RFA ฉบับเต็ม] [สร้าง RFA ตัวถัดไป] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Action กดได้ทันที ไม่ต้องพิมพ์ใหม่
|
||||
- ถ้ากด → ส่ง query ใหม่อัตโนมัติ (เช่น "ดู RFA ฉบับเต็ม")
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Network Error
|
||||
|
||||
- แสดง "ไม่สามารถเชื่อมต่อ AI ได้ กรุณาลองใหม่" + ปุ่ม Retry
|
||||
- ไม่ clear messages ที่มีอยู่
|
||||
|
||||
### AI Timeout (> 10s)
|
||||
|
||||
- แสดง "AI ตอบช้าเกินไป กรุณาลองอีกครั้ง"
|
||||
- Log ใน ai_audit_logs สำหรับ debug
|
||||
|
||||
### Permission Error (จาก Tool Layer)
|
||||
|
||||
- แสดง "คุณไม่มีสิทธิ์เข้าถึงข้อมูลนี้" (จาก message ที่ Tool Layer คืน)
|
||||
- ไม่ expose technical details
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Keyboard:** Toggle ด้วย `Ctrl/Cmd + .` (custom shortcut)
|
||||
- **Focus trap:** เมื่อ panel เปิด focus อยู่ใน panel จนกว่าจะปิด
|
||||
- **Screen reader:** อ่าน "AI Chat panel opened" / "AI message received"
|
||||
- **Reduced motion:** ปิด animation เมื่อ user ตั้งค่า reduced motion
|
||||
|
||||
---
|
||||
|
||||
## Audit & Observability
|
||||
|
||||
ทุก interaction บันทึกใน `ai_audit_logs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "chat_message",
|
||||
"contextType": "drawing",
|
||||
"contextPublicId": "0195...",
|
||||
"query": "สรุปเอกสารนี้",
|
||||
"responseType": "text",
|
||||
"hasSuggestedActions": true,
|
||||
"latencyMs": 2500,
|
||||
"projectPublicId": "...",
|
||||
"userPublicId": "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Context ของเอกสารไม่หายระหว่างถาม AI
|
||||
- สลับไปมาระหว่างเอกสารและ chat ได้ราบรื่น
|
||||
- Responsive รองรับทั้ง office และไซต์ก่อสร้าง
|
||||
- ตำแหน่ง consistent — ผู้ใช้รู้ว่าหา chat ที่ไหน
|
||||
|
||||
### Negative
|
||||
- เอกสารหลักเหลือพื้นที่น้อยลงบนจอเล็ก (แต่ collapsible ช่วยได้)
|
||||
- Mobile bottom sheet อาจบดบังเนื้อหาส่วนล่างของเอกสาร
|
||||
- Chat history ไม่ persist (refresh หาย) — v2 อาจเพิ่ม server persistence
|
||||
|
||||
### Risks
|
||||
- User เปิด chat ทิ้งไว้แล้วลืม → สิ้นเปลืองพื้นที่จอ → mitigate ด้วย auto-collapse เมื่อ navigate ไปหน้าอื่น
|
||||
- Mobile bottom sheet gesture ชนกับ scrolling → mitigate ด้วย drag handle ชัดเจน
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Phase 4)
|
||||
|
||||
- **Multi-document chat** — chat ที่ context หลายเอกสารพร้อมกัน
|
||||
- **Persistent chat history** — เก็บสนทนาย้อนหลังบน server
|
||||
- **Real-time collaboration** — หลายคน chat ในห้องเดียวกัน
|
||||
- **Voice input** — พิมพ์อย่างเดียว (v1)
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes (ADR-009)
|
||||
|
||||
- ไม่มี schema change — ADR-026 เป็น frontend-only decision
|
||||
- ใช้ตาราง `ai_audit_logs` ที่มีอยู่แล้วสำหรับ logging
|
||||
- เพิ่ม component ใน `frontend/components/ai/`
|
||||
|
||||
Reference in New Issue
Block a user