-
Notifications
You must be signed in to change notification settings - Fork 1.9k
.NET: Add observer for OpenAIWebSearch #5894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
...ples/02-agents/Harness/Harness_Step01_Research/OpenAIResponsesWebSearchDisplayObserver.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| #pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage. | ||
|
|
||
| using System.Text; | ||
| using Harness.Shared.Console; | ||
| using Harness.Shared.Console.Observers; | ||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Extensions.AI; | ||
| using OpenAI.Responses; | ||
|
|
||
| namespace SampleApp; | ||
|
|
||
| /// <summary> | ||
| /// Displays web search activity in the scroll area. Shows search queries, | ||
| /// page opens, and find-in-page actions as they stream in from the API. | ||
| /// </summary> | ||
| internal sealed class OpenAIResponsesWebSearchDisplayObserver : ConsoleObserver | ||
| { | ||
| private const int MaxQueryDisplayLength = 120; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session) | ||
| { | ||
| if (content is WebSearchToolResultContent resultContent | ||
| && resultContent.RawRepresentation is WebSearchCallResponseItem wscri) | ||
| { | ||
| await WriteActionAsync(ux, wscri, resultContent.Outputs); | ||
| } | ||
| } | ||
|
|
||
| private static async Task WriteActionAsync(IUXStateDriver ux, WebSearchCallResponseItem wscri, IList<AIContent>? outputs) | ||
| { | ||
| WebSearchAction? action = wscri.Action; | ||
| if (action is null) | ||
| { | ||
| await ux.WriteInfoLineAsync("🌐 Web Search Tool (no action details)", ConsoleColor.DarkCyan); | ||
| return; | ||
| } | ||
|
|
||
| switch (action) | ||
| { | ||
| case WebSearchFindInPageAction findInPage: | ||
| await WriteFindInPageAsync(ux, findInPage); | ||
| break; | ||
|
|
||
| case WebSearchOpenPageAction openPage: | ||
| await WriteOpenPageAsync(ux, openPage); | ||
| break; | ||
|
|
||
| case WebSearchSearchAction search: | ||
| await WriteSearchAsync(ux, search, outputs); | ||
| break; | ||
|
|
||
| default: | ||
| await ux.WriteInfoLineAsync("🌐 Web Search Tool (unknown action)", ConsoleColor.DarkCyan); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| private static async Task WriteSearchAsync(IUXStateDriver ux, WebSearchSearchAction search, IList<AIContent>? outputs) | ||
| { | ||
| // Read queries directly from the typed action. | ||
| IList<string> queries = search.Queries; | ||
|
|
||
| if (queries.Count == 0) | ||
| { | ||
| await ux.WriteInfoLineAsync("🌐 Web Search Tool: search", ConsoleColor.DarkCyan); | ||
| return; | ||
| } | ||
|
|
||
| var sb = new StringBuilder(); | ||
| sb.Append("🌐 Web Search Tool: search"); | ||
|
|
||
| // Show the search queries. | ||
| bool hasResults = outputs is { Count: > 0 }; | ||
| for (int i = 0; i < queries.Count; i++) | ||
| { | ||
| string connector = (i < queries.Count - 1 || hasResults) ? "├─" : "└─"; | ||
| string query = Truncate(queries[i], MaxQueryDisplayLength); | ||
| sb.Append($"\n {connector} \"{query}\""); | ||
| } | ||
|
|
||
| // Show search result sources (URLs + titles) when available. | ||
| // Sources come from M.E.AI's Outputs when IncludedResponseProperty.WebSearchCallActionSources is set, | ||
| // or directly from the SDK's WebSearchSearchAction.Sources. | ||
| if (hasResults) | ||
| { | ||
| sb.Append("\n │"); | ||
| for (int i = 0; i < outputs!.Count; i++) | ||
| { | ||
| string connector = i < outputs.Count - 1 ? "├─" : "└─"; | ||
| string line = FormatOutput(outputs[i]); | ||
| sb.Append($"\n {connector} {line}"); | ||
| } | ||
| } | ||
| else if (search.Sources is { Count: > 0 } sources) | ||
| { | ||
| sb.Append("\n │"); | ||
| for (int i = 0; i < sources.Count; i++) | ||
| { | ||
| string connector = i < sources.Count - 1 ? "├─" : "└─"; | ||
| string line = FormatSource(sources[i]); | ||
| sb.Append($"\n {connector} {line}"); | ||
| } | ||
| } | ||
|
|
||
| await ux.WriteInfoLineAsync(sb.ToString(), ConsoleColor.DarkCyan); | ||
| } | ||
|
|
||
| private static async Task WriteOpenPageAsync(IUXStateDriver ux, WebSearchOpenPageAction openPage) | ||
| { | ||
| string url = openPage.Uri?.AbsoluteUri ?? "(unknown)"; | ||
| await ux.WriteInfoLineAsync( | ||
| $"🌐 Web Search Tool: open page\n └─ {url}", | ||
| ConsoleColor.DarkCyan); | ||
| } | ||
|
|
||
| private static async Task WriteFindInPageAsync(IUXStateDriver ux, WebSearchFindInPageAction findInPage) | ||
| { | ||
| string url = findInPage.Uri?.AbsoluteUri ?? "(unknown)"; | ||
| string pattern = findInPage.Pattern ?? "(unknown)"; | ||
|
|
||
| await ux.WriteInfoLineAsync( | ||
| $"🌐 Web Search Tool: find in page\n ├─ \"{Truncate(pattern, MaxQueryDisplayLength)}\"\n └─ {url}", | ||
| ConsoleColor.DarkCyan); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Formats a single search result source from the SDK's <see cref="WebSearchActionSource"/> for display. | ||
| /// </summary> | ||
| private static string FormatSource(WebSearchActionSource source) | ||
| { | ||
| if (source is WebSearchActionUriSource uriSource) | ||
| { | ||
| string url = uriSource.Uri?.AbsoluteUri ?? "(unknown)"; | ||
|
|
||
| // WebSearchActionUriSource doesn't expose a title property, | ||
| // but the API may include one in the raw response JSON. | ||
| string? title = GetTitleFromRawRepresentation(uriSource); | ||
|
|
||
| return title is not null | ||
| ? $"{Truncate(title, MaxQueryDisplayLength)} — {url}" | ||
| : url; | ||
| } | ||
|
|
||
| return source.ToString() ?? "(unknown source)"; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Formats a single search result output from M.E.AI's <see cref="AIContent"/> for display. | ||
| /// </summary> | ||
| private static string FormatOutput(AIContent output) | ||
| { | ||
| if (output is UriContent uriContent) | ||
| { | ||
| string url = uriContent.Uri?.AbsoluteUri ?? "(unknown)"; | ||
|
|
||
| // Try to extract a title from the raw JSON of the source. | ||
| // The SDK's WebSearchActionUriSource doesn't expose a title property, | ||
| // but the API may include one in the raw response. | ||
| string? title = GetTitleFromRawRepresentation(uriContent.RawRepresentation) | ||
| ?? (uriContent.AdditionalProperties?.TryGetValue("title", out var t) is true ? t?.ToString() : null); | ||
|
|
||
| return title is not null | ||
| ? $"{Truncate(title, MaxQueryDisplayLength)} — {url}" | ||
| : url; | ||
| } | ||
|
|
||
| return output.ToString() ?? "(unknown output)"; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Attempts to extract a "title" field from a raw representation object by serializing it to JSON. | ||
| /// The SDK's <see cref="WebSearchActionUriSource"/> doesn't expose a title property, | ||
| /// but the API may include one in the raw JSON — this is forward-compatible for when | ||
| /// the SDK adds title support. | ||
| /// </summary> | ||
| private static string? GetTitleFromRawRepresentation(object? rawRepresentation) | ||
| { | ||
| if (rawRepresentation is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| var data = System.ClientModel.Primitives.ModelReaderWriter.Write(rawRepresentation); | ||
| using var doc = System.Text.Json.JsonDocument.Parse(data); | ||
| if (doc.RootElement.TryGetProperty("title", out var titleEl) | ||
|
westey-m marked this conversation as resolved.
|
||
| && titleEl.ValueKind == System.Text.Json.JsonValueKind.String) | ||
| { | ||
| return titleEl.GetString(); | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Serialization may not be supported for this object type. | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private static string Truncate(string text, int maxLength) | ||
| => text.Length <= maxLength ? text : string.Concat(text.AsSpan(0, maxLength - 1), "…"); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.