← All source files

V8 / 34b774036cd1

WebOperations.cs

Lilith/Core/WebResearch/WebOperations.cs

Changed in this version
CS
using System.Text.Json;
using System.Text.RegularExpressions;

namespace Lilith.Core.WebResearch;

internal static class WebOperations
{
    internal static readonly string[] Names = ["web_search_operation", "web_fetch_operation", "web_check_answer_operation"];
    internal static async Task<string> ExecuteAsync(IResearchBrowser browser, string name, IReadOnlyDictionary<string, string> inputs, CancellationToken ct)
    {
        string Get(string key) => inputs.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : throw new ArgumentException("Missing web input: " + key);
        switch (name)
        {
            case "web_search_operation":
                string question = Get("question");
                if (question.Length > 2000 || !int.TryParse(Get("source_count"), out int count) || count is < 1 or > 20) throw new ArgumentException("Question must be at most 2000 characters; source_count must be 1-20.");
                return JsonSerializer.Serialize(await browser.SearchAsync(question, Math.Min(30, count * 3), ct), JsonOptions);
            case "web_fetch_operation":
                string url = PublicWeb.ValidateUri(Get("url")).AbsoluteUri;
                try { return JsonSerializer.Serialize(await browser.ReadAsync(url, ct), JsonOptions); }
                catch (Exception ex) when (ex is IOException or System.Net.Http.HttpRequestException or ArgumentException)
                { return JsonSerializer.Serialize(new WebPage(url, "", false, "", "Public page could not be read.", DateTimeOffset.UtcNow), JsonOptions); }
            case "web_check_answer_operation":
                using (var evidence = JsonDocument.Parse(Get("evidence")))
                {
                    var data = evidence.RootElement;
                    var urls = data.GetProperty("items").EnumerateArray().Where(i => i.TryGetProperty("readable", out var readable) && bool.TryParse(readable.ToString(), out bool valid) && valid)
                        .Select(i => PublicWeb.ValidateUri(i.GetProperty("url").GetString()!).AbsoluteUri).ToHashSet(StringComparer.Ordinal);
                    int requested = data.GetProperty("requested").GetInt32(), attempts = data.GetProperty("attempted").GetInt32();
                    if (urls.Count == 0) return "I could not verify an answer from readable public sources. Search engines or pages may be unavailable, blocked, or require a browser interaction. No current answer is claimed. Sources read: 0 of " + requested + ".";
                    string answer = Get("answer");
                    var links = Regex.Matches(answer, @"\[[^\]]*\]\(([^\s)]+)\)");
                    if (links.Count == 0 || links.Cast<Match>().Any(m => !Uri.TryCreate(m.Groups[1].Value, UriKind.Absolute, out var uri) || !urls.Contains(uri.AbsoluteUri)))
                        throw new InvalidDataException("Synthesis cited no verified source links, or cited a page that was not read.");
                    return answer + $"\n\nSources read: {urls.Count} of {requested} requested ({attempts} page attempts).";
                }
            default: throw new ArgumentException("Unknown research operation.");
        }
    }
    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
}