Added local RAG (#756)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Paul KoudelkaandThorsten Sommer authored and GitHub committed 2026-09-09 18:43:37 +02:00
1 parent d043fbc8f0
commit c7b42bee96
179 files changed
+276977 -1421

No files matched your search

@@ -2,11 +2,12 @@ using AIStudio.Components;
using AIStudio.Provider;
using AIStudio.Provider.HuggingFace;
using AIStudio.Settings;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using AIStudio.Tools.Validation;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Dialogs;
@@ -82,6 +83,15 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
[Parameter]
public bool IsEditing { get; init; }
[Parameter]
public string DataTokenizerPath { get; set; } = string.Empty;
[Parameter]
public int DataTokenLimit { get; set; } = EmbeddingProvider.DEFAULT_TOKEN_LIMIT;
[Parameter]
public int DataEmbeddingBatchSize { get; set; } = EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE;
/// <summary>
/// Whether this embedding provider is managed by an enterprise configuration plugin. When true,
/// every field except the API key is locked, matching Settings.EmbeddingProvider.IsEnterpriseConfiguration.
@@ -110,6 +120,13 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private string dataAPIKeyStorageIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private string dataLoadingModelsIssue = string.Empty;
private string dataFilePath = string.Empty;
private string dataCustomTokenizerValidationIssue = string.Empty;
private Task dataTokenizerValidationTask = Task.CompletedTask;
private bool dataStoreWasAttempted;
private bool isTokenizerFileDialogOpen;
private bool showExpertSettings;
private int dataTokenizerValidationRevision;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
@@ -117,7 +134,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private readonly List<Model> availableModels = new();
private readonly Encryption encryption = Program.ENCRYPTION;
private readonly ProviderValidation providerValidation;
public EmbeddingProviderDialog()
{
this.providerValidation = new()
@@ -127,7 +144,8 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
GetPreviousInstanceName = () => this.dataEditingPreviousInstanceName,
GetUsedInstanceNames = () => this.UsedInstanceNames,
GetHost = () => this.DataHost,
IsModelProvidedManually = () => this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is Host.OLLAMA,
IsModelProvidedManually = () => this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost),
GetCustomTokenizerValidationIssue = () => this.dataCustomTokenizerValidationIssue,
};
}
@@ -137,7 +155,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
Model model = default;
if(this.DataLLMProvider is LLMProviders.SELF_HOSTED)
{
if (this.DataHost is Host.OLLAMA)
if (this.DataLLMProvider.IsEmbeddingModelProvidedManually(this.DataHost))
model = new Model(this.dataManuallyModel, null);
else if (this.DataHost is Host.LM_STUDIO)
model = this.DataModel;
@@ -157,6 +175,9 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
Host = this.DataHost,
IsEnterpriseConfiguration = this.IsEnterpriseConfiguration,
EnterpriseConfigurationPluginId = Guid.Empty,
TokenizerPath = this.dataFilePath,
EmbeddingBatchSize = this.DataEmbeddingBatchSize,
TokenLimit = this.DataTokenLimit,
CustomIconDataUrl = this.DataCustomIconDataUrl,
HFInferenceProvider = this.HFInferenceProviderId,
};
@@ -179,6 +200,10 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
if(this.IsEditing)
{
this.dataEditingPreviousInstanceName = this.DataName.ToLowerInvariant();
this.dataFilePath = this.DataTokenizerPath;
this.showExpertSettings = !string.IsNullOrWhiteSpace(this.DataTokenizerPath)
|| this.DataTokenLimit != EmbeddingProvider.DEFAULT_TOKEN_LIMIT
|| this.DataEmbeddingBatchSize != EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE;
// When using self-hosted embedding, we must copy the model name:
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED)
@@ -187,7 +212,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
//
// We cannot load the API key for self-hosted providers:
//
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA)
if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA && this.DataHost is not Host.VLLM)
{
await this.ReloadModels();
await base.OnInitializedAsync();
@@ -244,6 +269,8 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private async Task Store()
{
this.dataStoreWasAttempted = true;
await this.dataTokenizerValidationTask;
await this.form.Validate();
this.dataAPIKeyStorageIssue = string.Empty;
@@ -260,6 +287,15 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
if (!this.dataIsValid)
return;
var response = await this.StoreOrDeleteTokenizerAsync();
if (!response.Success)
{
this.dataCustomTokenizerValidationIssue = string.IsNullOrWhiteSpace(response.Message) ? string.Empty : response.Message;
await this.form.Validate();
return;
}
this.dataFilePath = response.StoredPath;
// Use the data model to store the provider.
// We just return this data to the parent component:
var addedProviderSettings = this.CreateEmbeddingProviderSettings();
@@ -302,6 +338,22 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
return null;
}
private string? ValidateTokenLimit(int tokenLimit)
{
if (tokenLimit < 1)
return T("Please enter a token limit greater than 0.");
return null;
}
private string? ValidateEmbeddingBatchSize(int embeddingBatchSize)
{
if (embeddingBatchSize < 1)
return T("Please enter an embedding batch size greater than 0.");
return null;
}
private void Cancel() => this.MudDialog.Cancel();
private async Task OnAPIKeyChanged(string apiKey)
@@ -314,6 +366,90 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
}
}
private async Task OpenTokenizerFileDialog()
{
if (this.isTokenizerFileDialogOpen)
return;
this.isTokenizerFileDialogOpen = true;
try
{
var response = await this.RustService.SelectFile(T("Choose a custom tokenizer here"), [ FileTypes.JSON ], string.IsNullOrWhiteSpace(this.dataFilePath) ? null : this.dataFilePath);
if (!response.UserCancelled)
await this.OnDataFilePathChanged(response.SelectedFilePath);
}
finally
{
this.isTokenizerFileDialogOpen = false;
}
}
private Task ClearPathTokenizer(MouseEventArgs _)
{
return this.OnDataFilePathChanged(string.Empty);
}
private async Task OnDataFilePathChanged(string filePath)
{
this.dataFilePath = filePath;
var validationRevision = ++this.dataTokenizerValidationRevision;
this.dataTokenizerValidationTask = this.ValidateCustomTokenizer(filePath, validationRevision);
await this.dataTokenizerValidationTask;
if (validationRevision != this.dataTokenizerValidationRevision)
return;
if (this.dataStoreWasAttempted)
await this.form.Validate();
else
this.form.ResetValidation();
}
private async Task ValidateCustomTokenizer(string filePath, int validationRevision)
{
if (string.IsNullOrWhiteSpace(filePath))
{
if (validationRevision == this.dataTokenizerValidationRevision)
this.dataCustomTokenizerValidationIssue = string.Empty;
return;
}
try
{
var response = await this.RustService.ValidateTokenizer(filePath);
if (validationRevision != this.dataTokenizerValidationRevision)
return;
if (response.Success)
this.dataCustomTokenizerValidationIssue = string.Empty;
else
this.dataCustomTokenizerValidationIssue = T("Invalid tokenizer: ") + response.Message;
}
catch (Exception e)
{
if (validationRevision != this.dataTokenizerValidationRevision)
return;
this.Logger.LogError(e, "Failed to validate custom tokenizer.");
this.dataCustomTokenizerValidationIssue = T("Failed to validate the selected tokenizer. Please try again.");
}
}
/// <summary>
/// Stores a new tokenizer or deletes the existing one, based on the specified tokenizer path.
/// If the path is null or empty, any existing tokenizer is removed.
/// Otherwise, the tokenizer is stored at the specified path.
/// </summary>
private Task<TokenizerResponse> StoreOrDeleteTokenizerAsync()
{
var tokenizerId = TokenizerModelId.ForEmbeddingProviderId(this.DataId);
if (string.IsNullOrWhiteSpace(this.dataFilePath))
return this.RustService.DeleteTokenizer(tokenizerId);
return this.RustService.StoreTokenizer(tokenizerId, this.dataFilePath);
}
private void OnHostChanged(Host selectedHost)
{
// When the host changes, reset the model selection state:
@@ -374,4 +510,8 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
};
private bool IsNoneProvider => this.DataLLMProvider is LLMProviders.NONE;
private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings;
private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty;
}