using System.Text.Json.Nodes; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.RAG; using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.SemanticSearch; namespace AIStudio.Tests.Tools.ToolCalling; /// /// Checks what a search tells the chat and the model besides its passages. /// /// /// The chat raises what it requires from a provider as soon as confidential passages land in it, /// and never lowers it again. A search which found nothing brought nothing in, so it must not raise /// anything: otherwise, one search in the wrong data source would lock a chat to self-hosted /// providers for good. The model, for its part, has to tell a data source which found nothing /// from one which could not be searched, or it would take a failure for an answer. /// [TestFixture] public sealed class SemanticSearchToolResultTests { private static readonly IDataSource HANDBOOK = new DataSourceLocalDirectory { Num = 1, Id = "11111111-1111-1111-1111-111111111111", Name = "Handbook", MaxMatches = 10, ConfidenceLevel = ConfidenceLevel.HIGH }; private static readonly IDataSource MINUTES = new DataSourceLocalFile { Num = 2, Id = "22222222-2222-2222-2222-222222222222", Name = "Minutes", MaxMatches = 10, ConfidenceLevel = ConfidenceLevel.LOW }; private static readonly IDataSource INTRANET = new DataSourceERI_V1 { Num = 3, Id = "33333333-3333-3333-3333-333333333333", Name = "Intranet", MaxMatches = 10, SecurityPolicy = DataSourceSecurity.SELF_HOSTED }; [Test] public void ASearchWhichFoundNothingRequiresNothing() { var requirements = SemanticSearchTool.GetRequirements([HANDBOOK, INTRANET], [0, 0]); Assert.That(requirements, Is.EqualTo((ConfidenceLevel.NONE, DataSourceSecurity.NOT_SPECIFIED)), "NOT_SPECIFIED leaves the data security of the chat as it was."); } [Test] public void OnlyTheDataSourcesWithPassagesRaiseTheRequirements() { var requirements = SemanticSearchTool.GetRequirements([HANDBOOK, MINUTES, INTRANET], [0, 2, 0]); Assert.That(requirements, Is.EqualTo((ConfidenceLevel.LOW, DataSourceSecurity.ALLOW_ANY)), "Neither the highly confidential handbook nor the self-hosted intranet brought anything into the chat."); } [Test] public void ASelfHostedDataSourceWithPassagesRestrictsTheChat() { var requirements = SemanticSearchTool.GetRequirements([MINUTES, INTRANET], [1, 1]); Assert.That(requirements, Is.EqualTo((ConfidenceLevel.LOW, DataSourceSecurity.SELF_HOSTED))); } [Test] public void ADataSourceWhichCouldNotBeSearchedSaysSo() { var result = SemanticSearchTool.DescribeResult(INTRANET, RetrievalPage.EMPTY with { Gaps = [RetrievalGap.NOT_SEARCHED] }, 0, 0); Assert.Multiple(() => { Assert.That(result["result_count"]!.GetValue(), Is.Zero); Assert.That(Issues(result), Has.Count.EqualTo(1), "A result count of zero alone reads as finding nothing."); }); } [Test] public void PassagesLeftOutForTheBudgetAreCountedWithAWayOut() { var result = SemanticSearchTool.DescribeResult(HANDBOOK, RetrievalPage.EMPTY with { HasMore = true }, 4, 3); Assert.Multiple(() => { Assert.That(result["result_count"]!.GetValue(), Is.EqualTo(4), "Only the passages which reached the model count as results."); Assert.That(result["has_more"]!.GetValue(), Is.True); Assert.That(Issues(result), Has.Count.EqualTo(1)); Assert.That(Issues(result).FirstOrDefault(), Does.StartWith("3 further passages").And.Contain("narrower query"), "The model has to learn how many are missing, and how to get to them."); }); } [Test] public void ACompleteSearchReportsNoIssues() { var result = SemanticSearchTool.DescribeResult(MINUTES, RetrievalPage.EMPTY, 0, 0); Assert.Multiple(() => { Assert.That(result.ContainsKey("issues"), Is.False, "An empty list of issues would cost tokens in every result for nothing."); Assert.That(result["id"]!.GetValue(), Is.EqualTo(MINUTES.Id)); Assert.That(result["has_more"]!.GetValue(), Is.False); }); } private static IReadOnlyList Issues(JsonObject result) => result["issues"] is JsonArray issues ? issues.Select(issue => issue!.GetValue()).ToList() : []; }