2024年6月29日土曜日

GAS で Claude API を使った LINE bot を作る

先日、GPTGemini を使った LINE bot をそれぞれ作りましたが、今回は Claude の API を使った LINE bot を GAS(Google Apps Script)で作ってみました。

モデルは、Claude 3.5 Sonnet を使っています。プログラミングのお供にめちゃくちゃ便利ですね。


詳解! Google Apps Script完全入門 [第3版]
詳解! Google Apps Script完全入門 [第3版]

by SimpleImageLink


それでは、作り方です。

LINE Messaging API チャンネルの作成とアクセストークンの取得。及び、Claude の API キーの取得は済んでる想定です。

それぞれ、GAS の「プロジェクトの設定」よりスクリプト プロパティとして追加・保存しておきます。プロパティ名は「Line_key」と「Claude_key」としています。

また、スプレッドシートを用意して、「chat」シート、「system」シート、「temperature」シートを作っておきます(サンプル)。(作り方は「GAS で ChatGPT とやりとりできる簡単な LINE bot の作り方」を参照)


★ 機能一覧
  • 文頭に「続き)」を付けることで会話を継続。(例:続き)もっと詳細に教えて
  • 文頭に「昔話!」のように付けることで、スプレッドシートの「system」シートにあらかじめ登録したシステム指示を実行。(例:昔話!きのこ
  • 文頭に「画像)」で画像の処理モード。(例:画像)何ですか? → 画像を送信)
  • 文末に「(厳密」「(創造」で temperature を指定。通常 0.5、厳密 0、創造 1。(例:3行で小噺を作って(創造

コードは以下の通りです。

const spreadsheet = SpreadsheetApp.openById('スプレッドシート ID); // スプレッドシート ID を書き換え

const chatSheet = spreadsheet.getSheetByName('chat');
let chatLastRow = chatSheet.getLastRow();
let chatRange = chatSheet.getRange(2, 1, chatLastRow, 2);

const systemSheet = spreadsheet.getSheetByName('system');
const systemLastRow = systemSheet.getLastRow();
const systemWatchwordRange = systemSheet.getRange(2, 1, systemLastRow - 1, 1);
const systemContentRange = systemSheet.getRange(2, 2, systemLastRow - 1, 1);

const tempSheet = spreadsheet.getSheetByName('temperature');
const tempRange = tempSheet.getRange(1, 1);

function doPost(e) {
  try {
    const event = JSON.parse(e.postData.contents).events[0];
    let prompt = [];
    let replyMessage = '';

    switch(event.message.type) {
      case 'text': // リクエストがテキストの場合
        let messageContent = event.message.text;

        if (messageContent.startsWith('画像)')) {
          // 「画像)」に続いて指示で画像処理
          // 画像処理の場合
          messageContent = messageContent.replace('画像)', '');
          claudeRequestText(messageContent);
          sendLineMessage(event.replyToken, '画像を送信してください'); 
        } else {
          // 画像処理以外の場合
          prompt = claudeRequestText(messageContent);
          replyMessage = getClaudeReply(prompt); // Claude で回答
          setAiMessage('assistant', replyMessage); // AI の返答をスプレッドシートに'role': 'assistant'で格納
          sendLineMessage(event.replyToken, replyMessage);
        }
        break;
      case 'image': // リクエストが画像の場合
        prompt = claudeRequestImage(event.message.id);
        replyMessage = getClaudeReply(prompt); // Claude で回答
        sendLineMessage(event.replyToken, replyMessage);
        break;
      default:
        // リクエストがサポート外のデータ形式の場合
        sendLineMessage(event.replyToken, 'サポート外のデータ形式です');
    }
  } catch {
    // エラー発生時
    sendLineMessage(event.replyToken, '不明なエラーが発生しました');
  }
}

// Claude でリクエストデータ(テキスト)を生成する関数
function claudeRequestText(messageContent) {
  // system ロールの配列を生成
  const systemWatchwordArry = systemWatchwordRange.getValues().flat();
  const systemContentArry = systemContentRange.getValues().flat();
  let systemWatchword = '';
  let systemContent = '';

  // リクエスト内容の処理
  if (messageContent.startsWith('続き)')) {
    // 文頭に「続き)」で会話を継続
    messageContent = messageContent.replace('続き)', '');
    chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['user', messageContent]]);
  } else {
    // temperature を取得してスプレッドシートにセット
    // 文末に「(創造」「(厳密」で指定
    if (messageContent.endsWith('(創造')) {
      messageContent = messageContent.replace('(創造', '');
      tempRange.setValue(1);
    } else if (messageContent.endsWith('(厳密')) {
      messageContent = messageContent.replace('(厳密', '');
      tempRange.setValue(0);
    } else {
      tempRange.setValue(0.5);
    }
    // スプレッドシートにあらかじめ登録した system ロールの読み込み
    // 文頭に「〇〇!」でsystem ロールを指定
    for (let i = 0; i < systemWatchwordArry.length; i++) {
      if (messageContent.startsWith(systemWatchwordArry[i] + '!')) {
        systemWatchword = systemWatchwordArry[i] + '!';
        systemContent = systemContentArry[i];
        break;
      }
    }
    messageContent = messageContent.replace(systemWatchword, '');

    chatRange.clear(); // 以前のチャット内容を削除

    // リクエスト内容をスプレッドシートに格納
    if (systemContent) {
      chatSheet.getRange(2, 1, 2, 2).setValues([['system', systemContent], ['user', messageContent]]);
      chatRange = chatSheet.getRange(2, 1, 2, 2);
    } else {
      chatSheet.getRange(2, 1, 1, 2).setValues([['user', messageContent]]);
      chatRange = chatSheet.getRange(2, 1, 1, 2);
    }
  }

  // リクエストデータを生成
  let messages = [];
  let values = chatRange.getValues();
  for(let i = 0; i < values.length; i++) {
    if(values[i][0] == 'system') {
      continue;
    }
    messages.push({'role': values[i][0], 'content': values[i][1]});
  }

  return messages;
}

// Claude でリクエストデータ(画像)を生成する関数
function claudeRequestImage(id) {
  // 画像を取得
  const response = UrlFetchApp.fetch('https://api-data.line.me/v2/bot/message/' + id + '/content',{
    'headers': {
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'method': 'get'
  });
  const imageBlob = response.getBlob().getAs('image/png');

  // 画像を Base64 にエンコード
  const base64Image = Utilities.base64Encode(imageBlob.getBytes());

  // リクエストデータを生成
  let messages = [];
  let values = chatSheet.getRange(2, 1, chatLastRow - 1, 2).getValues();
  for(let i = 0; i < values.length; i++) {
    if(values[i][0] == 'user') {
      messages.push({'role': 'user','content': [{'type': 'image','source': {'type': 'base64','media_type': 'image/png','data': base64Image}},{'type': 'text','text': values[i][1]}]});
    } else {
      messages.push({'role': values[i][0], 'content': values[i][1]});
    }
  }

  return messages;
}

// Claude から回答を得る関数
function getClaudeReply(prompt) {
  let system = "";
  if(chatSheet.getRange(2, 1).getValue() == 'system') {
    system = chatSheet.getRange(2, 2).getValue();
  }

  const payload = {
    'model': 'claude-3-5-sonnet-20240620', // 使用するモデル,
    'max_tokens': 1024, // 生成する文章の最大トークン数
    'temperature': tempRange.getValue(),
    'system': system, // システム指示
    'messages': prompt
  };
  
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': PropertiesService.getScriptProperties().getProperty('Claude_key'),
      'anthropic-version': '2023-06-01',
    },
    payload: JSON.stringify(payload),
  };

  try {
    const response = JSON.parse(UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', options).getContentText());
    return response.content[0].text; // Claude の回答
  } catch (error) {
    // エラー発生時
    console.error('Error:', error);
    return 'エラー: Claude の回答が得られませんでした';
  }
}

// AI の返答をスプレッドシートに格納する関数
function setAiMessage(role, replyMessage) {
  chatLastRow = chatSheet.getLastRow();
  chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([[role, replyMessage]]);
}

// LINE メッセージを送信する関数
function sendLineMessage(replyToken, replyMessage) {
  const linePayload = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`,
    },
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': replyMessage,
      }],
    }),
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', linePayload);
}

1行目の「スプレッドシート ID」は書き換えてください。

コード作成後にウェブアプリとして公開、URL を LINE Messaging API チャンネルで Webhook として設定して完成です。




***

生成AI、本当に日進月歩で性能が上がっていきますね。自身にとってのより良い活かし方を、日々探っていきたいですね。


cultivationdata.net では、GPT、Gemini、Claude を搭載した LINE bot の作り方を公開しています。 → note での公開に変更しました(2024-8-17)