この記事はKotlin Advent Calendar 2024 1日目の記事です。
KotlinからOpenAI APIを使う例を紹介したいと思います。
以前以下の発表で、KotlinとCloud Vision APIを使用して、領収書から金額などの情報を抽出する方法を話したことがありました。
詳細は資料の方に載っているので省きますが、電子帳簿保存法の対応のために
- 領収書から金額、日付を抽出する
- 抽出した情報を使用して領収書のファイル名を変更する
ということをやりたかったためです。
しかしこの時の方法では領収書のフォーマットごとに抽出のロジックを実装しなければならないため、あらゆる領収書に対応しようとすると手間がかかります。
そこで、生成AIで解析してフォーマット問わず抽出するようにできないかと考えていました。
今回はその実装をしてみたので、紹介します。
OpenAI APIをKotlinで使う
まず、KotlinでOpen AI APIを使う方法を紹介します。
事前準備
Open AI APIを使うには、いくつかの準備が必要です。
アカウント登録
まず、OpenAIのアカウントを持っていない場合は以下のページから登録が必要です。
API Keyの取得
登録が完了したらそのアカウントでログインします。
そしてAPI Keysのページへ移動します。
「Create new secret key」を選択します。
ダイアログで必要な情報を入力し、「Create secret key」を押すとAPI Keyが作成されます。
こちらを実装時に使用するので、保管しておいてください。
(画面を閉じると再表示できないため注意してください)
クレジットの購入
OpenAI APIは従量課金制で、使用する際はクレジットを購入してチャージする必要があります。
今度はBillingのページへ移動します。
「Add to credit balance」を選択します。
初めて購入する場合はIndividual(個人)かCompany(企業)かを選択します。
支払い方法の入力を求められるので、クレジットカードと住所(英語)を登録します。
そして、購入するクレジットの金額を入力して確定します。
この時に「Yes, automatically recharge...」をonにしてしまうと使い切った時に勝手にチャージされてしまうので、不正利用や予期せず叩きすぎてしまう可能性も踏まえ、offにしておく方が安全です。
API実行にかかる金額は以下に載っています。
正直読んでもあんまりピンと来ないかもしれませんが、私はこの記事を書くために何回も叩いて、$10買ったうちの$0.02分しか消費していません。
なのでちょっと検証してみるくらいであれば、最小限の金額で大丈夫です。
ここまでで事前準備は完了です。
実装
さて実装です。
適当なKotlinのプロジェクトを作り、build.gradle.ktsのdependenciesに以下の依存関係を追加します。
dependencies { implementation(platform("com.aallam.openai:openai-client-bom:3.8.2")) implementation("com.aallam.openai:openai-client") implementation("io.ktor:ktor-client-okhttp") testImplementation(kotlin("test")) }
OpenAI APIを扱うために、openai-kotlinというライブラリを使用しています。
com.aallam.openai:openai-client
と、そこから依存しているio.ktor:ktor-client-okhttp
を追加する必要があります。
そして以下のmain関数を作成します。
fun main() = runBlocking { // 作成したAPI Keyを設定する val apiKey = "xxxxxxxx" // OpenAIクライアントの初期化 val openAI = OpenAI(apiKey) // ChatGPTへのメッセージ val messages = listOf( ChatMessage( role = ChatRole.User, content = "こんにちは、元気ですか?", ) ) // ChatCompletionリクエストの作成 val request = ChatCompletionRequest( model = ModelId("gpt-4o-mini"), messages = messages, maxTokens = 50, ) // リクエストの送信と応答の取得 val response = openAI.chatCompletion(request) // 応答の表示 response.choices.forEach { choice -> println("ChatGPT: ${choice.message?.content}") } }
OpenAIクライアントの初期化
まず、以下のコードでOpenAI APIを実行するためのクライアントを初期化しています。
// 作成したAPI Keyを設定する val apiKey = "xxxxxxxx" // OpenAIクライアントの初期化 val openAI = OpenAI(apiKey)
変数apiKeyには先ほど作成したAPI Keyの値を設定します。
リクエストの作成
OpenAI APIへのリクエストを作成します。
// ChatGPTへのメッセージ val messages = listOf( ChatMessage( role = ChatRole.User, content = "こんにちは、元気ですか?", ) ) // ChatCompletionリクエストの作成 val request = ChatCompletionRequest( model = ModelId("gpt-4o-mini"), messages = messages, maxTokens = 50, )
ChatMessage
を使用して、roleと送信する内容(ChatGPTのプロンプトに当たる内容)を設定します。
シンプルに挨拶をするだけの内容になっています。
そしてChatCompletionRequest
に作成したChatMessage
を渡します。
また、model
プロパティで使用するGPTのモデルを指定します。
(ここでは最新のgpt-4o-miniを設定しています)
レスポンスの取得、表示
クライアントに作成したリクエストを渡して、結果を取得します。
// リクエストの送信と応答の取得 val response = openAI.chatCompletion(request) // 応答の表示 response.choices.forEach { choice -> println("ChatGPT: ${choice.message?.content}") }
chatCompletion
でAPIを実行しています。
そしてresponse.choices
から結果が取得できます。
以下のような実行結果になります。
ChatGPT: こんにちは!私は元気です、ありがとう。あなたはいかがですか?
挨拶が返ってきました。
GhatGPTのプロンプトに文章を入力して、返答が表示されるのと同じですね。
このように、openai-kotlinを使うことで実行自体は簡単にできます。
もし使わなかった場合は、自分でHttpClientを使って叩いたり、リクエストとレスポンスのdata classを作ったりする必要があります。
領収書のPDFから日付と金額を抽出する
では、今回作りたかった領収書から情報を抽出してファイルをリネームするコードを実装します。
やりたいこと
改めてやりたいことですが、以下になります。
- 領収書から取引先名、請求日、請求額の情報を取得する
- 取得した情報を使用してファイル名をリネームする
実装
build.gradle.ktsのdependenciesは以下のようになります。
dependencies { implementation(platform("com.aallam.openai:openai-client-bom:3.8.2")) implementation("com.aallam.openai:openai-client") implementation("io.ktor:ktor-client-okhttp") implementation("org.apache.pdfbox:pdfbox:2.0.32") testImplementation(kotlin("test")) }
前述のサンプルに加え、PDFファイルを処理するためのorg.apache.pdfbox:pdfbox
を追加しています。
コードは以下のようになります。
import com.aallam.openai.api.chat.ChatCompletionRequest import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatRole import com.aallam.openai.api.model.ModelId import com.aallam.openai.client.OpenAI import kotlinx.coroutines.runBlocking import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.text.PDFTextStripper import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption fun main() = runBlocking { // PDFファイルのパス val pdfPath = "/Users/hogehoge/receipt_sampl.pdf" // PDFをテキストに変換 val pdfText = PDDocument.load(File(pdfPath)).use { document -> val pdfStripper = PDFTextStripper() pdfStripper.getText(document) } // OpenAIクライアントを初期化 val apiKey = "xxxxxxxx" val openAI = OpenAI(apiKey) // テキストから日付と金額を抽出 val result = extractDateAndAmountFromText(openAI, pdfText) val resultMap = result.split("\n").map { it.split(":") }.associate { it[0].trim() to it[1].trim() } val companyName = resultMap["取引先名"] val date = resultMap["注文日"] val amount = resultMap["合計額"] val oldFile = File(pdfPath) val newFile = File("/Users/take7010/receipt/after/${date}_${companyName}_$amount.pdf") Files.copy(oldFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) println("done.") } suspend fun extractDateAndAmountFromText(openAI: OpenAI, text: String): String { val messages = listOf( ChatMessage( role = ChatRole.System, content = "あなたはテキストから取引先名と注文日と合計額を抽出するアシスタントです。" ), ChatMessage( role = ChatRole.User, content = """ 以下のテキストから取引先名と注文日と合計額を抽出してください。請求日はYYYYMMDD、合計額はカンマ、単位なしで数字だけにしてください。 テキスト: $text """.trimIndent() ) ) val request = ChatCompletionRequest( model = ModelId("gpt-4o-mini"), messages = messages ) val response = openAI.chatCompletion(request) return response.choices.firstOrNull()?.message?.content ?: "データが見つかりませんでした" }
PDFをテキスト化してOpenAI API に渡す処理
まず、OpenAI APIに直接PDFを渡すことはできないため、PDFの内容をテキスト化してリクエストとして渡します。
// PDFファイルのパス val pdfPath = "/Users/hogehoge/receipt_sampl.pdf" // PDFをテキストに変換 val pdfText = PDDocument.load(File(pdfPath)).use { document -> val pdfStripper = PDFTextStripper() pdfStripper.getText(document) } // OpenAIクライアントを初期化 val apiKey = "xxxxxxxx" val openAI = OpenAI(apiKey) // テキストから日付と金額を抽出 val result = extractDateAndAmountFromText(openAI, pdfText)
ファイルのパスを指定してPDDocument.load
で読み込み、PDFTextStripper.getText
で中身の文字列を取得します。
そして前述のサンプルと同様にOpenAIクライアントを初期化し、クライアントとPDFの内容の文字列をextractDateAndAmountFromText
関数(後述)に渡します。
OpenAI APIでテキストからデータの抽出をする処理
OpenAI APIの実行部分として、以下のextractDateAndAmountFromText
関数を定義しています。
suspend fun extractDateAndAmountFromText(openAI: OpenAI, text: String): String { val messages = listOf( ChatMessage( role = ChatRole.System, content = "あなたはテキストから取引先名と注文日と合計額を抽出するアシスタントです。" ), ChatMessage( role = ChatRole.User, content = """ 以下のテキストから取引先名と注文日と合計額を抽出してください。請求日はYYYYMMDD、合計額はカンマ、単位なしで数字だけにしてください。 テキスト: $text """.trimIndent() ) ) val request = ChatCompletionRequest( model = ModelId("gpt-4o-mini"), messages = messages ) val response = openAI.chatCompletion(request) return response.choices.firstOrNull()?.message?.content ?: "データが見つかりませんでした" }
リクエストとして渡すChatMessage
には、PDFから作成した文字列を含め、その中から欲しい情報(取引先名、注文日、合計額)を抽出する内容のプロンプトを設定しています。
また、日付と数値のフォーマットも扱いやすいように指定しています。
そして実行した結果を返却しています。
結果の値を使用してファイル名をリネームする処理
main関数の方に戻り、レスポンスの内容を使用してファイルをリネームする処理は以下になります。
val resultMap = result.split("\n").map { it.split(":") }.associate { it[0].trim() to it[1].trim() } val companyName = resultMap["取引先名"] val date = resultMap["注文日"] val amount = resultMap["合計額"] val oldFile = File(pdfPath) val newFile = File("/Users/take7010/receipt/after/${date}_${companyName}_$amount.pdf") Files.copy(oldFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
レスポンスには以下の文字列が結果として入っています。
取引先名: Amazon.co.jp 注文日: 20241201 合計額: 1000
そこから左側の項目名(取引先名、注文日、合計額)がkey、と右側の値がvalueになるMapを作成し、それぞれの項目を取得して変数に入れています。
(このレスポンスのデータの形式もプロンプトで指定した方が安全かもしれません)
そしてoldFile
に読み込んだPDFのパス、newFile
に取得した情報を使用したファイル名を含む出力先のパスを指定し、Files.copy
でリネームしたファイルを出力します。
これで完成です。
実行結果
ファイルパスを書き換え、以下のAmazonの領収書をリクエストに渡して実行してみます。
結果として以下のように、リネームされたファイルが出力されました。
beforeディレクトリ配下のsample_receipt.pdfをリネームして、afterディレクトリ配下に出力しています。
取引先名: Amazon.co.jp
注文日: 20240419
合計額: 1190
としてファイル名に含まれています。
Amazon以外の領収書も何個かやってみましたが、概ね正しい情報で作られていました。
ただ、ChatGPTなど海外サービスでドル表記などの領収書は、さらに請求時のレートで円に変換するなどひと手間必要そうです。
まとめ
openai-kotlin
を使用すれば簡単にKotlinから実行できる- どこまで上手く自分の欲しい情報にできるかはプロンプト次第
- ドル表記の領収書の対応など、まだ改善の余地はある
Kotlinでも結構簡単に使えることがわかったので、これから活用して色々と作ってみようかと思います。
宣伝: Kotlin愛好会の参加受付中です!
最後に宣伝です。
12/17(火)に、年内最後のKotlin愛好会を開催します!
現在参加受付中ですので、Kotlinについて語り合いたい方はぜひお申し込みください。
Kotlin Advent Calendar 2024で気になった記事の話なども大歓迎です!