2025-02-22 19:51:06 +00:00
using System.Text ;
using AIStudio.Chat ;
2026-08-23 09:09:11 +00:00
using AIStudio.Tools.Security ;
2025-02-22 19:51:06 +00:00
namespace AIStudio.Tools.RAG ;
public static class IRetrievalContextExtensions
{
2026-06-10 19:01:27 +00:00
private static readonly ILogger < IRetrievalContext > LOGGER = Program . LOGGER_FACTORY . CreateLogger < IRetrievalContext > ( ) ;
2025-02-22 19:51:06 +00:00
public static async Task < string > AsMarkdown ( this IReadOnlyList < IRetrievalContext > retrievalContexts , StringBuilder ? sb = null , CancellationToken token = default )
{
sb ? ? = new StringBuilder ( ) ;
var index = 0 ;
2026-08-23 09:09:11 +00:00
//
// One report for the whole retrieval run: a query may pull in dozens of contexts, and
// the user wants to know that something was filtered, not to acknowledge it per context.
//
var guardService = Program . SERVICE_PROVIDER . GetRequiredService < PromptInjectionGuardService > ( ) ;
await using var reportingScope = guardService . BeginAction ( ) ;
2025-02-22 19:51:06 +00:00
foreach ( var retrievalContext in retrievalContexts )
{
index + + ;
await retrievalContext . AsMarkdown ( sb , index , retrievalContexts . Count , token ) ;
}
2026-08-23 09:09:11 +00:00
2025-02-22 19:51:06 +00:00
return sb . ToString ( ) ;
}
public static async Task < string > AsMarkdown ( this IRetrievalContext retrievalContext , StringBuilder ? sb = null , int index = - 1 , int numTotalRetrievalContexts = - 1 , CancellationToken token = default )
{
sb ? ? = new StringBuilder ( ) ;
2026-08-23 09:09:11 +00:00
var contextBuilder = new StringBuilder ( ) ;
2025-02-22 19:51:06 +00:00
switch ( index )
{
case > 0 when numTotalRetrievalContexts is - 1 :
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( $"# Retrieval context {index}" ) ;
2025-02-22 19:51:06 +00:00
break ;
case > 0 when numTotalRetrievalContexts > 0 :
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( $"# Retrieval context {index} of {numTotalRetrievalContexts}" ) ;
2025-02-22 19:51:06 +00:00
break ;
default :
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( "# Retrieval context" ) ;
2025-02-22 19:51:06 +00:00
break ;
}
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( $"Data source name: {retrievalContext.DataSourceName}" ) ;
contextBuilder . AppendLine ( $"Content category: {retrievalContext.Category}" ) ;
contextBuilder . AppendLine ( $"Content type: {retrievalContext.Type}" ) ;
contextBuilder . AppendLine ( $"Content path: {retrievalContext.Path}" ) ;
2025-02-22 19:51:06 +00:00
if ( retrievalContext . Links . Count > 0 )
{
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( "Additional links:" ) ;
2025-02-22 19:51:06 +00:00
foreach ( var link in retrievalContext . Links )
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( $"- {link}" ) ;
2025-02-22 19:51:06 +00:00
}
2026-08-23 09:09:11 +00:00
var guardService = Program . SERVICE_PROVIDER . GetRequiredService < PromptInjectionGuardService > ( ) ;
var source = PromptInjectionSource . RetrievalContext ( retrievalContext . DataSourceName , retrievalContext . Path ) ;
2025-02-22 19:51:06 +00:00
switch ( retrievalContext )
{
case RetrievalTextContext textContext :
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( ) ;
contextBuilder . AppendLine ( "Matched text content:" ) ;
contextBuilder . AppendLine ( "````" ) ;
contextBuilder . AppendLine ( textContext . MatchedText ) ;
contextBuilder . AppendLine ( "````" ) ;
2025-02-22 19:51:06 +00:00
if ( textContext . SurroundingContent . Count > 0 )
{
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( ) ;
contextBuilder . AppendLine ( "Surrounding text content:" ) ;
2025-02-22 19:51:06 +00:00
foreach ( var surrounding in textContext . SurroundingContent )
{
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( ) ;
contextBuilder . AppendLine ( "````" ) ;
contextBuilder . AppendLine ( surrounding ) ;
contextBuilder . AppendLine ( "````" ) ;
2025-02-22 19:51:06 +00:00
}
}
2026-08-23 09:09:11 +00:00
await FilterWhatWeHaveSoFar ( ) ;
2025-02-22 19:51:06 +00:00
break ;
2026-08-23 09:09:11 +00:00
2025-02-22 19:51:06 +00:00
case RetrievalImageContext imageContext :
2026-08-23 09:09:11 +00:00
//
// Filtering happens before the image is appended, and only covers the text
// around it. Base64 image data is not prose, and running it through the filter
// would have it treated as one enormous encoded carrier.
//
await FilterWhatWeHaveSoFar ( ) ;
contextBuilder . AppendLine ( ) ;
contextBuilder . AppendLine ( "Matched image content as base64-encoded data:" ) ;
contextBuilder . AppendLine ( "````" ) ;
contextBuilder . AppendLine ( await imageContext . TryAsBase64 ( token ) is ( success : true , { } base64Image )
? base64Image
2025-12-30 17:30:32 +00:00
: string . Empty ) ;
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( "````" ) ;
2025-02-22 19:51:06 +00:00
break ;
2026-08-23 09:09:11 +00:00
2025-02-22 19:51:06 +00:00
default :
2026-08-23 09:09:11 +00:00
await FilterWhatWeHaveSoFar ( ) ;
2025-02-22 19:51:06 +00:00
LOGGER . LogWarning ( $"The retrieval content type '{retrievalContext.Type}' of data source '{retrievalContext.DataSourceName}' at location '{retrievalContext.Path}' is not supported yet." ) ;
break ;
}
2026-08-23 09:09:11 +00:00
contextBuilder . AppendLine ( ) ;
sb . Append ( contextBuilder ) ;
2025-02-22 19:51:06 +00:00
return sb . ToString ( ) ;
2026-08-23 09:09:11 +00:00
//
// Replaces what has been built so far with its filtered version. A data source is as
// untrusted as any other external content: it may serve text written to steer the model
// rather than to answer the query.
//
async Task FilterWhatWeHaveSoFar ( )
{
var sanitized = await guardService . SanitizeAsync ( contextBuilder . ToString ( ) , source ) ;
contextBuilder . Clear ( ) ;
contextBuilder . Append ( sanitized ) ;
}
2025-02-22 19:51:06 +00:00
}
}