Home About Contact
GPT , LLM , Kotlin Script

カタログスペックテキストから特定の情報を抜き出す GPT-4o を使用

以前のエントリーでカタログスペックテキストから 日本語LLM ELYZA を使って情報を抜き出すことを試しました。 これを GPT-4oで試してみます。 今回は Python ではなく Kotlin スクリプトを使い、直接 OpenAI API にアクセスします。

Open AI API の Chat Completions API の使い方はこちらをご覧ください。

もし GPT-4o が十分に性能が高ければカタログスペックテキスト校正の支援ができるのではないか?と期待したり。

環境

$ kotlin -version
Kotlin version 1.8.10-release-430 (JRE 17.0.10+7-Ubuntu-122.04.1)

コード

問い合わせ用の JSON文字列を組み立てて API に投げて結果をもらうだけです。 メインの部分のコードは次の通り。

// main
val apiKey = toOpenaiAPIKey()
if( apiKey=="unknown" ){ System.exit(0) }

val uri = URI("https://api.openai.com/v1/chat/completions")
val m = "gpt-4o-2024-05-13"
val s = "You are a helpful assistant."
val u = """次の例文から原料名と原産地名をセットにして抜き出して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)"""

val p = toDefaultChatCompletionParams(m, s, u)
val q = toQueryJsonObject(p)
val a = invoke(uri, q, apiKey)
println(a)

コード全体はあとで掲載します。

以前 は原産地名のみの抜きだしを依頼しましたが、今回は原料名と原産地をセットで答えてもらうことにしました。

結果はこれ:

- ブロッコリー - エクアドル
- じゃがいも - 国産
- 植物油脂 - (原産地は明記されていません)
- 殻付き海老 - インド
- いか - 中国

原産地指定がない原料名もリストされました。これはよいです。

参考までに toQueryJsonObject(p) として組み立てたJSONはこれ:

{
  "temperature":0.8,
  "messages": [
    {
      "role":"system",
      "content":"You are a helpful assistant."
    },
    {
      "role":"user",
      "content":"次の例文から原料名と原産地名をセットにして抜き出して箇条書きにしてください。\n例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)"
    }
  ],
  "model":"gpt-4o-2024-05-13"
}

temperature は 0.8 にしていますが、DTPなどの厳密な校正に使う場合は 0 などにした方がよいかもしれません。(まだ試していません。)

GPT-4o からの回答の JSON はこれです。

{
  "id": "chatcmpl-XXX",
  "object": "chat.completion",
  "created": 1715830708,
  "model": "gpt-4o-2024-05-13",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "- ブロッコリー - エクアドル\n- じゃがいも - 国産\n- 植物油脂 - (原産地は明記されていません)\n- 殻付き海老 - インド\n- いか - 中国"
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 94,
    "completion_tokens": 59,
    "total_tokens": 153
  },
  "system_fingerprint": "fp_xxx"
}

もし、別の例でもこのようにうまくいくとしたら・・・ですが、 校正のための事前データチェックなどに使える可能性があるかもしれません。

つまり、この例でいえば特定の原料名には必ず原産地(または原産国)の表記が必要、という校正チェック項目があったとして、 それをこのようなプロンプトを使ってカタログ全体のスペックテキストに対して機械的に一括処理をして一覧表(エクセルデータ化)を作成。 その後、その一覧表に対して(これは古典的な)スクリプトで抜け漏れチェックをする、といったワークフローが考えられます。

また、GPT-4o は画像も扱えるようなので(まだ試していません)、 たとえば、「カラーバリエーションがある商品については必ず色情報について言及する必要がある」といった 校正条件があった場合に、画像とテキストをセットで見せることで、カラバリなのに色の説明がない ことを GPT-4o に指摘させることができるかもしれません。

もっとも、そもそも商品画像自体にカラーバリエーションを持つかどうか判定できる情報が含まれていない場合がある、という話があります。 そういうことまで考えると、いくら GPT-4o が優秀になったとしてもそれで太刀打ちできるほど DTPの校正 は易しいものではない気もします、残念ながら。

まとめ

先に掲載した main 部分のコードを動かすために必要な関数(などを)書いた残りのスクリプトを掲載してまとめとします。

環境変数 OPENAI_API_KEY にキーが設定されている必要がある点に注意してください。

val toOpenaiAPIKey: ()->APIKey = {
    System.getenv().get("OPENAI_API_KEY")?:"unknown"
}

gpt4o.main.kts

@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.json:json:20240303")

import java.net.HttpURLConnection
import java.net.URI
import java.io.DataOutputStream
import java.io.BufferedReader
import java.io.InputStreamReader

import org.json.JSONObject
import org.json.JSONArray

typealias APIKey = String
typealias ModelName = String
typealias SystemMessage = String
typealias UserMessage = String


val toOpenaiAPIKey: ()->APIKey = {
    System.getenv().get("OPENAI_API_KEY")?:"unknown"
}

data class ChatCompletionParams(
    val modelName: String,
    val systemMessage: String,
    val userMessage: String,
    val temperature: Float)

val toDefaultChatCompletionParams: (ModelName, SystemMessage, UserMessage)->ChatCompletionParams = { m, s, u->
    ChatCompletionParams(m, s, u, 0.8f)
}

val doPost: (URI, JSONObject, APIKey)->String = { uri, jsonObject, apiKey->
    val method = "POST"
    val bytes = jsonObject.toString().toByteArray(Charsets.UTF_8)

    (uri.toURL().openConnection() as HttpURLConnection).let { connection->
        connection.setRequestMethod(method)

        //connection.setDoInput(true)
        connection.setDoOutput(true)

        connection.setFixedLengthStreamingMode(bytes.size)

        connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
        connection.setRequestProperty("Authorization", "Bearer ${apiKey}")

        DataOutputStream(connection.getOutputStream()).use { it.write(bytes) }
        val resultJson = BufferedReader(InputStreamReader(connection.getInputStream(), Charsets.UTF_8)).use { it.readText() }

        connection.disconnect()

        resultJson
    }
}

val justAnswerText: (JSONObject)->String = { jsonObject->
    val ng = "NO ANSWER"
    if( jsonObject.has("choices") ){
        val jsonArray = jsonObject.getJSONArray("choices")
        val len = jsonArray.length()
        if( len>0 ){
            val item0 = jsonArray[0]
            if( item0 is JSONObject ){
                val message0 = item0.getJSONObject("message")
                if( message0 is JSONObject ){
                    message0.getString("content")
                } else {
                    ng
                }
            } else {
                ng
            }
        } else {
            ng
        }
    } else {
        ng
    }
}

val invoke: (URI, JSONObject, APIKey)->String = { uri, jsonObject, apiKey->
    val jsonString = doPost(uri, jsonObject, apiKey)
    justAnswerText( JSONObject(jsonString) )
}

val toQueryJsonObject: (ChatCompletionParams)->JSONObject = { params->
    JSONObject().let { jsonObj->
        val systemObj = JSONObject().apply {
            put("role", "system")
            put("content", params.systemMessage)
        }
        
        val userObj = JSONObject().apply {
            put("role", "user")
            put("content", params.userMessage)
        }
        
        val jsonArray = JSONArray().apply {
            put(systemObj)
            put(userObj)
        }

        jsonObj.put("model", params.modelName)
        jsonObj.put("messages", jsonArray)

        val valid = (0f <= params.temperature && params.temperature <= 2f)
        if( valid ){
            jsonObj.put("temperature", params.temperature)
        }
    
        jsonObj
    }
}


// main
val apiKey = toOpenaiAPIKey()
if( apiKey=="unknown" ){ System.exit(0) }

val uri = URI("https://api.openai.com/v1/chat/completions")
val m = "gpt-4o-2024-05-13"
val s = "You are a helpful assistant."
val u = """次の例文から原料名と原産地名をセットにして抜き出して箇条書きにしてください。
例文: ブロッコリー(エクアドル)、揚げじゃがいも(じゃがいも(国産)、植物油脂)、殻付き海老(インド)、いか(中国)"""

val p = toDefaultChatCompletionParams(m, s, u)
val q = toQueryJsonObject(p)
val a = invoke(uri, q, apiKey)
println(a)

次のようにして実行します。結果は標準出力されます。

$ kotlin gpt4o.main.kts

以上です。