ChatGPTのChrome拡張を作った時の話

## 概要
ChatGPTの良い拡張がなくて、自分のために作ったChrome拡張のリポジトリをpublicに変更しました。
複数のプロンプトを呼び出せて、履歴管理できて、機能がシンプル・・・  という拡張です。
GPTを召喚するようなイメージで、chatとスペルのにてる単語つけてChantGPTと名付けました。

- 紹介動画

https://www.youtube.com/watch?v=fddlVKo-iQ4

- chrome extension

https://chrome.google.com/webstore/detail/pnlepcahiadlbcjgikjinjgbdgeflhdf

- リポジトリ

https://github.com/nabepon/chant-gpt-prompt


## 手こずった部分

これはChrome拡張というよりChatGPTの `Chat Completions API`　の話。
1文字1文字リアルタイムにレスポンスを表示したかったので、今回はstreamオプションを有効にしてstreamで処理することにしました。
で、試すと以下のような `data: {JSON}` という文字列が1行1行流れてきます。

```
data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"Chrome"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"拡"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"張"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"の"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"作"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"り"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"方"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: [DONE]
```

そして最後は `data: [DONE]` というstringが流れてきて終了します。

これは、JSONLでも無いし、何の仕様だ？

streamを扱うのが初めてだったので何かの仕様だと思いパーサーを調べたものの、これが何なのか分からず結局自前で適当にパースしました。あとから分かったんですが、`Server-Sent Events` というらしいです。ただ最後の `data: [DONE]` は独自の仕様のよう。

で、これが確実に1行1行流れてくるならいいんですが、変なところで切れて流れてくることもありました。例えば以下のように2つに分かれて流れてくることがあります。

```
data: {"id":"chatcmpl-foo","choices":[{"index":0
```

```
,"delta":{"content":"Chrome"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}


data: {"id":"chatcmpl-foo","choices":[{"index":0,"delta":{"content":"拡"},"finish_reason":null}],"object":"chat.completion.chunk","created":1694240542,"model":"gpt-3.5-turbo-0613"}
```

雑なパーサを書いていたので、こういう部分でエラーになって止まることがありました。
`Server-Sent Events` ってそういうものなのか・・・・・・？
仕方ないのでパース処理を適当に拡張して対応しましたが、いまだによく分かってません。

また、streamオプションを有効にすると、レスポンスのトークンの量がわからず。トークン量によってハンドリングすることが出来ない問題があります。エラーが返ってきて初めてトークン数を超えたことが分かり、しかし何トークン超えているのかはわからない、というAPIの設計になってます。つらい。

調べると一応 [GPT-3-Encoder](https://github.com/nem035/GPT-3-Encoder) というのがあり、これでトークン数を計算することはできますが、GPT3.5とは計算方法が違うので大まかなトークン量しか知ることができないのであんまり使えません。アルファベット以外は何トークンになるか分からないので、それはそうなんですけどね。

せめてトークン数を計算するAPIを別途用意してくれたら助かるんですけど、OpenAIさんには是非お願いしたい所です。




## 他に学びあったこと

- ChatGPTがmarkdownで返してくることが多いので、markdownでレンダリングするようにしました。使ったのは [react-markdown](https://github.com/remarkjs/react-markdown) です。ただシンタックスハイライトに対応してないので、 [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter) を合わせて使うことになります。しかしこのライブラリ、サイズもでかければ処理も重く、気軽にポンポン呼び出すには厳しいものがありました。慌てずREADMEをみると Async Build という項目があり、ここの`PrismAsyncLight` を使うことでめちゃくちゃ軽くなりました。

- Chrome拡張にはショートカットキーやリクエストを読み取ったりなど色々な権限がありますが、v3になってから結構厳しくなっていました。リクエストを乗っ取ってmockするような拡張がRequestlyしかないので、もっとシンプルなRequestlyっぽいものを自分で作れないかと思ったんですが、v2での権限でしか作れないことが分かりました。いつかv2が廃止されたら使えなくなってしまうので、シンプルなRequestlyを作るのは断念しました。

- Chrome拡張はストレージの種類が2種類あるんですね。1つはChromeアカウントに紐づいてネットワーク上に保存する　`chrome.storage.sync`　と、ローカルストレージに似たブラウザ内に保存する `chrome.storage.local` の2種類があります。前者は保存容量すくないですが、Chrome拡張のポータビリティが増すので便利です。今回はpromptのテンプレートを保存するのに活用し、後者のストレージは履歴を保存するのに使いました。

- chrome extensionはサンプルが豊富で、サンプルを見れば大体どう作ればいいのか分かるので助かりました。ドキュメントと合わせて読めば他に調査が必要になることはほとんどないです。逆にいうとここに無いことはできません https://github.com/GoogleChrome/chrome-extensions-samples



今回は普段業務では扱わないAPIに沢山触れられて楽しかったです。