Return Kotlin Objects from GPT Output
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.