-
Notifications
You must be signed in to change notification settings - Fork 854
Add IHostedFileClient and friends #7269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="HostedFile"/> class. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
HostedFileInfoor 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.