Skip to content

Commit f4f8637

Browse files
authored
.NET - Introduce ChatCompletionAgent Allow-List of supported content (#10350)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Ensure `ChatCompletionAgent` only accepts content types / patterns allowed by chat-completion service. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> The sample [Concepts/Agents/MixedChat_Images](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Agents/MixedChat_Images.cs) fails (connector assertion) when passing a message generated by `OpenAIAssistantAgent` to `ChatCompletionAgent` that contains only `FileReferenceContent`. > This sample historically worked, but there may have been an internal logic update for the connector as part of a maintenance task. I didn't bother to do the forensics, as the fact that it fails now is sufficient data to address the bug. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent 3724e24 commit f4f8637

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

dotnet/src/Agents/Core/ChatHistoryChannel.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
2+
using System;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Runtime.CompilerServices;
@@ -16,6 +17,15 @@ namespace Microsoft.SemanticKernel.Agents;
1617
/// </summary>
1718
internal sealed class ChatHistoryChannel : AgentChannel
1819
{
20+
// Supported content types for <see cref="ReceiveAsync"/> when
21+
// <see cref="ChatMessageContent.Content"/> is empty.
22+
private static readonly HashSet<Type> s_contentMap =
23+
[
24+
typeof(FunctionCallContent),
25+
typeof(FunctionResultContent),
26+
typeof(ImageContent),
27+
];
28+
1929
private readonly ChatHistory _history;
2030

2131
/// <inheritdoc/>
@@ -105,7 +115,11 @@ protected override async IAsyncEnumerable<StreamingChatMessageContent> InvokeStr
105115
/// <inheritdoc/>
106116
protected override Task ReceiveAsync(IEnumerable<ChatMessageContent> history, CancellationToken cancellationToken)
107117
{
108-
this._history.AddRange(history);
118+
// Only add messages with valid content or supported content-items.
119+
this._history.AddRange(
120+
history.Where(
121+
m => !string.IsNullOrEmpty(m.Content) ||
122+
m.Items.Where(i => s_contentMap.Contains(i.GetType())).Any()));
109123

110124
return Task.CompletedTask;
111125
}

dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs

+94-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
2+
using System;
23
using System.Linq;
34
using System.Threading.Tasks;
45
using Microsoft.SemanticKernel;
56
using Microsoft.SemanticKernel.Agents;
7+
using Microsoft.SemanticKernel.ChatCompletion;
68
using Moq;
79
using Xunit;
810

@@ -18,13 +20,103 @@ public class ChatHistoryChannelTests
1820
/// does not implement <see cref="ChatHistoryKernelAgent"/>.
1921
/// </summary>
2022
[Fact]
21-
public async Task VerifyAgentWithoutIChatHistoryHandlerAsync()
23+
public async Task VerifyAgentIsChatHistoryKernelAgentAsync()
2224
{
2325
// Arrange
2426
Mock<Agent> agent = new(); // Not a ChatHistoryKernelAgent
25-
ChatHistoryChannel channel = new(); // Requires IChatHistoryHandler
27+
ChatHistoryChannel channel = new();
2628

2729
// Act & Assert
2830
await Assert.ThrowsAsync<KernelException>(() => channel.InvokeAsync(agent.Object).ToArrayAsync().AsTask());
2931
}
32+
33+
/// <summary>
34+
/// Verify a <see cref="ChatHistoryChannel"/> filters empty content on receive.
35+
/// </summary>
36+
[Fact]
37+
public async Task VerifyReceiveFiltersEmptyContentAsync()
38+
{
39+
// Arrange
40+
ChatHistoryChannel channel = new();
41+
42+
// Act
43+
await channel.ReceiveAsync([new ChatMessageContent(AuthorRole.Assistant, string.Empty)]);
44+
45+
// Assert
46+
Assert.Empty(await channel.GetHistoryAsync().ToArrayAsync());
47+
}
48+
49+
/// <summary>
50+
/// Verify a <see cref="ChatHistoryChannel"/> filters file content on receive.
51+
/// </summary>
52+
/// <remarks>
53+
/// As long as content is not empty, extraneous file content is ok.
54+
/// </remarks>
55+
[Fact]
56+
public async Task VerifyReceiveFiltersFileContentAsync()
57+
{
58+
// Arrange
59+
ChatHistoryChannel channel = new();
60+
61+
// Act
62+
await channel.ReceiveAsync([new ChatMessageContent(AuthorRole.Assistant, [new FileReferenceContent("fileId")])]);
63+
64+
// Assert
65+
Assert.Empty(await channel.GetHistoryAsync().ToArrayAsync());
66+
67+
// Act
68+
await channel.ReceiveAsync(
69+
[new ChatMessageContent(
70+
AuthorRole.Assistant,
71+
[
72+
new TextContent("test"),
73+
new FileReferenceContent("fileId")
74+
])]);
75+
76+
// Assert
77+
var history = await channel.GetHistoryAsync().ToArrayAsync();
78+
Assert.Single(history);
79+
Assert.Equal(2, history[0].Items.Count);
80+
}
81+
82+
/// <summary>
83+
/// Verify a <see cref="ChatHistoryChannel"/> accepts function content on receive.
84+
/// </summary>
85+
[Fact]
86+
public async Task VerifyReceiveAcceptsFunctionContentAsync()
87+
{
88+
// Arrange
89+
ChatHistoryChannel channel = new();
90+
91+
// Act
92+
await channel.ReceiveAsync([new ChatMessageContent(AuthorRole.Assistant, [new FunctionCallContent("test-func")])]);
93+
94+
// Assert
95+
Assert.Single(await channel.GetHistoryAsync().ToArrayAsync());
96+
97+
// Arrange
98+
channel = new();
99+
100+
// Act
101+
await channel.ReceiveAsync([new ChatMessageContent(AuthorRole.Assistant, [new FunctionResultContent("test-func")])]);
102+
103+
// Assert
104+
Assert.Single(await channel.GetHistoryAsync().ToArrayAsync());
105+
}
106+
107+
/// <summary>
108+
/// Verify a <see cref="ChatHistoryChannel"/> accepts image content on receive.
109+
/// </summary>
110+
[Fact]
111+
public async Task VerifyReceiveAcceptsImageContentAsync()
112+
{
113+
// Arrange
114+
ChatHistoryChannel channel = new();
115+
116+
// Act
117+
await channel.ReceiveAsync([new ChatMessageContent(AuthorRole.Assistant, [new ImageContent(new Uri("http://test.ms/test.jpg"))])]);
118+
119+
// Assert
120+
Assert.Single(await channel.GetHistoryAsync().ToArrayAsync());
121+
}
30122
}

0 commit comments

Comments
 (0)