Googleの生成系AI「Gemini」をAPI経由で使用する事にする。
他の実装済みのライブラリを検討する
EmacsからGeminiを使用するための拡張機能は、自分で実装しなくても既に十分な実装がある。簡単な調査の結果、次のライブラリが適していそうだ。
https://github.com/karthink/gptel
gptelは複数の生成系AIのサービスに対応している。特に理由がない場合は、これを使うと良さそうだ。
なぜgptelや他の実装済みの拡張を使わないか
先程getelが良さそうだと記述したが、僕はgptelを使わない事にした。理由は以下の通りだ。
- 自分の使用する機能以外のコード(ライブラリも含む)を出来る限り持ちたくない。 近年のプログラミング言語や、そのエコシステムの状況を見ていると、多くのライブラリに依存する傾向がある。標準ライブラリにも、サードパーティライブラリにも、たくさん依存して様々な機能を実現し提供している。またサードパーティライブラリはそれ自体が、たくさんのサードパーティライブラリに依存している事もある。その結果、ソフトウェアを管理しているプログラマーが、内部で何が起きているのかを把握できない状態となる。そしてトラブルが発生した時、どうしようもなくなってしまう。 もちろん、アップデートしなくとも最新のコードを取得し、進化していくという利点はあるけれど、最新のコードが常に正しいと誰が決めたんだ?アップデートのたびに確認すれば良いという意見もあるかもしれないが、現実にはアップデートは確認せずに適用されることが多い。実情はノールックアップデートだ。 体制を整えれば、アップデートの確認も可能かもしれないが、現時点で私が関心があるのは自分用のEmacsの拡張であり、そのようなものに対して体制を整備するのは馬鹿げている。 ゆえに自分が利用する機能以外のコードはできるだけ減らしたい。
- 機能を追加したい場合、固有のライブラリの使い方や内部実装を調べる手間を省きたい。 実現したい機能のイメージがあり、APIの使用も理解したとしても、すぐに実装は実装できない。既存の実装を知る必要があるからだ。それが自分の書いた実装なら把握もしやすいが、誰かが書いたライブラリやツールであれば、これはそこそこ大変な作業になる。この作業で右往左往するのだが、別に僕はそこで右往左往したいわけじゃない。誰かの作った実装のほうが高機能であったとしても、その機能の全てを僕が使いたい訳じゃない。使いたい機能なんて本当に少ししかないはずだ。
- GeminiのAPIの使い方に関する知識を得たい。 今回の目的の一つはGeminiのAPIを把握する事でもある。ライブラリやツールは変われど、結局の所GeminiのAPIに追従する形になるからだ。だからライブラリを学ぶのではなく、GeminiのAPIを学びたい。
これらの理由により、僕は自分で実装することにした。
APIキーを取得する
Googleアカウントを持っていれば、以下のページからAPIキーを生成できる。
https://aistudio.google.com/app/apikey
簡単なリクエストを送信する
まずは動作を確認するために、restclientで簡単なリクエストを送信する。
: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
// Vary: X-Origin
// Vary: Referer
// Date: Sat, 02 Mar 2024 07:14:44 GMT
// Server: scaffolding on HTTPServer2
// Cache-Control: private
// X-XSS-Protection: 0
// X-Frame-Options: SAMEORIGIN
// X-Content-Type-Options: nosniff
// Server-Timing: gfet4t7; dur=981
// Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
// Accept-Ranges: none
// Vary: Origin,Accept-Encoding
// Transfer-Encoding: chunked
// Request duration: 1.303599s
OpenAI APIと同じような感じで、REST APIとしてリクエストを送信しレスポンスを取得する。
APIのクエリパラメータとして alt=sse
を追加する事で、 Sever 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 // Vary: Origin // Vary: X-Origin // Vary: Referer // Date: Sat, 02 Mar 2024 07:37:32 GMT // Server: scaffolding on HTTPServer2 // Content-Length: 774 // X-XSS-Protection: 0 // X-Frame-Options: SAMEORIGIN // X-Content-Type-Options: nosniff // Server-Timing: gfet4t7; dur=865 // Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 // Request duration: 1.246321s #+END_SRC
ここまで把握できたら、もう実装できる。
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 --- Google Gemini Utility -*- lexical-binding: t -*-
;; Copyright (C) 2024 TakesxiSximada
;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Repository:
;; Version: 1
;; Package-Version: 20240301.0000
;; Package-Requires: ((emacs "28.0")
;; Date: 2024-03-03
;; This file is part of google-gemini.el.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Code:
(defvar google-gemini-api-key nil)
(defvar google-gemini-api-origin "https://generativelanguage.googleapis.com")
(defvar google-gemini-api-path "/v1beta/models/gemini-pro:generateContent")
(defvar google-gemini-api-path-with-sse (format "%s?alt=sse" google-gemini-api-path))
(defvar google-gemini-curl-config-buffer-name "*Google Gemini: cURL: config*" "")
(defvar google-gemini-curl-process-buffer-name "*Google Gemini: cURL: process*" "")
(defvar google-gemini-curl-process-error-buffer-name "*Google Gemini: cURL: error*" "")
(defvar google-gemini-curl-process nil)
(defvar-local google-gemini-curl-process-buffer-current-position nil
"解析済みのcurlプロセスの出力の位置")
(defvar google-gemini-result-buffer-name "*Google Gemini: result*" "")
(defun google-gemini-create-curl-config (&optional text)
(interactive "sGoogle Gemini: ")
(with-current-buffer (get-buffer-create google-gemini-curl-config-buffer-name)
(erase-buffer)
(insert (format
"# cURL Configuration for Google Gemini API
url %s
request %s
header %s
header %s
data %s
"
(json-encode-string (format "%s%s" google-gemini-api-origin google-gemini-api-path-with-sse))
(json-encode-string "POST")
(json-encode-string "Content-Type: application/json")
(json-encode-string (format "x-goog-api-key: %s" google-gemini-api-key))
(json-encode-string
(json-encode-plist
`((contents . (((:parts (((:text . ,text)))))))))))))
(with-current-buffer (get-buffer-create google-gemini-result-buffer-name)
(goto-char (point-max))
(insert "\n\n------------------------------\n"
"[ME]\n"
text)))
(defun google-gemini-send-request ()
(interactive)
(with-current-buffer (get-buffer-create google-gemini-curl-process-buffer-name)
(erase-buffer)
(setq google-gemini-curl-process-buffer-current-position (point-min))
(add-hook 'after-change-functions #'google-gemini-handle-process-ouput)
)
(setq google-gemini-curl-process
(make-process :name google-gemini-curl-process-buffer-name
:buffer google-gemini-curl-process-buffer-name
:stderr google-gemini-curl-process-error-buffer-name
:command '("/usr/bin/curl" "-K-")))
(with-current-buffer (get-buffer google-gemini-curl-config-buffer-name)
(process-send-string google-gemini-curl-process (buffer-string))
(process-send-eof google-gemini-curl-process)))
(defun google-gemini-handle-process-ouput (&optional beg end len)
(interactive)
(with-current-buffer (get-buffer-create google-gemini-curl-process-buffer-name)
(save-excursion
(goto-char google-gemini-curl-process-buffer-current-position)
(ignore-errors
(skip-chars-forward "\r\n")
(search-forward "data: "))
(if-let ((sse-data (ignore-error '(json-readtable-error json-end-of-file) (json-read))))
(progn
(if-let ((text-list
(map 'list #'(lambda (part) (alist-get 'text part))
(car (map 'list #'(lambda (candidate) (alist-get 'parts (alist-get 'content candidate)))
(alist-get 'candidates sse-data))))))
(progn
(with-current-buffer (get-buffer-create google-gemini-result-buffer-name)
(display-buffer (get-buffer-create google-gemini-result-buffer-name))
(insert "\n\n------------------------------\n"
"[Gemini]\n")
(mapc #'insert text-list)) ;; 出力用バッファに書き込み
(setq google-gemini-curl-process-buffer-current-position (point)) ;; 解析済みの位置を更新
(google-gemini-handle-process-ouput) ;; 再帰処理により次のデータを処理する
)))))))
;;;###autoload
(defun google-gemini (&optional prompt)
(interactive
(list (read-string-from-buffer
"sGoogle Gemini"
(if (region-active-p)
(let ((region-text (buffer-substring-no-properties
(region-beginning) (region-end))))
(with-temp-buffer
(insert region-text)
(goto-char (point-min))
(replace-regexp "^" "> ")
(buffer-string)))
""))))
(when (and prompt (not (string-blank-p prompt)))
(google-gemini-create-curl-config prompt)
(google-gemini-send-request)))
(provide 'google-gemini)
;; google-gemini.el ends here
名前被り
実は世の中にはGoogleではない、全く別のGeminiがある。しかも、少なくとも2つ(Emacs関連のプロジェクトと、暗号通貨関連のサービス)はある。名前被りはある程度は仕方がないんだけれど、一般的な名詞をプロジェクトに付けるのは、正直避けた方がいい。Babelとかも同じ事が言える。本当にどれの事について言っているのか分からなくなる。