Throttle batch processing with random delays

This commit is contained in:
Thorsten Sommer 2026-08-11 19:37:02 +02:00
parent 9e07155e3d
commit 30340ecbcb
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
12 changed files with 211 additions and 6 deletions

View File

@ -148,6 +148,34 @@ else
@T("We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.")
</MudJustifiedText>
<MudText Typo="Typo.h5" Class="mb-3 mt-6">
@T("Processing pace")
</MudText>
@if (MinimumDelayIsManaged)
{
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-3">
@(string.Format(T("Your organization requires a pause of at least {0} seconds between files."), this.ManagedMinimumDelaySeconds))
</MudAlert>
}
else
{
<MudTextSlider T="int" Label="@T("Minimum pause between files")" Min="@DataBatchProcessing.MIN_DELAY_SECONDS" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" @bind-Value="@this.minimumDelaySeconds" Disabled="@(() => this.isProcessingBatch)"/>
}
<MudTextSlider T="int" Label="@T("Maximum pause between files")" Min="@this.EffectiveMinimumDelaySeconds" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" @bind-Value="@this.maximumDelaySeconds" Disabled="@(() => this.isProcessingBatch)"/>
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause.")
</MudJustifiedText>
@if (this.pauseBeforeNextFileSeconds > 0)
{
<MudAlert Severity="Severity.Info" Icon="@Icons.Material.Filled.HourglassTop" Dense="true" Class="mb-3">
@(string.Format(T("Waiting {0} seconds before starting the next file."), this.pauseBeforeNextFileSeconds))
</MudAlert>
}
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProviderWithBatchState" Disabled="@this.isProcessingBatch" ExplicitMinimumConfidence="@this.GetMinimumConfidenceLevel()"/>
@if (this.fileResults.Count > 0)

View File

@ -0,0 +1,58 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Assistants.BatchProcessing;
public partial class AssistantBatchProcessing
{
[Inject]
private ThreadSafeRandom Rng { get; init; } = null!;
private static bool MinimumDelayIsManaged => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumDelaySeconds, out var meta)
&& meta.ManagedMode is not null;
private int ManagedMinimumDelaySeconds => Math.Clamp(this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds,
DataBatchProcessing.MIN_DELAY_SECONDS,
DataBatchProcessing.MAX_DELAY_SECONDS);
private int EffectiveMinimumDelaySeconds => MinimumDelayIsManaged ? this.ManagedMinimumDelaySeconds
: Math.Clamp(this.minimumDelaySeconds, DataBatchProcessing.MIN_DELAY_SECONDS, DataBatchProcessing.MAX_DELAY_SECONDS);
private (int Minimum, int Maximum) GetEffectiveDelayRange()
{
var minimum = this.EffectiveMinimumDelaySeconds;
var maximum = Math.Clamp(this.maximumDelaySeconds, minimum, DataBatchProcessing.MAX_DELAY_SECONDS);
return (minimum, maximum);
}
/// <summary>
/// Waits for a random, inclusive duration before the next file starts.
/// </summary>
private async Task WaitBeforeNextFileAsync(int minimumSeconds, int maximumSeconds, CancellationToken token)
{
if (token.IsCancellationRequested)
return;
// ThreadSafeRandom is the application-wide singleton. Batch runs must
// not create private Random instances because several runs may execute
// concurrently in different assistant sessions.
this.pauseBeforeNextFileSeconds = this.Rng.Next(minimumSeconds, maximumSeconds + 1);
this.Logger.LogInformation("Batch processing waits {DelaySeconds} seconds before starting the next file.", this.pauseBeforeNextFileSeconds);
await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
try
{
await Task.Delay(TimeSpan.FromSeconds(this.pauseBeforeNextFileSeconds), token);
}
finally
{
this.pauseBeforeNextFileSeconds = 0;
await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
}
}
}

View File

@ -41,6 +41,7 @@ public partial class AssistantBatchProcessing
this.usedResultFileNames.Clear();
this.hasReportedWriteFailure = false;
this.numProcessedFiles = 0;
this.pauseBeforeNextFileSeconds = 0;
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(this.inputDirectory, file);
@ -81,13 +82,16 @@ public partial class AssistantBatchProcessing
{
this.isProcessingBatch = true;
var stopwatch = Stopwatch.StartNew();
var delayRange = this.GetEffectiveDelayRange();
this.Logger.LogInformation(
"Batch processing started. InputDirectory='{InputDirectory}', OutputDirectory='{OutputDirectory}', TotalFiles={TotalFiles}, RestoredFiles={RestoredFiles}, Model='{Model}'.",
"Batch processing started. InputDirectory='{InputDirectory}', OutputDirectory='{OutputDirectory}', TotalFiles={TotalFiles}, RestoredFiles={RestoredFiles}, Model='{Model}', MinimumDelaySeconds={MinimumDelaySeconds}, MaximumDelaySeconds={MaximumDelaySeconds}.",
this.inputDirectory,
resolvedOutputDirectory,
this.fileResults.Count,
this.fileResults.Count(fileResult => fileResult.Status is BatchProcessingFileStatus.DONE),
this.ProviderSettings.Model);
this.ProviderSettings.Model,
delayRange.Minimum,
delayRange.Maximum);
// We use the cancellation token of the assistant base class, which
// creates it before it calls us and disposes it after we returned.
@ -97,8 +101,10 @@ public partial class AssistantBatchProcessing
try
{
foreach (var fileResult in this.fileResults)
for (var index = 0; index < this.fileResults.Count; index++)
{
var fileResult = this.fileResults[index];
// Restored from the log of a previous run:
if (fileResult.Status is BatchProcessingFileStatus.DONE)
continue;
@ -120,6 +126,10 @@ public partial class AssistantBatchProcessing
await this.WriteAggregatedResultsAsync(resolvedOutputDirectory);
await this.CheckpointAssistantSession();
await this.RefreshAssistantUIAsync();
var anotherFileIsWaiting = this.fileResults.Skip(index + 1).Any(nextFile => nextFile.Status is not BatchProcessingFileStatus.DONE);
if (anotherFileIsWaiting)
await this.WaitBeforeNextFileAsync(delayRange.Minimum, delayRange.Maximum, token);
}
}
finally

View File

@ -20,11 +20,14 @@ public partial class AssistantBatchProcessing
private static readonly AssistantSessionStateKey<string> CSV_FILE_NAME_STATE_KEY = new(nameof(csvFileName));
private static readonly AssistantSessionStateKey<BatchProcessingCsvSeparator> CSV_SEPARATOR_STATE_KEY = new(nameof(csvSeparator));
private static readonly AssistantSessionStateKey<string> CUSTOM_CSV_SEPARATOR_STATE_KEY = new(nameof(customCsvSeparator));
private static readonly AssistantSessionStateKey<int> MINIMUM_DELAY_SECONDS_STATE_KEY = new(nameof(minimumDelaySeconds));
private static readonly AssistantSessionStateKey<int> MAXIMUM_DELAY_SECONDS_STATE_KEY = new(nameof(maximumDelaySeconds));
private static readonly AssistantSessionStateKey<List<BatchProcessingFileResult>> FILE_RESULTS_STATE_KEY = new(nameof(fileResults));
private static readonly AssistantSessionStateKey<HashSet<string>> USED_RESULT_FILE_NAMES_STATE_KEY = new(nameof(usedResultFileNames));
private static readonly AssistantSessionStateKey<bool> IS_PROCESSING_BATCH_STATE_KEY = new(nameof(isProcessingBatch));
private static readonly AssistantSessionStateKey<bool> HAS_REPORTED_WRITE_FAILURE_STATE_KEY = new(nameof(hasReportedWriteFailure));
private static readonly AssistantSessionStateKey<int> NUM_PROCESSED_FILES_STATE_KEY = new(nameof(numProcessedFiles));
private static readonly AssistantSessionStateKey<int> PAUSE_BEFORE_NEXT_FILE_SECONDS_STATE_KEY = new(nameof(pauseBeforeNextFileSeconds));
/// <inheritdoc />
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
@ -44,11 +47,14 @@ public partial class AssistantBatchProcessing
state.Set(CSV_FILE_NAME_STATE_KEY, this.csvFileName);
state.Set(CSV_SEPARATOR_STATE_KEY, this.csvSeparator);
state.Set(CUSTOM_CSV_SEPARATOR_STATE_KEY, this.customCsvSeparator);
state.Set(MINIMUM_DELAY_SECONDS_STATE_KEY, this.minimumDelaySeconds);
state.Set(MAXIMUM_DELAY_SECONDS_STATE_KEY, this.maximumDelaySeconds);
state.SetList(FILE_RESULTS_STATE_KEY, this.fileResults.Select(CloneFileResult));
state.SetHashSet(USED_RESULT_FILE_NAMES_STATE_KEY, this.usedResultFileNames);
state.Set(IS_PROCESSING_BATCH_STATE_KEY, this.isProcessingBatch);
state.Set(HAS_REPORTED_WRITE_FAILURE_STATE_KEY, this.hasReportedWriteFailure);
state.Set(NUM_PROCESSED_FILES_STATE_KEY, this.numProcessedFiles);
state.Set(PAUSE_BEFORE_NEXT_FILE_SECONDS_STATE_KEY, this.pauseBeforeNextFileSeconds);
}
/// <inheritdoc />
@ -69,6 +75,8 @@ public partial class AssistantBatchProcessing
state.Restore(CSV_FILE_NAME_STATE_KEY, value => this.csvFileName = value);
state.Restore(CSV_SEPARATOR_STATE_KEY, value => this.csvSeparator = value);
state.Restore(CUSTOM_CSV_SEPARATOR_STATE_KEY, value => this.customCsvSeparator = value);
state.Restore(MINIMUM_DELAY_SECONDS_STATE_KEY, value => this.minimumDelaySeconds = value);
state.Restore(MAXIMUM_DELAY_SECONDS_STATE_KEY, value => this.maximumDelaySeconds = value);
state.Restore(FILE_RESULTS_STATE_KEY, values =>
{
this.fileResults.Clear();
@ -78,6 +86,7 @@ public partial class AssistantBatchProcessing
state.Restore(IS_PROCESSING_BATCH_STATE_KEY, value => this.isProcessingBatch = value);
state.Restore(HAS_REPORTED_WRITE_FAILURE_STATE_KEY, value => this.hasReportedWriteFailure = value);
state.Restore(NUM_PROCESSED_FILES_STATE_KEY, value => this.numProcessedFiles = value);
state.Restore(PAUSE_BEFORE_NEXT_FILE_SECONDS_STATE_KEY, value => this.pauseBeforeNextFileSeconds = value);
}
private static BatchProcessingFileResult CloneFileResult(BatchProcessingFileResult source)

View File

@ -59,6 +59,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
this.usedResultFileNames.Clear();
this.hasReportedWriteFailure = false;
this.numProcessedFiles = 0;
this.pauseBeforeNextFileSeconds = 0;
}
protected override bool MightPreselectValues()
@ -91,12 +92,15 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
private string csvFileName = string.Empty;
private BatchProcessingCsvSeparator csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
private string customCsvSeparator = string.Empty;
private int minimumDelaySeconds = DataBatchProcessing.DEFAULT_MIN_DELAY_SECONDS;
private int maximumDelaySeconds = DataBatchProcessing.DEFAULT_MAX_DELAY_SECONDS;
private readonly List<BatchProcessingFileResult> fileResults = [];
private readonly HashSet<string> usedResultFileNames = new(StringComparer.OrdinalIgnoreCase);
private bool isProcessingBatch;
private bool hasReportedWriteFailure;
private int numProcessedFiles;
private int pauseBeforeNextFileSeconds;
/// <summary>
/// The header of the column of the results table that holds the AI answer.
@ -161,6 +165,8 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
this.csvFileName = string.Empty;
this.csvSeparator = BatchProcessingCsvSeparator.SEMICOLON;
this.customCsvSeparator = string.Empty;
this.minimumDelaySeconds = MinimumDelayIsManaged ? this.ManagedMinimumDelaySeconds : DataBatchProcessing.DEFAULT_MIN_DELAY_SECONDS;
this.maximumDelaySeconds = Math.Clamp(DataBatchProcessing.DEFAULT_MAX_DELAY_SECONDS, this.minimumDelaySeconds, DataBatchProcessing.MAX_DELAY_SECONDS);
return;
}
@ -178,6 +184,9 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
this.csvFileName = settings.CsvFileName;
this.csvSeparator = settings.CsvSeparator;
this.customCsvSeparator = settings.CustomCsvSeparator;
this.minimumDelaySeconds = MinimumDelayIsManaged ? this.ManagedMinimumDelaySeconds
: Math.Clamp(settings.MinimumDelaySeconds, DataBatchProcessing.MIN_DELAY_SECONDS, DataBatchProcessing.MAX_DELAY_SECONDS);
this.maximumDelaySeconds = Math.Clamp(settings.MaximumDelaySeconds, this.minimumDelaySeconds, DataBatchProcessing.MAX_DELAY_SECONDS);
}
private async Task LoadConfiguredPromptFileAsync()

View File

@ -340,6 +340,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Name of the results table (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1127380661"] = "Name of the results table (optional)"
-- Your organization requires a pause of at least {0} seconds between files.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1155517317"] = "Your organization requires a pause of at least {0} seconds between files."
-- The results table contains one row per document, starting with the file name. Here you can name the column that holds the AI answer, e.g., Summary. When left empty, we use 'Result'.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1164512104"] = "The results table contains one row per document, starting with the file name. Here you can name the column that holds the AI answer, e.g., Summary. When left empty, we use 'Result'."
@ -394,9 +397,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The configured default policy no longer exists. Please select another document analysis policy.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T169666151"] = "The configured default policy no longer exists. Please select another document analysis policy."
-- Waiting {0} seconds before starting the next file.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1708373046"] = "Waiting {0} seconds before starting the next file."
-- seconds
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1723256298"] = "seconds"
-- The selected folder does not exist.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "The selected folder does not exist."
-- Minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Minimum pause between files"
-- Was not able to read the input folder: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1871021621"] = "Was not able to read the input folder: {0}"
@ -487,6 +499,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Each answer is stored as its own Markdown file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result.md.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2724126639"] = "Each answer is stored as its own Markdown file. Those files are named after the document, e.g., the answer for report.pdf is stored as report_result.md."
-- Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2742154256"] = "Before the next file starts, AI Studio waits for a random number of whole seconds from this interval. The minimum is always 6 seconds and the maximum is 300 seconds (5 minutes). Restored files and the end of a run do not add another pause."
-- Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2908365499"] = "Please enter exactly one punctuation or symbol character. Letters, numbers, spaces, quotation marks, and line breaks cannot be used as CSV separators."
@ -496,6 +511,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The batch run finished, but one file could not be processed. See the progress table and log for details.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details."
-- Maximum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximum pause between files"
-- Please select the folder that contains the documents you want to process.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T33077198"] = "Please select the folder that contains the documents you want to process."
@ -508,6 +526,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Header of the result column (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Header of the result column (optional)"
-- Processing pace
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3428873429"] = "Processing pace"
-- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'."
@ -6541,12 +6562,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T6790
-- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model."
-- Default minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Default minimum pause between files"
-- Instructions
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1221801316"] = "Instructions"
-- Leave empty to use the ai-results subfolder of the input folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1550632323"] = "Leave empty to use the ai-results subfolder of the input folder."
-- seconds
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1723256298"] = "seconds"
-- Default prompt
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1750564968"] = "Default prompt"
@ -6568,6 +6595,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T21
-- Default output folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T223484721"] = "Default output folder"
-- The lower end of the random pause interval. AI Studio never allows less than 6 seconds.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "The lower end of the random pause interval. AI Studio never allows less than 6 seconds."
-- When enabled, new batch runs start with the defaults configured below.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below."
@ -6598,6 +6628,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T29
-- Only the selected folder is processed
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2958253681"] = "Only the selected folder is processed"
-- Default maximum pause between files
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3011459001"] = "Default maximum pause between files"
-- Include subfolders by default?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3106330739"] = "Include subfolders by default?"
@ -6613,12 +6646,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T34
-- Default result column header
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3425186124"] = "Default result column header"
-- Processing pace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3428873429"] = "Processing pace"
-- The upper end of the random pause interval. The app-wide maximum is 300 seconds (5 minutes).
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3434290122"] = "The upper end of the random pause interval. The app-wide maximum is 300 seconds (5 minutes)."
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3448155331"] = "Close"
-- The current content of this Markdown file is loaded whenever the defaults are applied.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3468539567"] = "The current content of this Markdown file is loaded whenever the defaults are applied."
-- Your organization requires a pause of at least {0} seconds between files. Users can configure only the upper limit.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3663516199"] = "Your organization requires a pause of at least {0} seconds between files. Users can configure only the upper limit."
-- Load default prompt from file
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T3763644960"] = "Load default prompt from file"

View File

@ -55,6 +55,17 @@
}
<ConfigurationDirectory OptionDescription="@T("Default output folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default output folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.OutputDirectory = value)" OptionHelp="@T("Leave empty to use the ai-results subfolder of the input folder.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.OutputDirectory, out var meta) && meta.IsLocked"/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("Processing pace")</MudText>
@if (this.MinimumDelayIsManaged)
{
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-6">@(string.Format(T("Your organization requires a pause of at least {0} seconds between files. Users can configure only the upper limit."), this.ManagedMinimumDelaySeconds))</MudAlert>
}
else
{
<ConfigurationSlider T="int" OptionDescription="@T("Default minimum pause between files")" Min="@DataBatchProcessing.MIN_DELAY_SECONDS" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds = value)" OptionHelp="@T("The lower end of the random pause interval. AI Studio never allows less than 6 seconds.")"/>
}
<ConfigurationSlider T="int" OptionDescription="@T("Default maximum pause between files")" Min="@this.EffectiveMinimumDelaySeconds" Max="@DataBatchProcessing.MAX_DELAY_SECONDS" Step="1" Unit="@T("seconds")" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MaximumDelaySeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MaximumDelaySeconds = value)" OptionHelp="@T("The upper end of the random pause interval. The app-wide maximum is 300 seconds (5 minutes).")"/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">@T("AI selection")</MudText>
<ConfigurationMinConfidenceSelection Disabled="@this.DefaultsDisabled" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.BATCH_PROCESSING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>

View File

@ -1,5 +1,6 @@
using AIStudio.Assistants.BatchProcessing;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Dialogs.Settings;
@ -7,6 +8,21 @@ public partial class SettingsDialogBatchProcessing : SettingsDialogBase
{
private bool DefaultsDisabled() => !this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions;
private bool MinimumDelayIsManaged => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumDelaySeconds, out var meta)
&& meta.ManagedMode is not null;
private int ManagedMinimumDelaySeconds => Math.Clamp(
this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds,
DataBatchProcessing.MIN_DELAY_SECONDS,
DataBatchProcessing.MAX_DELAY_SECONDS);
private int EffectiveMinimumDelaySeconds => this.MinimumDelayIsManaged
? this.ManagedMinimumDelaySeconds
: Math.Clamp(
this.SettingsManager.ConfigurationData.BatchProcessing.MinimumDelaySeconds,
DataBatchProcessing.MIN_DELAY_SECONDS,
DataBatchProcessing.MAX_DELAY_SECONDS);
private bool FreePromptImportDisabled() => this.DefaultsDisabled()
|| ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FreePrompt, out var meta) && meta.IsLocked;

View File

@ -429,6 +429,11 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataBatchProcessing.CsvSeparator"] = "SEMICOLON"
-- CONFIG["SETTINGS"]["DataBatchProcessing.CustomCsvSeparator"] = "^"
--
-- Enforce the lower end of the random pause between files for the organization.
-- The value must be between 6 and 300 seconds. Users can configure only the upper
-- end of the interval while this setting is managed by a configuration plugin.
-- CONFIG["SETTINGS"]["DataBatchProcessing.MinimumDelaySeconds"] = 12
--
-- Configure the minimum provider confidence and the default provider.
-- Allowed confidence values are: NONE, UNTRUSTED, UNKNOWN, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- A policy can require a higher minimum confidence; the stricter level wins.

View File

@ -12,6 +12,10 @@ namespace AIStudio.Settings.DataModel;
public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessing>>? configSelection = null)
{
public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm";
public const int MIN_DELAY_SECONDS = 6;
public const int MAX_DELAY_SECONDS = 300;
public const int DEFAULT_MIN_DELAY_SECONDS = 6;
public const int DEFAULT_MAX_DELAY_SECONDS = 10;
/// <summary>
/// Initializes an unmanaged Batch Processing settings instance.
@ -48,6 +52,10 @@ public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessin
public string CustomCsvSeparator { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CustomCsvSeparator, string.Empty);
public int MinimumDelaySeconds { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumDelaySeconds, DEFAULT_MIN_DELAY_SECONDS);
public int MaximumDelaySeconds { get; set; } = DEFAULT_MAX_DELAY_SECONDS;
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);

View File

@ -83,6 +83,7 @@ public static partial class ManagedConfiguration
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="dryRun">When true, the method will not apply any changes, but only check if the configuration can be read.</param>
/// <param name="_">An unused parameter to help with type inference. You might ignore it when calling the method.</param>
/// <param name="validator">An optional validator for rejecting parsed values outside the setting's supported range.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
@ -92,7 +93,8 @@ public static partial class ManagedConfiguration
Guid configPluginId,
LuaTable settings,
bool dryRun,
ISpanParsable<TValue>? _ = null)
ISpanParsable<TValue>? _ = null,
Func<TValue, bool>? validator = null)
where TValue : struct, ISpanParsable<TValue>
{
//
@ -113,7 +115,8 @@ public static partial class ManagedConfiguration
if (configuredLuaValue.Type is LuaValueType.String && configuredLuaValue.TryRead<string>(out var configuredLuaValueText))
{
// Step 3 -- try to parse the string as the target type:
if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue))
if (TValue.TryParse(configuredLuaValueText, CultureInfo.InvariantCulture, out var configuredParsedValue)
&& (validator?.Invoke(configuredParsedValue) ?? true))
{
configuredValue = configuredParsedValue;
successful = true;
@ -121,7 +124,8 @@ public static partial class ManagedConfiguration
}
// Step 2b -- try to read the Lua value:
if(configuredLuaValue.TryRead<TValue>(out var configuredLuaValueInstance))
if(configuredLuaValue.TryRead<TValue>(out var configuredLuaValueInstance)
&& (validator?.Invoke(configuredLuaValueInstance) ?? true))
{
configuredValue = configuredLuaValueInstance;
successful = true;

View File

@ -352,6 +352,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.ResultColumnHeader, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.CsvSeparator, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.CustomCsvSeparator, this.Id, settingsTable, dryRun);
var minimumDelayIsValid = ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.MinimumDelaySeconds, this.Id, settingsTable, dryRun, validator: value => value is >= DataBatchProcessing.MIN_DELAY_SECONDS and <= DataBatchProcessing.MAX_DELAY_SECONDS);
if (!minimumDelayIsValid && settingsTable.TryGetValue("DataBatchProcessing.MinimumDelaySeconds", out _))
LOG.LogWarning("The Batch Processing minimum delay configured by plugin {ConfigPluginId} must be between {MinimumDelaySeconds} and {MaximumDelaySeconds} seconds.", this.Id, DataBatchProcessing.MIN_DELAY_SECONDS, DataBatchProcessing.MAX_DELAY_SECONDS);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);