From 774f84d8dec0e9128862585c5f7fb9283d2d07ee Mon Sep 17 00:00:00 2001 From: Zoltan Juhasz Date: Sat, 27 Apr 2024 12:39:06 +0200 Subject: [PATCH] v1.4.4 Service factory improvements --- Forge.OpenAI/Forge.OpenAI.csproj | 3 +- Forge.OpenAI/Infrastructure/MappingHelper.cs | 43 +++++++++ Forge.OpenAI/Services/OpenAIService.cs | 20 +++- .../MultipleApiKeyUsage.csproj | 6 +- Playgrounds/MultipleApiKeyUsage/Program.cs | 91 ++++++++++++++----- 5 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 Forge.OpenAI/Infrastructure/MappingHelper.cs diff --git a/Forge.OpenAI/Forge.OpenAI.csproj b/Forge.OpenAI/Forge.OpenAI.csproj index 7ceb8f0..56dfc25 100644 --- a/Forge.OpenAI/Forge.OpenAI.csproj +++ b/Forge.OpenAI/Forge.OpenAI.csproj @@ -37,12 +37,13 @@ README.md https://github.com/JZO001/Forge.OpenAI git - 1.4.3.0 + 1.4.4.0 $(AssemblyVersion) $(AssemblyVersion) LICENSE OpenAI, Azure-OpenAI, Azure-OpenAI-API, ChatGPT, GPT4, GPT-4, GPT-4-API, GPT35, GPT-35, GPT-35-API, GPT3, GPT-3, GPT-3-API, DALLE, DALL-E, DALL-E-API, OpenAi, openAi, azure, assistant, threads, Whisper, AI, ML, dotnet, dotnetcore, machine-learning, sdk, forge, translation, transcription, chat, chatbot, image, image-processing, embedding, embedding-models, moderation, text-completion, fine-tune, dotNet, csharp + v1.4.4 - Improved service factory methods and Playground examples v1.4.3 - Improved service factory methods v1.4.2 - Fix issues v1.4.1 - Fix issue in MessageResponseBase, duplicated status field and wrong "incomplete_details" field. Constants updated in Tool class. diff --git a/Forge.OpenAI/Infrastructure/MappingHelper.cs b/Forge.OpenAI/Infrastructure/MappingHelper.cs new file mode 100644 index 0000000..09d58f2 --- /dev/null +++ b/Forge.OpenAI/Infrastructure/MappingHelper.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; + +namespace Forge.OpenAI.Infrastructure +{ + + /// Make a copy about the source instance into the destination one + public static class MappingHelper + { + + public static void Map(TSource source, TDestination destination, bool throwErrorOnMissingTargetProperty = false) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + Type sourceType = source.GetType(); + Type destinationType = destination.GetType(); + + foreach (PropertyInfo sourceProperty in sourceType.GetProperties()) + { + PropertyInfo destinationProperty = destinationType.GetProperty(sourceProperty.Name); + + if (destinationProperty != null) + { + destinationProperty.SetValue(destination, sourceProperty.GetValue(source)); + } + else if (throwErrorOnMissingTargetProperty) + { + throw new InvalidOperationException($"Property {sourceProperty.Name} not found in destination object"); + } + } + } + + } + +} diff --git a/Forge.OpenAI/Services/OpenAIService.cs b/Forge.OpenAI/Services/OpenAIService.cs index 50771b9..72000f6 100644 --- a/Forge.OpenAI/Services/OpenAIService.cs +++ b/Forge.OpenAI/Services/OpenAIService.cs @@ -141,6 +141,24 @@ public OpenAIService(IModelService modelService, RunStepService = runStepService; } + /// + /// Creates a new service instance with individual options. + /// The method gets back the ServiceProvider instance for further use. + /// It is the caller responsibility to dispose it, at the end of the OpenAIService instance lifecycle. + /// + /// The options. + /// The constructed IServiceProvider instance. + /// + /// IOpenAIService + /// + public static IOpenAIService CreateService(OpenAIOptions options, out ServiceProvider serviceProvider) + { + return CreateService((OpenAIOptions o) => + { + MappingHelper.Map(options, o); + }, out serviceProvider); + } + /// /// Creates a new service instance with individual options. /// The method gets back the ServiceProvider instance for further use. @@ -149,7 +167,7 @@ public OpenAIService(IModelService modelService, /// The configuration handler. /// The constructed IServiceProvider instance. /// - ///
+ /// IOpenAIService ///
/// configure public static IOpenAIService CreateService(Action configure, out ServiceProvider serviceProvider) diff --git a/Playgrounds/MultipleApiKeyUsage/MultipleApiKeyUsage.csproj b/Playgrounds/MultipleApiKeyUsage/MultipleApiKeyUsage.csproj index 4a2bd8e..67b9ee9 100644 --- a/Playgrounds/MultipleApiKeyUsage/MultipleApiKeyUsage.csproj +++ b/Playgrounds/MultipleApiKeyUsage/MultipleApiKeyUsage.csproj @@ -15,7 +15,11 @@ - + + + + + diff --git a/Playgrounds/MultipleApiKeyUsage/Program.cs b/Playgrounds/MultipleApiKeyUsage/Program.cs index ee9bb82..27b19ab 100644 --- a/Playgrounds/MultipleApiKeyUsage/Program.cs +++ b/Playgrounds/MultipleApiKeyUsage/Program.cs @@ -2,11 +2,11 @@ using Forge.OpenAI.Authentication; using Forge.OpenAI.Interfaces.Services; using Forge.OpenAI.Models.Common; -using Forge.OpenAI.Models.TextEdits; +using Forge.OpenAI.Models.ChatCompletions; using Forge.OpenAI.Services; using Forge.OpenAI.Settings; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using Forge.OpenAI.GPT; namespace MultipleApiKeyUsage { @@ -23,11 +23,14 @@ static async Task Main(string[] args) // Using the loggedIn account, navigate to https://platform.openai.com/account/api-keys // Here you can create apiKey(s), for example let's create two for this demo. + // Add the created API keys here const string apiKeyForUserA = ""; const string apiKeyForUserB = ""; + const string apiKeyForUserC = ""; + - // Create the OpenAI service instances for the users with manual DI configuration + // 1, First example: create the OpenAI service instances for the users with manual DI configuration // AddForgeOpenAI can be replaced with other init methods, see ServiceCollectionExtensions.cs IOpenAIService openAiInstanceForUserA = OpenAIService @@ -35,52 +38,90 @@ static async Task Main(string[] args) sc.AddForgeOpenAI(options => options.AuthenticationInfo = new AuthenticationInfo(apiKeyForUserA)), out ServiceProvider serviceProviderA); - // The same can be done with the OpenAIOptions - OpenAIOptions optionsForUserB = new OpenAIOptions(); - optionsForUserB.AuthenticationInfo = new AuthenticationInfo(apiKeyForUserB); + // 2, Second example: the same can be done with an action used to configure OpenAIOptions IOpenAIService openAiInstanceForUserB = OpenAIService.CreateService((OpenAIOptions options) => { options.AuthenticationInfo = new AuthenticationInfo(apiKeyForUserB); }, out ServiceProvider serviceProviderB); + // 3, Third example: the same can be done with an own OpenAIOptions instance + OpenAIOptions optionsForUserC = new OpenAIOptions(); + optionsForUserC.AuthenticationInfo = new AuthenticationInfo(apiKeyForUserC); + + IOpenAIService openAiInstanceForUserC = OpenAIService.CreateService(optionsForUserC, out ServiceProvider serviceProviderC); + + + // Now lets use the OpenAI instances using (serviceProviderA) { using (serviceProviderB) { - await TextEditExampleAsync(openAiInstanceForUserA); - await TextEditExampleAsync(openAiInstanceForUserB); - - // NOTE: there is an other example in the Playgrounds here, which demonstrates - // how you can use the OpenAPI with multiple users, but with only one ApiKey - // This is useful, if you have multiple users, becuase it is highly recommended - // to differentiate them. If a user against the OpenAPI rules, this user will be - // denied and not your whole apiKey and your other users. + using (serviceProviderC) + { + // make actions with the OpenAI instances + await ChatWithStreamingModeWithCallback(openAiInstanceForUserA); + await ChatWithStreamingModeWithCallback(openAiInstanceForUserB); + await ChatWithStreamingModeWithCallback(openAiInstanceForUserC); + + // NOTE: there is an other example in the Playgrounds here, which demonstrates + // how you can use the OpenAPI with multiple users, but with only one ApiKey + // This is useful, if you have multiple users, because it is highly recommended + // to differentiate them. If a user against the OpenAPI rules, this user will be + // denied and not your whole apiKey and your other users. + } } } + + // NOTE: if you do want to manage the lifecycle of the ServiceProvider instance, you can discard it, like that: + // IOpenAIService openAiInstanceForUserC = OpenAIService.CreateService(optionsForUserC, out _); + // + // This is not a generally recommended way, because you can't dispose the ServiceProvider instance + // and you can't release the resources, which are used by the OpenAI services. + // The recommended way is to use the using statement, like in the example above. + // If you don't want to use the using statement, you can store the ServiceProvider instance + // and dispose it manually, when you don't need it anymore. + // But in this case, you have to be careful, because you have to dispose the ServiceProvider instance + // in the right time, otherwise you can have memory leaks. + // + // The only exception is, when you use the OpenAI services in a long running application, like a web application, + // where you don't want to dispose the ServiceProvider instance, because you want to use it during the whole lifecycle + // of the application. In this case, you have to be careful, because you have to dispose the ServiceProvider instance + // when the application is shutting down, otherwise you can have memory leaks. + } - static async Task TextEditExampleAsync(IOpenAIService openAIService) + static async Task ChatWithStreamingModeWithCallback(IOpenAIService openAi) { - TextEditRequest request = new TextEditRequest(); - request.InputTextForEditing = "Do you happy with your order?"; - request.Instruction = "Fix the grammar"; + // this method is useful for older .NET where the IAsyncEnumerable is not supported + + ChatCompletionRequest request = new ChatCompletionRequest(ChatMessage.CreateFromUser("Write a C# code which demonstrate how to open a text file and read its content")); + request.MaxTokens = 4096 - GPT3Tokenizer.Encode(request.Messages[0].Content).Count; // calculating max token + request.Temperature = 0.1; // lower value means more precise answer - Console.WriteLine(request.InputTextForEditing); - Console.WriteLine(request.Instruction); + Console.WriteLine(request.Messages[0].Content); - HttpOperationResult response = - await openAIService.TextEditService.GetAsync(request, CancellationToken.None); - + Action> receivedDataHandler = (HttpOperationResult actionResponse) => + { + if (actionResponse.IsSuccess) + { + Console.Write(actionResponse.Result.Choices[0].Delta.Content); + } + else + { + Console.WriteLine(actionResponse); + } + }; + + HttpOperationResult response = await openAi.ChatCompletionService.GetStreamAsync(request, receivedDataHandler, CancellationToken.None); if (response.IsSuccess) { - response.Result!.Choices.ToList().ForEach(c => Console.WriteLine(c.Text)); // output: Are you happy with your order? + Console.WriteLine(); } else { Console.WriteLine(response); } - } }