using AIStudio.Tools.ToolCallingSystem; namespace AIStudio.Assistants.BeyondFactsLab; /// /// What a research agent of the BeyondFacts Lab found on the web about one topic, stored as research/<topic>.json /// in the folder of the step "Strategy". /// /// /// Besides the text, a research keeps what left AI Studio for it: the public details it received, the queries it sent /// to the web search, and the addresses of the pages it opened. The person sees all of it, so that they can check that /// nothing else of their preparation went out. Whether the research is outdated follows from the details it received, /// see IsOutdated. /// public sealed class BflResearch { /// /// The version of the stored form this version of AI Studio writes. Raise it with every new property. /// public const int CURRENT_SCHEMA_VERSION = 1; /// /// How many characters of the answer of the agent a research keeps at most, since the text goes into the prompt of /// the step "Strategy". /// public const int MAX_TEXT_CHARACTERS = 20_000; /// /// How many characters of the title of a page a research keeps at most. /// public const int MAX_TITLE_CHARACTERS = 300; /// /// The argument with which the tool Web Search gets its query, see WebSearchTool. /// internal const string QUERY_ARGUMENT = "query"; /// /// The argument with which the tool Read Web Page gets its address, see ReadWebPageTool. /// internal const string URL_ARGUMENT = "url"; /// /// Gets or sets the version of the stored form. /// public int SchemaVersion { get; set; } = CURRENT_SCHEMA_VERSION; /// /// Gets or sets what the agent found, in Markdown, with the sources as links. /// public string Text { get; set; } = string.Empty; /// /// Gets or sets the public details the agent received, in the order of the fields. /// public List Details { get; set; } = []; /// /// Gets or sets the queries the agent sent to the web search, in the order it sent them. /// public List SearchQueries { get; set; } = []; /// /// Gets or sets the pages the agent opened, in the order it opened them. /// public List Pages { get; set; } = []; /// /// Gets or sets when the agent finished. /// public DateTimeOffset CreatedAtUtc { get; set; } /// /// Tells whether the public details changed since the agent received them, so that the research may no longer fit /// the event. /// /// The public details as they are now, see BflPromptComposer.PublicDetailsOf. public bool IsOutdated(IReadOnlyList details) => this.Details.Count != details.Count || this.Details.Zip(details).Any(pair => !string.Equals(pair.First.FieldId, pair.Second.FieldId, StringComparison.Ordinal) || !string.Equals(pair.First.Value, pair.Second.Value, StringComparison.Ordinal)); /// /// Reads a research from the answer of an agent and the calls of its tools. /// /// /// A query or an address counts once the tool ran with it, whether it found something or not: then it has left AI /// Studio. A call which the lab or the organization blocked sent nothing. The text is limited, since it goes into /// the prompt of the step "Strategy"; it is the answer of a model which read only filtered web content. /// /// The answer of the agent. /// The calls of tools the agent made. /// The sources the tools reported, for the titles of the pages. /// The public details the agent received. /// When the agent finished. /// The research; its text is empty when the agent wrote nothing. public static BflResearch Of(string answer, IEnumerable invocations, IEnumerable sources, IReadOnlyList details, DateTimeOffset createdAtUtc) { var text = BflResultText.Read(answer); if (text.Length > MAX_TEXT_CHARACTERS) text = BflMaterialBudget.EndAtLineEnd(text[..MAX_TEXT_CHARACTERS]); var executed = invocations.Where(invocation => invocation.WasExecuted).OrderBy(invocation => invocation.Order).ToList(); var queries = executed .Where(invocation => string.Equals(invocation.ToolId, ToolSelectionRules.WEB_SEARCH_TOOL_ID, StringComparison.Ordinal)) .Select(invocation => invocation.Arguments.GetValueOrDefault(QUERY_ARGUMENT, string.Empty).Trim()) .Where(query => query.Length > 0) .Distinct(StringComparer.Ordinal) .ToList(); var titles = new Dictionary(StringComparer.Ordinal); foreach (var source in sources.Where(source => source.Origin is SourceOrigin.TOOL && !string.IsNullOrWhiteSpace(source.Title))) titles.TryAdd(source.URL, source.Title.Trim()); var pages = executed .Where(invocation => string.Equals(invocation.ToolId, ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, StringComparison.Ordinal)) .Select(invocation => invocation.Arguments.GetValueOrDefault(URL_ARGUMENT, string.Empty).Trim()) .Where(IsWebAddress) .Distinct(StringComparer.Ordinal) .Select(url => new BflResearchPage { Url = url, Title = Shorten(titles.GetValueOrDefault(url, url)) }) .ToList(); return new BflResearch { Text = text, Details = [.. details.Select(detail => new BflResearchDetail { FieldId = detail.FieldId, Value = detail.Value })], SearchQueries = queries, Pages = pages, CreatedAtUtc = createdAtUtc, }; } /// /// Tells whether a text is a complete address of a web page. /// /// The text. private static bool IsWebAddress(string url) => Uri.TryCreate(url, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp); /// /// Limits the title of a page. /// /// The title. private static string Shorten(string title) => title.Length <= MAX_TITLE_CHARACTERS ? title : $"{title[..(MAX_TITLE_CHARACTERS - 1)].TrimEnd()}…"; }