Skip to main content

novel_openai/
file.rs

1use bytes::Bytes;
2
3use crate::config::Config;
4use crate::error::OpenAIError;
5use crate::spec::files::{CreateFileRequest, DeleteFileResponse, ListFilesResponse, OpenAIFile};
6use crate::{Client, RequestOptions};
7
8/// Files are used to upload documents that can be used with features like Assistants and
9/// Fine-tuning.
10pub struct Files<'c, C: Config> {
11    client: &'c Client<C>,
12    pub(crate) request_options: RequestOptions,
13}
14
15impl<'c, C: Config> Files<'c, C> {
16    pub fn new(client: &'c Client<C>) -> Self {
17        Self {
18            client,
19            request_options: RequestOptions::new(),
20        }
21    }
22
23    /// Upload a file that can be used across various endpoints. Individual files can be up to 512
24    /// MB, and the size of all files uploaded by one organization can be up to 1 TB.
25    ///
26    /// The Assistants API supports files up to 2 million tokens and of specific file types. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) for details.
27    ///
28    /// The Fine-tuning API only supports `.jsonl` files. The input also has certain required formats for fine-tuning [chat](https://platform.openai.com/docs/api-reference/fine-tuning/chat-input) or [completions](https://platform.openai.com/docs/api-reference/fine-tuning/completions-input) models.
29    ///
30    /// The Batch API only supports `.jsonl` files up to 200 MB in size. The input also has a specific required [format](https://platform.openai.com/docs/api-reference/batch/request-input).
31    ///
32    /// Please [contact us](https://help.openai.com/) if you need to increase these storage limits.
33    #[crate::byot(
34        T0 = Clone,
35        R = serde::de::DeserializeOwned,
36        where_clause =  "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
37    )]
38    pub async fn create(&self, request: CreateFileRequest) -> Result<OpenAIFile, OpenAIError> {
39        self.client
40            .post_form("/files", request, &self.request_options)
41            .await
42    }
43
44    /// Returns a list of files that belong to the user's organization.
45    #[crate::byot(R = serde::de::DeserializeOwned)]
46    pub async fn list(&self) -> Result<ListFilesResponse, OpenAIError> {
47        self.client.get("/files", &self.request_options).await
48    }
49
50    /// Returns information about a specific file.
51    #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
52    pub async fn retrieve(&self, file_id: &str) -> Result<OpenAIFile, OpenAIError> {
53        self.client
54            .get(format!("/files/{file_id}").as_str(), &self.request_options)
55            .await
56    }
57
58    /// Delete a file.
59    #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
60    pub async fn delete(&self, file_id: &str) -> Result<DeleteFileResponse, OpenAIError> {
61        self.client
62            .delete(format!("/files/{file_id}").as_str(), &self.request_options)
63            .await
64    }
65
66    /// Returns the contents of the specified file
67    pub async fn content(&self, file_id: &str) -> Result<Bytes, OpenAIError> {
68        let (bytes, _headers) = self
69            .client
70            .get_raw(
71                format!("/files/{file_id}/content").as_str(),
72                &self.request_options,
73            )
74            .await?;
75        Ok(bytes)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use crate::Client;
82    use crate::spec::files::{
83        CreateFileRequestArgs, FileExpirationAfter, FileExpirationAfterAnchor, FilePurpose,
84    };
85    use crate::traits::RequestOptionsBuilder;
86
87    #[tokio::test]
88    async fn test_file_mod() {
89        let test_file_path = "/tmp/test.jsonl";
90        let contents = concat!(
91            "{\"prompt\": \"<prompt text>\", \"completion\": \"<ideal generated text>\"}\n", /* \n is to make it valid jsonl */
92            "{\"prompt\": \"<prompt text>\", \"completion\": \"<ideal generated text>\"}"
93        );
94
95        tokio::fs::write(test_file_path, contents).await.unwrap();
96
97        let client = Client::new();
98
99        let request = CreateFileRequestArgs::default()
100            .file(test_file_path)
101            .purpose(FilePurpose::FineTune)
102            .expires_after(FileExpirationAfter {
103                anchor: FileExpirationAfterAnchor::CreatedAt,
104                seconds: 3600,
105            })
106            .build()
107            .unwrap();
108
109        let openai_file = client.files().create(request).await.unwrap();
110
111        assert_eq!(openai_file.bytes, 135);
112        assert_eq!(openai_file.filename, "test.jsonl");
113        // assert_eq!(openai_file.purpose, "fine-tune");
114
115        // assert_eq!(openai_file.status, Some("processed".to_owned())); // uploaded or processed
116        let query = [("purpose", "fine-tune")];
117
118        let list_files = client.files().query(&query).unwrap().list().await.unwrap();
119
120        assert_eq!(list_files.data.into_iter().last().unwrap(), openai_file);
121
122        let retrieved_file = client.files().retrieve(&openai_file.id).await.unwrap();
123
124        assert_eq!(openai_file.created_at, retrieved_file.created_at);
125        assert_eq!(openai_file.bytes, retrieved_file.bytes);
126        assert_eq!(openai_file.filename, retrieved_file.filename);
127        assert_eq!(openai_file.purpose, retrieved_file.purpose);
128        assert_eq!(openai_file.expires_at, retrieved_file.expires_at);
129
130        // "To help mitigate abuse, downloading of fine-tune training files is disabled for free
131        // accounts." let retrieved_contents =
132        // client.files().retrieve_content(&openai_file.id) .await
133        // .unwrap();
134        //
135        // assert_eq!(contents, retrieved_contents);
136
137        // Sleep to prevent "File is still processing. Check back later."
138        tokio::time::sleep(std::time::Duration::from_secs(15)).await;
139        let delete_response = client.files().delete(&openai_file.id).await.unwrap();
140
141        assert_eq!(openai_file.id, delete_response.id);
142        assert!(delete_response.deleted);
143    }
144
145    // Ensures that list files succeeds if there are no files in account
146    // Prerequisite: No files in account
147    #[tokio::test]
148    async fn test_empty_file_list() {
149        let client = Client::new();
150        let result = client.files().list().await.unwrap();
151        assert!(result.data.is_empty());
152    }
153}