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()}…";
}