From eaed27899bea8e34f9b6d18381d746d5f03c1f5d Mon Sep 17 00:00:00 2001 From: Zeeshan Mustafa Date: Sun, 11 Jun 2023 07:11:33 +0500 Subject: [PATCH 1/2] Added 'Edit' endpoint implementation and its test cases. --- OpenAI_API/Edit/EditEndpoint.cs | 116 ++++++++++++++++++++++++++++++ OpenAI_API/Edit/EditRequest.cs | 102 ++++++++++++++++++++++++++ OpenAI_API/Edit/EditResult.cs | 77 ++++++++++++++++++++ OpenAI_API/Edit/IEditEndpoint.cs | 71 ++++++++++++++++++ OpenAI_API/Model/Model.cs | 17 +++-- OpenAI_API/OpenAIAPI.cs | 10 ++- OpenAI_Tests/EditEndpointTests.cs | 109 ++++++++++++++++++++++++++++ 7 files changed, 497 insertions(+), 5 deletions(-) create mode 100644 OpenAI_API/Edit/EditEndpoint.cs create mode 100644 OpenAI_API/Edit/EditRequest.cs create mode 100644 OpenAI_API/Edit/EditResult.cs create mode 100644 OpenAI_API/Edit/IEditEndpoint.cs create mode 100644 OpenAI_Tests/EditEndpointTests.cs diff --git a/OpenAI_API/Edit/EditEndpoint.cs b/OpenAI_API/Edit/EditEndpoint.cs new file mode 100644 index 0000000..1838d06 --- /dev/null +++ b/OpenAI_API/Edit/EditEndpoint.cs @@ -0,0 +1,116 @@ +using OpenAI_API.Models; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace OpenAI_API.Edits +{ + /// + /// This API lets you edit the prompt. Given a prompt and instruction, this will return an edited version of the prompt. This API lets you edit the prompt. Given a prompt and instruction, this will return an edited version of the prompt. + /// + public class EditEndpoint : EndpointBase, IEditEndpoint + { + /// + /// This allows you to set default parameters for every request, for example to set a default temperature or max tokens. For every request, if you do not have a parameter set on the request but do have it set here as a default, the request will automatically pick up the default value. + /// + public EditRequest DefaultEditRequestArgs { get; set; } = new EditRequest() { Model = Model.TextDavinciEdit }; + + /// + /// The name of the endpoint, which is the final path segment in the API URL. For example, "edits". + /// + protected override string Endpoint { get { return "edits"; } } + + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal EditEndpoint(OpenAIAPI api) : base(api) { } + + /// + /// Ask the API to edit the prompt using the specified request. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Asynchronously returns the edits result. Look in its property for the edits. + public async Task CreateEditsAsync(EditRequest request) + { + if(request.Model != Model.TextDavinciEdit.ModelID && request.Model != Model.CodeDavinciEdit.ModelID) + throw new ArgumentException($"Model must be either '{Model.TextDavinciEdit.ModelID}' or '{Model.CodeDavinciEdit.ModelID}'. For more details, refer https://platform.openai.com/docs/api-reference/edits"); + return await HttpPost(postData: request); + } + + /// + /// Ask the API to edit the prompt using the specified request and a requested number of outputs. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Overrides as a convenience. + /// Asynchronously returns the edits result. Look in its property for the edits, which should have a length equal to . + public Task CreateEditsAsync(EditRequest request, int numOutputs = 5) + { + request.NumChoicesPerPrompt = numOutputs; + return CreateEditsAsync(request); + } + + /// + /// Ask the API to edit the prompt. This is non-streaming, so it will wait until the API returns the full result. Any non-specified parameters will fall back to default values specified in if present. + /// + /// The input text to use as a starting point for the edit. Defaults to an empty string. + /// The instruction that tells the model how to edit the prompt. (Required) + /// ID of the model to use. You can use or for edit endpoint. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. It is generally recommend to use this or but not both. + /// How many edits to generate for the input and instruction. + /// + public Task CreateEditsAsync(string prompt, + string instruction, + Model model = null, + double? temperature = null, + double? top_p = null, + int? numOutputs = null + ) + { + EditRequest request = new EditRequest(DefaultEditRequestArgs) + { + Input = prompt, + Model = model ?? DefaultEditRequestArgs.Model, + Instruction = string.IsNullOrEmpty(instruction) ? DefaultEditRequestArgs.Instruction : instruction, + Temperature = temperature ?? DefaultEditRequestArgs.Temperature, + TopP = top_p ?? DefaultEditRequestArgs.TopP, + NumChoicesPerPrompt = numOutputs ?? DefaultEditRequestArgs.NumChoicesPerPrompt, + }; + return CreateEditsAsync(request); + } + + + /// + /// Simply returns edited prompt string + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// The best edited result + public async Task CreateAndFormatEdits(EditRequest request) + { + string prompt = request.Input; + var result = await CreateEditsAsync(request); + return result.ToString(); + } + + /// + /// Simply returns the best edit + /// + /// The input prompt to be edited + /// The instruction that tells model how to edit the prompt + /// The best edited result + public async Task GetEdits(string prompt, string instruction) + { + EditRequest request = new EditRequest(DefaultEditRequestArgs) + { + Input = prompt, + Instruction = instruction, + NumChoicesPerPrompt = 1 + }; + var result = await CreateEditsAsync(request); + return result.ToString(); + } + + } +} diff --git a/OpenAI_API/Edit/EditRequest.cs b/OpenAI_API/Edit/EditRequest.cs new file mode 100644 index 0000000..8060d07 --- /dev/null +++ b/OpenAI_API/Edit/EditRequest.cs @@ -0,0 +1,102 @@ +using Newtonsoft.Json; +using OpenAI_API.Completions; +using OpenAI_API.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OpenAI_API.Edits +{ + /// + /// Represents a request to the Edit API. Mostly matches the parameters in the OpenAI docs, although some might have been renamed for ease of use. + /// + public class EditRequest + { + /// + /// ID of the model to use. You can use or for edit endpoint. + /// + [JsonProperty("model")] + public string Model { get; set; } = OpenAI_API.Models.Model.TextDavinciEdit; + + /// + /// The input text to use as a starting point for the edit. Defaults to an empty string. + /// + [JsonProperty("input")] + public string Input { get; set; } + + /// + /// The instruction that tells the model how to edit the prompt. (Required) + /// + [JsonProperty("instruction")] + public string Instruction { get; set; } + + /// + /// How many edits to generate for the input and instruction. + /// + [JsonProperty("n")] + public int? NumChoicesPerPrompt { get; set; } + + /// + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// + [JsonProperty("temperature")] + public double? Temperature { get; set; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. It is generally recommend to use this or but not both. + /// + [JsonProperty("top_p")] + public double? TopP { get; set; } + + + /// + /// Cretes a new, empty + /// + public EditRequest() + { + this.Model = OpenAI_API.Models.Model.TextDavinciEdit; + } + + /// + /// Creates a new , inheriting any parameters set in . + /// + /// The to copy + public EditRequest(EditRequest basedOn) + { + this.Model = basedOn.Model; + this.Input = basedOn.Input; + this.Instruction = basedOn.Instruction; + this.Temperature = basedOn.Temperature; + this.TopP = basedOn.TopP; + this.NumChoicesPerPrompt = basedOn.NumChoicesPerPrompt; + } + + + /// + /// Creates a new with the specified parameters + /// + /// The input text to use as a starting point for the edit. Defaults to an empty string. + /// The instruction that tells the model how to edit the prompt. (Required) + /// ID of the model to use. You can use or for edit endpoint. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. It is generally recommend to use this or but not both. + /// How many edits to generate for the input and instruction. + public EditRequest( + string input, + string instruction, + Model model = null, + double? temperature = null, + double? top_p = null, + int? numOutputs = null) + { + this.Model = model; + this.Input = input; + this.Instruction = instruction; + this.Temperature = temperature; + this.TopP = top_p; + this.NumChoicesPerPrompt = numOutputs; + + } + } +} diff --git a/OpenAI_API/Edit/EditResult.cs b/OpenAI_API/Edit/EditResult.cs new file mode 100644 index 0000000..3faddcd --- /dev/null +++ b/OpenAI_API/Edit/EditResult.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json; +using OpenAI_API.Chat; +using System; +using System.Collections.Generic; +using System.Text; + +namespace OpenAI_API.Edits +{ + /// + /// Represents a result from calling the Edit API + /// + public class EditResult : ApiResultBase + { + /// + /// The list of choices that the user was presented with during the edit interation + /// + [JsonProperty("choices")] + public IReadOnlyList Choices { get; set; } + + /// + /// The usage statistics for the edit call + /// + [JsonProperty("usage")] + public EditUsage Usage { get; set; } + + /// + /// A convenience method to return the content of the message in the first choice of this response + /// + /// The edited text returned by the API as reponse. + public override string ToString() + { + if (Choices != null && Choices.Count > 0) + return Choices[0].ToString(); + else + return null; + } + } + + /// + /// A message received from the API, including the text and index. + /// + public class EditChoice + { + /// + /// The index of the choice in the list of choices + /// + [JsonProperty("index")] + public int Index { get; set; } + + /// + /// The edited text that was presented to the user as the choice. This is returned as response from API + /// + [JsonProperty("text")] + public string Text { get; set; } + + /// + /// A convenience method to return the content of the message in this response + /// + /// The edited text returned by the API as reponse. + public override string ToString() + { + return Text; + } + } + + /// + /// How many tokens were used in this edit message. + /// + public class EditUsage : Usage + { + /// + /// The number of completion tokens used during the edit + /// + [JsonProperty("completion_tokens")] + public int CompletionTokens { get; set; } + } +} diff --git a/OpenAI_API/Edit/IEditEndpoint.cs b/OpenAI_API/Edit/IEditEndpoint.cs new file mode 100644 index 0000000..3555724 --- /dev/null +++ b/OpenAI_API/Edit/IEditEndpoint.cs @@ -0,0 +1,71 @@ +using OpenAI_API.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Edits +{ + /// + /// An interface for , for ease of mock testing, etc + /// + public interface IEditEndpoint + { + /// + /// This allows you to set default parameters for every request, for example to set a default temperature or max tokens. For every request, if you do not have a parameter set on the request but do have it set here as a default, the request will automatically pick up the default value. + /// + EditRequest DefaultEditRequestArgs { get; set; } + + /// + /// Ask the API to edit the prompt using the specified request. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Asynchronously returns the edits result. Look in its property for the edits. + Task CreateEditsAsync(EditRequest request); + + /// + /// Ask the API to edit the prompt using the specified request and a requested number of outputs. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Overrides as a convenience. + /// Asynchronously returns the edits result. Look in its property for the edits, which should have a length equal to . + Task CreateEditsAsync(EditRequest request, int numOutputs = 5); + + + /// + /// Ask the API to edit the prompt. This is non-streaming, so it will wait until the API returns the full result. Any non-specified parameters will fall back to default values specified in if present. + /// + /// The input text to use as a starting point for the edit. Defaults to an empty string. + /// The instruction that tells the model how to edit the prompt. (Required) + /// ID of the model to use. You can use or for edit endpoint. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. It is generally recommend to use this or but not both. + /// How many edits to generate for the input and instruction. + /// + Task CreateEditsAsync(string prompt, + string instruction, + Model model = null, + double? temperature = null, + double? top_p = null, + int? numOutputs = null + ); + + + + /// + /// Simply returns edited prompt string + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// The best edited result + Task CreateAndFormatEdits(EditRequest request); + + /// + /// Simply returns the best edit + /// + /// The input prompt to be edited + /// The instruction that tells model how to edit the prompt + /// The best edited result + Task GetEdits(string prompt, string instruction); + + } +} diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index bdeeb2b..a20973f 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -164,13 +164,22 @@ public Model() /// public static Model TextModerationLatest => new Model("text-moderation-latest") { OwnedBy = "openai" }; + /// + /// A specialized model in GTP-3 series, that can be used to edit the text. Provide some text and an instruction, and the model will attempt to modify it accordingly. + /// + public static Model TextDavinciEdit => new Model("text-davinci-edit-001") { OwnedBy = "openai" }; /// - /// Gets more details about this Model from the API, specifically properties such as and permissions. + /// A specialized model in Codex series, that can be used to edit the code. Provide some code and an instruction, and the model will attempt to modify it accordingly. /// - /// An instance of the API with authentication in order to call the endpoint. - /// Asynchronously returns an Model with all relevant properties filled in - public async Task RetrieveModelDetailsAsync(OpenAI_API.OpenAIAPI api) + public static Model CodeDavinciEdit => new Model("code-davinci-edit-001") { OwnedBy = "openai" }; + + /// + /// Gets more details about this Model from the API, specifically properties such as and permissions. + /// + /// An instance of the API with authentication in order to call the endpoint. + /// Asynchronously returns an Model with all relevant properties filled in + public async Task RetrieveModelDetailsAsync(OpenAI_API.OpenAIAPI api) { return await api.Models.RetrieveModelDetailsAsync(this.ModelID); } diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index f1e2bda..65e9109 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,11 +1,13 @@ using OpenAI_API.Chat; using OpenAI_API.Completions; +using OpenAI_API.Edits; using OpenAI_API.Embedding; using OpenAI_API.Files; using OpenAI_API.Images; using OpenAI_API.Models; using OpenAI_API.Moderation; using System.Net.Http; +using static System.Net.WebRequestMethods; namespace OpenAI_API { @@ -50,6 +52,7 @@ public OpenAIAPI(APIAuthentication apiKeys = null) Chat = new ChatEndpoint(this); Moderation = new ModerationEndpoint(this); ImageGenerations = new ImageGenerationEndpoint(this); + Edit = new EditEndpoint(this); } /// @@ -101,5 +104,10 @@ public static OpenAIAPI ForAzure(string YourResourceName, string deploymentId, A /// The API lets you do operations with images. Given a prompt and/or an input image, the model will generate a new image. /// public IImageGenerationEndpoint ImageGenerations { get; } - } + + /// + /// This API lets you edit the prompt. Given a prompt and instruction, this will return an edited version of the prompt. + /// + public IEditEndpoint Edit { get; } + } } diff --git a/OpenAI_Tests/EditEndpointTests.cs b/OpenAI_Tests/EditEndpointTests.cs new file mode 100644 index 0000000..27b3ebe --- /dev/null +++ b/OpenAI_Tests/EditEndpointTests.cs @@ -0,0 +1,109 @@ +using FluentAssertions; +using NUnit.Framework; +using OpenAI_API.Edits; +using OpenAI_API.Models; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace OpenAI_Tests +{ + public class EditEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication("TEST_OPENAI_SECRET_KEY"); + } + + [Test] + public void EditRequest_CanOnlyUseValidModels() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Edit); + + Assert.That(async () => await api.Edit.CreateEditsAsync(new EditRequest("B A D C E G H F", "Correct the alphabets order", model: Model.ChatGPTTurbo, temperature: 0.0)), + Throws.Exception + .TypeOf()); + + } + + [Test] + public async Task GetBasicEdit() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Edit); + + var results = await api.Edit.CreateEditsAsync(new EditRequest("B A D C E G H F", "Correct the alphabets order", model: Model.TextDavinciEdit, temperature: 0.0)); + Assert.IsNotNull(results); + Assert.NotNull(results.CreatedUnixTime); + Assert.NotZero(results.CreatedUnixTime.Value); + Assert.NotNull(results.Created); + Assert.NotNull(results.Choices); + Assert.NotZero(results.Choices.Count); + Assert.That(results.Choices.Any(c => c.Text.Trim().ToLower().StartsWith("a"))); + } + + [Test] + public async Task GetSimpleEdit() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Edit); + + var results = await api.Edit.CreateEditsAsync("B A D C E G H F", "correct the lphabets sequence", temperature: 0); + Assert.IsNotNull(results); + Assert.NotNull(results.Choices); + Assert.NotZero(results.Choices.Count); + Assert.That(results.Choices.Any(c => c.Text.Trim().ToLower().StartsWith("a"))); + } + + + [Test] + public async Task EditsUsageDataWorks() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Edit); + + var results = await api.Edit.CreateEditsAsync(new EditRequest("B A D C E G H F", "Correct the alphabets sequence", model: Model.TextDavinciEdit, temperature: 0)); + Assert.IsNotNull(results); + Assert.IsNotNull(results.Usage); + Assert.Greater(results.Usage.PromptTokens, 1); + Assert.Greater(results.Usage.CompletionTokens, 0); + Assert.GreaterOrEqual(results.Usage.TotalTokens, results.Usage.PromptTokens + results.Usage.CompletionTokens); + } + + [Test] + public async Task GetEdits_ReturnsResultString() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Edit); + + var result = await api.Edit.GetEdits("B A D C E G H F", "Correct the alphabets sequence"); + Assert.IsNotNull(result); + Assert.That(result.ToLower().StartsWith("a")); + } + + [Test] + public async Task CreateEditsAsync_ShouldGenerateMultipleChoices() + { + var api = new OpenAI_API.OpenAIAPI(); + + var req = new EditRequest + { + Input = "B A D C E G H F", + Instruction = "Correct the alphabets sequence", + Temperature = 0, + NumChoicesPerPrompt = 2 + }; + + var result = await api.Edit.CreateEditsAsync(req); + Assert.IsNotNull(result); + result.Choices.Count.Should().Be(2, "NumChoisesPerPropmt is set to 2"); + } + } +} From 252c5f30769800b7075ab99cf8e4ffbfc2ce9d52 Mon Sep 17 00:00:00 2001 From: Zeeshan Mustafa Date: Sun, 11 Jun 2023 07:28:32 +0500 Subject: [PATCH 2/2] Updated README.md for Edits endpoint details --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index dbc06ee..73ae36f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Console.WriteLine(result); * [Embeddings API](#embeddings) * [Moderation API](#moderation) * [Files API](#files-for-fine-tuning) + * [Edits API](#edits) * [Image APIs (DALL-E)](#images) * [Azure](#azure) * [Additonal Documentation](#documentation) @@ -270,6 +271,31 @@ There are also methods to get file contents, delete a file, etc. The fine-tuning endpoint itself has not yet been implemented, but will be added soon. +### Edits +The Edits API endpoint is accessed via `OpenAIAPI.Edit`: + +```csharp +async Task CreateEditsAsync(EditRequest request); + +// for example +var result = await api.Edit.CreateEditsAsync(new EditRequest("B A D C E G H F", "Correct the alphabets order", model: Model.TextDavinciEdit, temperature: 0.0)); +//or +var results = await api.Edit.CreateEditsAsync("B A D C E G H F", "correct the lphabets sequence", temperature: 0); +//or +var req = new EditRequest + { + Input = "B A D C E G H F", + Instruction = "Correct the alphabets sequence", + Temperature = 0, + NumChoicesPerPrompt = 2 + }; + +``` + +You can create your `EditRequest` ahead of time or use one of the helper overloads for convenience. It returns a `EditResult` which is mostly metadata, so use its `.ToString()` method to get the text if all you want is the edited result. + +This endpoint currenlty supports `text-davinci-edit-001` and `code-davinci-edit-001` models. + ### Images The DALL-E Image Generation API is accessed via `OpenAIAPI.ImageGenerations`: