【LINE BOT】会話の内容をGASで収集してGPTsで分析する(コード全文付き)
背景
最近MacBookのコマンドキーがお亡くなりになって、買い換えるお金もないので、ちょうど先週が締切のLINE Botのハッカソンに参加しました。
コマンドキーが必要な作業が全部できなかったので、大変でした。
「生成AIとLINE」って組み合わせだったんですが、ユーザーからの自由形式の問いかけをトリガーとして、LLMが処理して何かアクションを起こすというのが、大枠なんじゃないでしょうか。
また、GASでGmailとかカレンダーとかの「パーソナライズ」な機能をLINEのアカウントごとに振り分けさせるのが認証の問題で結構難しく、Webhookも一つしか使えないので、制約条件多めですね。
なので、「会話入力」→「回答」→「データ収集」→「データ分析」でユーザーの入力情報を自動収集する方に設計しました。
基本機能
AIアザラシの"むーちゃん"は悩みごとを気軽に相談できる愛らしいChatbotです。LLMのモデルは「Llama3-70b-8192」を利用しており、自然に回答するようにsystemContentを設定しています。(たまに日本語が不自由なので、帰国子女の設定)。
会話の中で、20文字を超える内容は悩み事の情報として、GASでGoogle spreadsheetへ集約されます。Google spreadsheetの処理としては、IDの作成、情報が追加された日時の記載、内容からのカテゴリー選別などが、内容の記載と同時に新規行へ追加されます。
また専用のGPTsを使用することで、集約されたデータに対しての示唆を容易に行うことが可能となります。人々の悩み事のデータからジャンルごとに世の中の傾向を探ることで、新たなサービスへの活用に役立つのではないでしょうか。
主な機能
GAS
function doPost(e) {
if (!e || !e.postData) {
Logger.log('Event data is missing or incorrect.');
return;
}
const cache = CacheService.getScriptCache();
const event = JSON.parse(e.postData.contents).events[0];
let userMessage = event.message.text;
if (userMessage === undefined) {
userMessage = '何かお悩みはありますか?なんでもおっしゃって下さい。';
}
let previousMessages = cache.get('previousMessages');
if (previousMessages) {
previousMessages = JSON.parse(previousMessages);
} else {
previousMessages = [];
}
const systemContent = "あなたは{日本語のみ}で丁寧に悩みを聞いてくれる可愛いアザラシ型のAIアシスタントです。あなたは会話の最後に(キュー)という鳴き声が混じります。あなたは会話の他に以下のスキルを備えています。# 日本語会話スキル # 優しく悩みを聞いてあげるスキル # 可愛く癒してあげるスキル";
previousMessages.unshift({"role": "system", "content": systemContent});
previousMessages.push({"role": "user", "content": userMessage});
cache.put('previousMessages', JSON.stringify(previousMessages), 300);
const props = PropertiesService.getScriptProperties();
const requestOptions = {
method: "post",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + props.getProperty('OPENAI_APIKEY')
},
payload: JSON.stringify({
model: "llama3-70b-8192",
messages: previousMessages
})
};
try {
const response = UrlFetchApp.fetch("https://api.groq.com/openai/v1/chat/completions", requestOptions);
const responseText = response.getContentText();
const json = JSON.parse(responseText);
const text = json.choices[0].message.content.trim();
Logger.log("API Response: " + responseText); // レスポンスをログに記録
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + props.getProperty('LINE_ACCESS_TOKEN'),
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': event.replyToken,
'messages': [{
'type': 'text',
'text': text,
}]
})
});
} catch (error) {
Logger.log('Error sending request: ' + error.toString()); // エラーをログに記録
}
recordToSpreadsheet(event.source.userId, new Date(event.timestamp), userMessage, categorizeMessage(userMessage));
}
function categorizeMessage(message) {
// メッセージが undefined または空文字の場合、デフォルトカテゴリを返す
if (!message) {
return 'その他';
}
var categories = {
'健康': ['病気', '健康', '体調', '医者', '治療', '健康診断'],
'容姿・外見': ['見た目', '容姿', '外見', '美容', 'スタイル', 'コンプレックス'],
'仕事': ['職業', '職場', 'キャリア', '転職', '求人', '労働条件', '取引先'],
'目標達成': ['目標', '成功', '達成', '計画', '進捗', '挑戦'],
'人間関係': ['友人', '関係', 'コミュニケーション', '同僚', 'トラブル', '人付き合い','友達','同級生'],
'恋愛': ['恋愛', 'デート', '彼氏', '彼女', '結婚', '浮気', '不倫', '元カレ', '元カノ'],
'子育・介護': ['子育て', '介護', '育児', '保育園', '老人ホーム', '世話'],
'借金': ['借金', 'お金', '負債', 'ローン', '返済', '金融機関'],
'報酬': ['給料', '報酬', '収入', 'ボーナス', '昇給', '経済状況'],
'教育': ['学校', '勉強', '試験', '教育', '入試', '学習支援'],
'趣味・娯楽': ['趣味', 'ゲーム', '映画', '旅行', 'アウトドア', 'エンターテイメント'],
'精神的な健康': ['ストレス', '不安', 'うつ', 'メンタル', 'リラックス', '心理'],
'法律・規制': ['法律', '訴訟', '規制', '法的問題', '契約', '法律相談'],
'住まい': ['住宅', '引っ越し', '不動産', '賃貸', '住宅ローン', '家探し'],
'テクノロジー': ['スマホ', 'コンピュータ', 'インターネット', 'アプリ', 'デジタル', 'ITトラブル'],
'その他': ['その他', '未分類', '雑多', '特定不能', '一般的な質問']
};
for (var category in categories) {
var keywords = categories[category];
for (var i = 0; i < keywords.length; i++) {
if (message.includes(keywords[i])) {
return category;
}
}
}
return 'その他'; // どのカテゴリーにも当てはまらない場合
}
function generateShortUserId(userId) {
const hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, userId)
.map(byte => byte < 0x10 ? `0${byte.toString(16)}` : `${byte.toString(16)}`)
.join('');
return hash.slice(0, 8); // 先頭8文字を取得
}
function generateShortUserId(userId) {
// userIdがnullまたはundefinedでない場合のみ処理を行う
if (userId) {
const hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, userId)
.map(byte => byte < 0x10 ? `0${byte.toString(16)}` : `${byte.toString(16)}`)
.join('');
return hash.slice(0, 8);
} else {
// userIdがnullまたはundefinedの場合、空文字列を返す
return '';
}
}
function recordToSpreadsheet(userId, timestamp, messageText, category) {
if (!messageText || messageText.length === 0) {
Logger.log('Error: Message text is undefined or empty.');
return;
}
if (messageText.length <= 15) {
Logger.log('Message skipped due to insufficient length: ' + messageText);
return;
}
var formattedDate = Utilities.formatDate(new Date(timestamp), "JST", "yyyy-MM-dd HH:mm:ss");
var spreadsheetUrl = 'https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXX/';
var sheet = SpreadsheetApp.openByUrl(spreadsheetUrl).getActiveSheet();
var shortUserId = generateShortUserId(userId);
// Using Cache Service to reduce redundant spreadsheet checks
var cache = CacheService.getScriptCache();
var cacheKey = shortUserId + formattedDate + messageText;
var cached = cache.get(cacheKey);
if (cached) {
Logger.log('Duplicate entry found in cache and skipped: ' + messageText);
return;
}
// Append data if not found in cache
try {
sheet.appendRow(['', shortUserId, formattedDate, messageText, category]);
cache.put(cacheKey, 'stored', 3600); // Store in cache for 1 hour
} catch (e) {
Logger.log('Error writing to spreadsheet: ' + e.toString());
}
}
GPTs schema
{
"openapi": "3.0.0",
"info": {
"title": "Spreadsheet Content Response API",
"description": "This API interfaces with a specific Google Spreadsheet to retrieve content in HTML format.",
"version": "1.0.0"
},
"servers": [
{
"url": "https://script.google.com",
"description": "Server endpoint for Google Apps Script"
}
],
"paths": {
"/macros/s/xxxxx/exec": {
"get": {
"operationId": "getSheetHtmlView",
"summary": "Returns an HTML view of 'Sheet1'.",
"parameters": [
{
"name": "scriptId",
"in": "path",
"required": true,
"description": "The Google Apps Script ID that hosts the doGet function.",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "HTML output of 'Sheet1'",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"500": {
"description": "Error response if there is an issue accessing the spreadsheet",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
総評
今回初めてLINE botを作りましたが、結構愛らしくて好きです。またLLMがオープンソースモデルを使っているので、実質維持費が無料という点も良いかと思います。
むーちゃんは悩み事相談なのですが、これを何かに特化させた相談にすればもっと有益なデータ取得が可能になるのではないでしょうか。
LINEユーザーとの相性を考えると、法律とか税務とかじゃなくてめちゃくちゃライトなものが良いかなって思いますけどね。
ちなみにもう一つの応募作品はこちら