From 9efa7c3f386d77591548f10d46bb6b37ab89e043 Mon Sep 17 00:00:00 2001 From: ahmedharis994 Date: Wed, 10 Jan 2024 19:52:07 +0500 Subject: [PATCH 1/7] Add additional_instructions property into Assistant->Run->RunRequest --- .../kotlin/com.aallam.openai.api/run/RunRequest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt index bca1a6e7..ba310f53 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt @@ -30,6 +30,12 @@ public data class RunRequest( */ @SerialName("instructions") val instructions: String? = null, + /** + * Appends additional instructions at the end of the instructions for the run. + * This is useful for modifying the behavior on a per-run basis without overriding other instructions. + */ + @SerialName("additional_instructions") val additionalInstructions: String? = null, + /** * Override the tools the assistant can use for this run. * This is useful for modifying the behavior on a per-run basis. @@ -74,6 +80,12 @@ public class RunRequestBuilder { */ public var instructions: String? = null + /** + * Appends additional instructions at the end of the instructions for the run. + * This is useful for modifying the behavior on a per-run basis without overriding other instructions. + */ + public var additionalInstructions: String? = null, + /** * Override the tools the assistant can use for this run. * This is useful for modifying the behavior on a per-run basis. @@ -94,6 +106,7 @@ public class RunRequestBuilder { assistantId = requireNotNull(assistantId) { "assistantId is required" }, model = model, instructions = instructions, + additionalInstructions = additionalInstructions, tools = tools, metadata = metadata, ) From fe3eafabeb88563fcabd4b90000edd9a752e9d98 Mon Sep 17 00:00:00 2001 From: ahmedharis994 Date: Mon, 15 Jul 2024 21:24:24 +0500 Subject: [PATCH 2/7] remove , from end --- .../commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt index ba310f53..a41334bc 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt @@ -84,7 +84,7 @@ public class RunRequestBuilder { * Appends additional instructions at the end of the instructions for the run. * This is useful for modifying the behavior on a per-run basis without overriding other instructions. */ - public var additionalInstructions: String? = null, + public var additionalInstructions: String? = null /** * Override the tools the assistant can use for this run. From 36a796e140d0ec48f418c0735e879efef1cac2f9 Mon Sep 17 00:00:00 2001 From: ahmedharis994 Date: Mon, 15 Jul 2024 21:48:54 +0500 Subject: [PATCH 3/7] Add new fields into run --- .../kotlin/com.aallam.openai.api/run/Run.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt index 5bd08469..3bdbea61 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt @@ -7,6 +7,7 @@ import com.aallam.openai.api.core.Status import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.thread.ThreadId import com.aallam.openai.api.core.LastError +import com.aallam.openai.api.core.Usage import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -102,4 +103,30 @@ public data class Run( * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. */ @SerialName("metadata") val metadata: Map? = null, + + /** + * Usage statistics related to the run. + * This value will be null if the run is not in a terminal state (i.e. in_progress, queued, etc.). + */ + @SerialName("usage") public val usage: Usage? = null, + + /** + * The Unix timestamp (in seconds) for when the run was completed. + */ + @SerialName("temperature") val temperature: Int? = null, + + /** + * The nucleus sampling value used for this run. If not set, defaults to 1. + */ + @SerialName("top_p") val topP: Int? = null, + + /** + * The maximum number of prompt tokens specified to have been used over the course of the run. + */ + @SerialName("max_prompt_tokens") val maxPromptTokens: Int? = null, + + /** + * The maximum number of completion tokens specified to have been used over the course of the run. + */ + @SerialName("max_completion_tokens") val maxCompletionTokens: Int? = null, ) From bcb3b69379310b6f1499311d92eda73a4ebed1df Mon Sep 17 00:00:00 2001 From: Ahmed Haris Javaid Mirza Date: Thu, 24 Oct 2024 19:38:17 +0500 Subject: [PATCH 4/7] Add new response format into Assistant to return structured response. --- .../assistant/AssistantResponseFormat.kt | 102 +++++++++++++----- .../openai/sample/jvm/AssistantsFunction.kt | 36 ++++++- .../openai/sample/jvm/AssistantsRetrieval.kt | 1 - 3 files changed, 113 insertions(+), 26 deletions(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt index ecef0607..592b8186 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt @@ -9,9 +9,12 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive /** @@ -24,36 +27,38 @@ import kotlinx.serialization.json.jsonPrimitive @BetaOpenAI @Serializable(with = AssistantResponseFormat.ResponseFormatSerializer::class) public data class AssistantResponseFormat( - val format: String? = null, - val objectType: AssistantResponseType? = null, + val type: String, + val jsonSchema: JsonSchema? = null ) { + @Serializable - public data class AssistantResponseType( - val type: String + public data class JsonSchema( + val name: String, + val description: String? = null, + val schema: JsonObject, + val strict: Boolean? = null ) public companion object { - public val AUTO: AssistantResponseFormat = AssistantResponseFormat(format = "auto") - public val TEXT: AssistantResponseFormat = AssistantResponseFormat(objectType = AssistantResponseType(type = "text")) - public val JSON_OBJECT: AssistantResponseFormat = AssistantResponseFormat(objectType = AssistantResponseType(type = "json_object")) + public val AUTO: AssistantResponseFormat = AssistantResponseFormat("auto") + public val TEXT: AssistantResponseFormat = AssistantResponseFormat("text") + public val JSON_OBJECT: AssistantResponseFormat = AssistantResponseFormat("json_object") + public fun JSON_SCHEMA( + name: String, + description: String? = null, + schema: JsonObject, + strict: Boolean? = null + ): AssistantResponseFormat = AssistantResponseFormat( + "json_schema", + JsonSchema(name, description, schema, strict) + ) } + public object ResponseFormatSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AssistantResponseFormat") { - element("format", isOptional = true) - element("type", isOptional = true) - } - - override fun serialize(encoder: Encoder, value: AssistantResponseFormat) { - val jsonEncoder = encoder as? kotlinx.serialization.json.JsonEncoder - ?: throw SerializationException("This class can be saved only by Json") - - if (value.format != null) { - jsonEncoder.encodeJsonElement(JsonPrimitive(value.format)) - } else if (value.objectType != null) { - val jsonElement: JsonElement = JsonObject(mapOf("type" to JsonPrimitive(value.objectType.type))) - jsonEncoder.encodeJsonElement(jsonElement) - } + element("type") + element("json_schema", isOptional = true) // Only for "json_schema" type } override fun deserialize(decoder: Decoder): AssistantResponseFormat { @@ -63,14 +68,63 @@ public data class AssistantResponseFormat( val jsonElement = jsonDecoder.decodeJsonElement() return when { jsonElement is JsonPrimitive && jsonElement.isString -> { - AssistantResponseFormat(format = jsonElement.content) + AssistantResponseFormat(type = jsonElement.content) } jsonElement is JsonObject && "type" in jsonElement -> { val type = jsonElement["type"]!!.jsonPrimitive.content - AssistantResponseFormat(objectType = AssistantResponseType(type)) + when (type) { + "json_schema" -> { + val schemaObject = jsonElement["json_schema"]?.jsonObject + val name = schemaObject?.get("name")?.jsonPrimitive?.content ?: "" + val description = schemaObject?.get("description")?.jsonPrimitive?.contentOrNull + val schema = schemaObject?.get("schema")?.jsonObject ?: JsonObject(emptyMap()) + val strict = schemaObject?.get("strict")?.jsonPrimitive?.booleanOrNull + AssistantResponseFormat( + type = "json_schema", + jsonSchema = JsonSchema(name = name, description = description, schema = schema, strict = strict) + ) + } + "json_object" -> AssistantResponseFormat(type = "json_object") + "auto" -> AssistantResponseFormat(type = "auto") + "text" -> AssistantResponseFormat(type = "text") + else -> throw SerializationException("Unknown response format type: $type") + } } else -> throw SerializationException("Unknown response format: $jsonElement") } } + + override fun serialize(encoder: Encoder, value: AssistantResponseFormat) { + val jsonEncoder = encoder as? kotlinx.serialization.json.JsonEncoder + ?: throw SerializationException("This class can be saved only by Json") + + val jsonElement = when (value.type) { + "json_schema" -> { + JsonObject( + mapOf( + "type" to JsonPrimitive("json_schema"), + "json_schema" to JsonObject( + mapOf( + "name" to JsonPrimitive(value.jsonSchema?.name ?: ""), + "description" to JsonPrimitive(value.jsonSchema?.description ?: ""), + "schema" to (value.jsonSchema?.schema ?: JsonObject(emptyMap())), + "strict" to JsonPrimitive(value.jsonSchema?.strict ?: false) + ) + ) + ) + ) + } + "json_object" -> JsonObject(mapOf("type" to JsonPrimitive("json_object"))) + "auto" -> JsonPrimitive("auto") + "text" -> JsonObject(mapOf("type" to JsonPrimitive("text"))) + else -> throw SerializationException("Unsupported response format type: ${value.type}") + } + jsonEncoder.encodeJsonElement(jsonElement) + } + } } + +public fun JsonObject.Companion.buildJsonObject(block: JsonObjectBuilder.() -> Unit): JsonObject { + return kotlinx.serialization.json.buildJsonObject(block) +} diff --git a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt index 30f5a99f..40a4153a 100644 --- a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt +++ b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt @@ -2,6 +2,7 @@ package com.aallam.openai.sample.jvm import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.assistant.AssistantRequest +import com.aallam.openai.api.assistant.AssistantResponseFormat import com.aallam.openai.api.assistant.AssistantTool import com.aallam.openai.api.assistant.Function import com.aallam.openai.api.chat.ToolCall @@ -17,7 +18,10 @@ import com.aallam.openai.api.run.RunRequest import com.aallam.openai.api.run.ToolOutput import com.aallam.openai.client.OpenAI import kotlinx.coroutines.delay +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject @@ -29,6 +33,36 @@ suspend fun assistantsFunctions(openAI: OpenAI) { request = AssistantRequest( name = "Math Tutor", instructions = "You are a weather bot. Use the provided functions to answer questions.", + responseFormat = AssistantResponseFormat.JSON_SCHEMA( + name = "math_response", + strict = true, + schema = buildJsonObject { + put("type", "object") + putJsonObject("properties") { + putJsonObject("steps") { + put("type", "array") + putJsonObject("items") { + put("type", "object") + putJsonObject("properties") { + putJsonObject("explanation") { + put("type", "string") + } + putJsonObject("output") { + put("type", "string") + } + } + put("required", JsonArray(listOf(JsonPrimitive("explanation"), JsonPrimitive("output")))) + put("additionalProperties", false) + } + } + putJsonObject("final_answer") { + put("type", "string") + } + } + put("additionalProperties", false) + put("required", JsonArray(listOf(JsonPrimitive("steps"), JsonPrimitive("final_answer")))) + }, + ), tools = listOf( AssistantTool.FunctionTool( function = Function( @@ -74,7 +108,7 @@ suspend fun assistantsFunctions(openAI: OpenAI) { ) ) ), - model = ModelId("gpt-4-1106-preview") + model = ModelId("gpt-4o-mini") ) ) diff --git a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsRetrieval.kt b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsRetrieval.kt index 57c80468..19404785 100644 --- a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsRetrieval.kt +++ b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsRetrieval.kt @@ -30,7 +30,6 @@ suspend fun assistantsRetrieval(openAI: OpenAI) { instructions = "You are a chatbot specialized in 'The Universal Declaration of Human Rights.' Answer questions and provide information based on this document.", tools = listOf(AssistantTool.FileSearch), model = ModelId("gpt-4-1106-preview"), - fileIds = listOf(knowledgeBase.id) ) ) From 176898f71398f0361dd70cb080ba8d6f187e7cd4 Mon Sep 17 00:00:00 2001 From: Ahmed Haris Javaid Mirza Date: Fri, 25 Oct 2024 02:51:03 +0500 Subject: [PATCH 5/7] add java docs --- .../assistant/AssistantRequest.kt | 11 +++++++- .../assistant/AssistantResponseFormat.kt | 25 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt index 26c95a0d..4ca6cbbc 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt @@ -67,7 +67,15 @@ public data class AssistantRequest( * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo * models since gpt-3.5-turbo-1106. * - * Setting to [AssistantResponseFormat.JsonObject] enables JSON mode, which guarantees the message the model + * Setting to [AssistantResponseFormat.JSON_SCHEMA] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * + * Structured Outputs ([AssistantResponseFormat.JSON_SCHEMA]) are available in our latest large language models, starting with GPT-4o: + * 1. gpt-4o-mini-2024-07-18 and later + * 2. gpt-4o-2024-08-06 and later + * + * Older models like gpt-4-turbo and earlier may use JSON mode ([AssistantResponseFormat.JSON_OBJECT]) instead. + * + * Setting to [AssistantResponseFormat.JSON_OBJECT] enables JSON mode, which guarantees the message the model * generates is valid JSON. * * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user @@ -75,6 +83,7 @@ public data class AssistantRequest( * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or * the conversation exceeded the max context length. + * */ @SerialName("response_format") val responseFormat: AssistantResponseFormat? = null, ) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt index 592b8186..d9135dfc 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt @@ -18,11 +18,10 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive /** - * string: auto is the default value + * Represents the format of the response from the assistant. * - * object: An object describing the expected output of the model. If json_object only function type tools are allowed to be passed to the Run. - * If text, the model can return text or any value needed. - * type: string Must be one of text or json_object. + * @property type The type of the response format. + * @property jsonSchema The JSON schema associated with the response format, if type is "json_schema" otherwise null. */ @BetaOpenAI @Serializable(with = AssistantResponseFormat.ResponseFormatSerializer::class) @@ -31,6 +30,14 @@ public data class AssistantResponseFormat( val jsonSchema: JsonSchema? = null ) { + /** + * Represents a JSON schema. + * + * @property name The name of the schema. + * @property description The description of the schema. + * @property schema The actual JSON schema. + * @property strict Indicates if the schema is strict. + */ @Serializable public data class JsonSchema( val name: String, @@ -43,6 +50,16 @@ public data class AssistantResponseFormat( public val AUTO: AssistantResponseFormat = AssistantResponseFormat("auto") public val TEXT: AssistantResponseFormat = AssistantResponseFormat("text") public val JSON_OBJECT: AssistantResponseFormat = AssistantResponseFormat("json_object") + + /** + * Creates an instance of `AssistantResponseFormat` with type `json_schema`. + * + * @param name The name of the schema. + * @param description The description of the schema. + * @param schema The actual JSON schema. + * @param strict Indicates if the schema is strict. + * @return An instance of `AssistantResponseFormat` with the specified JSON schema. + */ public fun JSON_SCHEMA( name: String, description: String? = null, From c162579ad5398153ff3f4caf79326b3bffaa698b Mon Sep 17 00:00:00 2001 From: Ahmed Haris Javaid Mirza Date: Fri, 25 Oct 2024 14:49:19 +0500 Subject: [PATCH 6/7] add test cases --- .../aallam/openai/client/TestAssistants.kt | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt index 671e09f5..35a02c62 100644 --- a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt @@ -4,12 +4,19 @@ import com.aallam.openai.api.assistant.AssistantResponseFormat import com.aallam.openai.api.assistant.AssistantTool import com.aallam.openai.api.assistant.assistantRequest import com.aallam.openai.api.chat.ToolCall -import com.aallam.openai.api.core.RequestOptions import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.run.RequiredAction import com.aallam.openai.api.run.Run import com.aallam.openai.client.internal.JsonLenient -import kotlin.test.* +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue class TestAssistants : TestOpenAI() { @@ -144,4 +151,64 @@ class TestAssistants : TestOpenAI() { val action = decoded.requiredAction as RequiredAction.SubmitToolOutputs assertIs(action.toolOutputs.toolCalls.first()) } + + @Test + fun jsonSchemaAssistant() = test { + val jsonSchema = AssistantResponseFormat.JSON_SCHEMA( + name = "TestSchema", + description = "A test schema", + schema = buildJsonObject { + put("type", "object") + put("properties", buildJsonObject { + put("name", buildJsonObject { + put("type", "string") + }) + }) + put("required", JsonArray(listOf(JsonPrimitive("name")))) + put("additionalProperties", false) + }, + strict = true + ) + + val request = assistantRequest { + name = "Schema Assistant" + model = ModelId("gpt-4o-mini") + responseFormat = jsonSchema + } + + val assistant = openAI.assistant( + request = request, + ) + assertEquals(request.name, assistant.name) + assertEquals(request.model, assistant.model) + assertEquals(request.responseFormat, assistant.responseFormat) + + val getAssistant = openAI.assistant( + assistant.id, + ) + assertEquals(getAssistant, assistant) + + val assistants = openAI.assistants() + assertTrue { assistants.isNotEmpty() } + + val updated = assistantRequest { + name = "Updated Schema Assistant" + responseFormat = AssistantResponseFormat.AUTO + } + val updatedAssistant = openAI.assistant( + assistant.id, + updated, + ) + assertEquals(updated.name, updatedAssistant.name) + assertEquals(updated.responseFormat, updatedAssistant.responseFormat) + + openAI.delete( + updatedAssistant.id, + ) + + val fileGetAfterDelete = openAI.assistant( + updatedAssistant.id, + ) + assertNull(fileGetAfterDelete) + } } From 28eda6393e2fa99a26c934ebea933ea38c086cac Mon Sep 17 00:00:00 2001 From: Ahmed Haris Javaid Mirza Date: Fri, 8 Nov 2024 19:18:55 +0500 Subject: [PATCH 7/7] Refactor response format structure to enforce type safety and add response format in Run also. --- .../aallam/openai/client/TestAssistants.kt | 38 +++-- .../assistant/Assistant.kt | 5 +- .../assistant/AssistantRequest.kt | 33 ++-- .../assistant/AssistantResponseFormat.kt | 147 ------------------ .../assistant/Function.kt | 20 ++- .../com.aallam.openai.api/core/JsonSchema.kt | 81 ++++++++++ .../core/ResponseFormat.kt | 93 +++++++++++ .../com.aallam.openai.api/core/Schema.kt | 77 +++++++++ .../kotlin/com.aallam.openai.api/run/Run.kt | 30 ++++ .../com.aallam.openai.api/run/RunRequest.kt | 129 +++++++++++++++ .../openai/sample/jvm/AssistantsFunction.kt | 66 ++++---- 11 files changed, 513 insertions(+), 206 deletions(-) delete mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/JsonSchema.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ResponseFormat.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Schema.kt diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt index 35a02c62..c31471cd 100644 --- a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt @@ -1,9 +1,11 @@ package com.aallam.openai.client -import com.aallam.openai.api.assistant.AssistantResponseFormat import com.aallam.openai.api.assistant.AssistantTool import com.aallam.openai.api.assistant.assistantRequest import com.aallam.openai.api.chat.ToolCall +import com.aallam.openai.api.core.JsonSchema +import com.aallam.openai.api.core.ResponseFormat +import com.aallam.openai.api.core.Schema import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.run.RequiredAction import com.aallam.openai.api.run.Run @@ -26,7 +28,7 @@ class TestAssistants : TestOpenAI() { name = "Math Tutor" tools = listOf(AssistantTool.CodeInterpreter) model = ModelId("gpt-4o") - responseFormat = AssistantResponseFormat.TEXT + responseFormat = ResponseFormat.TextResponseFormat } val assistant = openAI.assistant( request = request, @@ -46,7 +48,7 @@ class TestAssistants : TestOpenAI() { val updated = assistantRequest { name = "Super Math Tutor" - responseFormat = AssistantResponseFormat.AUTO + responseFormat = ResponseFormat.AutoResponseFormat } val updatedAssistant = openAI.assistant( assistant.id, @@ -154,20 +156,22 @@ class TestAssistants : TestOpenAI() { @Test fun jsonSchemaAssistant() = test { - val jsonSchema = AssistantResponseFormat.JSON_SCHEMA( - name = "TestSchema", - description = "A test schema", - schema = buildJsonObject { - put("type", "object") - put("properties", buildJsonObject { - put("name", buildJsonObject { - put("type", "string") + val jsonSchema = ResponseFormat.JsonSchemaResponseFormat( + schema = JsonSchema( + name = "TestSchema", + description = "A test schema", + schema = Schema.buildJsonObject { + put("type", "object") + put("properties", buildJsonObject { + put("name", buildJsonObject { + put("type", "string") + }) }) - }) - put("required", JsonArray(listOf(JsonPrimitive("name")))) - put("additionalProperties", false) - }, - strict = true + put("required", JsonArray(listOf(JsonPrimitive("name")))) + put("additionalProperties", false) + }, + strict = true + ) ) val request = assistantRequest { @@ -193,7 +197,7 @@ class TestAssistants : TestOpenAI() { val updated = assistantRequest { name = "Updated Schema Assistant" - responseFormat = AssistantResponseFormat.AUTO + responseFormat = ResponseFormat.AutoResponseFormat } val updatedAssistant = openAI.assistant( assistant.id, diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt index 539270dd..7da71cda 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt @@ -2,6 +2,7 @@ package com.aallam.openai.api.assistant import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.assistant.AssistantTool.* +import com.aallam.openai.api.core.ResponseFormat import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -80,7 +81,7 @@ public data class Assistant( * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo * models since gpt-3.5-turbo-1106. * - * Setting to [AssistantResponseFormat.JsonObject] enables JSON mode, which guarantees the message the model + * Setting to [ResponseFormat.JsonObject] enables JSON mode, which guarantees the message the model * generates is valid JSON. * * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user @@ -89,5 +90,5 @@ public data class Assistant( * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or * the conversation exceeded the max context length. */ - @SerialName("response_format") public val responseFormat: AssistantResponseFormat? = null, + @SerialName("response_format") public val responseFormat: ResponseFormat? = null, ) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt index 4ca6cbbc..96663682 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt @@ -2,6 +2,7 @@ package com.aallam.openai.api.assistant import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.core.ResponseFormat import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -67,15 +68,15 @@ public data class AssistantRequest( * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo * models since gpt-3.5-turbo-1106. * - * Setting to [AssistantResponseFormat.JSON_SCHEMA] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * Setting to [ResponseFormat.JsonSchemaResponseFormat] enables Structured Outputs which ensures the model will match your supplied JSON schema. * - * Structured Outputs ([AssistantResponseFormat.JSON_SCHEMA]) are available in our latest large language models, starting with GPT-4o: + * Structured Outputs [ResponseFormat.JsonSchemaResponseFormat] are available in our latest large language models, starting with GPT-4o: * 1. gpt-4o-mini-2024-07-18 and later * 2. gpt-4o-2024-08-06 and later * - * Older models like gpt-4-turbo and earlier may use JSON mode ([AssistantResponseFormat.JSON_OBJECT]) instead. + * Older models like gpt-4-turbo and earlier may use JSON mode [ResponseFormat.JsonObjectResponseFormat] instead. * - * Setting to [AssistantResponseFormat.JSON_OBJECT] enables JSON mode, which guarantees the message the model + * Setting to [ResponseFormat.JsonObjectResponseFormat] enables JSON mode, which guarantees the message the model * generates is valid JSON. * * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user @@ -83,9 +84,8 @@ public data class AssistantRequest( * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or * the conversation exceeded the max context length. - * */ - @SerialName("response_format") val responseFormat: AssistantResponseFormat? = null, + @SerialName("response_format") val responseFormat: ResponseFormat? = null, ) @BetaOpenAI @@ -141,10 +141,25 @@ public class AssistantRequestBuilder { /** * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo * models since gpt-3.5-turbo-1106. + * + * Setting to [OldResponseFormat.JSON_SCHEMA] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * + * Structured Outputs ([OldResponseFormat.JSON_SCHEMA]) are available in our latest large language models, starting with GPT-4o: + * 1. gpt-4o-mini-2024-07-18 and later + * 2. gpt-4o-2024-08-06 and later + * + * Older models like gpt-4-turbo and earlier may use JSON mode ([OldResponseFormat.JSON_OBJECT]) instead. + * + * Setting to [OldResponseFormat.JSON_OBJECT] enables JSON mode, which guarantees the message the model + * generates is valid JSON. + * + * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user + * message. Without this, the model may generate an unending stream of whitespace until the generation reaches the + * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be + * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or + * the conversation exceeded the max context length. */ - public var responseFormat: AssistantResponseFormat? = null - - + public var responseFormat: ResponseFormat? = null /** * Create [Assistant] instance. diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt deleted file mode 100644 index d9135dfc..00000000 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantResponseFormat.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.aallam.openai.api.assistant - -import com.aallam.openai.api.BetaOpenAI -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.descriptors.element -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive - -/** - * Represents the format of the response from the assistant. - * - * @property type The type of the response format. - * @property jsonSchema The JSON schema associated with the response format, if type is "json_schema" otherwise null. - */ -@BetaOpenAI -@Serializable(with = AssistantResponseFormat.ResponseFormatSerializer::class) -public data class AssistantResponseFormat( - val type: String, - val jsonSchema: JsonSchema? = null -) { - - /** - * Represents a JSON schema. - * - * @property name The name of the schema. - * @property description The description of the schema. - * @property schema The actual JSON schema. - * @property strict Indicates if the schema is strict. - */ - @Serializable - public data class JsonSchema( - val name: String, - val description: String? = null, - val schema: JsonObject, - val strict: Boolean? = null - ) - - public companion object { - public val AUTO: AssistantResponseFormat = AssistantResponseFormat("auto") - public val TEXT: AssistantResponseFormat = AssistantResponseFormat("text") - public val JSON_OBJECT: AssistantResponseFormat = AssistantResponseFormat("json_object") - - /** - * Creates an instance of `AssistantResponseFormat` with type `json_schema`. - * - * @param name The name of the schema. - * @param description The description of the schema. - * @param schema The actual JSON schema. - * @param strict Indicates if the schema is strict. - * @return An instance of `AssistantResponseFormat` with the specified JSON schema. - */ - public fun JSON_SCHEMA( - name: String, - description: String? = null, - schema: JsonObject, - strict: Boolean? = null - ): AssistantResponseFormat = AssistantResponseFormat( - "json_schema", - JsonSchema(name, description, schema, strict) - ) - } - - - public object ResponseFormatSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AssistantResponseFormat") { - element("type") - element("json_schema", isOptional = true) // Only for "json_schema" type - } - - override fun deserialize(decoder: Decoder): AssistantResponseFormat { - val jsonDecoder = decoder as? kotlinx.serialization.json.JsonDecoder - ?: throw SerializationException("This class can be loaded only by Json") - - val jsonElement = jsonDecoder.decodeJsonElement() - return when { - jsonElement is JsonPrimitive && jsonElement.isString -> { - AssistantResponseFormat(type = jsonElement.content) - } - jsonElement is JsonObject && "type" in jsonElement -> { - val type = jsonElement["type"]!!.jsonPrimitive.content - when (type) { - "json_schema" -> { - val schemaObject = jsonElement["json_schema"]?.jsonObject - val name = schemaObject?.get("name")?.jsonPrimitive?.content ?: "" - val description = schemaObject?.get("description")?.jsonPrimitive?.contentOrNull - val schema = schemaObject?.get("schema")?.jsonObject ?: JsonObject(emptyMap()) - val strict = schemaObject?.get("strict")?.jsonPrimitive?.booleanOrNull - AssistantResponseFormat( - type = "json_schema", - jsonSchema = JsonSchema(name = name, description = description, schema = schema, strict = strict) - ) - } - "json_object" -> AssistantResponseFormat(type = "json_object") - "auto" -> AssistantResponseFormat(type = "auto") - "text" -> AssistantResponseFormat(type = "text") - else -> throw SerializationException("Unknown response format type: $type") - } - } - else -> throw SerializationException("Unknown response format: $jsonElement") - } - } - - override fun serialize(encoder: Encoder, value: AssistantResponseFormat) { - val jsonEncoder = encoder as? kotlinx.serialization.json.JsonEncoder - ?: throw SerializationException("This class can be saved only by Json") - - val jsonElement = when (value.type) { - "json_schema" -> { - JsonObject( - mapOf( - "type" to JsonPrimitive("json_schema"), - "json_schema" to JsonObject( - mapOf( - "name" to JsonPrimitive(value.jsonSchema?.name ?: ""), - "description" to JsonPrimitive(value.jsonSchema?.description ?: ""), - "schema" to (value.jsonSchema?.schema ?: JsonObject(emptyMap())), - "strict" to JsonPrimitive(value.jsonSchema?.strict ?: false) - ) - ) - ) - ) - } - "json_object" -> JsonObject(mapOf("type" to JsonPrimitive("json_object"))) - "auto" -> JsonPrimitive("auto") - "text" -> JsonObject(mapOf("type" to JsonPrimitive("text"))) - else -> throw SerializationException("Unsupported response format type: ${value.type}") - } - jsonEncoder.encodeJsonElement(jsonElement) - } - - } -} - -public fun JsonObject.Companion.buildJsonObject(block: JsonObjectBuilder.() -> Unit): JsonObject { - return kotlinx.serialization.json.buildJsonObject(block) -} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt index 127c3a1d..4f24d27f 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt @@ -15,7 +15,7 @@ public data class Function( */ @SerialName("name") val name: String, /** - * The description of what the function does. + * The description of what the function does. used by the model to choose when and how to call the function. */ @SerialName("description") val description: String, /** @@ -25,6 +25,13 @@ public data class Function( * To describe a function that accepts no parameters, provide [Parameters.Empty]`. */ @SerialName("parameters") val parameters: Parameters, + /** + * Whether to enable strict schema adherence when generating the function call. + * If set to true, the model will always follow the exact schema defined in the parameters field. + * Only a subset of JSON Schema is supported when strict is true. + * To learn more about Structured Outputs in the [function calling guide](https://platform.openai.com/docs/api-reference/assistants/docs/guides/function-calling). + */ + val strict: Boolean? = null ) /** @@ -49,13 +56,22 @@ public class FunctionBuilder { */ public var parameters: Parameters? = Parameters.Empty + /** + * Whether to enable strict schema adherence when generating the function call. + * If set to true, the model will always follow the exact schema defined in the parameters field. + * Only a subset of JSON Schema is supported when strict is true. + * To learn more about Structured Outputs in the [function calling guide](https://platform.openai.com/docs/api-reference/assistants/docs/guides/function-calling). + */ + public var strict: Boolean? = null + /** * Create [Function] instance. */ public fun build(): Function = Function( name = requireNotNull(name) { "name is required" }, description = requireNotNull(description) { "description is required" }, - parameters = requireNotNull(parameters) { "parameters is required" } + parameters = requireNotNull(parameters) { "parameters is required" }, + strict = strict ) } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/JsonSchema.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/JsonSchema.kt new file mode 100644 index 00000000..ba716fc6 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/JsonSchema.kt @@ -0,0 +1,81 @@ +package com.aallam.openai.api.core + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.OpenAIDsl +import kotlinx.serialization.Serializable + + +@BetaOpenAI +@Serializable +public data class JsonSchema( + + /** + * The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, + * with a maximum length of 64. + */ + val name: String, + + /** + * A description of what the response format is for, + * used by the model to determine how to respond in the format. + */ + val description: String? = null, + + /** + * The schema for the response format, described as a JSON Schema object. + */ + val schema: Schema, + + /** + * Whether to enable strict schema adherence when generating the output. + * If set to true, the model will always follow the exact schema defined in the schema field. + * Only a subset of JSON Schema is supported when strict is true. + * To learn more, read the [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + */ + val strict: Boolean? = null +) + +@BetaOpenAI +@OpenAIDsl +public class JsonSchemaBuilder { + /** + * The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, + * with a maximum length of 64. + */ + public var name: String? = null + + /** + * A description of what the response format is for, + * used by the model to determine how to respond in the format. + */ + public var description: String? = null + + /** + * The schema for the response format, described as a JSON Schema object. + */ + public var schema: Schema? = Schema.Empty + + /** + * Whether to enable strict schema adherence when generating the output. + * If set to true, the model will always follow the exact schema defined in the schema field. + * Only a subset of JSON Schema is supported when strict is true. + * To learn more, read the [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + */ + public var strict: Boolean? = true + + public fun build(): JsonSchema = JsonSchema( + name = requireNotNull(name) { "name is required" }, + description = description, + schema = requireNotNull(schema) { "schema is required" }, + strict = strict + ) +} + +/** + * Creates a [JsonSchema] instance using a [JsonSchemaBuilder]. + * + * @param block The [JsonSchemaBuilder] to use. + */ +@OptIn(BetaOpenAI::class) +public fun jsonSchema(block: JsonSchemaBuilder.() -> Unit): JsonSchema = + JsonSchemaBuilder().apply(block).build() diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ResponseFormat.kt new file mode 100644 index 00000000..62346130 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ResponseFormat.kt @@ -0,0 +1,93 @@ +package com.aallam.openai.api.core + +import com.aallam.openai.api.BetaOpenAI +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive + +/** + * Represents the format of the response. + * Response format can be of types: auto, text, json_object, or json_schema. + */ +@BetaOpenAI +@Serializable(with = ResponseFormat.Serializer::class) +public sealed interface ResponseFormat { + + /** + * The type of response format: text + */ + @BetaOpenAI + @Serializable + @SerialName("auto") + public data object AutoResponseFormat : ResponseFormat + + /** + * The type of response format: text + */ + @BetaOpenAI + @Serializable + @SerialName("text") + public data object TextResponseFormat : ResponseFormat + + /** + * The type of response format: json_object + */ + @BetaOpenAI + @Serializable + @SerialName("json_object") + public data object JsonObjectResponseFormat : ResponseFormat + + /** + * The type of response format: json_schema + */ + @BetaOpenAI + @Serializable + @SerialName("json_schema") + public data class JsonSchemaResponseFormat( + /** + * The actual JSON schema. + */ + @SerialName("json_schema") public val schema: JsonSchema, + ) : ResponseFormat + + public object Serializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ResponseFormat") { + element("type") + } + + override fun serialize(encoder: Encoder, value: ResponseFormat) { + require(encoder is JsonEncoder) + val json = when (value) { + is AutoResponseFormat -> JsonPrimitive("auto") + is TextResponseFormat -> JsonObject(mapOf("type" to JsonPrimitive("text"))) + is JsonObjectResponseFormat -> JsonObject(mapOf("type" to JsonPrimitive("json_object"))) + is JsonSchemaResponseFormat -> JsonObject(mapOf("type" to JsonPrimitive("json_schema"), "json_schema" to Json.encodeToJsonElement(JsonSchema.serializer(), value.schema))) + } + encoder.encodeJsonElement(json) + } + + override fun deserialize(decoder: Decoder): ResponseFormat { + require(decoder is JsonDecoder) + val json = decoder.decodeJsonElement() + return when { + json is JsonPrimitive && json.content == "auto" -> AutoResponseFormat + json is JsonObject && json["type"]?.jsonPrimitive?.content == "text" -> TextResponseFormat + json is JsonObject && json["type"]?.jsonPrimitive?.content == "json_object" -> JsonObjectResponseFormat + json is JsonObject && json["type"]?.jsonPrimitive?.content == "json_schema" -> JsonSchemaResponseFormat(Json.decodeFromJsonElement(JsonSchema.serializer(), json["json_schema"]!!)) + else -> throw IllegalArgumentException("Unknown ResponseFormat: $json") + } + } + } + +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Schema.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Schema.kt new file mode 100644 index 00000000..5bf95f2c --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Schema.kt @@ -0,0 +1,77 @@ +package com.aallam.openai.api.core + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +/** + * The schema for the response format, described as a JSON Schema object. + * + * @property schema Json Schema Object. + */ +@Serializable(with = Schema.JsonDataSerializer::class) +public data class Schema(public val schema: JsonElement) { + + /** + * Custom serializer for the [Schema] class. + */ + public object JsonDataSerializer : KSerializer { + override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor + + /** + * Deserializes [Schema] from JSON format. + */ + override fun deserialize(decoder: Decoder): Schema { + require(decoder is JsonDecoder) { "This decoder is not a JsonDecoder. Cannot deserialize `JsonSchema`." } + return Schema(decoder.decodeJsonElement()) + } + + /** + * Serializes [Schema] to JSON format. + */ + override fun serialize(encoder: Encoder, value: Schema) { + require(encoder is JsonEncoder) { "This encoder is not a JsonEncoder. Cannot serialize `JsonSchema`." } + encoder.encodeJsonElement(value.schema) + } + } + + public companion object { + + /** + * Creates a [Schema] instance from a JSON string. + * + * @param json The JSON string to parse. + */ + public fun fromJsonString(json: String): Schema = Schema(Json.parseToJsonElement(json)) + + /** + * Creates a [Schema] instance using a [JsonObjectBuilder]. + * + * @param block The [JsonObjectBuilder] to use. + */ + public fun buildJsonObject(block: JsonObjectBuilder.() -> Unit): Schema { + val json = kotlinx.serialization.json.buildJsonObject(block) + return Schema(json) + } + + /** + * Represents a no params json. Equivalent to: + * ```json + * {"type": "object", "properties": {}} + * ``` + */ + public val Empty: Schema = buildJsonObject { + put("type", "object") + putJsonObject("properties") {} + } + } +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt index d617b78d..e2fc7284 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/Run.kt @@ -7,6 +7,7 @@ import com.aallam.openai.api.core.Status import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.thread.ThreadId import com.aallam.openai.api.core.LastError +import com.aallam.openai.api.core.ResponseFormat import com.aallam.openai.api.core.Usage import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -129,4 +130,33 @@ public data class Run( * The maximum number of completion tokens specified to have been used over the course of the run. */ @SerialName("max_completion_tokens") val maxCompletionTokens: Int? = null, + + /** + * Whether to enable parallel function calling during tool use. + */ + public var parallelToolCalls: Boolean? = null, + + /** + * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo + * models since gpt-3.5-turbo-1106. + * + * Setting to [ResponseFormat.JsonSchemaResponseFormat] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * + * Structured Outputs [ResponseFormat.JsonSchemaResponseFormat] are available in our latest large language models, starting with GPT-4o: + * 1. gpt-4o-mini-2024-07-18 and later + * 2. gpt-4o-2024-08-06 and later + * + * Older models like gpt-4-turbo and earlier may use JSON mode [ResponseFormat.JsonObjectResponseFormat] instead. + * + * Setting to [ResponseFormat.JsonObjectResponseFormat] enables JSON mode, which guarantees the message the model + * generates is valid JSON. + * + * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user + * message. Without this, the model may generate an unending stream of whitespace until the generation reaches the + * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be + * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or + * the conversation exceeded the max context length. + * + */ + public var responseFormat: ResponseFormat? = null ) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt index a41334bc..8f723ee1 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/run/RunRequest.kt @@ -3,6 +3,7 @@ package com.aallam.openai.api.run import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.assistant.AssistantId import com.aallam.openai.api.assistant.AssistantTool +import com.aallam.openai.api.core.ResponseFormat import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -48,6 +49,67 @@ public data class RunRequest( * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. */ @SerialName("metadata") val metadata: Map? = null, + + /** + * What sampling temperature to use, between 0 and 2. + * Higher values like 0.8 will make the output more random, + * while lower values like 0.2 will make it more focused and deterministic. + */ + @SerialName("temperature") val temperature: Double? = null, + + /** + * An alternative to sampling with temperature, called nucleus sampling, + * where the model considers the results of the tokens with top_p probability mass. + * So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + @SerialName("top_p") val topP: Double? = null, + + /** + * The maximum number of prompt tokens that may be used over the course of the run. + * The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. + * If the run exceeds the number of prompt tokens specified, the run will end with status incomplete. + * See incomplete_details for more info. + */ + @SerialName("max_prompt_tokens") val maxPromptTokens: Int? = null, + + /** + * The maximum number of completion tokens that may be used over the course of the run. + * The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. + * If the run exceeds the number of completion tokens specified, the run will end with status incomplete. + * See incomplete_details for more info. + */ + @SerialName("max_completion_tokens") val maxCompletionTokens: Int? = null, + + /** + * Whether to enable parallel function calling during tool use. + */ + @SerialName("parallel_tool_calls") val parallelToolCalls: Boolean? = null, + + /** + * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo + * models since gpt-3.5-turbo-1106. + * + * Setting to [OldResponseFormat.JSON_SCHEMA] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * + * Structured Outputs ([OldResponseFormat.JSON_SCHEMA]) are available in our latest large language models, starting with GPT-4o: + * 1. gpt-4o-mini-2024-07-18 and later + * 2. gpt-4o-2024-08-06 and later + * + * Older models like gpt-4-turbo and earlier may use JSON mode ([OldResponseFormat.JSON_OBJECT]) instead. + * + * Setting to [OldResponseFormat.JSON_OBJECT] enables JSON mode, which guarantees the message the model + * generates is valid JSON. + * + * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user + * message. Without this, the model may generate an unending stream of whitespace until the generation reaches the + * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be + * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or + * the conversation exceeded the max context length. + * + */ + @SerialName("response_format") val responseFormat: ResponseFormat? = null, ) /** @@ -99,6 +161,67 @@ public class RunRequestBuilder { */ public var metadata: Map? = null + /** + * What sampling temperature to use, between 0 and 2. + * Higher values like 0.8 will make the output more random, + * while lower values like 0.2 will make it more focused and deterministic. + */ + public var temperature: Double? = null + + /** + * An alternative to sampling with temperature, called nucleus sampling, + * where the model considers the results of the tokens with top_p probability mass. + * So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + public var topP: Double? = null + + /** + * The maximum number of prompt tokens that may be used over the course of the run. + * The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. + * If the run exceeds the number of prompt tokens specified, the run will end with status incomplete. + * See incomplete_details for more info. + */ + public var maxPromptTokens: Int? = null + + /** + * The maximum number of completion tokens that may be used over the course of the run. + * The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. + * If the run exceeds the number of completion tokens specified, the run will end with status incomplete. + * See incomplete_details for more info. + */ + public var maxCompletionTokens: Int? = null + + /** + * Whether to enable parallel function calling during tool use. + */ + public var parallelToolCalls: Boolean? = null + + /** + * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and all GPT-3.5 Turbo + * models since gpt-3.5-turbo-1106. + * + * Setting to [ResponseFormat.JsonSchemaResponseFormat] enables Structured Outputs which ensures the model will match your supplied JSON schema. + * + * Structured Outputs [ResponseFormat.JsonSchemaResponseFormat] are available in our latest large language models, starting with GPT-4o: + * 1. gpt-4o-mini-2024-07-18 and later + * 2. gpt-4o-2024-08-06 and later + * + * Older models like gpt-4-turbo and earlier may use JSON mode [ResponseFormat.JsonObjectResponseFormat] instead. + * + * Setting to [ResponseFormat.JsonObjectResponseFormat] enables JSON mode, which guarantees the message the model + * generates is valid JSON. + * + * important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user + * message. Without this, the model may generate an unending stream of whitespace until the generation reaches the + * token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be + * partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or + * the conversation exceeded the max context length. + * + */ + public var responseFormat: ResponseFormat? = null + /** * Build a [RunRequest] instance. */ @@ -109,5 +232,11 @@ public class RunRequestBuilder { additionalInstructions = additionalInstructions, tools = tools, metadata = metadata, + temperature = temperature, + topP = topP, + parallelToolCalls = parallelToolCalls, + maxCompletionTokens = maxCompletionTokens, + maxPromptTokens = maxPromptTokens, + responseFormat = responseFormat, ) } diff --git a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt index 40a4153a..5f094fcc 100644 --- a/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt +++ b/sample/jvm/src/main/kotlin/com/aallam/openai/sample/jvm/AssistantsFunction.kt @@ -2,12 +2,14 @@ package com.aallam.openai.sample.jvm import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.assistant.AssistantRequest -import com.aallam.openai.api.assistant.AssistantResponseFormat import com.aallam.openai.api.assistant.AssistantTool import com.aallam.openai.api.assistant.Function import com.aallam.openai.api.chat.ToolCall +import com.aallam.openai.api.core.JsonSchema import com.aallam.openai.api.core.Parameters +import com.aallam.openai.api.core.ResponseFormat import com.aallam.openai.api.core.Role +import com.aallam.openai.api.core.Schema import com.aallam.openai.api.core.Status import com.aallam.openai.api.message.MessageContent import com.aallam.openai.api.message.MessageRequest @@ -18,10 +20,11 @@ import com.aallam.openai.api.run.RunRequest import com.aallam.openai.api.run.ToolOutput import com.aallam.openai.client.OpenAI import kotlinx.coroutines.delay +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject @@ -33,35 +36,38 @@ suspend fun assistantsFunctions(openAI: OpenAI) { request = AssistantRequest( name = "Math Tutor", instructions = "You are a weather bot. Use the provided functions to answer questions.", - responseFormat = AssistantResponseFormat.JSON_SCHEMA( - name = "math_response", - strict = true, - schema = buildJsonObject { - put("type", "object") - putJsonObject("properties") { - putJsonObject("steps") { - put("type", "array") - putJsonObject("items") { - put("type", "object") - putJsonObject("properties") { - putJsonObject("explanation") { - put("type", "string") - } - putJsonObject("output") { - put("type", "string") + responseFormat = ResponseFormat.JsonSchemaResponseFormat( + schema = JsonSchema( + name = "math_response", + strict = true, + description = "The response format for the math tutor assistant.", + schema = Schema.buildJsonObject { + put("type", "object") + putJsonObject("properties") { + putJsonObject("steps") { + put("type", "array") + putJsonObject("items") { + put("type", "object") + putJsonObject("properties") { + putJsonObject("explanation") { + put("type", "string") + } + putJsonObject("output") { + put("type", "string") + } } + put("required", JsonArray(listOf(JsonPrimitive("explanation"), JsonPrimitive("output")))) + put("additionalProperties", false) } - put("required", JsonArray(listOf(JsonPrimitive("explanation"), JsonPrimitive("output")))) - put("additionalProperties", false) + } + putJsonObject("final_answer") { + put("type", "string") } } - putJsonObject("final_answer") { - put("type", "string") - } + put("additionalProperties", false) + put("required", JsonArray(listOf(JsonPrimitive("steps"), JsonPrimitive("final_answer")))) } - put("additionalProperties", false) - put("required", JsonArray(listOf(JsonPrimitive("steps"), JsonPrimitive("final_answer")))) - }, + ) ), tools = listOf( AssistantTool.FunctionTool( @@ -112,7 +118,9 @@ suspend fun assistantsFunctions(openAI: OpenAI) { ) ) - // 2. Create a thread + println(Json.encodeToString(assistant)) + + //2. Create a thread val thread = openAI.thread() // 3. Add a message to the thread @@ -133,10 +141,10 @@ suspend fun assistantsFunctions(openAI: OpenAI) { // 4. Run the assistant val run = openAI.createRun( threadId = thread.id, - request = RunRequest(assistantId = assistant.id) + request = RunRequest(assistantId = assistant.id, responseFormat = ResponseFormat.TextResponseFormat) ) - // 5. Check the run status +// // 5. Check the run status var retrievedRun: Run do { delay(1500)