OpenAIとDeeplのAPIを使ってChatGPTのCLIツール作ってみた

大瀧

2023.03.23

248

こんにちは、大瀧です。


最近はChatGPTくんのおかげで作業効率が爆上がりしています。

便利なものはどんどん使って効率化していきたいですね。


さて今回はタイトル通り、

OpenAI API と Deepl API を使ってターミナルから使えるChatGPTライクなCLIツールを作成したことについてお話しします。

こんな感じで使えます。



Deeplの翻訳をかけているせいで "fmt" がエフエムティーになっちゃってますね。

でも、これはこれでなんだか可愛らしいので良しとしましょう。

作った背景

CLIにしたのは単純にvscodeのターミナルからChatGPTが使えたら便利かも、と思ったから。

ただ問題点として、OpenAI API単体でCLIツールにしても、日本語入力でリクエストを送ると回答の精度が著しく低下するということがあり頭を悩ませていました。

そこで入力した日本語を一度英語に翻訳してOpenAI APIにリクエストを投げることで回答の精度をあげることができるのではと思い、今回Deeplを使用することにしました。

使い方

バイナリファイルをダウンロードする

chatgpt-cliという名前のファイルをダウンロードしてください。

もしくは自分のローカルに git pull してコンパイルしてもらっても構いません。

https://github.com/yootaki/chat-cli

自作コマンド用のディレクトリを作成してPATHを通す

$ vim ~/.zshrc で設定ファイルを開き、下記を追記します。

export PATH=$PATH:$HOME/追加するディレクトリ

#自分の場合はこんな感じにしました
export PATH=$PATH:$HOME/my_commands

$ source ~/.zshrc で設定を再読み込みします。

ダウンロードしたバイナリファイルを先ほど作成したディレクトリに入れる

ダウンロードもしくはコンパイルしたバイナリファイルを、先ほど作成したディレクトリ配下に移動させます。

実行権限を付与する

chmod u+x /<ディレクトリパス>/<ファイル名>でファイルに実行権限を付与してあげます。

環境変数にOpenAI APIとDeepl APIのAPIキーを設定する

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を押せば入力プロンプトが表示されます。

実装について

main.go

メインとなる処理。

処理の流れとしては

入力→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.go

リクエストを作成して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.go

こちらも基本的にリクエストを作成して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くんと協力しながらちょくちょくアップデートしていけたらなと思っています。

この記事をシェアする