diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
new file mode 100644
index 000000000000..ee6fb9b38f2a
--- /dev/null
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Text;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace Agents;
+
+///
+/// Demonstrate creation of and
+/// eliciting its response to three explicit user messages.
+///
+public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseTest(output)
+{
+ private const string ParrotName = "Parrot";
+ private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound.";
+
+ [Fact]
+ public async Task UseStreamingChatCompletionAgentAsync()
+ {
+ // Define the agent
+ ChatCompletionAgent agent =
+ new()
+ {
+ Name = ParrotName,
+ Instructions = ParrotInstructions,
+ Kernel = this.CreateKernelWithChatCompletion(),
+ };
+
+ ChatHistory chat = [];
+
+ // Respond to user input
+ await InvokeAgentAsync("Fortune favors the bold.");
+ await InvokeAgentAsync("I came, I saw, I conquered.");
+ await InvokeAgentAsync("Practice makes perfect.");
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(string input)
+ {
+ chat.Add(new ChatMessageContent(AuthorRole.User, input));
+
+ Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+
+ StringBuilder builder = new();
+ await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat))
+ {
+ if (string.IsNullOrEmpty(message.Content))
+ {
+ continue;
+ }
+
+ if (builder.Length == 0)
+ {
+ Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:");
+ }
+
+ Console.WriteLine($"\t > streamed: '{message.Content}'");
+ builder.Append(message.Content);
+ }
+
+ if (builder.Length > 0)
+ {
+ // Display full response and capture in chat history
+ Console.WriteLine($"\t > complete: '{builder}'");
+ chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name });
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs
index c9ffcdac8a84..ddab79f032b0 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs
@@ -27,7 +27,7 @@ public async Task UseSingleChatCompletionAgentAsync()
};
/// Create a chat for agent interaction. For more, .
- ChatHistory chat = new();
+ ChatHistory chat = [];
// Respond to user input
await InvokeAgentAsync("Fortune favors the bold.");
@@ -41,7 +41,7 @@ async Task InvokeAgentAsync(string input)
Console.WriteLine($"# {AuthorRole.User}: '{input}'");
- await foreach (var content in agent.InvokeAsync(chat))
+ await foreach (ChatMessageContent content in agent.InvokeAsync(chat))
{
Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs
index a28f9013d85e..61737de498be 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs
@@ -34,7 +34,7 @@ public async Task UseChatCompletionWithPluginAgentAsync()
agent.Kernel.Plugins.Add(plugin);
/// Create a chat for agent interaction. For more, .
- AgentGroupChat chat = new();
+ ChatHistory chat = [];
// Respond to user input, invoking functions where appropriate.
await InvokeAgentAsync("Hello");
@@ -45,10 +45,10 @@ public async Task UseChatCompletionWithPluginAgentAsync()
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
+ chat.Add(new ChatMessageContent(AuthorRole.User, input));
Console.WriteLine($"# {AuthorRole.User}: '{input}'");
- await foreach (var content in chat.InvokeAsync(agent))
+ await foreach (var content in agent.InvokeAsync(chat))
{
Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
}
diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs
index 3baeb934a52b..2bb5616ff959 100644
--- a/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs
+++ b/dotnet/src/Agents/Abstractions/ChatHistoryChannel.cs
@@ -25,7 +25,7 @@ protected internal sealed override async IAsyncEnumerable In
throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})");
}
- await foreach (var message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false))
+ await foreach (ChatMessageContent message in historyHandler.InvokeAsync(this._history, cancellationToken).ConfigureAwait(false))
{
this._history.Add(message);
diff --git a/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs b/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs
index 315f7bc37cbc..3de87da3de06 100644
--- a/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs
+++ b/dotnet/src/Agents/Abstractions/ChatHistoryKernelAgent.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.ChatCompletion;
namespace Microsoft.SemanticKernel.Agents;
@@ -31,6 +32,11 @@ protected internal sealed override Task CreateChannelAsync(Cancell
///
public abstract IAsyncEnumerable InvokeAsync(
- IReadOnlyList history,
+ ChatHistory history,
+ CancellationToken cancellationToken = default);
+
+ ///
+ public abstract IAsyncEnumerable InvokeStreamingAsync(
+ ChatHistory history,
CancellationToken cancellationToken = default);
}
diff --git a/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs b/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs
index 13fedcd0d0cb..8b7dab748c81 100644
--- a/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs
+++ b/dotnet/src/Agents/Abstractions/IChatHistoryHandler.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
+using Microsoft.SemanticKernel.ChatCompletion;
namespace Microsoft.SemanticKernel.Agents;
@@ -10,12 +11,22 @@ namespace Microsoft.SemanticKernel.Agents;
public interface IChatHistoryHandler
{
///
- /// Entry point for calling into an agent from a a .
+ /// Entry point for calling into an agent from a .
///
/// The chat history at the point the channel is created.
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
IAsyncEnumerable InvokeAsync(
- IReadOnlyList history,
+ ChatHistory history,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Entry point for calling into an agent from a for streaming content.
+ ///
+ /// The chat history at the point the channel is created.
+ /// The to monitor for cancellation requests. The default is .
+ /// Asynchronous enumeration of streaming content.
+ public abstract IAsyncEnumerable InvokeStreamingAsync(
+ ChatHistory history,
CancellationToken cancellationToken = default);
}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 659c1a7c6313..b84d29494b8e 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -23,17 +24,12 @@ public sealed class ChatCompletionAgent : ChatHistoryKernelAgent
///
public override async IAsyncEnumerable InvokeAsync(
- IReadOnlyList history,
+ ChatHistory history,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- var chatCompletionService = this.Kernel.GetRequiredService();
+ IChatCompletionService chatCompletionService = this.Kernel.GetRequiredService();
- ChatHistory chat = [];
- if (!string.IsNullOrWhiteSpace(this.Instructions))
- {
- chat.Add(new ChatMessageContent(AuthorRole.System, this.Instructions) { AuthorName = this.Name });
- }
- chat.AddRange(history);
+ ChatHistory chat = this.SetupAgentChatHistory(history);
int messageCount = chat.Count;
@@ -58,7 +54,7 @@ await chatCompletionService.GetChatMessageContentsAsync(
message.AuthorName = this.Name;
- yield return message;
+ history.Add(message);
}
foreach (ChatMessageContent message in messages ?? [])
@@ -69,4 +65,62 @@ await chatCompletionService.GetChatMessageContentsAsync(
yield return message;
}
}
+
+ ///
+ public override async IAsyncEnumerable InvokeStreamingAsync(
+ ChatHistory history,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ IChatCompletionService chatCompletionService = this.Kernel.GetRequiredService();
+
+ ChatHistory chat = this.SetupAgentChatHistory(history);
+
+ int messageCount = chat.Count;
+
+ this.Logger.LogDebug("[{MethodName}] Invoking {ServiceType}.", nameof(InvokeAsync), chatCompletionService.GetType());
+
+ IAsyncEnumerable messages =
+ chatCompletionService.GetStreamingChatMessageContentsAsync(
+ chat,
+ this.ExecutionSettings,
+ this.Kernel,
+ cancellationToken);
+
+ if (this.Logger.IsEnabled(LogLevel.Information))
+ {
+ this.Logger.LogInformation("[{MethodName}] Invoked {ServiceType} with streaming messages.", nameof(InvokeAsync), chatCompletionService.GetType());
+ }
+
+ // Capture mutated messages related function calling / tools
+ for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++)
+ {
+ ChatMessageContent message = chat[messageIndex];
+
+ message.AuthorName = this.Name;
+
+ history.Add(message);
+ }
+
+ await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false))
+ {
+ // TODO: MESSAGE SOURCE - ISSUE #5731
+ message.AuthorName = this.Name;
+
+ yield return message;
+ }
+ }
+
+ private ChatHistory SetupAgentChatHistory(IReadOnlyList history)
+ {
+ ChatHistory chat = [];
+
+ if (!string.IsNullOrWhiteSpace(this.Instructions))
+ {
+ chat.Add(new ChatMessageContent(AuthorRole.System, this.Instructions) { AuthorName = this.Name });
+ }
+
+ chat.AddRange(history);
+
+ return chat;
+ }
}
diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs
index bc8e2b42e29a..89ff7f02cff2 100644
--- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs
+++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs
@@ -135,7 +135,7 @@ private sealed class TestAgent : ChatHistoryKernelAgent
public int InvokeCount { get; private set; }
public override async IAsyncEnumerable InvokeAsync(
- IReadOnlyList history,
+ ChatHistory history,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await Task.Delay(0, cancellationToken);
@@ -144,5 +144,16 @@ public override async IAsyncEnumerable InvokeAsync(
yield return new ChatMessageContent(AuthorRole.Assistant, "sup");
}
+
+ public override IAsyncEnumerable InvokeStreamingAsync(
+ ChatHistory history,
+ CancellationToken cancellationToken = default)
+ {
+ this.InvokeCount++;
+
+ StreamingChatMessageContent[] contents = [new(AuthorRole.Assistant, "sup")];
+
+ return contents.ToAsyncEnumerable();
+ }
}
}
diff --git a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
index 0fb1d8817902..c4a974cbadc9 100644
--- a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
@@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -87,7 +86,7 @@ private static Mock CreateMockAgent()
Mock agent = new();
ChatMessageContent[] messages = [new ChatMessageContent(AuthorRole.Assistant, "test agent")];
- agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable());
+ agent.Setup(a => a.InvokeAsync(It.IsAny(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable());
return agent;
}
diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
index 48b652491f53..921e0acce016 100644
--- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
@@ -198,7 +198,7 @@ private static Mock CreateMockAgent()
Mock agent = new();
ChatMessageContent[] messages = [new ChatMessageContent(AuthorRole.Assistant, "test")];
- agent.Setup(a => a.InvokeAsync(It.IsAny>(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable());
+ agent.Setup(a => a.InvokeAsync(It.IsAny(), It.IsAny())).Returns(() => messages.ToAsyncEnumerable());
return agent;
}
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
index 5357f0edbd11..ae7657c8189c 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
@@ -73,6 +73,48 @@ public async Task VerifyChatCompletionAgentInvocationAsync()
Times.Once);
}
+ ///
+ /// Verify the streaming invocation and response of .
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingAsync()
+ {
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ var mockService = new Mock();
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).Returns(returnContent.ToAsyncEnumerable());
+
+ var agent =
+ new ChatCompletionAgent()
+ {
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ ExecutionSettings = new(),
+ };
+
+ var result = await agent.InvokeStreamingAsync([]).ToArrayAsync();
+
+ Assert.Equal(2, result.Length);
+
+ mockService.Verify(
+ x =>
+ x.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()),
+ Times.Once);
+ }
+
private static Kernel CreateKernel(IChatCompletionService chatCompletionService)
{
var builder = Kernel.CreateBuilder();