Fixed answers that quietly skipped your data sources (#968)

This commit is contained in:
Thorsten Sommer 2026-09-14 11:20:57 +02:00 committed by GitHub
parent 828541ce93
commit 6e735523c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 142 additions and 8 deletions

View File

@ -3346,6 +3346,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3267850764"] = "The selected mode
-- We could load models from '{0}', but the provider did not return any usable text models.
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T3378120620"] = "We could load models from '{0}', but the provider did not return any usable text models."
-- Your data sources could not be used. This answer was created without them.
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T373499115"] = "Your data sources could not be used. This answer was created without them."
-- The local image file does not exist. Skipping the image.
UI_TEXT_CONTENT["AISTUDIO::CHAT::IIMAGESOURCEEXTENSIONS::T255679918"] = "The local image file does not exist. Skipping the image."
@ -10462,6 +10465,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3267850764"] = "The sel
-- We could load models from '{0}', but the provider did not return any usable text models.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3378120620"] = "We could load models from '{0}', but the provider did not return any usable text models."
-- Your data sources could not be used. This answer was created without them.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T373499115"] = "Your data sources could not be used. This answer was created without them."
-- Software Development
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T1025369409"] = "Software Development"
@ -11566,15 +11572,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTYPEEXTENSIONS::T335338363
-- Standard augmentation process
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T1072508429"] = "Standard augmentation process"
-- No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T2710880477"] = "No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found."
-- This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T3240406069"] = "This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread."
-- The check of which passages fit your question failed. This answer uses all passages that were found.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T392269104"] = "The check of which passages fit your question failed. This answer uses all passages that were found."
-- Automatic AI data source selection with heuristik source reduction
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCSELWITHDYNHEUR::T2339257645"] = "Automatic AI data source selection with heuristik source reduction"
-- Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCSELWITHDYNHEUR::T648937779"] = "Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources."
-- None of your selected data sources is available for the chosen provider. This answer was created without them.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T1696726639"] = "None of your selected data sources is available for the chosen provider. This answer was created without them."
-- This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3047786484"] = "This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context."

View File

@ -85,9 +85,25 @@ public sealed class ContentText : IContent
var rag = new AISrcSelWithRetCtxVal();
chatThread = await rag.ProcessAsync(provider, lastUserPrompt, chatThread, token);
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
//
// The user canceled the request. That is not an error, and it must not reach the
// user as one. We do not rethrow here: the streaming task below observes the same
// token and ends the request itself, which keeps its finally block intact. That
// block is what tells the UI that the streaming is over.
//
LOGGER.LogInformation("The RAG process was canceled before the answer was requested.");
}
catch (Exception e)
{
LOGGER.LogError(e, "Skipping the RAG process due to an error.");
//
// The answer is about to be created without the data the user expected it to use.
// Without this message, that answer is indistinguishable from one that did use it:
//
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Source, TB("Your data sources could not be used. This answer was created without them.")));
}
}

View File

@ -1025,7 +1025,16 @@ public partial class ChatComponent : MSGComponentBase
}
}
else
lastUserPrompt = this.ChatThread.Blocks.Last(x => x.Role is ChatRole.USER).Content;
{
//
// Regenerating asks again with the prompt that led to this answer. A thread which never
// carried one -- a chat template whose example conversation holds AI blocks only -- has
// nothing to reuse here. That is no reason to fail: the thread itself is what the model
// is given, and everything downstream already reads a missing prompt as "no data source
// lookup, just answer again".
//
lastUserPrompt = this.ChatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.USER)?.Content;
}
//
// Add the AI response to the thread:

View File

@ -240,6 +240,12 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
catch (Exception e)
{
logger.LogError(e, "Skipping the RAG process due to an error.");
//
// The answer is about to be created without the data the user expected it to use.
// Without this message, that answer is indistinguishable from one that did use it:
//
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Source, TB("Your data sources could not be used. This answer was created without them.")));
}
token.ThrowIfCancellationRequested();

View File

@ -2,6 +2,8 @@ namespace AIStudio.Tools;
public static class IConfidenceExtensions
{
private static readonly ILogger<IConfidence> LOGGER = Program.LOGGER_FACTORY.CreateLogger<IConfidence>();
public static TargetWindow DetermineTargetWindow<T>(this IReadOnlyList<T> items, TargetWindowStrategy strategy, int numMaximumItems = 30) where T : IConfidence
{
switch (strategy)
@ -53,8 +55,18 @@ public static class IConfidenceExtensions
{
if(!targetWindow.IsValid())
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<IConfidence>>()!;
logger.LogWarning("The target window is invalid. Returning 0f as threshold.");
LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold.");
return 0f;
}
//
// Without items there is no threshold to find, and the Min and Max calls below would throw
// on an empty sequence. Every caller checks this today, which is precisely how such a guard
// goes missing once a new caller arrives. It belongs here, next to the calls it protects:
//
if(items.Count == 0)
{
LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold.");
return 0f;
}
@ -91,10 +103,7 @@ public static class IConfidenceExtensions
}
}
else
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<IConfidence>>()!;
logger.LogWarning("The confidence values are too close. Returning 0f as threshold.");
}
LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold.");
return threshold;
}

View File

@ -70,10 +70,19 @@ public sealed class AugmentationOne : IAugmentationProcess
catch (Exception exception)
{
LOGGER.LogError(exception, "Retrieval context validation failed. Continuing augmentation with all retrieved contexts.");
//
// The user switched this check on. Continuing without it silently would hide
// that the answer rests on unfiltered passages:
//
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.FactCheck, TB("The check of which passages fit your question failed. This answer uses all passages that were found.")));
}
}
else
{
LOGGER.LogWarning("Skipping retrieval context validation because no sufficiently trusted validation agent provider is available. Continuing augmentation with all retrieved contexts.");
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.FactCheck, TB("No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found.")));
}
}
LOGGER.LogInformation($"Starting the augmentation process over {retrievalContexts.Count:###,###,###,###} of {numTotalRetrievalContexts:###,###,###,###} retrieved contexts.");

View File

@ -50,7 +50,7 @@ public class AgenticSrcSelWithDynHeur : IDataSourceSelectionProcess
}
// Log the selected data sources:
var selectedDataSourceInfo = aiSelectedDataSources.Select(ds => $"[Id={ds.Id}, reason={ds.Reason}, confidence={ds.Confidence}]").Aggregate((a, b) => $"'{a}', '{b}'");
var selectedDataSourceInfo = string.Join(", ", aiSelectedDataSources.Select(ds => $"'[Id={ds.Id}, reason={ds.Reason}, confidence={ds.Confidence}]'"));
LOGGER.LogInformation($"The AI selected the data sources automatically. {aiSelectedDataSources.Count} data source(s) are selected: {selectedDataSourceInfo}.");
//

View File

@ -106,6 +106,15 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
{
LOGGER.LogWarning("No data sources are selected. The RAG process is skipped.");
proceedWithRAG = false;
//
// When the user picked the sources, none of them survived the security and
// confidence checks. That is worth saying out loud: the user chose them and
// expects this answer to use them. When the AI picked instead, finding nothing
// suitable for this prompt is a normal outcome and stays in the log.
//
if(!chatThread.DataSourceOptions.AutomaticDataSourceSelection)
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Source, TB("None of your selected data sources is available for the chosen provider. This answer was created without them.")));
}
else
{

View File

@ -40,4 +40,7 @@
- Fixed the copy button leaving the sources behind. Copy an answer, and its sources come along.
- Fixed a tile that opens a chat directly always demanding a workspace. Leave the workspace empty in the Assistant Builder, and the tile opens a disappearing chat instead.
- Fixed the same restriction for plugin authors: a direct-chat launcher can now open a chat without naming a workspace. The example assistant plugin shows both ways.
- Fixed an answer built without your data sources looking exactly like one that used them. When AI Studio cannot reach the sources you picked, it now tells you instead of quietly answering without them.
- Fixed the same silence when the step that picks the fitting passages out of your documents cannot run. You are told that the answer rests on everything that was found.
- Fixed the regenerate button taking an answer away without producing a new one. This happened in chats started from a template that holds no question of your own.
- Upgraded the Visual Briefing Assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing Assistant there.

View File

@ -0,0 +1,58 @@
using AIStudio.Tools;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks that determining a confidence threshold survives a list with nothing in it.
/// </summary>
/// <remarks>
/// GetConfidenceThreshold reaches for Min and Max, both of which throw on an empty sequence. Until
/// v26.9.1 the only thing standing between them and that exception was a count check at each of the
/// two call sites. The RAG process already lost such a guard once -- a log line was written before
/// the check that was meant to protect it, which is how every new chat produced an
/// InvalidOperationException nobody noticed, because the RAG process catches and logs everything.
/// The threshold is now asked to defend itself, and these tests hold it to that.
/// </remarks>
[TestFixture]
public sealed class ConfidenceThresholdTests
{
private readonly record struct Decision(float Confidence) : IConfidence;
[Test]
public void AnEmptyListYieldsAThresholdInsteadOfThrowing()
{
IReadOnlyList<Decision> nothingWasDecided = [];
var targetWindow = new TargetWindow(1, 2, 3, 0f);
Assert.That(nothingWasDecided.GetConfidenceThreshold(targetWindow), Is.EqualTo(0f), "A threshold of zero keeps everything that follows, which is the harmless answer for a list that holds nothing to filter.");
}
[Test]
public void AnEmptyListIsAnsweredEvenWhenTheWindowDemandsItems()
{
//
// The window asks for between five and ten items while not a single one exists. The guard
// has to hold regardless of what the window wants:
//
IReadOnlyList<Decision> nothingWasDecided = [];
var demandingWindow = new TargetWindow(4, 5, 10, 0.5f);
Assert.That(nothingWasDecided.GetConfidenceThreshold(demandingWindow), Is.EqualTo(0f), "The number of items the window asks for cannot conjure items to measure.");
}
[Test]
public void ASpreadOfDecisionsIsNarrowedToTheTargetWindow()
{
//
// Guarding the empty case must not change what the threshold does with actual items. Two
// weak decisions and three strong ones, with a window asking for two to three:
//
IReadOnlyList<Decision> decisions = [new(0.1f), new(0.2f), new(0.9f), new(0.95f), new(1.0f)];
var targetWindow = new TargetWindow(1, 2, 3, 0f);
var threshold = decisions.GetConfidenceThreshold(targetWindow);
var survivors = decisions.Count(decision => decision.Confidence >= threshold);
Assert.That(survivors, Is.InRange(targetWindow.TargetWindowMin, targetWindow.TargetWindowMax), "The threshold exists to cut a list down to the size the window asks for, and the three strong decisions are what should be left.");
}
}