Skip to content

Commit eaec578

Browse files
committed
Add files api
1 parent 33a99a7 commit eaec578

File tree

13 files changed

+474
-6
lines changed

13 files changed

+474
-6
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package files
2+
3+
import com.cjcrafter.openai.files.FilePurpose
4+
import com.cjcrafter.openai.files.ListFilesRequest
5+
import com.cjcrafter.openai.files.fileUploadRequest
6+
import com.cjcrafter.openai.files.listFilesRequest
7+
import com.cjcrafter.openai.openAI
8+
import io.github.cdimascio.dotenv.dotenv
9+
import java.io.File
10+
import java.util.Scanner
11+
12+
// To use dotenv, you need to add the "io.github.cdimascio:dotenv-kotlin:version"
13+
// dependency. Then you can add a .env file in your project directory.
14+
val openai = openAI { apiKey(dotenv()["OPENAI_TOKEN"]) }
15+
16+
fun main() {
17+
do {
18+
println("""
19+
1. List files
20+
2. Upload file
21+
3. Delete file
22+
4. Retrieve file
23+
5. Retrieve file contents
24+
6. Exit
25+
""".trimIndent())
26+
print("Enter your choice: ")
27+
val choice = readln().toInt()
28+
when (choice) {
29+
1 -> listFiles()
30+
2 -> uploadFile()
31+
3 -> deleteFile()
32+
4 -> retrieveFile()
33+
5 -> retrieveFileContents()
34+
}
35+
} while (choice != 6)
36+
}
37+
38+
fun listFiles() {
39+
val response = openai.listFiles(listFilesRequest { })
40+
println(response)
41+
}
42+
43+
fun uploadFile() {
44+
print("Enter the file name: ")
45+
val fileName = readln()
46+
val input = File(fileName)
47+
val request = fileUploadRequest {
48+
file(input)
49+
purpose(FilePurpose.ASSISTANTS)
50+
}
51+
val response = openai.uploadFile(request)
52+
println(response)
53+
}
54+
55+
fun deleteFile() {
56+
print("Enter the file id: ")
57+
val fileId = readln()
58+
val response = openai.deleteFile(fileId)
59+
println(response)
60+
}
61+
62+
fun retrieveFile() {
63+
print("Enter the file id: ")
64+
val fileId = readln()
65+
val response = openai.retrieveFile(fileId)
66+
println(response)
67+
}
68+
69+
fun retrieveFileContents() {
70+
print("Enter the file id: ")
71+
val fileId = readln()
72+
val contents = openai.retrieveFileContents(fileId)
73+
println(contents)
74+
}

src/main/kotlin/com/cjcrafter/openai/OpenAI.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.cjcrafter.openai.completions.CompletionResponse
77
import com.cjcrafter.openai.completions.CompletionResponseChunk
88
import com.cjcrafter.openai.embeddings.EmbeddingsRequest
99
import com.cjcrafter.openai.embeddings.EmbeddingsResponse
10+
import com.cjcrafter.openai.files.*
1011
import com.cjcrafter.openai.util.OpenAIDslMarker
1112
import com.fasterxml.jackson.annotation.JsonAutoDetect
1213
import com.fasterxml.jackson.annotation.JsonInclude
@@ -19,6 +20,21 @@ import org.jetbrains.annotations.ApiStatus
1920
import org.jetbrains.annotations.Contract
2021
import org.slf4j.LoggerFactory
2122

23+
/**
24+
* The main interface for the OpenAI API. This interface contains methods for
25+
* all the API endpoints. To instantiate an instance of this interface, use
26+
* [builder].
27+
*
28+
* All the methods in this class are blocking (except the stream methods,
29+
* [streamCompletion] and [streamChatCompletion], which return an iterator
30+
* which blocks the thread).
31+
*
32+
* The methods in this class all throw io exceptions if the request fails. The
33+
* error message will contain the JSON response from the API (if present).
34+
* Common errors include:
35+
* 1. Not having a valid API key
36+
* 2. Passing a bad parameter to a request
37+
*/
2238
interface OpenAI {
2339

2440
/**
@@ -99,8 +115,64 @@ interface OpenAI {
99115
* @return The response from the API
100116
*/
101117
@Contract(pure = true)
118+
@ApiStatus.Experimental
102119
fun createEmbeddings(request: EmbeddingsRequest): EmbeddingsResponse
103120

121+
/**
122+
* Calls the [list files](https://platform.openai.com/docs/api-reference/files)
123+
* endpoint to return a list of all files that belong to your organization.
124+
* This method is blocking.
125+
*
126+
* @param request The request to send to the API
127+
* @return The list of files returned from the API
128+
*/
129+
@Contract(pure = true)
130+
@ApiStatus.Experimental
131+
fun listFiles(request: ListFilesRequest): ListFilesResponse
132+
133+
/**
134+
* Uploads a file to the [files](https://platform.openai.com/docs/api-reference/files)
135+
* endpoint. This method is blocking.
136+
*
137+
* @param request The file to upload
138+
* @return The OpenAI file object created
139+
*/
140+
@ApiStatus.Experimental
141+
fun uploadFile(request: FileUploadRequest): FileObject
142+
143+
@ApiStatus.Experimental
144+
fun deleteFile(fileId: String): FileDeletionStatus
145+
146+
/**
147+
* Retrieves the file wrapper data using the [files](https://platform.openai.com/docs/api-reference/files)
148+
* endpoint. This method is blocking.
149+
*
150+
* This method does not return the *contents* of the file, only some metadata.
151+
* To retrieve the contents of the file, use [retrieveFileContents].
152+
*
153+
* @param fileId The id of the file to retrieve
154+
* @return The OpenAI file object
155+
*/
156+
@Contract(pure = true)
157+
@ApiStatus.Experimental
158+
fun retrieveFile(fileId: String): FileObject
159+
160+
/**
161+
* Returns the contents of the file as a string. This method is blocking.
162+
*
163+
* OpenAI does not allow you to download files that you uploaded. Instead,
164+
* this method will only work if the file's purpose is:
165+
* 1. [FilePurpose.ASSISTANTS_OUTPUT]
166+
* 2. [FilePurpose.FINE_TUNE_RESULTS]
167+
*
168+
* @param fileId The id of the file to retrieve
169+
* @return The contents of the file as a string
170+
*/
171+
@Contract(pure = true)
172+
@ApiStatus.Experimental
173+
fun retrieveFileContents(fileId: String): String
174+
175+
104176
@OpenAIDslMarker
105177
open class Builder internal constructor() {
106178
protected var apiKey: String? = null

src/main/kotlin/com/cjcrafter/openai/OpenAIImpl.kt

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import com.cjcrafter.openai.completions.CompletionResponse
66
import com.cjcrafter.openai.completions.CompletionResponseChunk
77
import com.cjcrafter.openai.embeddings.EmbeddingsRequest
88
import com.cjcrafter.openai.embeddings.EmbeddingsResponse
9+
import com.cjcrafter.openai.files.*
910
import com.fasterxml.jackson.databind.JavaType
1011
import com.fasterxml.jackson.databind.node.ObjectNode
1112
import okhttp3.*
13+
import okhttp3.HttpUrl.Companion.toHttpUrl
1214
import okhttp3.MediaType.Companion.toMediaType
1315
import okhttp3.RequestBody.Companion.toRequestBody
16+
import org.intellij.lang.annotations.Language
1417
import org.jetbrains.annotations.ApiStatus
1518
import java.io.BufferedReader
19+
import java.io.File
1620
import java.io.IOException
1721

1822
open class OpenAIImpl @ApiStatus.Internal constructor(
@@ -35,7 +39,36 @@ open class OpenAIImpl @ApiStatus.Internal constructor(
3539
.post(body).build()
3640
}
3741

38-
protected open fun <T> executeRequest(httpRequest: Request, responseType: Class<T>): T {
42+
protected open fun buildRequestNoBody(endpoint: String, params: Map<String, Any>? = null): Request.Builder{
43+
val url = "$baseUrl/$endpoint".toHttpUrl().newBuilder()
44+
.apply {
45+
params?.forEach { (key, value) -> addQueryParameter(key, value.toString()) }
46+
}.build().toString()
47+
48+
return Request.Builder()
49+
.url(url)
50+
.addHeader("Authorization", "Bearer $apiKey")
51+
.apply { if (organization != null) addHeader("OpenAI-Organization", organization) }
52+
}
53+
54+
protected open fun buildMultipartRequest(
55+
endpoint: String,
56+
function: MultipartBody.Builder.() -> Unit,
57+
): Request {
58+
59+
val multipartBody = MultipartBody.Builder()
60+
.setType(MultipartBody.FORM)
61+
.apply(function)
62+
.build()
63+
64+
return Request.Builder()
65+
.url("$baseUrl/$endpoint")
66+
.addHeader("Authorization", "Bearer $apiKey")
67+
.apply { if (organization != null) addHeader("OpenAI-Organization", organization) }
68+
.post(multipartBody).build()
69+
}
70+
71+
protected open fun executeRequest(httpRequest: Request): String {
3972
val httpResponse = client.newCall(httpRequest).execute()
4073
if (!httpResponse.isSuccessful) {
4174
val json = httpResponse.body?.byteStream()?.bufferedReader()?.readText()
@@ -47,7 +80,12 @@ open class OpenAIImpl @ApiStatus.Internal constructor(
4780
?: throw IOException("Response body is null")
4881
val responseStr = jsonReader.readText()
4982
OpenAI.logger.debug(responseStr)
50-
return objectMapper.readValue(responseStr, responseType)
83+
return responseStr
84+
}
85+
86+
protected open fun <T> executeRequest(httpRequest: Request, responseType: Class<T>): T {
87+
val str = executeRequest(httpRequest)
88+
return objectMapper.readValue(str, responseType)
5189
}
5290

5391
private fun <T> streamResponses(
@@ -145,9 +183,39 @@ open class OpenAIImpl @ApiStatus.Internal constructor(
145183
return executeRequest(httpRequest, EmbeddingsResponse::class.java)
146184
}
147185

186+
override fun listFiles(request: ListFilesRequest): ListFilesResponse {
187+
val httpRequest = buildRequestNoBody(FILES_ENDPOINT, request.toMap()).get().build()
188+
return executeRequest(httpRequest, ListFilesResponse::class.java)
189+
}
190+
191+
override fun uploadFile(request: FileUploadRequest): FileObject {
192+
val httpRequest = buildMultipartRequest(FILES_ENDPOINT) {
193+
addFormDataPart("purpose", OpenAI.createObjectMapper().writeValueAsString(request.purpose).trim('"'))
194+
addFormDataPart("file", request.fileName, request.requestBody)
195+
}
196+
return executeRequest(httpRequest, FileObject::class.java)
197+
}
198+
199+
override fun deleteFile(fileId: String): FileDeletionStatus {
200+
val httpRequest = buildRequestNoBody("$FILES_ENDPOINT/$fileId").delete().build()
201+
return executeRequest(httpRequest, FileDeletionStatus::class.java)
202+
}
203+
204+
override fun retrieveFile(fileId: String): FileObject {
205+
val httpRequest = buildRequestNoBody("$FILES_ENDPOINT/$fileId").get().build()
206+
return executeRequest(httpRequest, FileObject::class.java)
207+
}
208+
209+
override fun retrieveFileContents(fileId: String): String {
210+
val httpRequest = buildRequestNoBody("$FILES_ENDPOINT/$fileId/content").get().build()
211+
return executeRequest(httpRequest)
212+
}
213+
214+
148215
companion object {
149216
const val COMPLETIONS_ENDPOINT = "v1/completions"
150217
const val CHAT_ENDPOINT = "v1/chat/completions"
151218
const val EMBEDDINGS_ENDPOINT = "v1/embeddings"
219+
const val FILES_ENDPOINT = "v1/files"
152220
}
153221
}

src/main/kotlin/com/cjcrafter/openai/completions/CompletionRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ data class CompletionRequest @JvmOverloads constructor(
6868
* ```
6969
*/
7070
@OpenAIDslMarker
71-
class Builder {
71+
class Builder internal constructor() {
7272
private var model: String? = null
7373
private var prompt: Any? = null
7474
private var suffix: String? = null

src/main/kotlin/com/cjcrafter/openai/embeddings/Embedding.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package com.cjcrafter.openai.embeddings
33
/**
44
* Represents 1 embedding as a vector of floats or strings.
55
*
6-
* @property embedding
7-
* @property index
8-
* @constructor Create empty Embedding
6+
* @property embedding The embedding as a list of floats or strings
7+
* @property index The index of the embedding in the list of embeddings
98
*/
109
data class Embedding(
1110
val embedding: List<Any>,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.cjcrafter.openai.files
2+
3+
/**
4+
* The returned result after attempting to delete a file.
5+
*
6+
* @property id The unique id of the file
7+
* @property deleted Whether the file was deleted
8+
*/
9+
data class FileDeletionStatus(
10+
val id: String,
11+
val deleted: Boolean,
12+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.cjcrafter.openai.files
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
import java.time.Instant
5+
import java.time.ZoneId
6+
import java.time.ZonedDateTime
7+
import java.util.*
8+
9+
/**
10+
* Represents a [file uploaded to the OpenAI API](https://platform.openai.com/docs/api-reference/files/object).
11+
*
12+
* @property id The unique id of the file
13+
* @property bytes How large the file is in bytes
14+
* @property createdAt The unix timestamp this file wrapper was created
15+
* @property fileName The name of the file (with the extension at the end)
16+
* @property purpose The reason this file was uploaded
17+
* @constructor Create empty File object
18+
*/
19+
data class FileObject(
20+
val id: String,
21+
val bytes: Int,
22+
@JsonProperty("created_at") val createdAt: Int,
23+
@JsonProperty("filename") val fileName: String,
24+
val purpose: FilePurpose,
25+
) {
26+
27+
/**
28+
* Returns the [Instant] time that this file object was created.
29+
* The time is measured as a unix timestamp (measured in seconds since
30+
* 00:00:00 UTC on January 1, 1970).
31+
*
32+
* Note that users expect time to be measured in their timezone, so
33+
* [getZonedTime] is preferred.
34+
*
35+
* @return The instant the api created this response.
36+
* @see getZonedTime
37+
*/
38+
fun getTime(): Instant {
39+
return Instant.ofEpochSecond(createdAt.toLong())
40+
}
41+
42+
/**
43+
* Returns the time-zoned instant that this file object was created.
44+
* By default, this method uses the system's timezone.
45+
*
46+
* @param timezone The user's timezone.
47+
* @return The timezone adjusted date time.
48+
* @see TimeZone.getDefault
49+
*/
50+
@JvmOverloads
51+
fun getZonedTime(timezone: ZoneId = TimeZone.getDefault().toZoneId()): ZonedDateTime {
52+
return ZonedDateTime.ofInstant(getTime(), timezone)
53+
}
54+
}

0 commit comments

Comments
 (0)