Home About Contact
GPT , LLM , Kotlin

OpenAI API で GPT-4o-mini に英作文の添削をしてもらう

以前に大学受験の学生に英作文の添削を頼まれてつくったものです。 普通に ChatGPT でもできると思いますが、 毎回添削対象英作文以外のプロンプトは固定なので、APIでつくりました。 単に間違いを修正するだけでなく、修正個所について箇条書きで説明してもらうようにプロンプトで指示を出しています。

今なら GPTs を使うとよいのかもしれません。

API の場合、月額課金ではなく使用量に応じた課金なので、処理量が少なかったり実験レベルのアイデアの検証には API 課金の方が良い。

なお、 ここではシンプルにエンドポイントのURLに所定形式のJSONを投げて回答をもらう方式で実装していきます。 実装言語は Kotlin です。

詳細は OpenAPI Chat https://platform.openai.com/docs/api-reference/chat/create を参照。

環境

$ kotlin -version
Kotlin version 2.0.20-release-360 (JRE 17.0.12+7-Ubuntu-1ubuntu222.04)

プロンプト用 JSON の組み立て

プロンプトとして 所定の JSON を組み立てる部分:

// gpt.main.kts

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

import org.apache.commons.io.IOUtils
import org.json.JSONObject
import org.json.JSONArray

val stdin: BufferedReader = System.`in`.bufferedReader(Charsets.UTF_8)
val targetText = stdin.use { IOUtils.toString(it) }

val systemMessage = """
You are a helpful English teacher.
Reply with a corrected version of the input sentence with all grammatical and spelling errors fixed.
If there are no errors, reply with a copy of the original sentence.
""".trim().split(System.getProperty("line.separator")).joinToString("")

val userMessage = """
次の英文を校正してください。
校正後の英文を出力した後に、
該当修正部分をリストアップして日本語で補足説明してください。
英文: $targetText
""".trim().split(System.getProperty("line.separator")).joinToString("")

val model = "gpt-4o-mini"
//val model = "gpt-4o"

val systemObj = JSONObject()
systemObj.put("role", "system")
systemObj.put("content", systemMessage)

val userObj = JSONObject()
userObj.put("role", "user")
userObj.put("content", userMessage)

val jsonArray = JSONArray()
jsonArray.put(systemObj)
jsonArray.put(userObj)

val jsonObj = JSONObject()
jsonObj.put("model", model)
jsonObj.put("messages", jsonArray)

println(jsonObj.toString())

添削してもらう英作文は標準入力から与えるようにしています。 使用するモデルは gpt-4o-mini です。

モデルの説明: https://platform.openai.com/docs/models とその値段: https://openai.com/api/pricing/

添削対象の英文を example.txt に用意します。

Thank you very much for your feedback.
But In my opinion, this problem depends on Boox hardware.
If I could get it, I could test and maybe fix it or something.
I understand this is a fatal problem.
For now, I can not afford to get it.
I am sorry for this.

先ほど書いた英語のメールですが、短いのであまり訂正するところもないのではないかとか思いつつ。

次のようにして実行します。 この段階では プロンプトとして使う JSON を組み立てているだけで まだ OpenAI の API は使っていません。

$ cat example.txt | kotlin gpt.main.kts | jq .
{
  "messages": [
    {
      "role": "system",
      "content": "You are a helpful English teacher. Reply with a corrected version of the input sentence with all grammatical and spelling errors fixed. If there are no errors, reply with a copy of the original sentence."
    },
    {
      "role": "user",
      "content": "次の英文を校正してください。校正後の英文を出力した後に、該当修正部分をリストアップして日本語で補足説明してください。英文: Thank you very much for your feedback.But In my opinion, this problem depends on Boox hardware.If I could get it, I could test and maybe fix it or something.I understand this is a fatal problem.For now, I can not afford to get it.I am sorry for this."
    }
  ],
  "model": "gpt-4o-mini"
}

出力JSON を jq で整形しています。

POST 処理用 HttpURLConnectionHelper

java.net.HttpURLConnection を使った POST 処理です。 OpenAI API を使うには API KEY が必要になります。 ここでは、その API KEY が環境変数 OPENAI_API_KEY にセットされていることを前提としています。

import java.net.HttpURLConnection
import java.net.URI
import java.io.*

object HttpURLConnectionHelper {
    val isValid: ()-> Boolean  = { toOpenaiAPIKey().isNotEmpty() }

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

    private fun buildAuthHeader(): String {
        val openaiAPIKey = toOpenaiAPIKey()
        return "Bearer $openaiAPIKey"
    }

    fun doPost(uri: URI, jsonStr: String): String {
        val method = "POST"
        val bytes = jsonStr.toByteArray(Charsets.UTF_8)

        val connection = uri.toURL().openConnection() as HttpURLConnection
        connection.setRequestMethod(method)
        connection.setDoOutput(true)
        connection.setFixedLengthStreamingMode(bytes.size)
        connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
        connection.setRequestProperty("Authorization", buildAuthHeader())

        // 送信
        val outputStream = DataOutputStream(connection.getOutputStream())
        outputStream.write(bytes)
        outputStream.close()

        // 受信
        val reader = BufferedReader(InputStreamReader(connection.getInputStream(), Charsets.UTF_8))
        val resultJson = reader.readText()
        reader.close()

        connection.disconnect()

        return resultJson
    }
}

API KEY を取得したら

$ export OPENAI_API_KEY=xxxxxxxx

のようにして環境変数をセットしておきます。

API にJSON を投げて回答を得る

それでは 組み立てた JSON と HttpURLConnectionHelper.doPost を使って実際に英作文を添削してもらいましょう。

if( HttpURLConnectionHelper.isValid() ){
    val uri = URI("https://api.openai.com/v1/chat/completions")
    val q = jsonObj.toString()
    val r = HttpURLConnectionHelper.doPost(uri, q)
    println( r )
} else {
    println( "Error: OPENAI API KEY is empty." )
}
$ cat example.txt | kotlin gpt.main.kts

添削された内容は次のようなJSONとなりました。

{
  "id": "chatcmpl-XXXXXXXX",
  "object": "chat.completion",
  "created": 1730371045,
  "model": "gpt-4o-mini-2024-07-18",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Thank you very much for your feedback. But in my opinion, this problem depends on Boox hardware. If I could get it, I could test and maybe fix it or something. I understand this is a fatal problem. For now, I cannot afford to get it. I am sorry for this.\n\n修正部分:\n1. \"But In my opinion\" → \"But in my opinion\"  \n   - \"In\"の最初の文字が大文字になっていたため、文中では小文字に修正しました。\n\n2. \"can not\" → \"cannot\"  \n   - \"can not\"を一語の\"cannot\"に修正しました。これは「できない」という意味の単語です。\n\n全体的には文法や文の構成のエラーはありませんでしたが、一部のスペルやキャピタリゼーションの修正が必要でした。",
        "refusal": null
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 148,
    "completion_tokens": 186,
    "total_tokens": 334,
    "prompt_tokens_details": {
      "cached_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0
    }
  },
  "system_fingerprint": "fp_xxxxxx"
}

このままでは回答が読み辛いので、整形するための finalAnswer 関数(後述)を通すと添削結果は次のようになりました。

Thank you very much for your feedback. But in my opinion, this problem depends on Boox hardware. If I could get it, I could test and maybe fix it or something. I understand this is a fatal problem. For now, I cannot afford to get it. I am sorry for this.

修正部分:
1. "But In my opinion" → "But in my opinion"  
   - "In"の最初の文字が大文字になっていたため、文中では小文字に修正しました。

2. "can not" → "cannot"  
   - "can not"を一語の"cannot"に修正しました。これは「できない」という意味の単語です。

全体的には文法や文の構成のエラーはありませんでしたが、一部のスペルやキャピタリゼーションの修正が必要でした。

あぁそうでした。普通に できない を表現するのは cannot で、can と not とを分けて書くということはほぼないとか。

まとめ

それではここまで書いてきたコードをまとめます。 結果の JSON から必要な部分だけ整形して取り出す関数 finalAnswer も記述してあります。

// gpt.main.kts

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

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

import org.apache.commons.io.IOUtils
import org.json.JSONObject
import org.json.JSONArray


val finalAnswer: (String)->String = { result->
    val jsonObj = JSONObject(result)
    val jsonArray = jsonObj.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 ){
                val content0 = message0.getString("content")
                content0
            } else {
                ""
            }
        } else {
            ""
        }
    } else {
        ""
    }
}

object HttpURLConnectionHelper {
    val isValid: ()-> Boolean  = { toOpenaiAPIKey().isNotEmpty() }

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

    private fun buildAuthHeader(): String {
        val openaiAPIKey = toOpenaiAPIKey()
        return "Bearer $openaiAPIKey"
    }

    fun doPost(uri: URI, jsonStr: String): String {
        val method = "POST"
        val bytes = jsonStr.toByteArray(Charsets.UTF_8)

        val connection = uri.toURL().openConnection() as HttpURLConnection
        connection.setRequestMethod(method)
        connection.setDoOutput(true)
        connection.setFixedLengthStreamingMode(bytes.size)
        connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
        connection.setRequestProperty("Authorization", buildAuthHeader())

        // 送信
        val outputStream = DataOutputStream(connection.getOutputStream())
        outputStream.write(bytes)
        outputStream.close()

        // 受信
        val reader = BufferedReader(InputStreamReader(connection.getInputStream(), Charsets.UTF_8))
        val resultJson = reader.readText()
        reader.close()

        connection.disconnect()

        return resultJson
    }
}


val stdin: BufferedReader = System.`in`.bufferedReader(Charsets.UTF_8)
val targetText = stdin.use { IOUtils.toString(it) }

val systemMessage =  "You are a helpful English teacher. Reply with a corrected version of the input sentence with all grammatical and spelling errors fixed. If there are no errors, reply with a copy of the original sentence."

val userMessage = """
次の英文を校正してください。
校正後の英文を出力した後に、
該当修正部分をリストアップして日本語で補足説明してください。
英文: $targetText
""".trim().split(System.getProperty("line.separator")).joinToString("")


val model = "gpt-4o-mini"
//val model = "gpt-4o"

val systemObj = JSONObject()
systemObj.put("role", "system")
systemObj.put("content", systemMessage)

val userObj = JSONObject()
userObj.put("role", "user")
userObj.put("content", userMessage)

val jsonArray = JSONArray()
jsonArray.put(systemObj)
jsonArray.put(userObj)

val jsonObj = JSONObject()
jsonObj.put("model", model)
jsonObj.put("messages", jsonArray)
println(jsonObj.toString())

if( HttpURLConnectionHelper.isValid() ){
    val uri = URI("https://api.openai.com/v1/chat/completions")
    val q = jsonObj.toString()
    val r = HttpURLConnectionHelper.doPost(uri, q)
    println( finalAnswer(r) )
} else {
    println( "Error: OPENAI API KEY is empty." )
}

以上です。