diff --git a/OpenAI_API.sln b/OpenAI_API.sln index 1f1864f..9412cfe 100644 --- a/OpenAI_API.sln +++ b/OpenAI_API.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30309.148 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI_API", "OpenAI_API\OpenAI_API.csproj", "{99C80D3E-3F0F-4ACC-900D-7AAE6230A780}" EndProject diff --git a/OpenAI_API/BaseEndpoint.cs b/OpenAI_API/BaseEndpoint.cs new file mode 100644 index 0000000..d4a21a8 --- /dev/null +++ b/OpenAI_API/BaseEndpoint.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API +{ + public abstract class BaseEndpoint + { + protected readonly OpenAIAPI api; + + public BaseEndpoint(OpenAIAPI api) + { + this.api = api; + } + + protected abstract string GetEndpoint(); + + protected string GetUrl() + { + return $"{OpenAIAPI.API_URL}{GetEndpoint()}"; + } + + protected HttpClient GetClient() + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", api.Auth?.ThisOrDefault().ApiKey); + client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); + return client; + } + + protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") + { + return $"Error at {name} ( {description} ) with HTTP status code: {response.StatusCode} . Content: {resultAsString}"; + } + + + } +} diff --git a/OpenAI_API/Files/File.cs b/OpenAI_API/Files/File.cs new file mode 100644 index 0000000..e6054e7 --- /dev/null +++ b/OpenAI_API/Files/File.cs @@ -0,0 +1,69 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Files +{ + public class File + { + /// + /// unique id for this file, so that it can be referenced in other operations + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// the name of the file + /// + [JsonProperty("filename")] + public string Name { get; set; } + + + /// + /// What is the purpose of this file, fine-tune, search, etc + /// + [JsonProperty("Purpose")] + public string Purpose { get; set; } + + /// + /// object type, ie: file, image, etc + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// The size of the file in bytes + /// + [JsonProperty("bytes")] + public long Bytes { get; set; } + + /// + /// Timestamp for the creation time of this file + /// + [JsonProperty("created_at")] + public long CreatedAt { get; set; } + + /// + /// The object was deleted, this attribute is used in the Delete file operation + /// + [JsonProperty("deleted")] + public bool Deleted { get; set; } + + + /// + /// The status of the File (ie when an upload operation was done: "uploaded") + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// The status details, it could be null + /// + [JsonProperty("status_details")] + public string StatusDetails { get; set; } + + } +} diff --git a/OpenAI_API/Files/FilesEndpoint.cs b/OpenAI_API/Files/FilesEndpoint.cs new file mode 100644 index 0000000..5a93a13 --- /dev/null +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -0,0 +1,128 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Files +{ + /// + /// The API endpoint for operations List, Upload, Delete, Retrieve files + /// + public class FilesEndpoint : BaseEndpoint + { + public FilesEndpoint(OpenAIAPI api) : base(api) {} + + protected override string GetEndpoint() { return "files"; } + + /// + /// Get the list of all files + /// + /// + /// + public async Task> GetFilesAsync() + { + var response = await GetClient().GetAsync(GetUrl()); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var files = JsonConvert.DeserializeObject(resultAsString).Data; + return files; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "List files", "Get the list of all files")); + } + + /// + /// Returns information about a specific file + /// + /// The ID of the file to use for this request + /// + public async Task GetFileAsync(string fileId) + { + var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var file = JsonConvert.DeserializeObject(resultAsString); + return file; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file")); + } + + + /// + /// Returns the contents of the specific file as string + /// + /// The ID of the file to use for this request + /// + public async Task GetFileContentAsStringAsync(string fileId) + { + var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}/content"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + return resultAsString; + + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file content")); + } + + /// + /// Delete a file + /// + /// The ID of the file to use for this request + /// + public async Task DeleteFileAsync(string fileId) + { + var response = await GetClient().DeleteAsync($"{GetUrl()}/{fileId}"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var file = JsonConvert.DeserializeObject(resultAsString); + return file; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Delete file")); + } + + /// + /// Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact us if you need to increase the storage limit + /// + /// The name of the file to use for this request + /// The intendend purpose of the uploaded documents. Use "fine-tune" for Fine-tuning. This allows us to validate the format of the uploaded file. + public async Task UploadFileAsync(string file, string purpose = "fine-tune") + { + HttpClient client = GetClient(); + var content = new MultipartFormDataContent + { + { new StringContent(purpose), "purpose" }, + { new ByteArrayContent(System.IO.File.ReadAllBytes(file)), "file", Path.GetFileName(file) } + }; + var response = await client.PostAsync($"{GetUrl()}", content); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(resultAsString); + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Upload file", "Upload a file that contains document(s) to be used across various endpoints/features.")); + } + } + /// + /// A helper class to deserialize the JSON API responses. This should not be used directly. + /// + class FilesData + { + [JsonProperty("data")] + public List Data { get; set; } + [JsonProperty("object")] + public string Obj { get; set; } + } +} diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index 2baedea..27c419a 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OpenAI_API.Files; using System; using System.Collections.Generic; using System.IO; @@ -14,6 +15,11 @@ namespace OpenAI_API /// public class OpenAIAPI { + /// + /// Base url for OpenAI + /// + public const string API_URL = "https://api.openai.com/v1/"; + /// /// The API authentication information to use for API calls /// @@ -36,6 +42,7 @@ public OpenAIAPI(APIAuthentication apiKeys = null, Engine engine = null) Completions = new CompletionEndpoint(this); Engines = new EnginesEndpoint(this); Search = new SearchEndpoint(this); + Files = new FilesEndpoint(this); } /// @@ -53,7 +60,10 @@ public OpenAIAPI(APIAuthentication apiKeys = null, Engine engine = null) /// public SearchEndpoint Search { get; } - + /// + /// The API lets you do operations with files. You can upload, delete or retrieve files. Files can be used for fine-tuning, search, etc. + /// + public FilesEndpoint Files { get; } diff --git a/OpenAI_Tests/FilesEndpointTests.cs b/OpenAI_Tests/FilesEndpointTests.cs new file mode 100644 index 0000000..c824a7b --- /dev/null +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -0,0 +1,91 @@ +using NUnit.Framework; +using OpenAI_API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI_Tests +{ + public class FilesEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + } + + [Test] + [Order(1)] + public async Task UploadFile() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.UploadFileAsync("fine-tuning-data.jsonl"); + Assert.IsNotNull(response); + Assert.IsTrue(response.Id.Length > 0); + Assert.IsTrue(response.Object == "file"); + Assert.IsTrue(response.Bytes > 0); + Assert.IsTrue(response.CreatedAt > 0); + Assert.IsTrue(response.Status == "uploaded"); + // The file must be processed before it can be used in other operations, so for testing purposes we just sleep awhile. + Thread.Sleep(10000); + } + + [Test] + [Order(2)] + public async Task ListFiles() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + } + } + + + [Test] + [Order(3)] + public async Task GetFile() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + string id = file.Id; + if (file.Name == "fine-tuning-data.jsonl") + { + var fileResponse = await api.Files.GetFileAsync(file.Id); + Assert.IsNotNull(fileResponse); + Assert.IsTrue(fileResponse.Id == id); + } + } + } + + [Test] + [Order(4)] + public async Task DeleteFiles() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + if (file.Name == "fine-tuning-data.jsonl") + { + var deleteResponse = await api.Files.DeleteFileAsync(file.Id); + Assert.IsNotNull(deleteResponse); + Assert.IsTrue(deleteResponse.Deleted); + } + } + } + + } +} diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index 56b6dea..9da037d 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -16,4 +16,10 @@ + + + PreserveNewest + + + diff --git a/OpenAI_Tests/fine-tuning-data.jsonl b/OpenAI_Tests/fine-tuning-data.jsonl new file mode 100644 index 0000000..d903bdb --- /dev/null +++ b/OpenAI_Tests/fine-tuning-data.jsonl @@ -0,0 +1,75 @@ +{ "prompt": "type for FilterRelationType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for FilterOperation", "completion":"Numeric(4.0).###"} +{ "prompt": "type for TargetType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for RuntimeEnvironment", "completion":"Numeric(4.0).###"} +{ "prompt": "type for MapType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for LogLevel", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StorePurchaseState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for StorePurchasePlatform", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StoreProductType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StorePurchaseStatus", "completion":"Numeric(4.0).###"} +{ "prompt": "type for MediaMetadataKey", "completion":"VarChar(50).###"} +{ "prompt": "type for MediaStreamType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for DeviceAuthenticationPolicy", "completion":"Numeric(1.0).###"} +{ "prompt": "type for Url", "completion":"VarChar(1000).###"} +{ "prompt": "type for IMEMode", "completion":"Character(40).###"} +{ "prompt": "type for Time", "completion":"DateTime.###"} +{ "prompt": "type for Encoding", "completion":"Character(256).###"} +{ "prompt": "type for Timezones", "completion":"Character(60).###"} +{ "prompt": "type for Effect", "completion":"Character(20).###"} +{ "prompt": "type for CallType", "completion":"Character(20).###"} +{ "prompt": "type for CryptoEncryptAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for CryptoHashAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for CryptoSignAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for TrnMode", "completion":"Character(3).###"} +{ "prompt": "type for Address", "completion":"VarChar(1K).###"} +{ "prompt": "type for Component", "completion":"VarChar(1000).###"} +{ "prompt": "type for Email", "completion":"VarChar(100).###"} +{ "prompt": "type for Geolocation", "completion":"Character(50).###"} +{ "prompt": "type for Html", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for Phone", "completion":"Character(20).###"} +{ "prompt": "type for APIAuthorizationStatus", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MessageTypes", "completion":"Numeric(2.0).###"} +{ "prompt": "type for ProgressIndicatorType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for RecentLinksOptions", "completion":"Numeric(4.0).###"} +{ "prompt": "type for ObjectName", "completion":"VarChar(256).###"} +{ "prompt": "type for CallTargetSize", "completion":"Character(10).###"} +{ "prompt": "type for EventExecution", "completion":"Numeric(1.0).###"} +{ "prompt": "type for PushNotificationPriority", "completion":"Character(20).###"} +{ "prompt": "type for SmartDeviceType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for CameraAPIQuality", "completion":"Numeric(1.0).###"} +{ "prompt": "type for AudioAPISessionType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MediaDuration", "completion":"Numeric(12.0).###"} +{ "prompt": "type for PlaybackState", "completion":"Numeric(4.0).###"} +{ "prompt": "type for NetworkAPIConnectionType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for EventAction", "completion":"Numeric(4.0).###"} +{ "prompt": "type for EventStatus", "completion":"Numeric(4.0).###"} +{ "prompt": "type for EventData", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for EventErrors", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for ApplicationState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for SynchronizationReceiveResult", "completion":"Numeric(4.0).###"} +{ "prompt": "type for RegionState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for BeaconProximity", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MediaFinishReason", "completion":"Numeric(4.0).###"} +{ "prompt": "type for HttpMethod", "completion":"Character(7).###"} +{ "prompt": "type for HttpAuthenticationType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for CommonCallTarget", "completion":"Character(20).###"} +{ "prompt": "type for BarcodeType", "completion":"VarChar(40).###"} +{ "prompt": "type for Name", "completion":"VarChar(100).###"} +{ "prompt": "type for ContactData", "completion":"VarChar(80).###"} +{ "prompt": "type for Lang", "completion":"Character(3).###"} +{ "prompt": "type for Bio", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for FullName", "completion":"VarChar(150).###"} +{ "prompt": "type for Status", "completion":"Character(1).###"} +{ "prompt": "type for Id", "completion":"Numeric(8.0).###"} +{ "prompt": "type for SessionType", "completion":"Character(1).###"} +{ "prompt": "type for Title", "completion":"VarChar(160).###"} +{ "prompt": "type for Abstract", "completion":"VarChar(1000).###"} +{ "prompt": "type for Position", "completion":"Numeric(4.0).###"} +{ "prompt": "type for Hashtag", "completion":"VarChar(40).###"} +{ "prompt": "type for Duration", "completion":"Numeric(3.0).###"} +{ "prompt": "type for ColorTrack", "completion":"Character(3).###"} +{ "prompt": "type for Description", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for SponsorType", "completion":"Character(1).###"} +{ "prompt": "type for Count", "completion":"Numeric(4.0).###"} +{ "prompt": "type for ListType", "completion":"Character(1).###"}