見出し画像

Chatworkで問い合わせbotを作る方法

(2025/01/15追記)
こちらの記事はChatGPT登場以前に書いたもので、だいぶ古くなったので無料で公開します。
※2025年以降に購入された方は何かしらの手段でご返金しますので、個別にご連絡ください m(__)m

はじめに

このノートは、自分でも作れるけど、少し面倒なコードを提供することで日本中から「車輪の再発明」をなくし、生産性を高めて働き方改革が進むことを目的にしています。エンジニアでなくても、ITに弱くても、誰でも簡単に使えて仕事が捗るスクリプトです。

できること

チャットワークを利用して、問い合わせに自動返信するbotを作成できます。
外部ユーザ向けというよりは、社内からの問い合わせ回答に向いてると思います。

一度導入してしまえば設定は簡単。
スプレッドシートにキーワードと回答を設定し、

画像1

チャットワークでbotに質問すると回答してくれます。

画像2

例えば、社内の『生き字引き』的な存在の人。「あの人に聞いたらなんでも知ってる」という人に使ってほしいです。「○○のやり方を教えて」とか「〇〇ファイルの場所教えて」とか、質問ばかりで自分の仕事が進まない、という人は、問合せbotを作り、「今後はまずbotに質問ください!」と宣言して生産性を上げましょう!

画像3
画像4

特長

・スプレッドシートで追加修正して即反映されるから手間がかからない。
・問い合わせの履歴が自動で残るので、内容を見ながら追加・修正できる。

画像5

用意するもの

・チャットワークアカウント(bot専用アカウントの作成がおすすめ)
・Googleアカウント
・Google Cloud Platformアカウント

作り方

簡単ですが、少し長くなるので、こちらのブログにまとめました。
【GASでつくるChatWorkで問い合わせbot】作成手順

使い方

作成したスプレッドシートで回答とキーワードを設定し、チャットワークで質問を投稿すると回答してくれます。

詳細はこちらのブログに記載しています。
【GASでつくるChatWorkで問い合わせbot】使い方

ご利用上のご注意

このノートは、少しでも多くの方の働き方が改善され、商品・サービスが良くなることを目的に作成しています。
購入すると、下にコードが表示されますので、使い方に記載のとおりご利用ください。

今回のスクリプトでは、Googleの自然言語API(エンティティ分析)を使用しています。月に5,000回までは無料で使用できますが、それ以降1,000件ごとに1ドルがかかりますのでご注意ください。
https://cloud.google.com/natural-language/pricing

購入することによって必ずしもコードの動作を保証するものではありません。Googleやチャットワークの仕様変更により利用できなくなる可能性があります。が、ご相談いただければ速やかに修正を行う予定です。また、よい改善が思いついたら、こちらのnoteも更新していきます。

なお、コードを利用したことによって損害が発生した場合の補償はできかねます。予めご了承ください。
コードはご購入者のみでご利用ください。ご自分で改変するのは自由ですが、コードを他の方に共有・公開などはご遠慮ください。

以上、ご納得いただける方のみご購入ください。
GASを活用してみなさんの業務が改善されることを願っております!

(2019/11/21更新)
Chatworkの仕様変更(返信)に対応しました。
すべてのメンバー宛(TO ALL)メッセージに反応しないよう修正しました。

(2020/2/27更新)
GASの仕様変更(V8エンジン)に対応しました。

(2020/06/05更新)
ユーザ名を取得してログに残すよう修正しました。

手順とコード

こちらの記事を参考にして作成してください。
【GASでつくるChatWorkで問い合わせbot】作成手順

使用するコードは以下のとおりです。
コピペしてご使用ください。

// ------ 初期設定 ここから --------------

// google Cloud Platform API KEY
var API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// Chatwork API Token
var CW_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// 回答の信頼性基準(初期値:20 → 語句が20%以上一致していれば回答させる)
var Threshold = 20;
// 設定用シート名
var SHEET_1_NAME = "シート1";
// ログ用シート名
var SHEET_2_NAME = "シート2";

// ------ 初期設定 ここまで --------------

// メニューを追加
function onOpen() {
 SpreadsheetApp.getUi()
     .createMenu('初期設定')
     .addItem('初期設定を行う', 'setFieldName')
     .addToUi();
}

function setFieldName() {
 var ss = SpreadsheetApp.getActiveSpreadsheet();
 var sheet = ss.getSheetByName(SHEET_1_NAME);
 var fieldNames = new Array();
 fieldNames.push('回答');
 for(var i = 1;i <= 10;i++){
   fieldNames.push('キーワード' + i );
 }
 var fields = new Array();
 fields.push(fieldNames);
 sheet.getRange(1,1,1,fieldNames.length).setValues( fields );
 var logSheet = ss.getSheetByName(SHEET_2_NAME);
 if(!logSheet){
   ss.insertSheet(SHEET_2_NAME);
 }
}

function doPost(e) {
 var json = JSON.parse(e.postData.contents);
 var room_id = json.webhook_event.room_id;
 var event_time = new Date(json.webhook_event_time*1000);
 var from_account_id = json.webhook_event.from_account_id; // 2020/06/05追加
 var user_name = getUserName(room_id, from_account_id); // 2020/06/05追加
 //質問解析
 var content = json.webhook_event.body;
 //if( content.search(/\[To:9999999\]/) != -1 ) return false;
 if( content.search(/\[toall\]/) != -1 ) return false;
 if( content.search(/\[dtext/) != -1 ) return false;
 content = content.replace( /\[rp.*?\n/g, "" ); //旧リプライ削除
 content = content.replace( /\[返信.*?\n/g, "" ); //新リプライ削除
 content = content.replace( /\[To.*?\n/g, "" ); //To削除
 content = content.replace( /\[.*?\]/g, "" ); //タグ削除
 var entities = getEntities(content);
 // 大文字に変換
 entities.forEach( function( value, index, array ) {
   var str = value.replace(/[A-Za-z0-9]/g, function(s) {
     return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
   });
   array[ index ] = str.toUpperCase();
 });
 //回答読込
 var ss = SpreadsheetApp.getActiveSpreadsheet();
 var sheet = ss.getSheetByName(SHEET_1_NAME);
 var ansData = convertSheet2Json(sheet);
 // スコア算定
 for(var i=0; i<ansData.length; i++){
   var total_score = 0;
   entities.forEach(function(entitie) {
     var min_kw_score = 1;
     ansData[i]['keywords'].forEach(function(keyword) {
       //レーベンシュタイン距離
       var score = calcDist(entitie, keyword);
       //標準化
       score = (entitie.length > keyword.length)? score / entitie.length : score / keyword.length;
       if(score < min_kw_score) min_kw_score = score;
     });
     total_score += min_kw_score;
   });
   //entitieの平均
   ansData[i]['score'] = total_score / entities.length;
 }
 // scoreの順にソート(昇順)
 ansData.sort(function(a,b){
   if (a.score < b.score) return -1;
   if (a.score > b.score) return 1;
   return 0;
 });
 // 確度算出
 var reliability = (1-ansData[0].score)*100;
 // 回答文作成
 if( reliability >= Threshold ){
   var message = ansData[0].message;
 } else {
   var message = entities + " に関する回答は見つかりませんでした(^^;)";
 }
 // チャットワークメッセージ送信
 sendMessage(room_id,message);
 // ログを残す
 var logSheet = ss.getSheetByName(SHEET_2_NAME);
 if( logSheet && SHEET_1_NAME != SHEET_2_NAME){
   var fieldNames = [ '日時', '質問', '抽出語句', '信頼性', '回答', 'RoomID', 'ユーザ名' ]; // 2020/06/05修正
   var logData = readSheet(logSheet, fieldNames.length);
   if( !logData ) logData = new Array();
   var logRecord = [event_time.toLocaleString(), content, entities.join(','), reliability, message, room_id, user_name]; // 2020/06/05修正
   logData.unshift(logRecord);
   if(logData.length > 100)logData.splice(100, 1);
   logData.unshift(fieldNames);
   logSheet.getRange(1,1,logData.length,fieldNames.length).setValues( logData );
 }
 return true;
}

// チャットワークにメッセージを送る
function sendMessage(room_id,body){
 var params = {
   headers : {"X-ChatWorkToken" : CW_TOKEN},
   method : "post",
   payload : {
     body : body
   }
 };
 var url = "https://api.chatwork.com/v2/rooms/" + room_id + "/messages";
 UrlFetchApp.fetch(url, params); 
}

// 語句を抽出する
function getEntities(content){ 
 var response = analyzeEntities(content);
 var json = JSON.parse(response.getContentText());
 var entities = new Array();
 for(var i=0; i<json.entities.length; i++) {
   entities.push(json.entities[i].name);
 }
 // 空の場合は元の文字列をそのまま返す
 if(!entities[0]){
   entities[0] = content;
 }
 //Logger.log(entities);
 return entities;
}
// POST API
function analyzeEntities(content){
 var data = {
   'document' : {
     'type' : 'PLAIN_TEXT',
     'language' : 'ja',
     'content' : content
   },
   'encodingType': 'UTF8'
 };  
 var params = {
   'contentType' : 'application/json',
   'method' : 'post',
   'payload' : JSON.stringify(data)
 };
 var url = 'https://language.googleapis.com/v1/documents:analyzeEntities?key=' + API_KEY;
 return UrlFetchApp.fetch(url, params);
}

// シートを読み込んでjson形式で返す
function convertSheet2Json(sheet) {
 
 // シートを読み込んで二次元配列を返す
 var values = readSheet(sheet, 11);

 if( !values ) return false;
 // create json
 var jsonArray = [];
 for(var i=0; i<values.length; i++) {
   var json = new Object();
   if(!values[i][0])break;
   json['message'] = values[i][0];
   json['keywords'] = new Array();
   for(var j=1; j<=10; j++){
     if(!values[i][j])break;
     json['keywords'][j] = values[i][j];
   }
   jsonArray.push(json);
 }
 return jsonArray;
}

// シートを読み込んで二次元配列を返す
function readSheet(sheet, colNum){
 var lastRow = sheet.getLastRow();
 if( lastRow <= 1 ) return false;
 var rowIndex = 2; //最初のデータ行
 var colStartIndex = 1; //最初のデータ列 
 var range = sheet.getRange(rowIndex, colStartIndex, lastRow, colNum);
 var values = range.getValues();
 return values;
}

// レーベンシュタイン距離
function calcDist(a, b) {
 if (a == b) return 0;
 if (a.length == 0) return b.length;
 if (b.length == 0) return a.length;
 var matrix = new Array(a.length + 1);
 for (var i = 0; i <= a.length; i++)
   matrix[i] = new Array(b.length + 1);
 for (var i = 0; i <= a.length; i++)
   matrix[i][0] = i;
 for (var j = 0; j <= b.length; j++)
   matrix[0][j] = j;
 for (var i = 1; i <= a.length; i++) {
   for (var j = 1; j <= b.length; j++) {
     var x = a[i - 1] == b[j -1] ? 0 : 1;
     matrix[i][j] = Math.min(
       matrix[i - 1][j] + 1,
       matrix[i][j - 1] + 1,
       matrix[i - 1][j- 1] + x
     );
   }
 }
 return matrix[a.length][b.length];
}

function test(){
 var room_id = 999999999; // テスト用のルームID
 var body = "こんにちは";
 var webhook = { 
   "webhook_event_time": 1498028130,
   "webhook_event":{
     "room_id": room_id,
     "body": body
   }
 };
 var json = JSON.stringify( webhook );
 var obj = {postData:{contents:json }};
 doPost(obj);
}

// 2020/06/05追加
// ユーザ名を取得する関数
function getUserName(room_id, account_id){
 // ChatWork apiに投げるパラメータを設定
 var params = {
   headers : {"X-ChatWorkToken" : CW_TOKEN},
   method : "get"
 };
 //ルーム一覧を取得するURL
 var url = "https://api.chatwork.com/v2/rooms/" + room_id + "/members";
 //チャットワークAPIエンドポイントからレスポンスを取得
 var strRespons = UrlFetchApp.fetch(url, params);
 // 中身がなかったら終了
 if( strRespons.getContentText() == '' ) return false;
 // レスポンス文字列をJSON形式として解析
 var json = JSON.parse(strRespons.getContentText());
 var userName = "不明";
 json.some(function( members ) {
   if( members.account_id == account_id ){
     userName = members.name;
     return true;
   }
 });
 return userName;
}

いいなと思ったら応援しよう!