สร้าง Chatbot แบบง่ายๆด้วย Dialogflow และ Google Sheets + Apps Script

Cover image

จะเป็นอย่างไรถ้าเอา Dialogflow ไปเชื่อมต่อกับ Google Apps Script แล้วใช้ Google Sheets เป็น database บทความนี้มีคำตอบครับ

บอกไว้ก่อน

ก่อนจะเข้าเนื้อหาผมอยากให้ทุกคนศึกษาพื้นฐานของสิ่งเหล่านี้ก่อนนะครับ

โจทย์วันนี้

ผมจะสร้าง chatbot ร้านรองเท้าที่แสนจะธรรมดาครับ bot ตัวนี้จะมีหน้าที่เดียวเลยคือบอกราคาของรองเท้าแต่ละรุ่นที่มีในร้านครับ ถ้าไม่มีก็ไม่ตอบ (เอาแบบนี้เลย!) โดยผมจะใช้เครื่องมือยอดนิยมอย่าง Dialogflow ของ Google เป็นตัวกลางในการทำ NLP(Natural Language Processing) แล้วส่งไปประมวลผลกับ Google Apps Script ที่มี Google Sheets เป็นฐานข้อมูลราคาอีกทีครับ

เริ่มจาก Dialogflow

  1. ผมสร้าง Project ก่อนเลยครับ ผมจะให้ชื่อว่า Sheet-Chatbot เลือกภาษา Thai - th เพราะเราจะรองรับแค่ภาษาไทย สร้าง Project บน Dialogflow
  2. เนื่องจากเราจะทำ bot ร้านรองเท้า เราก็ต้องทำให้มันรู้จักชื่อรองเท้าที่ในร้านก่อนครับ ด้วยการสร้าง Entity ขึ้นมาชื่อว่า product ครับ สร้าง Entity ของรองเท้าแต่ละรุ่น
  3. หลังจากนั้นเราก็จะมาสร้าง Intent ของคนใช้กันครับ แปลง่ายๆก็น่าจะเป็น "ความต้องการ" ของการสื่อสารกับ bot น่ะครับ โดยผมตั้งชื่อว่า Ask for price ก็ตรงตัวครับ ถามราคา สร้าง Intent ของการถามราคา
  4. คราวนี้เราก็มาที่ Training Phase ครับ สอนให้ Bot รู้จักรูปประโยคของการ ถามราคา กันหน่อย ตรงนี้ยิ่งใส่เยอะๆยิ่งดีครับ จะเห็นว่าบางประโยคมันจะรู้เลยว่าคำนี้คือ product ของเรา ก็เพราะว่าผมใส่ไว้ใน Entity แล้วนั่นเองครับ ใส่ Training Phase ของการถามราคา
  5. ใส่ parameters ของ Intent ด้วยนะครับ บางครั้งเราไม่ได้ใส่ training phase ที่มี entity ติดไปด้วย เราก็จำเป็นต้องมาเพิ่มนะครับ เพื่อให้ bot ถามต่อจนกว่าจะได้ข้อมูลที่ครบถ้วนสมบูรณ์ ใส่ Parameters ของการถามราคา
  6. Define Prompts ด้วยนะครับ เพื่อให้ดูไม่เป็น bot จนเกินไปเราสามารถใส่ประโยคคำถามถึงตัว product ได้หลายๆรูปแบบเลย ใส่ Prompts ของ product
  7. เพื่อทดสอบคร่าวๆ ผมจะใส่ Text Response ตรงๆลงไปก่อนนะครับ เดี๋ยวตรงนี้เราจะมาแก้ทีหลัง ใส่ Text response ไปก่อน
  8. ทดสอบดูว่า bot เข้าใจอย่างที่เราอยากให้มันเข้าใจจริงๆหรือยังด้วยการพิมพ์ถามไปใน field ด้านข้างของจอได้เลยครับ สังเกตดูนะครับว่าที่ส่วน Intent จะขึ้นว่า Ask for price หรือเปล่า ถ้าเป็นแบบนั้นก็แปลว่ามันเข้าใจคำถามเราแล้ว ทดสอบพิมพ์คำถาม

คำถามแรกของเรายังไม่มีข้อมูลของตัว product มันเลยต้องถามเราต่ออีกว่าเรา "ดูรุ่นไหนอยู่" ผมก็ตอบไปสั้นๆว่า "next%" ตอนนี้ให้สังเกตที่ parameter กับ value นะครับ เห็น Nike ZoomX Vaporfly NEXT% ใช่ไหม แบบนี้แปลว่า bot น่าจะเข้าใจคำถามและรู้ด้วยว่าเราถามถึงสินค้าตัวไหน แต่ที่มันตอบไปว่า อันนี้ก็ไม่ทราบ :P เพราะเราบอกให้มันตอบไปแบบนั้นไงครับ

ตอบรุ่นของรองเท้าไปด้วย

มาที่ Google Apps Script กับ Google Sheets บ้าง

  1. เริ่มจากการ clone Apps Script Starter แล้วก็ติดตั้ง dependency package ก่อนเลยครับ
git clone https://github.com/labnol/apps-script-starter sheet-chatbot
cd sheet-chatbot
npm install
  1. สร้าง Project บน Apps Script บ้าง โดยเลือก type เป็น sheets นะครับ เดี๋ยวเราจะใช้ sheet เก็บข้อมูลราคา
npx clasp create --type sheets --title "Sheet Chatbot" --rootDir ./dist

สร้าง Project บน Apps Script

  1. หลังจากนั้นก็ใช้ IDE เปิด folder ขึ้นมาครับ อย่างผมใช้ Visual Studio Code ก็แค่พิมพ์ code . บน terminal แล้วหลังจากนั้นก็ลบไฟล์ตั้งต้นใน folder src ทิ้งให้หมดครับ เหลือไว้แค่ index.js ซึ่งผมแก้เป็นแบบนี้
const helloWorld = () => {
  Logger.log('Hello World');
};

global.helloWorld = helloWorld;

ลบไฟล์ใน src ทิ้ง

  1. เปิดไฟล์ appsscript.json ขึ้นมาแล้วก็ลบทุกสิ่งที่ไม่ได้ใช้ทิ้งเช่นกันครับ ผมแก้ webapp.access ให้ ANYONE_ANONYMOUS ด้วยนะ เพราะว่าหลังจากเรา deploy ไปแล้วเราต้องการให้ Dialogflow เข้ามา request ได้ด้วย

appsscript.json

{
  "timeZone": "Asia/Bangkok",
  "runtimeVersion": "V8",
  "dependencies": {
    "libraries": []
  },
  "webapp": {
    "access": "ANYONE_ANONYMOUS",
    "executeAs": "USER_DEPLOYING"
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": []
}
  1. ทำการ deploy แล้วเข้าไปเช็คที่เว็บด้วยคำสั่ง
npm run deploy
clasp open

เปิดดู script บนเว็บ

  1. สั่ง Run function helloWorld ลองดูสักหน่อย ถ้าใน Log ขึ้นว่า "Hello World" แปลว่าไม่มีอะไรผิดพลาด ผล run ของ hello world
  2. เพื่อความสะดวกสบายในชีวิต ผมติดตั้ง 3rd party library เข้าไป 2 ตัวนะครับ โดยไปที่ Resources" > "Libraries... แล้วติดตั้งพวกนี้ลงไป
  • Tamotsu เอาไว้ mapping ข้อมูลบน Sheet ให้กลายเป็น Object ทำให้เราสะดวกในการเรียกใช้ครับ ไม่จำเป็นต้องไปปวดหัวกับ cell, row, column เลย key ของตัวนี้ก็คือ
1OiJIgWlrg_DFHFYX_SoaEzhFJPCmwbbfEHEqYEfLEEhKRloTNVJ-3U4s
  • BetterLog บันทึก log ลงบน Sheet ของเรา บางครั้งที่เรา call function จากข้างนอก web editor เราไม่สามารถเปิดเข้าไปดู log ได้ครับ ตัวนี้จะทำให้เราเช็คได้จาก sheet ของเราแทน key ของตัวนี้ก็คือ
1DSyxam1ceq72bMHsE6aOVeOl94X78WCwiYPytKi7chlg4x5GqiNXSw0l

อย่าลืมเลือก version ล่าสุดกันด้วยล่ะครับ

ติดตั้ง Libraries ให้ app script

  1. หลังจากเพิ่ม Library ใน web editor แล้วก็มาเพิ่มใน appsscript.json ด้วยนะ

appsscript.json

{
  "timeZone": "Asia/Calcutta",
  "dependencies": {
    "libraries": [
      {
        "userSymbol": "BetterLog",
        "libraryId": "1DSyxam1ceq72bMHsE6aOVeOl94X78WCwiYPytKi7chlg4x5GqiNXSw0l",
        "version": "27"
      },
      {
        "userSymbol": "Tamotsu",
        "libraryId": "1OiJIgWlrg_DFHFYX_SoaEzhFJPCmwbbfEHEqYEfLEEhKRloTNVJ-3U4s",
        "version": "31"
      }
    ]
  },
  "webapp": {
    "access": "ANYONE_ANONYMOUS",
    "executeAs": "USER_DEPLOYING"
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": []
}

ที่ .eslintrc ก็ต้องเพิ่ม Library ใหม่ของเราเข้าไปที่ globals ด้วยนะครับ เดี๋ยว build ไม่ผ่าน

.eslintrc

{
  "root": true,
  "parser": "babel-eslint",
  "extends": [
    "eslint:recommended",
    "airbnb-base",
    "plugin:prettier/recommended"
  ],
  "plugins": ["prettier", "googleappsscript"],
  "env": {
    "googleappsscript/googleappsscript": true
  },
  "rules": {
    "prettier/prettier": "error",
    "import/prefer-default-export": "error"
  },
  "globals": {
    "Tamotsu": true,
    "BetterLog": true
  }
}
  1. เข้าไปที่ https://sheets.google.com แล้วเปิดไฟล์ Sheet Chatbot ที่ clasp สร้างเอาไว้ มองหาไฟล์ Sheet Chatbot
  2. ใส่ข้อมูลของสินค้าลงไปที่ sheet ชื่อ Products โดย # คือรหัสสินค้า นอกจากนั้นก็ตามชื่อ column เลยครับ อย่าลืม copy sheet id ออกมาจาก url ด้วยนะครับ อย่างในรูปของผมคือ 1wEvuDUsMDTBIvnkni6A1th1oEehDNVdaTjvUP3gJdLw มองหาไฟล์ Sheet Chatbot
  3. ได้ sheet id แล้วก็มาสร้างไฟล์สำหรับตั้งค่าตัว Library ครับ ตรงนี้สามารถเข้าไปอ่านเพิ่มเติมได้ที่ github repository ของตัว library เองเลยนะครับ

src/sheets.config.js

const sheetId = '1wEvuDUsMDTBIvnkni6A1th1oEehDNVdaTjvUP3gJdLw';
// eslint-disable-next-line no-global-assign
Logger = BetterLog.useSpreadsheet(sheetId);
Tamotsu.initialize();
  1. ลอง deploy แล้ว run helloWorld ใหม่อีกรอบ คราวนี้เราจะเห็น Log ไปโผลที่ Sheet ใหม่ชื่อว่า Log ครับ
npm run deploy
clasp open

จะมีขอ permissoin นะครับ ก็ให้อนุญาตการเข้าถึงไปตามปกติ

ขอ permission จาก BetterLog

มี Log มาแสดงบน Sheet แล้ว

Log แสดงใน sheet แล้ว

  1. ลองแก้ src/index.js ให้ทำงานหลังถูก request ด้วย method POST จากข้างนอกดูบ้างครับ โดยผมจะให้มัน Log ค่าที่ Dialogflow ส่ง request มาด้วย (โดยปกติ POST มันจะเข้ามาที่ function doPost นะครับ ส่วนนี้เป็นของ Apps Script เองเลย)

src/index.js

import './sheets.config';

const doPost = (e) => {
  const data = JSON.parse(e.postData.contents);
  Logger.log(JSON.stringify(data));
};

global.doPost = doPost;
  1. ลอง deploy ใหม่อีกครั้งแล้ว เข้าไปที่ Publish > Deploy as webapp ใน Web editor แล้ว copy เอา Current web app URL: เก็บไว้ครับ
npm run deploy
clasp open

copy webapp url

  1. กลับไปที่ Dialogflow ครับ เลือกที่เมนู Fulfillment ในส่วนของ Webhook ให้เราวาง url ที่ copy มาเมื่อสักครู่ลงไปครับ อย่าลืม scroll ลงไปกดปุ่ม Done ด้วยนะครับ

วาง Url ลงใน Webhook

  1. ยังอยู่ที่ Dialogflow นะครับ กลับไปเปิด Intent ที่เราสร้างไว้เมื่อช่วงแรกที่ชื่อว่า Ask for price (หวังว่าจะยังไม่ลืมนะครับ) แล้ว scroll ลงไปที่ด้านล่างสุดเลย ลบ Text Reponse ที่เราเคยพิมพ์ไว้ออกไป ที่ Fulfillment ให้เปิด "Enable webhook call for this intent" เอาไว้เลยนะครับ

เปิดให้ Intent ทำงานกับ Webhook

  1. ทดลองคุยกับ bot เหมือนช่วงแรกเลยครับ แน่นอนว่าเรายังไม่ได้คำตอบกลับมาหรอก

ทดลองคุยกับ bot อีกครั้งเพื่อรอให้ bot ส่ง request ไปที่ Apps Script

  1. กลับเปิดที่ Sheet ของเราครับ คราวนี้เราจะได้เห็น request object ที่ถูกส่งมาจาก Dialogflow

Request Object ถูกเขียนลงไปใน Log แล้ว

มาวิเคราะห์กันดีกว่าว่ามีอะไรส่งมาให้เราดูบ้าง

{
  "responseId": "...",
  "queryResult": {
    "queryText": "next%",
    "parameters": {
      "product": "Nike ZoomX Vaporfly NEXT%"
    },
    "allRequiredParamsPresent": true,
    "fulfillmentMessages": [
      {
        "text": {
          "text": [""]
        }
      }
    ],
    "intent": {
      "name": "projects/sheet-chatbot-unfjpc/agent/intents/74d5284b-e57f-455e-a3ba-fcf7b8439239",
      "displayName": "Ask for price"
    },
    "intentDetectionConfidence": 1,
    "languageCode": "th"
  },
  "originalDetectIntentRequest": {
    "payload": {}
  },
  "session": "..."
}

หลายคนน่าจะมองออกแล้วว่าสิ่งที่เราต้องการตอนนี้มีอะไรบ้าง แต่ใครที่ยังไม่รู้ให้สังเกตที่ queryResult.parameters กับ queryResult.intent ครับผม 2 ค่านี้เพียงพอที่จะทำให้เรารู้แล้วว่าคนที่คุยกับ bot ต้องการอะไร ถ้าแปลงให้เห็นชัดๆก็คือ

intent: Ask for price
product: Nike ZoomX Vaporfly NEXT%

ชัดขนาดนี้แล้วอย่าเสียเวลาเลยครับ ตอนนี้เรารู้แล้วว่าจะเขียน response ราคากลับไปหาได้ยังไง

  1. กลับมาที่ Apps Script เพื่อสร้าง Product model ด้วยความสามารถของ Tamotsu เลยครับ แค่ชี้ไปบอกว่า sheet ชื่ออะไรหลังจากนั้นเราก็สามารถเรียกข้อมูลจาก Product ออกมาได้เหมือน query document ออกมาจาก database เลย

src/product.model.js

const Product = Tamotsu.Table.define({ sheetName: 'Products' });
export default Product;
  1. มาปรับแต่ง src/index.js เพ่ิมเติมเพื่อให้ response กลับไปหา Dialogflow แบบที่มันควรจะเป็นกันหน่อย
//เรียก config และ Product Model
import './sheets.config';
import Product from './product.model';

//เป็นท่ามาตรฐานในการสร้าง JSON Output ของ Apps Script ครับ
const responseJSON = (jsonObject) => {
  return ContentService.createTextOutput(
    JSON.stringify(jsonObject),
  ).setMimeType(ContentService.MimeType.JSON);
};

const doPost = (e) => {
  const data = JSON.parse(e.postData.contents);
  //ตรวจสอบ request ว่ามีข้อมูลที่ต้องการไหม
  if (!data.queryResult) {
    return responseJSON({ fulfillmentText: 'หนูว่ามีปัญหาแล้วอันนี้' });
  }
  const { parameters, intent } = data.queryResult;
  //ตรวจสอบว่า intent เป็นการถามราคาหรือเปล่า (เผื่อมีหลาย intent)
  if (intent.displayName === 'Ask for price') {
    const productName = parameters.product;

    //query เอา product ที่มี name ตรงกับที่ dialogflow ส่งมาให้
    const product = Product.where({ name: productName }).first();

    //สร้าง fulfillment text เพื่อตอบกลับไปที่ dialoflow
    const response = {
      fulfillmentText: `${product.name} ราคา £${product.price} ค่ะ`,
    };

    //ส่งคำตอบกลับไป
    return responseJSON(response);
  }

  //ในการณีที่ไม่เจอ Intent ที่เขียนเอาไว้้
  return responseJSON({ fulfillmentText: 'ไม่เข้าใจค่ะ ลองใหม่อีกทีนะคะ' });
};

global.doPost = doPost;
  1. กลับมาที่ Dialogflow อีกครั้งเพื่อทดสอบครั้งสุดท้าย แต่รอบนี้เพื่อให้เหมือนจริง ผมจะใช้ Web Demo คุยกับ bot ดูบ้าง วิธีเปิดก็ง่ายๆครับไปที่เมนู Integrations แล้วเปิด Web Demo หลังจากเปิดแล้วจะมี popup ขึ้นมาพร้อมกับบอก Url ของ web ให้เราเข้าไปคุยกับ bot ได้ครับ

เปิด Web Demo

  1. แล้ว Bot ก็ตอบราคากลับมาอย่างที่เราตั้งใจไว้ซะที

ทดลองคุยบน Web Demo

ถ้าต้องการเอา bot ตัวนี้ไปทำงานบน chat platform ดังๆเช่น LINE, Facebook, Slack ก็สามารถได้นะครับ ที่เมนู Integrations มีบอกครบทุกอย่างเลย แต่ว่าในแต่ละ platform เราก็จำเป็นต้องเขียน fulfillmentMessage ใน Apps Script ให้ต่างกันออกไปด้วยครับ จากตอนแรกเราส่งไปแค่ fulfillmentText เช่น

//เพ่ิม fulfillment ของ LINE
const response = {
    fulfillmentText: 'ข้อความที่จะตอบกลับแบบปกติ'
    fulfillmentMessages: [
      {
        platform: 'line',
        type: 4,
        payload: {
          line: {
            type: 'text',
            text: 'ข้อความที่จะตอบกลับใน LINE'
          }
        }
      }
    ]
}

เสร็จเรียบร้อยไปแล้วนะครับ สำหรับการทำ chatbot อย่างง่ายโดยใช้ Dialogflow และ Google Apps Script + Google Sheets จากตรงนี้เราสามารถต่อยอดได้ด้วยการเพิ่ม Intent เป็นเรื่องต่างๆได้เท่าที่ database ของเราจะมีข้อมูลไว้ตอบกลับไปได้ครับ ซึ่งจริงๆแล้วนอกจาก GAS แล้วเรายังเอาหลักคิดแบบนี้ไปใช้บน Platform อื่นได้หมดเลยนะครับ บทความนี้แค่ทำมาให้คนงกที่ไม่อยากเสียตังค์ค่า server ได้ลองเล่นกันครับ หวังว่าจะมีประโยชน์กับทุกคนที่สนใจการทำ Bot ไม่มากก็น้อยนะครับ มีคำถามสามารถสอบถามเข้ามาที่ Inbox ของ Facebook Page ได้เลยนะครับ

บทความใกล้เคียง