Skip to content
Open
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
@@ -0,0 +1,102 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// A delegating file client that wraps an inner <see cref="IHostedFileClient"/>.
/// </summary>
/// <remarks>
/// This class provides a base for creating file clients that modify or enhance the behavior
/// of another <see cref="IHostedFileClient"/>. By default, all methods delegate to the inner client.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIFiles, UrlFormat = DiagnosticIds.UrlFormat)]
public class DelegatingHostedFileClient : IHostedFileClient
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingHostedFileClient"/> class.
/// </summary>
/// <param name="innerClient">The inner client to delegate to.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerClient"/> is <see langword="null"/>.</exception>
protected DelegatingHostedFileClient(IHostedFileClient innerClient)
{
InnerClient = Throw.IfNull(innerClient);
}

/// <summary>Gets the inner <see cref="IHostedFileClient"/>.</summary>
protected IHostedFileClient InnerClient { get; }

/// <inheritdoc />
public virtual Task<HostedFile> UploadAsync(
Stream content,
string? mediaType = null,
string? fileName = null,
HostedFileUploadOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.UploadAsync(content, mediaType, fileName, options, cancellationToken);

/// <inheritdoc />
public virtual Task<HostedFileDownloadStream> DownloadAsync(
string fileId,
HostedFileDownloadOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.DownloadAsync(fileId, options, cancellationToken);

/// <inheritdoc />
public virtual Task<HostedFile?> GetFileInfoAsync(
string fileId,
HostedFileGetOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.GetFileInfoAsync(fileId, options, cancellationToken);

/// <inheritdoc />
public virtual IAsyncEnumerable<HostedFile> ListFilesAsync(
HostedFileListOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.ListFilesAsync(options, cancellationToken);

/// <inheritdoc />
public virtual Task<bool> DeleteAsync(
string fileId,
HostedFileDeleteOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.DeleteAsync(fileId, options, cancellationToken);

/// <inheritdoc />
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);

// If the key is non-null, we don't know what it means so pass through to the inner service.
return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
InnerClient.GetService(serviceType, serviceKey);
}

/// <inheritdoc />
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Disposes the instance.
/// </summary>
/// <param name="disposing">
/// <see langword="true"/> if being called from <see cref="Dispose()"/>; otherwise, <see langword="false"/>.
/// </param>
protected virtual void Dispose(bool disposing)
{
// By default, do not dispose the inner client, as it may be shared.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents metadata about a file hosted by an AI service.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[Experimental(DiagnosticIds.Experiments.AIFiles, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class HostedFile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't actually have the file content, consider a name like HostedFileInfo or consider API that lets folks get the content from the file, presumably such API would also require the file to maintain a reference to the client that it's associated with.

{
/// <summary>
/// Initializes a new instance of the <see cref="HostedFile"/> class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In it's current form it looks like we only ever expect providers to instantiate this. I wonder why this is different than HostedFileContent?

/// </summary>
/// <param name="id">The unique identifier of the file.</param>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
public HostedFile(string id)
{
Id = Throw.IfNullOrWhitespace(id);
}

/// <summary>Gets the unique identifier of the file.</summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call out the scope of uniqueness so that providers implement correctly?

public string Id { get; }

/// <summary>Gets or sets the name of the file.</summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this contain path information? If so, what rules are applied to those paths -- presumably they need to be platform agnostic. It may be important for folks wanting to save/upload using platform-specific assumptions.

public string? Name { get; set; }

/// <summary>Gets or sets the media type (MIME type) of the file.</summary>
public string? MediaType { get; set; }

/// <summary>Gets or sets the size of the file in bytes.</summary>
public long? SizeInBytes { get; set; }

/// <summary>Gets or sets when the file was created.</summary>
public DateTimeOffset? CreatedAt { get; set; }

/// <summary>Gets or sets the purpose for which the file was uploaded.</summary>
/// <remarks>
/// Common values include "assistants", "fine-tune", "batch", or "vision",
/// but the specific values supported depend on the provider.
/// </remarks>
public string? Purpose { get; set; }

/// <summary>Gets or sets additional properties associated with the file.</summary>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }

/// <summary>Gets or sets the raw representation of the file from the underlying provider.</summary>
/// <remarks>
/// If the <see cref="HostedFile"/> was created from an underlying provider's response,
/// this property contains the original response object.
/// </remarks>
public object? RawRepresentation { get; set; }

/// <summary>
/// Creates a <see cref="HostedFileContent"/> that references this file.
/// </summary>
/// <returns>A new <see cref="HostedFileContent"/> instance referencing this file.</returns>
public HostedFileContent ToHostedFileContent() =>
new(Id)
{
Name = Name,
MediaType = MediaType
};

/// <summary>Gets a string representing this instance to display in the debugger.</summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay
{
get
{
string display = $"Id = {Id}";

if (Name is not null)
{
display += $", Name = \"{Name}\"";
}

if (MediaType is not null)
{
display += $", MediaType = {MediaType}";
}

if (SizeInBytes is not null)
{
display += $", Size = {SizeInBytes} bytes";
}

return display;
}
}
}
Loading
Loading