diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotation.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationExtensions.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationKind.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIAnnotationReference.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs index 5bbde1e1444..57f7fac1962 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs @@ -183,6 +183,13 @@ public string Uri [JsonIgnore] public string MediaType { get; } + /// Gets or sets an optional name associated with the data. + /// + /// A service might use this name as part of citations or to help infer the type of data + /// being represented based on a file extension. + /// + public string? Name { get; set; } + /// Gets the data represented by this instance. /// /// If the instance was constructed from a , this property returns that data. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index b47765269af..4d190fccda8 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1349,6 +1349,10 @@ "Member": "System.ReadOnlyMemory Microsoft.Extensions.AI.DataContent.Data { get; }", "Stage": "Stable" }, + { + "Member": "string? Microsoft.Extensions.AI.DataContent.Name { get; set; }", + "Stage": "Stable" + }, { "Member": "string Microsoft.Extensions.AI.DataContent.MediaType { get; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 394fccad1b6..52dfc3dc8af 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -264,7 +264,7 @@ private static List ToOpenAIChatContent(IList break; case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase): - return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"); + return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, dataContent.Name ?? $"{Guid.NewGuid():N}.pdf"); case AIContent when content.RawRepresentation is ChatMessageContentPart rawContentPart: return rawContentPart; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index c4a1261844c..57ed46721b0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -639,7 +639,7 @@ private static List ToOpenAIResponsesContent(IList([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"), + JsonSerializer.Serialize( + new DataContent(new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream"), + TestJsonSerializerContext.Default.Options)); + + Assert.Equal( + """{"uri":"data:application/octet-stream;base64,AQIDBA==","name":"test.bin"}""", + JsonSerializer.Serialize( + new DataContent(new ReadOnlyMemory([0x01, 0x02, 0x03, 0x04]), "application/octet-stream") { Name = "test.bin" }, TestJsonSerializerContext.Default.Options)); } @@ -260,4 +267,13 @@ public void NonBase64Data_Normalized() Assert.Equal("aGVsbG8gd29ybGQ=", content.Base64Data.ToString()); Assert.Equal("hello world", Encoding.ASCII.GetString(content.Data.ToArray())); } + + [Fact] + public void FileName_Roundtrips() + { + DataContent content = new(new byte[] { 1, 2, 3 }, "application/octet-stream"); + Assert.Null(content.Name); + content.Name = "test.bin"; + Assert.Equal("test.bin", content.Name); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index ffa94f64531..c9f0e09abf3 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -202,7 +202,7 @@ public virtual async Task MultiModal_DescribePdf() new(ChatRole.User, [ new TextContent("What text does this document contain?"), - new DataContent(ImageDataUri.GetPdfDataUri(), "application/pdf"), + new DataContent(ImageDataUri.GetPdfDataUri(), "application/pdf") { Name = "sample.pdf" }, ]) ], new() { ModelId = GetModel_MultiModal_DescribeImage() });