From 09ab9fe45a1d5463f84e9cde7af57f88c740573c Mon Sep 17 00:00:00 2001 From: Mouaad Aallam Date: Sun, 12 Nov 2023 20:10:13 +0100 Subject: [PATCH 1/3] feat(assistants): api implementation --- .../com.aallam.openai.client/Assistants.kt | 120 ++++++++++++++ .../kotlin/com.aallam.openai.client/Audio.kt | 6 +- .../kotlin/com.aallam.openai.client/OpenAI.kt | 3 +- .../internal/OpenAIApi.kt | 1 + .../internal/api/ApiPath.kt | 1 + .../internal/api/AssistantsApi.kt | 152 ++++++++++++++++++ .../internal/extension/Request.kt | 5 + .../aallam/openai/client/TestAssistants.kt | 40 +++++ .../assistant/Assistant.kt | 63 ++++++++ .../assistant/AssistantFile.kt | 26 +++ .../assistant/AssistantId.kt | 13 ++ .../assistant/AssistantRequest.kt | 111 +++++++++++++ .../assistant/AssistantTool.kt | 55 +++++++ .../assistant/Function.kt | 67 ++++++++ .../internal/AssistantToolSerializer.kt | 43 +++++ .../chat/ChatCompletionFunction.kt | 9 +- .../chat/ChatCompletionRequest.kt | 1 + .../com.aallam.openai.api/chat/Parameters.kt | 68 +------- .../kotlin/com.aallam.openai.api/chat/Tool.kt | 7 +- .../core/ListResponse.kt | 17 +- .../com.aallam.openai.api/core/Parameters.kt | 71 ++++++++ .../com.aallam.openai.api/core/SortOrder.kt | 13 ++ 22 files changed, 814 insertions(+), 78 deletions(-) create mode 100644 openai-client/src/commonMain/kotlin/com.aallam.openai.client/Assistants.kt create mode 100644 openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt create mode 100644 openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantFile.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantId.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantTool.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Parameters.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/SortOrder.kt diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Assistants.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Assistants.kt new file mode 100644 index 00000000..b0627c86 --- /dev/null +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Assistants.kt @@ -0,0 +1,120 @@ +package com.aallam.openai.client + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.assistant.Assistant +import com.aallam.openai.api.assistant.AssistantFile +import com.aallam.openai.api.assistant.AssistantId +import com.aallam.openai.api.assistant.AssistantRequest +import com.aallam.openai.api.core.SortOrder +import com.aallam.openai.api.file.FileId + +/** + * Build assistants that can call models and use tools to perform tasks. + */ +@BetaOpenAI +public interface Assistants { + + /** + * Create an assistant with a model and instructions. + * + * @param request the request to create an assistant. + */ + @BetaOpenAI + public suspend fun assistant(request: AssistantRequest): Assistant + + /** + * Retrieves an assistant. + * + * @param id the ID of the assistant to retrieve. + */ + @BetaOpenAI + public suspend fun assistant(id: AssistantId): Assistant? + + /** + * Update an assistant. + * + * @param id rhe ID of the assistant to modify. + */ + @BetaOpenAI + public suspend fun assistant(id: AssistantId, request: AssistantRequest): Assistant + + /** + * Delete an assistant. + * + * @param id ID of the assistant to delete. + */ + @BetaOpenAI + public suspend fun delete(id: AssistantId): Boolean + + /** + * Returns a list of assistants. + * + * @param limit a limit on the number of objects to be returned. The Limit can range between 1 and 100, and the default is 20. + * @param order sort order by the `createdAt` timestamp of the objects. [SortOrder.Ascending] for ascending order + * and [SortOrder.Descending] for descending order. + * @param after a cursor for use in pagination. `after` is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can + * include `after="obj_foo"` in order to fetch the next page of the list. + * @param before a cursor for use in pagination. Before is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with `obj_foo`, your subsequent call can + * include `before="obj_foo"` in order to fetch the previous page of the list. + */ + @BetaOpenAI + public suspend fun assistants( + limit: Int? = null, + order: SortOrder? = null, + after: String? = null, + before: String? = null, + ): List + + /** + * Create an assistant file by attaching a File to an assistant. + * + * @param assistantId the ID of the assistant for which to create a File. + * @param fileId a File ID (with purpose="assistants") that the assistant should use. + * Useful for tools like retrieval and code interpreter that can access files. + */ + @BetaOpenAI + public suspend fun create(assistantId: AssistantId, fileId: FileId): AssistantFile + + /** + * Retrieves an [AssistantFile]. + * + * @param assistantId the ID of the assistant who the file belongs to. + * @param fileId the ID of the file we're getting. + */ + @BetaOpenAI + public suspend fun file(assistantId: AssistantId, fileId: FileId): AssistantFile + + /** + * Delete an assistant file. + * + * @param assistantId the ID of the assistant that the file belongs to. + * @param fileId the ID of the file to delete. + */ + @BetaOpenAI + public suspend fun delete(assistantId: AssistantId, fileId: FileId): Boolean + + /** + * Returns a list of assistant files. + * + * @param id the ID of the assistant the file belongs to. + * @param limit a limit on the number of objects to be returned. The Limit can range between 1 and 100, and the default is 20. + * @param order sort order by the `createdAt` timestamp of the objects. [SortOrder.Ascending] for ascending order + * and [SortOrder.Descending] for descending order. + * @param after a cursor for use in pagination. `after` is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can + * include `after="obj_foo"` in order to fetch the next page of the list. + * @param before a cursor for use in pagination. Before is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with `obj_foo`, your subsequent call can + * include `before="obj_foo"` in order to fetch the previous page of the list. + */ + @BetaOpenAI + public suspend fun files( + id: AssistantId, + limit: Int? = null, + order: SortOrder? = null, + after: String? = null, + before: String? = null, + ): List +} diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Audio.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Audio.kt index 3c3e0d91..15fb8598 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Audio.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Audio.kt @@ -1,7 +1,9 @@ package com.aallam.openai.client -import com.aallam.openai.api.BetaOpenAI -import com.aallam.openai.api.audio.* +import com.aallam.openai.api.audio.Transcription +import com.aallam.openai.api.audio.TranscriptionRequest +import com.aallam.openai.api.audio.Translation +import com.aallam.openai.api.audio.TranslationRequest /** * Learn how to turn audio into text. diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt index ec7a8a23..adea0140 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt @@ -11,8 +11,7 @@ import kotlin.time.Duration.Companion.seconds * OpenAI API. */ public interface OpenAI : Completions, Files, Edits, Embeddings, Models, Moderations, FineTunes, Images, Chat, Audio, - FineTuning, - Closeable + FineTuning, Assistants, Closeable /** * Creates an instance of [OpenAI]. diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt index 4faa42cc..a0adcaf6 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt @@ -23,4 +23,5 @@ internal class OpenAIApi( Chat by ChatApi(requester), Audio by AudioApi(requester), FineTuning by FineTuningApi(requester), + Assistants by AssistantsApi(requester), Closeable by requester diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt index 651cd7fa..b5da3760 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt @@ -18,4 +18,5 @@ internal object ApiPath { const val Models = "models" const val Moderations = "moderations" const val FineTuningJobs = "fine_tuning/jobs" + const val Assistants = "assistants" } diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt new file mode 100644 index 00000000..14c9e7af --- /dev/null +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt @@ -0,0 +1,152 @@ +package com.aallam.openai.client.internal.api + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.assistant.Assistant +import com.aallam.openai.api.assistant.AssistantFile +import com.aallam.openai.api.assistant.AssistantId +import com.aallam.openai.api.assistant.AssistantRequest +import com.aallam.openai.api.core.DeleteResponse +import com.aallam.openai.api.core.ListResponse +import com.aallam.openai.api.core.SortOrder +import com.aallam.openai.api.exception.OpenAIAPIException +import com.aallam.openai.api.file.FileId +import com.aallam.openai.client.Assistants +import com.aallam.openai.client.internal.extension.beta +import com.aallam.openai.client.internal.http.HttpRequester +import com.aallam.openai.client.internal.http.perform +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +internal class AssistantsApi(val requester: HttpRequester) : Assistants { + @BetaOpenAI + override suspend fun assistant(request: AssistantRequest): Assistant { + return requester.perform { + it.post { + url(path = ApiPath.Assistants) + setBody(request) + contentType(ContentType.Application.Json) + beta("assistants", 1) + }.body() + } + } + + @BetaOpenAI + override suspend fun assistant(id: AssistantId): Assistant? { + try { + return requester.perform { + it.get { + url(path = "${ApiPath.Assistants}/${id.id}") + beta("assistants", 1) + } + }.body() + } catch (e: OpenAIAPIException) { + if (e.statusCode == HttpStatusCode.NotFound.value) return null + throw e + } + } + + @BetaOpenAI + override suspend fun assistant(id: AssistantId, request: AssistantRequest): Assistant { + return requester.perform { + it.post { + url(path = "${ApiPath.Assistants}/${id.id}") + setBody(request) + contentType(ContentType.Application.Json) + beta("assistants", 1) + }.body() + } + } + + @BetaOpenAI + override suspend fun delete(id: AssistantId): Boolean { + val response = requester.perform { + it.delete { + url(path = "${ApiPath.Assistants}/${id.id}") + beta("assistants", 1) + } + } + return when (response.status) { + HttpStatusCode.NotFound -> false + else -> response.body().deleted + } + } + + @BetaOpenAI + override suspend fun delete(assistantId: AssistantId, fileId: FileId): Boolean { + val response = requester.perform { + it.delete { + url(path = "${ApiPath.Assistants}/${assistantId.id}/files/${fileId.id}") + beta("assistants", 1) + } + } + return when (response.status) { + HttpStatusCode.NotFound -> false + else -> response.body().deleted + } + } + + @BetaOpenAI + override suspend fun assistants(limit: Int?, order: SortOrder?, after: String?, before: String?): List { + return requester.perform> { client -> + client.get { + url { + path(ApiPath.Assistants) + limit?.let { parameter("limit", it) } + order?.let { parameter("order", it.order) } + after?.let { parameter("after", it) } + before?.let { parameter("before", it) } + } + beta("assistants", 1) + }.body() + } + } + + @BetaOpenAI + override suspend fun create(assistantId: AssistantId, fileId: FileId): AssistantFile { + val request = buildJsonObject { put("file", fileId.id) } + return requester.perform { + it.post { + url(path = "${ApiPath.Assistants}/${assistantId.id}") + setBody(request) + contentType(ContentType.Application.Json) + beta("assistants", 1) + }.body() + } + } + + @BetaOpenAI + override suspend fun file(assistantId: AssistantId, fileId: FileId): AssistantFile { + return requester.perform { + it.get { + url(path = "${ApiPath.Assistants}/${assistantId.id}/files/${fileId.id}") + beta("assistants", 1) + } + } + } + + @BetaOpenAI + override suspend fun files( + id: AssistantId, + limit: Int?, + order: SortOrder?, + after: String?, + before: String? + ): List { + return requester.perform> { client -> + client.get { + url { + path("${ApiPath.Assistants}/${id.id}/files") + limit?.let { parameter("limit", it) } + order?.let { parameter("order", it.order) } + after?.let { parameter("after", it) } + before?.let { parameter("before", it) } + } + beta("assistants", 1) + }.body() + } + } +} \ No newline at end of file diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/extension/Request.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/extension/Request.kt index 6e1256bb..8aabcc8d 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/extension/Request.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/extension/Request.kt @@ -3,6 +3,7 @@ package com.aallam.openai.client.internal.extension import com.aallam.openai.api.completion.CompletionRequest import com.aallam.openai.api.file.FileSource import com.aallam.openai.client.internal.JsonLenient +import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.http.* import io.ktor.utils.io.core.* @@ -37,3 +38,7 @@ internal fun FormBuilder.appendFileSource(key: String, fileSource: FileSource) { } } } + +internal fun HttpMessageBuilder.beta(api: String, version: Int) { + header("OpenAI-Beta", "$api=v$version") +} 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 new file mode 100644 index 00000000..8c012235 --- /dev/null +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestAssistants.kt @@ -0,0 +1,40 @@ +package com.aallam.openai.client + +import com.aallam.openai.api.assistant.CodeInterpreterTool +import com.aallam.openai.api.assistant.assistantRequest +import com.aallam.openai.api.model.ModelId +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TestAssistants : TestOpenAI() { + + @Test + fun listAssistants() = test { + val request = assistantRequest { + name = "Math Tutor" + tools = listOf(CodeInterpreterTool) + model = ModelId("gpt-4") + } + val assistant = openAI.assistant(request) + assertEquals(request.name, assistant.name) + assertEquals(request.tools, assistant.tools) + assertEquals(request.model, assistant.model) + + val getAssistant = openAI.assistant(assistant.id) + assertEquals(getAssistant, assistant) + + val assistants = openAI.assistants() + assertTrue { assistants.isNotEmpty() } + + val updated = assistantRequest { name = "Super Math Tutor" } + val updatedAssistant = openAI.assistant(assistant.id, updated) + assertEquals(updated.name, updatedAssistant.name) + + openAI.delete(updatedAssistant.id) + + val fileGetAfterDelete = openAI.assistant(updatedAssistant.id) + assertNull(fileGetAfterDelete) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..3c22884f --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Assistant.kt @@ -0,0 +1,63 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.file.FileId +import com.aallam.openai.api.model.ModelId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@BetaOpenAI +@Serializable +public data class Assistant( + /** + * The identifier, which can be referenced in API endpoints. + */ + @SerialName("id") public val id: AssistantId, + /** + * The Unix timestamp (in seconds) for when the assistant was created. + */ + @SerialName("created_at") public val createdAt: Long, + + /** + * The name of the assistant. The maximum length is 256 characters. + */ + @SerialName("name") public val name: String, + + /** + * The description of the assistant. The maximum length is 512 characters. + */ + @SerialName("description") public val description: String? = null, + + /** + * ID of the model to use. You can use the [List](https://platform.openai.com/docs/api-reference/models/list) models + * API to see all of your [available models](https://platform.openai.com/docs/models/overview), or see our Model + * overview for descriptions of them. + */ + @SerialName("model") public val model: ModelId, + + /** + * The system instructions that the assistant uses. The maximum length is 32768 characters. + */ + @SerialName("instructions") public val instructions: String? = null, + + /** + * A list of tool enabled on the assistant. + * There can be a maximum of 128 tools per assistant. + * Tools can be of types [CodeInterpreterTool], [RetrievalTool], or [FunctionTool]. + */ + @SerialName("tools") public val tools: List, + + /** + * A list of file IDs attached to this assistant. + * There can be a maximum of 20 files attached to the assistant. + * Files are ordered by their creation date in ascending order. + */ + @SerialName("file_ids") public val fileIds: List, + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + @SerialName("metadata") public val metadata: Map, +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantFile.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantFile.kt new file mode 100644 index 00000000..8cf0fe3d --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantFile.kt @@ -0,0 +1,26 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.file.FileId +import kotlinx.serialization.SerialName + +/** + * File attached to an assistant. + */ +@BetaOpenAI +public data class AssistantFile( + /** + * The identifier, which can be referenced in API endpoints. + */ + @SerialName("id") public val id: FileId, + + /** + * The Unix timestamp (in seconds) for when the assistant file was created. + */ + @SerialName("created_at") public val createdAt: Int, + + /** + * The assistant ID that the file is attached to. + */ + @SerialName("assistant_id") public val assistantId: AssistantId +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantId.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantId.kt new file mode 100644 index 00000000..e199f043 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantId.kt @@ -0,0 +1,13 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + +/** + * ID of an assistant. + */ +@BetaOpenAI +@Serializable +@JvmInline +public value class AssistantId(public val id: String) 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 new file mode 100644 index 00000000..2a05cf67 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantRequest.kt @@ -0,0 +1,111 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.file.FileId +import com.aallam.openai.api.model.ModelId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@BetaOpenAI +@Serializable +public data class AssistantRequest( + /** + * ID of the model to use. + * This is required if the assistant does not yet exist. + */ + @SerialName("model") val model: ModelId? = null, + + /** + * The name of the assistant. Optional. The maximum length is 256 characters. + */ + @SerialName("name") val name: String? = null, + + /** + * The description of the assistant. Optional. The maximum length is 512 characters. + */ + @SerialName("description") val description: String? = null, + + /** + * The system instructions that the assistant uses. Optional. The maximum length is 32768 characters. + */ + @SerialName("instructions") val instructions: String? = null, + + /** + * A list of tools enabled on the assistant. Optional. Defaults to an empty list. + * Tools can be of types code_interpreter, retrieval, or function. + */ + @SerialName("tools") val tools: List? = null, + + /** + * A list of file IDs attached to this assistant. Optional. Defaults to an empty list. + * There can be a maximum of 20 files attached to the assistant. + */ + @SerialName("file_ids") val fileIds: List? = null, + + /** + * Set of 16 key-value pairs that can be attached to an object. Optional. + * 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 +) + +@BetaOpenAI +@OpenAIDsl +public class AssistantRequestBuilder { + + /** + * The name of the assistant. The maximum length is 256 characters. + */ + public var name: String? = null + + /** + * The description of the assistant. The maximum length is 512 characters. + */ + public var description: String? = null + + /** + * ID of the model to use. + */ + public var model: ModelId? = null + + /** + * The system instructions that the assistant uses. The maximum length is 32768 characters. + */ + public var instructions: String? = null + + /** + * A list of tools enabled on the assistant. + */ + public var tools: List? = null + + /** + * A list of file IDs attached to this assistant. + */ + public var fileIds: List? = null + + /** + * Set of 16 key-value pairs that can be attached to an object. + */ + public var metadata: Map? = null + + /** + * Create [Assistant] instance. + */ + public fun build(): AssistantRequest = AssistantRequest( + model = model, + name = name, + description = description, + instructions = instructions, + tools = tools, + fileIds = fileIds, + metadata = metadata, + ) +} + +/** + * Creates [AssistantRequest] instance. + */ +@BetaOpenAI +public fun assistantRequest(block: AssistantRequestBuilder.() -> Unit): AssistantRequest = + AssistantRequestBuilder().apply(block).build() diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantTool.kt new file mode 100644 index 00000000..85c1978b --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/AssistantTool.kt @@ -0,0 +1,55 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.assistant.internal.AssistantToolSerializer +import kotlinx.serialization.Required +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Tool enabled on the assistant. There can be a maximum of 128 tools per assistant. + * Tools can be of types code_interpreter, retrieval, or function. + */ +@BetaOpenAI +@Serializable(with = AssistantToolSerializer::class) +public sealed interface AssistantTool { + public val type: String +} + +/** + * The type of tool being defined: code_interpreter + */ +@BetaOpenAI +@Serializable +public object CodeInterpreterTool : AssistantTool { + @SerialName("type") + @Required + override val type: String = "code_interpreter" +} + +/** + * The type of tool being defined: retrieval + */ +@BetaOpenAI +@Serializable +public object RetrievalTool : AssistantTool { + @SerialName("type") + @Required + override val type: String = "retrieval" +} + +/** + * The type of tool being defined: function + */ +@BetaOpenAI +@Serializable +public class FunctionTool( + /** + * The name of the function to be called. + */ + @SerialName("function") public val function: Function, +) : AssistantTool { + @SerialName("type") + @Required + override val type: String = "function" +} 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 new file mode 100644 index 00000000..127c3a1d --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/Function.kt @@ -0,0 +1,67 @@ +package com.aallam.openai.api.assistant + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.core.Parameters +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@BetaOpenAI +@Serializable +public data class Function( + /** + * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + * length of 64. + */ + @SerialName("name") val name: String, + /** + * The description of what the function does. + */ + @SerialName("description") val description: String, + /** + * The parameters the functions accept, described as a JSON Schema object. + * See [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. + * + * To describe a function that accepts no parameters, provide [Parameters.Empty]`. + */ + @SerialName("parameters") val parameters: Parameters, +) + +/** + * Builder of [Function] instances. + */ +@BetaOpenAI +@OpenAIDsl +public class FunctionBuilder { + + /** + * The name of the function to be called. + */ + public var name: String? = null + + /** + * The description of what the function does. + */ + public var description: String? = null + + /** + * The parameters the function accepts. + */ + public var parameters: Parameters? = Parameters.Empty + + /** + * 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" } + ) +} + +/** + * Creates [Function] instance. + */ +@BetaOpenAI +public fun function(block: FunctionBuilder.() -> Unit): Function = + FunctionBuilder().apply(block).build() diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt new file mode 100644 index 00000000..c3e1bb68 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt @@ -0,0 +1,43 @@ +package com.aallam.openai.api.assistant.internal + +import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.assistant.AssistantTool +import com.aallam.openai.api.assistant.CodeInterpreterTool +import com.aallam.openai.api.assistant.FunctionTool +import com.aallam.openai.api.assistant.RetrievalTool +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive + +@OptIn(BetaOpenAI::class) +internal class AssistantToolSerializer : KSerializer { + @OptIn(InternalSerializationApi::class) + override val descriptor: SerialDescriptor = buildSerialDescriptor("AssistantTool", PolymorphicKind.SEALED) + + override fun deserialize(decoder: Decoder): AssistantTool { + require(decoder is JsonDecoder) { "This decoder is not a JsonDecoder. Cannot deserialize `AssistantTool`" } + val json = decoder.decodeJsonElement() as JsonObject + val type = json["type"]?.jsonPrimitive?.content + return when (type) { + "code_interpreter" -> CodeInterpreterTool + "retrieval" -> RetrievalTool + "function" -> decoder.json.decodeFromJsonElement(FunctionTool.serializer(), json) + else -> throw UnsupportedOperationException("Cannot deserialize AssistantTool. Unsupported type $type.") + } + } + + override fun serialize(encoder: Encoder, value: AssistantTool) { + when (value) { + is CodeInterpreterTool -> CodeInterpreterTool.serializer().serialize(encoder, value) + is RetrievalTool -> RetrievalTool.serializer().serialize(encoder, value) + is FunctionTool -> FunctionTool.serializer().serialize(encoder, value) + } + } +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionFunction.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionFunction.kt index 511a45bd..8835e799 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionFunction.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionFunction.kt @@ -1,6 +1,7 @@ package com.aallam.openai.api.chat import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.core.Parameters import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,11 +17,7 @@ public data class ChatCompletionFunction( */ @SerialName("description") val description: String? = null, /** - * The parameters the functions accepts, described as a JSON Schema object. See the guide for examples and the - * JSON Schema reference for documentation about the format. - */ - /** - * The parameters the functions accepts, described as a JSON Schema object. + * The parameters the functions accept, described as a JSON Schema object. * See the [guide](https://github.com/aallam/openai-kotlin/blob/main/guides/ChatFunctionCall.md) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. * * To describe a function that accepts no parameters, provide [Parameters.Empty]`. @@ -47,7 +44,7 @@ public class ChatCompletionFunctionBuilder { /** * The parameters the function accepts. */ - public var parameters: Parameters? = null + public var parameters: Parameters? = Parameters.Empty /** * Create [ChatCompletionFunction] instance. diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt index 0362d36f..ab67fa93 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt @@ -4,6 +4,7 @@ package com.aallam.openai.api.chat import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.core.Parameters import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Parameters.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Parameters.kt index 5896f629..48ac8292 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Parameters.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Parameters.kt @@ -1,71 +1,7 @@ package com.aallam.openai.api.chat -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.* - /** * Represents parameters that a function accepts, described as a JSON Schema object. - * - * @property schema Json Schema object. */ -@Serializable(with = Parameters.JsonDataSerializer::class) -public data class Parameters(public val schema: JsonElement) { - - /** - * Custom serializer for the [Parameters] class. - */ - public object JsonDataSerializer : KSerializer { - override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor - - /** - * Deserializes [Parameters] from JSON format. - */ - override fun deserialize(decoder: Decoder): Parameters { - require(decoder is JsonDecoder) { "This decoder is not a JsonDecoder. Cannot deserialize `FunctionParameters`." } - return Parameters(decoder.decodeJsonElement()) - } - - /** - * Serializes [Parameters] to JSON format. - */ - override fun serialize(encoder: Encoder, value: Parameters) { - require(encoder is JsonEncoder) { "This encoder is not a JsonEncoder. Cannot serialize `FunctionParameters`." } - encoder.encodeJsonElement(value.schema) - } - } - - public companion object { - - /** - * Creates a [Parameters] instance from a JSON string. - * - * @param json The JSON string to parse. - */ - public fun fromJsonString(json: String): Parameters = Parameters(Json.parseToJsonElement(json)) - - /** - * Creates a [Parameters] instance using a [JsonObjectBuilder]. - * - * @param block The [JsonObjectBuilder] to use. - */ - public fun buildJsonObject(block: JsonObjectBuilder.() -> Unit): Parameters { - val json = kotlinx.serialization.json.buildJsonObject(block) - return Parameters(json) - } - - /** - * Represents a no params function. Equivalent to: - * ```json - * {"type": "object", "properties": {}} - * ``` - */ - public val Empty: Parameters = buildJsonObject { - put("type", "object") - putJsonObject("properties") {} - } - } -} +@Deprecated("Use com.aallam.openai.api.core.Parameters instead", ReplaceWith("com.aallam.openai.api.core.Parameters")) +public typealias Parameters = com.aallam.openai.api.core.Parameters diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Tool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Tool.kt index 59d0cf5b..aa81700f 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Tool.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/Tool.kt @@ -1,6 +1,7 @@ package com.aallam.openai.api.chat import com.aallam.openai.api.chat.internal.ToolType +import com.aallam.openai.api.core.Parameters import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -36,7 +37,11 @@ public data class Tool( * @param parameters The parameters the function accepts, described as a JSON Schema object. */ public fun function(name: String, description: String? = null, parameters: Parameters): Tool = - Tool(type = ToolType.Function, description = description, function = FunctionTool(name = name, parameters = parameters)) + Tool( + type = ToolType.Function, + description = description, + function = FunctionTool(name = name, parameters = parameters) + ) } } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ListResponse.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ListResponse.kt index 30992939..1f963d9f 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ListResponse.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/ListResponse.kt @@ -18,4 +18,19 @@ public class ListResponse( * Embedding usage data. */ @SerialName("usage") public val usage: Usage? = null, -) + + /** + * The ID of the first element returned. + */ + @SerialName("first_id") public val firstId: String? = null, + + /** + * The ID of the last element returned. + */ + @SerialName("last_id") public val lastId: String? = null, + + /** + * If the list is truncated. + */ + @SerialName("has_more") public val hasMore: Boolean? = null, +) : List by data diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Parameters.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Parameters.kt new file mode 100644 index 00000000..db6fc3d0 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/Parameters.kt @@ -0,0 +1,71 @@ +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.* + +/** + * Represents parameters that a function accepts, described as a JSON Schema object. + * + * @property schema Json Schema object. + */ +@Serializable(with = Parameters.JsonDataSerializer::class) +public data class Parameters(public val schema: JsonElement) { + + /** + * Custom serializer for the [Parameters] class. + */ + public object JsonDataSerializer : KSerializer { + override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor + + /** + * Deserializes [Parameters] from JSON format. + */ + override fun deserialize(decoder: Decoder): Parameters { + require(decoder is JsonDecoder) { "This decoder is not a JsonDecoder. Cannot deserialize `FunctionParameters`." } + return Parameters(decoder.decodeJsonElement()) + } + + /** + * Serializes [Parameters] to JSON format. + */ + override fun serialize(encoder: Encoder, value: Parameters) { + require(encoder is JsonEncoder) { "This encoder is not a JsonEncoder. Cannot serialize `FunctionParameters`." } + encoder.encodeJsonElement(value.schema) + } + } + + public companion object { + + /** + * Creates a [Parameters] instance from a JSON string. + * + * @param json The JSON string to parse. + */ + public fun fromJsonString(json: String): Parameters = Parameters(Json.parseToJsonElement(json)) + + /** + * Creates a [Parameters] instance using a [JsonObjectBuilder]. + * + * @param block The [JsonObjectBuilder] to use. + */ + public fun buildJsonObject(block: JsonObjectBuilder.() -> Unit): Parameters { + val json = kotlinx.serialization.json.buildJsonObject(block) + return Parameters(json) + } + + /** + * Represents a no params function. Equivalent to: + * ```json + * {"type": "object", "properties": {}} + * ``` + */ + public val Empty: Parameters = buildJsonObject { + put("type", "object") + putJsonObject("properties") {} + } + } +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/SortOrder.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/SortOrder.kt new file mode 100644 index 00000000..a478ffd9 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/core/SortOrder.kt @@ -0,0 +1,13 @@ +package com.aallam.openai.api.core + +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + +@Serializable +@JvmInline +public value class SortOrder(public val order: String) { + public companion object { + public val Ascending: SortOrder = SortOrder("asc") + public val Descending: SortOrder = SortOrder("desc") + } +} From 15218fb31b2ffa9187dbb6ee7972e4b607c23214 Mon Sep 17 00:00:00 2001 From: Mouaad Aallam Date: Sun, 12 Nov 2023 20:11:34 +0100 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcaed28f..a3ba74c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **Images**: Support for model selection for `ImageCreation`, `ImageEdit` and `ImageVariations` (#257) - **Chat**: add tool calls (#256) - **Chat**: add vision feature (#258) +- **Assistants**: add `Assistants` feature (#259) # 3.5.1 > Published 05 Nov 2023 From 7a07c2aee20011bb9ec9e3f07c63eea2f361f4d0 Mon Sep 17 00:00:00 2001 From: Mouaad Aallam Date: Sun, 12 Nov 2023 20:26:51 +0100 Subject: [PATCH 3/3] apply lint changes --- .../com.aallam.openai.client/internal/api/AssistantsApi.kt | 2 +- .../kotlin/com/aallam/openai/client/TestAssistants.kt | 2 +- .../assistant/internal/AssistantToolSerializer.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt index 14c9e7af..24375391 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/AssistantsApi.kt @@ -149,4 +149,4 @@ internal class AssistantsApi(val requester: HttpRequester) : Assistants { }.body() } } -} \ No newline at end of file +} 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 8c012235..f7bcb4ce 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 @@ -37,4 +37,4 @@ class TestAssistants : TestOpenAI() { val fileGetAfterDelete = openAI.assistant(updatedAssistant.id) assertNull(fileGetAfterDelete) } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt index c3e1bb68..3a684248 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/assistant/internal/AssistantToolSerializer.kt @@ -40,4 +40,4 @@ internal class AssistantToolSerializer : KSerializer { is FunctionTool -> FunctionTool.serializer().serialize(encoder, value) } } -} \ No newline at end of file +}