プレスリリースやお知らせ、開発ブログ、会社の活動状況、Mattermost・aws・AI等の技術情報などを発信しています。

Mattermost と ChatGPT で 英語学習 Bot を実装してみた!

前回は ChatGPT を気軽に使えるように、Mattermost に ChatGPT を実装し、会話やコード生成を楽しみました。

https://www.d-make.co.jp/blog/2023/03/06/%e3%81%8a%e6%89%8b%e8%bb%bd%ef%bc%81-mattermost-%e3%81%ab-chatgpt-bot-%e3%82%92%e5%ae%9f%e8%a3%85%e3%81%97%e3%81%a6%e3%81%bf%e3%81%9f%ef%bc%81/

今回は 英語学習 を目的に Mattermost に ChatGPT を実装していこうと思います。

筆者は英語が極端に苦手なので、英語が苦手な人の視点から英語学習 Bot の実装をしました。

使用するAPIの概要

gpt-3.5-turbo

https://platform.openai.com/docs/api-reference/chat

前回と同様 ChatGPT で一番性能が高いモデルである gpt-3.5-turbo をメインに使って行きます

{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
        "role": "user",
        "content": "Hello!"
    }
  ]
}

gpt-3.5-turbo はモデルを指定し、メッセージのリストを設定することで、過去の文脈を踏まえた文章を生成してくれます。

role にはいくつか種類がありますが、投稿者を user に Bot が生成した文章を assistant とすることで一般的な会話が楽しめます。

text-davinci-edit-001

https://platform.openai.com/docs/api-reference/edits

今回は text-davinci-edit-001 という古いモデルも使用します。
gpt-3.5-turbo だけでも良いのですが、優秀なモデルであるがゆえに、必要のない文も生成してしまうことがあります。
そのため、文の生成を命令できるモデルも使用します。

{
  "model": "text-davinci-edit-001",
  "input": "What day of the wek is it?",
  "instruction": "Fix the spelling mistakes",
}

input には入力する一文を、モデルが生成する文に対する命令を instruction に記述します。

実装構成

構成図

今回実装した Bot の構成図です。

ChatGPT APIの呼び出しとSpreadsheetとのやり取りを GAS を用いて行っています。

以下のようなフローで実行しています。

英語学習 Bot に追加した機能

英語学習ということで3つの機能を実装することにしました。

  1. 英語での会話機能
  2. 入力にスペルミス、日本語が含まれていた場合の訂正機能
  3. 登録した単語すべてを使った英語文章生成機能

1, 2 は英語で会話する際に毎回実行されます。

3 は事前に単語を登録し、登録した単語で物語風の英文を生成してくれます、覚えたい単語 を登録した文章を読むことで、効率UPとモチベーションを維持した学習ができることを期待します!

実装

Mattermost で Bot 用チャンネルを作成し、 内向きWebhook を作成します

BOT用のチャンネルを用意したら 統合機能 から 内向きWebhook を選択し、各種設定を行います

内向きWebhook の設定では以下を求められるので、それぞれ入力します。

  • タイトル: 任意のタイトル
  • 説明: 任意
  • チャンネル: チャンネルは Bot 用に作ったものを選択します
  • このチャンネルに固定する: 任意
  • ユーザー名: Bot が投稿する際の名前の設定
  • プロフィール画像: Bot が投稿する際の Icon の設定

入力を保存すると URL が発行されますが、GAS のコード内でこの URL を使います。

※システムコンソールから統合機能管理を開き「内向きのウェブフックを有効にする」「外向きのウェブフックを有効にする」を有効にしてください。

※システムコンソールから統合機能管理を開き「統合機能によるユーザー名の上書きを許可する」「統合機能によるプロフィール画像アイコンの上書きを許可する」を有効にしてください。

GAS から コールバックURL を発行します

GAS に以下のコードを書き込みます。

OpenAI の APIキー 等は以下を参考に取得出来ます。

https://auto-worker.com/blog/?p=6988

OpenAPI の APIキー と 内向きWebhook の URL を各変数に割り当てます。

// openAIApi.gs
class OpenAIAPI {
  
  constructor (api_key, organization){
    this.api_key = api_key;
    this.organization = organization;
    this.spreadsheet_id = null;
  }
  
    // 会話生成モデル
  textChatCompletions(messages){
    const url="https://api.openai.com/v1/chat/completions"
    const payload = {
      "model": "gpt-3.5-turbo",
      "messages": messages,
    }
    const options = {
      "headers": {
        'Authorization': 'Bearer ' + this.api_key,
        'Content-Type': 'application/json',
      },
      "payload": JSON.stringify(payload)
    }
    return UrlFetchApp.fetch(url, options);
  }
    
  // payload初期化
  initPayloadEdits(){
    return({
      "model": "text-davinci-edit-001",
      "input": "",
      "instruction": "Fix the spelling mistakes" // スペルミスの修正
    })
  }

  // 文章生成モデル
  textDavinciEdit001Edits(payload) {
    const url = "https://api.openai.com/v1/edits";
    
    const options = {
      "headers": {
        'Authorization': 'Bearer ' + this.api_key,
        'Content-Type': 'application/json',
      },
      "payload": JSON.stringify(payload)
    }
    return UrlFetchApp.fetch(url, options);
  }
  
  // chat履歴の書き込み
  chatHistoryWriter(role, content, sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    spreadSheet.getSheetByName(sheetName).appendRow(
      [new Date(), role, content]
    );
  }

  // chat履歴の読み込み
  chatHistoryReader(sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    const sheetChatHistory = spreadSheet.getSheetByName(sheetName)
    
    try{
      const range = sheetChatHistory.getRange(1, 2, sheetChatHistory.getLastRow(), sheetChatHistory.getLastColumn());
      return range.getValues()
    }catch{
      return []
    }
  }

  chatHistoryClear(sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    const sheetChatHistory = spreadSheet.getSheetByName(sheetName)
    sheetChatHistory.clear()
  }

    // スプレッドシートへの単語登録
  dictionaryAddWord(textList, sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    const now = new Date()
    textList.forEach(i=>{
      spreadSheet.getSheetByName(sheetName).appendRow(
        [now, i]
      );
    })
  }
  
    // スプレッドシート上の単語取得
  dictionaryGetWord(sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    const sheetChatHistory = spreadSheet.getSheetByName(sheetName);
    try{
      const range = sheetChatHistory.getRange(1, 1, sheetChatHistory.getLastRow(), sheetChatHistory.getLastColumn());
      return range.getValues()
    }catch{
      return []
    }
  }

    // スプレッドシート上の単語消去
  dictionaryClear(sheetName){
    const spreadSheet = SpreadsheetApp.openById(this.spreadsheet_id);  
    const sheetChatHistory = spreadSheet.getSheetByName(sheetName);
    sheetChatHistory.clear()
  }
}
// main.gs
// OpenAiのApi情報その他
const API_KEY = 'OpenAIのAPIkey';
const ORGANIZATION = 'OpenAIのORGANIZATION';
const SPREADSHEET_ID = 'スプレッドシートのID';

const MATTERMOST_HOOK_EN = "英会話BotのMattermostの内向きWebhook"
const HISTORY_SHEET_NAME = '履歴のシート名'
const DICTIONARY_SHEET_NAME = '単語登録のシート名'

// mattermostへの送信
function mattermostSendMessage(text, url){
  
  const payload = {
    "text": text,
  }
  const options = {
    "headers": {
      'Content-Type': 'application/json',
    },
    "payload": JSON.stringify(payload)
  }
  UrlFetchApp.fetch(url, options);
}

// 履歴形式からインプット形式へ
function historyToMessages(history, text){
  const inputText = {"role": "user", "content": text}

  const messages = []
  history.forEach(v=>{
    messages.push({"role": v[0], "content": v[1]})
  })  
  messages.push(inputText)
  return messages
}

// APIを実行し、レスポンスをchat表示にフォーマット
function getEditResponseMessage(text, instruction, openAIAPI){
  const payloadEdit = openAIAPI.initPayloadEdits()
  payloadEdit.input = text
  payloadEdit.instruction = instruction
  const textDavinciEdit001Edits = openAIAPI.textDavinciEdit001Edits(payloadEdit)
  const responseJsonEdit = JSON.parse(textDavinciEdit001Edits.getContentText())
  if(responseJsonEdit["choices"][0]["text"] !== ""){
    return "\n\n" + instruction + ": \n" + responseJsonEdit["choices"][0]["text"]
  }
  return ""
}

// APIを実行し、レスポンスのメッセージのみを取得
function getChatResponseMessage(messages, openAIAPI){
  const response = openAIAPI.textChatCompletions(messages)
  const responseJson = JSON.parse(response.getContentText())
  return responseJson["choices"][0]["message"]
}

function enChat(text){
  
  // openai初期化
  const openAIAPI = new OpenAIAPI(API_KEY, ORGANIZATION);
  openAIAPI.spreadsheet_id = SPREADSHEET_ID
  
  if(text.indexOf("履歴消去") === 0){
    openAIAPI.chatHistoryClear(HISTORY_SHEET_ENGLISH_NAME)
    mattermostSendMessage("履歴を消去しました", MATTERMOST_HOOK_EN)
    return
  }

  if(text.indexOf("単語消去") === 0){
    openAIAPI.dictionaryClear(DICTIONARY_SHEET_NAME)
    mattermostSendMessage("単語を消去しました", MATTERMOST_HOOK_EN)
    return
  }

  if(text.indexOf("単語登録") === 0){
    const wordList = text.split('\n')
    wordList.shift()
    openAIAPI.dictionaryAddWord(wordList,DICTIONARY_SHEET_NAME)
    mattermostSendMessage("単語を登録しました", MATTERMOST_HOOK_EN)
    return
  }
  
  if(text.indexOf("文章作成") === 0){
    let word = ""
    const wordList = openAIAPI.dictionaryGetWord(DICTIONARY_SHEET_NAME)
    wordList.forEach(i=>{
      word += i[1] + '\n'
    })
    if (word == ""){
      mattermostSendMessage("単語が登録されていません", MATTERMOST_HOOK_EN)
      return
    }
    text = "Create a story using input words: \n" + word
    
    const messages = historyToMessages([], text)
    const responseMessage = getChatResponseMessage(messages, openAIAPI)

    messages.push({'role':responseMessage['role'],'content':responseMessage['content'] })
    messages.push({'role':'user','content': '日本語に翻訳して\n'+responseMessage['content'] })

    const reResponseMessage = getChatResponseMessage(messages, openAIAPI)
    mattermostSendMessage(responseMessage["content"]+'\n\n' + reResponseMessage['content'], MATTERMOST_HOOK_EN)
    return
  }

  // 履歴読み込み
  const chatHistory = openAIAPI.chatHistoryReader(HISTORY_SHEET_ENGLISH_NAME)
  const messages = historyToMessages(chatHistory, text)

  const responseMessage = getChatResponseMessage(messages, openAIAPI)

  openAIAPI.chatHistoryWriter("user", text, HISTORY_SHEET_ENGLISH_NAME)
  openAIAPI.chatHistoryWriter(responseMessage["role"], responseMessage["content"], HISTORY_SHEET_ENGLISH_NAME)

  let responseLine = responseMessage["content"]

  // スペルミス訂正命令
  responseLine += getEditResponseMessage(messages.slice(-1)[0]["content"], "Correct spelling mistakes and Japanese in English",  openAIAPI)
  mattermostSendMessage(responseLine, MATTERMOST_HOOK_EN)
}

// mattermostからの呼び出し
function doPost(e) {

  const params = JSON.stringify(e);
  const json = JSON.parse(params);
  const contents = json["parameters"];
  const text = contents["text"][0];
  
  enChat(text);
}

GAS にて上記のコードを入力した後、デプロイボタンを押すことでURLが発行されます

※デプロイ時、「アクセスできるユーザー」「全員」としなければ Mattermost 側からのコールバックを受け取れないので注意してください

「英単語を保存するシート」「会話履歴を保存するシート」が必要になるので、事前に作成をしてください

Mattermost で 外向きWebhook の設定をします。

外向きWebhook の設定では以下を求められるので、それぞれ入力します

  • タイトル: 任意のタイトル
  • 説明: 任意
  • チャンネル: チャンネルは Bot 用に作ったものを選択します
  • コンテントタイプ: application/json
  • トリガーワード(1つにつき1行): なし
  • トリガーとなる条件: 最初の単語がトリガーワードと正確に一致する
  • このチャンネルに固定する: 任意
  • コールバックURL(1つにつき1行): GAS のコードデプロイ時の URL を記入
  • ユーザー名: なし
  • プロフィール画像: なし

上記入力後保存します、以上で実装は完了です。

Botと英語学習

英会話と文章訂正機能の利用

英語を教えてと挨拶してみました。

「weak point」 の誤字と日本語で「教えて」と混ぜて見ましたが、しっかり訂正されています、しかし回答を見る限り言いたいことが伝わっているわけではない様子。

もう少し具体性をもたせて会話します。

ゲームを久しぶりにやりたいと思ったのでゲームについて聞いて見ました。間違えていそうな拙い文法で聞いて見ましたが、しっかり答えてくれました。

ゲームを教えてくれるだけでなく、しっかりと説明も入れてくれました。

文法も訂正はされているようですが、補足もしてくれていますね。

英語文章生成機能

はじめに単語を登録します。

単語登録は登録したい単語を一行ごとに入力することで登録出来ます。

「文章作成」入力で登録単語を使った英語文と翻訳した日本語文が両方返却されます。

登録単語以外もたっぷりあるので、良い勉強になりそうですね!

日本語の翻訳をチェックすると、物語として成立した文章を生成してくれたのが分かりますね、ChatGPTすごい!

まとめ

今回は ChatGPT と Mattermost で 英語学習用 Bot を実装しました。

英語学習という目的を持った実装をするのは大変でした、ChatGPT に適当なメッセージを送ると、欲しい返答以上になってしまう・逆に足りないなど、自然言語で命令することの難しさを実感しました。

Mattermost への 英語学習 Bot 実装はこの記事だけでも完結するので、興味のある方は是非試して見てください!

Mattermost導入サービス
  • B!

おすすめ記事リンク