« ^ »

Google GeminiをAPI経由で使う

所要時間: 約 8分

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-processcurl コマンドをサブプロセスとして起動する方法を使う。

なぜHTTPライブラリではなくmake-processとcurlを使うのか

HTTPリクエストを送信する時、通常はEmacs Lispで実装された request.elplz など、HTTPクライアントライブラリを使う。もしかしたらEmacsで標準ライブラリとして組み込まれている url-retrieve を使う事もあるかもしれない。それぞれ長短があったりはするが、概ね良くできている。しかし問題が発生した時、新しく機能を追加したい時では、それぞれのライブラリを知っておく必要がある。しかし、Emacs Lispのライブラリについて知りたいかと言われると、正直そんなに知りたい訳ではない。

その一方、 make-processcurl コマンドをサブプロセスとして起動する方法では、問題を curl に寄せる事ができる。 curl は全ての人が熟知しているツールであるため、トラブルを解決しやすい。この方法の問題点はcurlがインストールされていないと動かないという事だろう。 request.elplz.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とかも同じ事が言える。本当にどれの事について言っているのか分からなくなる。