บทที่ 5backend|20 KB

บทที่ 5: AI Integration (Google Gemini API)

บทที่ 5: AI Integration (Google Gemini API)

🎯 เป้าหมายการเรียนรู้

หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:

  • ใช้ Google Gemini API สำหรับ AI Content Generation
  • ออกแบบ Fallback Strategy (AI → Google Search → Local)
  • จัดการ API Rate Limiting และ Cooldown
  • ใช้ Caching เพื่อประหยัด API calls
  • Parse และ Validate AI Responses
  • จัดการ Errors และ Retry Logic

📚 ทฤษฎีและแนวคิด

AI Content Generation คืออะไร?

AI Content Generation คือการใช้ AI สร้างเนื้อหาอัตโนมัติ เช่น:

  • 📝 สรุปข้อความ (Summarization)
  • 💬 สร้างคำคม (Quote Generation)
  • 🔤 แปลภาษา (Translation)
  • 🎨 Paraphrasing (เขียนใหม่)

Google Gemini API

Gemini คือ Large Language Model (LLM) จาก Google

ข้อดี:

  • ✅ รองรับภาษาไทยได้ดี
  • ✅ มี Free Tier (60 requests/minute)
  • ✅ Response เร็ว
  • ✅ รองรับ JSON output

ข้อจำกัด:

  • ⚠️ มี Rate Limit (429 Error)
  • ⚠️ ต้องจัดการ Quota
  • ⚠️ Response อาจไม่ consistent

Fallback Strategy (3 Layers)

Layer 1: Google Gemini AI
   ↓ (ถ้า fail หรือ quota เต็ม)
Layer 2: Google Search API
   ↓ (ถ้า fail หรือไม่เจอ)
Layer 3: Local Quotes (400 items)
   ↓
Always Return Something!

ทำไมต้องมี Fallback:

  • 🛡️ Reliability: แอปไม่พังถ้า AI fail
  • 💰 Cost Control: ประหยัด API costs
  • ⚡ Performance: ใช้ cache ลด latency

💻 โค้ดจริงจาก QUOM App

1. Setup และ Configuration

ไฟล์: backend/src/services/ai.js

import { GoogleGenerativeAI } from "@google/generative-ai";
import dotenv from "dotenv";
import crypto from "crypto";

dotenv.config();

// Gemini initialization
const GEMINI_KEY = process.env.GEMINI_API_KEY || "";
const genAI = GEMINI_KEY ? new GoogleGenerativeAI(GEMINI_KEY) : null;

// Cache + Cooldown (in-memory)
const GEN_CACHE = new Map(); // key -> { ts, out }
const CACHE_TTL_MS = 1000 * 60 * 30; // 30 minutes
let geminiCooldownUntil = 0;

อธิบายทีละบรรทัด (Imports):

  • import { GoogleGenerativeAI } from "@google/generative-ai"

    • Library หลักของ Google Gemini (ต้องติดตั้งเพิ่มด้วย npm)
  • import dotenv from "dotenv"

    • ใช้โหลด API Key จากไฟล์ .env
  • import crypto from "crypto"

    • Module มาตรฐานของ Node.js ใช้ทำ Hash (SHA-1) สร้าง Cache Key

อธิบาย:

  1. Import Gemini SDK

    import { GoogleGenerativeAI } from "@google/generative-ai";
    
    • Official SDK จาก Google
  2. Initialize Client

    const GEMINI_KEY = process.env.GEMINI_API_KEY || "";
    const genAI = GEMINI_KEY ? new GoogleGenerativeAI(GEMINI_KEY) : null;
    
    • ถ้าไม่มี API key ให้ genAI = null
    • ป้องกัน crash ถ้าลืมตั้งค่า
  3. In-Memory Cache

    const GEN_CACHE = new Map();
    const CACHE_TTL_MS = 1000 * 60 * 30; // 30 minutes
    
    • เก็บ response ไว้ 30 นาที
    • ลด API calls สำหรับ input เดิม
  4. Cooldown Mechanism

    let geminiCooldownUntil = 0;
    
    • เก็บ timestamp ที่ต้องรอก่อนเรียก API อีกครั้ง
    • ใช้เมื่อเจอ 429 (Rate Limit)

2. Cache Functions

function now() {
  return Date.now();
}

function makeKey({ transcript, style, language }) {
  const raw = `${language}::${style}::${transcript}`;
  return crypto.createHash("sha1").update(raw).digest("hex");
}

function cacheGet(key) {
  const hit = GEN_CACHE.get(key);
  if (!hit) return null;
  
  // Check if expired
  if (now() - hit.ts > CACHE_TTL_MS) {
    GEN_CACHE.delete(key);
    return null;
  }
  
  return hit.out;
}

function cacheSet(key, out) {
  GEN_CACHE.set(key, { ts: now(), out });
}

อธิบาย:

  1. makeKey()

    const raw = `${language}::${style}::${transcript}`;
    return crypto.createHash("sha1").update(raw).digest("hex");
    
    • สร้าง unique key จาก input parameters
    • ใช้ SHA-1 hash เพื่อความเร็ว
    • ตัวอย่าง key: a3f5b2c1d4e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0
  2. cacheGet()

    • ตรวจสอบว่ามี cache หรือไม่
    • ตรวจสอบว่า cache หมดอายุหรือยัง
    • ถ้าหมดอายุ ลบออก
  3. cacheSet()

    • เก็บ response พร้อม timestamp

3. Main Generation Function

export async function generateSummaryAndQuote({ 
  transcript, 
  style = "inspire", 
  language = "th" 
}) {
  const input = {
    transcript: String(transcript || "").trim(),
    style: String(style || "inspire"),
    language: String(language || "th"),
  };

  // Normalize language: auto -> th
  if (input.language === "auto") input.language = "th";
  
  if (!input.transcript) {
    return {
      lines: [],
      bullets: [],
      keywords: [],
      quote: "",
    };
  }

  // ---- Step 1: Check Cache ----
  const key = makeKey(input);
  const cached = cacheGet(key);
  if (cached) {
    return { ...cached, _source: "cache" };
  }

  // ---- Step 2: Try Gemini ----
  console.log("[SELECTION] Step 1: Trying AI...");
  const geminiOk = await tryGeminiOnce(input);
  if (geminiOk) {
    cacheSet(key, geminiOk);
    console.log("✅ AI generation successful");
    return { ...geminiOk, _source: "gemini" };
  }

  // ---- Step 3: Try Google Search ----
  console.log("[SELECTION] Step 2: Falling back to Google Search...");
  try {
    const googleResult = await tryGoogleSearch(
      input.transcript, 
      input.style, 
      input.language
    );
    if (googleResult) {
      cacheSet(key, googleResult);
      return { ...googleResult, _source: "google" };
    }
    throw new Error("Google Search returned null");
  } catch (googleErr) {
    console.warn("⚠️ Google Search failed:", googleErr?.message);
  }

  // ---- Step 4: Local Quotes ----
  console.log("[SELECTION] Step 3: Falling back to Local Quotes...");
  const local = tryRandomQuotes(input.transcript, input.style);
  cacheSet(key, local);
  return { ...local, _source: "local" };
}

อธิบาย Flow:

  1. Input Validation

    • Normalize input
    • แปลง auto เป็น th
    • ตรวจสอบ transcript ไม่ว่าง
  2. Layer 1: Cache

    • เช็ค cache ก่อนเสมอ
    • ถ้ามี return ทันที (เร็วที่สุด)
  3. Layer 2: Gemini AI

    • เรียก tryGeminiOnce()
    • ถ้าสำเร็จ cache และ return
  4. Layer 3: Google Search

    • ถ้า AI fail ลอง Google Search
    • ใช้ try-catch เพราะอาจ fail
  5. Layer 4: Local Fallback

    • ถ้าทุกอย่าง fail ใช้ local quotes
    • Always return something!

4. Gemini AI Implementation

async function tryGeminiOnce({ transcript, style, language }) {
  if (!genAI) {
    console.warn("⚠️ GEMINI_API_KEY missing -> skip AI");
    return null;
  }

  // Check cooldown
  if (now() < geminiCooldownUntil) {
    const left = Math.ceil((geminiCooldownUntil - now()) / 1000);
    console.warn(`⚠️ Gemini cooldown active (${left}s) -> skip AI`);
    return null;
  }

  try {
    const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp" });

    const langName = language === "th" ? "ไทย" : "English";
    const prompt = `
คุณคือผู้ช่วยสร้างคำคมที่สร้างแรงบันดาลใจ
ให้ตอบเป็นภาษา: ${langName}
สไตล์คำคม: ${style}

**สำคัญมาก:** 
- คำคม (quote) ต้อง**แตกต่าง**จากข้อความต้นฉบับ
- อย่าคัดลอกข้อความต้นฉบับมาเป็นคำคม
- สร้างคำคมใหม่ที่สร้างแรงบันดาลใจจากแนวคิดของข้อความต้นฉบับ
- คำคมควรสั้น กระชับ และมีพลัง

ตอบเป็น JSON เท่านั้น (ห้ามมีข้อความอื่น):
{
  "lines": ["สรุป 3-5 บรรทัด"],
  "bullets": ["หัวข้อย่อย 3 ข้อ"],
  "keywords": ["คีย์เวิร์ด 3-5 คำ"],
  "quote": "คำคมใหม่ที่สร้างแรงบันดาลใจ 1-2 ประโยค",
  "translation": "แปลคำคม (quote) เป็นภาษาไทย"
}

ข้อความต้นฉบับ:
"${transcript}"
    `.trim();

    const result = await model.generateContent(prompt);
    const text = result.response.text();
    const parsed = extractJson(text);

    return {
      lines: Array.isArray(parsed.lines) ? parsed.lines : [],
      bullets: Array.isArray(parsed.bullets) ? parsed.bullets : [],
      keywords: Array.isArray(parsed.keywords) ? parsed.keywords : [],
      quote: String(parsed.quote || "").trim(),
      translation: String(parsed.translation || "").trim(),
    };
  } catch (err) {
    // Handle 429 (Rate Limit)
    if (err?.status === 429) {
      const retryMs = parseRetryDelayMs(err) ?? 30_000;
      geminiCooldownUntil = now() + retryMs;
      console.warn(`⚠️ Gemini 429 -> cooldown ${Math.ceil(retryMs / 1000)}s`);
    }

    console.error("Gemini API Error:", err?.message || err);
    return null;
  }
}

อธิบาย:

  1. Cooldown Check

    if (now() < geminiCooldownUntil) {
      const left = Math.ceil((geminiCooldownUntil - now()) / 1000);
      console.warn(`⚠️ Gemini cooldown active (${left}s)`);
      return null;
    }
    
    • ถ้ายังอยู่ใน cooldown period ข้ามไป
    • แสดงเวลาที่เหลือ
  2. Get Model

    const model = genAI.getGenerativeModel({ 
      model: "gemini-2.0-flash-exp" 
    });
    
    • ใช้ model gemini-2.0-flash-exp (เร็วและถูก)
  3. Prompt Engineering

    const prompt = `
    คุณคือผู้ช่วยสร้างคำคมที่สร้างแรงบันดาลใจ
    ให้ตอบเป็นภาษา: ${langName}
    สไตล์คำคม: ${style}
    ...
    `;
    
    • สำคัญมาก: Prompt ต้องชัดเจน
    • บอกให้ตอบเป็น JSON
    • บอกให้คำคมแตกต่างจากต้นฉบับ
  4. Generate Content

    const result = await model.generateContent(prompt);
    const text = result.response.text();
    
    • เรียก API
    • ได้ text response
  5. Parse JSON

    const parsed = extractJson(text);
    
    • แปลง text เป็น JSON object
    • จัดการกรณีที่มี ```json fences
  6. Error Handling - 429

    if (err?.status === 429) {
      const retryMs = parseRetryDelayMs(err) ?? 30_000;
      geminiCooldownUntil = now() + retryMs;
    }
    
    • ถ้าเจอ 429 (Rate Limit Exceeded)
    • อ่าน retry delay จาก error
    • ตั้ง cooldown

5. JSON Extraction

function extractJson(text) {
  const s = String(text || "").trim();
  
  // Remove ```json fences
  const cleaned = s
    .replace(/```json/gi, "")
    .replace(/```/g, "")
    .trim();
  
  // Find JSON object
  const start = cleaned.indexOf("{");
  const end = cleaned.lastIndexOf("}");
  
  if (start >= 0 && end > start) {
    return JSON.parse(cleaned.slice(start, end + 1));
  }
  
  return JSON.parse(cleaned);
}

อธิบาย:

  1. Remove Markdown Fences

    • AI อาจ return json ...
    • ต้องตัดออก
  2. Extract JSON

    • หา { แรกและ } สุดท้าย
    • ตัดเฉพาะส่วนนั้น
  3. Parse

    • ใช้ JSON.parse()
    • ถ้า fail จะ throw error

6. Rate Limit Handling

function parseRetryDelayMs(err) {
  try {
    const details = err?.errorDetails || [];
    const retry = details.find(d => 
      String(d["@type"] || "").includes("RetryInfo")
    );
    const s = String(retry?.retryDelay || "").trim(); // e.g. "44s"
    const m = /^(\d+)\s*s$/.exec(s);
    if (m) return Number(m[1]) * 1000;
  } catch { }
  return null;
}

อธิบาย:

  • Google API บอก retry delay ใน error
  • ตัวอย่าง: "44s" = 44 วินาที
  • แปลงเป็น milliseconds

7. Local Fallback

function tryRandomQuotes(transcript, style) {
  console.log("📚 Using Local Quotes (Fallback Layer 3)...");

  const localQuotes = [
    "ความล้มเหลวไม่ใช่จุดจบ แต่เป็นจุดเริ่มต้นของความสำเร็จ",
    "อย่ารอให้โอกาสเข้ามาหา แต่จงออกไปสร้างโอกาสด้วยตัวเอง",
    "ทุกความพยายามอาจไม่นำมาซึ่งความสำเร็จ แต่ทุกความสำเร็จล้วนเกิดจากความพยายาม",
    // ... 400+ quotes
  ];

  const randomQuote = localQuotes[
    Math.floor(Math.random() * localQuotes.length)
  ];

  return {
    lines: [
      `เกี่ยวกับ: ${transcript.substring(0, 30)}...`,
      "ระบบใช้คำคมจากคลังข้อมูลสำรอง",
      "เนื่องจากโควต้า AI เต็มหรือค้นหาไม่พบ",
    ],
    bullets: ["สร้างแรงบันดาลใจ", "ข้อคิดเตือนใจ", "มุมมองชีวิต"],
    keywords: ["แรงบันดาลใจ", "ชีวิต", "ความสำเร็จ", "พลังบวก"],
    quote: randomQuote,
  };
}

อธิบาย:

  • มี local quotes 400+ ข้อ
  • สุ่มเลือก 1 ข้อ
  • Always return something
  • ไม่มีทางที่ function นี้จะ fail

🔄 Flow Diagram

User Request
    ↓
generateSummaryAndQuote()
    ↓
┌─────────────────────────────────┐
│ 1. Check Cache                  │
│    ✅ Hit → Return immediately   │
│    ❌ Miss → Continue            │
└────────────┬────────────────────┘
             ↓
┌─────────────────────────────────┐
│ 2. Try Gemini AI                │
│    ✅ Success → Cache & Return   │
│    ❌ Fail (429/Error) → Next    │
└────────────┬────────────────────┘
             ↓
┌─────────────────────────────────┐
│ 3. Try Google Search             │
│    ✅ Found → Cache & Return     │
│    ❌ Not Found → Next           │
└────────────┬────────────────────┘
             ↓
┌─────────────────────────────────┐
│ 4. Local Quotes                  │
│    ✅ Always Success             │
└────────────┬────────────────────┘
             ↓
        Return Result

🎯 Best Practices

1. Always Have Fallback

// ✅ ดี - มี fallback
const result = await tryAI() || await trySearch() || getLocal();

// ❌ ไม่ดี - อาจ return null
const result = await tryAI();

2. Cache Aggressively

// ✅ ดี - cache ก่อนเรียก API
const cached = getCache(key);
if (cached) return cached;

const result = await callAPI();
setCache(key, result);

3. Handle Rate Limits

// ✅ ดี - ตั้ง cooldown
if (err.status === 429) {
  cooldownUntil = now() + retryDelay;
}

// ❌ ไม่ดี - เรียก API ซ้ำทันที
if (err.status === 429) {
  return await callAPI(); // จะโดน 429 อีก!
}

📝 แบบฝึกหัด

คำถามทบทวน

  1. ทำไมต้องมี Fallback Strategy?
  2. Cache ช่วยอะไร?
  3. 429 Error คืออะไร?
  4. Cooldown Mechanism ทำงานอย่างไร?
  5. ทำไม Local Fallback ต้องอยู่ท้ายสุด?

โจทย์ปฏิบัติ

โจทย์ที่ 1: เพิ่ม Retry Logic

// TODO: เพิ่ม retry 3 ครั้งสำหรับ Gemini API
// - ถ้า fail ครั้งแรก รอ 1 วินาที แล้วลองใหม่
// - ถ้า fail ครั้งที่ 2 รอ 2 วินาที
// - ถ้า fail ครั้งที่ 3 ไปใช้ fallback

โจทย์ที่ 2: Implement Redis Cache

// TODO: แทนที่ in-memory cache ด้วย Redis
// - ใช้ redis.get() และ redis.set()
// - ตั้ง TTL ด้วย EXPIRE

โจทย์ที่ 3: Add Metrics

// TODO: เพิ่ม metrics tracking
// - นับจำนวน cache hits/misses
// - นับจำนวน API calls ต่อ source
// - คำนวณ cost savings จาก cache

📌 สรุป

ในบทนี้เราได้เรียนรู้:

✅ Google Gemini API สำหรับ AI generation
✅ 3-Layer Fallback (AI → Search → Local)
✅ Caching เพื่อประหยัด API calls
✅ Rate Limiting และ Cooldown handling
✅ Prompt Engineering สำหรับ JSON output
✅ Error Handling และ Retry logic

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 6: File Upload System (Multer)
เราจะเรียนรู้การจัดการ file uploads, validation, และ storage


📚 แหล่งเรียนรู้เพิ่มเติม