✍️

生成AIを使ったテキスト変換ツール: 翻訳、リファクタリング、Lint、レビュー(更新版)

2024/06/30に公開

生成AIを使ったテキスト変換ツール textforge

OpenAI API でテキストを変換・加工する CLI Tool textforgeを作ってみました。

ChatGPTやGithub Copilotなどの生成AIツールはとても便利で開発にも重宝しますが、チャットやIDE上での補完だけでなく、コマンドライン上で行うテキストの定型処理にも生成AIを活用したいと考えたのが開発のきっかけです。

Github でバイナリを公開しているので、よかったら使ってみてもらえると嬉しいです。ただし、利用するにはOpenAI APIキーが必要です。

textforgeでできること

  • 多様なテキスト変換処理: CLI上で様々なプロンプトを使ってテキスト変換を実行できます。
  • 豊富な入出力オプション: 標準入力からプロンプトを受け取る、複数ファイルを処理対象に指定できる、変換結果を上書き保存するなど、多彩なオプションがあります。
  • プロンプト補完機能: 入力されたプロンプトを補完してテキスト変換を実行します(時折、結果がうまく取得できないこともありますが、現在改良中です)。

textforgeでできないこと

  • 複数ファイルをまとめて入力トークンとして処理する: 複数ファイル扱うことはできますがファイル単体に対して繰り返し入力トークンを作成してを送信します。(一つにまとめることができるとコンテキスト情報が増えより精度が上がるのですが OpenAI の Chat APIでは今のところできないようです)
  • 大容量ファイルの処理: 数百キロバイトのファイルの場合、APIのレスポンストークンサイズ上限により途中で途切れることがあります。

使い方

[詳細は READMEを見てください。OpenAI APIキーが必要です。](https://github.com/ytka/textforge/blob/main/README.ja.md)

ユースケース

textforgeを開発する際に、textforgeを開発支援ツールとして使っていますが、以下のような使い方をしています。

  • コードレビューの自動化: git diffで変更したファイルをGoogleのGo Guidelineに基づいてコードレビューし、自動修正。
  • lintエラーの自動修正: lint fixで修正しきれないエラーを自動で修正。
  • ドキュメント翻訳: 日本語ドキュメントを英語に一括翻訳。
  • コミットメッセージの自動生成: git diffを元にコミットメッセージを自動生成。
  • READMEの自動更新: CLIのHELPを元にREADMEを更新。

これらの自動処理は Taskfileにタスク定義してCLIから利用しています。

textforgeの開発で実際に使いながら機能を拡張・修正していますが、READMEなどの開発ドキュメントの更新や翻訳をコマンド一発で実行してくれるのは便利です。
また、個人で一人だけで開発している場合でもプロンプトを工夫することで、コーディングガイドに基づいたレビュー・修正をしてもらうこともできます。PRに対してコメントを付けることはできませんが、CLIで上書き更新し、その場で diffを見るだけなのは手軽さがあります。

textforge自身で textforgeコードを修正した例

コメントの冒頭を大文字にする、数値リテラルを見やすくする、コメントを追加してくれています。

@@ -50,7 +50,7 @@ func init() {
 	rootCmd.Flags().StringVarP(&c.PromptPath, "prompt-path", "P", "", "Prompt file path")
 	rootCmd.Flags().BoolVarP(&c.PromptOptimize, "prompt-optimize", "O", true, "Optimize prompt text")
 
-	// model options
+	// Model options
 	rootCmd.Flags().StringVarP(&c.Model, "model", "m", "gpt-4o", "model to use for text generation")
 	rootCmd.Flags().IntVarP(&c.MaxTokens, "max-tokens", "t", 0, "Max tokens to generate")
 	rootCmd.Flags().IntVar(&c.MaxCompletionRepeatCount, "max-completion-repeat-count", 1, "Max completion repeat count")
diff --git a/internal/openai/pricing.go b/internal/openai/pricing.go
index 07dee4d..33584cd 100644
--- a/internal/openai/pricing.go
+++ b/internal/openai/pricing.go
@@ -8,7 +8,7 @@ type Pricing struct {
 	OutputTokens           float64
 }
 
-const OneMillion = 1000_000
+const OneMillion = 1_000_000
 
 var pricingList = []Pricing{
 	{Model: "gpt-4o", InputTokensCostDollar: 5, InputTokens: OneMillion, OutputTokensCostDollar: 15, OutputTokens: OneMillion},
@@ -16,6 +16,7 @@ var pricingList = []Pricing{
 	{Model: "gpt-3.5-turbo-0125", InputTokensCostDollar: 0.5, InputTokens: OneMillion, OutputTokensCostDollar: 1.5, OutputTokens: OneMillion},
 	{Model: "gpt-3.5-turbo-instruct", InputTokensCostDollar: 1.5, InputTokens: OneMillion, OutputTokensCostDollar: 2, OutputTokens: OneMillion},
 }
+
 var pricingMap = map[string]Pricing{}
 
 func init() {
diff --git a/internal/openai/totalusagecost.go b/internal/openai/totalusagecost.go
index 92f25b2..e45bc31 100644
--- a/internal/openai/totalusagecost.go
+++ b/internal/openai/totalusagecost.go
@@ -4,12 +4,14 @@ type TotalUsageCost struct {
 	UsageCosts []*UsageCost
 }
 
+// NewTotalUsageCost creates a new TotalUsageCost instance.
 func NewTotalUsageCost(usageCosts []*UsageCost) *TotalUsageCost {
 	return &TotalUsageCost{
 		UsageCosts: usageCosts,
 	}
 }

こちらは linterでの規約違反を自動修正した例です。

 package steps
 
 import (
+	"errors"
 	"fmt"
 	"os"
 )
 
+var ErrPromptRequired = errors.New("prompt is required")
+
 func GetPromptText(prompt, promptPath string) (string, error) {
 	if prompt == "" && promptPath == "" {
-		return "", fmt.Errorf("prompt is required")
+		return "", ErrPromptRequired
 	}
 
 	if prompt == "" && promptPath != "" {

linterの自動修正機能で修正できない場合でも修正してくれます。ただし、上手く修正できないケースや誤った修正をすることもあるので、そういった場合は問題のある箇所を revertしています。

プロンプト補完

与えられたプロンプトをそのまま送ると、レスポンスに不要な補足説明が含まれるなどの不都合があるため、そのままでは以下のような補完をした上で送信しています。

  • 入力と出力をタグ付けすることで、レスポンスから必要な結果だけを取り出しやすくしています。
  • 出力結果は、与えられたプロンプトの言語の基づいた言語で結果を返す。これがないと補完プロンプトが英語のため、入力プロンプトが日本語であっても英語で返ってくることがあります。
  • 入力にファイルを含める場合、ファイル内容だけでなくファイルパスも含めて渡しています。lintの全ファイルのエラー結果を渡した場合でも対象ファイルのみ修正できるようになります。

具体的な実装はこんなコードになります。

func optimizePrompt(inputFilePath, prompt, input string) string {
	supplements := []string{
		"The subject of the Instruction is the area enclosed by the textforge-input tag.",
		"The result should be returned in the language of the Instruction, but if the Instruction has a language specification, that language should be given priority.",
		"Wrap the result in a <textforge-output> tag and return it. Only results should be returned and no explanation or supplementary information is required, but additional explanation or details should be provided if explicitly requested in the instructions.",
	}
	supplementation := strings.Join(supplements, " ")
	header := ""
	if inputFilePath != "" && inputFilePath != "-" {
		header = fmt.Sprintf("filepath=\"%s\"\n", inputFilePath)
	}
	return fmt.Sprintf("<Instruction>%s. (%s)</Instruction>\n%s<textforge-input>\n%s\n</textforge-input>", prompt, supplementation, header, input)
}

作ってみた感想

OpenAI APIを初めて触りましたが、シンプルで使いやすかったです。チャットコンソールと同様にファイル添付にも対応してもらえるともっと用途が広がりそうです。本当はPDFなどのバイナリファイルの生成や変換もしたかったものの無理そうなので諦めました。

生成AI用のプロンプトについては、今回はプロンプトを固定せず、汎用の入力を与えられるようにしたかったため、プロンプトへの指示を補完して入力や結果にタグを付けるなど工夫しましたが、なかなか思う通りに結果が返ってこないこともありました。まだまだ工夫の余地がありそうだけど、できればAPI仕様レベルで解決できると嬉しいですね。

CLIにはGoを使いましたが、TUIフレームワークの Bubble TeaはリッチなTUIが簡単に書けるのと、GoReleaserもGithubリリースに対応していて非常に便利でした。

Discussion