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)