ヘルプデスクGPTの作り方と実践ガイド - 自動応答とFAQ管理

 今回はヘルプデスクのチャットボットのように自動で回答してくれるチャットボットをGPTsで作成してみました。自動応答だけだとよく見かけるチャットボットと大差ないので、FAQなどを自動でメンテナンスしてくれるFAQ管理機能も付けてみました。

実際作ってみると、これがちゃんとできてしまえばもうヘルプデスクに人要らないのでは?と思うくらい凄いと思いましたので参考になると幸いです。



ヘルプデスクGPTsの全体概要

まず、利用者側のチャットボットと管理者側のチャットボットとでIFを分けてみました。

全部をまとめようとするとGPTsを作るのが大変なので、それぞれ別にすることでシンプルにしようとしています。ヘルプデスクGPTの概要図は以下のようなイメージ図です。


全体概要

大抵のヘルプデスクなどでは、お問い合わせの一覧的なものがあるかと思いますので、QA一覧はそういった管理表を想定しています。

FAQ一覧はよくある質問などのFAQの一覧等を想定しています。

Googleスプレッドシートへの操作はチャットボット側から操作する際は、Google Apps Scriptを利用して操作します。

管理者側から操作する際もGoogle Apps Scriptを利用して操作しています。これは後程紹介しますが、ChatGPTのAIの能力を活かすためです。普通にスプレッドシートを開いてそのまま利用してもいいと思います。

今回はGPTsの構成の部分の知識にファイルをアップロードするのではなく、何故Googleスプレッドシートを使うのかと言うと、知識の部分は人の手でメンテナンスする必要があるからです。また、知識の部分にアップロードできるファイルの数にも上限があったりするようです。この2つの部分を効率化するためにも今回はGPTs内部ではなく、外部にデータを持たせることにしています。



GPTsの動作

では、早速に作ってみたGPTsの動作から見ていきましょう。今回はIT関係のヘルプデスクを想定してQAやFAQのサンプルデータを作成してます。


利用者側GPTsの動作

利用者が任意の質問をすると、FAQやこれまでのQAから回答を探して、回答してくれます。ここまでだとこれまでの古のチャットボットと変わらないじゃないかと思うかもしれませんが、GPTsはインターネット検索することができますので、FAQやQAに回答が無ければ、インターネット上の一般的な知識を利用しても回答をしてくれます。やっぱりChatGPT使うならChatGPT自体の能力も活用したいですよね!

以下のような感じで動きます。

まずはユーザが問い合わせるところからです。まずは以下のような感じで問い合わせせるとします。すると、スプレッドシートのQA一覧にお問い合わせ内容を記録するとともに、回答をFAQや過去のQAの履歴から検索します。今回はFAQにもQAにも回答が無かったので、ChatGPT側の知識でいくつか追加情報をユーザに求めてヒアリングしようとしています。今回はインターネット検索していないですが、たまにインターネット検索することもあります。

利用者側GPTs動作1

ちなみに、この時のFAQ一覧は以下の状態です。

FAQ一覧1


QA一覧は以下の状態です。No.12に今回のユーザの問い合わせ内容が記録されていますね。


利用者側GPTs動作2

では、言われたとおりに追加情報を伝えてみましょう。実はFAQにブラウザに関する情報を入れていたので、今回は聞いてくれなかったですが、ブラウザの情報も提供してみることにします。すると、FAQには具体的な回答は無いとは言うものの、Firefoxは動作保証がされていない旨の情報を提供してくれました!GPTさんやりますね!!

利用者側GPTs動作3

このチャットのタイミングでApps Scriptへ通信しようとしてますが、これはユーザから提供された内容を基に質問に追加情報を付加してQA一覧のお問い合わせ内容を更新する処理です。この処理後、スプレッドシートのNo.12のお問い合わせ内容は以下の通りFirefoxを利用している旨の情報が付加されています。こうやってユーザに自動でヒアリングしてくれてどんどん情報を付加していってくれるととっても便利ですよね!


利用者側GPTs動作4


では、解決したことにしてみましょう。すると以下のように今後のためにどうやって解決したのかを聞いてくれます。


そして、解決方法を教えてもらったらQA一覧へ回答内容を入力してくれます。解決後のQA一覧の内容は以下の状態です。

利用者側GPTs動作6

ここまで自動でやってくれると相当便利ではないでしょうか!私も探り探りできるのかな?と思ってやってみてましたが、かなりChatGPTが賢くてあっさりとやりたいところまで全てできてしまいました。

回答を自動で記録してくれるということは、同様の質問が別のユーザから来たとしても次回は回答できてしまうということです!

では、別のユーザが問い合わせしてきたとして、新しいチャットを開いて似たような問い合わせをしてみましょう。

利用者側GPTs動作7
はい、もうGPT先生天才過ぎます。次からすぐ回答してくれるようになりました。これはヘルプデスクの回答精度がみるみる上がっていきそうですね!

こちらの問い合わせも上手に回答してくれたので、解決したことにしましょう。

利用者側GPTs動作8

「ありがとうございました。」などの不要な言葉が入っていると、ChatGPT側で判断して、以下のように不要なワードは削除してQA一覧に記録してくれます。賢いですね。

利用者側GPTs動作9


利用者側のGPTsの動作は以上です。ここまででかなりChatGPTの凄さを体感しましたが、本当に恐ろしく凄いのはここからです。では、次は管理者側のGPTsの動作を見ていきましょう!



管理者側GPTsの動作

管理者側のGPTsの機能としては、FAQ一覧のメンテナンス機能と未回答一覧の取得機能を作ってみました。

人間がやるには非常に面倒なFAQのメンテナンスですが、GPT先生はそんな簡単にやってしまうのでしょうか!?やってみましょう。

まずは、更新前のFAQ一覧は先ほどと同様に以下の通りです。


このFAQ一覧を先ほどのQA一覧の内容を基に更新をかけます。チャットはおまじないのように「FAQメンテナンス」と唱えればいいだけにしました。「FAQメンテナンス」と伝えるとFAQ一覧が以下のような感じに更新されます。凄い!!!!

管理者側GPTs動作2

このFAQ一覧のメンテナンスですが、QA一覧にあってFAQ一覧に足りない分を補っているだけかと思ったら大間違い。GPTs側に追加で指示を記述したのですが、個別対応となるような問い合わせは除外するように指定してます。そのため、FAQにあるべきではないシステム復旧依頼のような問い合わせについては、FAQに追加されてません。けっこう何度もこのメンテナンス処理をしてみましたが、復旧依頼の問い合わせについては、一度も追加されたケースは見かけませんでした。

もちろんChatGPT側で判断していい具合に更新をかけるので、毎回若干更新内容は異なります。ここは上手く指示するなり人間のチェックで補正をかけるなりで対応が必要なところでしょう。

しかし、FAQ更新のドラフトがこんなに簡単に出来上がり、かつ利用者側のチャットボットに即時反映されるのであれば、今までの手間は何だったんだというくらいに圧倒的に楽になるのではないでしょうか!

ただ、こちらのFAQメンテナンスボットですが、なぜかユーザへの回答では以下のようにエラーと返してしまいます。私の実装が若干不足しているだけだと思いますが、メインとなるスプレッドシートの更新まできちんと意図した通りできているので、時間ももったいないしここで諦めました。

管理者側GPTs動作1


また、おまけで管理者っぽく未回答の状況を確認できる機能の方も作ってます。こちらは「未回答一覧取得」と唱えると動作してくれます。


こうして全体の状況などを参照できると管理者としてはやりやすいかと思います。これができればChatGPTと一緒に回答を検討して回答案を作成したりできるようになると思うので、いろいろ活用の幅が広がりそうですよね。



利用者側GPTsの作り方

では本題のGPTsの作り方を説明します。利用者側のGPTsの構成(GPTの設定)は以下の通りです。

利用者側GPTs構成1
利用者側GPTs構成2


指示の内容

  1. 以下の入力パターンに応じて回答するボットです。
  2. #対応パターン1
  3. 以下の回答段取りでユーザへ回答してください。
  4. ##回答段取り
  5. 1.ユーザから問い合わせがあったら、ユーザの質問内容をスプレッドシートに追加してください。
  6. 2.以下の「回答検索段取り」で回答を取得してください。
  7. 3.回答を見つけたら'回答更新'のリクエストをスプレッドシートへ送信してください。
  8. 4.ユーザへ回答してください。「回答検索段取り」で回答を取得し、質問にマッチする回答案が無い場合は、スプレッドシートに追加した結果の氏名、カテゴリ、お問い合わせ内容、回答希望日を回答の上、ユーザに少々お時間を頂くように連絡してください。また、他に提供可能な情報が無いかいくつか選択肢を提示してヒアリングを行ってください。
  9. 5.ユーザが情報提供してくれた場合には、ヒアリング内容を基に'質問更新'を実施してスプレッドシートに提供された情報を付加して問い合わせ内容を更新してください。
  10. ##回答検索段取り
  11. 1.スプレッドシートのFAQ一覧から、回答案を取得してください。
  12. 2.FAQ一覧内に質問にマッチする回答案が無い場合は、スプレッドシートのQA一覧を検索し、最も近い回答案を取得してください。
  13. 3.QA一覧内に質問にマッチする回答案が無い場合は、ウェブ参照にてインターネットを検索し、最も近い回答案を取得してください。
  14. #対応パターン2
  15. ユーザから別の回答案、もしくは別の調査項目が欲しい旨の要望があった場合は、以下の「追加対応手順」に従い対応してください。
  16. ##追加対応手順
  17. 1.ヒアリング内容を基に'質問更新'を実施してスプレッドシートに追加した問い合わせ内容を更新してください。
  18. 2.ヒアリングを行って質問へマッチする回答案が見つかった場合は回答案をスプレッドシートへ'回答更新'します。
  19. 3.回答更新後、更新した回答案をユーザへ回答をしてください。
  20. #対応パターン3
  21. 対応パターン1の対応後、別の質問をされたら、再度対応パターン1の通りに対応してくしてください。
  22. #対応パターン4
  23. ユーザが問題を自己解決した場合は、以下の「解決方法確認手順」に従い対応してください。
  24. ##解決方法確認手順
  25. 1.ユーザにどうのようにしたら解決したかを確認する。今後のサービス向上のために必要である旨を添えてください。
  26. 2.ユーザに確認した解決方法を'回答更新'を実行することで、回答を更新する。

ChatGPTへの指示は、パターンの考え方や手順のみとし、それぞれの手順の詳細等は考え方だけを指示することで、ChatGPTに考えさせます。いろいろな詳細事項を考えるところが一番面倒なところなので、その一番面倒な部分をAIに考えさせるように作りこむことで全体を設計します。


利用者側GPTsのActionのスキーマは以下の通りです。

  1. {
  2.   "openapi": "3.1.0",
  3.   "info": {
  4.     "title": "スプレッドシートデータの検索・更新・追加",
  5.     "description": "ユーザの問い合わせの状況に応じてスプレッドシートデータの追加・回答更新・取得・質問更新を行います。",
  6.     "version": "v1.0.0"
  7.   },
  8.   "servers": [
  9.     {
  10.       "url": "https://script.google.com"
  11.     }
  12.   ],
  13.   "paths": {
  14.     "/macros/s/AKfycbwSacdMPMciSGO0J85mPwJ43j-9iamODzPGQyWkYaB7T9_QipCPi-eRFm_SP8KYenVB/exec": {
  15.       "get": {
  16.         "description": "ユーザから問い合わせがあった場合は、問い合わせ内容の'追加'のリクエストを送信します。回答案を取得する場合は、'取得'のリクエストを送信します。ユーザへの回答後に回答をスプレッドシートへ更新する場合は、'回答更新'のリクエストを送信します。質問の更新をする場合は、'質問更新'のリクエストを送信します。",
  17.         "operationId": "operation sheet",
  18.         "parameters": [
  19.           {
  20.             "name": "action",
  21.             "in": "query",
  22.             "description": "'追加' or '取得' or '回答更新' or '質問更新'",
  23.             "required": true,
  24.             "schema": {
  25.               "type": "string"
  26.             }
  27.           },
  28.           {
  29.             "name": "sheet",
  30.             "in": "query",
  31.             "description": "回答案を取得するシート名で、'FAQ' or 'QA'",
  32.             "required": true,
  33.             "schema": {
  34.               "type": "string"
  35.             }
  36.           },
  37.           {
  38.             "name": "timestamp",
  39.             "in": "query",
  40.             "description": "タイムスタンプがこのスプレッドシートのIDの代わりになります。'追加'時は空文字を送信します。タイムスタンプの形式は、'yyyy/MM/dd hh:mm:ss'で、ユーザが入力した時のJSTです。'取得'、'回答更新'、'質問更新'時は最初に質問を追加した時にgoogle apps scriptから返却されたレスポンスのタイムスタンプを送信します。",
  41.             "required": true,
  42.             "schema": {
  43.               "type": "string"
  44.             }
  45.           },
  46.           {
  47.             "name": "name",
  48.             "in": "query",
  49.             "description": "氏名",
  50.             "required": true,
  51.             "schema": {
  52.               "type": "string"
  53.             }
  54.           },
  55.           {
  56.             "name": "category",
  57.             "in": "query",
  58.             "description": "主にシステム名になるカテゴリです。",
  59.             "required": true,
  60.             "schema": {
  61.               "type": "string"
  62.             }
  63.           },
  64.           {
  65.             "name": "content",
  66.             "in": "query",
  67.             "description": "お問い合わせ内容。対応パターン2の場合は、最初の質問内容にユーザからヒアリングした内容を付加して値をお問い合わせ内容をセットします。",
  68.             "required": true,
  69.             "schema": {
  70.               "type": "string"
  71.             }
  72.           },
  73.           {
  74.             "name": "deadline",
  75.             "in": "query",
  76.             "description": "回答希望日。フォーマットは、'yyyy/MM/dd'",
  77.             "required": true,
  78.             "schema": {
  79.               "type": "string"
  80.             }
  81.           },
  82.           {
  83.             "name": "answer",
  84.             "in": "query",
  85.             "description": "ユーザへ回答した内容",
  86.             "required": true,
  87.             "schema": {
  88.               "type": "string"
  89.             }
  90.           }
  91.         ],
  92.         "deprecated": false
  93.       }
  94.     }
  95.   },
  96.   "components": {
  97.     "schemas": {
  98.       "NameResponse": {
  99.         "type": "object",
  100.         "properties": {
  101.           "name": {
  102.             "type": "string"
  103.           },
  104.           "category": {
  105.             "type": "string"
  106.           },
  107.           "content": {
  108.             "type": "string"
  109.           },
  110.           "deadline": {
  111.             "type": "string"
  112.           },
  113.           "answer": {
  114.             "type": "string"
  115.           },
  116.           "QAdata": {
  117.             "type": "string"
  118.           }
  119.         }
  120.       }
  121.     }
  122.   }
  123. }

こちらは、APIのパラメータ値でApps Script側で処理を分岐をさせるので、それぞれのパラメータの役割をきちんと教える必要があります。ここは確実に間違えないように具体的な値を指示する必要があります。

また、データ型も間違えるとApps Script側でエラーになるので、データフォーマットも具体的に指定します。ChatGPTへの指示も、何をする部分なのかに応じて指示の抽象度を変えて指示していくことがコツになると思っています。


Actionには一番最初の全体概要に示した通り、google Apps Scriptを使っています。Apps Scriptの利用者側のコードは以下の通りです。

QABot.gs

  1. function doGet(e) {
  2.   var action = e.parameter.action;
  3.   if (action == '追加') {
  4.     return addAction(e);
  5.   } else if (action == '取得') {
  6.     return getData(e);
  7.   } else if (action == '回答更新') {
  8.     return updateAnswer(e);
  9.   } else if (action == '質問更新') {
  10.     return updateQuestion(e);
  11.   } else {
  12.     return ContentService.createTextOutput(
  13.       JSON.stringify({ "error": "Invalid Action." })
  14.     ).setMimeType(ContentService.MimeType.JSON);
  15.   }
  16. }
  17. function addAction(e) {
  18.   // リクエストからパラメータを取得
  19.   var addtimestamp = Utilities.formatDate( new Date(), 'Asia/Tokyo', 'yyyy/MM/dd hh:mm:ss');
  20.   var addname = e.parameter.name;
  21.   var addcategory = e.parameter.category;
  22.   var addcontent = e.parameter.content;
  23.   var adddeadline = e.parameter.deadline;
  24.   // スプレッドシートデータの取得
  25.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('QA');
  26.   var lastRow = sheet.getLastRow();
  27.   if (lastRow > 1) { // データがある場合
  28.     var lastIdCell = sheet.getRange(lastRow, 1).getValue(); // 最後の行のIDを取得
  29.     lastId = parseInt(lastIdCell, 10);
  30.   }
  31.   sheet.appendRow([addtimestamp, addname, addcategory, addcontent, adddeadline]);
  32.   return ContentService.createTextOutput(
  33.     JSON.stringify({ "timestamp": addtimestamp, "name": addname, "category": addcategory, "content": addcontent, "deadline": adddeadline, "answer": "", "QAdata": "" })
  34.   ).setMimeType(ContentService.MimeType.JSON);
  35. }
  36. function getData(e) {
  37.   // リクエストからパラメータを取得
  38.   var targetsheet = e.parameter.sheet;
  39.   // スプレッドシートの準備
  40.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName(targetsheet);
  41.   var data = sheet.getDataRange().getValues();
  42.   // スプレッドシートのデータを返却
  43.   return ContentService.createTextOutput(
  44.     JSON.stringify({ "timestamp": "", "name": "", "category": "", "content": "", "deadline": "", "answer": "", "QAdata": data })
  45.   ).setMimeType(ContentService.MimeType.JSON);
  46. }
  47. function updateAnswer(e) {
  48.   // リクエストからパラメータを取得
  49.   var searchtimestamp = e.parameter.timestamp;
  50.   var updateanswer = e.parameter.answer;
  51.   // スプレッドシートデータの取得
  52.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('QA');
  53.   var data = sheet.getDataRange().getValues();
  54.   // スプレッドシートを検索
  55.   for (var i = 1; i < data.length; i++) { // 2行目から開始
  56.     if (Utilities.formatDate(data[i][0], "JST", "yyyy/MM/dd HH:mm:ss") === searchtimestamp.toString()) {
  57.       sheet.getRange(i + 1, 6).setValue(updateanswer);
  58.       var responseanswer = sheet.getRange(i + 1, 6).getValues();
  59.       // 見つかった場合、JSONとして返す
  60.       return ContentService.createTextOutput(
  61.         JSON.stringify({ "timestamp": "", "name": "", "category": "", "content": "", "deadline": "", "answer": responseanswer, "QAdata": "" })
  62.       ).setMimeType(ContentService.MimeType.JSON);
  63.     }
  64.   }
  65.   
  66.   // 一致するデータが見つからない場合
  67.   return ContentService.createTextOutput(
  68.     JSON.stringify({ "error": "回答を記録する対象の質問が見つかりませんでした。" })
  69.   ).setMimeType(ContentService.MimeType.JSON);
  70. }
  71. function updateQuestion(e) {
  72.   // リクエストからパラメータを取得
  73.   var searchtimestamp = e.parameter.timestamp;
  74.   var updatecontent = e.parameter.content;
  75.   // スプレッドシートデータの取得
  76.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('QA');
  77.   var data = sheet.getDataRange().getValues();
  78.   // スプレッドシートを検索
  79.   for (var i = 1; i < data.length; i++) { // 2行目から開始
  80.     if (Utilities.formatDate(data[i][0], "JST", "yyyy/MM/dd HH:mm:ss") === searchtimestamp.toString()) {
  81.       sheet.getRange(i + 1, 4).setValue(updatecontent);
  82.       var responseContent = sheet.getRange(i + 1, 4).getValues();
  83.       // 見つかった場合、JSONとして返す
  84.       return ContentService.createTextOutput(
  85.         JSON.stringify({ "timestamp": "", "name": "", "category": "", "content": responseContent, "deadline": "", "answer": "", "QAdata": "" })
  86.       ).setMimeType(ContentService.MimeType.JSON);
  87.     }
  88.   }
  89.   
  90.   // 一致するデータが見つからない場合
  91.   return ContentService.createTextOutput(
  92.     JSON.stringify({ "error": "更新対象の質問が見つかりませんでした。" })
  93.   ).setMimeType(ContentService.MimeType.JSON);
  94. }


管理者側GPTsの作り方

管理者側のGPTsの構成は以下の通りです。

管理者側GPTs構成1
管理者側GPTs構成2


指示の内容

  1. 以下の入力パターンに応じて対応するボットです。
  2. #対応パターン1
  3. 「FAQメンテナンス」とユーザが指示したら、以下の「メンテナンス段取り」でFAQデータをメンテナンスします。
  4. ##「メンテナンス段取り」
  5. 1.FAQ一覧を取得します。取得した内容は表示不要です。
  6. 2.QA一覧を取得します。取得した内容は表示不要です。
  7. 3.「FAQ一覧の更新の考え方」に基づいてFAQ一覧を更新し、'FAQ更新'処理を実行する。FAQのデータ形式は変更しない。FAQの更新内容はユーザへ確認及び表示せずに'FAQ更新'処理を実行します。
  8. ###FAQ一覧の更新の考え方
  9. 1.FAQ一覧に掲載されておらず、QA一覧にて頻出の質問は追加する。
  10. 2.FAQ一覧に掲載されている回答よりも、もっと良い回答がQA一覧にあれば、FAQの回答をその回答に更新する。
  11. 3.個別の質問・対応内容と考えられるQAについては掲載しない。
  12. #対応パターン2
  13. 「未回答一覧取得」とユーザが指示したら、QA一覧を取得し、未回答分の質問を出力します。
  14. 未回答件数が10件未満の場合は、すべての質問の一覧を出力します。
  15. 未回答件数が10件以上ある場合は、未回答の件数を出力し、任意の10件の質問の一覧を出力します。

一番メインとなるFAQ一覧の更新のやり方については、考え方のレベルまでを指示します。具体的にきちんと指示するのであれば、それは通常のプログラム等にて処理すればよいことになります。あくまでAIにやってほしいことは、抽象的な内容から具体的な操作に落とし込む面倒なところをやってほしいので、その面倒な部分をChatGPTに考えさせます。


管理者側GPTsのActionのスキーマは以下の通りです。

  1. {
  2.   "openapi": "3.1.0",
  3.   "info": {
  4.     "title": "QA管理者用機能",
  5.     "description": "FAQもしくはQAの'取得'、'FAQ更新'、'未回答一覧取得'を行う",
  6.     "version": "v1.0.0"
  7.   },
  8.   "servers": [
  9.     {
  10.       "url": "https://script.google.com"
  11.     }
  12.   ],
  13.   "paths": {
  14.     "/macros/s/AKfycbwSacdMPMciSGO0J85mPwJ43j-9iamODzPGQyWkYaB7T9_QipCPi-eRFm_SP8KYenVB/exec": {
  15.       "post": {
  16.         "description": "FAQかQAの一覧取得の場合は、'取得'のリクエストを送信します。FAQ一覧を更新する場合は、'FAQ更新'のリクエストを送信します。未回答一覧を取得する場合は、'未回答一覧取得'のリクエストを送信します。",
  17.         "operationId": "operation sheet",
  18.         "parameters": [
  19.           {
  20.             "name": "action",
  21.             "in": "query",
  22.             "description": "'取得' or 'FAQ更新' or '未回答一覧取得'",
  23.             "required": true,
  24.             "schema": {
  25.               "type": "string"
  26.             }
  27.           },
  28.           {
  29.             "name": "sheet",
  30.             "in": "query",
  31.             "description": "FAQ一覧を取得する場合は、'FAQ'をセットし、QA一覧を取得する場合は'QA'をセットする。",
  32.             "required": true,
  33.             "schema": {
  34.               "type": "string"
  35.             }
  36.           },
  37.           {
  38.             "name": "faqdata",
  39.             "in": "query",
  40.             "description": "FAQ一覧を更新するための更新後のFAQ一覧の文字列型の配列データをセットする。セットするFAQデータのサンプルは、次のフォーマットで、各要素全て文字列です。#フォーマット:[[\"a\",\"b\",\"c\",\"d\"],[\"e\",\"f\",\"g\",\"h\"]]。ヘッダ行は削除する。",
  41.             "required": false,
  42.             "schema": {
  43.               "type": "string"
  44.             }
  45.           }
  46.         ],
  47.         "deprecated": false
  48.       }
  49.     }
  50.   },
  51.   "components": {
  52.     "schemas": {
  53.       "NameResponse": {
  54.         "type": "object",
  55.         "properties": {
  56.           "responsedata": {
  57.             "type": "string"
  58.           }
  59.         }
  60.       }
  61.     }
  62.   }
  63. }

こちらは、APIのパラメータで分岐をさせるので、それぞれのパラメータの役割をきちんと教える必要があります。特に、今回のApps Scriptは文字列で分岐する設計になっているので、ここは確実に間違えないように具体的な値を指示する必要があります。


Apps Scriptの管理者側のコードは以下の通りです。

QAmaintenance.gs

  1. function doPost(e) {
  2.   var action = e.parameter.action;
  3.   if (action == '取得') {
  4.     return getDataForMaintenance(e);
  5.   } else if (action == 'FAQ更新') {
  6.     return updateFaq(e);
  7.   } else if (action == '未回答一覧取得') {
  8.     return getUnAnsweredData(e);
  9.   } else {
  10.     return ContentService.createTextOutput(
  11.       JSON.stringify({ "error": "Invalid Action." })
  12.     ).setMimeType(ContentService.MimeType.JSON);
  13.   }
  14. }
  15. function getDataForMaintenance(e) {
  16.   // リクエストからパラメータを取得
  17.   var targetsheet = e.parameter.sheet;
  18.   // スプレッドシートデータの取得
  19.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName(targetsheet);
  20.   var data = sheet.getDataRange().getValues();
  21.   // スプレッドシートのデータを返却
  22.   return ContentService.createTextOutput(
  23.     JSON.stringify({"responsedata": data })
  24.   ).setMimeType(ContentService.MimeType.JSON);
  25. }
  26. function updateFaq(e) {
  27.   // リクエストからパラメータを取得
  28.   var faqStringData = e.parameter.faqdata;
  29.   var faqArray = JSON.parse(faqStringData);
  30.   // スプレッドシートの準備
  31.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('FAQ');
  32.   // 既存のデータを取得
  33.   var existingData = sheet.getDataRange().getValues();
  34.   // Noをキーにしてデータをマップに変換
  35.   var dataMap = {};
  36.   for (var i = 0; i < faqArray.length; i++) {
  37.     dataMap[faqArray[i][0]] = faqArray[i];
  38.   }
  39.   // 既存データを更新
  40.   for (var j = 0; j < existingData.length; j++) {
  41.     var no = existingData[j][0];
  42.     if (dataMap[no]) {
  43.       existingData[j] = dataMap[no];
  44.       delete dataMap[no]; // 更新済みのデータは削除
  45.     }
  46.   }
  47.   // 新しいデータを追加
  48.   for (var key in dataMap) {
  49.     existingData.push(dataMap[key]);
  50.   }
  51.   // 更新したデータをスプレッドシートに反映
  52.   sheet.getRange(1, 1, existingData.length, 4).setValues(existingData);
  53.   return ContentService.createTextOutput(
  54.     JSON.stringify({"responsedata": "FAQ一覧のメンテナンスが完了しました。" })
  55.   ).setMimeType(ContentService.MimeType.JSON);
  56. }
  57. function getUnAnsweredData(e) {
  58.   // スプレッドシートデータの取得
  59.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('QA');
  60.   var data = sheet.getDataRange().getValues();
  61.   // 未回答のデータを格納する配列
  62.   var unansweredFAQs = [];
  63.   
  64.   // 未回答のデータを抽出
  65.   for (var i = 0; i < data.length; i++) {
  66.     var row = data[i];
  67.     if (row[5] === "") { // 回答列が空の場合
  68.       unansweredFAQs.push(row);
  69.     }
  70.   }
  71.   // スプレッドシートのデータを返却
  72.   return ContentService.createTextOutput(
  73.     JSON.stringify({"responsedata": unansweredFAQs })
  74.   ).setMimeType(ContentService.MimeType.JSON);
  75. }


※利用者側のApps Scriptは管理者側のApps Scriptと同一プロジェクトにしてます。そのため、こちらはPostメソッドでリクエストを受けるAPIになっておりますが、単なるPostの実験ですので、気にしないでください。



まとめ

やれるかな~と思って実際作ってみると、思った以上にChatGPTが賢く、これはさらに活用の幅が広がるなと思いました。チャットなどでやり取りできる仕事などはどんどんAIに置き換えられていきそうな感じですね。

作成にあたってのコツも私なりにいくつか記載してみましたので、参考となりましたら幸いです。

ポイントとしては、以下の通りと考えます。

  • 考え方だけを指示するだけでChatGPTは概ね上手く考えて動いてくれる。
  • AIにどこの部分をを考えてほしいのかを明確にして設計する。

また何か面白そうな使い方を思いついたら記事を書きたいと思いますので、次回お会いしましょう。

今回の記事については、一回くらいはGPTsを作ったことがある人向けの内容になっていると思いますので、そもそもGPTsってどう作るんだっけ?という人は、以下のサイトなどを参考に一度ご自身のGPTを作成してみてください。


参考サイト






GPTとActionsの連携方法を徹底解説 - 効率的な構築ガイド

 GPTsにてActionsが使えるようになってからActionsを使ったサンプルを元にいくつか真似して自分用のGPTsを作ってみましたが、どの記事も一番ポイントになる「どの情報を元にChatGPTはActionsに値を渡すの?」が分からないままでした。ここについては未だかなりブラックボックス状態ですが、使ってみた経験を基にイメージを理解できるような内容をお伝えできる記事になればと思います。

この記事ではかなりポイントを絞ってお伝えします。その他の情報は参考サイトのリンクを掲載いたしますので、そちらをご確認ください。


結論

それなりに説明が長くなるので、まずは結論からお話します。

この記事で言いたいことは、「Description(指示)に書く内容でGPTsをコントロールすることを意識してGPTsは作れ!」ということです。大事なことなので言い換えて説明すると、GPTsの構成のDescription(指示)も、ActionsのDescription(指示)もDescriptionに書くこと次第でChatGPT側が判断する内容が変わるので、Descriptionに書くことがほぼ全てだということです。

これさえ意識してネット上のGPTsのサンプルなどを見ていくと、かなり理解しやすくなるものと考えます。これに気づいてから私もActionsの応用版がどんどん作成していけるようになりました。

では、以下詳細な説明をしていきます。


説明に使うGPTsの概要

今回はGoogleスプレッドシートを操作してくれるbotを参考に説明します。ユーザが指示した内容を基にスプレッドシートの検索・更新・追加ができるbotです。

また、隠し機能として、天気を聞くと今日の東京の天気を回答してくれます。(この隠し機能もこの後の説明においてそれなりに重要)

ChatGPTがどうやってアクションなどを判断するのかを説明するために、Actionsに2アクション入れています。

作成にあたっては以下のサイトを参考にしておりますので、同様のものを作ってみたい方はまずはこちらの記事を参考にしてください。




GPTsの構成

GPTsの構成1

GPTsの構成2

Description(指示)は以下の通りです。

  1. 以下の入力パターンに応じて回答するボットです。
  2. #対応パターン1
  3. ユーザーから質問を受けたら、入力された番号から氏名を検索してください。
  4. #対応パターン2
  5. ユーザから指定の番号の氏名の更新依頼があったら、番号を検索して氏名を更新してください。
  6. #対応パターン3
  7. ユーザから氏名の追加依頼があったら、ユーザが指定した氏名を追加してください。
  8. 追加したら追加した結果のID、苗字、氏名を教えてください。
  9. #対応パターン4
  10. ユーザから天気に関する質問があったら、天気を回答してください。
  11. #対応パターン5
  12. ユーザから大阪の天気を質問されたら、今日の東京の天気を取得して、その返却された気温をスプレッドシートのfirstnameに追加してください。


Actionsの内容

google apps scriptへのアクション

  1. {
  2.   "openapi": "3.1.0",
  3.   "info": {
  4.     "title": "スプレッドシートデータの検索・更新・追加",
  5.     "description": "ユーザの指示に応じてスプレッドシートデータの検索・更新・追加を行います。",
  6.     "version": "v1.0.0"
  7.   },
  8.   "servers": [
  9.     {
  10.       "url": "https://script.google.com"
  11.     }
  12.   ],
  13.   "paths": {
  14.     "/macros/s/{ビルドID}/exec": {
  15.       "get": {
  16.         "description": "ユーザの検索、更新、追加の依頼に応じてスプレッドシートへリクエストを送信します。",
  17.         "operationId": "operation sheet",
  18.         "parameters": [
  19.           {
  20.             "name": "action",
  21.             "in": "query",
  22.             "description": "'検索' or '更新' or '追加'",
  23.             "required": true,
  24.             "schema": {
  25.               "type": "string"
  26.             }
  27.           },
  28.           {
  29.             "name": "id",
  30.             "in": "query",
  31.             "description": "ID No.",
  32.             "required": true,
  33.             "schema": {
  34.               "type": "string"
  35.             }
  36.           },
  37.           {
  38.             "name": "firstname",
  39.             "in": "query",
  40.             "description": "firstname",
  41.             "required": true,
  42.             "schema": {
  43.               "type": "string"
  44.             }
  45.           },
  46.           {
  47.             "name": "lastname",
  48.             "in": "query",
  49.             "description": "lastname",
  50.             "required": true,
  51.             "schema": {
  52.               "type": "string"
  53.             }
  54.           }
  55.         ],
  56.         "deprecated": false
  57.       }
  58.     }
  59.   },
  60.   "components": {
  61.     "schemas": {
  62.       "NameResponse": {
  63.         "type": "object",
  64.         "properties": {
  65.           "id": {
  66.             "type": "string"
  67.           },
  68.           "lastname": {
  69.             "type": "string"
  70.           },
  71.           "firstname": {
  72.             "type": "string"
  73.           }
  74.         }
  75.       }
  76.     }
  77.   }
  78. }


open-meteoへのアクション

  1. {
  2.   "openapi": "3.1.0",
  3.   "info": {
  4.     "title": "東京の天気取得",
  5.     "description": "東京の天気を取得する。",
  6.     "version": "v1.0.0"
  7.   },
  8.   "servers": [
  9.     {
  10.       "url": "https://api.open-meteo.com"
  11.     }
  12.   ],
  13.   "paths": {
  14.     "/v1/forecast": {
  15.       "get": {
  16.         "description": "ユーザから東京以外の場所を聞かれても必ず東京の天気を取得して回答します。東京以外の場所の天気を聞かれた場合はボケとして、堂々と東京の天気を回答します。",
  17.         "operationId": "GetCurrentWeather",
  18.         "parameters": [
  19.           {
  20.             "name": "latitude",
  21.             "in": "query",
  22.             "description": "緯度",
  23.             "required": true,
  24.             "schema": {
  25.               "type": "string"
  26.             }
  27.           },
  28.           {
  29.             "name": "longitude",
  30.             "in": "query",
  31.             "description": "経度",
  32.             "required": true,
  33.             "schema": {
  34.               "type": "string"
  35.             }
  36.           },
  37.           {
  38.             "name": "current_weather",
  39.             "in": "query",
  40.             "description": "現在の天気か否か",
  41.             "required": true,
  42.             "schema": {
  43.               "type": "string"
  44.             }
  45.           },
  46.           {
  47.             "name": "timezone",
  48.             "in": "query",
  49.             "description": "タイムゾーン",
  50.             "required": true,
  51.             "schema": {
  52.               "type": "string"
  53.             }
  54.           }
  55.         ],
  56.         "deprecated": false
  57.       }
  58.     }
  59.   },
  60.   "components": {
  61.     "schemas": {
  62.       "Response": {
  63.         "type": "object",
  64.         "properties": {
  65.           "current_weather": {
  66.             "type": "string"
  67.           }
  68.         }
  69.       }
  70.     }
  71.   }
  72. }


Googleスプレッドシートの内容

googleスプレッドシートの内容



GoogleAppsScriptの内容

  1. function doGet(e) {
  2.   var action = e.parameter.action;
  3.   if (action == '検索') {
  4.     return searchAction(e);
  5.   } else if (action == '更新') {
  6.     return updateAction(e);
  7.   } else if (action == '追加') {
  8.     return addAction(e);
  9.   } else {
  10.     return ContentService.createTextOutput(
  11.       JSON.stringify({ "error": "Invalid Action." })
  12.     ).setMimeType(ContentService.MimeType.JSON);
  13.   }
  14. }
  15. function searchAction(e) {
  16.   // リクエストからパラメータを取得
  17.   console.log("start GPTs Custom Actions API test");
  18.   console.log("e: " + JSON.stringify(e));
  19.   var searchQuery = e.parameter.id;
  20.   console.log("e.parameter.id: " + e.parameter.id);
  21.   // 4桁の数字かどうかをチェック
  22.   if (!searchQuery || !/^\d{4}$/.test(searchQuery)) {
  23.     console.log("bad format: " + searchQuery);
  24.     return ContentService.createTextOutput(
  25.       JSON.stringify({ "error": "Invalid request. Please provide a 4-digit number." })
  26.     ).setMimeType(ContentService.MimeType.JSON);
  27.   }
  28.   // スプレッドシートの準備
  29.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('{シート名}');
  30.   var data = sheet.getDataRange().getValues();
  31.   console.log("format OK");
  32.   // スプレッドシートを検索
  33.   for (var i = 1; i < data.length; i++) { // 2行目から開始
  34.     if (data[i][0].toString() === searchQuery) {
  35.       console.log("hit: " + i);
  36.       console.log("last: " + data[i][1]);
  37.       console.log("first: " + data[i][2]);
  38.       // 見つかった場合、JSONとして返す
  39.       return ContentService.createTextOutput(
  40.         JSON.stringify({ "id": data[i][0], "lastname": data[i][1], "firstname": data[i][2] })
  41.       ).setMimeType(ContentService.MimeType.JSON);
  42.     }
  43.   }
  44.   console.log("not found");
  45.   // 一致するデータが見つからない場合
  46.   return ContentService.createTextOutput(
  47.     JSON.stringify({ "error": "No data found for the provided number." })
  48.   ).setMimeType(ContentService.MimeType.JSON);
  49. }
  50. function updateAction(e) {
  51.   // リクエストからパラメータを取得
  52.   console.log("start GPTs Custom Actions API update test");
  53.   console.log("e: " + JSON.stringify(e));
  54.   var searchId = e.parameter.id;
  55.   console.log("e.parameter.id: " + e.parameter.id);
  56.   var firstname = e.parameter.firstname;
  57.   console.log("e.parameter.firstname: " + e.parameter.firstname);
  58.   var lastname = e.parameter.lastname;
  59.   console.log("e.parameter.lastname: " + e.parameter.lastname);
  60.   // 4桁の数字かどうかをチェック
  61.   if (!searchId || !/^\d{4}$/.test(searchId)) {
  62.     console.log("bad format: " + searchId);
  63.     return ContentService.createTextOutput(
  64.       JSON.stringify({ "error": "Invalid request. Please provide a 4-digit number." })
  65.     ).setMimeType(ContentService.MimeType.JSON);
  66.   }
  67.   // スプレッドシートの準備
  68.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('{シート名}');
  69.   var data = sheet.getDataRange().getValues();
  70.   console.log("format OK");
  71.   // スプレッドシートを検索
  72.   for (var i = 1; i < data.length; i++) { // 2行目から開始
  73.     if (data[i][0].toString() === searchId) {
  74.       console.log("hit: " + i);
  75.       console.log("last: " + data[i][1]);
  76.       console.log("first: " + data[i][2]);
  77.       sheet.getRange(i + 1, 2).setValue(firstname);
  78.       sheet.getRange(i + 1, 3).setValue(lastname);
  79.       var responsefirstname = sheet.getRange(i + 1, 2).getValues();
  80.       var responselastname = sheet.getRange(i + 1, 3).getValues();
  81.       // 見つかった場合、JSONとして返す
  82.       return ContentService.createTextOutput(
  83.         JSON.stringify({ "id": data[i][0], "firstname": responsefirstname, "lastname": responselastname})
  84.       ).setMimeType(ContentService.MimeType.JSON);
  85.     }
  86.   }
  87.   console.log("not found");
  88.   // 一致するデータが見つからない場合
  89.   return ContentService.createTextOutput(
  90.     JSON.stringify({ "error": "No data found for the provided number." })
  91.   ).setMimeType(ContentService.MimeType.JSON);
  92. }
  93. function addAction(e) {
  94.   // リクエストからパラメータを取得
  95.   console.log("start GPTs Custom Actions API add test");
  96.   console.log("e: " + JSON.stringify(e));
  97.   var addLastname = e.parameter.lastname;
  98.   console.log("e.parameter.lastname: " + e.parameter.lastname);
  99.   var addFirstname = e.parameter.firstname;
  100.   console.log("e.parameter.firstname: " + e.parameter.firstname);
  101.   // スプレッドシートの準備
  102.   var sheet = SpreadsheetApp.openById('{スプレッドシートID}').getSheetByName('{シート名}');
  103.   var data = sheet.getDataRange().getValues();
  104.   console.log("format OK");
  105.   var lastRow = sheet.getLastRow();
  106.   var lastId = 0;
  107.   if (lastRow > 1) { // データがある場合
  108.     var lastIdCell = sheet.getRange(lastRow, 1).getValue(); // 最後の行のIDを取得
  109.     lastId = parseInt(lastIdCell, 10);
  110.   }
  111.   var newId = lastId + 1;
  112.   sheet.appendRow([newId, addLastname, addFirstname]);
  113.   return ContentService.createTextOutput(
  114.     JSON.stringify({ "id": newId, "updatelastname": addLastname, "Updatefirstname": addFirstname })
  115.   ).setMimeType(ContentService.MimeType.JSON);
  116. }


各パターンごとの動作と説明

GPTsの構成を見て頂くとわかる通り、どのAPIを使うかなどを指示しなくてもChatGPT側でいい感じに判断してくれて、APIを使い分けてくれます。

では、それぞれどのようなリクエストを送信し、どのようなレスポンスが返ってくれるのでしょうか?

リクエストとレスポンスをどちらも見れるのはプレビューの時のみなので、プレビューで動作確認します。

パターン1

パターン1の結果


こちらは「検索」というようなキーワードを入力せずとも、うまい具合に理解してAPIのactionパラメータに"検索"を入れてくれます。おそらく構成のDescriptionの対応パターンに検索してほしい旨の指示を入力しているからだと思われます。また、ActionsのDescriptionには、"'検索' or '更新' or '追加'"とキーワードを絞った趣旨の記載をしているので、それを汲み取ってより近い検索を選択しているものと推察されます。Descriptionに指示を上手く記載することで、APIに変なパラメータを送信することを防ぐ工夫をすると上手くいくようです。


パターン2

パターン2の結果1
パターン2の結果2

パターン2の結果3

こちらは「更新」というキーワードが入っているので、操作パターンの指定は簡単そうですね。今回も「スプレッドシート」というワードやどのAPIを使えなどの指示は不要です。Descriptionにきちんと記載しておけばChatGPTが良い感じに判断してくれます。このパターンで注目すべきは返り値です。レスポンスの型がちょっと間違って配列に格納されてしまってますが、これを最後ユーザに更新連絡する際も、いい感じにデータを取り出して回答しています。よく凡ミスしがちなデータ型について、あまり意識しなくてもいい感じに取り扱ってくれるのは、プログラミング経験者からすると相当ありがたいことが分かるのではないでしょうか。


パターン3

パターン3の結果1

パターン3の結果2

こちらも操作は追加と指定しているので、操作については簡単です。このパターンで注目すべき点も返り値です。Apps Scriptの返り値にセットしているJSONとActionの返り値に指定しているプロパティを見比べてみてください。これもミスって"lastname"と"firstname"ではなく、"updatelastname"と"Updatefirstname"を返してしまってます。私もこのミスに気づかず作っていたのですが、途中で全然返り値の名前違うのにいい感じに受け取ってることに気づいて、凄すぎ!と驚愕しました。とりあえずGPTsは最初は雑に作っちゃっても大丈夫そうですね。


パターン4

パターン4の結果1
パターン4の結果2

こちらもAPI指定などは不要です。パターン4のようなざっくりした指示内容だけで天気の方のAPIを使うことをChatGPT側が判断してリクエスト送信してくれます。また、GPTsのDescriptionの方には東京やリクエスト値に関することは何も記載していませんが、Actions側のDescriptionには、東京の天気を回答するよう指示しているので、東京の緯度と経度をChatGPT側で考えてリクエストパラメータを送信してくれます。いや~、これまた凄いですね。いい感じに…って言葉が最適ですね。APIのパラメータすらもChatGPTが自分で考えてセットしてくれるんです。これは便利すぎるだろう…w


パターン5





今回は大阪の天気を質問されたので、パターン5になって気温を取得して、続けてスプレッドシートに登録しようとしてます。流石です!ボケっていう指示をしたので、きちんと東京の天気を取得しているにもかかわらず、スプレッドシートには「大阪の天気」って登録しているところがご愛嬌w 回答には東京の気温って回答しているのに。きちんとボケてくれたのでツッコミを入れたら必死に謝ってきましたw


各パターンごとの詳細説明は以上です。各パターンごとの動作は文章では分かりにくそうかと思いましたので動画にしました。

以下の動画を参照してください。



まとめ

上記のDescriptionはかなり雑な指示になっているので、100%安定して動作する感じではありません。しかし、雑に指示した時こそChatGPTの凄さを体感できるのです!こちらの指示の趣旨を良い感じに汲み取って動作してくれるので、まずは雑な指示でChatGPTがどんな理解の仕方(誤解の仕方)をするのかをきちんと確認した上でDescriptionをブラッシュアップしていくといいDescriptionがスムーズに出来上がっていくだろうと考えています。




ヘルプデスクGPTの作り方と実践ガイド - 自動応答とFAQ管理

 今回はヘルプデスクのチャットボットのように自動で回答してくれるチャットボットをGPTsで作成してみました。自動応答だけだとよく見かけるチャットボットと大差ないので、FAQなどを自動でメンテナンスしてくれるFAQ管理機能も付けてみました。 実際作ってみると、これがちゃんとできてし...