はじめに
AIアシスタントと対話するためのコマンドラインツール「gpt-cli」の紹介です。これはOpenAIやAnthropicのAIモデルとターミナル上で対話することができるAPIクライアントです。
お使いのエディタがVimであれば、後述するVimプラグインの設定をすることでコーディングの補助やドキュメント生成、テストコード作成など、様々な用途に活用できます。
Vimエディタをお使いでない方も、ターミナル上でAIと対話できることの便利さを実感していただけると思います。
使い方
gptコマンド
インストールとAPIキーのセットアップが済んでいることが前提です。
$ gpt -m "モデル名" --max-tokens 100 -s 'システムプロンプト' 'ユーザープロンプト'
その他オプションはオプション参照
少しだけ実装の話。
メイン関数はコマンドラインをパースしてLLM
クラスのインスタンスを作成し、`llm.ask()関数へ渡します。
async function main() {
// Parse command argument
const params = parseArgs();
// console.debug(params);
if (params.version) {
console.error(`gpt ${VERSION}`);
Deno.exit(0);
}
if (params.help) {
console.error(helpMessage);
Deno.exit(0);
}
// create LLM モデルの作成
const llm = createLLM(params);
const messages: Message[] = params.content !== undefined
? [{ role: Role.User, content: params.content }]
: [];
// query LLM 一回限りの応答
if (params.no_conversation) {
const content = await llm.query(messages);
console.log(content);
return;
}
// ask LLM 対話的応答
console.log("Ctrl-D to confirm input, q or exit to end conversation");
llm.ask(messages);
}
モデル名の始まりの文字でモデルごとのクラスを使い分けます。
function createLLM(params: Params): LLM {
if (params.model.startsWith("gpt")) {
return new GPT(
params.model,
params.temperature,
params.max_tokens,
params.system_prompt,
);
} else if (params.model.startsWith("claude")) {
return new Claude(
params.model,
params.temperature,
params.max_tokens,
params.system_prompt,
);
} else {
throw new Error(`invalid model: ${params.model}`);
}
}
例えばGPT
クラスのask()
メソッドは次の役割を非同期に行います。Claude
クラスも同様です。
- 入力の受付
- スピナーの開始、終了
- GPT APIの入出力
- 1文字ずつの出力
class GPT implements LLM {
// ...snip
/** ChatGPT へ対話形式に質問し、回答を得る */
public async ask(messages: Message[]) {
messages = await setUserInputInMessage(messages);
if (this.system_prompt) {
messages = this.pushSustemPrompt(messages);
}
const spinnerID = spinner.start();
// POST data to OpenAI API
const resp = await this.agent(messages);
spinner.stop(spinnerID);
messages = await this.print(resp, messages);
await this.ask(messages);
}
LLMインターフェイスで複数のAPIを規格化します。
-
agent
はAPIのインスタンスです。 -
ask()
は入出力プロンプトをやり取りします。 -
query()
は一度限りの回答をしてコマンドを終了します。 -
getContent()
はAPIから返ってきたJSONの出力プロンプトのみを取得します。
interface LLM {
agent: (messages: Message[]) => Promise<void>;
ask(messages: Message[]): Promise<void>;
query(messages: Message[]): Promise<string>;
getContent(data: Response): string;
}
APIの出力を待っている間のロードスピナーの実装です。
id = spinner.start()
でスタートして、spinner.stop(id)
で止めます。
コンストラクタの第1引数にスピナー表示の文字列アレイ、第2に引数にスピナーが切り替わる間隔をmsecで指定します。
/** 戻り値のIDがclearInterval()によって削除されるまで
* ., .., ...を繰り返しターミナルに表示するロードスピナー
* usage:
* const spinner = new Spinner([".", "..", "..."], 100);
* const spinnerID = spinner.start();
* // processing...
* spinner.stop(spinnerID);
*/
class Spinner {
constructor(
private readonly texts: string[],
private readonly interval: number,
) {}
start(): number {
let i = 0;
return setInterval(() => {
i = ++i % this.texts.length;
Deno.stderr.writeSync(new TextEncoder().encode("\r" + this.texts[i]));
}, this.interval);
}
/** Load spinner stop */
stop(id: number) {
clearInterval(id);
}
}
const spinner = new Spinner([".", "..", "..."], 100);
class GPT implements LLM {
// ...snip
// print1by1() の完了を待つために
// async (data)として、print1by1()をawaitする
public async print(response: Response, messages: Message[]) {
if (response.error) {
throw new Error(`Fetch request failed: ${response.error}`);
}
const content = this.getContent(response);
// assistantの回答をmessagesに追加
messages.push({ role: Role.Assistant, content: content });
// console.debug(messages);
await print1by1(`\n${this.model}: ${content}`);
return messages;
}
}
// 渡された文字列を1文字ずつ20msecごとにターミナルに表示する
export async function print1by1(str: string): Promise<void> {
str += "\n";
return await new Promise((resolve) => {
let i = 0;
const intervalId = setInterval(() => {
Deno.stdout.writeSync(new TextEncoder().encode(str[i]));
i++;
if (i === str.length) {
clearInterval(intervalId);
resolve();
}
}, 20);
});
}
v0.3.4時点での実装です。
詳細はリポジトリを覗いてください。
ご挨拶
gpt
コマンドを実行するとLLMモデルとの会話が始まります。
Claudeとも会話できます
デフォルトではOpenAI社のLLM GPT-3.5-turbo と会話します。--model
または-m
オプションでモデルを指定することで、モデルを指定できます。GPT4やAnthropic社のLLM Claudeとも会話できます。
引数を渡すと即座にそのプロンプトに基づいて会話が始まります。
例えば、gpt-cli.ts
の内容の一部をtail
コマンドを使ってプロンプトに含めると、コードの解説をしてくれました。
役割を与える
system prompt(-s) オプションに役割を与えることができます。
同じhello worldを渡しても、システムプロンプトを与えない場合では"Hello there! I'm ..."と挨拶を返すのに対して、システムプロンプトに'generate python code'を指定すると、print("hello world")
を返してきました。
Vim plugin1
Github Copilot的なことができます。
Vimmerの方以外は残念ながらまとめまで読み飛ばしてもらって結構です。
コメントからコードを生成してもらう
:GPTGenerateCode
で現在行のfizz buzz
をユーザープロンプトに追加してコードを生成してもらいます。
:GPTGenerateCodeには次を設定しています。
command! -nargs=0 -range GPTGenerateCode
\ <line1>,<line2>call gptcli#GPT('You are best of code generator.
\ Generate a prompt to continue coding based on the given input code.
\ Generate only code effectively, DO NOT generate descriptions nor code blocks.
\ If you need describe code, please comment out it.',
\ { "max_tokens": 400, "temperature": 0.5, "model": "claude-3-haiku-20240307" })
つまり、
$ gpt --max-tokens 400 \
--temperature 0.5 \
--model claude-3-haiku-20240307 \
--system-prompt 'You are best of code generator. {...snip} \
'{選択範囲}'
を実行して、その結果を選択範囲の最後に書き込みます。ここでは選択範囲にfizz buzz
を渡しましたので、Claude3 HaikuがPythonでfizz buzzのコードを生成してくれました。
autoloadのGPT()関数
gpt-cli/autoload/gptcli.vim
で次のGPT()
関数を読み込みます。
" Usage
" call gptcli#GPT("your system prompt")
" call gptcli#GPT("your system prompt", {model="gpt-3.5-turbo", max_tokens=1000, temperature=1.0})
" 上に挙げたkwargsはgptのデフォルト値なので指定しなければこの値でgptが実行される。
function! gptcli#GPT(system_prompt, kwargs={}) range
" \ max_tokens=1000,
" \ temperature=1.0,
" \ model="gpt-3.5-turbo") range
" filetype &ft はGPTの実行先ファイルに応じて取得する
" シングルクォートで囲まないと特殊文字をshellコマンドとして渡すときにエラー
let l:args = ["gpt", "-n"]
if a:system_prompt != ""
if &ft != ""
let syntax = " Use syntax of " . &ft
let l:system_prompt = "'" . a:system_prompt . l:syntax . ".'"
else
let l:system_prompt = "'" . a:system_prompt . ".'"
endif
call add(l:args, "-s")
call add(l:args, l:system_prompt)
endif
" オプションの追加
" gptのmodelのデフォルトはgpt-3.5-turbo
if has_key(a:kwargs, "model")
call add(l:args, "-m")
call add(l:args, a:kwargs["model"])
endif
" gptのmax_tokensのデフォルトは1000
if has_key(a:kwargs, "max_tokens")
call add(l:args, "-x")
call add(l:args, a:kwargs["max_tokens"])
endif
" gptのmax_tokensのデフォルトは1000
if has_key(a:kwargs, "temperature")
call add(l:args, "-t")
call add(l:args, a:kwargs["temperature"])
endif
" 範囲指定をuser_promptとして使う。
" 範囲指定がない場合は、現在のカーソル位置の行を使用する。
let lines = getline(a:firstline, a:lastline)
let l:user_prompt = "'" . join(lines, "\n") . "'"
" ユーザープロンプトを追加
call add(l:args, l:user_prompt)
let l:cmd = join(l:args)
echo l:cmd
" コマンドを実行して選択範囲の最終行以降に追加する。
let l:result = systemlist(l:cmd)
call append(a:lastline, l:result)
endfunction
好きな言語でコードを生成してもらう
現在のfiletypeを認識してその言語でコードを返します。
先程のGifではno ft
ですのでAIが得意な言語のPythonを返してきましたが、こちらのGifでは:set ft=javascript
としてJavaScriptに設定することで、:GPTGenerateCode
に言語を明示しなくても、JavaScriptでfizz buzzのコードを生成してくれます。
GPT()のこの部分でftを読み込んでsystem promptに渡します。
{...snip}
if a:system_prompt != ""
if &ft != ""
let syntax = " Use syntax of " . &ft
let l:system_prompt = "'" . a:system_prompt . l:syntax . ".'"
else
let l:system_prompt = "'" . a:system_prompt . ".'"
endif
call add(l:args, "-s")
call add(l:args, l:system_prompt)
endif
{...snip}
つまり、system promptにUse syntax of javascript.
が追加されます。
テストコードを生成してもらう
こちらのGifでは選択範囲のコードを:GPTGenerateTest
でテストコードを生成して解説を加えてくれています。
:GPTGenerateTestには次を設定しています。
command! -nargs=0 -range GPTGenerateTest
\ <line1>,<line2>call gptcli#GPT('You are the best code tester.
\ Please write test code that covers all cases to try the given code.',
\ { "temperature": 0.5, "model": "claude-3-haiku-20240307" })
しかし、解説が英語で返されているので、分かりづらいですね。
日本語で解説してもらう
選択範囲を:GPTGenerateDocs
に与えて、日本語でテストコードの解説をしてくれています。
:GPTGenerateDocsには次を設定しています。
command! -nargs=0 -range GPTGenerateDocs
\ <line1>,<line2>call gptcli#GPT('あなたは最高のコードライターです。
\ 与えられたコードに基づいてわかりやすい日本語のドキュメントを
\ コメントアウトして生成してください。',
\ {"max_tokens": 2000, "model": "claude-3-haiku-20240307"})
ご覧のように、お好きなプロンプトをプリセットしておくことで、簡単に生成AIをコーディングに取り込めます。
入力補完
キーバインドC-X,C-* で様々な補完が使えることはVimmer諸兄ならご存知のことかと思います。
例えば下記のようにしてマッピングを割り当てると、「コメントを書いてコード実装」したり、「最初のコードを自身が書いて残りをAIが実装」することができます。
" C-X, C-Gで:GPTGenerateCode
inoremap <C-x><C-g> <Esc>:GPTGenerateCode<CR>
コマンド入力を省略した、いわゆる「略式詠唱」みたいなことでしょうか。
GPT()
関数は選択範囲を渡さないと現在行を基に補完を行います。
コード生成に限らず、好きなコマンド、好きなプロンプトを好きなマッピング2に割り当てられます。
任意のプロンプト
PythonコードをGo言語に書き直してもらっています。
先程Pythonで書いてもらったfizz buzzコードを選択して、コマンドには:GPTComplete Go言語で書き直して
と入力します。
:GPTCompleteには次を指定しています。
command! -nargs=? -range GPTComplete
\ <line1>,<line2>call gptcli#GPT(<q-args>,
\ { "model": "claude-3-haiku-20240307" })
対話する
別ウィンドウにターミナルを開いて対話することができます。ウィンドウ間でコードをコピペできるので便利です。
コーディングして、相談して、またしばらくコーディングして、詰まったら自分のコードをコピペして、相談して...ができるので快適です。
:GPTConversateには次を設定しています。
q-argsで引数を渡せばシステムプロンプトを設定できます。
例えばGifでは関西弁で話してもらうように命じました。
command! -nargs=? GPTConversate
\ call gptcli#GPTWindow(<q-args>,
\ {"model": "claude-3-sonnet-20240229" })
GPTWindow()関数は次のようになっています。
function! gptcli#GPTWindow(system_prompt="", kwargs={})
" \ max_tokens=1000,
" \ temperature=1.0,
" \ model="gpt-3.5-turbo")
" gptを起動するコマンドを構築する
let l:args = ["gpt"]
" system_promptがあれば追加
if a:system_prompt != ""
call extend(l:args, [ "-s", a:system_prompt ])
endif
" gptのmodelのデフォルトはgpt-3.5-turbo
if has_key(a:kwargs, "model")
call add(l:args, "-m")
call add(l:args, a:kwargs["model"])
endif
" gptのmax_tokensのデフォルトは1000
if has_key(a:kwargs, "max_tokens")
call add(l:args, "-x")
call add(l:args, a:kwargs["max_tokens"])
endif
" gptのtemperatureのデフォルトは1.0
if has_key(a:kwargs, "temperature")
call add(l:args, "-t")
call add(l:args, a:kwargs["temperature"])
endif
echo join(l:args)
" 新しいWindowでterminalでgptコマンドを実行する
let l:cmd = ["new", "|", "term"]
call extend(l:cmd, l:args)
execute join(l:cmd)
" call setline(1, l:user_prompt) " システムプロンプトを最初の行に設定
endfunction
インストール
3つのオプションがあります。
バイナリをダウンロードする
最も簡単な方法です。
ただし、リポジトリまたはReleaseを見て最新バージョンであることを確認してください。
$ curl -LO https://github.com/u1and0/gpt-cli/releases/download/v0.3.4/gpt-cli-linux.zip
$ unzip gpt-cli-linux.zip
$ chmod 755 gpt
$ sudo ln -s ./gpt /usr/bin
$ gpt -v
gpt-cli-macos.zipとgpt-cli-windows.zipもあります3。
zipファイル名を入れ替えてみてください。
ランタイムを使う
denoが必要です。
deno installを使って
~/.deno/bin`にリンクを作成します。
$ git clone https://github.com/u1and0/gpt-cli
$ cd gpt-cli
$ deno install -f --allow-net --allow-env --name gpt gpt-cli.ts
$ export PATH=$PATH:~/.deno/bin
$ bash -l
ソースからコンパイル
denoが必要です。
deno compile
でバイナリを作成します。
$ git clone https://github.com/u1and0/gpt-cli
$ cd gpt-cli
$ deno compile --allow-net --allow-env --no-check --output gpt gpt-cli.ts
$ chmod 755 ./gpt
$ sudo ln -s ./gpt /usr/bin
セットアップ
API キー
OpenAI API (GPT)
OpenAI APIキーの取得を行い、環境変数を設定します。
$ export OPENAI_API_KEY='sk-*****'
Anthropic API (Claude)
Anthropic APIキーを取得し、環境変数を設定します。
$ export ANTHROPIC_API_KEY='sk-ant-*****'
Windowsユーザーはシステムのプロパティから環境変数に設定してください。
Vimプラグイン
VimにGithub Copilotのような体験をもたらします。
例えば、deinでプラグインを管理したい場合、以下のようなtomlファイルを書きます。
[[plugins]]
repo = 'u1and0/gpt-cli'
if = '''executable('gpt')'''
hook_add = '''
command! -nargs=0 -range GPTGenerateCode <line1>,<line2>call gptcli#GPT('You are best of code generator. Generate a prompt to continue coding based on the given input code. Generate only code effectively, DO NOT generate descriptions nor code blocks. If you need describe code, please comment out it.', { "max_tokens": 400, "temperature": 0.5, "model": "claude-3-haiku-20240307" })
" Keybind C-X, C-G
inoremap <C-x><C-g> <Esc>:GPTGenerateCode<CR>
" Docs to code
command! -nargs=0 -range GPTGenerateDocs <line1>,<line2>call gptcli#GPT('あなたは最高のコードライターです。 与えられたコードに基づいてわかりやすい日本語のドキュメントをコメントアウトして生成してください。', {"max_tokens": 2000, "model": "claude-3-haiku-20240307"})
" Create test code
command! -nargs=0 -range GPTGenerateTest <line1>,<line2>call gptcli#GPT('You are the best code tester. Please write test code that covers all cases to try the given code.', { "temperature": 0.5, "model": "claude-3-haiku-20240307" })
" Any system prompt
command! -nargs=? -range GPTComplete <line1>,<line2>call gptcli#GPT(<q-args>, { "model": "claude-3-haiku-20240307" })
" Conversate with GPT
command! -nargs=? GPTConversate call gptcli#GPTWindow(<q-args>, {"model": "claude-3-sonnet-20240229" })
'''
GPT()関数に独自のプロンプトを設定することで、AIは選択範囲を基に補完します。
GPTWindow()関数を使えば、コマンドラインにケースバイケースのシステムプロンプトを置くことで、ターミナルを開き、Vim上でAIと対話することができます。
オプション
短いオプション | 長いオプション | 型 | 説明 |
---|---|---|---|
-v | --version | boolean | バージョンを表示する。 |
-h | --help | boolean | ヘルプメッセージを表示する。 |
-m | --model | string | OpenAI または Anthropic モデル (gpt-4, claude-instant-1.2, claude-3-opus-20240229, claude-3-haiku-20240307, デフォルト gpt-3.5-turbo) |
-x | --max_tokens | number | 回答の最大トークンの数(デフォルト1000) |
-t | --temperature | number | 数値が高いほどクリエイティブな回答、低いほど正確な回答(デフォルト1.0) |
-s | --system-prompt | string | AIモデルの返答を導くために与えられる最初の指示 |
-n | --no-conversation | boolan | 会話モードなし。1回の質問と回答のみ。 |
まとめ
本記事では、AIアシスタントと対話できるコマンドラインツール「gpt-cli」とそのVimプラグインについて解説しました。
gpt-cliを使えば、ターミナルから離れることなくAIモデルと簡単に対話することができます。単なる質問応答だけでなく、shellの入出力を利用できる点が強みです。
また、Vimプラグインでは、エディタ上でAIとインタラクティブに作業できる点が大きな魅力です。コメントからコードを生成したり、テストコードを自動生成したり、任意のプロンプトに応じた回答を得られるため、コーディングの効率が大幅に向上します。
AIを活用したプログラミング作業を体験してみたい方は、ぜひgpt-cliを試してみてはいかがでしょうか。AIの時代に向けて、新しいプログラミングスタイルを手に入れられるはずです。
-
補完といえばC-X
で始まるものが一般的です。
h:compl`でデフォルトのマッピングを見ることができます。衝突しないように気をつけましょう。 ↩