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 @@ -9,8 +9,8 @@ namespace Microsoft.Extensions.AI;
/// <summary>Represents the options for a chat request.</summary>
public class ChatOptions
{
/// <summary>Gets or sets an optional identifier used to associate a request with an existing chat thread.</summary>
public string? ChatThreadId { get; set; }
/// <summary>Gets or sets an optional identifier used to associate a request with an existing conversation.</summary>
public string? ConversationId { get; set; }

/// <summary>Gets or sets the temperature for generating chat responses.</summary>
/// <remarks>
Expand Down Expand Up @@ -105,7 +105,7 @@ public virtual ChatOptions Clone()
{
ChatOptions options = new()
{
ChatThreadId = ChatThreadId,
ConversationId = ConversationId,
Temperature = Temperature,
MaxOutputTokens = MaxOutputTokens,
TopP = TopP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,17 @@ public IList<ChatMessage> Messages
/// <summary>Gets or sets the ID of the chat response.</summary>
public string? ResponseId { get; set; }

/// <summary>Gets or sets the chat thread ID associated with this chat response.</summary>
/// <summary>Gets or sets an identifier for the state of the conversation.</summary>
/// <remarks>
/// Some <see cref="IChatClient"/> implementations are capable of storing the state for a chat thread, such that
/// Some <see cref="IChatClient"/> implementations are capable of storing the state for a conversation, such that
/// the input messages supplied to <see cref="IChatClient.GetResponseAsync"/> need only be the additional messages beyond
/// what's already stored. If this property is non-<see langword="null"/>, it represents an identifier for that state,
/// and it should be used in a subsequent <see cref="ChatOptions.ChatThreadId"/> instead of supplying the same messages
/// (and this <see cref="ChatResponse"/>'s message) as part of the <c>messages</c> parameter.
/// and it should be used in a subsequent <see cref="ChatOptions.ConversationId"/> instead of supplying the same messages
/// (and this <see cref="ChatResponse"/>'s message) as part of the <c>messages</c> parameter. Note that the value may
/// or may not differ on every response, depending on whether the underlying provider uses a fixed ID for each conversation
/// or updates it for each message.
/// </remarks>
public string? ChatThreadId { get; set; }
public string? ConversationId { get; set; }

/// <summary>Gets or sets the model ID used in the creation of the chat response.</summary>
public string? ModelId { get; set; }
Expand Down Expand Up @@ -127,7 +129,7 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
ChatMessage message = _messages![i];
updates[i] = new ChatResponseUpdate
{
ChatThreadId = ChatThreadId,
ConversationId = ConversationId,

AdditionalProperties = message.AdditionalProperties,
AuthorName = message.AuthorName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
response.ResponseId = update.ResponseId;
}

if (update.ChatThreadId is not null)
if (update.ConversationId is not null)
{
response.ChatThreadId = update.ChatThreadId;
response.ConversationId = update.ConversationId;
}

if (update.CreatedAt is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,16 @@ public IList<AIContent> Contents
/// </remarks>
public string? MessageId { get; set; }

/// <summary>Gets or sets the chat thread ID associated with the chat response of which this update is a part.</summary>
/// <summary>Gets or sets an identifier for the state of the conversation of which this update is a part.</summary>
/// <remarks>
/// Some <see cref="IChatClient"/> implementations are capable of storing the state for a chat thread, such that
/// Some <see cref="IChatClient"/> implementations are capable of storing the state for a conversation, such that
/// the input messages supplied to <see cref="IChatClient.GetStreamingResponseAsync"/> need only be the additional messages beyond
/// what's already stored. If this property is non-<see langword="null"/>, it represents an identifier for that state,
/// and it should be used in a subsequent <see cref="ChatOptions.ChatThreadId"/> instead of supplying the same messages
/// (and this streaming message) as part of the <c>messages</c> parameter.
/// and it should be used in a subsequent <see cref="ChatOptions.ConversationId"/> instead of supplying the same messages
/// (and this streaming message) as part of the <c>messages</c> parameter. Note that the value may or may not differ on every
/// response, depending on whether the underlying provider uses a fixed ID for each conversation or updates it for each message.
/// </remarks>
public string? ChatThreadId { get; set; }
public string? ConversationId { get; set; }

/// <summary>Gets or sets a timestamp for the response update.</summary>
public DateTimeOffset? CreatedAt { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public async Task<ChatResponse> GetResponseAsync(
ChatResponse response = new()
{
ResponseId = openAIResponse.Id,
ConversationId = openAIResponse.Id,
CreatedAt = openAIResponse.CreatedAt,
FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason),
Messages = [new(ChatRole.Assistant, [])],
Expand Down Expand Up @@ -176,6 +177,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
Contents = ToUsageDetails(completedUpdate.Response) is { } usage ? [new UsageContent(usage)] : [],
CreatedAt = createdAt,
ResponseId = responseId,
ConversationId = responseId,
FinishReason =
ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ??
(functionCallInfos is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop),
Expand Down Expand Up @@ -213,6 +215,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
MessageId = lastMessageId,
ModelId = modelId,
ResponseId = responseId,
ConversationId = responseId,
};
break;

Expand Down Expand Up @@ -246,6 +249,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
MessageId = lastMessageId,
ModelId = modelId,
ResponseId = responseId,
ConversationId = responseId,
};
}

Expand All @@ -259,6 +263,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
MessageId = lastMessageId,
ModelId = modelId,
ResponseId = responseId,
ConversationId = responseId,
Contents =
[
new ErrorContent(errorUpdate.Message)
Expand Down Expand Up @@ -304,7 +309,7 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio
{
// Handle strongly-typed properties.
result.MaxOutputTokenCount = options.MaxOutputTokens;
result.PreviousResponseId = options.ChatThreadId;
result.PreviousResponseId = options.ConversationId;
result.TopP = options.TopP;
result.Temperature = options.Temperature;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
if (await ReadCacheStreamingAsync(cacheKey, cancellationToken) is { } existingChunks)
{
// Yield all of the cached items.
string? chatThreadId = null;
string? conversationId = null;
foreach (var chunk in existingChunks)
{
chatThreadId ??= chunk.ChatThreadId;
conversationId ??= chunk.ConversationId;
yield return chunk;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public ChatResponse(ChatResponse response, JsonSerializerOptions serializerOptio
{
_serializerOptions = Throw.IfNull(serializerOptions);
AdditionalProperties = response.AdditionalProperties;
ChatThreadId = response.ChatThreadId;
ConversationId = response.ConversationId;
CreatedAt = response.CreatedAt;
FinishReason = response.FinishReason;
ModelId = response.ModelId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public override async Task<ChatResponse> GetResponseAsync(
List<ChatMessage>? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response
UsageDetails? totalUsage = null; // tracked usage across all turns, to be used for the final response
List<FunctionCallContent>? functionCallContents = null; // function call contents that need responding to in the current turn
bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
bool lastIterationHadConversationId = false; // whether the last iteration's response had a ConversationId set
int consecutiveErrorCount = 0;

for (int iteration = 0; ; iteration++)
Expand Down Expand Up @@ -274,7 +274,7 @@ public override async Task<ChatResponse> GetResponseAsync(
}

// Prepare the history for the next iteration.
FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadConversationId);

// Add the responses from the function calls into the augmented history and also into the tracked
// list of response messages.
Expand All @@ -287,7 +287,7 @@ public override async Task<ChatResponse> GetResponseAsync(
break;
}

UpdateOptionsForNextIteration(ref options!, response.ChatThreadId);
UpdateOptionsForNextIteration(ref options!, response.ConversationId);
}

Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages.");
Expand Down Expand Up @@ -318,7 +318,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
List<ChatMessage>? augmentedHistory = null; // the actual history of messages sent on turns other than the first
List<FunctionCallContent>? functionCallContents = null; // function call contents that need responding to in the current turn
List<ChatMessage>? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
bool lastIterationHadConversationId = false; // whether the last iteration's response had a ConversationId set
List<ChatResponseUpdate> updates = []; // updates from the current response
int consecutiveErrorCount = 0;

Expand Down Expand Up @@ -368,7 +368,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
(responseMessages ??= []).AddRange(response.Messages);

// Prepare the history for the next iteration.
FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadConversationId);

// Process all of the functions, adding their results into the history.
var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, cancellationToken);
Expand All @@ -390,7 +390,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
{
AdditionalProperties = message.AdditionalProperties,
AuthorName = message.AuthorName,
ChatThreadId = response.ChatThreadId,
ConversationId = response.ConversationId,
CreatedAt = DateTimeOffset.UtcNow,
Contents = message.Contents,
RawRepresentation = message.RawRepresentation,
Expand All @@ -408,7 +408,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
break;
}

UpdateOptionsForNextIteration(ref options, response.ChatThreadId);
UpdateOptionsForNextIteration(ref options, response.ConversationId);
}

AddUsageTags(activity, totalUsage);
Expand Down Expand Up @@ -437,18 +437,18 @@ private static void AddUsageTags(Activity? activity, UsageDetails? usage)
/// <param name="augmentedHistory">The augmented history containing all the messages to be sent.</param>
/// <param name="response">The most recent response being handled.</param>
/// <param name="allTurnsResponseMessages">A list of all response messages received up until this point.</param>
/// <param name="lastIterationHadThreadId">Whether the previous iteration's response had a thread id.</param>
/// <param name="lastIterationHadConversationId">Whether the previous iteration's response had a conversation id.</param>
private static void FixupHistories(
IEnumerable<ChatMessage> originalMessages,
ref IEnumerable<ChatMessage> messages,
[NotNull] ref List<ChatMessage>? augmentedHistory,
ChatResponse response,
List<ChatMessage> allTurnsResponseMessages,
ref bool lastIterationHadThreadId)
ref bool lastIterationHadConversationId)
{
// We're now going to need to augment the history with function result contents.
// That means we need a separate list to store the augmented history.
if (response.ChatThreadId is not null)
if (response.ConversationId is not null)
{
// The response indicates the inner client is tracking the history, so we don't want to send
// anything we've already sent or received.
Expand All @@ -461,9 +461,9 @@ private static void FixupHistories(
augmentedHistory = [];
}

lastIterationHadThreadId = true;
lastIterationHadConversationId = true;
}
else if (lastIterationHadThreadId)
else if (lastIterationHadConversationId)
{
// In the very rare case where the inner client returned a response with a thread ID but then
// returned a subsequent response without one, we want to reconstitue the full history. To do that,
Expand All @@ -474,7 +474,7 @@ private static void FixupHistories(
augmentedHistory.AddRange(originalMessages);
augmentedHistory.AddRange(allTurnsResponseMessages);

lastIterationHadThreadId = false;
lastIterationHadConversationId = false;
}
else
{
Expand All @@ -486,7 +486,7 @@ private static void FixupHistories(
// Now add the most recent response messages.
augmentedHistory.AddMessages(response);

lastIterationHadThreadId = false;
lastIterationHadConversationId = false;
}

// Use the augmented history as the new set of messages to send.
Expand Down Expand Up @@ -525,22 +525,22 @@ private static bool CopyFunctionCalls(
return any;
}

private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? chatThreadId)
private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? conversationId)
{
if (options.ToolMode is RequiredChatToolMode)
{
// We have to reset the tool mode to be non-required after the first iteration,
// as otherwise we'll be in an infinite loop.
options = options.Clone();
options.ToolMode = null;
options.ChatThreadId = chatThreadId;
options.ConversationId = conversationId;
}
else if (options.ChatThreadId != chatThreadId)
else if (options.ConversationId != conversationId)
{
// As with the other modes, ensure we've propagated the chat thread ID to the options.
// We only need to clone the options if we're actually mutating it.
options = options.Clone();
options.ChatThreadId = chatThreadId;
options.ConversationId = conversationId;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ChatOptionsTests
public void Constructor_Parameterless_PropsDefaulted()
{
ChatOptions options = new();
Assert.Null(options.ChatThreadId);
Assert.Null(options.ConversationId);
Assert.Null(options.Temperature);
Assert.Null(options.MaxOutputTokens);
Assert.Null(options.TopP);
Expand All @@ -29,7 +29,7 @@ public void Constructor_Parameterless_PropsDefaulted()
Assert.Null(options.AdditionalProperties);

ChatOptions clone = options.Clone();
Assert.Null(options.ChatThreadId);
Assert.Null(options.ConversationId);
Assert.Null(clone.Temperature);
Assert.Null(clone.MaxOutputTokens);
Assert.Null(clone.TopP);
Expand Down Expand Up @@ -67,7 +67,7 @@ public void Properties_Roundtrip()
["key"] = "value",
};

options.ChatThreadId = "12345";
options.ConversationId = "12345";
options.Temperature = 0.1f;
options.MaxOutputTokens = 2;
options.TopP = 0.3f;
Expand All @@ -82,7 +82,7 @@ public void Properties_Roundtrip()
options.Tools = tools;
options.AdditionalProperties = additionalProps;

Assert.Equal("12345", options.ChatThreadId);
Assert.Equal("12345", options.ConversationId);
Assert.Equal(0.1f, options.Temperature);
Assert.Equal(2, options.MaxOutputTokens);
Assert.Equal(0.3f, options.TopP);
Expand All @@ -98,7 +98,7 @@ public void Properties_Roundtrip()
Assert.Same(additionalProps, options.AdditionalProperties);

ChatOptions clone = options.Clone();
Assert.Equal("12345", options.ChatThreadId);
Assert.Equal("12345", options.ConversationId);
Assert.Equal(0.1f, clone.Temperature);
Assert.Equal(2, clone.MaxOutputTokens);
Assert.Equal(0.3f, clone.TopP);
Expand Down Expand Up @@ -130,7 +130,7 @@ public void JsonSerialization_Roundtrips()
["key"] = "value",
};

options.ChatThreadId = "12345";
options.ConversationId = "12345";
options.Temperature = 0.1f;
options.MaxOutputTokens = 2;
options.TopP = 0.3f;
Expand All @@ -154,7 +154,7 @@ public void JsonSerialization_Roundtrips()
ChatOptions? deserialized = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.ChatOptions);
Assert.NotNull(deserialized);

Assert.Equal("12345", deserialized.ChatThreadId);
Assert.Equal("12345", deserialized.ConversationId);
Assert.Equal(0.1f, deserialized.Temperature);
Assert.Equal(2, deserialized.MaxOutputTokens);
Assert.Equal(0.3f, deserialized.TopP);
Expand Down
Loading
Loading