💥

ChatGPTをターミナルから使うコマンドラインツール(CLI)を作った

2023/03/27に公開

はじめに

ChatGPT流行っていますね。私はGPT-4が使えるようになってから触るようになりました。触っている過程でよりストレスなく使うにはターミナルから使えた方が良いと感じコマンドラインツール(CLI)を作ることにしました。

実際の操作感は以下ようなイメージです。
gif

Web版のChatGPT(https://chat.openai.com)は便利ですが、以下の点でもやもやがありました

  • ブラウザを開く必要がある
  • New Chatしすぎると整理が難しい
  • チャットの履歴を横断的に見ることが難しい(あいまい検索など)
  • ブラウザのエディタが使いにくい
  • 障害で2日程度チャットの履歴が見れない期間が存在した
  • API以外(ChatGPTとか)からの入力は学習用途に利用される[1]
  • 使い方によっては、ChatGPT Plusのサブスクより、従量の方が安い場合がありえる

ただこれらの点は一部今後改善されると思いますので、要は趣味です。

リポジトリ

リポジトリは、github.com/shuntaka9576/oaxです。ツール名は、oax(OpenAI eXecutor)です。

必要なもの

  • OpenAI APIのAPIキー
  • Vim/Neovimのような同期的に実行可能なエディタ(試していませんが多分Nanoも使えると思います)

導入方法

以下のコマンドで導入可能です。

brew tap shuntaka9576/tap
brew install shuntaka9576/tap/oax

導入後APIキーと利用エディタの設定をしますが、本記事では割愛します。README.mdのQuickStartを参照ください。

サポート機能

  • chat.openai.comのような対話機能(oax chat)
  • プロファイル機能(複数のアカウントAPIキーがあっても、指定してリクエスト可能。--profileオプションで指定可能)
    • Organization IDを指定したリクエストも可

詳細はREADME.mdを参照してください。

チャット機能

新規でチャットする場合

新規でチャットを開始すると、TOML形式のチャット履歴ファイルが作成されます。チャット履歴ファイルは~/.config/oax/chat-logに保存されます。
gif

チャット履歴ファイルは以下のような形式で、role="user"が質問者でrole="assistant"がChatGPTからの回答です。messages配列が全てOpenAIのAPIに送信されるので、コンテキストに基づいたチャットが可能です。

チャット履歴ファイル
[[messages]]
  role = "user"
  content = '''
しりとりしよう
'''

[[messages]]
  role = "assistant"
  content = '''
りんご
'''

[[messages]]
  role = "user"
  content = '''
ゴリら
'''

チャット履歴を保存するパスを変更することも可能です。例えばghqを使っている場合は、~/repos/github.com/{OWNER名}/chat-logを指定することでGit管理下にチャット履歴を管理できます。変更方法は以下の通りです。

設定ファイルを開く
oax config --settings
履歴を保存するパスを変更
[setting]
  editor = "nvim"
  chatLogDir = "~/repos/github.com/shuntaka9576/chat-log" # <-- 追加

詳細はこちらを参照ください。

-mオプションでモデルの指定が可能です。デフォルトはgpt-3.5-turboです。

$ oax chat --help
Usage: oax chat

Provides a dialogue function like chat.openai.com.

Flags:
  -h, --help                     Show context-sensitive help.
  -p, --profile=STRING           Specify the profile.
  -v, --version                  Print the version.

  -m, --model="gpt-3.5-turbo"    Specify the ID of the model to use(gpt-4, gpt-4-0314, gpt-4-32k, gpt-4-32k-0314, gpt-3.5-turbo,
                                 gpt-3.5-turbo-0301).
  -f, --file=FILE                Specify the chat history file with the full path.

チャットを再開する場合

-fオプションでチャット履歴ファイルをフルパスで指定します。

gif

テンプレート機能

chat機能は、設定ファイルを編集することで便利に扱えるようになります。

oax config --settings
~/.config/oax/settings.toml
[chat]
  # デフォルトで指定するモデルの設定(-mオプションでモデルを指定した場合はオプション優先)
  model = "gpt-3.5-turbo"

  # 新規でチャットを開始する際の会話テンプレートの設定
  [[chat.templates]]
    name = "friends"

    [[chat.templates.messages]]
      role = "system"
      content = "You are ChatGPT, a large language model trained by OpenAI. You are a friendly assistant that can provide help, advice, and engage in casual conversations."

以下のように-tコマンドでテンプレート名を指定して実行します。

oax chat -t friends

チャット履歴ファイルに指定したテンプレートの内容が挿入されて生成されます。

[[messages]] # <-- 挿入される
  role = "system"
  content = '''
You are ChatGPT, a large language model trained by OpenAI. You are a friendly assistant that can provide help, advice, and engage in casual conversations.
'''

[[messages]]
  role = "user"
  content = '''
# Remove this comment and specify content to send to OpenAI API; otherwise, nothing is sent.
'''

技術

Go

  • シングルバイナリで配布しやすい
  • チャンネルを使って簡単にマルチスレッドが可能
  • goreleaserのバイナリリリース配布機能が強力(クロスビルド、brew tap連携など)

今回ですと、後述するSSEのイベントをサブスクライブし、イベント取得したらチャンネルに送信して差分更新をし、Web版ChatGPTのような表示を実現しています。

https://github.com/shuntaka9576/oax/blob/1f254be80695c9a82a53c489d8c657c9ab7ff7c8/openai/chat.go#L45-L81

io.MultiWriterでbytes.Bufferと標準出力に書き込み、回答が返却されたタイミングでtomlファイルに出力しています。

https://github.com/shuntaka9576/oax/blob/main/cli/chat.go#L105-L194

Go経験が乏しく結構辛みのあるコードになっていますが、今後リファクタリングしていく所存です。

TOML

設定ファイル2種類とチャット履歴のファイルにTOMLを採用しています。これは'''で括ると改行にエスケープシーケンスを利用しなくて済むので可読性が上がり、採用しました。
ただTOMLにChatGPTの返信内容をデシリアライズして書き込むと'''表記にはならないので、ループで文字列のを繋げて出力しています。

https://github.com/shuntaka9576/oax/blob/1f254be80695c9a82a53c489d8c657c9ab7ff7c8/chat.go#L104-L110

OpenAI API

OpenAIのAPIの/v1/chat/completionsは、SSE(Sever-sent events)に対応しています。

GoでSSEのイベント取り回すライブラリでPOSTのSSEに対応しておらず、少しハマりました。詳しくはChatGPTのストリーミング(SSE)APIを試してみた(Go実装)を参照ください。Feature Request: Support non-GET method and request body in the sse clientでForkされたsseライブラリをgit submoduleで参照しています。

go.modでreplaceディレクティブを利用することで、インポートパスを自分のGitHubのOwner名に書き換えないですみます。

https://github.com/shuntaka9576/oax/blob/1f254be80695c9a82a53c489d8c657c9ab7ff7c8/go.mod#L16

git submoduleを使う場合は、actions/checkout@v3のsubmodulesオプションをtrueにする必要があります。
https://github.com/shuntaka9576/oax/blob/1f254be80695c9a82a53c489d8c657c9ab7ff7c8/.github/workflows/release.yml#L12-L16

bubbleteaの採用検討と見送り

bubblesのtextareaとviewportを使って、bubblesのサンプルのようなUIを作る予定でしたが、実力不足で見送りになりました。。

  • viewport
    • 1行の横幅が長い場合に改行されない
    • 自動スクロールを別途実装する必要がある
  • 標準出力をbubbletea側で握られており、今回の要件では逆に普通の標準出力できることをやろうとすると別途実装が必要だった

より研究して使いこなせるようにしていきたいです。

今後

個人的なユースケースとしては現状で満足していますが、以下の内容を今後対応したいと思います。

  • モデル指定のデフォルト設定を設定ファイルに外だしする v0.1.0にて対応
  • chat.openai.comのような会話履歴内容からタイトル要約し会話履歴ファイル名として保存する
  • テンプレート機能 v0.1.0にて対応
  • Windows対応(やりかけになっている、、)
  • 他のOpen AIのAPIで便利そうな機能の導入

多分公式からもっとすごいやつが出る気がしますが、薄く簡単に使えるツールとして差別化して改善しようと思います、、

脚注
  1. OpenAI の利用規約改定!!2023年3月1日更新 / API 経由は学習・訓練には使用されない。 ↩︎

Discussion