4
2

ChatGPTとGASを使ってLINE Botを作った(WIP)

Last updated at Posted at 2024-05-03

背景

先日、BOT AWARDS 2024 ハッカソン@東京に参加し、その際、LINE Botに前日見た断片的な夢を投げると、それを元にストーリーと夢診断をしてくれるLINE Botアプリを作った。
また、今回はスプレッドシートをDBとして使った。
その備忘録。

全体はこちら

やること

LINEアプリ(LIFF)を使ってみる。
LINEとChatGPTを連携し、Botを作る。
メッセージをGASを使ってスプレッドシートに格納する。

LINE DevelopersやOpenAIの登録

LINE DevelopersやOpenAIの登録が済んでいない場合は、それぞれ登録を済ませる。

LINE Developers の登録

こちら参照
https://qiita.com/tatsuya1970/items/34ce62b53162eb69c809#2-line-developers-%E3%81%AE%E7%99%BB%E9%8C%B2%E7%84%A1%E6%96%99

OpenAIの登録

こちらを参照
https://qiita.com/tatsuya1970/items/34ce62b53162eb69c809#4-openai-%E7%99%BB%E9%8C%B2-%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AE%E6%96%B9%E3%81%AF18%E3%83%89%E3%83%AB%E3%81%BE%E3%81%A7%E7%84%A1%E6%96%99

アカウントを作成したら、そこでそれぞれ発行されるAPIキーを利用していきますが、そこらへんについては、いろんな人が解説しているので、省略いたします。

ChatGPTとLINE Botの連携

まずGPTを使ったLINE Botを作成する。
発行されたAPIをenvファイルに記述する。

.env
OPENAI_API_KEY="OpenAI API"
LINE_CHANNEL_SECRET=LINE Messaging API のチャネルシークレット"
LINE_CHANNEL_ACCESS_TOKEN="LINE Messaging API のチャネルアクセストークン"

パッケージをインストール

$ npm install openai
$ npm install @line/bot-sdk express
$ npm install axios

実行プログラム app.js を作成。

app.js
'use strict';

require("dotenv").config();

const { createAndUploadRichMenu } = require('./richMenuSetup');
const { accessSpreadsheet, accessStorySpreadsheet, checkForSimilarDreams, fetchDreamsFromSpreadsheet } = require('./spreadsheet');

const OpenAI = require('openai');
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const express = require('express');
const path = require('path');
const axios = require('axios');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000; // 今回はngrokでローカルサーバーを立ち上げたため

const config = {
  channelSecret: process.env.LINE_CHANNEL_SECRET,
  channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN
};

const app = express();
const client = new line.Client(config);

const sessions = {}; //instead of DB

app.get('/', async (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
}); // 無くても構わないが、LINEアプリの見た目をindex.htmlでデザインすることができるので、今回のハッカソンでは、オリジナルのデザインを作成した
app.use('/liff', express.static(__dirname + '/public'))
app.get('/fetchDreams', async (req, res) => {
  const userId = req.query.userId;
  const dreams = await fetchDreamsFromSpreadsheet(userId);
  res.json(dreams);
});

// GPTを使っているのはこっから下
app.post('/webhook', line.middleware(config), (req, res) => {
  Promise.all(req.body.events.map(event => {
      return handleEvent(event);
  })).then((result) => res.json(result));
});

const questions = [
  "夢であなたはどこにいましたか?",
  "誰が夢に現れましたか?",
  "そこであなたはなにをしていましたか?"
];

async function generateFreudianFeedback(story) {
  const feedback = await generatePsychologicalInterpretation(story); // generate feedback after analyzing keyword
  return feedback;
}

async function generatePsychologicalInterpretation(symbols) {
  const prompt = `Interpret these dream symbols psychologically based on Freud's Dream Interpretation in Japanese(日本語で回答): ${symbols}`;
  const completion = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    messages: [{ role: "system", content: prompt }]
  });
  return completion.choices[0].message.content;
}

async function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
      return Promise.resolve(null);
  }

  const userId = event.source.userId;
  const text = event.message.text;

  await accessSpreadsheet({ UserId: userId, Text: text, Timestamp: new Date().toISOString() });

  if (text === "夢日記をはじめます。") {
    sessions[userId] = {
      userId: userId,
      responses: [],
      questionIndex: 0
    };
    return client.replyMessage(event.replyToken, { type: 'text', text: "夢であなたはどこにいましたか?" });
  }

  if (!sessions[userId]) {
    sessions[userId] = {
      userId: userId,
      responses: [],
      questionIndex: 0  // question index
    };
  }

  const userSession = sessions[userId];

  if (userSession.questionIndex < questions.length) {
    userSession.responses.push(text);  // save user's answer
    userSession.questionIndex++;  // update question index
  }

  if (userSession.questionIndex < questions.length) {
    // next question
    return client.replyMessage(event.replyToken, { type: 'text', text: questions[userSession.questionIndex] });
  } else {
    // generate the story once the questions are completed
    const story = await generateStory(userSession.responses);
    const illustrationUrl = await generateIllustration(story);
    const feedback = await generateFreudianFeedback(story);

    sessions[userId] = null; // reset the session

    await accessStorySpreadsheet({ UserId: userId, Story: story, Timestamp: new Date().toISOString(), Feedback: feedback });
    const similarDreamsCount = await checkForSimilarDreams(story, new Date().toDateString());

    if (illustrationUrl) {
      // send url of text and illustration to LINE
      await client.replyMessage(event.replyToken, [
          { type: 'text', text: story },
          { type: 'image', originalContentUrl: illustrationUrl, previewImageUrl: illustrationUrl },
          { type: 'text', text: `フロイト夢分析:\n\n${feedback}` },
          { type: 'text', text: `あなたの他に、今日同じ夢を見た人が${similarDreamsCount - 1}人いました` },
      ]);
    } else {
        // if it's failed
        return client.replyMessage(event.replyToken, { type: 'text', text: story });
    }
  }
}

async function generateIllustration(prompt) {
  try {
      const response =  await openai.images.generate({
          model: "dall-e-3",
          prompt: prompt,
          n: 1,  // the number of images
          size: "1024x1024"  // size of image
      });
      return response.data[0].url;  // return url of generated image
  } catch (error) {
      console.error('Error generating illustration:', error);
      return null;
  }
}

async function generateStory(responses) {
  if (responses.length < 3) {
      // Error handling
      return "Error: Not enough data to generate a story. Please provide three parts.";
  }
  // Create the prompt based on user's answer
  const prompt = `Create a short story based on these elements in Japanese:\n1. Dream about: ${responses[0]}\n2. With: ${responses[1]}\n3. Doing: ${responses[2]}\n\nStory:`;
  
  const completion = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    messages: [{ role: "system", content: prompt }]
  });
  
  return completion.choices[0].message.content;
}

app.listen(PORT, () => {
  console.log(`Server running at ${PORT}`);
  createAndUploadRichMenu().then(richMenuId => {
      console.log('Rich menu is set up with ID:', richMenuId);
  }).catch(err => {
      console.error('Failed to set up rich menu:', err);
  });
});

Webhook設定(LINE Developers)

LINE Developers
Messaging API設定 > WebhookURL > 編集

ここに先ほどのアドレスをペーストし、末尾に /webhook を付けたすことを忘れないように。(各々作成したAPIに合わせる必要がある)

Screenshot 2024-05-03 at 23.35.31.png

動作テスト

ローカルサーバーで試すためにngrokを使った。

ngrokをインストール

$ npm install -g ngrok

ngrokを起動

$ ngrok http 3000

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2