using AIStudio.Tools.Security;
namespace AIStudio.Tools.Web;
///
/// Filters prompt injections out of the web page content a tool returns to a model.
///
///
/// Web search and reading a single page share this: both hand the model text they fetched from
/// the public web, and neither may pass it on unchecked. All pages of one tool call are filtered
/// in a single runtime request, so a search across five pages costs one round trip and produces
/// one report for the user.
///
public static class WebPageContentSanitizer
{
///
/// How many single-value fields each page contributes, in the order they are collected. The
/// author list follows them and varies in length, so rebuilding depends on this being right.
///
private const int SINGLE_VALUE_FIELD_COUNT = 6;
///
/// Filters every model-facing text of the given pages.
///
/// The guard service performing the filtering.
/// The page contents to filter, each with the source it came from.
/// The filtered contents, in the order they came in.
public static async Task> SanitizeAsync(PromptInjectionGuardService guardService,
IReadOnlyList<(WebPageModelContent Content, PromptInjectionSource Source)> pages)
{
if (pages.Count is 0)
return [];
List texts = [];
foreach (var (content, source) in pages)
{
texts.Add(new(content.Markdown, source));
texts.Add(new(content.Title, source));
texts.Add(new(content.Description, source));
texts.Add(new(content.Language, source));
texts.Add(new(content.PublishedTime, source));
texts.Add(new(content.ModifiedTime, source));
foreach (var author in content.Authors)
texts.Add(new(author, source));
}
var sanitizedTexts = await guardService.SanitizeAsync(texts);
var sanitizedPages = new List(pages.Count);
var offset = 0;
foreach (var (content, _) in pages)
{
var authors = new List(content.Authors.Count);
for (var authorIndex = 0; authorIndex < content.Authors.Count; authorIndex++)
authors.Add(sanitizedTexts[offset + SINGLE_VALUE_FIELD_COUNT + authorIndex]);
sanitizedPages.Add(new(
sanitizedTexts[offset],
sanitizedTexts[offset + 1],
sanitizedTexts[offset + 2],
authors,
sanitizedTexts[offset + 3],
sanitizedTexts[offset + 4],
sanitizedTexts[offset + 5]));
offset += SINGLE_VALUE_FIELD_COUNT + content.Authors.Count;
}
return sanitizedPages;
}
///
/// Filters every model-facing text of a single page.
///
public static async Task SanitizeAsync(
PromptInjectionGuardService guardService,
WebPageModelContent content,
PromptInjectionSource source) => (await SanitizeAsync(guardService, [(content, source)]))[0];
}