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 @@ -225,6 +225,61 @@ private static void CoalesceImageResultContent(IList<AIContent> contents)
}
}

/// <summary>
/// Coalesces web search tool call content elements in the provided list of <see cref="AIContent"/> items.
/// Unlike other content coalescing methods, this will coalesce non-sequential items based on their CallId property,
/// merging data from all items with the same CallId into the first occurrence.
/// </summary>
private static void CoalesceWebSearchToolCallContent(IList<AIContent> contents)
{
Dictionary<string, int>? webSearchCallIndexById = null;
bool hasRemovals = false;

for (int i = 0; i < contents.Count; i++)
{
if (contents[i] is WebSearchToolCallContent webSearchCall && !string.IsNullOrEmpty(webSearchCall.CallId))
{
webSearchCallIndexById ??= new(StringComparer.Ordinal);

if (webSearchCallIndexById.TryGetValue(webSearchCall.CallId!, out int existingIndex))
{
// Merge data from the new item into the existing one.
var existing = (WebSearchToolCallContent)contents[existingIndex];

if (webSearchCall.Queries is { Count: > 0 })
{
if (existing.Queries is null)
{
existing.Queries = webSearchCall.Queries;
}
else
{
foreach (var query in webSearchCall.Queries)
{
existing.Queries.Add(query);
}
Comment thread
stephentoub marked this conversation as resolved.
}
}

existing.RawRepresentation ??= webSearchCall.RawRepresentation;
existing.AdditionalProperties ??= webSearchCall.AdditionalProperties;

contents[i] = null!;
hasRemovals = true;
}
else
{
webSearchCallIndexById[webSearchCall.CallId!] = i;
}
}
}

if (hasRemovals)
{
RemoveNullContents(contents);
}
}

/// <summary>Coalesces sequential <see cref="AIContent"/> content elements.</summary>
internal static void CoalesceContent(IList<AIContent> contents)
{
Expand Down Expand Up @@ -262,6 +317,8 @@ internal static void CoalesceContent(IList<AIContent> contents)

CoalesceImageResultContent(contents);

CoalesceWebSearchToolCallContent(contents);

Coalesce<DataContent>(
contents,
mergeSingle: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace Microsoft.Extensions.AI;
// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")]
// [JsonDerivedType(typeof(ImageGenerationToolCallContent), typeDiscriminator: "imageGenerationToolCall")]
// [JsonDerivedType(typeof(ImageGenerationToolResultContent), typeDiscriminator: "imageGenerationToolResult")]
// [JsonDerivedType(typeof(WebSearchToolCallContent), typeDiscriminator: "webSearchToolCall")]
// [JsonDerivedType(typeof(WebSearchToolResultContent), typeDiscriminator: "webSearchToolResult")]

public class AIContent
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Microsoft.Extensions.AI;
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
// [JsonDerivedType(typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall")]
// [JsonDerivedType(typeof(ImageGenerationToolCallContent), "imageGenerationToolCall")]
// [JsonDerivedType(typeof(WebSearchToolCallContent), "webSearchToolCall")]
public class ToolCallContent : AIContent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Microsoft.Extensions.AI;
// as well as the [JsonSerializable] attributes for them on the JsonContext should be removed.
// [JsonDerivedType(typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult")]
// [JsonDerivedType(typeof(ImageGenerationToolResultContent), "imageGenerationToolResult")]
// [JsonDerivedType(typeof(WebSearchToolResultContent), "webSearchToolResult")]
public class ToolResultContent : AIContent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents a web search tool call invocation by a hosted service.
/// </summary>
/// <remarks>
/// This content type represents when a hosted AI service invokes a web search tool.
/// It is informational only and represents the call itself, not the result.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIWebSearch, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class WebSearchToolCallContent : ToolCallContent
{
/// <summary>
/// Initializes a new instance of the <see cref="WebSearchToolCallContent"/> class.
/// </summary>
/// <param name="callId">The tool call ID.</param>
public WebSearchToolCallContent(string callId)
: base(callId)
{
}

/// <summary>
/// Gets or sets the search queries issued by the service.
/// </summary>
public IList<string>? Queries { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents the result of a web search tool invocation by a hosted service.
/// </summary>
/// <remarks>
/// This content type represents the results found by a hosted AI service's web search tool.
/// The results contain a list of <see cref="AIContent"/> items describing the web pages
/// found during the search, typically as <see cref="UriContent"/> instances.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIWebSearch, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class WebSearchToolResultContent : ToolResultContent
{
/// <summary>
/// Initializes a new instance of the <see cref="WebSearchToolResultContent"/> class.
/// </summary>
/// <param name="callId">The tool call ID.</param>
public WebSearchToolResultContent(string callId)
: base(callId)
{
}

/// <summary>
/// Gets or sets the web search results.
/// </summary>
/// <remarks>
/// Each item represents a web page found during the search, typically as a <see cref="UriContent"/> instance.
/// If a title is available for a result, it may be stored in the item's <see cref="AIContent.AdditionalProperties"/>
/// under the key <c>"title"</c>.
/// </remarks>
public IList<AIContent>? Results { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ private static JsonSerializerOptions CreateDefaultOptions()
AddAIContentType(options, typeof(AIContent), typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false);
AddAIContentType(options, typeof(AIContent), typeof(ImageGenerationToolCallContent), typeDiscriminatorId: "imageGenerationToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(AIContent), typeof(ImageGenerationToolResultContent), typeDiscriminatorId: "imageGenerationToolResult", checkBuiltIn: false);
AddAIContentType(options, typeof(AIContent), typeof(WebSearchToolCallContent), typeDiscriminatorId: "webSearchToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(AIContent), typeof(WebSearchToolResultContent), typeDiscriminatorId: "webSearchToolResult", checkBuiltIn: false);

// Also register the experimental types as derived types of ToolCallContent/ToolResultContent.
AddAIContentType(options, typeof(ToolCallContent), typeof(CodeInterpreterToolCallContent), "codeInterpreterToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(ToolCallContent), typeof(ImageGenerationToolCallContent), "imageGenerationToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(ToolCallContent), typeof(WebSearchToolCallContent), "webSearchToolCall", checkBuiltIn: false);
AddAIContentType(options, typeof(ToolResultContent), typeof(CodeInterpreterToolResultContent), "codeInterpreterToolResult", checkBuiltIn: false);
AddAIContentType(options, typeof(ToolResultContent), typeof(ImageGenerationToolResultContent), "imageGenerationToolResult", checkBuiltIn: false);
AddAIContentType(options, typeof(ToolResultContent), typeof(WebSearchToolResultContent), "webSearchToolResult", checkBuiltIn: false);

if (JsonSerializer.IsReflectionEnabledByDefault)
{
Expand Down Expand Up @@ -129,6 +133,8 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(CodeInterpreterToolResultContent))]
[JsonSerializable(typeof(ImageGenerationToolCallContent))]
[JsonSerializable(typeof(ImageGenerationToolResultContent))]
[JsonSerializable(typeof(WebSearchToolCallContent))]
[JsonSerializable(typeof(WebSearchToolResultContent))]
[JsonSerializable(typeof(ResponseContinuationToken))]

// IEmbeddingGenerator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -620,9 +620,20 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
{
foreach (AITool tool in tools)
{
if (tool is AIFunctionDeclaration af)
switch (tool)
{
result.Tools.Add(ToOpenAIChatTool(af, options));
case AIFunctionDeclaration af:
result.Tools.Add(ToOpenAIChatTool(af, options));
break;

case HostedWebSearchTool:
#pragma warning disable OPENAI001 // WebSearchOptions is experimental
result.WebSearchOptions ??= new();
#pragma warning restore OPENAI001
// The Chat Completions API surfaces web search results via message-level annotations
// (handled in FromOpenAIChatCompletion) rather than as separate tool call response items.
// WebSearchToolCallContent/WebSearchToolResultContent are only used by the Responses API path.
break;
Comment thread
stephentoub marked this conversation as resolved.
}
}

Expand Down
Loading
Loading