Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assistants): api implementation #259

Merged
merged 3 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Assistant>

/**
* 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<AssistantFile>
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -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<HttpResponse> {
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<HttpResponse> {
it.delete {
url(path = "${ApiPath.Assistants}/${id.id}")
beta("assistants", 1)
}
}
return when (response.status) {
HttpStatusCode.NotFound -> false
else -> response.body<DeleteResponse>().deleted
}
}

@BetaOpenAI
override suspend fun delete(assistantId: AssistantId, fileId: FileId): Boolean {
val response = requester.perform<HttpResponse> {
it.delete {
url(path = "${ApiPath.Assistants}/${assistantId.id}/files/${fileId.id}")
beta("assistants", 1)
}
}
return when (response.status) {
HttpStatusCode.NotFound -> false
else -> response.body<DeleteResponse>().deleted
}
}

@BetaOpenAI
override suspend fun assistants(limit: Int?, order: SortOrder?, after: String?, before: String?): List<Assistant> {
return requester.perform<ListResponse<Assistant>> { 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<AssistantFile> {
return requester.perform<ListResponse<AssistantFile>> { 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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading