Skip to content

Commit ed336d1

Browse files
authored
Add HostedFile/VectorStoreContent, HostedFileSearchTool, and HostedCodeInterpreterTool.Inputs (#6620)
* Add HostedFileContent, HostedVectorStoreContent, HostedFileSearchTool, and HostedCodeInterpreterTool.Inputs
1 parent 1638200 commit ed336d1

19 files changed

+602
-73
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- Added `ChatMessage.CreatedAt` so that chat messages can carry their timestamp.
77
- Added a `[Description(...)]` attribute to `DataContent.Uri` to clarify its purpose when used in schemas.
88
- Added `DataContent.Name` property to associate a name with the binary data, like a filename.
9+
- Added `HostedFileContent` for representing files hosted by the service.
10+
- Added `HostedVectorStoreContent` for representing vector stores hosted by the service.
11+
- Added `HostedFileSearchTool` to represent server-side file search tools.
12+
- Added `HostedCodeInterpreterTool.Inputs` to supply context about what state is available to the code interpreter tool.
913
- Improved handling of function parameter data annotation attributes in `AIJsonUtilities.CreateJsonSchema`.
1014
- Fixed schema generation to include an items keyword for arrays of objects in `AIJsonUtilities.CreateJsonSchema`.
1115

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Text.Json.Serialization;
99

10+
#pragma warning disable S3358 // Ternary operators should not be nested
11+
1012
namespace Microsoft.Extensions.AI;
1113

1214
/// <summary>Represents a chat message used by an <see cref="IChatClient" />.</summary>
@@ -107,7 +109,17 @@ public IList<AIContent> Contents
107109

108110
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
109111
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
110-
private AIContent? ContentForDebuggerDisplay => _contents is { Count: > 0 } ? _contents[0] : null;
112+
private AIContent? ContentForDebuggerDisplay
113+
{
114+
get
115+
{
116+
string text = Text;
117+
return
118+
!string.IsNullOrWhiteSpace(text) ? new TextContent(text) :
119+
_contents is { Count: > 0 } ? _contents[0] :
120+
null;
121+
}
122+
}
111123

112124
/// <summary>Gets an indication for the debugger display of whether there's more content.</summary>
113125
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

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

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Linq;
89
using System.Text;
@@ -183,72 +184,106 @@ static async Task<ChatResponse> ToChatResponseAsync(
183184
}
184185

185186
/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
186-
internal static void CoalesceTextContent(List<AIContent> contents)
187+
internal static void CoalesceTextContent(IList<AIContent> contents)
187188
{
188-
Coalesce<TextContent>(contents, static text => new(text));
189-
Coalesce<TextReasoningContent>(contents, static text => new(text));
189+
Coalesce<TextContent>(contents, mergeSingle: false, static (contents, start, end) =>
190+
new(MergeText(contents, start, end))
191+
{
192+
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
193+
});
190194

191-
// This implementation relies on TContent's ToString returning its exact text.
192-
static void Coalesce<TContent>(List<AIContent> contents, Func<string, TContent> fromText)
193-
where TContent : AIContent
195+
Coalesce<TextReasoningContent>(contents, mergeSingle: false, static (contents, start, end) =>
196+
new(MergeText(contents, start, end))
197+
{
198+
AdditionalProperties = contents[start].AdditionalProperties?.Clone()
199+
});
200+
201+
static string MergeText(IList<AIContent> contents, int start, int end)
194202
{
195-
StringBuilder? coalescedText = null;
203+
StringBuilder sb = new();
204+
for (int i = start; i < end; i++)
205+
{
206+
_ = sb.Append(contents[i]);
207+
}
196208

209+
return sb.ToString();
210+
}
211+
212+
static void Coalesce<TContent>(IList<AIContent> contents, bool mergeSingle, Func<IList<AIContent>, int, int, TContent> merge)
213+
where TContent : AIContent
214+
{
197215
// Iterate through all of the items in the list looking for contiguous items that can be coalesced.
198216
int start = 0;
199-
while (start < contents.Count - 1)
217+
while (start < contents.Count)
200218
{
201-
// We need at least two TextContents in a row to be able to coalesce. We also avoid touching contents
202-
// that have annotations, as we want to ensure the annotations (and in particular any start/end indices
203-
// into the text content) remain accurate.
204-
if (!TryAsCoalescable(contents[start], out var firstText))
219+
if (!TryAsCoalescable(contents[start], out var firstContent))
205220
{
206221
start++;
207222
continue;
208223
}
209224

210-
if (!TryAsCoalescable(contents[start + 1], out var secondText))
225+
// Iterate until we find a non-coalescable item.
226+
int i = start + 1;
227+
while (i < contents.Count && TryAsCoalescable(contents[i], out _))
211228
{
212-
start += 2;
213-
continue;
229+
i++;
214230
}
215231

216-
// Append the text from those nodes and continue appending subsequent TextContents until we run out.
217-
// We null out nodes as their text is appended so that we can later remove them all in one O(N) operation.
218-
coalescedText ??= new();
219-
_ = coalescedText.Clear().Append(firstText).Append(secondText);
220-
contents[start + 1] = null!;
221-
int i = start + 2;
222-
for (; i < contents.Count && TryAsCoalescable(contents[i], out TContent? next); i++)
232+
// If there's only one item in the run, and we don't want to merge single items, skip it.
233+
if (start == i - 1 && !mergeSingle)
223234
{
224-
_ = coalescedText.Append(next);
225-
contents[i] = null!;
235+
start++;
236+
continue;
226237
}
227238

228-
// Store the replacement node. We inherit the properties of the first text node. We don't
229-
// currently propagate additional properties from the subsequent nodes. If we ever need to,
230-
// we can add that here.
231-
var newContent = fromText(coalescedText.ToString());
232-
contents[start] = newContent;
233-
newContent.AdditionalProperties = firstText.AdditionalProperties?.Clone();
239+
// Store the replacement node and null out all of the nodes that we coalesced.
240+
// We can then remove all coalesced nodes in one O(N) operation via RemoveAll.
241+
// Leave start positioned at the start of the next run.
242+
contents[start] = merge(contents, start, i);
234243

235-
start = i;
244+
start++;
245+
while (start < i)
246+
{
247+
contents[start++] = null!;
248+
}
236249

237250
static bool TryAsCoalescable(AIContent content, [NotNullWhen(true)] out TContent? coalescable)
238251
{
239-
if (content is TContent && (content is not TextContent tc || tc.Annotations is not { Count: > 0 }))
252+
if (content is TContent tmp && tmp.Annotations is not { Count: > 0 })
240253
{
241-
coalescable = (TContent)content;
254+
coalescable = tmp;
242255
return true;
243256
}
244257

245-
coalescable = null!;
258+
coalescable = null;
246259
return false;
247260
}
248261
}
249262

250263
// Remove all of the null slots left over from the coalescing process.
251-
_ = contents.RemoveAll(u => u is null);
264+
if (contents is List<AIContent> contentsList)
265+
{
266+
_ = contentsList.RemoveAll(u => u is null);
267+
}
268+
else
269+
{
270+
int nextSlot = 0;
271+
int contentsCount = contents.Count;
272+
for (int i = 0; i < contentsCount; i++)
273+
{
274+
if (contents[i] is { } content)
275+
{
276+
contents[nextSlot++] = content;
277+
}
278+
}
279+
280+
for (int i = contentsCount - 1; i >= nextSlot; i--)
281+
{
282+
contents.RemoveAt(i);
283+
}
284+
285+
Debug.Assert(nextSlot == contents.Count, "Expected final count to equal list length.");
286+
}
252287
}
253288
}
254289

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Text.Json.Serialization;
99

10+
#pragma warning disable S3358 // Ternary operators should not be nested
11+
1012
namespace Microsoft.Extensions.AI;
1113

1214
/// <summary>
@@ -141,7 +143,17 @@ public IList<AIContent> Contents
141143

142144
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
143145
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
144-
private AIContent? ContentForDebuggerDisplay => _contents is { Count: > 0 } ? _contents[0] : null;
146+
private AIContent? ContentForDebuggerDisplay
147+
{
148+
get
149+
{
150+
string text = Text;
151+
return
152+
!string.IsNullOrWhiteSpace(text) ? new TextContent(text) :
153+
_contents is { Count: > 0 } ? _contents[0] :
154+
null;
155+
}
156+
}
145157

146158
/// <summary>Gets an indication for the debugger display of whether there's more content.</summary>
147159
[DebuggerBrowsable(DebuggerBrowsableState.Never)]

src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace Microsoft.Extensions.AI;
1212
[JsonDerivedType(typeof(ErrorContent), typeDiscriminator: "error")]
1313
[JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: "functionCall")]
1414
[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: "functionResult")]
15+
[JsonDerivedType(typeof(HostedFileContent), typeDiscriminator: "hostedFile")]
16+
[JsonDerivedType(typeof(HostedVectorStoreContent), typeDiscriminator: "hostedVectorStore")]
1517
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
1618
[JsonDerivedType(typeof(TextReasoningContent), typeDiscriminator: "reasoning")]
1719
[JsonDerivedType(typeof(UriContent), typeDiscriminator: "uri")]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents a file that is hosted by the AI service.
12+
/// </summary>
13+
/// <remarks>
14+
/// Unlike <see cref="DataContent"/> which contains the data for a file or blob, this class represents a file that is hosted
15+
/// by the AI service and referenced by an identifier. Such identifiers are specific to the provider.
16+
/// </remarks>
17+
[DebuggerDisplay("FileId = {FileId}")]
18+
public sealed class HostedFileContent : AIContent
19+
{
20+
private string _fileId;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="HostedFileContent"/> class.
24+
/// </summary>
25+
/// <param name="fileId">The ID of the hosted file.</param>
26+
/// <exception cref="ArgumentNullException"><paramref name="fileId"/> is <see langword="null"/>.</exception>
27+
/// <exception cref="ArgumentException"><paramref name="fileId"/> is empty or composed entirely of whitespace.</exception>
28+
public HostedFileContent(string fileId)
29+
{
30+
_fileId = Throw.IfNullOrWhitespace(fileId);
31+
}
32+
33+
/// <summary>
34+
/// Gets or sets the ID of the hosted file.
35+
/// </summary>
36+
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
37+
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or composed entirely of whitespace.</exception>
38+
public string FileId
39+
{
40+
get => _fileId;
41+
set => _fileId = Throw.IfNullOrWhitespace(value);
42+
}
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Extensions.AI;
9+
10+
/// <summary>
11+
/// Represents a vector store that is hosted by the AI service.
12+
/// </summary>
13+
/// <remarks>
14+
/// Unlike <see cref="HostedFileContent"/> which represents a specific file that is hosted by the AI service,
15+
/// <see cref="HostedVectorStoreContent"/> represents a vector store that can contain multiple files, indexed
16+
/// for searching.
17+
/// </remarks>
18+
[DebuggerDisplay("VectorStoreId = {VectorStoreId}")]
19+
public sealed class HostedVectorStoreContent : AIContent
20+
{
21+
private string _vectorStoreId;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="HostedVectorStoreContent"/> class.
25+
/// </summary>
26+
/// <param name="vectorStoreId">The ID of the hosted file store.</param>
27+
/// <exception cref="ArgumentNullException"><paramref name="vectorStoreId"/> is <see langword="null"/>.</exception>
28+
/// <exception cref="ArgumentException"><paramref name="vectorStoreId"/> is empty or composed entirely of whitespace.</exception>
29+
public HostedVectorStoreContent(string vectorStoreId)
30+
{
31+
_vectorStoreId = Throw.IfNullOrWhitespace(vectorStoreId);
32+
}
33+
34+
/// <summary>
35+
/// Gets or sets the ID of the hosted vector store.
36+
/// </summary>
37+
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
38+
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or composed entirely of whitespace.</exception>
39+
public string VectorStoreId
40+
{
41+
get => _vectorStoreId;
42+
set => _vectorStoreId = Throw.IfNullOrWhitespace(value);
43+
}
44+
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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.Collections.Generic;
5+
46
namespace Microsoft.Extensions.AI;
57

68
/// <summary>Represents a hosted tool that can be specified to an AI service to enable it to execute code it generates.</summary>
@@ -14,4 +16,12 @@ public class HostedCodeInterpreterTool : AITool
1416
public HostedCodeInterpreterTool()
1517
{
1618
}
19+
20+
/// <summary>Gets or sets a collection of <see cref="AIContent"/> to be used as input to the code interpreter tool.</summary>
21+
/// <remarks>
22+
/// Services support different varied kinds of inputs. Most support the IDs of files that are hosted by the service,
23+
/// represented via <see cref="HostedFileContent"/>. Some also support binary data, represented via <see cref="DataContent"/>.
24+
/// Unsupported inputs will be ignored by the <see cref="IChatClient"/> to which the tool is passed.
25+
/// </remarks>
26+
public IList<AIContent>? Inputs { get; set; }
1727
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Extensions.AI;
7+
8+
/// <summary>Represents a hosted tool that can be specified to an AI service to enable it to perform file search operations.</summary>
9+
/// <remarks>
10+
/// This tool is designed to facilitate file search functionality within AI services. It allows the service to search
11+
/// for relevant content based on the provided inputs and constraints, such as the maximum number of results.
12+
/// </remarks>
13+
public class HostedFileSearchTool : AITool
14+
{
15+
/// <summary>Initializes a new instance of the <see cref="HostedFileSearchTool"/> class.</summary>
16+
public HostedFileSearchTool()
17+
{
18+
}
19+
20+
/// <summary>Gets or sets a collection of <see cref="AIContent"/> to be used as input to the file search tool.</summary>
21+
/// <remarks>
22+
/// If no explicit inputs are provided, the service will determine what inputs should be searched. Different services
23+
/// support different kinds of inputs, e.g. some may respect <see cref="HostedFileContent"/> using provider-specific file IDs,
24+
/// others may support binary data uploaded as part of the request in <see cref="DataContent"/>, while others may support
25+
/// content in a hosted vector store and represented by a <see cref="HostedVectorStoreContent"/>.
26+
/// </remarks>
27+
public IList<AIContent>? Inputs { get; set; }
28+
29+
/// <summary>Gets or sets a requested bound on the number of matches the tool should produce.</summary>
30+
public int? MaximumResultCount { get; set; }
31+
}

0 commit comments

Comments
 (0)