


Google GeminiをEmacsから使う時の技術的なメモ
これはGoogle Geminiの機能のいくつかをEmacsから利用する際に、EmacsをハブとしてGeminiを活用するための2つの異なるアプローチと、その過程で得られた知見のメモを残す。
Gemini CLI
最近Gemini CLIが登場したので使う事にした。これはGoogle GeminiのCUI エージェントだ。競合としてはClaude CodeとかOpenAI Code XとかOpenHandsなんかが当てはまるだろう。Gemini CLIの特徴はコンテキストウィンドウが広くて、無料で使える量も多いという事だ。ただ無料で使う場合、その情報は学習に使われる事になるから、Geminiに渡すデータについては注意が必要だ。
準備と起動
まずはインストールする。
npm install -g @google/gemini-cli
起動する。
gemini --debug
Gemini CLIは --sandbox
というオプションを提供している。これにより gemini
はコンテナ内で実行される。本来はこれを使うと良さそうではある。
しかし僕はこれを使わず、自分でコンテナの起動コマンドを指定する事でGemini CLIを実行する事にした。そうすれば何がGeminiによって使われるのか、明らかに把握できるからだ。これは、実行環境の透明性を確保し、予期せぬ挙動を避けるための選択である。
EmacsとGemini CLI
Gemini CLI
は制御コードを使ってTUI(ターミナルUI)を作っている。Emacsの標準の機能、例えば `M-x shell` や `M-x term` などでは、残念ながらこの制御コードを上手く扱えず、表示が崩れてしまう問題があった。
しかし、この手のツールは vterm
を使うと上手く扱う事ができる。`vterm`はEmacs内で動作する、libvtermを利用した高性能なターミナルエミュレータであり、モダンなCLIアプリケーションが生成する複雑なTUIも適切に表示できるため、この問題の解決策として採用した。
そんなユーティリティをEmacs Lispで書いた。
(defun my/agent-gemini-cli (&optional line cwd)
(interactive (list (progn
(when (use-region-p) (clipboard-kill-ring-save (region-beginning) (region-end)))
(read-shell-command "CMD$ "))
(read-directory-name "DIRECTORY: "
default-directory nil
default-directory)))
(let ((default-directory cwd)
(vterm-shell
(format
"docker run --volume $PWD:$PWD --volume $HOME/.gemini:/root/.gemini --workdir $PWD -it %s %s"
"gemini-cli"
line
))
(vterm-buffer-name
(format "*Agent: %s*" (expand-file-name
default-directory)))
(vterm-kill-buffer-on-exit nil))
(vterm)))
この関数は、カレントディレクトリやプロンプトを引数に取り、`vterm`内でDockerコンテナを起動してGemini CLIを実行するラッパーとして機能する。指定しているコンテナイメージには、事前にビルドしたGemini CLIインストール済みのDockerイメージを指定している。
ただ、これで実行するとgemini-cliへの日本語の入力ができなかった。`vterm`はキー入力を直接サブプロセスのシェルに送ってしまう「char-mode」で動作するため、Emacsの入力メソッドであるSKKが介入する余地がないからだ。これにより、日本語の変換処理がEmacs側で行われず、意図した文字列が入力できないという問題が発生した。
この問題を解決するため、日本語の入力ができるように、追加でちょっとしたコマンドを書く事にした。
(defun my/send-string-to-buffer-process ()
(interactive)
(send-string nil (read-from-minibuffer "PROMPT: ")))
この関数は、ミニバッファで文字列を読み取り、それをカレントバッファのプロセスに送信する。ミニバッファはEmacsの通常のバッファであるため、SKKなどの入力メソッドが問題なく使える。これにより、日本語のプロンプトを`vterm`内のGemini CLIに送ることが可能になる。多少面倒な所もあるけれど、これで一通りは使えるようになった。
Gemini APIの直接利用
エージェントではなくてGeminiの機能をAPIで利用する事もできる。まあ、Gemini CLIの方が後発ではあるのだけれど。
APIキーを取得する
Googleアカウントを持っていれば、以下のページからAPIキーを生成できる。
https://aistudio.google.com/app/apikey
簡単なリクエストを送信する
まずは動作を確認するために、restclientで簡単なリクエストを送信する。これはAPIの基本的な挙動や認証方法を確認するための概念実証(PoC)として行った。
:ORIGIN = https://generativelanguage.googleapis.com
:API_KEY := google-gemini-api-key
POST :ORIGIN/v1beta/models/gemini-pro:generateContent
x-goog-api-key: :API_KEY
{
"contents" : [
{
"parts": [
{
"text": "こんにちわ"
}
]
}
]
}
{
"candidates": [
{
"content": {
"parts": [
{
"text": "こんにちは。何かお手伝いできることはありますか?"
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0,
"safetyRatings": [
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
],
"promptFeedback": {
"safetyRatings": [
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
}
// POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent
// HTTP/1.1 200 OK
// Content-Type: application/json; charset=UTF-8
OpenAI APIと同じような感じで、REST APIとしてリクエストを送信しレスポンスを取得する。
APIのクエリパラメータとして alt=sse
を追加する事で、 Server Sent Events
を使い、少しずつ結果を取得する事もできる。このあたりもOpen AI APIに似ている。たしか、OpenAI APIはリクエストのBODYの中で指定するようになっていた。
:ORIGIN = https://generativelanguage.googleapis.com
:API_KEY := google-gemini-api-key
POST :ORIGIN/v1beta/models/gemini-pro:generateContent?alt=sse
x-goog-api-key: :API_KEY
{
"contents" : [
{
"parts": [
{
"text": "こんにちわ"
}
]
}
]
}
#+BEGIN_SRC js data: {"candidates": [{"content": {"parts": [{"text": "こんにちは。"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} // POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?alt=sse // HTTP/1.1 200 OK // Content-Type: text/event-stream // Content-Disposition: attachment
ここまで把握できた。
他の実装済みのライブラリを検討する
EmacsからGeminiを使用するための拡張機能は、自分で実装しなくても既に十分な実装がある。簡単な調査の結果、次のライブラリが適していそうだ。
https://github.com/karthink/gptel
gptelは複数の生成系AIのサービスに対応している。特に理由がない場合は、これを使うと良さそうだ。
なぜgptelや他の実装済みの拡張を使わないか
先程gptelが良さそうだと記述したが、僕はgptelを使わない事にした。理由は以下の通りだ。
- 自分の使用する機能以外のコード(ライブラリも含む)を出来る限り持ちたくない。 近年のプログラミング言語や、そのエコシステムの状況を見ていると、多くのライブラリに依存する傾向がある。標準ライブラリにも、サードパーティライブラリにも、たくさん依存して様々な機能を実現し提供している。またサードパーティライブラリはそれ自体が、たくさんのサードパーティライブラリに依存している事もある。その結果、ソフトウェアを管理しているプログラマーが、内部で何が起きているのかを把握できない状態となる。そしてトラブルが発生した時、どうしようもなくなってしまう。 もちろん、アップデートしなくとも最新のコードを取得し、進化していくという利点はあるけれど、最新のコードが常に正しいと誰が決めたんだ?アップデートのたびに確認すれば良いという意見もあるかもしれないが、現実にはアップデートは確認せずに適用されることが多い。実情はノールックアップデートだ。 体制を整えれば、アップデートの確認も可能かもしれないが、現時点で私が関心があるのは自分用のEmacsの拡張であり、そのようなものに対して体制を整備するのは馬鹿げている。 ゆえに自分が利用する機能以外のコードはできるだけ減らしたい。
- 機能を追加したい場合、固有のライブラリの使い方や内部実装を調べる手間を省きたい。 実現したい機能のイメージがあり、APIの使用も理解したとしても、すぐに実装は実装できない。既存の実装を知る必要があるからだ。それが自分の書いた実装なら把握もしやすいが、誰かが書いたライブラリやツールであれば、これはそこそこ大変な作業になる。この作業で右往左往するのだが、別に僕はそこで右往左往したいわけじゃない。誰かの作った実装のほうが高機能であったとしても、その機能の全てを僕が使いたい訳じゃない。使いたい機能なんて本当に少ししかないはずだ。
- GeminiのAPIの使い方に関する知識を得たい。 今回の目的の一つはGeminiのAPIを把握する事でもある。ライブラリやツールは変われど、結局の所GeminiのAPIに追従する形になるからだ。だからライブラリを学ぶのではなく、GeminiのAPIを学びたい。
これらの理由により、僕は自分で実装することにした。
Emacs拡張を実装する
Emacs拡張を実装していく。実装方法もいくつか考えられるだろうけれど make-process
で curl
コマンドをサブプロセスとして起動する方法を使う。
なぜHTTPライブラリではなくmake-processとcurlを使うのか
HTTPリクエストを送信する時、通常はEmacs Lispで実装された request.el
や plz
など、HTTPクライアントライブラリを使う。もしかしたらEmacsで標準ライブラリとして組み込まれている url-retrieve
を使う事もあるかもしれない。それぞれ長短があったりはするが、概ね良くできている。しかし問題が発生した時、新しく機能を追加したい時では、それぞれのライブラリを知っておく必要がある。しかし、Emacs Lispのライブラリについて知りたいかと言われると、正直そんなに知りたい訳ではない。
その一方、 make-process
で curl
コマンドをサブプロセスとして起動する方法では、問題を curl
に寄せる事ができる。 curl
は全ての人が熟知しているツールであるため、トラブルを解決しやすい。この方法の問題点はcurlがインストールされていないと動かないという事だろう。 request.el
や plz.el
は、 curl
があれば curl
を使うし、無ければ url-retrieve
にフォールバックする。これは素晴しいが、僕の環境で curl
が使えない事は基本的にないので、それについては考えない事にする。curl、入っているよね?入れればいいよね?だから今回も、この方法で行う。
ちなみにlibcurlを使うという方法もあるかもしれないが、そちらはビルドが必要な事、リクエスト送信中にブロックしてしまう事などを考えると、curlをサブプロセスで起動する方が、Emacsのスタイルには馴染むと思う。
実装の方針
おおまかな実装の方針としては、以前実装したopenai.elやdeepl.elと同じような実装にする。箇条書きにすると以下のようになる。
- make-processでcurlを起動してAPIにリクエストを送信する。
- レスポンスは
Server Sent Events
で受け取り、解析可能な範囲から解析していく。 - 解析した文字列は、解析済みテキスト保持用のQueue変数に詰める。
- 解析済みテキスト保持用のQueue変数からタイマー処理で解析済みテキストを取り出し、出力用バッファに挿入する。
データやコンポーネント、関数などごちゃまぜの概念の構成図を書いた。概ねこのようなイメージになる。
+-------------------------+ +-------------------------+
| | | |
| read-string-from-buffer | | api key |
| | | |
+----------+--------------+ +----------+--------------+
| |
+-----------------------------+
|
v
+----------+------+
| |
| curl config |
| |
+----------+------+
|
| +-----------------+ +-----------------+
| | | | |
--------->+ curl process | | Gemini API |
| | HTTPs POST | |
| +------------>+ |
| | | |
| | Server Sent | |
| | Event | |
+-----------------+ +<------------+ |
| | | | |
| | | Server Sent | |
| | | Event | |
| +--------------+ +<------------+ |
| | | | | |
| | | | Server Sent | |
| | | | Event | |
| | |-----------+ +<------------+ |
| | | | | | |
| | | | | | |
| | | | | | |
| | | +-------+---------+ +-----------------+
| | | |
| | | |
v v v | push
+--+--+--+------+ +-------+---------+ +-----------------+
| | | | | |
| Process | | Process | | Message Queue |
| Buffer +--->+ Filter |--->+ |
| | | | | |
| | | | | |
+---------------+ +-------+---------+ +-------+---------+
| | pop
| kick v
+-------+---------+ +-------+---------+
| | | |
| Process | | Timer |
| Sentinel +--->+ |
| | | |
+-----------------+ +-------+---------+
|
v
+-------+---------+
| |
| Result Buffer |
| |
+-----------------+
実装
google-gemini.el
名前被り
実は世の中にはGoogleではない、全く別のGeminiがある。しかも、少なくとも2つ(Emacs関連のプロジェクトと、暗号通貨関連のサービス)はある。名前被りはある程度は仕方がないんだけれど、一般的な名詞をプロジェクトに付けるのは、正直避けた方がいい。Babelとかも同じ事が言える。本当にどれの事について言っているのか分からなくなる。
まとめ
この記事では、EmacsからGoogle Geminiを利用するための2つのアプローチをそれぞれの実装と課題解決の過程を記録した。
Gemini CLIは、Emacsと`vterm`を経由して利用した。TUIの制御コードや入力メソッドとの衝突といった、Emacsと外部ターミナルアプリケーションとの間で発生する問題に対処するため、ミニバッファを介してコマンドを送信するという小さなヘルパー関数がを実装した。
APIを直接利用する方法では、初期実装の手間はかかるものの、依存関係を最小限に抑え、API仕様への深い理解を深められた。`curl`を非同期プロセスとして呼び出し、その結果をEmacs内で捌くアーキテクチャは、他のWeb APIを扱う際にも応用可能で汎用性が高い。使いにくさもあったけど。
どちらのアプローチが優れているというわけではないし、用途や思想に応じて最適な手段は異なる。適宜、状況を鑑み選んでいけばいい。