mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 14:58:00 +00:00
Fixed media transcripts reaching the AI without the prompt injection filter (#1083)
This commit is contained in:
1 parent
8e3660a74a
commit
34d88f723c
14 files changed
+423
-63
No files matched your search
+17
-6
@@ -28,6 +28,14 @@ public partial class AssistantBatchProcessing
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.TakeExtractedContentAsync(fileResult, extraction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hands on what the runtime read for a file, or records why there is nothing to hand on.
|
||||
/// </summary>
|
||||
private async Task<string?> TakeExtractedContentAsync(BatchProcessingFileResult fileResult, FileExtractionResult extraction)
|
||||
{
|
||||
//
|
||||
// The user stopped the batch run while we were reading this file. That says nothing about
|
||||
// the file, so it gets the same status as a cancelled AI request instead of a failure:
|
||||
@@ -80,20 +88,23 @@ public partial class AssistantBatchProcessing
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Through the runtime like every document, so its filter for prompt injections checks the
|
||||
// transcript as well. Like the documents of a run, a filtered transcript is reported right
|
||||
// away instead of when the run ends, which may be hours later:
|
||||
//
|
||||
FileExtractionResult extraction;
|
||||
try
|
||||
{
|
||||
return await File.ReadAllTextAsync(transcript.TranscriptPath, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled."));
|
||||
return null;
|
||||
extraction = await PersistentMediaTranscript.ReadTranscriptAsync(this.RustService, transcript, fileResult.FilePath, reportingScope: null, token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the existing transcript: {0}"), e.Message), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.TakeExtractedContentAsync(fileResult, extraction);
|
||||
}
|
||||
|
||||
private static bool IsTranscribableMedia(string filePath) => PersistentMediaTranscript.IsMediaPath(filePath);
|
||||
|
||||
@@ -164,6 +164,7 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
}
|
||||
|
||||
/// <summary>Applies a completed target transcript after progress or navigation.</summary>
|
||||
/// <remarks>The transcript arrives filtered for prompt injections, cf. MediaTranscriptionService.</remarks>
|
||||
private async Task SyncCompletedMediaTextAsync()
|
||||
{
|
||||
var delivery = this.MediaTranscriptionService.GetPendingDelivery(this.EffectiveMediaImportTarget);
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Text;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Security;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
@@ -155,6 +157,29 @@ internal static class PersistentMediaTranscript
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a transcript for an AI, through the runtime like every document.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The runtime filters prompt injections while it reads, so a transcript must never reach an AI
|
||||
/// by any other way, such as reading the file directly. What the filter removed is reported under
|
||||
/// the recording: the transcript is a file AI Studio made, while the recording is the file the
|
||||
/// user chose.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The runtime service.</param>
|
||||
/// <param name="transcript">A transcript with the status READY.</param>
|
||||
/// <param name="mediaPath">The audio or video file the transcript belongs to.</param>
|
||||
/// <param name="reportingScope">The action the transcript is read for, or null when it is read for none.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The filtered text of the transcript, or why it could not be read.</returns>
|
||||
public static Task<FileExtractionResult> ReadTranscriptAsync(RustService rustService, PersistentMediaTranscriptResult transcript, string mediaPath, PromptInjectionReportingScope? reportingScope, CancellationToken token)
|
||||
{
|
||||
if (transcript.Status is not PersistentMediaTranscriptStatus.READY)
|
||||
throw new ArgumentException($"Only a ready transcript can be read, but its status is {transcript.Status}.", nameof(transcript));
|
||||
|
||||
return rustService.ReadArbitraryFileData(transcript.TranscriptPath, int.MaxValue, reportAs: PromptInjectionSource.FileContent(mediaPath), reportingScope: reportingScope, token: token);
|
||||
}
|
||||
|
||||
private static PersistentMediaTranscriptResult SourceChanged(MediaTranscriptSlot slot) => PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.SOURCE_CHANGED, slot, TB("The media file changed while it was being transcribed, so its transcript was not kept."));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace AIStudio.Tools.Media;
|
||||
/// <remarks>
|
||||
/// The transcript itself is not part of it: whoever needs the text reads the file at
|
||||
/// TranscriptPath, so a transcript reaches the AI by the same path no matter whether it was just
|
||||
/// made or stored long ago.
|
||||
/// made or stored long ago. That read always goes through the runtime, which filters prompt
|
||||
/// injections: PersistentMediaTranscript.ReadTranscriptAsync reads a transcript as a whole, and a
|
||||
/// data source reads it in pieces for its index.
|
||||
/// </remarks>
|
||||
/// <param name="Status">How the request ended.</param>
|
||||
/// <param name="TranscriptPath">The path of the transcript; empty when the place could not be determined.</param>
|
||||
|
||||
@@ -5,6 +5,7 @@ using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Security;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
@@ -14,6 +15,7 @@ namespace AIStudio.Tools.Services;
|
||||
public sealed class MediaTranscriptionService(
|
||||
RustService rustService,
|
||||
SettingsManager settingsManager,
|
||||
PromptInjectionGuardService promptInjectionGuardService,
|
||||
IEnumerable<IMediaTranscriptStorage> transcriptStorages,
|
||||
ILogger<MediaTranscriptionService> logger) : IDisposable
|
||||
{
|
||||
@@ -210,7 +212,7 @@ public sealed class MediaTranscriptionService(
|
||||
{
|
||||
var result = await this.TranscribeImportAsync(mediaPath, target, cancellation.Token);
|
||||
if (result.Status is MediaTranscriptionResultStatus.SUCCEEDED)
|
||||
this.AddCompletedText(target, result.Text);
|
||||
await this.AddCompletedTextAsync(target, mediaPath, result.Text);
|
||||
else if (result.Status is MediaTranscriptionResultStatus.CANCELLED)
|
||||
status = MediaImportStatus.CANCELLED;
|
||||
else if (result.Status is MediaTranscriptionResultStatus.NO_AUDIBLE_SIGNAL)
|
||||
@@ -248,8 +250,18 @@ public sealed class MediaTranscriptionService(
|
||||
}
|
||||
|
||||
/// <summary>Stores a completed field transcript for reattachment after navigation.</summary>
|
||||
private void AddCompletedText(MediaImportTarget target, string text)
|
||||
/// <remarks>
|
||||
/// The field hands the transcript on to the AI, so it passes the filter for prompt injections
|
||||
/// first, like every document loaded into such a field, and is reported right away like one.
|
||||
/// It is filtered here rather than where the field takes it: only here is the recording known,
|
||||
/// which is what the user hears about, and the field may look for its transcript more than once.
|
||||
/// </remarks>
|
||||
/// <param name="target">The field which receives the transcript.</param>
|
||||
/// <param name="mediaPath">The recording the transcript belongs to.</param>
|
||||
/// <param name="transcript">The transcript as the provider returned it.</param>
|
||||
internal async Task AddCompletedTextAsync(MediaImportTarget target, string mediaPath, string transcript)
|
||||
{
|
||||
var text = await promptInjectionGuardService.SanitizeAsync(transcript, PromptInjectionSource.FileContent(mediaPath), reportingScope: null);
|
||||
lock (this.stateLock)
|
||||
{
|
||||
if (!this.pendingDeliveries.TryGetValue(target, out var pending))
|
||||
|
||||
@@ -37,8 +37,13 @@ public sealed partial class RustService
|
||||
/// attachment: nothing leaves the app on that path, so there is nothing to warn about, and
|
||||
/// reporting it there would warn a second time when the file is actually sent.
|
||||
/// </param>
|
||||
/// <param name="reportAs">
|
||||
/// The source the user is told about when passages were filtered out, or null for the file
|
||||
/// itself. A file which only holds what AI Studio made of another one, e.g. the transcript of a
|
||||
/// recording, names the file the user knows instead.
|
||||
/// </param>
|
||||
/// <param name="reportingScope">The action the file is read for, or null when it is read for none.</param>
|
||||
public async Task<FileExtractionResult> ReadArbitraryFileData(string path, int maxChunks, bool extractImages = false, bool reportPromptInjections = true, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default)
|
||||
public async Task<FileExtractionResult> ReadArbitraryFileData(string path, int maxChunks, bool extractImages = false, bool reportPromptInjections = true, PromptInjectionSource? reportAs = null, PromptInjectionReportingScope? reportingScope = null, CancellationToken token = default)
|
||||
{
|
||||
//
|
||||
// The runtime filters prompt injections while it streams the file. Doing it there rather
|
||||
@@ -250,7 +255,7 @@ public sealed partial class RustService
|
||||
// where the content never leaves the app.
|
||||
//
|
||||
if (reportPromptInjections)
|
||||
await guardService.ReportAsync(new(PromptInjectionSource.FileContent(path), promptInjectionFindings, promptInjectionRedactedCount), reportingScope);
|
||||
await guardService.ReportAsync(new(reportAs ?? PromptInjectionSource.FileContent(path), promptInjectionFindings, promptInjectionRedactedCount), reportingScope);
|
||||
|
||||
//
|
||||
// Filtering does not change the outcome: the passages were removed and the document
|
||||
|
||||
@@ -27,7 +27,8 @@ public sealed partial class RustService
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// File content does not go through here: the runtime filters it while it streams the file.
|
||||
/// This is the path for content the app fetched itself, i.e. web pages and retrieval contexts.
|
||||
/// This is the path for content the app holds itself, e.g. web pages, retrieval contexts, and
|
||||
/// the transcript of a recording loaded into the field of an assistant.
|
||||
/// </remarks>
|
||||
/// <param name="text">The content to filter.</param>
|
||||
/// <returns>The filtered content and what was found or null when the runtime could not be reached.</returns>
|
||||
|
||||
@@ -62,6 +62,22 @@ public sealed partial class RustService : BackgroundService
|
||||
this.extractionHttp = CreateHttpClient(apiPort, certificateFingerprint, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Talks to a runtime which a test plays itself.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only for tests. The runtime filters prompt injections out of every file and text, so without
|
||||
/// it no test could check which file the app asks for, or what it does with the answer.
|
||||
/// </remarks>
|
||||
/// <param name="runtime">Answers every request in place of the runtime.</param>
|
||||
internal RustService(HttpMessageHandler runtime)
|
||||
{
|
||||
this.apiPort = string.Empty;
|
||||
this.certificateFingerprint = string.Empty;
|
||||
this.http = new HttpClient(runtime, disposeHandler: false) { BaseAddress = new Uri("https://127.0.0.1"), Timeout = TimeSpan.FromSeconds(100) };
|
||||
this.extractionHttp = new HttpClient(runtime, disposeHandler: false) { BaseAddress = new Uri("https://127.0.0.1"), Timeout = Timeout.InfiniteTimeSpan };
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient(string apiPort, string certificateFingerprint, TimeSpan timeout)
|
||||
{
|
||||
var certificateValidationHandler = new HttpClientHandler
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
- Fixed AI Studio occasionally accepting an API key, token, or password which was encrypted with the secret of another organization, instead of refusing it. AI Studio then used a wrong key, so the provider, data source, or tool failed later with a misleading error. Such a value is now always refused in the new protected format, and almost always in the previous one.
|
||||
- Fixed AI Studio overlooking suspicious instructions (prompt injection) in files such as text or Markdown files when the instructions continued on the next line.
|
||||
- Fixed AI Studio removing harmless lines from files such as text or Markdown files, mistaking them for suspicious instructions (prompt injection).
|
||||
- Fixed AI Studio not checking the transcripts of audio and video files for suspicious instructions (prompt injection) in the Batch Processing assistant and when you load such a file into a field of an assistant. AI Studio now checks these transcripts like all other documents before the AI sees them, even very long ones.
|
||||
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
|
||||
- Upgraded several libraries to improve security.
|
||||
- Upgraded to Rust v1.99.0
|
||||
|
||||
Reference in new issue
Block a user