Code

This function is the principal function that will be used to call the OpenAI API. It will take a prompt which need to describe what we want to output.

@OptIn(BetaOpenAI::class)
suspend inline fun  <reified T : Any> askWithOutput(prompt: String): T {
    val describeConstructor = if(T::class.java.isArray) {
        describeConstructor(T::class.java.componentType.kotlin)
    } else {
        describeConstructor(T::class)
    }
    val chatCompletionRequest = ChatCompletionRequest(
        model = ModelId("gpt-4"),
        messages = listOf(
            ChatMessage(
                ChatRole("user"),
                prompt
            ),
            ChatMessage(
                ChatRole("user"),
                """
                Please provide a response only in a JSON format (example: { 'key': 'value', ... } ) that can be parsed into 
                an object of type $describeConstructor.
                Don't output anything else.
                 If you want to output multiple objects, please wrap them in a array like: [ {...}, {...}, ... ]"
                """
            )
        )
    )

    val openAI = OpenAI(token = "YOUR_TOKEN", timeout =  Timeout(socket = 20.minutes) )
    val completion: ChatCompletion = openAI.chatCompletion(chatCompletionRequest)

    val answer = completion.choices.joinToString("") {
        it.message!!.content!!
    }

    if(T::class.java == String::class.java) {
        return answer as T
    }
    var instance = createInstanceFromJson(T::class.java, answer)
    if(instance is List<*>) {
        instance = instance[0] as T
    }
    return instance
}

We construct a prompt from our generic type T with this part:

val describeConstructor = if(T::class.java.isArray) {
    describeConstructor(T::class.java.componentType.kotlin)
} else {
    describeConstructor(T::class)
}

The describeConstructor function will return a string that describe the constructor of our generic type T.

fun <T : Any> describeConstructor(kClass: KClass<T>): String {
    val constructors = kClass.constructors
    val constructor = constructors.first()
    val params = constructor.parameters.joinToString(", ") { param: KParameter ->
        "${param.name}: ${param.type}"
    }
    return "${kClass.simpleName}($params)"
}

This message will be used to ask the user to provide a response only in a JSON format that can be parsed into an object of type T. It’s quite important to carefully construct the prompt because you need a valid JSON format to be able to parse it into an object.

ChatMessage(
    ChatRole("user"),
    """
        Please provide a response only in a JSON format (example: { 'key': 'value', ... } ) that can be parsed into
        an object of type $describeConstructor.
        Don't output anything else.
        If you want to output multiple objects, please wrap them in a array like: [ {...}, {...}, ... ]"
    """
)

The last part take the answer from the API and parse it into an object of type T.

var instance = createInstanceFromJson(T::class.java, answer)
if(instance is List<*>) {
    instance = instance[0] as T
}
return instance

The createInstanceFromJson function is a simple function that use the Jackson library to parse the JSON into an object of type T.

fun <T> createInstanceFromJson(clazz: Class<T>, answer: String): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(answer, clazz)
}

Examples

Let’s take a simple example with a Circle class.

class Circle(val radius: Int){
    fun area(): Double {
        return Math.PI * radius * radius
    }
}

We can now call the askWithOutput function to create a circle of radius 5 and print the area of the circle.

askWithOutput<Circle>("Create a circle of radius 5")
    .let {
        println("area of circle " + it.area())
    }

That will print:

// area of circle 78.53981633974483

Given a Book class:

data class Book(val title: String, val author: String, val year: Int)

We can now call the askWithOutput function to create an array of books and print the title, author and year of all books.

askWithOutput<Array<Book>>("Suggest me a list of SciFi books")
    .forEach {
        println("Book: " + it.title + " by " + it.author + " in " + it.year)
    }

That will print:

/**
Book: Dune by Frank Herbert in 1965
Book: 1984 by George Orwell in 1949
Book: Brave New World by Aldous Huxley in 1932
Book: The War of the Worlds by H.G. Wells in 1898
Book: Ender's Game by Orson Scott Card in 1985
Book: The Time Machine by H.G. Wells in 1895
Book: A Wrinkle in Time by Madeleine L'Engle in 1962
Book: Frankenstein by Mary Shelley in 1818
Book: The Hunger Games by Suzanne Collins in 2008
Book: Fahrenheit 451 by Ray Bradbury in 1953
Book: Starship Troopers by Robert A. Heinlein in 1959
Book: The Left Hand of Darkness by Ursula K. Le Guin in 1969
Book: Snow Crash by Neal Stephenson in 1992
Book: A Canticle for Leibowitz by Walter M. Miller Jr. in 1960
Book: Neuromancer by William Gibson in 1984
*/

Go Further

You could also want to return an object that is not a simple object but an object that contains other objects.

data class Address(
    val street: String, 
    val city: String, 
    val state: String, 
    val zip: String
)

class Person(val address: Address){
    fun printAddress(){
        println("Address of person: " + address.street 
            + " " + address.city + " " + address.state + " " + address.zip)
    }
}

We can call the askWithOutput function to create a person and print the address of the person.

askWithOutput<Person>("""
    Create a person named Olivier Cavadenti that have
    an address in Morlaix with a street, a city, a state and a zip."""
).printAddress()

Unfortunately, the API will not return a valid JSON format for this example because of additional properties. We can try to avoid this problem by using FAIL_ON_UNKNOWN_PROPERTIES = false in the ObjectMapper.

fun <T> createInstanceFromJson(clazz: Class<T>, answer: String): T {
    val mapper = jacksonObjectMapper()
    mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    return mapper.readValue(answer, clazz)
}

Example of outputs:

// Address of person: 15 rue du Port Morlaix Brittany 29600

My Person object is created and the address object is created too !

That solution need to be improved because we need to manual prompting GPT to described nested objects. We could imagine a solution that will automatically prompt GPT to describe nested objects, but we need to avoid stack overflow and infinite loop.