Skip to content

.Net: SK integration with MEAI Abstractions (Service Selector + Contents) Phase 1 #10651

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

Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2411de2
Service Selector investigation
RogerBarreto Feb 20, 2025
e980d1b
MEAI integration chatclient kernel service selector
RogerBarreto Feb 20, 2025
4e82956
Update UT
RogerBarreto Feb 20, 2025
fdcf588
WIP
RogerBarreto Feb 20, 2025
8911ca6
UT for IChatClient service selector WIP
RogerBarreto Feb 24, 2025
ff00e14
Undo change
RogerBarreto Feb 24, 2025
0232073
Fix warnings
RogerBarreto Feb 24, 2025
79341db
UT passing OrderedServiceSelector
RogerBarreto Feb 26, 2025
d29ca14
Ensure kernel.Invoke can return MEAI.Contents
RogerBarreto Feb 26, 2025
e73fad7
Adding bidirectional content support
RogerBarreto Feb 26, 2025
26e3cbb
Content Permutations UT
RogerBarreto Feb 26, 2025
19dab90
Ensure types converge
RogerBarreto Feb 26, 2025
92d7b49
Fix warnings
RogerBarreto Feb 26, 2025
1bd07fd
Reducing public apis
RogerBarreto Feb 26, 2025
d82f7ea
Add suppressions
RogerBarreto Feb 26, 2025
66d3f0a
Address PR comments
RogerBarreto Feb 27, 2025
4071341
Addressed Selector comments
RogerBarreto Feb 27, 2025
e2fdba6
Added integration tests
RogerBarreto Feb 27, 2025
02ed447
GptAIServiceSelector
RogerBarreto Feb 27, 2025
1f6bd8b
Add experimental + fixes
RogerBarreto Feb 27, 2025
2d4b08f
Fix UT Agents
RogerBarreto Feb 27, 2025
7a50f09
Fix usings
RogerBarreto Feb 27, 2025
f26bffb
Address PR Feedback
RogerBarreto Feb 27, 2025
bb35f00
Fix warnings
RogerBarreto Feb 28, 2025
38724da
Fix warnings
RogerBarreto Feb 28, 2025
4bc6b63
Fix warnings
RogerBarreto Feb 28, 2025
b188434
Adding missing UT, addresssing Review feedback
RogerBarreto Feb 28, 2025
178be40
Add missing UT
RogerBarreto Feb 28, 2025
d2588bb
Fix warnings
RogerBarreto Feb 28, 2025
b70d35e
Warning fix + UT
RogerBarreto Mar 3, 2025
c7fb8cd
Fix GptSelector
RogerBarreto Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion dotnet/samples/Demos/HomeAutomation/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ Example that demonstrates how to use Semantic Kernel in conjunction with depende
using Microsoft.SemanticKernel.ChatCompletion;
// For Azure OpenAI configuration
#pragma warning disable IDE0005 // Using directive is unnecessary.
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Connectors.OpenAI;

namespace HomeAutomation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ public async Task FunctionUsageMetricsAreCapturedByTelemetryAsExpected()
// Set up a MeterListener to capture the measurements
using MeterListener listener = EnableTelemetryMeters();

var measurements = new Dictionary<string, List<int>>
var measurements = new Dictionary<string, List<long>>
{
["semantic_kernel.function.invocation.token_usage.prompt"] = [],
["semantic_kernel.function.invocation.token_usage.completion"] = [],
};

listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
if (instrument.Name == "semantic_kernel.function.invocation.token_usage.prompt" ||
instrument.Name == "semantic_kernel.function.invocation.token_usage.completion")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ public async Task FunctionUsageMetricsAreCapturedByTelemetryAsExpected()
// Set up a MeterListener to capture the measurements
using MeterListener listener = EnableTelemetryMeters();

var measurements = new Dictionary<string, List<int>>
var measurements = new Dictionary<string, List<long>>
{
["semantic_kernel.function.invocation.token_usage.prompt"] = [],
["semantic_kernel.function.invocation.token_usage.completion"] = [],
};

listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
if (instrument.Name == "semantic_kernel.function.invocation.token_usage.prompt" ||
instrument.Name == "semantic_kernel.function.invocation.token_usage.completion")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

#pragma warning disable IDE0005 // Using directive is unnecessary.
using System;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

#pragma warning disable IDE0005 // Using directive is unnecessary.
using System;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel.Services;

namespace Microsoft.SemanticKernel.AI.ChatCompletion;

/// <summary>
/// Allow <see cref="IChatClient"/> to be used as an <see cref="IAIService"/> in a <see cref="IAIServiceSelector"/>
/// </summary>
internal sealed class ChatClientAIService : IAIService, IChatClient
{
private readonly IChatClient _chatClient;

/// <summary>
/// Storage for AI service attributes.
/// </summary>
internal Dictionary<string, object?> _internalAttributes { get; } = [];

/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAIService"/> class.
/// </summary>
/// <param name="chatClient">Target <see cref="IChatClient"/>.</param>
internal ChatClientAIService(IChatClient chatClient)
{
Verify.NotNull(chatClient);
this._chatClient = chatClient;

var metadata = this._chatClient.GetService<ChatClientMetadata>();
Verify.NotNull(metadata);

this._internalAttributes[nameof(metadata.ModelId)] = metadata.ModelId;
this._internalAttributes[nameof(metadata.ProviderName)] = metadata.ProviderName;
this._internalAttributes[nameof(metadata.ProviderUri)] = metadata.ProviderUri;
}

/// <inheritdoc />
public IReadOnlyDictionary<string, object?> Attributes => this._internalAttributes;

/// <inheritdoc />
public void Dispose() => this._chatClient.Dispose();

/// <inheritdoc />
public Task<ChatResponse> GetResponseAsync(IList<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> this._chatClient.GetResponseAsync(chatMessages, options, cancellationToken);

/// <inheritdoc />
public object? GetService(Type serviceType, object? serviceKey = null)
=> this._chatClient.GetService(serviceType, serviceKey);

/// <inheritdoc />
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IList<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> this._chatClient.GetStreamingResponseAsync(chatMessages, options, cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;

namespace Microsoft.SemanticKernel.ChatCompletion;

/// <summary>
/// Class sponsor that holds extension methods for <see cref="IChatClient"/> interface.
/// </summary>
public static class ChatClientExtensions
{
/// <summary>
/// Get chat multiple chat message content choices for the prompt and settings.
/// </summary>
/// <remarks>
/// This should be used when the settings request for more than one choice.
/// </remarks>
/// <param name="chatClient">Target chat client service.</param>
/// <param name="prompt">The standardized prompt input.</param>
/// <param name="executionSettings">The AI execution settings (optional).</param>
/// <param name="kernel">The <see cref="Kernel"/> containing services, plugins, and other state for use throughout the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>List of different chat message content choices generated by the remote model</returns>
internal static Task<ChatResponse> GetResponseAsync(
this IChatClient chatClient,
string prompt,
PromptExecutionSettings? executionSettings = null,
Kernel? kernel = null,
CancellationToken cancellationToken = default)
{
var chatOptions = executionSettings.ToChatOptions(kernel);

// Try to parse the text as a chat history
if (ChatPromptParser.TryParse(prompt, out var chatHistoryFromPrompt))
{
var messageList = chatHistoryFromPrompt.ToChatMessageList();
return chatClient.GetResponseAsync(messageList, chatOptions, cancellationToken);
}

return chatClient.GetResponseAsync(prompt, chatOptions, cancellationToken);
}

/// <summary>Creates an <see cref="IChatCompletionService"/> for the specified <see cref="IChatClient"/>.</summary>
/// <param name="client">The chat client to be represented as a chat completion service.</param>
/// <param name="serviceProvider">An optional <see cref="IServiceProvider"/> that can be used to resolve services to use in the instance.</param>
/// <returns>
/// The <see cref="IChatCompletionService"/>. If <paramref name="client"/> is an <see cref="IChatCompletionService"/>, <paramref name="client"/> will
/// be returned. Otherwise, a new <see cref="IChatCompletionService"/> will be created that wraps <paramref name="client"/>.
/// </returns>
[Experimental("SKEXP0001")]
public static IChatCompletionService AsChatCompletionService(this IChatClient client, IServiceProvider? serviceProvider = null)
{
Verify.NotNull(client);

return client is IChatCompletionService chatCompletionService ?
chatCompletionService :
new ChatClientChatCompletionService(client, serviceProvider);
}

/// <summary>
/// Get the model identifier for the specified <see cref="IChatClient"/>.
/// </summary>
public static string? GetModelId(this IChatClient client)
{
Verify.NotNull(client);

return client.GetService<ChatClientMetadata>()?.ModelId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel.ChatCompletion;

namespace Microsoft.SemanticKernel.AI.ChatCompletion;

internal static class ChatMessageExtensions
{
/// <summary>Converts a <see cref="ChatMessage"/> to a <see cref="ChatMessageContent"/>.</summary>
/// <remarks>This conversion should not be necessary once SK eventually adopts the shared content types.</remarks>
internal static ChatMessageContent ToChatMessageContent(this ChatMessage message, ChatResponse? response = null)
{
ChatMessageContent result = new()
{
ModelId = response?.ModelId,
AuthorName = message.AuthorName,
InnerContent = response?.RawRepresentation ?? message.RawRepresentation,
Metadata = message.AdditionalProperties,
Role = new AuthorRole(message.Role.Value),
};

foreach (AIContent content in message.Contents)
{
KernelContent? resultContent = null;
switch (content)
{
case Microsoft.Extensions.AI.TextContent tc:
resultContent = new Microsoft.SemanticKernel.TextContent(tc.Text);
break;

case Microsoft.Extensions.AI.DataContent dc when dc.MediaTypeStartsWith("image/"):
resultContent = dc.Data is not null ?
new Microsoft.SemanticKernel.ImageContent(dc.Uri) :
new Microsoft.SemanticKernel.ImageContent(new Uri(dc.Uri));
break;

case Microsoft.Extensions.AI.DataContent dc when dc.MediaTypeStartsWith("audio/"):
resultContent = dc.Data is not null ?
new Microsoft.SemanticKernel.AudioContent(dc.Uri) :
new Microsoft.SemanticKernel.AudioContent(new Uri(dc.Uri));
break;

case Microsoft.Extensions.AI.DataContent dc:
resultContent = dc.Data is not null ?
new Microsoft.SemanticKernel.BinaryContent(dc.Uri) :
new Microsoft.SemanticKernel.BinaryContent(new Uri(dc.Uri));
break;

case Microsoft.Extensions.AI.FunctionCallContent fcc:
resultContent = new Microsoft.SemanticKernel.FunctionCallContent(fcc.Name, null, fcc.CallId, fcc.Arguments is not null ? new(fcc.Arguments) : null);
break;

case Microsoft.Extensions.AI.FunctionResultContent frc:
resultContent = new Microsoft.SemanticKernel.FunctionResultContent(callId: frc.CallId, result: frc.Result);
break;
}

if (resultContent is not null)
{
resultContent.Metadata = content.AdditionalProperties;
resultContent.InnerContent = content.RawRepresentation;
resultContent.ModelId = response?.ModelId;
result.Items.Add(resultContent);
}
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using Microsoft.Extensions.AI;

namespace Microsoft.SemanticKernel.ChatCompletion;

/// <summary>Provides extension methods for <see cref="ChatResponseUpdate"/>.</summary>
internal static class ChatResponseUpdateExtensions
{
/// <summary>Converts a <see cref="ChatResponseUpdate"/> to a <see cref="StreamingChatMessageContent"/>.</summary>
/// <remarks>This conversion should not be necessary once SK eventually adopts the shared content types.</remarks>
internal static StreamingChatMessageContent ToStreamingChatMessageContent(this ChatResponseUpdate update)
{
StreamingChatMessageContent content = new(
update.Role is not null ? new AuthorRole(update.Role.Value.Value) : null,
null)
{
InnerContent = update.RawRepresentation,
ChoiceIndex = update.ChoiceIndex,
Metadata = update.AdditionalProperties,
ModelId = update.ModelId
};

foreach (AIContent item in update.Contents)
{
StreamingKernelContent? resultContent =
item is Microsoft.Extensions.AI.TextContent tc ? new Microsoft.SemanticKernel.StreamingTextContent(tc.Text) :
item is Microsoft.Extensions.AI.FunctionCallContent fcc ?
new Microsoft.SemanticKernel.StreamingFunctionCallUpdateContent(fcc.CallId, fcc.Name, fcc.Arguments is not null ?
JsonSerializer.Serialize(fcc.Arguments!, AbstractionsJsonContext.Default.IDictionaryStringObject!) :
null) :
null;

if (resultContent is not null)
{
resultContent.ModelId = update.ModelId;
content.Items.Add(resultContent);
}
}

return content;
}
}
Loading
Loading