Gemini CLI with Discord

Gemini CLIをDiscordから呼び出せるようにしてみた

Gemini CLI with Discord

概要

前回に引き続きGeminiネタ

現状Raspberry Piの上で常時Gemini CLIが稼働できる状態になっている.

このままでも悪くはないが,使いたいときはSSHしてdocker compose execして...というように手数がかかる.

Discordからプロンプトを入力して,回答もそこで得られるようにしてみる

構成

実装

リポジトリとして公開できるほど整備してないのでコードだけ一部紹介.

まずはdocker-compose.yamlから.

services:
  gemini:
    build: .
    container_name: gemini
    command: python3 /root/bot.py
    restart: always
    environment:
      - DISCORD_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      - MODELS=gemini-2.5-flash
    stdin_open: true
    tty: true
    volumes:
      - ./home:/root
      - ./app:/app

docker-compose.yaml

  • ボリュームは適宜バインド
  • 環境変数でGeminiのモデルとDiscord Bot用のトークンを設定

続いてDockerfile.

FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl \
  && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
  && apt-get install -y nodejs \
  && npm install -g @google/gemini-cli

RUN set -eux; \
  npm install -g @google/gemini-cli > /dev/null 2>&1; \
  apt-get update -qq; \
  apt-get install -y --no-install-recommends \
    python3 \
    make \
    g++ \
    man-db \
    curl \
    dnsutils \
    less \
    jq \
    bc \
    gh \
    git \
    unzip \
    rsync \
    ripgrep \
    procps \
    psmisc \
    lsof \
    socat \
    ca-certificates \
    lsb-release \
    wget > /dev/null 2>&1; \
  install -m 0755 -d /etc/apt/keyrings; \
  wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | tee /etc/apt/keyrings/adoptium.asc > /dev/null; \
  echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" \
    | tee /etc/apt/sources.list.d/adoptium.list > /dev/null; \
  apt-get update -qq; \
  apt-get install -y --no-install-recommends temurin-17-jdk > /dev/null 2>&1; \
  apt-get clean; \
  rm -rf /var/lib/apt/lists/*


COPY home/requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

WORKDIR /app

CMD ["python", "~/bot.py"]

Dockerfile

  • ベースはPythonにしてDiscord Botをメインにして動くようにする.
  • Gemini CLIと共存させる
  • 実行環境を作るために必要なJavaと各種utilsを適宜インストール (任意)

最後にBot向けのコード.(ChatGPT頼み)

discord.py
python-dotenv

requirements.txt

import discord
from discord.ext import commands
from dotenv import load_dotenv
import os
import shlex
import asyncio

load_dotenv()

DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
if not DISCORD_TOKEN:
    raise ValueError("DISCORD_TOKEN is not set in environment variables.")

MODELS = os.getenv("MODELS", "gemini-2.5-pro")

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(command_prefix="/", intents=intents)

@bot.command()
async def gemini(ctx, *, question: str):
    session_id = f"thread-{ctx.channel.id}"  # unused
    escaped_question = shlex.quote(question)
    cmd = f"gemini -m {MODELS} -p {escaped_question} --yolo"

    try:
        await ctx.message.add_reaction("🤖")
    except discord.HTTPException:
        pass

    try:
        print(f"[gemini] Running command: {cmd}")
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()

        if stderr:
            error_msg = stderr.decode().strip()
            raise RuntimeError(error_msg)

        output = stdout.decode().strip()
        print(output)
        if not output:
            raise RuntimeError("No output from gemini.")

        # Discordの2000文字制限に対応して分割送信
        for i in range(0, len(output), 2000):
            await ctx.send(output[i:i+2000])

        try:
            await ctx.message.remove_reaction("🤖", bot.user)
            await ctx.message.add_reaction("✅")
        except discord.HTTPException:
            pass

    except Exception as e:
        try:
            await ctx.message.remove_reaction("🤖", bot.user)
            await ctx.message.add_reaction("❌")
        except discord.HTTPException:
            pass

        error_msg = str(e)
        if len(error_msg) <= 2000:
            await ctx.send(f"Error: {error_msg}")
        else:
            await ctx.send("Error occurred. Output too long to display.")

bot.run(DISCORD_TOKEN)

bot.py

  • 環境変数からDiscordトークン設定
  • コマンド /gemini を受け取ったら処理開始
    • geminiを呼び出すためのコマンド生成 (同じコンテナ内のサブプロセスで)
      cmd = f"gemini -m {MODELS} -p {escaped_question} --yolo"
      • -m でモデル指定.
      • -p でDiscordから送ったプロンプトを渡す
      • --yoloオプションでコマンド実行許可(コンテナ内なので壊れてOK)
      • ログ出力
    • Geminiからの回答をDiscordへ送信
      • 2000文字制限がある(らしい)ので分割して送信
      • ログには出力しておく
  • リアクション
      • コマンドを受け取ったあと🤖で反応
      • Gemini CLI側で処理成功の場合✅,NGの場合❌

これをいい感じに配置する

.
├── Dockerfile
├── README.md
├── app
│   └── simple-api  # 適当なJavaアプリケーション
├── docker-compose.yaml
└── home
    ├── bot.py
    └── requirements.txt

72 directories, 50 files

tree

動作例

呼び出し

送信直後にリアクション

結果に応じてリアクション更新

コンテナログ出力

raspberrypi4% docker compose logs -f
...
gemini  | [gemini] Running command: gemini -m gemini-2.5-flash -p hello --yolo
gemini  | こんにちは!何かお手伝いできることはありますか?

コマンド実行

できてそう

アプリケーション概要説明

単純な機能を持つSpringBootのサンプルアプリケーションについて説明させてみる.

GitHub - doariva/simple-api
Contribute to doariva/simple-api development by creating an account on GitHub.

概要すぎるけどいい感じ.もっと指示すればたぶん回答してもらえる

コード修正

Issueを読んでPRを作成してくれる

Controller処理分離 · Issue #33 · doariva/simple-api
AsIs Controller層に直接処理が記述されている ToBe これをService層に分けたい

ghコマンドを使えるようにトークン発行や認証は事前にしてあげている

Geminiからの回答(抜粋)

ここまで何度か実行していたのでコンテナ内にはすでに変更された状態が残っていたみたい.このあたりは割と汲み取って指示してあげる必要あり.まだ気を使わないといけないところ.

feat: Separate controller logic into service layer (#33) by doariva · Pull Request #34 · doariva/simple-api
Close #33

まあでも期待していたPRが出てきた.まだ簡単な処理だからイケてるように見えているが,アプリケーションの複雑性が増すとだめかも...

ビルド確認

Javaを入れているのでビルドしてテストが通るかは確認してくれる.

PRコメント

変更差分を見て概要をPRコメントさせる

PRコメント

Tips

システムプロンプト GEMINI.md

root@33e02bcb854e:/app# cat ~/.gemini/GEMINI.md
- 日本語で回答してください.
- gitの操作はgitコマンドを使用できます.
- GitHubのPRに関する操作はghコマンドを使用してください.
- GitHubのPR作成については,指示がなければmainブランチへのPRとしてください.
- リファクタリングできる点があった場合は相談してください.
- 現在のブランチと関連性がない新機能を実装する場合はブランチを切って作業してください.
- コミット/プッシュする前にビルドが通ることを確認してください.
- 作業する前はmainブランチを最新化してください.
- 回答はDiscordへ送信されるため,Marddown形式で読みやすくなるように心がけてください.

場所によってグローバル,プロジェクトごとに設定を変えられる

ここのチューニングをがんばって育てるしかない.あとはモデルの進化待ち

まとめ

気を使うところはあるものの,ひとり仲間が増えたみたいでうれしい.

flashなのでproを使えばもっと賢くなるはず.

実用性もだが,ChatOps的にやってみたかった欲が満たせたので自己満足

Pros

  • MacではなくRaspberry Piで動かせる
    • Macの開発に支障が出ない.環境が汚れない
    • 常にGeminiが動ける状態
    • sshせずにDiscord(=スマホ) から指示可
  • コンテナ内なので環境が壊れることを気にしなくてよい
  • Issue や PR など外部の情報を参照して,それなりに考えて動いてくれる

Cons

  • 1発では決まらない
    • 汲み取って途中で指示してあげる必要あり.
  • RaspberryPiのスペックに依存する
    • Gemini CLI自体はそこまででない(メモリ200MB程度)
    • ビルドはものによっては重い
  • サブプロセスで毎回単発でgeminiコマンドを叩くのでコンテキストが保てない
  • gemini-2.5-pro だとなぜかうまくいかなかった.エラーになる
    • 動作確認はすべてgemini-2.5-flash 使用