見出し画像

【LINE × Dify】 AIを使ってLINEの返信を高速化させる方法

はじめに


この記事では、Dify APIGoogleスプレッドシートを組み合わせた、LINE Bot用の効率的な返信管理システムを紹介します。ハッカソンで6時間という短時間で開発したプロトタイプのため、一部機能は未完成でバグも残っていますが、システムの全体像と基本的な仕組みを中心に解説していきます。LINEの返信の効率化に興味のある方の参考になれば幸いです。

主な機能

  • AIが自動で3つの返信候補を生成

  • 返信候補から選択または編集して送信可能

  • 会話履歴をスプレッドシートに自動保存

  • シンプルな管理画面でメッセージを一括管理

実際に開発したフロント画面

実際に、LINEでメッセージを受信した場合に、AIが即時で3個返信部を考えます。ユーザは、その3つの文章を編集/選択する。
その後に、「返信を送信」を選択することによって、実際に返信文を送信します。

システムの仕組み

  1. ユーザーからLINEメッセージを受信

  2. Dify APIが会話履歴を分析し、3つの返信候補を生成

  3. 管理者が返信候補を確認・編集

  4. 選択した返信をLINEに送信

  5. 全てのやり取りをスプレッドシートに記録

この記事の対象者:

  • LINE Botの自動応答機能を強化したい方

  • メッセージ管理を効率化し、手間を減らしたい方

  • Dify、GASやAPIを使って、LINE Botを自分でカスタマイズしたい方

  • AIを使った自動化に興味があるエンジニアや開発者

LINE Developers の設定


LINE Developers での設定

  1. アカウント作成と初期設定

  2. チャネルの作成

    • 「Messaging API」を選択

    • チャネル名、説明、アイコンなどの基本情報を入力

    • 利用規約に同意して作成

  3. アクセストークンの取得

    • チャネル基本設定ページへ移動

    • 「Messaging API設定」タブを選択

    • 「チャネルアクセストークン」セクションで発行

    • 発行されたトークンを保存(後でGASの環境変数として使用)

  4. Webhook設定

    • 「Webhook URL」に、GASのデプロイURLを設定(後でデプロイURLを発行します)

    • 「Webhookの利用」をオンに設定

    • 「LINE Official Account features」で自動応答メッセージをオフに

注意:アクセストークンは重要な認証情報なので、公開されないよう適切に管理してください。

Difyの設定


Difyのワークフロー作成画面

まずは、DifyでAPIを作成します。ワークフロー機能を使い、インプットには会話履歴を、アウトプットには返信候補の文章を3つJSON形式で返す設定にしました。

次に、日程調整のためのGASスクリプトも用意しました。このスクリプトを使って、自分のGoogleカレンダーを参照し、空いている日程を自動で取得します。

function doGet() {
  // カレンダーIDのリスト(固定)
  var calendarIds = [
    'your_mailaddress'
  ];

  // 今日の日付を取得して、1週間の範囲を設定
  var now = new Date();
  var startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  var endTime = new Date(startTime);
  endTime.setDate(endTime.getDate() + 7); // 今日から1週間後

  // freebusy.queryのリクエストボディを作成
  var request = {
    timeMin: startTime.toISOString(),
    timeMax: endTime.toISOString(),
    items: calendarIds.map(function(id) {
      return {id: id};
    })
  };

  // freebusy.queryメソッドを呼び出し
  var response = Calendar.Freebusy.query(request);

  // カレンダーごとの忙しい時間を取得
  var busyTimes = [];
  for (var id in response.calendars) {
    if (!response.calendars[id].errors) {
      var busy = response.calendars[id].busy;
      busyTimes.push(busy);
    }
  }

  // 共通の空き時間を計算
  var commonFreeTimes = getCommonFreeTimes(busyTimes, startTime, endTime);

  // 日本時間に変換してレスポンスを作成
  var freeTimesResponse = commonFreeTimes.map(function(time) {
    return {
      start: convertToJapanTime(time.start).toISOString(),
      end: convertToJapanTime(time.end).toISOString()
    };
  });

  return ContentService.createTextOutput(JSON.stringify(freeTimesResponse)).setMimeType(ContentService.MimeType.JSON);
}

// 共通の空き時間を計算する関数
function getCommonFreeTimes(busyTimesArray, startTime, endTime) {
  var allBusyTimes = [];

  busyTimesArray.forEach(function(busyTimes) {
    busyTimes.forEach(function(busy) {
      allBusyTimes.push({
        start: convertToJapanTime(new Date(busy.start)),
        end: convertToJapanTime(new Date(busy.end))
      });
    });
  });

  allBusyTimes.sort(function(a, b) {
    return a.start - b.start;
  });

  var mergedBusyTimes = [];
  allBusyTimes.forEach(function(busy) {
    if (mergedBusyTimes.length == 0) {
      mergedBusyTimes.push(busy);
    } else {
      var last = mergedBusyTimes[mergedBusyTimes.length - 1];
      if (busy.start <= last.end) {
        last.end = new Date(Math.max(last.end, busy.end));
      } else {
        mergedBusyTimes.push(busy);
      }
    }
  });

  var freeTimes = [];
  var lastEnd = startTime;
  mergedBusyTimes.forEach(function(busy) {
    if (busy.start > lastEnd) {
      freeTimes.push({
        start: new Date(lastEnd),
        end: new Date(busy.start)
      });
    }
    lastEnd = new Date(Math.max(lastEnd, busy.end));
  });
  if (lastEnd < endTime) {
    freeTimes.push({
      start: new Date(lastEnd),
      end: new Date(endTime)
    });
  }

  return freeTimes;
}

// 日本時間に変換する関数
function convertToJapanTime(date) {
  var jstOffset = 9 * 60; // 日本はUTC+9時間
  var localTime = new Date(date.getTime() + jstOffset * 60000);
  return localTime;
}

GASの設定


このコードでは、以下の機能を実装しています:

  1. LINEメッセージの受信と返信候補の生成
    LINEから送信されたメッセージを受け取り、Dify APIを使ってAIが返信候補を3つ生成します。

  2. スプレッドシートへのデータ保存
    受信したメッセージや生成された返信候補をGoogleスプレッドシートに自動で保存します。

  3. 返信の送信
    管理者が選択または編集した返信内容をLINEに送信し、その履歴をスプレッドシートに記録します。

このスクリプトは、GASを使ってLINE BotとDify APIを連携し、効率的な返信管理を実現します。

Code.gs

// 応答メッセージURL
const PUSH_URL = "https://api.line.me/v2/bot/message/push";

// スクリプトプロパティからアクセストークンを取得
const ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN');

// Dify API URL
const DIFY_API_URL = "https://api.dify.ai/v1/workflows/run";

// スクリプトプロパティからDify APIキーを取得
const DIFY_API_KEY = PropertiesService.getScriptProperties().getProperty('DIFY_API_KEY');

// スプレッドシート情報
const SHEET_ID = PropertiesService.getScriptProperties().getProperty('SHEET_ID'); // スクリプトプロパティから取得
const SHEET = SpreadsheetApp.openById(SHEET_ID).getSheetByName('Conversations');

// LINEから送られてきたデータを取得
function doPost(e) {
  try {
    Logger.log('doPost called');
    Logger.log('Event data: ' + JSON.stringify(e));

    // メッセージ受信
    const data = JSON.parse(e.postData.contents).events[0];
    Logger.log('Received data: ' + JSON.stringify(data));

    // ユーザーID取得
    const lineUserId = data.source.userId;
    Logger.log('User ID: ' + lineUserId);

    // メッセージが送られた日付取得
    const date = new Date(data.timestamp);
    const formattedDate = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss');
    Logger.log('Message date: ' + formattedDate);

    // メッセージ内容
    let message = '';

    // 送信されたメッセージの種類を取得
    const postType = data.message.type;
    Logger.log('Message type: ' + postType);

    if (postType === 'text') {
      message = data.message.text;
    } else if (postType === 'image') {
      message = '画像';
    } else {
      message = 'その他のメディア';
    }

    Logger.log('Message content: ' + message);

    // スプレッドシートにデータを保存 (受信メッセージ)
    saveDataToSheet(lineUserId, [message], '受信', formattedDate);

    // テキストメッセージの場合のみDify APIを呼び出す
    if (postType === 'text') {
      // スプレッドシートから会話履歴を取得
      const conversationHistory = getConversationHistory(lineUserId);
      Logger.log('Conversation history: ' + conversationHistory);

      // Dify APIを呼び出して三つの回答を取得
      const replies = callDifyAPI(conversationHistory);
      Logger.log('Dify API replies: ' + JSON.stringify(replies));

      // 返信候補をスプレッドシートに保存
      const replyDate = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss');
      saveReplyCandidatesToSheet(lineUserId, replies, replyDate);
      Logger.log('Replies saved to sheet.');
    }
  } catch (error) {
    Logger.log('Error in doPost: ' + error);
  }
}

// スプレッドシートにデータを保存
function saveDataToSheet(userId, messages, direction, date) {
  Logger.log('saveDataToSheet called with userId: ' + userId + ', messages: ' + messages + ', direction: ' + direction + ', date: ' + date);
  const userName = getUserDisplayName(userId);
  Logger.log('User name: ' + userName);

  try {
    // メッセージを適切な列に配置
    const row = [userId, userName, direction, date];
    for (let i = 0; i < 3; i++) {
      row.push(messages[i] || '');
    }
    SHEET.appendRow(row);
    Logger.log('Data appended to sheet.');
  } catch (error) {
    Logger.log('Error in saveDataToSheet: ' + error);
  }
}

// 返信候補をスプレッドシートに保存
function saveReplyCandidatesToSheet(userId, replies, date) {
  Logger.log('saveReplyCandidatesToSheet called with userId: ' + userId + ', replies: ' + JSON.stringify(replies));
  const userName = getUserDisplayName(userId);
  try {
    // 返信候補を一行にまとめて保存
    const row = [userId, userName, '返信候補', date];
    for (let i = 0; i < 3; i++) {
      row.push(replies[i] || '');
    }
    SHEET.appendRow(row);
    Logger.log('Reply candidates appended: ' + JSON.stringify(replies));
  } catch (error) {
    Logger.log('Error in saveReplyCandidatesToSheet: ' + error);
  }
}

// ユーザーのプロフィール名取得
function getUserDisplayName(userId) {
  try {
    Logger.log('getUserDisplayName called with userId: ' + userId);
    const url = 'https://api.line.me/v2/bot/profile/' + userId;
    const userProfile = UrlFetchApp.fetch(url, {
      'headers': {
        'Authorization': 'Bearer ' + ACCESS_TOKEN,
      },
    });
    const displayName = JSON.parse(userProfile.getContentText()).displayName;
    Logger.log('Display name: ' + displayName);
    return displayName;
  } catch (error) {
    Logger.log('Error in getUserDisplayName: ' + error);
    return 'Unknown';
  }
}

// スプレッドシートから会話履歴を取得
function getConversationHistory(userId) {
  Logger.log('getConversationHistory called with userId: ' + userId);
  const data = SHEET.getDataRange().getValues();
  const conversation = [];

  try {
    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      if (row[0] === userId && (row[2] === '受信' || row[2] === '送信')) {
        const messages = [row[4], row[5], row[6]].filter(msg => msg);
        conversation.push(...messages); // メッセージを追加
      }
    }

    const history = conversation.join('\n');
    Logger.log('Constructed conversation history: ' + history);
    return history;
  } catch (error) {
    Logger.log('Error in getConversationHistory: ' + error);
    return '';
  }
}

// Dify APIに会話履歴を送信して三つの結果を取得
function callDifyAPI(conversationHistory) {
  Logger.log('callDifyAPI called with conversationHistory: ' + conversationHistory);
  const headers = {
    'Authorization': 'Bearer ' + DIFY_API_KEY,
    'Content-Type': 'application/json'
  };

  const payload = {
    'inputs': {"contents": conversationHistory},
    'response_mode': 'blocking',
    'user': 'line-user-' + Math.random().toString(36).substr(2, 9)
  };

  const options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(payload),
    'muteHttpExceptions': true
  };

  try {
    Logger.log('Calling Dify API');
      const response = UrlFetchApp.fetch(DIFY_API_URL, options);
      const responseText = response.getContentText();
      const responseCode = response.getResponseCode();

      Logger.log("Dify API Response Code: " + responseCode);
      Logger.log("Dify API Response Text: " + responseText);

      if (responseCode === 200) {
        const responseBody = JSON.parse(responseText);
        Logger.log('Dify API response body: ' + JSON.stringify(responseBody));

      // outputs.text を取得し、それがJSON文字列であるためパース
      const outputsText = responseBody.data.outputs.text;
      Logger.log('Dify API outputs.text: ' + outputsText);

      const messagesObj = JSON.parse(outputsText);
      Logger.log('Parsed messages object: ' + JSON.stringify(messagesObj));

        const messages = [];
      messages.push(messagesObj.message1 || 'Dify APIから有効な応答がありません。');
      messages.push(messagesObj.message2 || 'Dify APIから有効な応答がありません。');
      messages.push(messagesObj.message3 || 'Dify APIから有効な応答がありません。');
        Logger.log('Messages extracted: ' + JSON.stringify(messages));
        return messages;
      } else {
        Logger.log('Dify API呼び出しエラー。');
      return ['Dify API呼び出しエラー'];
    }
  } catch (error) {
    Logger.log('Error in callDifyAPI: ' + error);
    return ['APIエラーが発生しました'];
  }
}

// フロントエンドから会話データを取得
function getConversations() {
  Logger.log('getConversations called');
  const data = SHEET.getDataRange().getValues();
  const conversations = [];

  try {
    // データをユーザーごとにまとめる
    const groupData = {};
    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const userId = row[0];
      const userName = row[1];
      const direction = row[2]; // '受信', '送信', '返信候補'
      const date = row[3];
      const message1 = row[4];
      const message2 = row[5];
      const message3 = row[6];

      if (!groupData[userId]) {
        groupData[userId] = {
          groupId: userId,
          groupName: userName,
          history: [],
          replies: [],
          latestDate: null,
          latestDirection: null
        };
        Logger.log('New groupData created for userId: ' + userId);
      }

      // メッセージを統合
      const messages = [message1, message2, message3].filter(msg => msg);

      // dateを文字列に変換
      const dateString = date instanceof Date ? Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss') : date;
      const dateObj = new Date(dateString);

      if (direction === '受信' || direction === '送信') {
        messages.forEach(message => {
          groupData[userId].history.push({ user: direction === '受信' ? userName : '私', message: message, date: dateString });
          Logger.log('Added to history: ' + message);

          // 最新のメッセージを確認
          if (!groupData[userId].latestDate || dateObj > new Date(groupData[userId].latestDate)) {
            groupData[userId].latestDate = dateString;
            groupData[userId].latestDirection = direction;
          }
        });
      } else if (direction === '返信候補') {
        // 返信候補を追加
        messages.forEach(message => {
          groupData[userId].replies.push({ message: message, date: dateString });
          Logger.log('Added to replies: ' + message);
        });
      }
    }

    // 各ユーザーのデータを処理
    for (const key in groupData) {
      const userData = groupData[key];

      // メッセージを日時でソート
      userData.history.sort((a, b) => new Date(a.date) - new Date(b.date));

      // 返信候補を最新のものに絞る
      if (userData.replies.length > 0) {
        // 返信候補を日時でソート(新しい順)
        userData.replies.sort((a, b) => new Date(b.date) - new Date(a.date));

        // 最新の返信候補の日時を取得
        const latestReplyDate = userData.replies[0].date;

        // 最新の返信候補のみを取得
        userData.replies = userData.replies.filter(reply => reply.date === latestReplyDate).map(reply => reply.message);

        // 最大3つまで表示
        userData.replies = userData.replies.slice(0, 3);
      }

      // 最新のメッセージが '送信' の場合、返信候補を表示しない
      if (userData.latestDirection === '送信') {
        userData.replies = [];
      }

      // conversationsに追加
      conversations.push(userData);
      Logger.log('Conversation added for userId: ' + key);
    }

    // 全体の会話を最新日時順(新しいものが上)にソート
    conversations.sort((a, b) => new Date(b.latestDate) - new Date(a.latestDate));

    Logger.log('Total conversations prepared: ' + conversations.length);
    Logger.log('Conversations data: ' + JSON.stringify(conversations)); // デバッグ用
    return conversations;
  } catch (error) {
    Logger.log('Error in getConversations: ' + error);
    return [];
  }
}

// フロントエンドからの返信送信を処理
function sendReplyToLine(replyData) {
  Logger.log('sendReplyToLine called with replyData: ' + JSON.stringify(replyData));
  for (let i = 0; i < replyData.length; i++) {
    const data = replyData[i];
    const userId = data.groupId;
    const replyText = data.replyText;

    Logger.log('Processing reply for userId: ' + userId);

    // プッシュメッセージを送信
    sendPushMessage(userId, replyText);

    // 送信したメッセージをスプレッドシートに保存
    const date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss');
    saveDataToSheet(userId, [replyText], '送信', date);
  }
}

// プッシュメッセージの送信
function sendPushMessage(to, message) {
  Logger.log('sendPushMessage called with to: ' + to + ', message: ' + message);
  const headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + ACCESS_TOKEN
  };

  const postData = {
    "to": to,
    "messages": [
      {
        "type": "text",
        "text": message
      }
    ]
  };

  const options = {
    "method": "POST",
    "headers": headers,
    "payload": JSON.stringify(postData),
    "muteHttpExceptions": true
  };

  try {
    const response = UrlFetchApp.fetch(PUSH_URL, options);
    Logger.log('Push message sent. Response code: ' + response.getResponseCode());
    Logger.log('Response body: ' + response.getContentText());
  } catch (error) {
    Logger.log('Error in sendPushMessage: ' + error);
  }
}

function doGet() {
  Logger.log('doGet called');
  try {
    return HtmlService.createTemplateFromFile('Index').evaluate();
  } catch (error) {
    Logger.log('Error in doGet: ' + error);
    return ContentService.createTextOutput('An error occurred.');
  }
}

Index.html

<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <style>
    /* CSSスタイル */
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f0f2f5;
      margin: 0;
      padding: 0;
    }

    .container {
      max-width: 800px;
      margin: 50px auto;
      background-color: #ffffff;
      padding: 30px;
      border-radius: 10px;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
    }

    .conversation {
      margin-bottom: 40px;
    }

    .group-name {
      font-size: 24px;
      font-weight: bold;
      color: #333333;
      margin-bottom: 10px;
    }

    .conversation-history {
      background-color: #e9edf0;
      padding: 15px;
      border-radius: 8px;
      margin-bottom: 20px;
      max-height: 200px;
      overflow-y: auto;
    }

    .message {
      margin-bottom: 10px;
    }

    .message strong {
      color: #555555;
    }

    .reply-candidates {
      margin-bottom: 15px;
    }

    .reply-candidate {
      display: flex;
      align-items: flex-start;
      padding: 10px;
      border-radius: 8px;
      margin-bottom: 10px;
      border: 1px solid #ccc;
      cursor: pointer;
      transition: background-color 0.2s, box-shadow 0.2s;
    }

    .reply-candidate:hover {
      background-color: #e2e8ec;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    .reply-candidate.selected {
      background-color: #cfe2f3;
      border: 2px solid #1a73e8;
    }

    .reply-candidate input[type="radio"] {
      margin-right: 10px;
      margin-top: 6px;
    }

    .reply-candidate textarea {
      width: 100%;
      padding: 8px;
      border: none;
      resize: vertical;
      font-size: 14px;
      font-family: inherit;
    }

    .buttons {
      display: flex;
      gap: 10px;
    }

    .send-button,
    .send-all-button {
      display: inline-block;
      padding: 10px 20px;
      background-color: #1a73e8;
      color: #ffffff;
      border-radius: 5px;
      text-decoration: none;
      font-weight: bold;
      cursor: pointer;
      transition: background-color 0.2s;
    }

    .send-button:hover,
    .send-all-button:hover {
      background-color: #1666c1;
    }
  </style>
</head>

<body>
  <div class="container">
    <!-- 会話リスト -->
    <div id="conversation-list">
      <!-- ここにJavaScriptで会話を動的に追加します -->
    </div>
    <!-- 一括送信ボタン -->
    <div>
      <a class="send-all-button" href="#" onclick="sendAllReplies()">全ての返信を送信</a>
    </div>
  </div>
  <script>
    // 選択された返信を保持するオブジェクト
    var selectedReplies = {};

    // サーバーから会話データを取得
    function loadConversations() {
      console.log('Loading conversations...');
      google.script.run
        .withSuccessHandler(function (conversations) {
          if (!conversations) {
            console.error('Conversations is null or undefined.');
            alert('会話データの取得に失敗しました。サーバーからデータが取得できませんでした。');
            return;
          }
          console.log('Conversations loaded:', conversations);
          displayConversations(conversations);
        })
        .withFailureHandler(function (error) {
          console.error('Error loading conversations:', error);
          alert('会話データの取得に失敗しました。エラー: ' + error.message);
        })
        .getConversations();
    }

    // 会話を表示
    function displayConversations(conversations) {
      console.log('Displaying conversations...');
      var list = document.getElementById('conversation-list');
      list.innerHTML = '';
      if (!conversations || conversations.length === 0) {
        console.log('No conversations to display.');
        list.innerHTML = '<p>表示する会話がありません。</p>';
        return;
      }
      for (var i = 0; i < conversations.length; i++) {
        var conv = conversations[i];
        console.log('Conversation:', conv);
        var div = document.createElement('div');
        div.className = 'conversation';

        var repliesHtml = '';
        if (conv.replies.length > 0) {
          repliesHtml = `
            <div class="reply-candidates" data-group-id="${conv.groupId}">
              ${conv.replies.map((reply, index) => `
                <label class="reply-candidate">
                  <input type="radio" name="reply-${conv.groupId}" onclick="selectReply('${conv.groupId}', this)">
                  <textarea oninput="updateReply('${conv.groupId}', ${index}, this.value)">${reply}</textarea>
                </label>
              `).join('')}
            </div>
            <div class="buttons">
              <a class="send-button" href="#" onclick="sendReply('${conv.groupId}', this)">返信を送信</a>
            </div>
          `;
        }

        div.innerHTML = `
          <div class="group-name">${conv.groupName}</div>
          <div class="conversation-history">
            ${conv.history.map(msg => `<div class="message"><strong>${msg.user}:</strong> ${msg.message}</div>`).join('')}
          </div>
          ${repliesHtml}
        `;
        list.appendChild(div);
      }
      console.log('Conversations displayed.');
    }

    // ページ読み込み時に会話をロード
    window.onload = loadConversations;

    // 返信候補を選択
    function selectReply(groupId, inputElement) {
      console.log('Selecting reply for groupId:', groupId);
      // その会話内の全ての返信候補から選択状態を解除
      var conversationDiv = inputElement.closest('.conversation');
      var candidates = conversationDiv.querySelectorAll('.reply-candidate');
      candidates.forEach(function (candidate) {
        candidate.classList.remove('selected');
      });
      // 選択された候補にクラスを追加
      var candidate = inputElement.closest('.reply-candidate');
      candidate.classList.add('selected');
      // 選択された返信を記録
      var textarea = candidate.querySelector('textarea');
      selectedReplies[groupId] = textarea.value;
      console.log('Selected reply:', selectedReplies[groupId]);
    }

    // 返信文が編集されたときに更新
    function updateReply(groupId, index, newValue) {
      console.log('Updating reply for groupId:', groupId, 'index:', index, 'newValue:', newValue);
      // もしこの返信候補が選択されていたら、selectedRepliesも更新
      var selectedRadio = document.querySelector(`input[name="reply-${groupId}"]:checked`);
      if (selectedRadio) {
        var candidate = selectedRadio.closest('.reply-candidate');
        var textarea = candidate.querySelector('textarea');
        selectedReplies[groupId] = textarea.value;
        console.log('Updated selected reply:', selectedReplies[groupId]);
      }
    }

    // 個別返信を送信
    function sendReply(groupId, button) {
      console.log('Sending reply for groupId:', groupId);
      if (!selectedReplies[groupId]) {
        alert('返信候補を選択してください。');
        return;
      }
      var replyText = selectedReplies[groupId];
      var replyData = [{ groupId: groupId, replyText: replyText }];
      console.log('Reply data:', replyData);
      google.script.run
        .withSuccessHandler(function () {
          console.log('Reply sent successfully for groupId:', groupId);
          alert('返信を送信しました。');
          // 選択状態をリセット
          delete selectedReplies[groupId];
          var conversationDiv = button.closest('.conversation');
          var candidates = conversationDiv.querySelectorAll('.reply-candidate');
          candidates.forEach(function (candidate) {
            candidate.classList.remove('selected');
            candidate.querySelector('input[type="radio"]').checked = false;
          });
          // 会話リストを再読み込み
          loadConversations();
        })
        .withFailureHandler(function (error) {
          console.error('Error sending reply:', error);
          alert('返信の送信に失敗しました。');
        })
        .sendReplyToLine(replyData);
    }

    // 全ての返信を送信
    function sendAllReplies() {
      console.log('Sending all replies...');
      var replyData = [];
      for (var groupId in selectedReplies) {
        replyData.push({ groupId: groupId, replyText: selectedReplies[groupId] });
      }
      if (replyData.length === 0) {
        alert('少なくとも一つの返信候補を選択してください。');
        return;
      }
      console.log('All reply data:', replyData);
      google.script.run
        .withSuccessHandler(function () {
          console.log('All replies sent successfully.');
          alert('送信完了');
          // 選択状態をリセット
          selectedReplies = {};
          var candidates = document.querySelectorAll('.reply-candidate');
          candidates.forEach(function (candidate) {
            candidate.classList.remove('selected');
            candidate.querySelector('input[type="radio"]').checked = false;
          });
          // ページをリロード
          location.reload();
        })
        .withFailureHandler(function (error) {
          console.error('Error sending all replies:', error);
          alert('返信の送信に失敗しました。');
        })
        .sendReplyToLine(replyData);
    }
  </script>
</body>

</html>

プロパティには、以下の環境変数を設定しました:

  • ACCESS_TOKEN: LINEで発行されたアクセストークン

  • DIFY_API_KEY: Difyから発行されたAPIキー

  • SHEET_ID: 使用するGoogleスプレッドシートのID

これらを使って、LINE APIやDify APIへの接続、スプレッドシートへのデータ保存を行います。

終わりに


今回は、LINEの返信を高速化するツールについて紹介しました。まだ開発途中で一部の機能やエラー修正が必要ですが、このツールが正常に動作すれば、特に不動産営業の方のように、毎日多くの時間をLINEの返信に費やしている方に大きなメリットを提供できる可能性があります。ツールを導入することで、返信作業の時間を大幅に削減し、より効率的な業務運営が可能になります。

さらに、Difyの拡張機能を活用すれば、レストランの提案など、さまざまな自動化タスクにも対応可能です。

LINEの返信を効率化したい方は、ぜひお気軽にお問い合わせください。

お仕事の依頼・相談について


提供できるサービス

私は、以下のようなシステム開発とコンテンツ作成を幅広くサポートしています。OpenAI API・ファインチューニングをはじめとするさまざまな技術を活用し、お客様のニーズに合わせたソリューションを提供します。

  • 自動化システムの構築:AIを利用したカスタマーサポート、FAQシステム、チャットボットなど、業務効率化を図るためのシステムを構築します。ニーズに応じて最適なツールや技術を組み合わせて対応します。

  • GPTs/Dify開発:OpenAIの技術を活用し、カスタムGPTモデルやDifyを使用したシステム開発をサポートします。

  • コンテンツ作成:AIを活用したコンテンツ作成を支援します。ブログ記事、マーケティング資料、教育コンテンツなど、さまざまな分野でのコンテンツを作成します。

詳しくはこちら:

案件のご相談はこちらから

これらの技術やサービスに興味がある方、または具体的なプロジェクトのご相談がある方は、ぜひお気軽にご連絡ください。お客様のニーズに合った最適なソリューションを提供いたします。

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

この記事が参加している募集