Skip to content

Commit

Permalink
Replace IOpenAiClients with IAiClient and refactor clients
Browse files Browse the repository at this point in the history
The 'IOpenAiClients' interface has been replaced by 'IAiClient', and all related classes have been refactored accordingly. `IAiClient` more accurately reflects that the client might not necessarily be specific to OpenAI and allows for broader API client development. Additional configuration files for AzureOpenAi and OpenRouter have also been added for more flexibility and future support. The version has been bumped to 4.0.0 due to this breaking change.

Other changes include:
- The Model property in ChatGPTConfig is not transformed anymore and assigned as it is.
- Some variations in hard-coded strings have been fixed for consistency.
- Documentation generation and some editor settings have been added to the build properties.

These will help to better support various AI providers, allow more flexibility for developers and improve overall code quality. This update is a part of an ongoing effort to refactor and improve the codebase.
  • Loading branch information
rodion-m committed Nov 25, 2023
1 parent e6db774 commit 8f9f747
Show file tree
Hide file tree
Showing 37 changed files with 652 additions and 237 deletions.
1 change: 1 addition & 0 deletions OpenAI_DotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "configs", "configs", "{77B5B4CD-2299-4FEE-B6C3-1090A8A8F2C2}"
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
src\.editorconfig = src\.editorconfig
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{130D40E9-8E33-4EBA-8AE1-8B9479BC286A}"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ If you want to configure request parameters, you can do it in `appsettings.json`
```json
{
"ChatGPTConfig": {
"InitialSystemMessage": null,
"InitialSystemMessage": "You are a helpful and kind assistant.",
"InitialUserMessage": null,
"MaxTokens": null,
"Model": null,
Expand Down
11 changes: 10 additions & 1 deletion samples/ChatGpt.BlazorExample/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"OpenAICredentials": {
"ApiKey": "** Your OpenAI ApiKey **"
"ApiKey": "** Your OpenAI ApiKey **. But better in "
},
"ChatGPTConfig": {
"Provider": "openai",
"InitialSystemMessage": "You are a helpful and kind assistant.",
"InitialUserMessage": null,
"MaxTokens": null,
"Model": null,
"Temperature": null,
"PassUserIdToOpenAiRequests": true
},
"Logging": {
"LogLevel": {
Expand Down
3 changes: 2 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<Project>
<PropertyGroup>
<Version>3.3.0</Version>
<Version>4.0.0</Version>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
Expand Down
62 changes: 62 additions & 0 deletions src/OpenAI.ChatGpt.AspNetCore/AiClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.Extensions.Options;

namespace OpenAI.ChatGpt.AspNetCore;

internal class AiClientFactory
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly OpenAICredentials _openAiCredentials;
private readonly AzureOpenAICredentials _azureOpenAiCredentials;
private readonly OpenRouterCredentials _openRouterCredentials;

public AiClientFactory(
IHttpClientFactory httpClientFactory,
IOptions<OpenAICredentials> openAiCredentialsOptions,
IOptions<AzureOpenAICredentials> azureOpenAiCredentialsOptions,
IOptions<OpenRouterCredentials> openRouterCredentialsOptions)
{
ArgumentNullException.ThrowIfNull(openAiCredentialsOptions);
ArgumentNullException.ThrowIfNull(azureOpenAiCredentialsOptions);
ArgumentNullException.ThrowIfNull(openRouterCredentialsOptions);
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
_openAiCredentials = openAiCredentialsOptions.Value;
_azureOpenAiCredentials = azureOpenAiCredentialsOptions.Value;
_openRouterCredentials = openRouterCredentialsOptions.Value;
}

public OpenAiClient GetOpenAiClient()
{
var httpClient = _httpClientFactory.CreateClient(nameof(OpenAiClient));
if (_openAiCredentials.ApiKey is null)
{
throw new InvalidOperationException(
$"OpenAI API key is not configured. Please configure it in {nameof(OpenAICredentials)}");
}
_openAiCredentials.SetupHttpClient(httpClient);
return new OpenAiClient(httpClient);
}

public AzureOpenAiClient GetAzureOpenAiClient()
{
var httpClient = _httpClientFactory.CreateClient(nameof(AzureOpenAiClient));
if (_azureOpenAiCredentials.ApiKey is null)
{
throw new InvalidOperationException(
$"Azure OpenAI API key is not configured. Please configure it in {nameof(AzureOpenAICredentials)}");
}
_azureOpenAiCredentials.SetupHttpClient(httpClient);
return new AzureOpenAiClient(httpClient);
}

public OpenRouterClient GetOpenRouterClient()
{
var httpClient = _httpClientFactory.CreateClient(nameof(OpenRouterClient));
if (_openRouterCredentials.ApiKey is null)
{
throw new InvalidOperationException(
$"OpenRouter API key is not configured. Please configure it in {nameof(OpenRouterCredentials)}");
}
_openRouterCredentials.SetupHttpClient(httpClient);
return new OpenRouterClient(httpClient);
}
}
119 changes: 119 additions & 0 deletions src/OpenAI.ChatGpt.AspNetCore/AiClientFromConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using Microsoft.Extensions.Configuration;

namespace OpenAI.ChatGpt.AspNetCore;

#pragma warning disable CS0618 // Type or member is obsolete
internal class AiClientFromConfiguration : IAiClient, IOpenAiClient
#pragma warning restore CS0618 // Type or member is obsolete
{
private const string OpenAiProvider = "openai";
private const string AzureOpenAiProvider = "azure_openai";
private const string OpenRouterProvider = "openrouter";

private static readonly string[] Providers =
{
OpenAiProvider, AzureOpenAiProvider, OpenRouterProvider
};
private readonly IAiClient _client;

public AiClientFromConfiguration(
AiClientFactory clientFactory,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(clientFactory);
ArgumentNullException.ThrowIfNull(configuration);
var provider = configuration.GetValue<string>("AIProvider")?.ToLower();
provider ??= OpenAiProvider;
if (!Providers.Contains(provider))
{
ThrowUnkownProviderException(provider);
}
_client = provider switch
{
OpenAiProvider => clientFactory.GetOpenAiClient(),
AzureOpenAiProvider => clientFactory.GetAzureOpenAiClient(),
OpenRouterProvider => clientFactory.GetOpenRouterClient(),
_ => throw new InvalidOperationException($"Unknown provider: {provider}")
};
}


private static void ThrowUnkownProviderException(string provider)
{
throw new ArgumentException($"Unknown AI provider: {provider}. " +
$"Supported providers: {string.Join(", ", Providers)}");
}

/// <inheritdoc />
public Task<string> GetChatCompletions(UserOrSystemMessage dialog,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default, float temperature = ChatCompletionTemperatures.Default,
string? user = null, bool jsonMode = false, long? seed = null,
Action<ChatCompletionRequest>? requestModifier = null,
Action<ChatCompletionResponse>? rawResponseGetter = null, CancellationToken cancellationToken = default)
{
return _client.GetChatCompletions(dialog, maxTokens, model, temperature, user, jsonMode, seed,
requestModifier, rawResponseGetter, cancellationToken);
}

/// <inheritdoc />
public Task<string> GetChatCompletions(IEnumerable<ChatCompletionMessage> messages,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default, float temperature = ChatCompletionTemperatures.Default,
string? user = null, bool jsonMode = false, long? seed = null,
Action<ChatCompletionRequest>? requestModifier = null,
Action<ChatCompletionResponse>? rawResponseGetter = null, CancellationToken cancellationToken = default)
{
return _client.GetChatCompletions(messages, maxTokens, model, temperature, user, jsonMode, seed,
requestModifier, rawResponseGetter, cancellationToken);
}

/// <inheritdoc />
public Task<ChatCompletionResponse> GetChatCompletionsRaw(IEnumerable<ChatCompletionMessage> messages,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default, float temperature = ChatCompletionTemperatures.Default,
string? user = null, bool jsonMode = false, long? seed = null,
Action<ChatCompletionRequest>? requestModifier = null,
CancellationToken cancellationToken = default)
{
return _client.GetChatCompletionsRaw(messages, maxTokens, model, temperature, user, jsonMode, seed,
requestModifier, cancellationToken);
}

/// <inheritdoc />
public IAsyncEnumerable<string> StreamChatCompletions(IEnumerable<ChatCompletionMessage> messages,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default, float temperature = ChatCompletionTemperatures.Default,
string? user = null, bool jsonMode = false, long? seed = null,
Action<ChatCompletionRequest>? requestModifier = null,
CancellationToken cancellationToken = default)
{
return _client.StreamChatCompletions(messages, maxTokens, model, temperature, user, jsonMode, seed,
requestModifier, cancellationToken);
}

/// <inheritdoc />
public IAsyncEnumerable<string> StreamChatCompletions(UserOrSystemMessage messages,
int maxTokens = ChatCompletionRequest.MaxTokensDefault, string model = ChatCompletionModels.Default,
float temperature = ChatCompletionTemperatures.Default, string? user = null, bool jsonMode = false,
long? seed = null, Action<ChatCompletionRequest>? requestModifier = null,
CancellationToken cancellationToken = default)
{
return _client.StreamChatCompletions(messages, maxTokens, model, temperature, user, jsonMode, seed,
requestModifier, cancellationToken);
}

/// <inheritdoc />
public IAsyncEnumerable<string> StreamChatCompletions(ChatCompletionRequest request,
CancellationToken cancellationToken = default)
{
return _client.StreamChatCompletions(request, cancellationToken);
}

/// <inheritdoc />
public IAsyncEnumerable<ChatCompletionResponse> StreamChatCompletionsRaw(ChatCompletionRequest request,
CancellationToken cancellationToken = default)
{
return _client.StreamChatCompletionsRaw(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Extensions.Hosting;

namespace OpenAI.ChatGpt.AspNetCore;

internal class AiClientStartupValidationBackgroundService : BackgroundService
{
private readonly AiClientFromConfiguration _aiClient;

public AiClientStartupValidationBackgroundService(AiClientFromConfiguration aiClient)
{
_aiClient = aiClient ?? throw new ArgumentNullException(nameof(aiClient));
}

protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.CompletedTask;
}
17 changes: 2 additions & 15 deletions src/OpenAI.ChatGpt.AspNetCore/ChatGPTFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ namespace OpenAI.ChatGpt.AspNetCore;
// ReSharper disable once InconsistentNaming
public class ChatGPTFactory : IDisposable
{
private readonly IOpenAiClient _client;
private readonly IAiClient _client;
private readonly ChatGPTConfig _config;
private readonly IChatHistoryStorage _chatHistoryStorage;
private readonly ITimeProvider _clock;
private readonly bool _isHttpClientInjected;
private volatile bool _ensureStorageCreatedCalled;

public ChatGPTFactory(
IOpenAiClient client,
IAiClient client,
IOptions<ChatGPTConfig> config,
IChatHistoryStorage chatHistoryStorage,
ITimeProvider clock)
Expand All @@ -29,19 +29,6 @@ public ChatGPTFactory(
_isHttpClientInjected = true;
}

internal ChatGPTFactory(
IOptions<OpenAICredentials> credentials,
IOptions<ChatGPTConfig> config,
IChatHistoryStorage chatHistoryStorage,
ITimeProvider clock)
{
if (credentials?.Value == null) throw new ArgumentNullException(nameof(credentials));
_config = config?.Value ?? throw new ArgumentNullException(nameof(config));
_chatHistoryStorage = chatHistoryStorage ?? throw new ArgumentNullException(nameof(chatHistoryStorage));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_client = new OpenAiClient(credentials.Value.ApiKey);
}

public ChatGPTFactory(
string apiKey,
IChatHistoryStorage chatHistoryStorage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ namespace OpenAI.ChatGpt.AspNetCore.Extensions;

public static class ServiceCollectionExtensions
{
public const string CredentialsConfigSectionPathDefault = "OpenAICredentials";
public const string OpenAiCredentialsConfigSectionPathDefault = "OpenAICredentials";
public const string AzureOpenAiCredentialsConfigSectionPathDefault = "AzureOpenAICredentials";
public const string OpenRouterCredentialsConfigSectionPathDefault = "OpenRouterCredentials";

// ReSharper disable once InconsistentNaming
public const string ChatGPTConfigSectionPathDefault = "ChatGPTConfig";

public static IHttpClientBuilder AddChatGptInMemoryIntegration(
public static IServiceCollection AddChatGptInMemoryIntegration(
this IServiceCollection services,
bool injectInMemoryChatService = true,
string credentialsConfigSectionPath = CredentialsConfigSectionPathDefault,
string completionsConfigSectionPath = ChatGPTConfigSectionPathDefault)
string credentialsConfigSectionPath = OpenAiCredentialsConfigSectionPathDefault,
string completionsConfigSectionPath = ChatGPTConfigSectionPathDefault,
bool validateAiClientProviderOnStart = true)
{
ArgumentNullException.ThrowIfNull(services);
if (string.IsNullOrWhiteSpace(credentialsConfigSectionPath))
Expand All @@ -36,8 +39,9 @@ public static IHttpClientBuilder AddChatGptInMemoryIntegration(
}

return services.AddChatGptIntegrationCore(
credentialsConfigSectionPath,
completionsConfigSectionPath
credentialsConfigSectionPath: credentialsConfigSectionPath,
completionsConfigSectionPath: completionsConfigSectionPath,
validateAiClientProviderOnStart: validateAiClientProviderOnStart
);
}

Expand All @@ -64,12 +68,13 @@ private static ChatService CreateChatService(IServiceProvider provider)
return chat;
}

public static IHttpClientBuilder AddChatGptIntegrationCore(
this IServiceCollection services,
string credentialsConfigSectionPath = CredentialsConfigSectionPathDefault,
public static IServiceCollection AddChatGptIntegrationCore(this IServiceCollection services,
string credentialsConfigSectionPath = OpenAiCredentialsConfigSectionPathDefault,
string completionsConfigSectionPath = ChatGPTConfigSectionPathDefault,
ServiceLifetime serviceLifetime = ServiceLifetime.Scoped
)
string azureOpenAiCredentialsConfigSectionPath = AzureOpenAiCredentialsConfigSectionPathDefault,
string openRouterCredentialsConfigSectionPath = OpenRouterCredentialsConfigSectionPathDefault,
ServiceLifetime gptFactoryLifetime = ServiceLifetime.Scoped,
bool validateAiClientProviderOnStart = true)
{
ArgumentNullException.ThrowIfNull(services);
if (string.IsNullOrWhiteSpace(credentialsConfigSectionPath))
Expand All @@ -84,28 +89,46 @@ public static IHttpClientBuilder AddChatGptIntegrationCore(
nameof(completionsConfigSectionPath));
}


services.AddOptions<OpenAICredentials>()
.BindConfiguration(credentialsConfigSectionPath)
.Configure(_ => { }) //make optional
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<AzureOpenAICredentials>()
.BindConfiguration(azureOpenAiCredentialsConfigSectionPath)
.Configure(_ => { }) //make optional
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<OpenRouterCredentials>()
.BindConfiguration(openRouterCredentialsConfigSectionPath)
.Configure(_ => { }) //make optional
.ValidateDataAnnotations()
.ValidateOnStart();

services.AddOptions<ChatGPTConfig>()
.BindConfiguration(completionsConfigSectionPath)
.Configure(_ => { }) //make optional
.ValidateDataAnnotations()
.ValidateOnStart();

services.AddSingleton<ITimeProvider, TimeProviderUtc>();
services.Add(new ServiceDescriptor(typeof(ChatGPTFactory), typeof(ChatGPTFactory), serviceLifetime));
services.Add(new ServiceDescriptor(typeof(ChatGPTFactory), typeof(ChatGPTFactory), gptFactoryLifetime));

return AddOpenAiClient(services);
}
services.AddHttpClient(nameof(OpenAiClient));
services.AddHttpClient(nameof(AzureOpenAiClient));
services.AddHttpClient(nameof(OpenRouterClient));

private static IHttpClientBuilder AddOpenAiClient(IServiceCollection services)
{
return services.AddHttpClient<IOpenAiClient, OpenAiClient>((provider, httpClient) =>
services.AddSingleton<IAiClient, AiClientFromConfiguration>();
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IOpenAiClient, AiClientFromConfiguration>();
#pragma warning restore CS0618 // Type or member is obsolete

if (validateAiClientProviderOnStart)
{
var credentials = provider.GetRequiredService<IOptions<OpenAICredentials>>().Value;
credentials.SetupHttpClient(httpClient);
});
services.AddHostedService<AiClientStartupValidationBackgroundService>();
}

return services;
}
}
Loading

0 comments on commit 8f9f747

Please sign in to comment.