ใน Codelab นี้คุณจะได้เรียนรู้วิธีการสร้างผู้ช่วยอัจฉริยะ ที่คอยตอบคำถามเหมือนผู้เชี่ยวชาญมาเอง โดยใช้ความรู้จากไฟล์ PDF, Video, Image, และ Audio ที่คุณมี พร้อมเทคนิคควบคุมการตอบเฉพาะเนื้อหาที่อยู่ในไฟล์ผ่าน LINE Chatbot ที่มี Gemini และ Firebase อยู่เบื้องหลังกัน
จุดเริ่มต้นของการพัฒนา LINE Chatbot คือคุณจะต้องสร้าง LINE OA(LINE Official Account) และเปิดใช้งาน Messaging API
หลังจากที่เรามี LINE OA เรียบร้อยแล้ว ขั้นตอนนี้จะพาทุกคนไปเพิ่มความสามารถให้ LINE OA ของเรากลายเป็น LINE Chatbot ได้
ขั้นตอนนี้เราจะเข้าไปใช้งาน LINE Developers Console ซึ่งเป็นเว็บไซต์สำหรับการบริหารจัดการ LINE Chatbot(LINE OA ที่เปิดใช้งาน Messaging API แล้ว) ในส่วนของนักพัฒนา
เบื้องหลังของ Chatbot ตัวนี้ เราจะใช้บริการใน Firebase อย่าง Cloud Functions for Firebase ดังนั้นขั้นตอนนี้เราจะมาสร้างโปรเจค Firebase เพื่อใช้งานกัน
เนื่องจาก Cloud Functions for Firebase มีเงื่อนไขว่า หากต้องการไป request ตัว APIs ที่อยู่ภายนอก Google คุณจำเป็นจะต้องใช้ Blaze plan(เราจะต้องไปเรียก Messaging API ของ LINE)
Firebase CLI เป็นเครื่องมือที่จำเป็นสำหรับการ deploy ตัวฟังก์ชันที่เราพัฒนาขึ้น อีกทั้งยังสามารถจำลองการทำงานฟังก์ชัน(Emulate) ภายในเครื่องที่เราพัฒนาอยู่(Locally) ได้
npm install -g firebase-tools
firebase --version
firebase login
mkdir bot
cd bot
firebase init functions
ขั้นตอนนี้เราจะสร้าง Webhook ขึ้นมาเพื่อให้ LINE Chatbot สามารถรับข้อมูลและโต้ตอบกับผู้ใช้งานได้ผ่าน Messaging API
ใน Codelab นี้เราจะใช้ axios มาเป็นตัวช่วยในการสร้าง request ซึ่งเราจะต้องทำการติดตั้ง dependency ตัวนี้ก่อน โดยให้เปิด command line แล้วเข้าไปที่ /functions จากนั้นใช้คำสั่ง
npm install axios --save
และจะใช้ node-cache มาเก็บข้อมูลไฟล์ที่ผู้ใช้ส่งมาใน memory ซึ่งสำหรับ serverless มันจะหายไปเองหาก Instance ไม่มีคนใช้งาน หรือมีการ deploy โค้ดขึ้นไปสร้าง Instance ใหม่ โดยให้ติดตั้งผ่านคำสั่ง
npm install node-cache --save
ถัดไปเราจะสร้าง Environment Variable โดยให้ไปสร้างไฟล์ .env ใน /functions แล้วให้ไป copy ค่า Channel Access Token ของ Messaging API Channel ที่ issue ไว้ก่อนหน้านี้มาระบุลงไป
CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN
เพื่อโค้ดที่สั้น เป็นระเบียบ เราจะแยกฟังก์ชันสำหรับการเรียกใช้ Messaging API ออกมาเป็น module โดยภายในจะมีฟังก์ชัน
ขั้นตอนนี้ให้สร้างไฟล์ /functions/utils/request.js แล้วให้ copy โค้ดด้านล่างนี้ไปวาง
const axios = require("axios");
const LINE_HEADER = {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CHANNEL_ACCESS_TOKEN}`
};
class Request {
getBinary(messageId) {
return axios({
method: "get",
headers: LINE_HEADER,
url: `https://api-data.line.me/v2/bot/message/${messageId}/content`,
responseType: "arraybuffer"
});
}
reply(replyToken, payload) {
return axios({
method: "post",
url: "https://api.line.me/v2/bot/message/reply",
headers: LINE_HEADER,
data: { replyToken, messages: payload }
});
}
loading(userId) {
return axios({
method: "post",
url: "https://api.line.me/v2/bot/chat/loading/start",
headers: LINE_HEADER,
data: { chatId: userId }
});
}
async curl(url) {
try {
return axios({ method: "get", url, responseType: "arraybuffer" });
} catch (error) {
throw error;
}
}
}
module.exports = new Request()
จากนั้นให้เปิดไฟล์ /functions/index.js แล้วให้ copy โค้ดด้านล่างนี้ไปแทนที่โค้ดเดิม เพื่อเตรียมรับ Webhook event ประเภท image , video , audio และ text จาก LINE
const { onRequest } = require("firebase-functions/v2/https");
const request = require("./utils/request");
const NodeCache = require("node-cache");
const cache = new NodeCache();
exports.webhook = onRequest(async (req, res) => {
if (req.method !== "POST") { return res.send(req.method); }
const events = req.body.events;
if (events.length === 0) { return res.end(); }
for (const event of events) {
const userId = event.source.userId;
switch (event.type) {
case "postback":
break;
case "message":
const messageType = event.message.type;
if (["image", "video", "audio"].includes(messageType)) {
}
if (messageType === "text") {
}
break;
}
}
res.end();
});
แล้วก็ให้เปิด command line ขึ้นมาอีกครั้ง และให้แน่ใจว่าเราอยู่ที่ /functions จากนั้นใช้คำสั่งด้านล่างนี้เพื่อ deploy ตัว webhook ของเราขึ้น production
firebase deploy --only functions
เมื่อ deploy เสร็จเรียบร้อยเราจะเห็น Webhook URL โผล่อยู่ใน command line เลย ก็ให้เรา copy มา หรือกรณีที่ไม่พบก็ให้เข้าไปที่ Firebase console เลือกโปรเจคที่เราสร้างไว้ แล้วเลือกเมนู Build > Functions จะเจอชื่อฟังก์ชันและ Webhook URL ที่นั่น
เมื่อได้ Webhook URL มาแล้ว ก็ให้เอา URL นี้ไปอัพเดทที่ Messaging API Channel ใน LINE Developers console ตามที่ได้สร้างไว้ และให้มั่นใจว่าได้เปิดใช้ Use webhook แล้ว
ขั้นตอนนี้เราจะไปเปิดใช้งาน Gemini API พร้อมสร้าง API Key เพื่อนำไปเชื่อมต่อกับ LINE Chatbot โดยให้เข้าสู่เว็บไซต์ Google AI Studio แล้วกดปุ่ม Create API key ที่มุมขวาบน จากนั้นจะเจอ dialog ที่ให้เราเลือก Google Cloud projects โดยให้เราเลือกโปรเจคเดียวกับที่เราได้สร้าง Firebase project ไว้ในข้อที่ 2 ซึ่งเมื่อกดปุ่ม Create API key in existing project เราก็จะได้ API key มา
ให้เราเพิ่มตัวแปร API_KEY ใน Environment Variable โดยเปิดไฟล์ .env ใน /functions แล้วให้เอา API key ระบุลงไป
CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN
API_KEY=YOUR-GEMINI-API-KEY
ขั้นตอนนี้เราจะมาติดตั้ง GoogleGenAI ซึ่งเป็น dependency สำหรับใช้งาน Gemini โดยให้เปิด command line แล้วเข้าไปที่ /functions จากนั้นใช้คำสั่ง
npm install @google/genai --save
และเพื่อให้โค้ดไม่ยาวจนเกินไป เราจะแยกฟังก์ชันทั้งหมดออกมาเป็น module โดยภายในจะมีฟังก์ชัน
โดยให้สร้างไฟล์ /functions/utils/gemini.js แล้วให้ copy โค้ดด้านล่างนี้ไปวาง
const { GoogleGenAI } = require("@google/genai");
const ai = new GoogleGenAI({ apiKey: `${process.env.API_KEY}` });
class Gemini {
isUrl(str) {
return /^(http(s)?:\/\/)?(www\.)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(str);
}
getMimeType(response) {
const contentType = response.headers["content-type"];
return contentType ? contentType.split(';')[0] : 'application/octet-stream';
}
isAllowedMimes(mimeType) {
return [
"application/pdf",
"image/jpeg",
"image/png",
"audio/wav",
"audio/mp3",
"audio/x-m4a",
"video/mp4",
"video/mov",
].includes(mimeType);
}
async multimodal(promptArray) {
const response = await ai.models.generateContent({
model: "gemini-2.0-flash",
contents: promptArray,
});
return response;
}
}
module.exports = new Gemini();
เสร็จแล้วก็ให้เราไปอัพเดทโค้ดในไฟล์ /functions/index.js โดยดึงเอา module ของ Gemini มาใช้งาน
const { onRequest } = require("firebase-functions/v2/https");
const request = require("./utils/request");
const NodeCache = require("node-cache");
const cache = new NodeCache();
const gemini = require("./utils/gemini");
ที่ไฟล์ /functions/index.js เราจะไปดึงเอา event.message.id จาก Webhook events มาแล้วเรียกใช้ฟังก์ชัน getBinary() เพื่อดึงเอา binary ของไฟล์ออกมาจาก LINE
// ...
if (["image", "video", "audio"].includes(messageType)) {
const response = await request.getBinary(event.message.id);
if (response.status === 202) {
await request.reply(event.replyToken, [{
type: "text",
text: `ระบบกำลังประมวลผลไฟล์ กรุณากดปุ่ม "ตรวจสอบสถานะ" ด้านล่างอีกครั้งครับ?`,
quickReply: {
items: [{
type: "action",
action: {
type: "postback", label: "ตรวจสอบสถานะ", data: `${event.message.id}`
}
}]
}
}]);
break;
}
if (response.status === 200) {
await getReady(response, userId, event.replyToken);
break;
}
}
// ...
ขั้นตอนนี้จะสำคัญตรงที่ เราต้องตรวจสอบ HTTP status ของ response จาก API ด้วย เนื่องจากเวลาในการประมวลผลไฟล์ที่ LINE sever จะขึ้นอยู่กับขนาดของไฟล์โดย response.status จะแบ่งออกเป็น 2 แบบ
กรณีที่ได้ HTTP status เป็น 202 และผู้ใช้งานได้กด "ตรวจสอบสถานะ" จาก Quick Reply มา เราจะต้องเขียนโค้ดเพื่อรอรับ Webhook event ประเภท Postback โดยในตัวอย่างนี้ เราจะเอา event.message.id ที่แนบมากับ Postback ไปยิง getBinary() ซ้ำ โดยหาก HTTP status เป็น 200 แล้วก็จะเรียกฟังก์ชัน getReady()
// ...
case "postback":
const response = await request.getBinary(event.postback.data);
if (response.status === 200) {
await getReady(response, userId, event.replyToken);
}
break;
// ...
ฟังก์ชัน getReady() นี้เราจะสร้างไว้ที่จุดล่างสุดของ /functions/index.js เลย โดยหน้าที่ของมันจะมี 3 อย่างด้วยกัน
// ...
const getReady = async (response, userId, replyToken) => {
let message = "ปัจจุบันฉันสามารถเข้าใจไฟล์ PDF, JPEG, PNG, WAV, MP3, MP4, และ MOV ได้เท่านั้น";
const mimeType = gemini.getMimeType(response);
if (gemini.isAllowedMimes(mimeType)) {
const base64 = Buffer.from(response.data).toString('base64');
cache.set(`${userId}`, { data: base64, mimeType });
message = "คุณอยากรุ้เรื่องอะไรจากไฟล์ที่คุณส่งมาครับ?";
}
await request.reply(replyToken, [{ type: "text", text: message }]);
}
นอกจากระบบเราจะรองรับไฟล์ที่ผู้ใช้ส่งมาแล้ว เพื่อเพิ่มความสะดวกในการใช้งาน เราจะเขียนโค้ดเพิ่มอีกหน่อย เพื่อรองรับการส่งไฟล์แบบ URL ไปด้วยเลย ซึ่งกรณีนี้จะทำให้ LINE Chatbot ของเรารับไฟล์ประเภท PDF ได้ (ปัจจุบันผู้ใช้งานไม่สามารถ browse หรือ drag ไฟล์ PDF เข้ามาใน LINE OA ได้)
เราจะสร้างเงื่อนไขในไฟล์ /functions/index.js เพื่อตรวจสอบว่าข้อความ Text ที่ส่งเข้ามาว่าเป็น URL หรือไม่ โดยหากเป็น URL จะไปเรียกฟังก์ชัน curl() เพื่อดึง binary ออกมา และเอา binary นั้นส่งไปยังฟังก์ชัน getReady() ตามโค้ดด้านล่างนี้
// ...
if (messageType === "text") {
const prompt = event.message.text.trim();
if (gemini.isUrl(prompt)) {
try {
const response = await request.curl(prompt);
await getReady(response, userId, event.replyToken);
} catch (error) {
await request.reply(
event.replyToken,
[{ type: "text", text: "ขออภัย ฉันไม่สามารถเปิด URL นี้ได้ครับ" }]
);
}
break;
}
}
// ...
กรณีที่ข้อความ Text ที่ส่งเข้ามาไม่ใช่ URL เราก็ตรวจสอบ cache ก่อนโดยเอา userId ของผู้ใช้ไปดึงมาว่าตอนนี้มีข้อมูลไฟล์อยู่ในนั้นไหม โดยหากมีข้อมูลไฟล์อยู่ใน cache เราจะเอามันมาปั้น prompt ซึ่ง prompt ของเราจะแบ่งออกเป็น 3 ส่วน
ให้เราเอา prompt ที่ปั้น ส่งไปให้ฟังก์ชัน multimodal() เพื่อให้ AI ประมวลผล ซึ่งเมื่อเราได้ผลลัพธ์มาก็จะ reply คำตอบกลับไปหาผู้ใช้งาน ตามโค้ดด้านล่าง
// ...
if (messageType === "text") {
// ...
const data = cache.get(userId);
if (data) {
await request.loading(userId);
const response = await gemini.multimodal([
"ตอบคำถามผู้ใช้เฉพาะเนื้อหาที่อยู่ในไฟล์เท่านั้น",
{ inlineData: data },
prompt
]);
await request.reply(
event.replyToken,
[{ type: "text", text: `${response.text}` }]
);
}
}
มาถึงตรงนี้โค้ดเราก็พร้อม deploy แล้ว จะรออะไรเปิด command line ขึ้นมา และให้แน่ใจว่าเรา cd ไปอยู่ที่ functions/ แล้ว จากนั้นเรียกคำสั่งด้านล่างได้เลย
firebase deploy --only functions
ตัวอย่าง ไฟล์ + Prompt สำหรับ Demo ตามนี้ครับ
โดยผลลัพธ์ที่ได้คือมันใช้ได้จริง แม่นมากๆ และที่สำคัญคือช่วยลดเวลาที่เราจะต้องใช้ในการไปอ่าน ไปแปล ไปดู ไปฟัง ไปศึกษา ได้เยอะมหาศาลเลยครับ
ยินดีด้วยครับ ถึงตรงนี้คุณก็มี LINE Chatbot ที่คอยตอบคำถามเหมือนผู้เชี่ยวชาญมาเอง โดยใช้ความรู้จากไฟล์ PDF, Video, Image, และ Audio ที่คุณมี พร้อมเทคนิคควบคุมการตอบเฉพาะเนื้อหาที่อยู่ในไฟล์ โดยที่มี Gemini และ Firebase อยู่เบื้องหลังกันได้แล้ว!!!