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 @@ -42,7 +42,25 @@ public static Task<ChatCompletion> CompleteAsync(
_ = Throw.IfNull(client);
_ = Throw.IfNull(chatMessage);

return client.CompleteAsync([new ChatMessage(ChatRole.User, chatMessage)], options, cancellationToken);
return client.CompleteAsync(new ChatMessage(ChatRole.User, chatMessage), options, cancellationToken);
}

/// <summary>Sends a chat message to the model and returns the response messages.</summary>
/// <param name="client">The chat client.</param>
/// <param name="chatMessage">The chat message to send.</param>
/// <param name="options">The chat options to configure the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The response messages generated by the client.</returns>
public static Task<ChatCompletion> CompleteAsync(
this IChatClient client,
ChatMessage chatMessage,
Copy link
Member

@eiriktsarpalis eiriktsarpalis Feb 10, 2025

Choose a reason for hiding this comment

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

This new set of extensions can only be used in cases where the chat history consists of only that one message. How common is such a scenario? Could it lead callers into incorrectly assuming that they're appending a chat message to a log that is being encapsulated by the client?

Copy link
Member Author

Choose a reason for hiding this comment

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

This new set of extensions can only be used in cases where the chat history consists of only that one message. How common is such a scenario?

It's pretty common, for at least:

  • Getting started scenarios, where you want to send a single input
  • Non-chat scenarios, where you're sending input to have the LLM give you back a response as part of the logic of you're program.
  • IChatClient implementations where the state is stored server-side, ala assistants

It's no different from the existing string-based overloads that are helpful in similar circumstances but that are limited to TextContent; these overloads allow you to customize beyond that.

ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(client);
_ = Throw.IfNull(chatMessage);

return client.CompleteAsync([chatMessage], options, cancellationToken);
}

/// <summary>Sends a user chat text message to the model and streams the response messages.</summary>
Expand All @@ -60,6 +78,24 @@ public static IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingA
_ = Throw.IfNull(client);
_ = Throw.IfNull(chatMessage);

return client.CompleteStreamingAsync([new ChatMessage(ChatRole.User, chatMessage)], options, cancellationToken);
return client.CompleteStreamingAsync(new ChatMessage(ChatRole.User, chatMessage), options, cancellationToken);
}

/// <summary>Sends a chat message to the model and streams the response messages.</summary>
/// <param name="client">The chat client.</param>
/// <param name="chatMessage">The chat message to send.</param>
/// <param name="options">The chat options to configure the request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The response messages generated by the client.</returns>
public static IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
this IChatClient client,
ChatMessage chatMessage,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(client);
_ = Throw.IfNull(chatMessage);

return client.CompleteStreamingAsync([chatMessage], options, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public class ChatOptions
public IList<string>? StopSequences { get; set; }

/// <summary>Gets or sets the tool mode for the chat request.</summary>
public ChatToolMode ToolMode { get; set; } = ChatToolMode.Auto;
/// <remarks>The default value is <see langword="null"/>, which is treated the same as <see cref="ChatToolMode.Auto"/>.</remarks>
public ChatToolMode? ToolMode { get; set; }

/// <summary>Gets or sets the list of tools to include with a chat request.</summary>
[JsonIgnore]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ namespace Microsoft.Extensions.AI;
/// Describes how tools should be selected by a <see cref="IChatClient"/>.
/// </summary>
/// <remarks>
/// The predefined values <see cref="Auto" /> and <see cref="RequireAny"/> are provided.
/// The predefined values <see cref="Auto" />, <see cref="None"/>, and <see cref="RequireAny"/> are provided.
/// To nominate a specific function, use <see cref="RequireSpecific(string)"/>.
/// </remarks>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(NoneChatToolMode), typeDiscriminator: "none")]
[JsonDerivedType(typeof(AutoChatToolMode), typeDiscriminator: "auto")]
[JsonDerivedType(typeof(RequiredChatToolMode), typeDiscriminator: "required")]
#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
Expand All @@ -32,7 +33,19 @@ private protected ChatToolMode()
/// <see cref="ChatOptions.Tools"/> can contain zero or more <see cref="AITool"/>
/// instances, and the <see cref="IChatClient"/> is free to invoke zero or more of them.
/// </remarks>
public static AutoChatToolMode Auto { get; } = new AutoChatToolMode();
public static AutoChatToolMode Auto { get; } = new();

/// <summary>
/// Gets a predefined <see cref="ChatToolMode"/> indicating that tool usage is unsupported.
/// </summary>
/// <remarks>
/// <see cref="ChatOptions.Tools"/> can contain zero or more <see cref="AITool"/>
/// instances, but the <see cref="IChatClient"/> should not request the invocation of
/// any of them. This can be used when the <see cref="IChatClient"/> should know about
/// tools in order to provide information about them or plan out their usage, but should
/// not request the invocation of any of them.
/// </remarks>
public static NoneChatToolMode None { get; } = new();

/// <summary>
/// Gets a predefined <see cref="ChatToolMode"/> indicating that tool usage is required,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,6 @@ public void Dispose()
/// <summary>Gets the inner <see cref="IChatClient" />.</summary>
protected IChatClient InnerClient { get; }

/// <summary>Provides a mechanism for releasing unmanaged resources.</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)
{
if (disposing)
{
InnerClient.Dispose();
}
}

/// <inheritdoc />
public virtual ChatClientMetadata Metadata => InnerClient.Metadata;

/// <inheritdoc />
public virtual Task<ChatCompletion> CompleteAsync(IList<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
Expand All @@ -72,4 +59,14 @@ public virtual IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreaming
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
InnerClient.GetService(serviceType, serviceKey);
}

/// <summary>Provides a mechanism for releasing unmanaged resources.</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)
{
if (disposing)
{
InnerClient.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
ChatOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>Gets metadata that describes the <see cref="IChatClient"/>.</summary>
ChatClientMetadata Metadata { get; }

/// <summary>Asks the <see cref="IChatClient"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Indicates that an <see cref="IChatClient"/> should not request the invocation of any tools.
/// </summary>
/// <remarks>
/// Use <see cref="ChatToolMode.None"/> to get an instance of <see cref="NoneChatToolMode"/>.
/// </remarks>
[DebuggerDisplay("None")]
public sealed class NoneChatToolMode : ChatToolMode
{
/// <summary>Initializes a new instance of the <see cref="NoneChatToolMode"/> class.</summary>
/// <remarks>Use <see cref="ChatToolMode.None"/> to get an instance of <see cref="NoneChatToolMode"/>.</remarks>
public NoneChatToolMode()
{
} // must exist in support of polymorphic deserialization of a ChatToolMode

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is NoneChatToolMode;

/// <inheritdoc/>
public override int GetHashCode() => typeof(NoneChatToolMode).GetHashCode();
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,13 @@ public string Uri
[JsonPropertyOrder(1)]
public string? MediaType { get; private set; }

/// <summary>
/// Gets a value indicating whether the content contains data rather than only being a reference to data.
/// </summary>
/// <remarks>
/// If the instance is constructed from a <see cref="ReadOnlyMemory{Byte}"/> or from a data URI, this property returns <see langword="true"/>,
/// as the instance actually contains all of the data it represents. If, however, the instance was constructed from another form of URI, one
/// that simply references where the data can be found but doesn't actually contain the data, this property returns <see langword="false"/>.
/// </remarks>
[MemberNotNullWhen(true, nameof(Data))]
[JsonIgnore]
public bool ContainsData => _dataUri is not null || _data is not null;

/// <summary>Gets the data represented by this instance.</summary>
/// <remarks>
/// If <see cref="ContainsData"/> is <see langword="true" />, this property returns the represented data.
/// If <see cref="ContainsData"/> is <see langword="false" />, this property returns <see langword="null" />.
/// If the instance was constructed from a <see cref="ReadOnlyMemory{Byte}"/>, this property returns that data.
/// If the instance was constructed from a data URI, this property the data contained within the data URI.
/// If, however, the instance was constructed from another form of URI, one that simply references where the
/// data can be found but doesn't actually contain the data, this property returns <see langword="null"/>;
/// no attempt is made to retrieve the data from that URI.
/// </remarks>
[JsonIgnore]
public ReadOnlyMemory<byte>? Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ public sealed class FunctionCallContent : AIContent
[JsonConstructor]
public FunctionCallContent(string callId, string name, IDictionary<string, object?>? arguments = null)
{
CallId = Throw.IfNull(callId);
Name = Throw.IfNull(name);
CallId = callId;
Arguments = arguments;
}

/// <summary>
/// Gets or sets the function call ID.
/// Gets the function call ID.
/// </summary>
public string CallId { get; set; }
public string CallId { get; }

/// <summary>
/// Gets or sets the name of the function requested.
/// Gets the name of the function requested.
/// </summary>
public string Name { get; set; }
public string Name { get; }

/// <summary>
/// Gets or sets the arguments requested to be provided to the function.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,26 @@ public sealed class FunctionResultContent : AIContent
/// Initializes a new instance of the <see cref="FunctionResultContent"/> class.
/// </summary>
/// <param name="callId">The function call ID for which this is the result.</param>
/// <param name="name">The function name that produced the result.</param>
/// <param name="result">
/// <see langword="null"/> if the function returned <see langword="null"/> or was void-returning
/// and thus had no result, or if the function call failed. Typically, however, to provide meaningfully representative
/// information to an AI service, a human-readable representation of those conditions should be supplied.
/// </param>
[JsonConstructor]
public FunctionResultContent(string callId, string name, object? result)
public FunctionResultContent(string callId, object? result)
{
CallId = Throw.IfNull(callId);
Name = Throw.IfNull(name);
Result = result;
}

/// <summary>
/// Gets or sets the ID of the function call for which this is the result.
/// Gets the ID of the function call for which this is the result.
/// </summary>
/// <remarks>
/// If this is the result for a <see cref="FunctionCallContent"/>, this property should contain the same
/// <see cref="FunctionCallContent.CallId"/> value.
/// </remarks>
public string CallId { get; set; }

/// <summary>
/// Gets or sets the name of the function that was called.
/// </summary>
public string Name { get; set; }
public string CallId { get; }

/// <summary>
/// Gets or sets the result of the function call, or a generic error message if the function call failed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,6 @@ public void Dispose()
GC.SuppressFinalize(this);
}

/// <summary>Provides a mechanism for releasing unmanaged resources.</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)
{
if (disposing)
{
InnerGenerator.Dispose();
}
}

/// <inheritdoc />
public virtual EmbeddingGeneratorMetadata Metadata =>
InnerGenerator.Metadata;

/// <inheritdoc />
public virtual Task<GeneratedEmbeddings<TEmbedding>> GenerateAsync(IEnumerable<TInput> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) =>
InnerGenerator.GenerateAsync(values, options, cancellationToken);
Expand All @@ -68,4 +54,14 @@ public virtual Task<GeneratedEmbeddings<TEmbedding>> GenerateAsync(IEnumerable<T
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
InnerGenerator.GetService(serviceType, serviceKey);
}

/// <summary>Provides a mechanism for releasing unmanaged resources.</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)
{
if (disposing)
{
InnerGenerator.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static class EmbeddingGeneratorExtensions

/// <summary>Generates an embedding vector from the specified <paramref name="value"/>.</summary>
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
/// <typeparam name="TEmbedding">The numeric type of the embedding data.</typeparam>
/// <typeparam name="TEmbeddingElement">The numeric type of the embedding data.</typeparam>
/// <param name="generator">The embedding generator.</param>
/// <param name="value">A value from which an embedding will be generated.</param>
/// <param name="options">The embedding generation options to configure the request.</param>
Expand All @@ -64,8 +64,8 @@ public static class EmbeddingGeneratorExtensions
/// This operation is equivalent to using <see cref="GenerateEmbeddingAsync"/> and returning the
/// resulting <see cref="Embedding{T}"/>'s <see cref="Embedding{T}.Vector"/> property.
/// </remarks>
public static async Task<ReadOnlyMemory<TEmbedding>> GenerateEmbeddingVectorAsync<TInput, TEmbedding>(
this IEmbeddingGenerator<TInput, Embedding<TEmbedding>> generator,
public static async Task<ReadOnlyMemory<TEmbeddingElement>> GenerateEmbeddingVectorAsync<TInput, TEmbeddingElement>(
this IEmbeddingGenerator<TInput, Embedding<TEmbeddingElement>> generator,
TInput value,
EmbeddingGenerationOptions? options = null,
CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ Task<GeneratedEmbeddings<TEmbedding>> GenerateAsync(
EmbeddingGenerationOptions? options = null,
CancellationToken cancellationToken = default);

/// <summary>Gets metadata that describes the <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>.</summary>
EmbeddingGeneratorMetadata Metadata { get; }

/// <summary>Asks the <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
Expand Down
26 changes: 16 additions & 10 deletions src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ using Microsoft.Extensions.AI;

public class SampleChatClient : IChatClient
{
public ChatClientMetadata Metadata { get; }
private readonly ChatClientMetadata _metadata;

public SampleChatClient(Uri endpoint, string modelId) =>
Metadata = new("SampleChatClient", endpoint, modelId);
_metadata = new("SampleChatClient", endpoint, modelId);

public async Task<ChatCompletion> CompleteAsync(
IList<ChatMessage> chatMessages,
Expand All @@ -61,11 +61,11 @@ public class SampleChatClient : IChatClient
"This is yet another response message."
];

return new([new ChatMessage()
return new(new ChatMessage()
{
Role = ChatRole.Assistant,
Text = responses[Random.Shared.Next(responses.Length)],
}]);
});
}

public async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
Expand All @@ -89,9 +89,12 @@ public class SampleChatClient : IChatClient
}
}

public TService? GetService<TService>(object? key = null) where TService : class =>
this as TService;

object? IChatClient.GetService(Type serviceType, object? serviceKey = null) =>
serviceKey is not null ? null :
serviceType == typeof(ChatClientMetadata) ? _metadata :
serviceType?.IsInstanceOfType(this) is true ? this :
null;

void IDisposable.Dispose() { }
}
```
Expand Down Expand Up @@ -446,7 +449,7 @@ using Microsoft.Extensions.AI;

public class SampleEmbeddingGenerator(Uri endpoint, string modelId) : IEmbeddingGenerator<string, Embedding<float>>
{
public EmbeddingGeneratorMetadata Metadata { get; } = new("SampleEmbeddingGenerator", endpoint, modelId);
private readonly EmbeddingGeneratorMetadata _metadata = new("SampleEmbeddingGenerator", endpoint, modelId);

public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
IEnumerable<string> values,
Expand All @@ -463,8 +466,11 @@ public class SampleEmbeddingGenerator(Uri endpoint, string modelId) : IEmbedding
Enumerable.Range(0, 384).Select(_ => Random.Shared.NextSingle()).ToArray()));
}

public TService? GetService<TService>(object? key = null) where TService : class =>
this as TService;
object? IChatClient.GetService(Type serviceType, object? serviceKey = null) =>
serviceKey is not null ? null :
serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata :
serviceType?.IsInstanceOfType(this) is true ? this :
null;

void IDisposable.Dispose() { }
}
Expand Down
Loading