Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,33 @@ public async Task UseOpenAIResponseAgentWithThreadedConversationStreamingAsync()
agentThread = await WriteAgentStreamMessageAsync(responseItems);
}
}

[Fact]
public async Task UseOpenAIResponseAgentWithImageContentAsync()
{
// Define the agent
OpenAIResponseAgent agent = new(this.Client)
{
Name = "ResponseAgent",
Instructions = "Provide a detailed description including the weather conditions.",
};

ICollection<ChatMessageContent> messages =
[
new ChatMessageContent(
AuthorRole.User,
items: [
new TextContent("What is in this image?"),
new ImageContent(new Uri("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"))
]
),
];

// Invoke the agent and output the response
var responseItems = agent.InvokeAsync(messages);
await foreach (ChatMessageContent responseItem in responseItems)
{
WriteAgentChatMessage(responseItem);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ public static IEnumerable<ThreadInitializationMessage> ToThreadInitializationMes
/// <returns>A <see cref="ResponseItem"/> instance.</returns>
public static ResponseItem ToResponseItem(this ChatMessageContent message)
{
string content = message.Content ?? string.Empty;
var items = message.Items;
IEnumerable<ResponseContentPart> contentParts = items.Select(item => item.ToResponseContentPart());
return message.Role.Label.ToUpperInvariant() switch
{
"SYSTEM" => ResponseItem.CreateSystemMessageItem(content),
"USER" => ResponseItem.CreateUserMessageItem(content),
"DEVELOPER" => ResponseItem.CreateDeveloperMessageItem(content),
"ASSISTANT" => ResponseItem.CreateAssistantMessageItem(content),
"SYSTEM" => ResponseItem.CreateSystemMessageItem(contentParts),
"USER" => ResponseItem.CreateUserMessageItem(contentParts),
"DEVELOPER" => ResponseItem.CreateDeveloperMessageItem(contentParts),
"ASSISTANT" => ResponseItem.CreateAssistantMessageItem(contentParts),
_ => throw new NotSupportedException($"Unsupported role {message.Role.Label}. Only system, user, developer or assistant roles are allowed."),
};
}
Expand Down
52 changes: 52 additions & 0 deletions dotnet/src/Agents/OpenAI/Extensions/KernelContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using OpenAI.Responses;

namespace Microsoft.SemanticKernel.Agents.OpenAI;

/// <summary>
/// Extensons methods for <see cref="KernelContent"/>.
/// </summary>
internal static class KernelContentExtensions
{
internal static ResponseContentPart ToResponseContentPart(this KernelContent content)
{
return content switch
{
TextContent textContent => textContent.ToResponseContentPart(),
ImageContent imageContent => imageContent.ToResponseContentPart(),
BinaryContent binaryContent => binaryContent.ToResponseContentPart(),
FileReferenceContent fileReferenceContent => fileReferenceContent.ToResponseContentPart(),
_ => throw new NotSupportedException($"Unsupported content type {content.GetType().Name}. Cannot convert to {nameof(ResponseContentPart)}.")
};
}

internal static ResponseContentPart ToResponseContentPart(this TextContent content)
{
return ResponseContentPart.CreateInputTextPart(content.Text);
}

internal static ResponseContentPart ToResponseContentPart(this ImageContent content)
{
return content.Uri is not null
? ResponseContentPart.CreateInputImagePart(content.Uri)
: content.Data is not null
? ResponseContentPart.CreateInputImagePart(new BinaryData(content.Data), content.MimeType)
: throw new NotSupportedException("ImageContent cannot be converted to ResponseContentPart. Only ImageContent with a uri or binary data is supported.");
}

internal static ResponseContentPart ToResponseContentPart(this BinaryContent content)
{
return content.Data is not null
? ResponseContentPart.CreateInputFilePart(new BinaryData(content.Data), content.MimeType, Guid.NewGuid().ToString())
: throw new NotSupportedException("AudioContent cannot be converted to ResponseContentPart. Only AudioContent with binary data is supported.");
}

internal static ResponseContentPart ToResponseContentPart(this FileReferenceContent content)
{
return content.FileId is not null
? ResponseContentPart.CreateInputFilePart(content.FileId)
: throw new NotSupportedException("FileReferenceContent cannot be converted to ResponseContentPart. Only FileReferenceContent with a file id is supported.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
using OpenAI.Responses;
using Xunit;

namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions;

/// <summary>
/// Unit tests for ChatContentMessageExtensions
/// </summary>
public class ChatContentMessageExtensionsTests
{
[Theory]
[InlineData("User")]
[InlineData("Assistant")]
[InlineData("System")]
public void VerifyToResponseItemWithUserChatMessageContent(string roleLabel)
{
// Arrange
var role = new AuthorRole(roleLabel);
var content = new ChatMessageContent(
role,
items: [
new TextContent("What is in this image?"),
new ImageContent(new Uri("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")),
new BinaryContent(new ReadOnlyMemory<byte>([0x52, 0x49, 0x46, 0x46, 0x24, 0x08, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45]), "audio/wav"),
new FileReferenceContent("file-abc123")
]
);

// Act
var responseItem = content.ToResponseItem();

// Assert
Assert.NotNull(responseItem);
Assert.IsType<MessageResponseItem>(responseItem, exactMatch: false);
var messageResponseItem = responseItem as MessageResponseItem;
Assert.NotNull(messageResponseItem);
Assert.Equal(role.Label.ToUpperInvariant(), messageResponseItem.Role.ToString().ToUpperInvariant());
Assert.Equal(4, messageResponseItem.Content.Count);

// Validate TextContent conversion - should create InputText part
var textContent = messageResponseItem.Content.FirstOrDefault(p => p.Kind == ResponseContentPartKind.InputText);
Assert.NotNull(textContent);
//Assert.IsType<>(textContent);
Assert.Equal("What is in this image?", textContent.Text);

// Validate ImageContent conversion - should create InputImage part
var imageContent = messageResponseItem.Content.FirstOrDefault(p => p.Kind == ResponseContentPartKind.InputImage);
Assert.NotNull(imageContent);

// Validate BinaryContent conversion - should create InputFile part
var binaryContent = messageResponseItem.Content.FirstOrDefault(p => p.Kind == ResponseContentPartKind.InputFile && p.InputFileBytes is not null);
Assert.NotNull(binaryContent);
Assert.Equal("audio/wav", binaryContent.InputFileBytesMediaType);

// Validate FileReferenceContent conversion - should create InputImage part
var fileContent = messageResponseItem.Content.FirstOrDefault(p => p.Kind == ResponseContentPartKind.InputFile && p.InputFileId is not null);
Assert.NotNull(fileContent);
Assert.Equal("file-abc123", fileContent.InputFileId);
}
}
Loading