📌

gemini APIが無料だったので触ってみました

2024/09/13に公開

Google Cloud Nextなどでもとても推されているgeminiですが、お恥ずかしながら前回のNextで無料でAPIが使える事を知りました。
tsを使用したAPI経由のリクエストを叩いてみたのでご紹介します。
ローカルで稼働させる分には何でもいいのですが、最近ハマっているsupabase + slackで実装します。webに載せるのもワンコマンドでできるので🙇‍♂️
slack側からリクエストを送信し、レスポンスをslackに返却する方式となります。

無料と言っていますが、学習データへの使用は避ける為社内では有料版を使用しています。

環境構築

geminiAPI keyを発行

2通りの発行手順があります。

どちらも会員登録など親切なuiに沿って実施するだけですので手順は割愛します。

supabase edge環境構築

supabase init
supabase functions new gemini_slack
supabase start
supabase functions deploy gemini_slack

上記のコマンドでDBとファンクションとストレージのローカル環境がdockerで稼働していると思います。

supabase/functions/gemini_slackというファンクションファイルが生成されているのでそちらを修正することでロジックを記載できます。

web環境もデプロイされているのでリクエスト先のurlが払い出されていると思います。

slack

  1. slack apiからcreate new appを実行

  2. OAuth & PermissionsからScopeを設定

Bot Token Scopes
app_mentions:read
channels:history
chat:write
groups:history
im:history
incoming-webhook
links:write
mpim:history
users:read

User Token Scopes
channels:history
groups:history
im:history
mpim:history

  1. incoming webhooksを有効化
    slackで返事をするので有効化しておきます。
  2. event subscriptionsを有効化
    slackでbotをメンションした時のトリガーを作成します。
    トリガー時にリクエストを送信するか設定ができるので、supabase deployした際のエンドポイントを設定します。
  3. botのインストール
    導入したいワークスペースにインストールします。

上記の内容でslackとサーバーの方は環境が整いました。

通常のテキストレスポンスを受ける場合のプログラム

supabase/functions/gemini_slackに記載しdeployコマンドを再実行すると即デプロイできます。

// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { WebClient } from 'npm:@slack/web-api';
import { GoogleGenerativeAI } from 'npm:@google/generative-ai';
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
const genAI = new GoogleGenerativeAI(Deno.env.get('geminiAPIkey'));
// Slack APIトークンを設定
const token = Deno.env.get('slack token');
// WebClientを初期化
const web = new WebClient(token);

Deno.serve(async (req) => {
  const body = await req.json()

  // ユーザー名を取得
  const userName = await getUserName(body.event.user);

  var threadText = "";

  // 返答先を設定
  var send_to = body.event.event_ts;


  // スレッドがある場合は、スレッドIDからスレッド情報を取得し、スレッドテキストを生成 また、返答先をスレッドIDに設定
  if (body.event.thread_ts) {
    const threadInfo = await getThreadInfo(body.event.thread_ts, body.event.channel);
    threadText = await generateThreadText(threadInfo);
    send_to = body.event.thread_ts;
  }

  // 文章を生成
  const text = body.event.text;
  const response = await generateResponse(text, userName, threadText);
  if (!response) {
    console.log("返答生成に失敗しました")
    return new Response(
      JSON.stringify("理由は返却しませんが、このリクエストはダメです"),
      { status: 500, headers: { "Content-Type": "application/json" } },
    )
  }


  // メッセージを送信
  try {
    const result = await web.chat.postMessage({
      channel: body.event.channel, // チャンネルIDまたはチャンネル名
      thread_ts: send_to, // スレッドID
      text: response, // 送信するメッセージ
    });
  } catch (error) {
    console.error(`Error sending message: ${error}`);
  }


  // bodyにchallengeが含まれている場合は、challengeを返す(slackのイベント購読の検証で必要)
  if (body.challenge) {
    return new Response(
      JSON.stringify({ challenge: body.challenge }),
      { headers: { "Content-Type": "application/json" } },
    )
  }
  return new Response(
    JSON.stringify("OK"),
    { headers: { "Content-Type": "application/json" } },
  )
})

// スレッドの情報を取得する関数
async function getThreadInfo(thread_ts: string, channelID: string) {

  // スレッドidを指定してスレッドのテキストを取得
  const result = await web.conversations.replies({
    channel: channelID,
    ts: thread_ts,
  });

  return result;
}

// ユーザーIDを指定しユーザー名を取得する関数
async function getUserName(user_id: string) {
  const result = await web.users.info({
    user: user_id,
  });

  return result.user.profile.display_name;
}

// スレッドjsonからユーザー名とテキストを生成する関数
// ユーザー名:ユーザーの発言内容
async function generateThreadText(thread: Object) {
  let text = "";
  let threadnum = 1;
  for (const message of thread.messages) {
    const user = await getUserName(message.user);
    var name = user;
    if (!message.user) {
      if (message.bot_id) {
        name = "bot"; 
      }
    }
    text += "スレッド番号"+ threadnum +" : "+ name + " : " + message.text + "\n";
  }

  return text;
}


// 返答を生成する関数
async function generateResponse(inputText: string, username: string, threadText: string) {
  const model = genAI.getGenerativeModel({ model: 'gemini-1.5-pro' });
  var template = `
  前提プロンプトを入力
  `;

  const user = "\n\n  # ユーザー\n" + username;
  var prompt = template + user + "\n\n  # 対象プロンプト\n" + inputText;

  if (threadText !== "") {
    prompt += "\n\n  # スレッド情報\n" + threadText;
  }

  try {
    const result = await model.generateContent(prompt);
    const response = await result.response;
    const answer = response.text();
    return answer;
  } catch (error) {
    console.error(`Error generating response: ${error}`);
    const answer = `${error}`;
    return answer;
  }

}

*ファイル解析を行う場合
gemini APIにはFile APIという機能があります。(1.5pro固有の機能だったと思います)
通常のテキストとは別の形式でリクエストが必要のため、今回は割愛させていただきます。

これでslackでの受け答えができるbotが出来上がると思います。

使用感

基本的には問題なく受け答えしてくれますが、上記のコードはファイルや社内の情報を与えていないため、プログラムの質問程度にしか使用できていないです。
また、geminiAPIはセンシティブ設定がありパワハラのようなハラスメントをするとAPIでパニックを起こします。別途エラーハンドリングは入れた方が良さそうです。
デバッグ時の挙動で確認できたので一部紹介


デバッグのため大変な暴言を吐いていますが社内は平和です。

レスキューナウテックブログ

Discussion