Skip to main content
Glama
ndodth

MCPTest

by ndodth
README.md
# สรุประบบ: (MCP + AI Agent)



---

## 1. ระบบนี้ทำอะไร
ระบบ MCP ที่คอยคุมการทำงานของ AI เป็นผู้ช่วยตอบคำถามเรื่องข้อมูล
* ผู้ใช้ถาม $\rightarrow$ Agent เลือก tool $\rightarrow$ tool เช็ก cache ก่อน ไม่มีค่อยไป DB จริง $\rightarrow$ ถ้า DB เปลี่ยน ระบบแจ้งเตือนกลับมาล้าง cache ทันที

---

## 2. องค์ประกอบหลัก
| ไฟล์ | หน้าที่ |
| :--- | :--- |
| `main.py` | AI Agent — รับคำถาม, ตัดสินใจเรียก tool ผ่าน Gemini |
| `server.py` | MCP Server — เปิด tools ให้ Agent เรียกใช้ฐานข้อมูล |
| `semantic_cache.py` | Cache ชั้น 1 — เก็บคำถาม-คำตอบเดิมแบบ embedding |
| `webhook_cdc.py` | รับแจ้งเตือนจาก Supabase เมื่อข้อมูลเปลี่ยน แล้วล้าง cache |
| `schema.sql` | โครงสร้าง DB (customers, purchase_ledger, customer_interactions) |

---

## 3. ในฐานข้อมูล (DB) เก็บอะไรไว้บ้าง และป้องกันยังไง
DB (PostgreSQL บน Supabase) มี 3 ตารางหลัก:

| ตาราง | เก็บอะไร | การป้องกัน / กลไก |
| :--- | :--- | :--- |
| `customers` | ข้อมูลโปรไฟล์ลูกค้า (ชื่อ, ข้อมูลติดต่อ ฯลฯ) | แก้ไขได้ปกติ แต่ทุกครั้งที่แก้ trigger จะยิง webhook แจ้งล้าง cache |
| `purchase_ledger` | ประวัติธุรกรรม/ยอดการเงินของลูกค้า | **Append-only** — ห้าม UPDATE/DELETE เด็ดขาด ผ่าน DB Trigger ที่บล็อกไว้ตรงๆ ที่ระดับฐานข้อมูล (ต่อให้เจาะเข้ามาตรงๆ ก็แก้ไม่ได้) |
| `customer_interactions` | ประวัติการพูดคุย/โต้ตอบกับลูกค้า พร้อมเก็บ embedding vector (ใช้ extension `pgvector`) | ใช้สำหรับ semantic search ย้อนดูบทสนทนาเก่าที่เกี่ยวข้อง |

### Hash Chain (สำหรับ `purchase_ledger` โดยเฉพาะ):
* แต่ละ record ในตารางนี้จะมีค่า `entry_hash` ที่คำนวณจากข้อมูลของ record นั้น + `prev_hash` (hash ของ record ก่อนหน้า) ด้วย SHA-256
* ทำให้ record ทั้งหมดเชื่อมกันเป็น "โซ่" ถ้ามีใครแอบไปแก้ข้อมูลตรงกลางโซ่ (แม้จะพยายามเลี่ยง trigger ก็ตาม) hash ของ record ถัดไปจะไม่ตรงกันทันที ตรวจจับได้ตอน audit
* พูดง่ายๆ: ต่อให้ป้องกันด้วย trigger แล้ว hash chain คือ "เช็คซ้ำอีกชั้น" ว่าข้อมูลไม่ถูกแตะต้องจริงๆ

### PROMPT ของ AI
```python
SYSTEM_PROMPT = """
You are an Enterprise CRM & Financial Audit Ledger AI Assistant.
You have access to trusted backend tools that provide customer profiles, live financial transactions, audit logs, and historical interaction text search.
...
"""
```

---

## 4. Workflow

```
[1] ผู้ใช้พิมพ์คำถามเข้า main.py
      │
      ▼
[2] ระบบแปลงคำถามเป็น embedding (ด้วย Sentence-Transformers, รันในเครื่อง ไม่เสีย token)
      │
      ▼
[3] เช็ค Layer 1 – Semantic Cache ใน Redis
    ค้นด้วย key: semantic_cache:customer:{customer_id}:*
    เทียบ embedding คำถามใหม่ กับคำถามเก่าที่เคยถามและตอบไปแล้ว (cosine similarity)
      │
      ├───────────────────────────────┐
      ▼ (similarity ≥ 0.92)           ▼ ไม่เจอ / ไม่คล้ายพอ (MISS)
[4a] ส่งคำตอบเดิมกลับทันที          [4b] ส่งคำถาม + รายชื่อ tool ที่มีให้ Gemini
     จบ flow ตรงนี้                     (ไม่เรียก DB, ไม่เรียก DB cache เลย)
                                      │
                                      ▼
                                [5] Gemini ตัดสินใจว่าจะเรียก MCP tool ไหน
                                    (เช่น get_customer_profile, get_customer_financial_summary)
                                      │
                                      ▼
                                [6] Tool ทำงานผ่าน server.py
                                    เช็ค Layer 2 – DB Result Cache ใน Redis ก่อน
                                    key: cache:db:profile:{customer_id}
                                    หรือ cache:db:financial:{customer_id}:{page}:{limit}
                                      │
                                ┌─────┴──────┐
                                ▼ HIT        ▼ MISS
                          [7a] ดึงข้อมูล      [7b] Query PostgreSQL จริง
                               จาก Redis          แล้วเก็บผลลัพธ์ลง Redis
                               (เร็ว ไม่กิน DB)     (ตั้ง TTL 24 ชม.)
                                │            │
                                └─────┬──────┘
                                      ▼
                                [8] ได้ข้อมูลดิบกลับมาที่ Agent
                                      │
                                      ▼
                                [9] เรียก Gemini อีกครั้ง เพื่อเรียบเรียงข้อมูลดิบ
                                    ให้เป็นคำตอบภาษาคนอ่านง่าย
                                      │
                                      ▼
                                [10] ตอบกลับผู้ใช้
                                     + บันทึกคำถาม-คำตอบคู่นี้ลง Layer 1 cache
                                     (ไว้ให้คำถามคล้ายๆ กันครั้งหน้า hit ได้)
```

* **Layer 1** เช็คจาก ความหมายของคำถาม
* **Layer 2** เช็คจาก ผลลัพธ์ของ DB query ที่เคยทำมาก่อน — เป็นด่านที่สองที่กัน DB ไม่ให้โดนถามซ้ำ
* ทั้งสอง layer แยกกันทำงานคนละหน้าที่ ไม่ใช่ตัวเดียวกัน

---

## 5. Webhook + Backend สำหรับล้าง Cache (CDC) — Workflow ละเอียด

```
[1] มีการเปลี่ยนแปลงข้อมูลจริงในตาราง customers หรือ purchase_ledger
    (เช่น เพิ่มธุรกรรมใหม่ / แก้โปรไฟล์ลูกค้า)
      │
      ▼
[2] PostgreSQL Trigger บนตารางนั้นทำงานทันที (ก่อน trigger นี้จะเรียกฟังก์ชัน notify_cache_invalidation() ที่เขียนไว้)
      │
      ▼
[3] ฟังก์ชันเรียก net.http_post(...) (มาจาก extension pg_net) ยิง HTTP POST ออกไปหา backend ภายนอก
      │
      ▼
[4] Backend ที่ deploy แยกไว้บน Render (ไฟล์ webhook_cdc.py, เขียนด้วย FastAPI) รับ request ที่ endpoint POST /api/webhook/db-change
      │
      ▼
[5] Backend ตรวจสอบ header x-supabase-signature เทียบกับ secret ที่ตั้งไว้
    → ถ้าไม่ตรง ปฏิเสธทันที (กัน webhook ปลอมยิงเข้ามาสั่งล้าง cache มั่ว)
      │
      ▼
[6] แกะ payload ดูว่า:
    - TG_TABLE_NAME = ตารางไหนที่เปลี่ยน (customers หรือ purchase_ledger)
    - ถ้า table = customers → เอา customer_id จาก record.id
    - ถ้า table = purchase_ledger → เอา customer_id จาก record.customer_id
      │
      ▼
[7] สั่งลบ key ใน Redis:
    - ลบ Layer 1 เสมอ: semantic_cache:customer:{customer_id}:*
      (เพราะคำตอบเก่าที่เคย cache ไว้อาจอ้างอิงข้อมูลลูกค้ารายนี้ที่เปลี่ยนไปแล้ว)
    - ลบ Layer 2 เฉพาะส่วนที่เกี่ยวกับตารางที่เปลี่ยน:
      cache:db:profile:{id} หรือ cache:db:financial:{id}:*
      │
      ▼
[8] ครั้งต่อไปที่มีคนถามถึงลูกค้ารายนี้ → Layer 1 และ Layer 2 จะ miss โดยอัตโนมัติ
    → ระบบไปดึงข้อมูลจาก DB จริง → ได้ข้อมูลล่าสุดเสมอ (Zero-Stale Data)
```

---

## 6. ทำไมเอา Redis มาใช้ (2-Layer Cache)
ปัญหาที่เจอ: ทุกครั้งที่ผู้ใช้ถามคำถาม ถ้าปล่อยให้ระบบวิ่งไปเรียก LLM หรือ query DB ใหม่ทุกครั้ง จะช้าและสิ้นเปลือง โดยเฉพาะกับคำถามที่ถามซ้ำๆ หรือคล้ายเดิม
เลยเพิ่ม Redis เข้ามาเป็น cache 2 ชั้น:
* **ชั้น 1 (Semantic Cache)** — คำถามคล้ายเดิม ตอบกลับเลยไม่ต้องเรียก LLM ใหม่
* **ชั้น 2 (DB Result Cache)** — ผลลัพธ์จาก DB ที่ไม่ค่อยเปลี่ยน ดึงจาก Redis แทนที่จะยิง query เข้า PostgreSQL ใหม่ทุกครั้ง

---

## 7. ปัญหาการใช้ Token (LLM) และวิธีลด
**ปัญหา:** การเรียก Gemini ทุกครั้งที่มีคำถามเข้ามา ทำให้เสีย token และมี latency สูง โดยเฉพาะเวลาที่มีคนถามคำถามซ้ำๆ หรือคล้ายกัน
**วิธีที่แก้:**
1. **Semantic Cache (Layer 1)** — เช็คก่อนเรียก Gemini ทุกครั้งว่าเคยมีคนถามคำถามที่ "ความหมายคล้ายกัน" ไหม ถ้าคล้าย $\ge$ 0.92 $\rightarrow$ ข้ามการเรียก Gemini ไปเลย
2. **ใช้ Local Embeddings (Sentence-Transformers)** แทนการยิง embedding ผ่าน API ของ Gemini/OpenAI — ขั้นตอนเช็คความคล้ายนี้เลยไม่เสีย token เพิ่มเลย เพราะรันฟรีแบบ offline
3. **แยก cache ตาม `customer_id` (multi-tenant)** กันคำตอบของลูกค้ารายหนึ่งไปปนกับอีกราย

> [!IMPORTANT]
> **ข้อควรรู้:** Layer 2 (DB cache) ช่วยลดแค่ฝั่ง DB load/latency เท่านั้น **ไม่ได้ลด token ฝั่ง Gemini** เพราะไม่ว่า Layer 2 จะ hit หรือ miss ตัว Agent ก็ยังต้องเรียก Gemini อย่างน้อย 2 ครั้งอยู่ดี (เลือก tool + สรุปคำตอบ) — **ตัวเดียวที่ลด token จริงคือ Layer 1**

---

## 8. ภาพรวมทั้งระบบ (End-to-End)
1. ผู้ใช้ถามผ่าน `main.py`
2. เช็ค Semantic Cache ก่อน — คล้ายพอก็ตอบเลย ไม่เรียก Gemini
3. ถ้าไม่เจอ $\rightarrow$ Gemini เลือก tool ที่เหมาะสม
4. Tool เช็ค DB Cache ก่อนค่อยไป PostgreSQL จริง
5. ตอบกลับ + จำคำตอบไว้ใน cache
6. ถ้า DB เปลี่ยนเมื่อไหร่ $\rightarrow$ Webhook ยิงมาล้าง cache อัตโนมัติทันที

---

## NOTE
```bash
python3 -m venv venv
source venv/bin/activate # (สำหรับ Windows ให้ใช้: venv\Scripts\activate)
pip install -r requirements.txt
```