-
Notifications
You must be signed in to change notification settings - Fork 0
Draft: Add M.E.AI.IChatClient adapter to OpenAI #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
97c8686
3aad53d
5ebf3ae
ec674f7
d201449
aa997f6
7665a6c
62791d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,45 @@ | ||||
#nullable enable | ||||
|
||||
using System.Diagnostics.CodeAnalysis; | ||||
using Microsoft.Extensions.AI; | ||||
using OpenAI.Audio; | ||||
using OpenAI.Chat; | ||||
using OpenAI.Embeddings; | ||||
using OpenAI.Responses; | ||||
|
||||
namespace OpenAI.Custom.Microsoft.Extensions.AI; | ||||
|
||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
/// <summary>Provides extension methods for working with <see cref="OpenAIClient"/>s.</summary> | ||||
public static class OpenAIClientExtensions | ||||
{ | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
/// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="ChatClient"/>.</summary> | ||||
/// <param name="chatClient">The client.</param> | ||||
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="ChatClient"/>.</returns> | ||||
[Experimental("OPENAI003")] | ||||
public static IChatClient AsIChatClient(this ChatClient chatClient) => | ||||
new OpenAIChatClient(chatClient); | ||||
|
||||
/// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="OpenAIResponseClient"/>.</summary> | ||||
/// <param name="responseClient">The client.</param> | ||||
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="OpenAIResponseClient"/>.</returns> | ||||
[Experimental("OPENAI003")] | ||||
public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) => | ||||
new OpenAIResponseChatClient(responseClient); | ||||
|
||||
/// <summary>Gets an <see cref="ISpeechToTextClient"/> for use with this <see cref="AudioClient"/>.</summary> | ||||
/// <param name="audioClient">The client.</param> | ||||
/// <returns>An <see cref="ISpeechToTextClient"/> that can be used to transcribe audio via the <see cref="AudioClient"/>.</returns> | ||||
[Experimental("OPENAI003")] | ||||
public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) => | ||||
new OpenAISpeechToTextClient(audioClient); | ||||
|
||||
/// <summary>Gets an <see cref="IEmbeddingGenerator{String, Single}"/> for use with this <see cref="EmbeddingClient"/>.</summary> | ||||
/// <param name="embeddingClient">The client.</param> | ||||
/// <param name="defaultModelDimensions">The number of dimensions to generate in each embedding.</param> | ||||
/// <returns>An <see cref="IEmbeddingGenerator{String, Embedding}"/> that can be used to generate embeddings via the <see cref="EmbeddingClient"/>.</returns> | ||||
[Experimental("OPENAI003")] | ||||
public static IEmbeddingGenerator<string, Embedding<float>> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) => | ||||
new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions); | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
#nullable enable | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using OpenAI; | ||
using OpenAI.Embeddings; | ||
|
||
namespace Microsoft.Extensions.AI; | ||
|
||
/// <summary>An <see cref="IEmbeddingGenerator{String, Embedding}"/> for an OpenAI <see cref="EmbeddingClient"/>.</summary> | ||
internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>> | ||
{ | ||
/// <summary>Metadata about the embedding generator.</summary> | ||
private readonly EmbeddingGeneratorMetadata _metadata; | ||
|
||
/// <summary>The underlying <see cref="OpenAI.Chat.ChatClient" />.</summary> | ||
private readonly EmbeddingClient _embeddingClient; | ||
|
||
/// <summary>The number of dimensions produced by the generator.</summary> | ||
private readonly int? _dimensions; | ||
|
||
/// <summary>Initializes a new instance of the <see cref="OpenAIEmbeddingGenerator"/> class.</summary> | ||
/// <param name="embeddingClient">The underlying client.</param> | ||
/// <param name="defaultModelDimensions">The number of dimensions to generate in each embedding.</param> | ||
/// <exception cref="ArgumentNullException"><paramref name="embeddingClient"/> is <see langword="null"/>.</exception> | ||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="defaultModelDimensions"/> is not positive.</exception> | ||
public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? defaultModelDimensions = null) | ||
{ | ||
Argument.AssertNotNull(embeddingClient, nameof(embeddingClient)); | ||
if (defaultModelDimensions < 1) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0."); | ||
} | ||
|
||
_embeddingClient = embeddingClient; | ||
_dimensions = defaultModelDimensions; | ||
_metadata = CreateMetadata("openai", | ||
embeddingClient.Endpoint ?? OpenAIChatClient.DefaultOpenAIEndpoint, | ||
embeddingClient.Model, | ||
defaultModelDimensions); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) | ||
{ | ||
OpenAI.Embeddings.EmbeddingGenerationOptions? openAIOptions = ToOpenAIOptions(options); | ||
|
||
var embeddings = (await _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken).ConfigureAwait(false)).Value; | ||
|
||
return new(embeddings.Select(e => | ||
new Embedding<float>(e.ToFloats()) | ||
{ | ||
CreatedAt = DateTimeOffset.UtcNow, | ||
ModelId = embeddings.Model, | ||
})) | ||
{ | ||
Usage = new() | ||
{ | ||
InputTokenCount = embeddings.Usage.InputTokenCount, | ||
TotalTokenCount = embeddings.Usage.TotalTokenCount | ||
}, | ||
}; | ||
} | ||
|
||
/// <inheritdoc /> | ||
void IDisposable.Dispose() | ||
{ | ||
// Nothing to dispose. Implementation required for the IEmbeddingGenerator interface. | ||
} | ||
|
||
/// <inheritdoc /> | ||
object? IEmbeddingGenerator.GetService(Type serviceType, object? serviceKey) | ||
{ | ||
Argument.AssertNotNull(serviceType, nameof(serviceType)); | ||
|
||
return | ||
serviceKey is not null ? null : | ||
serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata : | ||
serviceType == typeof(EmbeddingClient) ? _embeddingClient : | ||
serviceType.IsInstanceOfType(this) ? this : | ||
null; | ||
} | ||
|
||
/// <summary>Creates the <see cref="EmbeddingGeneratorMetadata"/> for this instance.</summary> | ||
private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, Uri providerUri, string? defaultModelId, int? defaultModelDimensions) => | ||
new(providerName, providerUri, defaultModelId, defaultModelDimensions); | ||
|
||
/// <summary>Converts an extensions options instance to an OpenAI options instance.</summary> | ||
private OpenAI.Embeddings.EmbeddingGenerationOptions? ToOpenAIOptions(EmbeddingGenerationOptions? options) | ||
{ | ||
OpenAI.Embeddings.EmbeddingGenerationOptions openAIOptions = new() | ||
{ | ||
Dimensions = options?.Dimensions ?? _dimensions, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we'd follow the same pattern of getting an initial |
||
|
||
if (options?.AdditionalProperties is { Count: > 0 } additionalProperties) | ||
{ | ||
if (additionalProperties.TryGetValue(nameof(openAIOptions.EndUserId), out string? endUserId)) | ||
{ | ||
openAIOptions.EndUserId = endUserId; | ||
} | ||
} | ||
|
||
return openAIOptions; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This namespace looks wrong. Should be
Microsoft.Extensions.AI
?