mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-30 05:44:26 +00:00
Added a pause between indexing steps for shared embedding servers (#1114)
This commit is contained in:
1 parent
cbc4ce5413
commit
dcba62b45c
21 files changed
+627
-9
No files matched your search
@@ -4405,6 +4405,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Yo
|
||||
-- Automatic local data source refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh"
|
||||
|
||||
-- Shortest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1308136668"] = "Shortest pause between indexing steps"
|
||||
|
||||
-- Refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh"
|
||||
|
||||
@@ -4423,6 +4426,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1592527757"] = "Ca
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1652430727"] = "External (ERI)"
|
||||
|
||||
-- Longest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1654263778"] = "Longest pause between indexing steps"
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1723256298"] = "seconds"
|
||||
|
||||
-- Delete Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1849107431"] = "Delete Data Source"
|
||||
|
||||
@@ -4432,6 +4441,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "Ar
|
||||
-- Local Directory Data Source Information
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information"
|
||||
|
||||
-- AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2352821039"] = "AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause."
|
||||
|
||||
-- Local data sources refresh when files change, mailboxes every 16 minutes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes."
|
||||
|
||||
@@ -4474,6 +4486,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3267849393"] = "Ed
|
||||
-- Are you sure you want to delete the data source '{0}' of type '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3337072977"] = "Are you sure you want to delete the data source '{0}' of type '{1}'?"
|
||||
|
||||
-- The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes).
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3401303590"] = "The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes)."
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3424652889"] = "Unknown"
|
||||
|
||||
@@ -10813,6 +10828,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1485378199"] = "These files match
|
||||
-- Outside the selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1512669069"] = "Outside the selection"
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1513645664"] = "Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share."
|
||||
|
||||
-- Failed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1531389696"] = "Failed: {0}"
|
||||
|
||||
@@ -10921,6 +10939,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3865031940"] = "Actions"
|
||||
-- These file types and folders were found in the folder of the data source, but its file patterns or its list of excluded files and folders leave them out. They are intentionally excluded and do not affect the indexing status. Excluded folders are not searched at all.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4007318423"] = "These file types and folders were found in the folder of the data source, but its file patterns or its list of excluded files and folders leave them out. They are intentionally excluded and do not affect the indexing status. Excluded folders are not searched at all."
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4167794090"] = "Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share."
|
||||
|
||||
-- Remove them from the index
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Remove them from the index"
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
|
||||
<MudPaper Class="pa-3 mb-3 border-dashed border rounded-lg">
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Shortest pause between indexing steps")" Min="0" Max="@this.MinimumPauseSliderMax" Step="1" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.DataSourceIndexing.MinimumPauseBetweenStepsSeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.App.DataSourceIndexing.MinimumPauseBetweenStepsSeconds = value)" OptionHelp="@T("AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause.")" IsLocked="@(() => IsMinimumPauseLocked)"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Longest pause between indexing steps")" Min="@this.MaximumPauseSliderMin" Max="@DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS" Step="1" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.DataSourceIndexing.MaximumPauseBetweenStepsSeconds)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.App.DataSourceIndexing.MaximumPauseBetweenStepsSeconds = value)" OptionHelp="@T("The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes).")" IsLocked="@(() => IsMaximumPauseLocked)"/>
|
||||
</MudPaper>
|
||||
|
||||
@{ var embeddingStatuses = this.DataSourceEmbeddingService.GetStatuses().ToDictionary(status => status.DataSourceId, StringComparer.OrdinalIgnoreCase); }
|
||||
<MudTable T="IDataSourceBase" Items="@this.ConfiguredDataSources" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
|
||||
@@ -5,6 +5,7 @@ using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Services.Indexing;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -39,6 +40,27 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
|
||||
private bool AreMailboxesEnabled => PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager);
|
||||
|
||||
private static bool IsMinimumPauseLocked => ManagedConfiguration.TryGet(x => x.App.DataSourceIndexing, x => x.MinimumPauseBetweenStepsSeconds, out var meta) && meta.IsLocked;
|
||||
|
||||
private static bool IsMaximumPauseLocked => ManagedConfiguration.TryGet(x => x.App.DataSourceIndexing, x => x.MaximumPauseBetweenStepsSeconds, out var meta) && meta.IsLocked;
|
||||
|
||||
/// <remarks>
|
||||
/// A slider moves a value which falls out of its range into it. A locked slider therefore keeps
|
||||
/// the whole range, since its value belongs to the organization. A free one ends at the locked
|
||||
/// upper bound, and the free value adjusts to it, cf. PauseRange.GetEffective.
|
||||
/// </remarks>
|
||||
private int MinimumPauseSliderMax => IsMaximumPauseLocked && !IsMinimumPauseLocked
|
||||
? PauseRange.FromSettings(this.SettingsManager.ConfigurationData.App.DataSourceIndexing).MaximumSeconds
|
||||
: DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS;
|
||||
|
||||
/// <remarks>
|
||||
/// The upper bound starts where the lower bound is, so raising the lower one above it raises the
|
||||
/// upper one along with it. A locked slider keeps the whole range, cf. MinimumPauseSliderMax.
|
||||
/// </remarks>
|
||||
private int MaximumPauseSliderMin => IsMaximumPauseLocked
|
||||
? 0
|
||||
: PauseRange.FromSettings(this.SettingsManager.ConfigurationData.App.DataSourceIndexing).MinimumSeconds;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2">
|
||||
@T("AI Studio indexes large data sources in steps of a few minutes, so that your other data sources get their turn in between.")
|
||||
@this.PauseBetweenStepsText
|
||||
</MudText>
|
||||
@if (this.AreMailboxesEnabled)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Services.Indexing;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -58,6 +59,23 @@ public partial class Embeddings : MSGComponentBase
|
||||
|
||||
private bool AreMailboxesEnabled => PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager);
|
||||
|
||||
/// <remarks>
|
||||
/// Says nothing while there is no pause between the steps, which is the default.
|
||||
/// </remarks>
|
||||
private string PauseBetweenStepsText
|
||||
{
|
||||
get
|
||||
{
|
||||
var pause = PauseRange.FromSettings(this.SettingsManager.ConfigurationData.App.DataSourceIndexing);
|
||||
if (pause.IsNone)
|
||||
return string.Empty;
|
||||
|
||||
return pause.MinimumSeconds == pause.MaximumSeconds
|
||||
? string.Format(T("Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share."), pause.MaximumSeconds)
|
||||
: string.Format(T("Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share."), pause.MinimumSeconds, pause.MaximumSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The chips above count documents, which says nothing about how far the list of data sources
|
||||
/// itself has come. While several of them wait their turn, this is the one line saying so. With a
|
||||
|
||||
@@ -1411,6 +1411,22 @@ CONFIG["SETTINGS"] = {}
|
||||
-- in CONFIG["MAILBOX_PROVIDERS"], this blocks every mailbox.
|
||||
-- CONFIG["SETTINGS"]["DataMailboxes.AllowOnlyOrganizationMailServers"] = true
|
||||
|
||||
-- Configure a pause between two indexing steps of local data sources, i.e., files, folders, and
|
||||
-- mailboxes. AI Studio indexes large data sources in steps of a few minutes. Between two steps, it
|
||||
-- waits for a random number of seconds between the lower and the upper bound, both included. This
|
||||
-- spares an embedding server which many users share: a single user with large folders or
|
||||
-- mailboxes no longer keeps it busy for hours. The pause applies after every step which worked on
|
||||
-- documents, also when the same data source carries on. Time which passes anyway counts toward
|
||||
-- it. Both values must be between 0 and 300 seconds. The default is 0 for both, i.e., no pause.
|
||||
-- You may set one bound or both. A bound you set wins, and AI Studio adjusts the other one to it.
|
||||
-- Should the upper bound be below the lower bound, the lower bound wins.
|
||||
-- CONFIG["SETTINGS"]["DataDataSourceIndexing.MinimumPauseBetweenStepsSeconds"] = 30
|
||||
-- CONFIG["SETTINGS"]["DataDataSourceIndexing.MaximumPauseBetweenStepsSeconds"] = 90
|
||||
--
|
||||
-- Allow users to change the bounds, so that your values are only defaults:
|
||||
-- CONFIG["SETTINGS"]["DataDataSourceIndexing.MinimumPauseBetweenStepsSeconds.AllowUserOverride"] = true
|
||||
-- CONFIG["SETTINGS"]["DataDataSourceIndexing.MaximumPauseBetweenStepsSeconds.AllowUserOverride"] = true
|
||||
|
||||
-- Configure the data source selection agent.
|
||||
-- This agent is used when chat data source options enable AI-based data source selection.
|
||||
-- The provider must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||
|
||||
+21
@@ -4407,6 +4407,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Si
|
||||
-- Automatic local data source refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatische Aktualisierung lokaler Datenquellen"
|
||||
|
||||
-- Shortest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1308136668"] = "Kürzeste Pause zwischen den Etappen der Indexierung"
|
||||
|
||||
-- Refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Aktualisieren"
|
||||
|
||||
@@ -4425,6 +4428,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1592527757"] = "Di
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1652430727"] = "Extern (ERI)"
|
||||
|
||||
-- Longest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1654263778"] = "Längste Pause zwischen den Etappen der Indexierung"
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1723256298"] = "Sekunden"
|
||||
|
||||
-- Delete Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1849107431"] = "Datenquelle löschen"
|
||||
|
||||
@@ -4434,6 +4443,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "M
|
||||
-- Local Directory Data Source Information
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Informationen zur lokalen Ordner-Datenquelle"
|
||||
|
||||
-- AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2352821039"] = "AI Studio indexiert große Datenquellen in Etappen von wenigen Minuten. Zwischen zwei Etappen wartet AI Studio eine zufällige Zeit, die zwischen der kürzesten und der längsten Pause liegt. Das entlastet einen Einbettungsserver, den sich viele Nutzer teilen, zum Beispiel in Ihrer Organisation. Lassen Sie beide Werte auf 0 Sekunden, wenn keine Pause eingelegt werden soll."
|
||||
|
||||
-- Local data sources refresh when files change, mailboxes every 16 minutes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Lokale Datenquellen werden aktualisiert, wenn sich Dateien ändern, Postfächer alle 16 Minuten."
|
||||
|
||||
@@ -4476,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3267849393"] = "Be
|
||||
-- Are you sure you want to delete the data source '{0}' of type '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3337072977"] = "Möchten Sie die Datenquelle „{0}“ vom Typ „{1}“ wirklich löschen?"
|
||||
|
||||
-- The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes).
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3401303590"] = "Das obere Ende der zufälligen Pause zwischen zwei Etappen der Indexierung. AI Studio erlaubt bis zu 300 Sekunden (5 Minuten)."
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3424652889"] = "Unbekannt"
|
||||
|
||||
@@ -10815,6 +10830,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1485378199"] = "Diese Dateien ent
|
||||
-- Outside the selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1512669069"] = "Außerhalb der Auswahl"
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1513645664"] = "Zwischen zwei Etappen pausiert AI Studio {0} Sekunden, um einen Einbettungsserver zu entlasten, den sich viele Nutzer teilen."
|
||||
|
||||
-- Failed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1531389696"] = "Fehlgeschlagen: {0}"
|
||||
|
||||
@@ -10923,6 +10941,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3865031940"] = "Aktionen"
|
||||
-- These file types and folders were found in the folder of the data source, but its file patterns or its list of excluded files and folders leave them out. They are intentionally excluded and do not affect the indexing status. Excluded folders are not searched at all.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4007318423"] = "Diese Dateitypen und Ordner wurden im Ordner der Datenquelle gefunden, aber ihre Dateimuster oder ihre Liste ausgeschlossener Dateien und Ordner lassen sie aus. Sie fehlen absichtlich und wirken sich nicht auf den Indexierungsstatus aus. Ausgeschlossene Ordner werden gar nicht durchsucht."
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4167794090"] = "Zwischen zwei Etappen pausiert AI Studio {0} bis {1} Sekunden, um einen Einbettungsserver zu entlasten, den sich viele Nutzer teilen."
|
||||
|
||||
-- Remove them from the index
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Aus dem Index entfernen"
|
||||
|
||||
|
||||
+21
@@ -4407,6 +4407,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Yo
|
||||
-- Automatic local data source refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh"
|
||||
|
||||
-- Shortest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1308136668"] = "Shortest pause between indexing steps"
|
||||
|
||||
-- Refresh
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh"
|
||||
|
||||
@@ -4425,6 +4428,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1592527757"] = "Ca
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1652430727"] = "External (ERI)"
|
||||
|
||||
-- Longest pause between indexing steps
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1654263778"] = "Longest pause between indexing steps"
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1723256298"] = "seconds"
|
||||
|
||||
-- Delete Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1849107431"] = "Delete Data Source"
|
||||
|
||||
@@ -4434,6 +4443,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "Ar
|
||||
-- Local Directory Data Source Information
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information"
|
||||
|
||||
-- AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2352821039"] = "AI Studio indexes large data sources in steps of a few minutes. Between two steps, it waits for a random time between the shortest and the longest pause. This spares an embedding server which many people share, for example in your organization. Leave both at 0 seconds for no pause."
|
||||
|
||||
-- Local data sources refresh when files change, mailboxes every 16 minutes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes."
|
||||
|
||||
@@ -4476,6 +4488,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3267849393"] = "Ed
|
||||
-- Are you sure you want to delete the data source '{0}' of type '{1}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3337072977"] = "Are you sure you want to delete the data source '{0}' of type '{1}'?"
|
||||
|
||||
-- The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes).
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3401303590"] = "The upper end of the random pause between two indexing steps. AI Studio allows up to 300 seconds (5 minutes)."
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T3424652889"] = "Unknown"
|
||||
|
||||
@@ -10815,6 +10830,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1485378199"] = "These files match
|
||||
-- Outside the selection
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1512669069"] = "Outside the selection"
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1513645664"] = "Between two steps, AI Studio pauses for {0} seconds, to spare an embedding server which many people share."
|
||||
|
||||
-- Failed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1531389696"] = "Failed: {0}"
|
||||
|
||||
@@ -10923,6 +10941,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3865031940"] = "Actions"
|
||||
-- These file types and folders were found in the folder of the data source, but its file patterns or its list of excluded files and folders leave them out. They are intentionally excluded and do not affect the indexing status. Excluded folders are not searched at all.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4007318423"] = "These file types and folders were found in the folder of the data source, but its file patterns or its list of excluded files and folders leave them out. They are intentionally excluded and do not affect the indexing status. Excluded folders are not searched at all."
|
||||
|
||||
-- Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4167794090"] = "Between two steps, AI Studio pauses for {0} to {1} seconds, to spare an embedding server which many people share."
|
||||
|
||||
-- Remove them from the index
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Remove them from the index"
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// <summary>
|
||||
/// Settings for indexing local data sources.
|
||||
/// </summary>
|
||||
public DataDataSourceIndexing DataSourceIndexing { get; init; } = new();
|
||||
public DataDataSourceIndexing DataSourceIndexing { get; init; } = new(x => x.App.DataSourceIndexing);
|
||||
|
||||
/// <summary>
|
||||
/// List of assistants that should be hidden from the UI.
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public sealed class DataDataSourceIndexing
|
||||
/// <summary>
|
||||
/// Settings for indexing local data sources.
|
||||
/// </summary>
|
||||
/// <param name="configSelection">The managed-configuration selector.</param>
|
||||
public sealed class DataDataSourceIndexing(Expression<Func<Data, DataDataSourceIndexing>>? configSelection = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// The longest pause between two indexing steps either bound may ask for.
|
||||
/// </summary>
|
||||
public const int MAX_PAUSE_BETWEEN_STEPS_SECONDS = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an unmanaged instance, e.g., for the JSON deserializer.
|
||||
/// </summary>
|
||||
public DataDataSourceIndexing() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether local data source embeddings should refresh automatically when files change.
|
||||
/// </summary>
|
||||
public bool AutomaticRefresh { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The lower bound of the random pause between two indexing steps, in seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pause spares an embedding server which many people share, e.g., in an organization. Zero
|
||||
/// together with a zero upper bound means no pause.
|
||||
/// </remarks>
|
||||
public int MinimumPauseBetweenStepsSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.MinimumPauseBetweenStepsSeconds, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The upper bound of the random pause between two indexing steps, in seconds.
|
||||
/// </summary>
|
||||
public int MaximumPauseBetweenStepsSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.MaximumPauseBetweenStepsSeconds, 0);
|
||||
}
|
||||
@@ -382,6 +382,21 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
// Config: mailboxes only on the mail servers of the organization?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.MailboxSettings, x => x.AllowOnlyOrganizationMailServers, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: the random pause between two indexing steps of local data sources
|
||||
var minimumPauseIsValid = ManagedConfiguration.TryProcessConfiguration(x => x.App.DataSourceIndexing, x => x.MinimumPauseBetweenStepsSeconds, this.Id, settingsTable, dryRun, validator: value => value is >= 0 and <= DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
if (!minimumPauseIsValid && settingsTable.TryGetValue("DataDataSourceIndexing.MinimumPauseBetweenStepsSeconds", out _))
|
||||
LOG.LogWarning("The lower bound of the pause between indexing steps configured by plugin {ConfigPluginId} must be between 0 and {MaximumPauseSeconds} seconds.", this.Id, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
|
||||
var maximumPauseIsValid = ManagedConfiguration.TryProcessConfiguration(x => x.App.DataSourceIndexing, x => x.MaximumPauseBetweenStepsSeconds, this.Id, settingsTable, dryRun, validator: value => value is >= 0 and <= DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
if (!maximumPauseIsValid && settingsTable.TryGetValue("DataDataSourceIndexing.MaximumPauseBetweenStepsSeconds", out _))
|
||||
LOG.LogWarning("The upper bound of the pause between indexing steps configured by plugin {ConfigPluginId} must be between 0 and {MaximumPauseSeconds} seconds.", this.Id, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
|
||||
if (minimumPauseIsValid && maximumPauseIsValid
|
||||
&& settingsTable.TryGetValue("DataDataSourceIndexing.MinimumPauseBetweenStepsSeconds", out var minimumPauseValue) && minimumPauseValue.TryRead<int>(out var minimumPauseSeconds)
|
||||
&& settingsTable.TryGetValue("DataDataSourceIndexing.MaximumPauseBetweenStepsSeconds", out var maximumPauseValue) && maximumPauseValue.TryRead<int>(out var maximumPauseSeconds)
|
||||
&& maximumPauseSeconds < minimumPauseSeconds)
|
||||
LOG.LogWarning("The upper bound of the pause between indexing steps configured by plugin {ConfigPluginId} is below its lower bound. The lower bound of {MinimumPauseSeconds} seconds wins.", this.Id, minimumPauseSeconds);
|
||||
|
||||
// Config: data source selection agent settings
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.AgentDataSourceSelection, x => x.PreselectAgentOptions, this.Id, settingsTable, dryRun);
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.AgentDataSourceSelection, x => x.PreselectedAgentProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -17,7 +17,7 @@ using static AIStudio.Tools.Services.Indexing.IndexingLogFormat;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class DataSourceEmbeddingService(SettingsManager settingsManager, RustService rustService, DatabaseClientProvider databaseClientProvider,
|
||||
PromptInjectionGuardService guardService, MediaTranscriptionService mediaTranscriptionService, ILogger<DataSourceEmbeddingService> logger) : BackgroundService, IMessageBusReceiver
|
||||
PromptInjectionGuardService guardService, MediaTranscriptionService mediaTranscriptionService, ThreadSafeRandom rng, ILogger<DataSourceEmbeddingService> logger) : BackgroundService, IMessageBusReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// How long the re-index check waits for the index database before it gives up.
|
||||
@@ -73,6 +73,11 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
/// </summary>
|
||||
private readonly IndexingPauses pauses = new();
|
||||
|
||||
/// <summary>
|
||||
/// The pause between two steps, cf. WaitForPauseBetweenStepsAsync.
|
||||
/// </summary>
|
||||
private readonly PauseBetweenSteps pauseBetweenSteps = new();
|
||||
|
||||
/// <summary>
|
||||
/// An active run.
|
||||
/// </summary>
|
||||
@@ -680,13 +685,31 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
// queue once the automatic refresh is switched on while the app runs. The wait counts as
|
||||
// part of the run, so deleting or repairing the data source ends the wait as well,
|
||||
// instead of waiting for the run before it to finish first. A paused indexing holds
|
||||
// every run back in front of the gate, cf. PauseIndexingAsync:
|
||||
// every run back in front of the gate, cf. PauseIndexingAsync. Behind the gate, the run
|
||||
// keeps its distance to the step before, cf. WaitForPauseBetweenStepsAsync:
|
||||
//
|
||||
await this.pauses.WaitWhilePausedAsync(runTokenSource.Token);
|
||||
await this.runGate.WaitAsync(runTokenSource.Token);
|
||||
try
|
||||
{
|
||||
await this.ProcessDataSourceAsync(dataSource, refreshMode, runControl, runTokenSource.Token);
|
||||
await this.WaitForPauseBetweenStepsAsync(dataSource, refreshMode, runTokenSource.Token);
|
||||
|
||||
//
|
||||
// Waiting may have taken minutes, while the user could change the data source. The
|
||||
// run has resolved nothing yet, so it starts with the data source as it is now,
|
||||
// instead of finding itself outdated once it records its signature:
|
||||
//
|
||||
if (!this.TryGetConfiguredIndexedSource(dataSource.Id, out var currentDataSource))
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Skipping embedding run for data source '{DataSourceName}' ({DataSourceId}) because it is no longer configured. RefreshMode={RefreshMode}.",
|
||||
dataSource.Name,
|
||||
dataSource.Id,
|
||||
refreshMode);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ProcessDataSourceAsync(currentDataSource, refreshMode, runControl, runTokenSource.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -718,6 +741,37 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for what is left of the pause between the last step and the run which is about to start, cf. PauseBetweenSteps.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pause spares an embedding server which many people share, so it holds whichever data
|
||||
/// source comes next, the same one included. It is waited for behind the run gate: only one run
|
||||
/// waits, and its token ends the wait, when the app shuts down, when the data source is deleted
|
||||
/// or repaired, or when a backup pauses the indexing. Meanwhile, the data source shows as queued,
|
||||
/// since nothing published a status for this run yet, and every other request is queued as usual.
|
||||
/// </remarks>
|
||||
/// <param name="dataSource">The data source of the run, for the log.</param>
|
||||
/// <param name="refreshMode">Why the run was asked for, for the log.</param>
|
||||
/// <param name="token">The token of the run.</param>
|
||||
private async Task WaitForPauseBetweenStepsAsync(IIndexedDataSource dataSource, DataSourceEmbeddingRefreshMode refreshMode, CancellationToken token)
|
||||
{
|
||||
var range = PauseRange.FromSettings(settingsManager.ConfigurationData.App.DataSourceIndexing);
|
||||
var pause = this.pauseBetweenSteps.GetPauseBeforeNextStep(DateTimeOffset.UtcNow, range, rng);
|
||||
if (pause.Remaining <= TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
logger.LogInformation(
|
||||
"Pausing {PauseSeconds:F0} seconds between two indexing steps. Data source '{DataSourceName}' ({DataSourceId}) starts in {RemainingSeconds:F0} seconds. RefreshMode={RefreshMode}.",
|
||||
pause.Pause.TotalSeconds,
|
||||
dataSource.Name,
|
||||
dataSource.Id,
|
||||
pause.Remaining.TotalSeconds,
|
||||
refreshMode);
|
||||
|
||||
await Task.Delay(pause.Remaining, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Works out everything an indexing run needs, before anything is read from the data source.
|
||||
/// </summary>
|
||||
@@ -891,6 +945,7 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
}
|
||||
|
||||
var outcome = IndexedRunOutcome.DONE;
|
||||
IndexedRunContext? context = null;
|
||||
try
|
||||
{
|
||||
//
|
||||
@@ -899,7 +954,7 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
// removes something in forty of them is one thing that happened to the user, not forty.
|
||||
//
|
||||
await using var promptInjectionScope = guardService.BeginAction();
|
||||
var context = await this.PrepareIndexedRunAsync(dataSource, refreshMode, indexer.Share, runControl, promptInjectionScope, token);
|
||||
context = await this.PrepareIndexedRunAsync(dataSource, refreshMode, indexer.Share, runControl, promptInjectionScope, token);
|
||||
if (context is null)
|
||||
return;
|
||||
|
||||
@@ -932,6 +987,13 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
}
|
||||
finally
|
||||
{
|
||||
//
|
||||
// Only a run which worked on documents put load on the embedding provider, so only such
|
||||
// a step makes the next one wait, even when it failed or stopped halfway:
|
||||
//
|
||||
if (context?.ShareStartedUtc is not null)
|
||||
this.pauseBetweenSteps.RecordStepEnded(DateTimeOffset.UtcNow);
|
||||
|
||||
//
|
||||
// A pass lives only while the last step left more to do. Whatever else ended this run,
|
||||
// the next one begins anew, and tries every document which failed so far once more:
|
||||
|
||||
@@ -40,6 +40,10 @@ The parts:
|
||||
a mailbox at most 500 mails. A large data source is indexed in steps, one run each. A run which returns
|
||||
`MORE_TO_DO` gets its next step in the back lane of `DataSourceRunQueue`, behind every data source which begins
|
||||
a pass, so a mailbox waits for one step at most.
|
||||
- **`PauseBetweenSteps`** - a gap after every step which worked on documents, so that many people can share one
|
||||
embedding server. The next run, of whichever data source, waits for a random pause between the bounds of the
|
||||
settings (`PauseRange`: 0 to 300 seconds, none by default), less the time which passed since the step ended.
|
||||
Not to be confused with the pause of the indexing as a whole (`IndexingPauses`), which a backup takes.
|
||||
- **`IndexingPass`** - the steps of one data source, from its first step until one of them is done. It remembers
|
||||
which documents failed during an earlier step, so the next steps do not read them again, and which failures
|
||||
were reported to the user already.
|
||||
@@ -98,6 +102,13 @@ Rules which are easy to break:
|
||||
holds two stores which do not fit together. Discarding the stored index of a data source, when it is deleted
|
||||
or repaired, waits for the end of a pause as well (`WaitWhilePausedAsync`); the user interface asks
|
||||
`IsIndexingPaused` first and tells the user instead of waiting.
|
||||
- **The pause between steps lies behind the run gate.** `ProcessDataSourceRunAsync` waits for it right after
|
||||
`runGate.WaitAsync`, before the run resolves anything: only one run waits, its token ends the wait, and its
|
||||
data source still shows as queued. Never wait for it in an indexer, where the run would hold its signature and
|
||||
its stores, and show as running. Afterward, the run looks up its data source anew, since the user may have
|
||||
changed it meanwhile. A step opens a gap only once it asked `HasDoneItsShare` (`IndexedRunContext.ShareStartedUtc`),
|
||||
so a run which found nothing to read leaves none, and a startup check over unchanged data sources does not
|
||||
slow down.
|
||||
- **A pass lives only while the last step left more to do.** The service drops the pass of a data source after
|
||||
every other outcome and every exception, and when the stored index is reset. A retry the user asks for, and a
|
||||
step with other embedding settings or another selection of documents, begin a new pass
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AIStudio.Tools.Services.Indexing;
|
||||
|
||||
/// <summary>
|
||||
/// How long the indexing step which is about to start waits, cf. PauseBetweenSteps.GetPauseBeforeNextStep.
|
||||
/// </summary>
|
||||
/// <param name="Pause">The pause drawn for the gap since the last step.</param>
|
||||
/// <param name="Remaining">What is left of the pause now. More than zero, unless this is NONE.</param>
|
||||
internal sealed record PauseBeforeStep(TimeSpan Pause, TimeSpan Remaining)
|
||||
{
|
||||
/// <summary>
|
||||
/// The step starts right away.
|
||||
/// </summary>
|
||||
public static readonly PauseBeforeStep NONE = new(TimeSpan.Zero, TimeSpan.Zero);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace AIStudio.Tools.Services.Indexing;
|
||||
|
||||
/// <summary>
|
||||
/// The pause between two indexing steps, which spares an embedding server that many people share.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not to be confused with the pauses of the indexing as a whole, e.g., while a backup copies the
|
||||
/// indexes, cf. IndexingPauses.<br/><br/>
|
||||
///
|
||||
/// The pause is a gap between the end of one step and the start of the next, whichever data source
|
||||
/// either of them belongs to: it is about the load on the embedding server, not about the order of
|
||||
/// the data sources. Only a step which worked on documents opens a gap. Its pause is drawn once the
|
||||
/// next step is about to start, from the bounds which apply then, and the time which passed in the
|
||||
/// meantime counts toward it. When no step comes, nothing is drawn.
|
||||
/// </remarks>
|
||||
internal sealed class PauseBetweenSteps
|
||||
{
|
||||
private readonly Lock stateLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// When the last step which worked on documents ended. Null before the first one.
|
||||
/// </summary>
|
||||
private DateTimeOffset? lastStepEndedUtc;
|
||||
|
||||
/// <summary>
|
||||
/// The pause drawn for the gap since the last step. Null until a step is about to start.
|
||||
/// </summary>
|
||||
private TimeSpan? drawnPause;
|
||||
|
||||
/// <summary>
|
||||
/// Notes that a step which worked on documents ended, which opens a new gap.
|
||||
/// </summary>
|
||||
/// <param name="nowUtc">The current time, passed in so the pause can be tested.</param>
|
||||
public void RecordStepEnded(DateTimeOffset nowUtc)
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
this.lastStepEndedUtc = nowUtc;
|
||||
this.drawnPause = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Works out how long the step which is about to start waits.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first step which asks after a gap opened draws its pause, with both bounds included. A step
|
||||
/// which stops while it waits, e.g., because its data source was deleted, leaves the same pause
|
||||
/// to the step after it, which waits only for the rest.
|
||||
/// </remarks>
|
||||
/// <param name="nowUtc">The current time, passed in so the pause can be tested.</param>
|
||||
/// <param name="range">The bounds which apply now.</param>
|
||||
/// <param name="random">Draws the pause.</param>
|
||||
/// <returns>The pause and what is left of it, or NONE when the step starts right away.</returns>
|
||||
public PauseBeforeStep GetPauseBeforeNextStep(DateTimeOffset nowUtc, PauseRange range, Random random)
|
||||
{
|
||||
lock (this.stateLock)
|
||||
{
|
||||
if (this.lastStepEndedUtc is not { } stepEndedUtc)
|
||||
return PauseBeforeStep.NONE;
|
||||
|
||||
if (this.drawnPause is null)
|
||||
{
|
||||
// Nothing is drawn, so bounds which are set before the next step still apply to this gap:
|
||||
if (range.IsNone)
|
||||
return PauseBeforeStep.NONE;
|
||||
|
||||
this.drawnPause = TimeSpan.FromSeconds(random.Next(range.MinimumSeconds, range.MaximumSeconds + 1));
|
||||
}
|
||||
|
||||
var remaining = stepEndedUtc + this.drawnPause.Value - nowUtc;
|
||||
return remaining > TimeSpan.Zero ? new(this.drawnPause.Value, remaining) : PauseBeforeStep.NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.Services.Indexing;
|
||||
|
||||
/// <summary>
|
||||
/// The bounds of the random pause between two indexing steps, cf. PauseBetweenSteps.
|
||||
/// </summary>
|
||||
/// <param name="MinimumSeconds">The shortest pause. Between zero and MaximumSeconds.</param>
|
||||
/// <param name="MaximumSeconds">The longest pause, which may be drawn as well. At most DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS.</param>
|
||||
internal sealed record PauseRange(int MinimumSeconds, int MaximumSeconds)
|
||||
{
|
||||
/// <summary>
|
||||
/// No pause at all, which is the default.
|
||||
/// </summary>
|
||||
public static readonly PauseRange NONE = new(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the next step starts right away.
|
||||
/// </summary>
|
||||
public bool IsNone => this.MaximumSeconds is 0;
|
||||
|
||||
/// <summary>
|
||||
/// The bounds which apply now, as the user set them and as configuration plugins lock them.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings of the indexing.</param>
|
||||
/// <returns>The bounds which apply.</returns>
|
||||
public static PauseRange FromSettings(DataDataSourceIndexing settings)
|
||||
{
|
||||
var minimumSeconds = settings.MinimumPauseBetweenStepsSeconds;
|
||||
var maximumSeconds = settings.MaximumPauseBetweenStepsSeconds;
|
||||
|
||||
//
|
||||
// Which bound a configuration plugin locks matters only while the bounds are the wrong way
|
||||
// round. Asking reads the managed state of the whole app, which bounds in order do without:
|
||||
//
|
||||
if (minimumSeconds <= maximumSeconds)
|
||||
return GetEffective(minimumSeconds, false, maximumSeconds, false);
|
||||
|
||||
var isMinimumLocked = ManagedConfiguration.TryGet(x => x.App.DataSourceIndexing, x => x.MinimumPauseBetweenStepsSeconds, out var minimumMeta) && minimumMeta.IsLocked;
|
||||
var isMaximumLocked = ManagedConfiguration.TryGet(x => x.App.DataSourceIndexing, x => x.MaximumPauseBetweenStepsSeconds, out var maximumMeta) && maximumMeta.IsLocked;
|
||||
return GetEffective(minimumSeconds, isMinimumLocked, maximumSeconds, isMaximumLocked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Works out the bounds which apply, so that the lower one never exceeds the upper one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A bound which a configuration plugin locks wins, and the other one adjusts to it. A bound the
|
||||
/// plugin only presets counts like one the user set. Otherwise, the lower bound wins, i.e., the
|
||||
/// longer pause: it spares the embedding server, which is what the pause is for. That holds as
|
||||
/// well when a plugin locks both bounds the wrong way round.
|
||||
/// </remarks>
|
||||
/// <param name="minimumSeconds">The lower bound as set.</param>
|
||||
/// <param name="isMinimumLocked">Whether a configuration plugin locks the lower bound.</param>
|
||||
/// <param name="maximumSeconds">The upper bound as set.</param>
|
||||
/// <param name="isMaximumLocked">Whether a configuration plugin locks the upper bound.</param>
|
||||
/// <returns>The bounds which apply.</returns>
|
||||
public static PauseRange GetEffective(int minimumSeconds, bool isMinimumLocked, int maximumSeconds, bool isMaximumLocked)
|
||||
{
|
||||
var minimum = Math.Clamp(minimumSeconds, 0, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
var maximum = Math.Clamp(maximumSeconds, 0, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS);
|
||||
if (isMaximumLocked && !isMinimumLocked)
|
||||
return new(Math.Min(minimum, maximum), maximum);
|
||||
|
||||
return new(minimum, Math.Max(minimum, maximum));
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
- Added a way for IT departments to provide the PowerPoint template of the Slide Assistant, its layouts for kinds of slides, which slides of the template a presentation keeps, the maximum number of bullet points per slide, whether slides keep empty picture placeholders, and a picture library, either fixed or as a default which users may change. The template and the picture library may come with the configuration of your organization.
|
||||
- Added a way to read what the AI received for an answer. In the list of sources below an answer, the eye button next to a source shows the passages the AI got from it, for example, from your data sources, a web page, a mail, or your Confluence wiki, as they were after AI Studio removed suspicious instructions. They stay readable when you open the chat again later. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||
- Added organization-wide management for whether AI Studio reads a file attached to a chat again after it has changed. IT departments can set this for everybody, either fixed or as a default which users may change.
|
||||
- Added a way for IT departments to slow down the indexing of local data sources, so that a few people with large folders or mailboxes no longer keep a shared embedding server busy for hours. IT departments set the shortest and the longest pause between the indexing steps, either fixed or as a default which users may change.
|
||||
- Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it.
|
||||
- Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal.
|
||||
- Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better.
|
||||
@@ -41,7 +42,7 @@
|
||||
- Improved the button in the chat for moving the chat to a workspace. You can now use it whenever the workspace list lets you move the chat as well, for example, right after you attached an audio or video file and before you sent your first message.
|
||||
- Improved how AI Studio keeps the message you have not sent yet. When AI Studio stores your chats automatically, your message and its attachments now stay with the chat, and they are back when you open the chat again, for example, after you switched to another chat.
|
||||
- Improved how AI Studio keeps the message of a new chat. When AI Studio stores your chats automatically and you leave a new chat before you send your message, AI Studio now stores the chat with your message. You find it among your disappearing chats, or in its workspace when you started it there.
|
||||
- Improved how AI Studio indexes large local data sources. It now works through them in steps of a few minutes, so your other data sources, such as your mailboxes, stay up to date in between.
|
||||
- Improved how AI Studio indexes large local data sources. It now works through them in steps of a few minutes, so your other data sources, such as your mailboxes, stay up to date in between, and you can set a pause between the steps to spare an embedding server you share with others.
|
||||
- Improved the plugins page with the details of each plugin. The info button next to the name of a plugin shows, among other things, its version, its authors, and where to get support.
|
||||
- Improved the details of a plugin on the plugins page, which now show when the plugin was last changed, in your time zone. Plugin authors can state this time, and AI Studio fills it in by itself when it writes a plugin for you, for example, with the Assistant Builder.
|
||||
- Improved local data sources for folders. You can now choose which files AI Studio indexes, for example, only PDF files, and whether it includes subfolders as well as hidden files and folders. By default, AI Studio now also indexes text files it did not recognize before, such as program code of any kind or a LICENSE file, so a Git repository makes a useful data source. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class DataSourceRelocationWorkflowTests
|
||||
clients.Add(DatabaseRole.INDEX_STORE, this.store.Client);
|
||||
clients.Add(DatabaseRole.VECTOR_STORE, this.vectors);
|
||||
var guard = new PromptInjectionGuardService(null!, this.settings, NullLogger<PromptInjectionGuardService>.Instance, NullLoggerFactory.Instance);
|
||||
this.service = new DataSourceEmbeddingService(this.settings, null!, this.databases, guard, null!, NullLogger<DataSourceEmbeddingService>.Instance);
|
||||
this.service = new DataSourceEmbeddingService(this.settings, null!, this.databases, guard, null!, new ThreadSafeRandom(), NullLogger<DataSourceEmbeddingService>.Instance);
|
||||
this.lifetime = new CancellationTokenSource();
|
||||
this.chunkIds.Clear();
|
||||
this.runningTasks.Clear();
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using AIStudio.Tools.Services.Indexing;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the pause between two indexing steps, which spares an embedding server that many people share.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pause is a gap after every step which worked on documents. It is drawn only once a step is
|
||||
/// about to start, and the time which passed by then counts toward it.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class PauseBetweenStepsTests
|
||||
{
|
||||
private static readonly DateTimeOffset STEP_ENDED = new(2026, 10, 27, 14, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static readonly PauseRange THIRTY_TO_SIXTY_SECONDS = new(30, 60);
|
||||
|
||||
[Test]
|
||||
public void BeforeTheFirstStepNothingWaits()
|
||||
{
|
||||
var random = new LargestRandom();
|
||||
var pause = new PauseBetweenSteps().GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(pause, Is.EqualTo(PauseBeforeStep.NONE));
|
||||
Assert.That(random.Draws, Is.Zero);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheNextStepWaitsForTheWholePauseRightAfterAStep()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
|
||||
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
|
||||
|
||||
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheUpperBoundCanBeDrawn()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
|
||||
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED, new PauseRange(45, 45), new LargestRandom());
|
||||
|
||||
Assert.That(pause.Pause, Is.EqualTo(TimeSpan.FromSeconds(45)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TimeWhichPassedSinceTheStepCountsTowardThePause()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
|
||||
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(25), THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
|
||||
|
||||
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(35))));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AStepWhichStartsLaterAnywayDoesNotWait()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
|
||||
var pause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(60), THIRTY_TO_SIXTY_SECONDS, new LargestRandom());
|
||||
|
||||
Assert.That(pause, Is.EqualTo(PauseBeforeStep.NONE));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NothingIsDrawnWhileThereIsNoPause()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
var random = new LargestRandom();
|
||||
|
||||
var withoutPause = pauses.GetPauseBeforeNextStep(STEP_ENDED, PauseRange.NONE, random);
|
||||
var withPause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(10), THIRTY_TO_SIXTY_SECONDS, random);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(withoutPause, Is.EqualTo(PauseBeforeStep.NONE));
|
||||
Assert.That(withPause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(50))));
|
||||
Assert.That(random.Draws, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AStepWhichStoppedWhileWaitingLeavesTheRestOfThePauseToTheNextOne()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
var random = new LargestRandom();
|
||||
|
||||
pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
|
||||
var nextPause = pauses.GetPauseBeforeNextStep(STEP_ENDED + TimeSpan.FromSeconds(20), new PauseRange(1, 1), random);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(nextPause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(40))));
|
||||
Assert.That(random.Draws, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ANewStepOpensANewGap()
|
||||
{
|
||||
var pauses = new PauseBetweenSteps();
|
||||
pauses.RecordStepEnded(STEP_ENDED);
|
||||
var random = new LargestRandom();
|
||||
pauses.GetPauseBeforeNextStep(STEP_ENDED, THIRTY_TO_SIXTY_SECONDS, random);
|
||||
|
||||
var nextStepEnded = STEP_ENDED + TimeSpan.FromMinutes(6);
|
||||
pauses.RecordStepEnded(nextStepEnded);
|
||||
var pause = pauses.GetPauseBeforeNextStep(nextStepEnded + TimeSpan.FromSeconds(5), new PauseRange(10, 10), random);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(pause, Is.EqualTo(new PauseBeforeStep(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5))));
|
||||
Assert.That(random.Draws, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always draws the largest value it may, and counts how often it was asked.
|
||||
/// </summary>
|
||||
private sealed class LargestRandom : Random
|
||||
{
|
||||
public int Draws { get; private set; }
|
||||
|
||||
public override int Next(int minValue, int maxValue)
|
||||
{
|
||||
this.Draws++;
|
||||
return maxValue - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services.Indexing;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which bounds of the pause between two indexing steps apply, as the user set them and as configuration plugins lock them.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class PauseRangeTests
|
||||
{
|
||||
[Test]
|
||||
public void BoundsInOrderStayAsTheyAre()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(10, false, 20, false), Is.EqualTo(new PauseRange(10, 20)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnUpperBoundBelowTheLowerOneIsRaisedToIt()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(60, false, 20, false), Is.EqualTo(new PauseRange(60, 60)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALockedLowerBoundRaisesTheUpperOne()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(60, true, 20, false), Is.EqualTo(new PauseRange(60, 60)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALockedUpperBoundLowersTheLowerOne()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(60, false, 20, true), Is.EqualTo(new PauseRange(20, 20)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BothBoundsLockedTheWrongWayRoundLetTheLongerPauseWin()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(60, true, 20, true), Is.EqualTo(new PauseRange(60, 60)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BoundsOutsideTheAllowedRangeAreClamped()
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(-5, false, 1_000, false), Is.EqualTo(new PauseRange(0, DataDataSourceIndexing.MAX_PAUSE_BETWEEN_STEPS_SECONDS)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BoundsInOrderComeFromTheSettingsWithoutAskingForLocks()
|
||||
{
|
||||
var settings = new DataDataSourceIndexing
|
||||
{
|
||||
MinimumPauseBetweenStepsSeconds = 10,
|
||||
MaximumPauseBetweenStepsSeconds = 20
|
||||
};
|
||||
|
||||
Assert.That(PauseRange.FromSettings(settings), Is.EqualTo(new PauseRange(10, 20)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyZeroForBothBoundsMeansNoPause()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(PauseRange.GetEffective(0, false, 0, false).IsNone, Is.True);
|
||||
Assert.That(PauseRange.GetEffective(0, false, 5, false).IsNone, Is.False);
|
||||
Assert.That(PauseRange.GetEffective(30, false, 0, false), Is.EqualTo(new PauseRange(30, 30)));
|
||||
Assert.That(new DataDataSourceIndexing().MinimumPauseBetweenStepsSeconds, Is.Zero);
|
||||
Assert.That(new DataDataSourceIndexing().MaximumPauseBetweenStepsSeconds, Is.Zero);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -117,5 +117,5 @@ public sealed class MailDraftSyncTests : ToolRegistryTestBase
|
||||
}
|
||||
|
||||
// Queueing a run needs none of the services which carry it out:
|
||||
private DataSourceEmbeddingService CreateService() => new(this.SettingsManager, null!, null!, null!, null!, NullLogger<DataSourceEmbeddingService>.Instance);
|
||||
private DataSourceEmbeddingService CreateService() => new(this.SettingsManager, null!, null!, null!, null!, null!, NullLogger<DataSourceEmbeddingService>.Instance);
|
||||
}
|
||||
Reference in new issue
Block a user