diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIClientContainerFileExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIClientContainerFileExtensions.cs new file mode 100644 index 0000000000..192bdcb379 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIClientContainerFileExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace OpenAI; + +/// +/// Provides extension methods for to download files from OpenAI containers. +/// +/// +/// Container files are generated by tools such as Code Interpreter and have IDs prefixed with +/// cfile_. These files reside within a specific container (identified by a cntr_ +/// prefixed ID) and cannot be downloaded using the standard Files API +/// (). Use these methods to download container-scoped +/// files through the Containers API instead. +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static class OpenAIClientContainerFileExtensions +{ + /// + /// Downloads a file from an OpenAI container. + /// + /// The to use for the download. + /// The ID of the container (e.g., cntr_...) that holds the file. + /// The ID of the file to download (e.g., cfile_...). + /// The to monitor for cancellation requests. The default is . + /// A containing the downloaded file data as . + /// Thrown when , , or is . + /// Thrown when or is empty or consists only of whitespace characters. + public static async Task> DownloadContainerFileAsync( + this OpenAIClient client, + string containerId, + string fileId, + CancellationToken cancellationToken = default) + { + Throw.IfNull(client); + Throw.IfNullOrWhitespace(containerId); + Throw.IfNullOrWhitespace(fileId); + + var containerClient = client.GetContainerClient(); + return await containerClient.DownloadContainerFileAsync(containerId, fileId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIClientContainerFileExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIClientContainerFileExtensionsTests.cs new file mode 100644 index 0000000000..0a6e6500a5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIClientContainerFileExtensionsTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using OpenAI; + +namespace Microsoft.Agents.AI.OpenAI.UnitTests.Extensions; + +/// +/// Unit tests for the class. +/// +public sealed class OpenAIClientContainerFileExtensionsTests +{ + /// + /// Verify that DownloadContainerFileAsync throws ArgumentNullException when client is null. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithNullClient_ThrowsArgumentNullExceptionAsync() + { + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => ((OpenAIClient)null!).DownloadContainerFileAsync("cntr_123", "cfile_456")); + + Assert.Equal("client", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentNullException when containerId is null. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithNullContainerId_ThrowsArgumentNullExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync(null!, "cfile_456")); + + Assert.Equal("containerId", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentException when containerId is empty. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithEmptyContainerId_ThrowsArgumentExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync("", "cfile_456")); + + Assert.Equal("containerId", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentException when containerId is whitespace. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithWhitespaceContainerId_ThrowsArgumentExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync(" ", "cfile_456")); + + Assert.Equal("containerId", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentNullException when fileId is null. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithNullFileId_ThrowsArgumentNullExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync("cntr_123", null!)); + + Assert.Equal("fileId", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentException when fileId is empty. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithEmptyFileId_ThrowsArgumentExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync("cntr_123", "")); + + Assert.Equal("fileId", exception.ParamName); + } + + /// + /// Verify that DownloadContainerFileAsync throws ArgumentException when fileId is whitespace. + /// + [Fact] + public async Task DownloadContainerFileAsync_WithWhitespaceFileId_ThrowsArgumentExceptionAsync() + { + // Arrange + var client = new TestOpenAIClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.DownloadContainerFileAsync("cntr_123", " ")); + + Assert.Equal("fileId", exception.ParamName); + } + + /// + /// A test subclass for unit testing without requiring an API key. + /// + private sealed class TestOpenAIClient : OpenAIClient + { + public TestOpenAIClient() + { + } + } +}