Skip to content

Commit 4c789c9

Browse files
committed
Merged PR 48177: Update IChatClient to support multiple return messages
Update IChatClient to support multiple return messages - IChatClient no longer bakes mutation of the messages into the contract. The messages are now an `IEnumerable<ChatMessage>` rather than an `IList<ChatMessage>`. - The purpose for mutation was to allow for multiple messages to be generated as part of an operation. All messages generated are now returned as part of the ChatResponse, which has a Messages rather than Message property. - Choices have been removed from the surface area, e.g. no ChatResponse.Choices and no ChatResponseUpdate.ChoiceIndex. ---- #### AI description (iteration 1) #### PR Classification New feature #### PR Summary This pull request updates the `IChatClient` to support multiple return messages. - `src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs`: Refactored `GetResponseAsync` and `GetStreamingResponseAsync` methods to handle multiple return messages, aggregate usage data, and process function calls across multiple iterations. <!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot -->
2 parents be25c10 + 9fea0aa commit 4c789c9

File tree

109 files changed

+1881
-1976
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+1881
-1976
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary{TValue}.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ public bool TryGetValue<T>(string key, [NotNullWhen(true)] out T? value)
201201
/// <inheritdoc />
202202
bool IReadOnlyDictionary<string, TValue>.TryGetValue(string key, out TValue value) => _dictionary.TryGetValue(key, out value!);
203203

204+
/// <summary>Copies all of the entries from <paramref name="items"/> into the dictionary, overwriting any existing items in the dictionary with the same key.</summary>
205+
/// <param name="items">The items to add.</param>
206+
internal void SetAll(IEnumerable<KeyValuePair<string, TValue>> items)
207+
{
208+
_ = Throw.IfNull(items);
209+
210+
foreach (var item in items)
211+
{
212+
_dictionary[item.Key] = item.Value;
213+
}
214+
}
215+
204216
/// <summary>Enumerates the elements of an <see cref="AdditionalPropertiesDictionary{TValue}"/>.</summary>
205217
public struct Enumerator : IEnumerator<KeyValuePair<string, TValue>>
206218
{

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatMessage.cs

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Text.Json.Serialization;
8-
using Microsoft.Shared.Diagnostics;
99

1010
namespace Microsoft.Extensions.AI;
1111

@@ -17,14 +17,15 @@ public class ChatMessage
1717
private string? _authorName;
1818

1919
/// <summary>Initializes a new instance of the <see cref="ChatMessage"/> class.</summary>
20+
/// <remarks>The instance defaults to having a role of <see cref="ChatRole.User"/>.</remarks>
2021
[JsonConstructor]
2122
public ChatMessage()
2223
{
2324
}
2425

2526
/// <summary>Initializes a new instance of the <see cref="ChatMessage"/> class.</summary>
2627
/// <param name="role">The role of the author of the message.</param>
27-
/// <param name="content">The contents of the message.</param>
28+
/// <param name="content">The text content of the message.</param>
2829
public ChatMessage(ChatRole role, string? content)
2930
: this(role, content is null ? [] : [new TextContent(content)])
3031
{
@@ -33,12 +34,10 @@ public ChatMessage(ChatRole role, string? content)
3334
/// <summary>Initializes a new instance of the <see cref="ChatMessage"/> class.</summary>
3435
/// <param name="role">The role of the author of the message.</param>
3536
/// <param name="contents">The contents for this message.</param>
36-
public ChatMessage(
37-
ChatRole role,
38-
IList<AIContent> contents)
37+
public ChatMessage(ChatRole role, IList<AIContent>? contents)
3938
{
4039
Role = role;
41-
_contents = Throw.IfNull(contents);
40+
_contents = contents;
4241
}
4342

4443
/// <summary>Clones the <see cref="ChatMessage"/> to a new <see cref="ChatMessage"/> instance.</summary>
@@ -67,29 +66,12 @@ public string? AuthorName
6766
/// <summary>Gets or sets the role of the author of the message.</summary>
6867
public ChatRole Role { get; set; } = ChatRole.User;
6968

70-
/// <summary>
71-
/// Gets or sets the text of the first <see cref="TextContent"/> instance in <see cref="Contents" />.
72-
/// </summary>
69+
/// <summary>Gets the text of this message.</summary>
7370
/// <remarks>
74-
/// If there is no <see cref="TextContent"/> instance in <see cref="Contents" />, then the getter returns <see langword="null" />,
75-
/// and the setter adds a new <see cref="TextContent"/> instance with the provided value.
71+
/// This property concatenates the text of all <see cref="TextContent"/> objects in <see cref="Contents"/>.
7672
/// </remarks>
7773
[JsonIgnore]
78-
public string? Text
79-
{
80-
get => Contents.FindFirst<TextContent>()?.Text;
81-
set
82-
{
83-
if (Contents.FindFirst<TextContent>() is { } textContent)
84-
{
85-
textContent.Text = value;
86-
}
87-
else if (value is not null)
88-
{
89-
Contents.Add(new TextContent(value));
90-
}
91-
}
92-
}
74+
public string Text => Contents.ConcatText();
9375

9476
/// <summary>Gets or sets the chat message content items.</summary>
9577
[AllowNull]
@@ -112,7 +94,7 @@ public IList<AIContent> Contents
11294
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
11395

11496
/// <inheritdoc/>
115-
public override string ToString() => Contents.ConcatText();
97+
public override string ToString() => Text;
11698

11799
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
118100
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponse.cs

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,62 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Text;
6+
using System.Diagnostics.CodeAnalysis;
77
using System.Text.Json.Serialization;
88
using Microsoft.Shared.Diagnostics;
99

1010
namespace Microsoft.Extensions.AI;
1111

1212
/// <summary>Represents the response to a chat request.</summary>
13+
/// <remarks>
14+
/// <see cref="ChatResponse"/> provides one or more response messages and metadata about the response.
15+
/// A typical response will contain a single message, however a response may contain multiple messages
16+
/// in a variety of scenarios. For example, if automatic function calling is employed, such that a single
17+
/// request to a <see cref="IChatClient"/> may actually generate multiple roundtrips to an inner <see cref="IChatClient"/>
18+
/// it uses, all of the involved messages may be surfaced as part of the final <see cref="ChatResponse"/>.
19+
/// </remarks>
1320
public class ChatResponse
1421
{
15-
/// <summary>The list of choices in the response.</summary>
16-
private IList<ChatMessage> _choices;
22+
/// <summary>The response messages.</summary>
23+
private IList<ChatMessage>? _messages;
1724

1825
/// <summary>Initializes a new instance of the <see cref="ChatResponse"/> class.</summary>
19-
/// <param name="choices">The list of choices in the response, one message per choice.</param>
20-
[JsonConstructor]
21-
public ChatResponse(IList<ChatMessage> choices)
26+
public ChatResponse()
2227
{
23-
_choices = Throw.IfNull(choices);
2428
}
2529

2630
/// <summary>Initializes a new instance of the <see cref="ChatResponse"/> class.</summary>
27-
/// <param name="message">The chat message representing the singular choice in the response.</param>
31+
/// <param name="message">The response message.</param>
32+
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
2833
public ChatResponse(ChatMessage message)
2934
{
3035
_ = Throw.IfNull(message);
31-
_choices = [message];
36+
37+
Messages.Add(message);
38+
}
39+
40+
/// <summary>Initializes a new instance of the <see cref="ChatResponse"/> class.</summary>
41+
/// <param name="messages">The response messages.</param>
42+
public ChatResponse(IList<ChatMessage>? messages)
43+
{
44+
_messages = messages;
3245
}
3346

34-
/// <summary>Gets or sets the list of chat response choices.</summary>
35-
public IList<ChatMessage> Choices
47+
/// <summary>Gets or sets the chat response messages.</summary>
48+
[AllowNull]
49+
public IList<ChatMessage> Messages
3650
{
37-
get => _choices;
38-
set => _choices = Throw.IfNull(value);
51+
get => _messages ??= new List<ChatMessage>(1);
52+
set => _messages = value;
3953
}
4054

41-
/// <summary>Gets the chat response message.</summary>
55+
/// <summary>Gets the text of the response.</summary>
4256
/// <remarks>
43-
/// If there are multiple choices, this property returns the first choice.
44-
/// If <see cref="Choices"/> is empty, this property will throw. Use <see cref="Choices"/> to access all choices directly.
57+
/// This property concatenates the <see cref="ChatMessage.Text"/> of all <see cref="ChatMessage"/>
58+
/// instances in <see cref="Messages"/>.
4559
/// </remarks>
4660
[JsonIgnore]
47-
public ChatMessage Message
48-
{
49-
get
50-
{
51-
var choices = Choices;
52-
if (choices.Count == 0)
53-
{
54-
throw new InvalidOperationException($"The {nameof(ChatResponse)} instance does not contain any {nameof(ChatMessage)} choices.");
55-
}
56-
57-
return choices[0];
58-
}
59-
}
61+
public string Text => _messages?.ConcatText() ?? string.Empty;
6062

6163
/// <summary>Gets or sets the ID of the chat response.</summary>
6264
public string? ResponseId { get; set; }
@@ -67,7 +69,7 @@ public ChatMessage Message
6769
/// the input messages supplied to <see cref="IChatClient.GetResponseAsync"/> need only be the additional messages beyond
6870
/// what's already stored. If this property is non-<see langword="null"/>, it represents an identifier for that state,
6971
/// and it should be used in a subsequent <see cref="ChatOptions.ChatThreadId"/> instead of supplying the same messages
70-
/// (and this <see cref="ChatResponse"/>'s message) as part of the <c>chatMessages</c> parameter.
72+
/// (and this <see cref="ChatResponse"/>'s message) as part of the <c>messages</c> parameter.
7173
/// </remarks>
7274
public string? ChatThreadId { get; set; }
7375

@@ -96,26 +98,7 @@ public ChatMessage Message
9698
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
9799

98100
/// <inheritdoc />
99-
public override string ToString()
100-
{
101-
if (Choices.Count == 1)
102-
{
103-
return Choices[0].ToString();
104-
}
105-
106-
StringBuilder sb = new();
107-
for (int i = 0; i < Choices.Count; i++)
108-
{
109-
if (i > 0)
110-
{
111-
_ = sb.AppendLine().AppendLine();
112-
}
113-
114-
_ = sb.Append("Choice ").Append(i).AppendLine(":").Append(Choices[i]);
115-
}
116-
117-
return sb.ToString();
118-
}
101+
public override string ToString() => Text;
119102

120103
/// <summary>Creates an array of <see cref="ChatResponseUpdate" /> instances that represent this <see cref="ChatResponse" />.</summary>
121104
/// <returns>An array of <see cref="ChatResponseUpdate" /> instances that may be used to represent this <see cref="ChatResponse" />.</returns>
@@ -135,22 +118,22 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
135118
}
136119
}
137120

138-
int choicesCount = Choices.Count;
139-
var updates = new ChatResponseUpdate[choicesCount + (extra is null ? 0 : 1)];
121+
int messageCount = _messages?.Count ?? 0;
122+
var updates = new ChatResponseUpdate[messageCount + (extra is not null ? 1 : 0)];
140123

141-
for (int choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++)
124+
int i;
125+
for (i = 0; i < messageCount; i++)
142126
{
143-
ChatMessage choice = Choices[choiceIndex];
144-
updates[choiceIndex] = new ChatResponseUpdate
127+
ChatMessage message = _messages![i];
128+
updates[i] = new ChatResponseUpdate
145129
{
146130
ChatThreadId = ChatThreadId,
147-
ChoiceIndex = choiceIndex,
148131

149-
AdditionalProperties = choice.AdditionalProperties,
150-
AuthorName = choice.AuthorName,
151-
Contents = choice.Contents,
152-
RawRepresentation = choice.RawRepresentation,
153-
Role = choice.Role,
132+
AdditionalProperties = message.AdditionalProperties,
133+
AuthorName = message.AuthorName,
134+
Contents = message.Contents,
135+
RawRepresentation = message.RawRepresentation,
136+
Role = message.Role,
154137

155138
ResponseId = ResponseId,
156139
CreatedAt = CreatedAt,
@@ -161,7 +144,7 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
161144

162145
if (extra is not null)
163146
{
164-
updates[choicesCount] = extra;
147+
updates[i] = extra;
165148
}
166149

167150
return updates;

0 commit comments

Comments
 (0)