From 3a7f9b4deba0b10b4c3053d1b7373fd86a23fd01 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 18 Sep 2026 18:11:00 +0200 Subject: [PATCH] Let people change the embedding of an indexed data source --- .../Components/DataSourceManagement.razor.cs | 8 +-- .../DataSourceLocalDirectoryDialog.razor | 53 +++++++------------ .../DataSourceLocalDirectoryDialog.razor.cs | 27 +++++----- .../Dialogs/DataSourceLocalFileDialog.razor | 53 +++++++------------ .../DataSourceLocalFileDialog.razor.cs | 27 +++++----- .../Services/DataSourceEmbeddingService.cs | 22 +++++++- 6 files changed, 88 insertions(+), 102 deletions(-) diff --git a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs index 47461d8f..2aaacbb7 100644 --- a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs +++ b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs @@ -335,8 +335,8 @@ public partial class DataSourceManagement : MSGComponentBase return; IDataSource? editedDataSource = null; - var lockDataSourceIdentity = dataSource is IInternalDataSource - && await this.DataSourceEmbeddingService.ShouldLockDataSourceIdentityAsync(dataSource.Id); + var lockDataSourceOrigin = dataSource is IInternalDataSource + && await this.DataSourceEmbeddingService.ShouldLockDataSourceOriginAsync(dataSource.Id); switch (dataSource) { case DataSourceLocalFile localFile: @@ -344,7 +344,7 @@ public partial class DataSourceManagement : MSGComponentBase { { x => x.IsEditing, true }, { x => x.DataSource, localFile }, - { x => x.LockSourceAndEmbedding, lockDataSourceIdentity }, + { x => x.LockSource, lockDataSourceOrigin }, { x => x.AvailableEmbeddings, this.availableEmbeddingProviders } }; @@ -361,7 +361,7 @@ public partial class DataSourceManagement : MSGComponentBase { { x => x.IsEditing, true }, { x => x.DataSource, localDirectory }, - { x => x.LockSourceAndEmbedding, lockDataSourceIdentity }, + { x => x.LockSource, lockDataSourceOrigin }, { x => x.AvailableEmbeddings, this.availableEmbeddingProviders } }; diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor index ad4f7c0f..600fd819 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor @@ -45,13 +45,13 @@ @T("Select a root directory for this data source. All data in this directory and all its subdirectories will be processed for this data source.") - @if (!this.CanChangeSourceAndEmbedding) + @if (!this.CanChangeSource) { - @T("This data source already has indexed embeddings. Delete and recreate it to change the folder path or embedding model.") + @T("The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding method below can be changed.") } - @if (this.CanChangeSourceAndEmbedding) + @if (this.CanChangeSource) { } @@ -74,38 +74,21 @@ @T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding method.") - @if (this.CanChangeSourceAndEmbedding) - { - - @foreach (var embedding in this.AvailableEmbeddings) - { - - @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) - { - - } - else - { - @embedding.Name - } - - } - - } - else - { - - } + + @foreach (var embedding in this.AvailableEmbeddings) + { + + @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) + { + + } + else + { + @embedding.Name + } + + } + @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId)) { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs index 56c6c7d5..ebd4a374 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs @@ -20,8 +20,15 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase [Parameter] public DataSourceLocalDirectory DataSource { get; set; } + /// + /// Whether the folder this data source reads must stay as it is. + /// + /// + /// Set once the index holds something for this data source. The embedding is not locked along + /// with it: it can be changed, and DataSourceReindexWarning asks what that costs. + /// [Parameter] - public bool LockSourceAndEmbedding { get; set; } + public bool LockSource { get; set; } [Parameter] public IReadOnlyList> AvailableEmbeddings { get; set; } = []; @@ -126,19 +133,10 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase private bool SelectedCloudEmbedding => this.SelectedEmbedding is { IsSelfHosted: false }; - private bool CanChangeSourceAndEmbedding => !this.IsEditing || !this.LockSourceAndEmbedding; + private bool CanChangeSource => !this.IsEditing || !this.LockSource; private IEnumerable> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData(); - private string SelectedEmbeddingNameText - { - get - { - var selectedEmbedding = this.AvailableEmbeddings.FirstOrDefault(x => x.Value == this.dataEmbeddingId); - return string.IsNullOrWhiteSpace(selectedEmbedding.Name) ? T("Unknown") : selectedEmbedding.Name; - } - } - private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null ? T("No embedding selected") : string.IsNullOrWhiteSpace(this.SelectedEmbedding.TokenizerPath) @@ -162,8 +160,11 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase Name = this.dataName, Description = this.dataDescription, Type = DataSourceType.LOCAL_DIRECTORY, - EmbeddingId = this.CanChangeSourceAndEmbedding ? this.dataEmbeddingId : this.DataSource.EmbeddingId, - Path = this.CanChangeSourceAndEmbedding ? this.dataPath : this.DataSource.Path, + EmbeddingId = this.dataEmbeddingId, + + // Kept out of reach of the form while the source is locked, so a stale field cannot point an + // indexed data source somewhere else: + Path = this.CanChangeSource ? this.dataPath : this.DataSource.Path, MaxChunkTokenLength = this.dataMaxChunkTokenLength, ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength, ConfidenceLevel = this.dataConfidenceLevel, diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor index 3a94c100..6d26a3d4 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor @@ -45,13 +45,13 @@ @T("Select a file for this data source. The content of this file will be processed for the data source.") - @if (!this.CanChangeSourceAndEmbedding) + @if (!this.CanChangeSource) { - @T("This data source already has indexed embeddings. Delete and recreate it to change the file path or embedding model.") + @T("The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding method below can be changed.") } - @if (this.CanChangeSourceAndEmbedding) + @if (this.CanChangeSource) { } @@ -74,38 +74,21 @@ @T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding method.") - @if (this.CanChangeSourceAndEmbedding) - { - - @foreach (var embedding in this.AvailableEmbeddings) - { - - @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) - { - - } - else - { - @embedding.Name - } - - } - - } - else - { - - } + + @foreach (var embedding in this.AvailableEmbeddings) + { + + @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) + { + + } + else + { + @embedding.Name + } + + } + @if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId)) { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs index 278db390..cdbe26ab 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -20,8 +20,15 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase [Parameter] public DataSourceLocalFile DataSource { get; set; } + /// + /// Whether the file this data source reads must stay as it is. + /// + /// + /// Set once the index holds something for this data source. The embedding is not locked along + /// with it: it can be changed, and DataSourceReindexWarning asks what that costs. + /// [Parameter] - public bool LockSourceAndEmbedding { get; set; } + public bool LockSource { get; set; } [Parameter] public IReadOnlyList> AvailableEmbeddings { get; set; } = []; @@ -126,19 +133,10 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase private bool SelectedCloudEmbedding => this.SelectedEmbedding is { IsSelfHosted: false }; - private bool CanChangeSourceAndEmbedding => !this.IsEditing || !this.LockSourceAndEmbedding; + private bool CanChangeSource => !this.IsEditing || !this.LockSource; private IEnumerable> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData(); - private string SelectedEmbeddingNameText - { - get - { - var selectedEmbedding = this.AvailableEmbeddings.FirstOrDefault(x => x.Value == this.dataEmbeddingId); - return string.IsNullOrWhiteSpace(selectedEmbedding.Name) ? T("Unknown") : selectedEmbedding.Name; - } - } - private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null ? T("No embedding selected") : string.IsNullOrWhiteSpace(this.SelectedEmbedding.TokenizerPath) @@ -162,8 +160,11 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase Name = this.dataName, Description = this.dataDescription, Type = DataSourceType.LOCAL_FILE, - EmbeddingId = this.CanChangeSourceAndEmbedding ? this.dataEmbeddingId : this.DataSource.EmbeddingId, - FilePath = this.CanChangeSourceAndEmbedding ? this.dataFilePath : this.DataSource.FilePath, + EmbeddingId = this.dataEmbeddingId, + + // Kept out of reach of the form while the source is locked, so a stale field cannot point an + // indexed data source somewhere else: + FilePath = this.CanChangeSource ? this.dataFilePath : this.DataSource.FilePath, MaxChunkTokenLength = this.dataMaxChunkTokenLength, ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength, ConfidenceLevel = this.dataConfidenceLevel, diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs index ccf326af..642ef9a1 100644 --- a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs +++ b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs @@ -199,12 +199,30 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM this.CanRefreshDataSource(dataSource); } - public async Task ShouldLockDataSourceIdentityAsync(string dataSourceId, CancellationToken token = default) + /// + /// Whether the file or folder a data source reads must stay as it is. + /// + /// + /// Locked as soon as the index holds anything, because where a data source reads from is what it + /// is: another folder is another data source, and the path reaches no signature, so swapping it + /// would leave the stored index describing documents nobody points at any more. + /// + /// The embedding provider used to be locked along with it and no longer is. It does reach the + /// signature, so changing it rebuilds the index cleanly -- and DataSourceReindexWarning asks + /// before it does. Locking it as well left a data source whose provider was deleted stuck on + /// keyword search for good, with no way back. + /// + /// Unclear counts as locked: an unavailable index database says nothing about what is stored. + /// + /// The data source to ask about. + /// The cancellation token. + /// True when the source must not be changed. + public async Task ShouldLockDataSourceOriginAsync(string dataSourceId, CancellationToken token = default) { var indexStore = await databaseClientProvider.GetIndexStoreAsync(token); if (!indexStore.IsAvailable) { - logger.LogWarning("Locking identity settings for data source '{DataSourceId}' because the local RAG index database '{DatabaseName}' is unavailable.", dataSourceId, indexStore.Name); + logger.LogWarning("Locking the source of data source '{DataSourceId}' because the local RAG index database '{DatabaseName}' is unavailable.", dataSourceId, indexStore.Name); return true; }