Added a feature so that matching results from data sources are also displayed at the end of a chat

This commit is contained in:
Thorsten Sommer 2025-09-25 19:44:19 +02:00
parent 5d173b04b2
commit 522913c2a5
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
15 changed files with 122 additions and 24 deletions

View File

@ -4882,9 +4882,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
-- no model selected
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
-- Sources
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
@ -5641,6 +5638,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No u
-- Failed to install update automatically. Please try again manually.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Failed to install update automatically. Please try again manually."
-- Sources provided by the data providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources provided by the data providers"
-- Sources provided by the AI
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
-- The hostname is not a valid HTTP(S) URL.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "The hostname is not a valid HTTP(S) URL."

View File

@ -28,7 +28,7 @@ public sealed class ContentImage : IContent, IImageSource
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public List<ISource> Sources { get; set; } = [];
public List<Source> Sources { get; set; } = [];
/// <inheritdoc />
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatChatThread, CancellationToken token = default)

View File

@ -38,7 +38,7 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public List<ISource> Sources { get; set; } = [];
public List<Source> Sources { get; set; } = [];
/// <inheritdoc />
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)

View File

@ -41,8 +41,12 @@ public interface IContent
/// <summary>
/// The provided sources, if any.
/// </summary>
/// <remarks>
/// We cannot use ISource here because System.Text.Json does not support
/// interface serialization. So we have to use a concrete class.
/// </remarks>
[JsonIgnore]
public List<ISource> Sources { get; set; }
public List<Source> Sources { get; set; }
/// <summary>
/// Uses the provider to create the content.

View File

@ -4884,9 +4884,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
-- no model selected
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgewählt"
-- Sources
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Quellen"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
@ -5643,6 +5640,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "Kein
-- Failed to install update automatically. Please try again manually.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Fehler bei der automatischen Installation des Updates. Bitte versuchen Sie es manuell erneut."
-- Sources provided by the data providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Von den Datenanbietern bereitgestellte Quellen"
-- Sources provided by the AI
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Von der KI bereitgestellte Quellen"
-- The hostname is not a valid HTTP(S) URL.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "Der Hostname ist keine gültige HTTP(S)-URL."

View File

@ -4884,9 +4884,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un
-- no model selected
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
-- Sources
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
@ -5643,6 +5640,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No u
-- Failed to install update automatically. Please try again manually.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T3709709946"] = "Failed to install update automatically. Please try again manually."
-- Sources provided by the data providers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources provided by the data providers"
-- Sources provided by the AI
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI"
-- The hostname is not a valid HTTP(S) URL.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"] = "The hostname is not a valid HTTP(S) URL."

View File

@ -30,7 +30,7 @@ public record ChatCompletionAnnotationStreamLine(string Id, string Object, uint
{
// Check if the annotation is of the expected type and extract the source information:
if (annotation is ChatCompletionAnnotatingURL urlAnnotation)
sources.Add(new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL));
sources.Add(new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL, SourceOrigin.LLM));
//
// Check for the unexpected annotation type of the Responses API.
@ -46,7 +46,7 @@ public record ChatCompletionAnnotationStreamLine(string Id, string Object, uint
// we are calling the chat completion endpoint.
//
if (annotation is ResponsesAnnotatingUrlCitationData citationData)
sources.Add(new Source(citationData.Title, citationData.URL));
sources.Add(new Source(citationData.Title, citationData.URL, SourceOrigin.LLM));
}
}

View File

@ -32,11 +32,11 @@ public sealed record ResponsesAnnotationStreamLine(string Type, int AnnotationIn
// into that type, even though we are calling the Responses API endpoint.
//
if (this.Annotation is ChatCompletionAnnotatingURL urlAnnotation)
return [new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL)];
return [new Source(urlAnnotation.UrlCitation.Title, urlAnnotation.UrlCitation.URL, SourceOrigin.LLM)];
// Check for the expected annotation type of the Responses API:
if (this.Annotation is ResponsesAnnotatingUrlCitationData urlCitationData)
return [new Source(urlCitationData.Title, urlCitationData.URL)];
return [new Source(urlCitationData.Title, urlCitationData.URL, SourceOrigin.LLM)];
return [];
}

View File

@ -5,4 +5,4 @@ namespace AIStudio.Provider.Perplexity;
/// </summary>
/// <param name="Title">The title of the search result.</param>
/// <param name="URL">The URL of the search result.</param>
public sealed record SearchResult(string Title, string URL) : Source(Title, URL);
public sealed record SearchResult(string Title, string URL) : Source(Title, URL, SourceOrigin.LLM);

View File

@ -14,4 +14,9 @@ public interface ISource
/// The URL of the source.
/// </summary>
public string URL { get; }
/// <summary>
/// The origin of the source, whether it was provided by the AI or by the RAG process.
/// </summary>
public SourceOrigin Origin { get; }
}

View File

@ -189,6 +189,36 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
var augmentationProcess = new AugmentationOne();
chatThread = await augmentationProcess.ProcessAsync(provider, lastUserPrompt, chatThread, dataContexts, token);
}
//
// Add sources from the selected data
//
// We know that the last block is the AI answer block (cf. check above):
var aiAnswerBlock = chatThread.Blocks.Last();
var aiAnswerSources = aiAnswerBlock.Content?.Sources;
// It should never happen that the AI answer block does not contain a content part.
// Just in case, we check this:
if(aiAnswerSources is null)
return chatThread;
var ragSources = new List<ISource>();
foreach (var retrievalContext in dataContexts)
{
var title = retrievalContext.DataSourceName;
if(string.IsNullOrWhiteSpace(title))
continue;
var link = retrievalContext.Path;
if(!link.StartsWith("http", StringComparison.OrdinalIgnoreCase))
continue;
ragSources.Add(new Source(title, link, SourceOrigin.RAG));
}
// Merge the sources, avoiding duplicates:
aiAnswerSources.MergeSources(ragSources);
}
return chatThread;

View File

@ -5,4 +5,4 @@ namespace AIStudio.Tools;
/// </summary>
/// <param name="Title">The title of the source.</param>
/// <param name="URL">The URL of the source.</param>
public record Source(string Title, string URL) : ISource;
public record Source(string Title, string URL, SourceOrigin Origin) : ISource;

View File

@ -13,14 +13,46 @@ public static class SourceExtensions
/// </summary>
/// <param name="sources">The list of sources to convert.</param>
/// <returns>A markdown-formatted string representing the sources.</returns>
public static string ToMarkdown(this IList<ISource> sources)
public static string ToMarkdown(this IList<Source> sources)
{
var sb = new StringBuilder();
sb.Append("## ");
sb.AppendLine(TB("Sources"));
var ragSources = new List<ISource>();
var sourceNum = 0;
var addedLLMHeaders = false;
foreach (var source in sources)
{
switch (source.Origin)
{
case SourceOrigin.RAG:
ragSources.Add(source);
break;
case SourceOrigin.LLM:
if (!addedLLMHeaders)
{
sb.Append("## ");
sb.AppendLine(TB("Sources provided by the AI"));
addedLLMHeaders = true;
}
sb.Append($"- [{++sourceNum}] ");
sb.Append('[');
sb.Append(source.Title);
sb.Append("](");
sb.Append(source.URL);
sb.AppendLine(")");
break;
}
}
if(ragSources.Count == 0)
return sb.ToString();
sb.AppendLine();
sb.Append("## ");
sb.AppendLine(TB("Sources provided by the data providers"));
foreach (var source in ragSources)
{
sb.Append($"- [{++sourceNum}] ");
sb.Append('[');
@ -38,10 +70,10 @@ public static class SourceExtensions
/// </summary>
/// <param name="sources">The existing list of sources to merge into.</param>
/// <param name="addedSources">The list of sources to add.</param>
public static void MergeSources(this IList<ISource> sources, IList<ISource> addedSources)
public static void MergeSources(this IList<Source> sources, IList<ISource> addedSources)
{
foreach (var addedSource in addedSources)
if (sources.All(s => s.URL != addedSource.URL && s.Title != addedSource.Title))
sources.Add(addedSource);
sources.Add((Source)addedSource);
}
}

View File

@ -0,0 +1,17 @@
namespace AIStudio.Tools;
/// <summary>
/// Represents the origin of a source, whether it was provided by the LLM or by the RAG process.
/// </summary>
public enum SourceOrigin
{
/// <summary>
/// The LLM provided the source.
/// </summary>
LLM,
/// <summary>
/// The source was provided by the RAG process.
/// </summary>
RAG,
}

View File

@ -1,3 +1,4 @@
# v0.9.52, build 227 (2025-09-xx xx:xx UTC)
- Added a feature so that matching results from data sources (local data sources as well as external ones via the ERI interface) are now also displayed at the end of a chat. All sources that come directly from the AI (like web searches) appear first, followed by those that come from the data sources. This source display works regardless of whether the AI actually used these sources, so users always get all the relevant information.
- Improved developer experience by detecting development environments and disabling update prompts in those environments.
- Fixed an issue where external data sources using the ERI interface weren't using the correct source names during the augmentation phase.