-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Add adapters to allow SK agents to be exposed as AIAgent #13240
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
f5d46fc
4279c17
5e6d734
9834fe9
dd0e1e4
ea938db
5032d28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
using MAAI = Microsoft.Agents.AI; | ||
|
||
namespace Microsoft.SemanticKernel.Agents.A2A; | ||
|
||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="A2AAgent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
public static class A2AAgentExtensions | ||
{ | ||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="A2AAgent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
/// <param name="a2aAgent">The Semantic Kernel <see cref="A2AAgent"/> to expose as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>.</param> | ||
/// <returns>The Semantic Kernel Agent Framework <see cref="Agent"/> exposed as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/></returns> | ||
[Experimental("SKEXP0110")] | ||
public static MAAI.AIAgent AsAIAgent(this A2AAgent a2aAgent) | ||
=> a2aAgent.AsAIAgent( | ||
() => new A2AAgentThread(a2aAgent.Client), | ||
(json, options) => | ||
{ | ||
var agentId = JsonSerializer.Deserialize<string>(json); | ||
return agentId is null ? new A2AAgentThread(a2aAgent.Client) : new A2AAgentThread(a2aAgent.Client, agentId); | ||
}, | ||
(thread, options) => JsonSerializer.SerializeToElement((thread as A2AAgentThread)?.Id)); | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,128 @@ | ||||
// Copyright (c) Microsoft. All rights reserved. | ||||
|
||||
using System; | ||||
using System.Collections.Generic; | ||||
using System.Linq; | ||||
using System.Threading.Tasks; | ||||
using System.Text.Json; | ||||
using Microsoft.Extensions.AI; | ||||
using System.Runtime.CompilerServices; | ||||
using System.Threading; | ||||
using MAAI = Microsoft.Agents.AI; | ||||
using System.Diagnostics.CodeAnalysis; | ||||
|
||||
namespace Microsoft.SemanticKernel.Agents; | ||||
|
||||
/// <summary> | ||||
/// Exposes a Semantic Kernel Agent Framework <see cref="Agent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||||
/// </summary> | ||||
[Experimental("SKEXP0110")] | ||||
internal sealed class AIAgentAdapter : MAAI.AIAgent | ||||
{ | ||||
private readonly Func<AgentThread> _threadFactory; | ||||
private readonly Func<JsonElement, JsonSerializerOptions?, AgentThread> _threadDeserializationFactory; | ||||
private readonly Func<AgentThread, JsonSerializerOptions?, JsonElement> _threadSerializer; | ||||
|
||||
/// <summary> | ||||
/// Initializes a new instance of the <see cref="AIAgentAdapter"/> class. | ||||
/// </summary> | ||||
/// <param name="semanticKernelAgent">The Semantic Kernel <see cref="Agent"/> to expose as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>.</param> | ||||
/// <param name="threadFactory">A factory method to create the required <see cref="AgentThread"/> type to use with the agent.</param> | ||||
/// <param name="threadDeserializationFactory">A factory method to deserialize the required <see cref="AgentThread"/> type.</param> | ||||
/// <param name="threadSerializer">A method to serialize the <see cref="AgentThread"/> type.</param> | ||||
public AIAgentAdapter( | ||||
Agent semanticKernelAgent, | ||||
Func<AgentThread> threadFactory, | ||||
Func<JsonElement, JsonSerializerOptions?, AgentThread> threadDeserializationFactory, | ||||
Func<AgentThread, JsonSerializerOptions?, JsonElement> threadSerializer) | ||||
{ | ||||
Throw.IfNull(semanticKernelAgent); | ||||
Throw.IfNull(threadFactory); | ||||
Throw.IfNull(threadDeserializationFactory); | ||||
Throw.IfNull(threadSerializer); | ||||
|
||||
this.InnerAgent = semanticKernelAgent; | ||||
this._threadFactory = threadFactory; | ||||
this._threadDeserializationFactory = threadDeserializationFactory; | ||||
this._threadSerializer = threadSerializer; | ||||
} | ||||
|
||||
/// <summary> | ||||
/// Gets the underlying Semantic Kernel Agent Framework <see cref="Agent"/>. | ||||
/// </summary> | ||||
public Agent InnerAgent { get; } | ||||
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. Not fully sure if we need to expose the Agent, we could just recommend the
Suggested change
|
||||
|
||||
/// <inheritdoc /> | ||||
public override MAAI.AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) | ||||
=> new AIAgentThreadAdapter(this._threadDeserializationFactory(serializedThread, jsonSerializerOptions), this._threadSerializer); | ||||
|
||||
/// <inheritdoc /> | ||||
public override MAAI.AgentThread GetNewThread() => new AIAgentThreadAdapter(this._threadFactory(), this._threadSerializer); | ||||
|
||||
/// <inheritdoc /> | ||||
public override async Task<MAAI.AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, MAAI.AgentThread? thread = null, MAAI.AgentRunOptions? options = null, CancellationToken cancellationToken = default) | ||||
{ | ||||
thread ??= this.GetNewThread(); | ||||
if (thread is not AIAgentThreadAdapter typedThread) | ||||
{ | ||||
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used."); | ||||
} | ||||
|
||||
List<ChatMessage> responseMessages = []; | ||||
var invokeOptions = new AgentInvokeOptions() | ||||
{ | ||||
OnIntermediateMessage = (msg) => | ||||
{ | ||||
responseMessages.Add(msg.ToChatMessage()); | ||||
return Task.CompletedTask; | ||||
} | ||||
}; | ||||
|
||||
AgentResponseItem<ChatMessageContent>? lastResponseItem = null; | ||||
ChatMessage? lastResponseMessage = null; | ||||
await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, invokeOptions, cancellationToken).ConfigureAwait(false)) | ||||
{ | ||||
lastResponseItem = responseItem; | ||||
lastResponseMessage = responseItem.Message.ToChatMessage(); | ||||
responseMessages.Add(lastResponseMessage); | ||||
} | ||||
|
||||
return new MAAI.AgentRunResponse(responseMessages) | ||||
{ | ||||
AgentId = this.InnerAgent.Id, | ||||
RawRepresentation = lastResponseItem, | ||||
AdditionalProperties = lastResponseMessage?.AdditionalProperties, | ||||
CreatedAt = lastResponseMessage?.CreatedAt, | ||||
}; | ||||
} | ||||
|
||||
/// <inheritdoc /> | ||||
public override async IAsyncEnumerable<MAAI.AgentRunResponseUpdate> RunStreamingAsync( | ||||
IEnumerable<ChatMessage> messages, | ||||
MAAI.AgentThread? thread = null, | ||||
MAAI.AgentRunOptions? options = null, | ||||
[EnumeratorCancellation] CancellationToken cancellationToken = default) | ||||
{ | ||||
thread ??= this.GetNewThread(); | ||||
if (thread is not AIAgentThreadAdapter typedThread) | ||||
{ | ||||
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used."); | ||||
} | ||||
|
||||
await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, cancellationToken: cancellationToken).ConfigureAwait(false)) | ||||
{ | ||||
var chatMessage = responseItem.Message.ToChatMessage(); | ||||
|
||||
yield return new MAAI.AgentRunResponseUpdate | ||||
{ | ||||
AgentId = this.InnerAgent.Id, | ||||
RawRepresentation = responseItem, | ||||
AdditionalProperties = chatMessage.AdditionalProperties, | ||||
MessageId = chatMessage.MessageId, | ||||
Role = chatMessage.Role, | ||||
CreatedAt = chatMessage.CreatedAt, | ||||
Contents = chatMessage.Contents | ||||
}; | ||||
} | ||||
} | ||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,38 @@ | ||||||||||||
// Copyright (c) Microsoft. All rights reserved. | ||||||||||||
|
||||||||||||
using System; | ||||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||||
using System.Text.Json; | ||||||||||||
using MAAI = Microsoft.Agents.AI; | ||||||||||||
|
||||||||||||
namespace Microsoft.SemanticKernel.Agents; | ||||||||||||
|
||||||||||||
[Experimental("SKEXP0110")] | ||||||||||||
internal sealed class AIAgentThreadAdapter : MAAI.AgentThread | ||||||||||||
{ | ||||||||||||
private readonly Func<AgentThread, JsonSerializerOptions?, JsonElement> _threadSerializer; | ||||||||||||
|
||||||||||||
internal AIAgentThreadAdapter(AgentThread thread, Func<AgentThread, JsonSerializerOptions?, JsonElement> threadSerializer) | ||||||||||||
{ | ||||||||||||
Throw.IfNull(thread); | ||||||||||||
Throw.IfNull(threadSerializer); | ||||||||||||
|
||||||||||||
this.InnerThread = thread; | ||||||||||||
this._threadSerializer = threadSerializer; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Gets the underlying Semantic Kernel Agent Framework <see cref="AgentThread"/>. | ||||||||||||
/// </summary> | ||||||||||||
public AgentThread InnerThread { get; } | ||||||||||||
Comment on lines
+24
to
+27
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. The rationale here is a bit similar to where after the conversion we don't expose the converted type back as we assume the caller will have the reference already. Line 18 in 3ac7861
Point to the
Suggested change
|
||||||||||||
|
||||||||||||
/// <inheritdoc /> | ||||||||||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) | ||||||||||||
=> this._threadSerializer(this.InnerThread, jsonSerializerOptions); | ||||||||||||
|
||||||||||||
public override object? GetService(Type serviceType, object? serviceKey = null) | ||||||||||||
=> base.GetService(serviceType, serviceKey) | ||||||||||||
?? (serviceType == typeof(AgentThread) && serviceKey is null | ||||||||||||
? this.InnerThread | ||||||||||||
: null); | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.Linq; | ||
using Microsoft.Extensions.AI; | ||
using Microsoft.SemanticKernel.ChatCompletion; | ||
using MEAI = Microsoft.Extensions.AI; | ||
|
||
namespace Microsoft.SemanticKernel.Agents; | ||
|
||
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. | ||
|
||
internal static class ChatMessageExtensions | ||
{ | ||
/// <summary>Converts a <see cref="ChatMessage"/> to a <see cref="ChatMessageContent"/>.</summary> | ||
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 = new AdditionalPropertiesDictionary(message.AdditionalProperties ?? []) { ["Usage"] = response?.Usage }, | ||
Role = new AuthorRole(message.Role.Value), | ||
}; | ||
|
||
foreach (AIContent content in message.Contents) | ||
{ | ||
KernelContent? resultContent = content switch | ||
{ | ||
MEAI.TextContent tc => new TextContent(tc.Text), | ||
DataContent dc when dc.HasTopLevelMediaType("image") => new ImageContent(dc.Uri), | ||
UriContent uc when uc.HasTopLevelMediaType("image") => new ImageContent(uc.Uri), | ||
DataContent dc when dc.HasTopLevelMediaType("audio") => new AudioContent(dc.Uri), | ||
UriContent uc when uc.HasTopLevelMediaType("audio") => new AudioContent(uc.Uri), | ||
DataContent dc => new BinaryContent(dc.Uri), | ||
UriContent uc => new BinaryContent(uc.Uri), | ||
MEAI.FunctionCallContent fcc => new FunctionCallContent( | ||
functionName: fcc.Name, | ||
id: fcc.CallId, | ||
arguments: fcc.Arguments is not null ? new(fcc.Arguments) : null), | ||
MEAI.FunctionResultContent frc => new FunctionResultContent( | ||
functionName: GetFunctionCallContent(frc.CallId)?.Name, | ||
callId: frc.CallId, | ||
result: frc.Result), | ||
_ => null | ||
}; | ||
|
||
if (resultContent is not null) | ||
{ | ||
resultContent.Metadata = content.AdditionalProperties; | ||
resultContent.InnerContent = content.RawRepresentation; | ||
resultContent.ModelId = response?.ModelId; | ||
result.Items.Add(resultContent); | ||
} | ||
} | ||
|
||
return result; | ||
|
||
MEAI.FunctionCallContent? GetFunctionCallContent(string callId) | ||
=> response?.Messages | ||
.Select(m => m.Contents | ||
.FirstOrDefault(c => c is MEAI.FunctionCallContent fcc && fcc.CallId == callId) as MEAI.FunctionCallContent) | ||
.FirstOrDefault(fcc => fcc is not null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
using MAAI = Microsoft.Agents.AI; | ||
|
||
namespace Microsoft.SemanticKernel.Agents; | ||
|
||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="Agent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
public static class AgentExtensions | ||
{ | ||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="Agent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
/// <param name="semanticKernelAgent">The Semantic Kernel <see cref="Agent"/> to expose as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>.</param> | ||
/// <param name="threadFactory">A factory method to create the required <see cref="AgentThread"/> type to use with the agent.</param> | ||
/// <param name="threadDeserializationFactory">A factory method to deserialize the required <see cref="AgentThread"/> type.</param> | ||
/// <param name="threadSerializer">A method to serialize the <see cref="AgentThread"/> type.</param> | ||
/// <returns>The Semantic Kernel Agent Framework <see cref="Agent"/> exposed as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/></returns> | ||
[Experimental("SKEXP0110")] | ||
public static MAAI.AIAgent AsAIAgent( | ||
this Agent semanticKernelAgent, | ||
Func<AgentThread> threadFactory, | ||
Func<JsonElement, JsonSerializerOptions?, AgentThread> threadDeserializationFactory, | ||
Func<AgentThread, JsonSerializerOptions?, JsonElement> threadSerializer) | ||
=> new AIAgentAdapter( | ||
semanticKernelAgent, | ||
threadFactory, | ||
threadDeserializationFactory, | ||
threadSerializer); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
using MAAI = Microsoft.Agents.AI; | ||
|
||
namespace Microsoft.SemanticKernel.Agents.AzureAI; | ||
|
||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="AzureAIAgent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
public static class AzureAIAgentExtensions | ||
{ | ||
/// <summary> | ||
/// Exposes a Semantic Kernel Agent Framework <see cref="AzureAIAgent"/> as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>. | ||
/// </summary> | ||
/// <param name="azureAIAgent">The Semantic Kernel <see cref="AzureAIAgent"/> to expose as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/>.</param> | ||
/// <returns>The Semantic Kernel Agent Framework <see cref="Agent"/> exposed as a Microsoft Agent Framework <see cref="MAAI.AIAgent"/></returns> | ||
[Experimental("SKEXP0110")] | ||
public static MAAI.AIAgent AsAIAgent(this AzureAIAgent azureAIAgent) | ||
=> azureAIAgent.AsAIAgent( | ||
() => new AzureAIAgentThread(azureAIAgent.Client), | ||
(json, options) => | ||
{ | ||
var agentId = JsonSerializer.Deserialize<string>(json); | ||
return agentId is null ? new AzureAIAgentThread(azureAIAgent.Client) : new AzureAIAgentThread(azureAIAgent.Client, agentId); | ||
}, | ||
(thread, options) => JsonSerializer.SerializeToElement((thread as AzureAIAgentThread)?.Id)); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
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.
nit. (Internal)
For the name of this class I would do something in the same lines we did for the ChatCompletionService vs ChatClient.
i.e:
SKAgentAIAgent
, see ChatCompletionServiceChatClient.cs