大瀧
2023.03.23
1359
こんにちは、大瀧です。
最近はChatGPTくんのおかげで作業効率が爆上がりしています。
便利なものはどんどん使って効率化していきたいですね。
さて今回はタイトル通り、
OpenAI API と Deepl API を使ってターミナルから使えるChatGPTライクなCLIツールを作成したことについてお話しします。
こんな感じで使えます。
Deeplの翻訳をかけているせいで "fmt" がエフエムティーになっちゃってますね。
でも、これはこれでなんだか可愛らしいので良しとしましょう。
CLIにしたのは単純にvscodeのターミナルからChatGPTが使えたら便利かも、と思ったから。
ただ問題点として、OpenAI API単体でCLIツールにしても、日本語入力でリクエストを送ると回答の精度が著しく低下するということがあり頭を悩ませていました。
そこで入力した日本語を一度英語に翻訳してOpenAI APIにリクエストを投げることで回答の精度をあげることができるのではと思い、今回Deeplを使用することにしました。
chatgpt-cliという名前のファイルをダウンロードしてください。
もしくは自分のローカルに git pull してコンパイルしてもらっても構いません。
$ vim ~/.zshrc
で設定ファイルを開き、下記を追記します。
export PATH=$PATH:$HOME/追加するディレクトリ
#自分の場合はこんな感じにしました
export PATH=$PATH:$HOME/my_commands
$ source ~/.zshrc
で設定を再読み込みします。
ダウンロードもしくはコンパイルしたバイナリファイルを、先ほど作成したディレクトリ配下に移動させます。
chmod u+x /<ディレクトリパス>/<ファイル名>
でファイルに実行権限を付与してあげます。
OpenAI APIはここから
https://openai.com/blog/openai-api
Deepl APIはここから
https://www.deepl.com/ja/pro/change-plan?cta=header-prices#developer
登録してAPIキーを発行します。
取得できたら.zshrcに下記を追記
export DEEPL_API_KEY=<DeeplAPIのAPIキー>
export OPENAI_API_KEY=<OpenAIAPIのAPIキー>
$ source ~/.zshrc
で再読み込み
ここまで設定したらいつでもどこでもコマンドが叩けるはず。
コマンドはバイナリファイル名なので chatgpt-cli
とターミナルで入力してEnterを押せば入力プロンプトが表示されます。
メインとなる処理。
処理の流れとしては
入力→Deeplで日本語から英語に翻訳→OpenAI APIに投げる→回答を英語から日本語に変換→出力
をひたすらループしています。
package main
import (
"fmt"
"os"
"strings"
openai "./openai"
deepl "./deepl"
)
func main() {
deeplClient, openaiClient := initClient()
for {
input, err := getInputFromUser()
if err != nil {
fmt.Println("入力の読み取りに失敗しました:", err)
continue
}
translated, err := deeplClient.Translate([]string{input}, "EN")
if err != nil {
fmt.Println("翻訳に失敗しました:", err)
continue
}
response, err := openaiClient.GetChatResponse(translated)
if err != nil {
fmt.Println("OpenAI APIに接続できませんでした:", err)
continue
}
if len(response) != 0 {
result, err := deeplClient.Translate(response, "JA")
if err != nil {
fmt.Println("翻訳に失敗しました:", err)
continue
}
fmt.Printf(">> answer : %s\n", result)
}
}
}
func initClient() (*deepl.Client, *openai.Client) {
deeplAPIKey := os.Getenv("DEEPL_API_KEY")
if deeplAPIKey == "" {
fmt.Println("DEEPL_API_KEYが設定されていません。")
return nil, nil
}
deeplClient := deepl.NewClient(deeplAPIKey)
openaiAPIKey := os.Getenv("OPENAI_API_KEY")
if openaiAPIKey == "" {
fmt.Println("OPENAI_API_KEYが設定されていません。")
return nil, nil
}
openaiClient := openai.NewClient(openaiAPIKey)
return deeplClient, openaiClient
}
func getInputFromUser() (string, error) {
fmt.Print(">> question : ")
var input string
_, err := fmt.Scanln(&input)
if err != nil {
return "", err
}
return strings.TrimSpace(input), nil
}
リクエストを作成してOpenAI APIに向かって投げているだけです。
package openai
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
)
const (
baseURL = "https://api.openai.com/v1"
endpoint = "/chat/completions"
model = "gpt-3.5-turbo"
role = "user"
temperature = 0.9
max_tokens = 1000
)
type Client struct {
apiKey string
client *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
client: &http.Client{},
}
}
// レスポンス
type Response struct {
ID string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
Choices []Choices `json:"choices"`
}
type Choices struct {
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
}
// リクエスト
type Request struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
func (c *Client) GetChatResponse(prompt string) ([]string, error) {
requestBody := Request{
Model: model,
MaxTokens: max_tokens,
Messages: []Message{{
Role: role,
Content: prompt,
}},
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
request, err := http.NewRequest("POST", baseURL+endpoint, bytes.NewBuffer(requestBodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create API request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer "+c.apiKey)
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %w", err)
}
defer response.Body.Close()
var responseBody Response
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
if len(responseBody.Choices) == 0 {
return nil, errors.New("ChatGPT API returned empty response")
}
var res []string
for _, r := range responseBody.Choices {
res = append(res, r.Message.Content)
}
return res, nil
}
こちらも基本的にリクエストを作成してDeepl APIにリクエストを投げているだけです。
package deepl
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
)
const (
baseURL = "https://api-free.deepl.com/v2"
)
type Client struct {
apiKey string
client *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
client: &http.Client{},
}
}
type Translation struct {
DetectedSourceLanguage string `json:"detected_source_language"`
Text string `json:"text"`
}
type TranslationResponse struct {
Translations []Translation `json:"translations"`
}
func (c *Client) Translate(text []string, targetLang string) (string, error) {
requestBody, err := json.Marshal(map[string]interface{}{
"text": text,
"target_lang": targetLang,
})
if err != nil {
return "", fmt.Errorf("failed to marshal request body: %w", err)
}
request, err := http.NewRequest("POST", baseURL+"/translate", bytes.NewReader(requestBody))
if err != nil {
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
q := request.URL.Query()
q.Add("auth_key", c.apiKey)
request.URL.RawQuery = q.Encode()
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", fmt.Errorf("failed to send HTTP request: %w", err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("failed to read HTTP response: %w", err)
}
var responseBody TranslationResponse
err = json.Unmarshal(body, &responseBody)
if err != nil {
return "", fmt.Errorf("failed to decode response body: %w", err)
}
if len(responseBody.Translations) == 0 {
return "", errors.New("DeepL APIから回答がありませんでした")
}
return responseBody.Translations[0].Text, nil
}
最初OpenAI APIだけで作った時には全く使い物にならなかったですが、Deeplと組み合わせただけでかなり精度が上がりました。
生成系AIはこれからもどんどん盛り上がっていきそうなので、こうして遊びながら触っていこうと思います。
またCLIツールとしても、改行に対応して複数行の質問を投げることができるようにしたり、生成されたプログラム部分にはDeeplの翻訳をかけないようにするなど、まだまだ改善できそうな点があるのでChatGPTくんと協力しながらちょくちょくアップデートしていけたらなと思っています。