using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tests.Tools.ToolCalling;
using AIStudio.Tools.RAG.RAGProcesses;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.SemanticSearch;
namespace AIStudio.Tests.Tools;
///
/// Checks where a chat may still send data once the classic RAG process found something in its data sources.
///
///
/// The classic process brings passages into the chat without any tool. Afterwards, a web search
/// or an address the model writes itself could carry them to a third party, just as after Semantic
/// Search. Both ways have to demand the same of the same data sources, or one of them stays open.
/// The process runs before the request puts its tools together, so the restriction it sets already
/// holds for the answer it prepares.
///
[TestFixture]
[NonParallelizable]
public sealed class ClassicRagOutboundDataRestrictionTests : ToolRegistryTestBase
{
private const string WEB_SEARCH = "web_search_test";
private static readonly IDataSource PROJECT_FILES = new DataSourceLocalDirectory { Num = 1, Id = "11111111-1111-1111-1111-111111111111", Name = "Project files", MaxMatches = 10, ConfidenceLevel = ConfidenceLevel.MEDIUM };
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, OutboundDataRestriction = OutboundDataRestriction.UNRESTRICTED };
private static readonly IDataSource PRESS_RELEASES = new DataSourceLocalFile { Num = 4, Id = "44444444-4444-4444-4444-444444444444", Name = "Press releases", MaxMatches = 10, OutboundDataRestriction = OutboundDataRestriction.UNRESTRICTED };
private static IEnumerable Searches
{
get
{
yield return new TestCaseData(new[] { PROJECT_FILES, MINUTES }, new[] { 0, 0 }, OutboundDataRestriction.UNRESTRICTED).SetName("Nothing found");
yield return new TestCaseData(new[] { PROJECT_FILES, PRESS_RELEASES }, new[] { 0, 3 }, OutboundDataRestriction.UNRESTRICTED).SetName("Found only content anybody may read");
yield return new TestCaseData(new[] { PROJECT_FILES, MINUTES }, new[] { 2, 1 }, OutboundDataRestriction.UNRESTRICTED).SetName("Two equally strict data sources");
yield return new TestCaseData(new[] { PRESS_RELEASES, INTRANET, PROJECT_FILES }, new[] { 1, 1, 1 }, OutboundDataRestriction.UNRESTRICTED).SetName("A data source for self-hosted providers only");
yield return new TestCaseData(new[] { PRESS_RELEASES, MINUTES }, new[] { 2, 0 }, OutboundDataRestriction.ONLY_CONFIGURED_SERVICES).SetName("A minimum of the organization");
}
}
[Test]
public void ARetrievalWhichFoundNothingLeavesTheChatAsItWas()
{
var chat = new ChatThread();
chat.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.CONFLUENCE_WIKI, string.Empty));
AISrcSelWithRetCtxVal.RequireOutboundDataRestriction(chat, [PROJECT_FILES, INTRANET], [0, 0], OutboundDataRestriction.UNRESTRICTED);
Assert.That(chat.RequiredOutboundDataRestriction, Is.EqualTo(new OutboundDataRequirement(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.CONFLUENCE_WIKI, string.Empty)), "Neither data source brought anything into the chat.");
}
[Test]
public void OnlyTheDataSourcesWithPassagesRestrictTheChat()
{
var chat = new ChatThread();
AISrcSelWithRetCtxVal.RequireOutboundDataRestriction(chat, [INTRANET, MINUTES], [0, 4], OutboundDataRestriction.UNRESTRICTED);
Assert.That(chat.RequiredOutboundDataRestriction, Is.EqualTo(new OutboundDataRequirement(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, MINUTES.Id)), "The intranet found nothing, so it demands nothing either.");
}
[TestCaseSource(nameof(Searches))]
public void BothWaysOfSearchingRestrictTheChatAlike(IDataSource[] dataSources, int[] passageCounts, OutboundDataRestriction organizationMinimum)
{
//
// Semantic Search offers the data sources in a fixed order, while the selection agent of the
// classic process may return them in any order. Both have to name the same data source.
//
// AsEnumerable() keeps Reverse() a reversed copy: from C# 14 on, Reverse() on an array
// reverses the array in place and returns nothing.
//
var classicChat = new ChatThread();
AISrcSelWithRetCtxVal.RequireOutboundDataRestriction(classicChat, [..dataSources.AsEnumerable().Reverse()], [..passageCounts.AsEnumerable().Reverse()], organizationMinimum);
var semanticSearchChat = new ChatThread();
semanticSearchChat.RequireOutboundDataRestriction(SemanticSearchTool.GetRequirements(dataSources, passageCounts, organizationMinimum).OutboundData);
Assert.That(classicChat.RequiredOutboundDataRestriction, Is.EqualTo(semanticSearchChat.RequiredOutboundDataRestriction));
}
[Test]
public async Task TheAnswerWhoseRetrievalRestrictedTheChatIsNotOfferedTheWebSearch()
{
var webSearch = new TestTool(Definition(WEB_SEARCH)) { OutboundData = ToolOutboundData.THIRD_PARTY_QUERIES };
var registry = this.CreateRegistry(webSearch);
//
// The chat puts the tools of the answer together before the classic process runs, while
// the chat is still unrestricted. Then the process finds something:
//
var chat = new ChatThread { RuntimeSelectedToolIds = [WEB_SEARCH] };
AISrcSelWithRetCtxVal.RequireOutboundDataRestriction(chat, [PROJECT_FILES], [2], OutboundDataRestriction.UNRESTRICTED);
var provider = ToolCapableProvider();
var context = new ToolResolutionContext
{
Provider = provider,
Component = AIStudio.Tools.Components.CHAT,
ProviderConfidence = provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level,
ChatThread = chat,
};
var runnableTools = await registry.GetRunnableToolsAsync(context, chat.RuntimeSelectedToolIds, mayRunTools: true);
Assert.That(runnableTools.Select(tool => tool.Definition.Id), Does.Not.Contain(WEB_SEARCH), "The request reads the restriction when it puts its tools together, after the classic process.");
}
}