mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-05 08:49:07 +00:00
Configure data sources (#259)
This commit is contained in:
parent
06f66fdab2
commit
63be312bb4
1277
app/ERIClientV1/Client.Generated.cs
Normal file
1277
app/ERIClientV1/Client.Generated.cs
Normal file
File diff suppressed because it is too large
Load Diff
10
app/ERIClientV1/ERIClientV1.csproj
Normal file
10
app/ERIClientV1/ERIClientV1.csproj
Normal file
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
@ -2,6 +2,10 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ERIClients", "ERIClients", "{5C2AF789-287B-4FCB-B675-7273D8CD4579}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERIClientV1", "ERIClientV1\ERIClientV1.csproj", "{9E35A273-0FA6-4BD5-8880-A1DDAC106926}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -12,5 +16,12 @@ Global
|
||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926} = {5C2AF789-287B-4FCB-B675-7273D8CD4579}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -52,7 +52,7 @@ else
|
||||
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
|
||||
Add ERI server preset
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Primary">
|
||||
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
|
||||
Delete this server preset
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@ -346,4 +346,4 @@ else
|
||||
</MudText>
|
||||
|
||||
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
|
||||
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server"/>
|
||||
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />
|
||||
|
@ -739,6 +739,17 @@ public partial class AssistantERI : AssistantBaseCore
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidateDirectory(string path)
|
||||
{
|
||||
if(!this.writeToFilesystem)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(path))
|
||||
return "Please provide a base directory for the ERI server to write files to.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetMultiSelectionAuthText(List<Auth> selectedValues)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
Text="@this.Directory"
|
||||
Label="@this.Label"
|
||||
ReadOnly="@true"
|
||||
Validation="@this.Validation"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Folder"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
|
@ -21,6 +21,9 @@ public partial class SelectDirectory : ComponentBase
|
||||
[Parameter]
|
||||
public string DirectoryDialogTitle { get; set; } = "Select Directory";
|
||||
|
||||
[Parameter]
|
||||
public Func<string, string?> Validation { get; set; } = _ => null;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
|
17
app/MindWork AI Studio/Components/SelectFile.razor
Normal file
17
app/MindWork AI Studio/Components/SelectFile.razor
Normal file
@ -0,0 +1,17 @@
|
||||
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.File"
|
||||
Label="@this.Label"
|
||||
ReadOnly="@true"
|
||||
Validation="@this.Validation"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AttachFile"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
|
||||
Choose File
|
||||
</MudButton>
|
||||
</MudStack>
|
63
app/MindWork AI Studio/Components/SelectFile.razor.cs
Normal file
63
app/MindWork AI Studio/Components/SelectFile.razor.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class SelectFile : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public string File { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> FileChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string FileDialogTitle { get; set; } = "Select File";
|
||||
|
||||
[Parameter]
|
||||
public Func<string, string?> Validation { get; set; } = _ => null;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public RustService RustService { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void InternalFileChanged(string file)
|
||||
{
|
||||
this.File = file;
|
||||
this.FileChanged.InvokeAsync(file);
|
||||
}
|
||||
|
||||
private async Task OpenFileDialog()
|
||||
{
|
||||
var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
|
||||
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
|
||||
|
||||
if (!response.UserCancelled)
|
||||
this.InternalFileChanged(response.SelectedFilePath);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.IntegrationInstructions" HeaderText="Configure Data Sources">
|
||||
<PreviewPrototype/>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
Configured Data Sources
|
||||
</MudText>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
You might configure different data sources. A data source can include one file, all files
|
||||
in a directory, or data from your company. Later, you can incorporate these data sources
|
||||
as needed when the AI requires this data to complete a certain task.
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudTable Items="@this.SettingsManager.ConfigurationData.DataSources" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
<col style="width: 3em;"/>
|
||||
<col/>
|
||||
<col style="width: 12em;"/>
|
||||
<col style="width: 12em;"/>
|
||||
<col style="width: 40em;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>#</MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Type</MudTh>
|
||||
<MudTh>Embedding</MudTh>
|
||||
<MudTh Style="text-align: left;">Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Num</MudTd>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.Type.GetDisplayName()</MudTd>
|
||||
<MudTd>@this.GetEmbeddingName(context)</MudTd>
|
||||
|
||||
<MudTd Style="text-align: left;">
|
||||
@if (context is IERIDataSource)
|
||||
{
|
||||
@* <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"> *@
|
||||
@* Show Information *@
|
||||
@* </MudButton> *@
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteDataSource(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mt-3">No data sources configured yet.</MudText>
|
||||
}
|
||||
|
||||
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Add Data Source" Color="Color.Primary" Variant="Variant.Filled" AnchorOrigin="Origin.CenterCenter" TransformOrigin="Origin.TopLeft" Class="mt-3 mb-6">
|
||||
<MudMenuItem OnClick="() => this.AddDataSource(DataSourceType.ERI_V1)">External Data (ERI-Server v1)</MudMenuItem>
|
||||
<MudMenuItem OnClick="() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY)">Local Directory</MudMenuItem>
|
||||
<MudMenuItem OnClick="() => this.AddDataSource(DataSourceType.LOCAL_FILE)">Local File</MudMenuItem>
|
||||
</MudMenu>
|
||||
</ExpansionPanel>
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using ERI_Client.V1;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components.Settings;
|
||||
|
||||
public partial class SettingsPanelDataSources : SettingsPanelBase
|
||||
{
|
||||
[Parameter]
|
||||
public List<ConfigurationSelectData<string>> AvailableDataSources { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<List<ConfigurationSelectData<string>>> AvailableDataSourcesChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IReadOnlyList<ConfigurationSelectData<string>>> AvailableEmbeddingsFunc { get; set; } = () => [];
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await this.UpdateDataSources();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetEmbeddingName(IDataSource dataSource)
|
||||
{
|
||||
if(dataSource is IInternalDataSource internalDataSource)
|
||||
{
|
||||
var matchedEmbedding = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == internalDataSource.EmbeddingId);
|
||||
if(matchedEmbedding == default)
|
||||
return "No valid embedding";
|
||||
|
||||
return matchedEmbedding.Name;
|
||||
}
|
||||
|
||||
if(dataSource is IExternalDataSource)
|
||||
return "External (ERI)";
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private async Task AddDataSource(DataSourceType type)
|
||||
{
|
||||
IDataSource? addedDataSource = null;
|
||||
switch (type)
|
||||
{
|
||||
case DataSourceType.LOCAL_FILE:
|
||||
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
};
|
||||
|
||||
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>("Add Local File as Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localFileDialogResult = await localFileDialogReference.Result;
|
||||
if (localFileDialogResult is null || localFileDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var localFile = (DataSourceLocalFile)localFileDialogResult.Data!;
|
||||
localFile = localFile with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
|
||||
addedDataSource = localFile;
|
||||
break;
|
||||
|
||||
case DataSourceType.LOCAL_DIRECTORY:
|
||||
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
};
|
||||
|
||||
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>("Add Local Directory as Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
|
||||
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var localDirectory = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
|
||||
localDirectory = localDirectory with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
|
||||
addedDataSource = localDirectory;
|
||||
break;
|
||||
|
||||
case DataSourceType.ERI_V1:
|
||||
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
};
|
||||
|
||||
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>("Add ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var eriDialogResult = await eriDialogReference.Result;
|
||||
if (eriDialogResult is null || eriDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
|
||||
eriDataSource = eriDataSource with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
|
||||
addedDataSource = eriDataSource;
|
||||
break;
|
||||
}
|
||||
|
||||
if(addedDataSource is null)
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.DataSources.Add(addedDataSource);
|
||||
await this.UpdateDataSources();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task EditDataSource(IDataSource dataSource)
|
||||
{
|
||||
IDataSource? editedDataSource = null;
|
||||
switch (dataSource)
|
||||
{
|
||||
case DataSourceLocalFile localFile:
|
||||
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localFile },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
};
|
||||
|
||||
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>("Edit Local File Data Source", localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localFileDialogResult = await localFileDialogReference.Result;
|
||||
if (localFileDialogResult is null || localFileDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!;
|
||||
break;
|
||||
|
||||
case DataSourceLocalDirectory localDirectory:
|
||||
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, localDirectory },
|
||||
{ x => x.AvailableEmbeddings, this.AvailableEmbeddingsFunc() }
|
||||
};
|
||||
|
||||
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>("Edit Local Directory Data Source", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
|
||||
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
|
||||
break;
|
||||
|
||||
case DataSourceERI_V1 eriDataSource:
|
||||
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
|
||||
{
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
};
|
||||
|
||||
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>("Edit ERI v1 Data Source", eriDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var eriDialogResult = await eriDialogReference.Result;
|
||||
if (eriDialogResult is null || eriDialogResult.Canceled)
|
||||
return;
|
||||
|
||||
editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
|
||||
break;
|
||||
}
|
||||
|
||||
if(editedDataSource is null)
|
||||
return;
|
||||
|
||||
this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource;
|
||||
|
||||
await this.UpdateDataSources();
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task DeleteDataSource(IDataSource dataSource)
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", $"Are you sure you want to delete the data source '{dataSource.Name}' of type {dataSource.Type.GetDisplayName()}?" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Data Source", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var applyChanges = dataSource is IInternalDataSource;
|
||||
|
||||
// External data sources may need a secret for authentication:
|
||||
if (dataSource is IExternalDataSource externalDataSource)
|
||||
{
|
||||
// When the auth method is NONE or KERBEROS, we don't need to delete a secret.
|
||||
// In the case of KERBEROS, we don't store the Kerberos ticket in the secret store.
|
||||
if(dataSource is IERIDataSource { AuthMethod: AuthMethod.NONE or AuthMethod.KERBEROS })
|
||||
applyChanges = true;
|
||||
|
||||
// All other auth methods require a secret, which we need to delete now:
|
||||
else
|
||||
{
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource);
|
||||
if (deleteSecretResponse.Success)
|
||||
applyChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(applyChanges)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.DataSources.Remove(dataSource);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.UpdateDataSources();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ShowInformation(IDataSource dataSource)
|
||||
{
|
||||
#warning Implement the information dialog for ERI data sources.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdateDataSources()
|
||||
{
|
||||
this.AvailableDataSources.Clear();
|
||||
foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
this.AvailableDataSources.Add(new (dataSource.Name, dataSource.Id));
|
||||
|
||||
await this.AvailableDataSourcesChanged.InvokeAsync(this.AvailableDataSources);
|
||||
}
|
||||
}
|
@ -21,7 +21,17 @@ public partial class SettingsPanelEmbeddings : SettingsPanelBase
|
||||
var modelName = provider.Model.ToString();
|
||||
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
||||
}
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await this.UpdateEmbeddingProviders();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task AddEmbeddingProvider()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
|
||||
|
128
app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
Normal file
128
app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
Normal file
@ -0,0 +1,128 @@
|
||||
@using ERI_Client.V1
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataName"
|
||||
Label="Data Source Name"
|
||||
Class="mb-6"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatingName"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
<MudStack Row="@true">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataHostname"
|
||||
Label="ERI v1 Server Hostname"
|
||||
Class="mb-6"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatingHostname"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.NetworkCheck"
|
||||
AdornmentColor="Color.Info"
|
||||
Variant="Variant.Text"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
|
||||
<MudNumericField
|
||||
Label="Port"
|
||||
Immediate="@true"
|
||||
Min="1" Max="65535"
|
||||
Validation="@this.dataSourceValidation.ValidatePort"
|
||||
@bind-Value="@this.dataPort"
|
||||
Variant="Variant.Text"
|
||||
Margin="Margin.Dense"/>
|
||||
</MudStack>
|
||||
|
||||
@if (!this.IsConnectionEncrypted())
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||
Please note: the connection to the ERI v1 server is not encrypted. This means that all
|
||||
data sent to the server is transmitted in plain text. Please ask the ERI server administrator
|
||||
to enable encryption.
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
@if (this.IsConnectionPossible())
|
||||
{
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled" Color="@this.GetTestResultColor()" StartIcon="@this.GetTestResultIcon()" Class="mb-3" OnClick="@this.TestConnection">
|
||||
Test connection & read available metadata
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@this.GetTestResultText()
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@if(this.availableAuthMethods.Count > 0 || this.dataAuthMethod != default)
|
||||
{
|
||||
<MudSelect @bind-Value="@this.dataAuthMethod" Text="@this.dataAuthMethod.DisplayName()" Label="Authentication Method" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateAuthMethod">
|
||||
@foreach (var authMethod in this.availableAuthMethods)
|
||||
{
|
||||
<MudSelectItem Value="@authMethod">@authMethod.DisplayName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
@if (this.NeedsSecret())
|
||||
{
|
||||
if (this.dataAuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataUsername"
|
||||
Label="Username"
|
||||
Class="mb-6"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateUsername"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Person2"
|
||||
AdornmentColor="Color.Info"
|
||||
Variant="Variant.Text"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataSecret"
|
||||
Label="@this.GetSecretLabel()"
|
||||
Class="mb-6"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatingSecret"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Security"
|
||||
AdornmentColor="Color.Info"
|
||||
Variant="Variant.Text"
|
||||
InputType="InputType.Password"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
</MudForm>
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@:Update
|
||||
}
|
||||
else
|
||||
{
|
||||
@:Add
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
277
app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
Normal file
277
app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
Normal file
@ -0,0 +1,277 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Validation;
|
||||
|
||||
using ERI_Client.V1;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public bool IsEditing { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public DataSourceERI_V1 DataSource { get; set; }
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ProviderDialog> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private readonly DataSourceValidation dataSourceValidation;
|
||||
private readonly Encryption encryption = Program.ENCRYPTION;
|
||||
|
||||
/// <summary>
|
||||
/// The list of used data source names. We need this to check for uniqueness.
|
||||
/// </summary>
|
||||
private List<string> UsedDataSourcesNames { get; set; } = [];
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataSecretStorageIssue = string.Empty;
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
private HttpClient? httpClient;
|
||||
private List<AuthMethod> availableAuthMethods = [];
|
||||
private bool connectionTested;
|
||||
private bool connectionSuccessfulTested;
|
||||
|
||||
private uint dataNum;
|
||||
private string dataSecret = string.Empty;
|
||||
private string dataId = Guid.NewGuid().ToString();
|
||||
private string dataName = string.Empty;
|
||||
private string dataHostname = string.Empty;
|
||||
private int dataPort;
|
||||
private AuthMethod dataAuthMethod;
|
||||
private string dataUsername = string.Empty;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
|
||||
public DataSourceERI_V1Dialog()
|
||||
{
|
||||
this.dataSourceValidation = new()
|
||||
{
|
||||
GetAuthMethod = () => this.dataAuthMethod,
|
||||
GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
|
||||
GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
|
||||
GetSecretStorageIssue = () => this.dataSecretStorageIssue,
|
||||
GetTestedConnection = () => this.connectionTested,
|
||||
GetTestedConnectionResult = () => this.connectionSuccessfulTested,
|
||||
GetAvailableAuthMethods = () => this.availableAuthMethods,
|
||||
};
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
// Load the used instance names:
|
||||
this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
|
||||
this.dataNum = this.DataSource.Num;
|
||||
this.dataId = this.DataSource.Id;
|
||||
this.dataName = this.DataSource.Name;
|
||||
this.dataHostname = this.DataSource.Hostname;
|
||||
this.dataPort = this.DataSource.Port;
|
||||
this.dataAuthMethod = this.DataSource.AuthMethod;
|
||||
this.dataUsername = this.DataSource.Username;
|
||||
|
||||
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
// Load the secret:
|
||||
var requestedSecret = await this.RustService.GetSecret(this);
|
||||
if (requestedSecret.Success)
|
||||
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
else
|
||||
{
|
||||
this.dataSecret = string.Empty;
|
||||
this.dataSecretStorageIssue = $"Failed to load the auth. secret from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the secret again.";
|
||||
await this.form.Validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Reset the validation when not editing and on the first render.
|
||||
// We don't want to show validation errors when the user opens the dialog.
|
||||
if(!this.IsEditing && firstRender)
|
||||
this.form.ResetValidation();
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ISecretId
|
||||
|
||||
public string SecretId => this.dataId;
|
||||
|
||||
public string SecretName => this.dataName;
|
||||
|
||||
#endregion
|
||||
|
||||
private DataSourceERI_V1 CreateDataSource()
|
||||
{
|
||||
var cleanedHostname = this.dataHostname.Trim();
|
||||
return new DataSourceERI_V1
|
||||
{
|
||||
Id = this.dataId,
|
||||
Num = this.dataNum,
|
||||
Port = this.dataPort,
|
||||
Name = this.dataName,
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
AuthMethod = this.dataAuthMethod,
|
||||
Username = this.dataUsername,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
private bool IsConnectionPossible()
|
||||
{
|
||||
if(this.dataSourceValidation.ValidatingHostname(this.dataHostname) is not null)
|
||||
return false;
|
||||
|
||||
if(this.dataSourceValidation.ValidatePort(this.dataPort) is not null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri($"{this.dataHostname}:{this.dataPort}"),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
|
||||
using (this.httpClient)
|
||||
{
|
||||
var client = new Client(this.httpClient);
|
||||
var authSchemes = await client.GetAuthMethodsAsync();
|
||||
if (authSchemes is null)
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||
this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server did not respond.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.availableAuthMethods = authSchemes.Select(n => n.AuthMethod).ToList();
|
||||
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccessfulTested = true;
|
||||
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||
this.dataIssues[^1] = $"Failed to connect to the ERI v1 server. The message was: {e.Message}";
|
||||
this.Logger.LogError($"Failed to connect to the ERI v1 server. Message: {e.Message}");
|
||||
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccessfulTested = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTestResultText()
|
||||
{
|
||||
if(!this.connectionTested)
|
||||
return "Not tested yet.";
|
||||
|
||||
return this.connectionSuccessfulTested ? "Connection successful." : "Connection failed.";
|
||||
}
|
||||
|
||||
private Color GetTestResultColor()
|
||||
{
|
||||
if (!this.connectionTested)
|
||||
return Color.Default;
|
||||
|
||||
return this.connectionSuccessfulTested ? Color.Success : Color.Error;
|
||||
}
|
||||
|
||||
private string GetTestResultIcon()
|
||||
{
|
||||
if (!this.connectionTested)
|
||||
return Icons.Material.Outlined.HourglassEmpty;
|
||||
|
||||
return this.connectionSuccessfulTested ? Icons.Material.Outlined.CheckCircle : Icons.Material.Outlined.Error;
|
||||
}
|
||||
|
||||
private bool NeedsSecret() => this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
|
||||
|
||||
private string GetSecretLabel() => this.dataAuthMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Access Token",
|
||||
AuthMethod.USERNAME_PASSWORD => "Password",
|
||||
_ => "Secret",
|
||||
};
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
var testConnectionValidation = this.dataSourceValidation.ValidateTestedConnection();
|
||||
if(testConnectionValidation is not null)
|
||||
{
|
||||
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||
this.dataIssues[^1] = testConnectionValidation;
|
||||
this.dataIsValid = false;
|
||||
}
|
||||
|
||||
this.dataSecretStorageIssue = string.Empty;
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
var addedDataSource = this.CreateDataSource();
|
||||
if (!string.IsNullOrWhiteSpace(this.dataSecret))
|
||||
{
|
||||
// Store the secret in the OS secure storage:
|
||||
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret);
|
||||
if (!storeResponse.Success)
|
||||
{
|
||||
this.dataSecretStorageIssue = $"Failed to store the auth. secret in the operating system. The message was: {storeResponse.Issue}. Please try again.";
|
||||
await this.form.Validate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.MudDialog.Close(DialogResult.Ok(addedDataSource));
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataName"
|
||||
Label="Data Source Name"
|
||||
Class="mb-6"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatingName"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
Select a root directory for this data source. All data in this directory and all
|
||||
its subdirectories will be processed for this data source.
|
||||
</MudJustifiedText>
|
||||
<SelectDirectory @bind-Directory="@this.dataPath" Label="Selected base directory for this data source" DirectoryDialogTitle="Select the base directory" Validation="@this.dataSourceValidation.ValidatePath" />
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
In order for the AI to be able to determine the appropriate data at any time, you must
|
||||
choose an embedding method.
|
||||
</MudJustifiedText>
|
||||
<MudSelect @bind-Value="@this.dataEmbeddingId" Label="Embedding" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateEmbeddingId">
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">@embedding.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
if (this.SelectedCloudEmbedding)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||
@if (string.IsNullOrWhiteSpace(this.dataPath))
|
||||
{
|
||||
@: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud.
|
||||
@: Please confirm that you have read and understood this.
|
||||
}
|
||||
else
|
||||
{
|
||||
@: Please note: the embedding you selected runs in the cloud. All your data from the
|
||||
@: folder '@this.dataPath' and all its subdirectories will be sent to the cloud. Please
|
||||
@: confirm that you have read and understood this.
|
||||
}
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.dataUserAcknowledgedCloudEmbedding" Label="I confirm that I have read and understood the above" LabelOn="Yes, please send my data to the cloud" LabelOff="No, I will chose another embedding" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
|
||||
</MudJustifiedText>
|
||||
}
|
||||
}
|
||||
</MudForm>
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@:Update
|
||||
}
|
||||
else
|
||||
{
|
||||
@:Add
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
@ -0,0 +1,120 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Validation;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceLocalDirectoryDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public bool IsEditing { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public DataSourceLocalDirectory DataSource { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<ConfigurationSelectData<string>> AvailableEmbeddings { get; set; } = [];
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private readonly DataSourceValidation dataSourceValidation;
|
||||
|
||||
/// <summary>
|
||||
/// The list of used data source names. We need this to check for uniqueness.
|
||||
/// </summary>
|
||||
private List<string> UsedDataSourcesNames { get; set; } = [];
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
|
||||
private uint dataNum;
|
||||
private string dataId = Guid.NewGuid().ToString();
|
||||
private string dataName = string.Empty;
|
||||
private bool dataUserAcknowledgedCloudEmbedding;
|
||||
private string dataEmbeddingId = string.Empty;
|
||||
private string dataPath = string.Empty;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
|
||||
public DataSourceLocalDirectoryDialog()
|
||||
{
|
||||
this.dataSourceValidation = new()
|
||||
{
|
||||
GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
|
||||
GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
|
||||
GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
|
||||
};
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
// Load the used instance names:
|
||||
this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
|
||||
this.dataNum = this.DataSource.Num;
|
||||
this.dataId = this.DataSource.Id;
|
||||
this.dataName = this.DataSource.Name;
|
||||
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
||||
this.dataPath = this.DataSource.Path;
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Reset the validation when not editing and on the first render.
|
||||
// We don't want to show validation errors when the user opens the dialog.
|
||||
if(!this.IsEditing && firstRender)
|
||||
this.form.ResetValidation();
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted;
|
||||
|
||||
private DataSourceLocalDirectory CreateDataSource() => new()
|
||||
{
|
||||
Id = this.dataId,
|
||||
Num = this.dataNum,
|
||||
Name = this.dataName,
|
||||
Type = DataSourceType.LOCAL_DIRECTORY,
|
||||
EmbeddingId = this.dataEmbeddingId,
|
||||
Path = this.dataPath,
|
||||
};
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
var addedDataSource = this.CreateDataSource();
|
||||
this.MudDialog.Close(DialogResult.Ok(addedDataSource));
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.dataName"
|
||||
Label="Data Source Name"
|
||||
Class="mb-6"
|
||||
MaxLength="40"
|
||||
Counter="40"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidatingName"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
|
||||
AdornmentColor="Color.Info"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
Select a file for this data source. The content of this file will be processed for the data source.
|
||||
</MudJustifiedText>
|
||||
<SelectFile @bind-File="@this.dataFilePath" Label="Selected file path for this data source" FileDialogTitle="Select the file" Validation="@this.dataSourceValidation.ValidateFilePath" />
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
In order for the AI to be able to determine the appropriate data at any time, you must
|
||||
choose an embedding method.
|
||||
</MudJustifiedText>
|
||||
<MudSelect @bind-Value="@this.dataEmbeddingId" Label="Embedding" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.GetSelectedCloudEmbedding">
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">@embedding.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
if (this.SelectedCloudEmbedding)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||
@if (string.IsNullOrWhiteSpace(this.dataFilePath))
|
||||
{
|
||||
@: Please note: the embedding you selected runs in the cloud. All your data will be sent to the cloud.
|
||||
@: Please confirm that you have read and understood this.
|
||||
}
|
||||
else
|
||||
{
|
||||
@: Please note: the embedding you selected runs in the cloud. All your data within the
|
||||
@: file '@this.dataFilePath' will be sent to the cloud. Please confirm that you have read
|
||||
@: and understood this.
|
||||
}
|
||||
</MudJustifiedText>
|
||||
<MudTextSwitch @bind-Value="@this.dataUserAcknowledgedCloudEmbedding" Label="I confirm that I have read and understood the above" LabelOn="Yes, please send my data to the cloud" LabelOff="No, I will chose another embedding" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
|
||||
</MudJustifiedText>
|
||||
}
|
||||
}
|
||||
</MudForm>
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
|
||||
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@if(this.IsEditing)
|
||||
{
|
||||
@:Update
|
||||
}
|
||||
else
|
||||
{
|
||||
@:Add
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
@ -0,0 +1,121 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Validation;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceLocalFileDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public bool IsEditing { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public DataSourceLocalFile DataSource { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<ConfigurationSelectData<string>> AvailableEmbeddings { get; set; } = [];
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private readonly DataSourceValidation dataSourceValidation;
|
||||
|
||||
/// <summary>
|
||||
/// The list of used data source names. We need this to check for uniqueness.
|
||||
/// </summary>
|
||||
private List<string> UsedDataSourcesNames { get; set; } = [];
|
||||
|
||||
private bool dataIsValid;
|
||||
private string[] dataIssues = [];
|
||||
private string dataEditingPreviousInstanceName = string.Empty;
|
||||
|
||||
private uint dataNum;
|
||||
private string dataId = Guid.NewGuid().ToString();
|
||||
private string dataName = string.Empty;
|
||||
private bool dataUserAcknowledgedCloudEmbedding;
|
||||
private string dataEmbeddingId = string.Empty;
|
||||
private string dataFilePath = string.Empty;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
|
||||
public DataSourceLocalFileDialog()
|
||||
{
|
||||
this.dataSourceValidation = new()
|
||||
{
|
||||
GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
|
||||
GetPreviousDataSourceName = () => this.dataEditingPreviousInstanceName,
|
||||
GetUsedDataSourceNames = () => this.UsedDataSourcesNames,
|
||||
};
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
// Load the used instance names:
|
||||
this.UsedDataSourcesNames = this.SettingsManager.ConfigurationData.DataSources.Select(x => x.Name.ToLowerInvariant()).ToList();
|
||||
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
|
||||
this.dataNum = this.DataSource.Num;
|
||||
this.dataId = this.DataSource.Id;
|
||||
this.dataName = this.DataSource.Name;
|
||||
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
||||
this.dataFilePath = this.DataSource.FilePath;
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Reset the validation when not editing and on the first render.
|
||||
// We don't want to show validation errors when the user opens the dialog.
|
||||
if(!this.IsEditing && firstRender)
|
||||
this.form.ResetValidation();
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId).IsSelfHosted;
|
||||
|
||||
private DataSourceLocalFile CreateDataSource() => new()
|
||||
{
|
||||
Id = this.dataId,
|
||||
Num = this.dataNum,
|
||||
Name = this.dataName,
|
||||
Type = DataSourceType.LOCAL_FILE,
|
||||
EmbeddingId = this.dataEmbeddingId,
|
||||
FilePath = this.dataFilePath,
|
||||
};
|
||||
|
||||
private async Task Store()
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
return;
|
||||
|
||||
var addedDataSource = this.CreateDataSource();
|
||||
this.MudDialog.Close(DialogResult.Ok(addedDataSource));
|
||||
}
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
}
|
@ -197,8 +197,7 @@ public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
|
||||
private async Task Store()
|
||||
{
|
||||
await this.form.Validate();
|
||||
if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
|
||||
this.dataAPIKeyStorageIssue = string.Empty;
|
||||
this.dataAPIKeyStorageIssue = string.Empty;
|
||||
|
||||
// When the data is not valid, we don't store it:
|
||||
if (!this.dataIsValid)
|
||||
|
@ -53,6 +53,10 @@
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ERIClientV1\ERIClientV1.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Read the meta data file -->
|
||||
<Target Name="ReadMetaData" BeforeTargets="BeforeBuild">
|
||||
<Error Text="The ../../metadata.txt file was not found!" Condition="!Exists('../../metadata.txt')" />
|
||||
|
@ -7,6 +7,7 @@
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
|
||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" />
|
||||
<SettingsPanelDataSources AvailableLLMProvidersFunc="() => this.availableLLMProviders" AvailableEmbeddingsFunc="() => this.availableEmbeddingProviders" @bind-AvailableDataSources="@this.availableDataSources" />
|
||||
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||
<SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||
|
@ -11,6 +11,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
|
||||
private List<ConfigurationSelectData<string>> availableLLMProviders = new();
|
||||
private List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
||||
private List<ConfigurationSelectData<string>> availableDataSources = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
|
@ -25,6 +25,11 @@ public sealed class Data
|
||||
/// A collection of embedding providers configured.
|
||||
/// </summary>
|
||||
public List<EmbeddingProvider> EmbeddingProviders { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// A collection of data sources configured.
|
||||
/// </summary>
|
||||
public List<IDataSource> DataSources { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// List of configured profiles.
|
||||
@ -41,6 +46,11 @@ public sealed class Data
|
||||
/// </summary>
|
||||
public uint NextEmbeddingNum { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The next data source number to use.
|
||||
/// </summary>
|
||||
public uint NextDataSourceNum { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The next profile number to use.
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,46 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// An external data source, accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI.
|
||||
/// </summary>
|
||||
public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
{
|
||||
public DataSourceERI_V1()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Num { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id { get; init; } = Guid.Empty.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceType Type { get; init; } = DataSourceType.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The hostname of the ERI server.
|
||||
/// </summary>
|
||||
public string Hostname { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The port of the ERI server.
|
||||
/// </summary>
|
||||
public int Port { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The authentication method to use.
|
||||
/// </summary>
|
||||
public AuthMethod AuthMethod { get; init; } = AuthMethod.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
|
||||
/// </summary>
|
||||
public string Username { get; init; } = string.Empty;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a local directory as a data source.
|
||||
/// </summary>
|
||||
public readonly record struct DataSourceLocalDirectory : IInternalDataSource
|
||||
{
|
||||
public DataSourceLocalDirectory()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Num { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id { get; init; } = Guid.Empty.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceType Type { get; init; } = DataSourceType.NONE;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EmbeddingId { get; init; } = Guid.Empty.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// The path to the directory.
|
||||
/// </summary>
|
||||
public string Path { get; init; } = string.Empty;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one local file as a data source.
|
||||
/// </summary>
|
||||
public readonly record struct DataSourceLocalFile : IInternalDataSource
|
||||
{
|
||||
public DataSourceLocalFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint Num { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id { get; init; } = Guid.Empty.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceType Type { get; init; } = DataSourceType.NONE;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EmbeddingId { get; init; } = Guid.Empty.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// The path to the file.
|
||||
/// </summary>
|
||||
public string FilePath { get; init; } = string.Empty;
|
||||
}
|
27
app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs
Normal file
27
app/MindWork AI Studio/Settings/DataModel/DataSourceType.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// AI Studio data source types.
|
||||
/// </summary>
|
||||
public enum DataSourceType
|
||||
{
|
||||
/// <summary>
|
||||
/// No data source.
|
||||
/// </summary>
|
||||
NONE = 0,
|
||||
|
||||
/// <summary>
|
||||
/// One file on the local machine (or a network share).
|
||||
/// </summary>
|
||||
LOCAL_FILE,
|
||||
|
||||
/// <summary>
|
||||
/// A directory on the local machine (or a network share).
|
||||
/// </summary>
|
||||
LOCAL_DIRECTORY,
|
||||
|
||||
/// <summary>
|
||||
/// External data source accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI.
|
||||
/// </summary>
|
||||
ERI_V1,
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for data source types.
|
||||
/// </summary>
|
||||
public static class DataSourceTypeExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the display name of the data source type.
|
||||
/// </summary>
|
||||
/// <param name="type">The data source type.</param>
|
||||
/// <returns>The display name of the data source type.</returns>
|
||||
public static string GetDisplayName(this DataSourceType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
DataSourceType.LOCAL_FILE => "Local File",
|
||||
DataSourceType.LOCAL_DIRECTORY => "Local Directory",
|
||||
DataSourceType.ERI_V1 => "External ERI Server (v1)",
|
||||
|
||||
_ => "None",
|
||||
};
|
||||
}
|
||||
}
|
35
app/MindWork AI Studio/Settings/IDataSource.cs
Normal file
35
app/MindWork AI Studio/Settings/IDataSource.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// The common interface for all data sources.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type_discriminator")]
|
||||
[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))]
|
||||
[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))]
|
||||
[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
|
||||
public interface IDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of the data source.
|
||||
/// </summary>
|
||||
public uint Num { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier of the data source.
|
||||
/// </summary>
|
||||
public string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the data source.
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which type of data source is this?
|
||||
/// </summary>
|
||||
public DataSourceType Type { get; init; }
|
||||
}
|
12
app/MindWork AI Studio/Settings/IERIDataSource.cs
Normal file
12
app/MindWork AI Studio/Settings/IERIDataSource.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public interface IERIDataSource : IExternalDataSource
|
||||
{
|
||||
public string Hostname { get; init; }
|
||||
|
||||
public int Port { get; init; }
|
||||
|
||||
public AuthMethod AuthMethod { get; init; }
|
||||
}
|
16
app/MindWork AI Studio/Settings/IExternalDataSource.cs
Normal file
16
app/MindWork AI Studio/Settings/IExternalDataSource.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public interface IExternalDataSource : IDataSource, ISecretId
|
||||
{
|
||||
#region Implementation of ISecretId
|
||||
|
||||
[JsonIgnore]
|
||||
string ISecretId.SecretId => this.Id;
|
||||
|
||||
[JsonIgnore]
|
||||
string ISecretId.SecretName => this.Name;
|
||||
|
||||
#endregion
|
||||
}
|
9
app/MindWork AI Studio/Settings/IInternalDataSource.cs
Normal file
9
app/MindWork AI Studio/Settings/IInternalDataSource.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
public interface IInternalDataSource : IDataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier of the embedding method used by this internal data source.
|
||||
/// </summary>
|
||||
public string EmbeddingId { get; init; }
|
||||
}
|
16
app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs
Normal file
16
app/MindWork AI Studio/Tools/AuthMethodsV1Extensions.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class AuthMethodsV1Extensions
|
||||
{
|
||||
public static string DisplayName(this AuthMethod authMethod) => authMethod switch
|
||||
{
|
||||
AuthMethod.NONE => "None",
|
||||
AuthMethod.USERNAME_PASSWORD => "Username & Password",
|
||||
AuthMethod.KERBEROS => "SSO (Kerberos)",
|
||||
AuthMethod.TOKEN => "Access Token",
|
||||
|
||||
_ => "Unknown authentication method",
|
||||
};
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for selecting a file.
|
||||
/// </summary>
|
||||
/// <param name="UserCancelled">Was the file selection canceled?</param>
|
||||
/// <param name="SelectedFilePath">The selected file, if any.</param>
|
||||
public readonly record struct FileSelectionResponse(bool UserCancelled, string SelectedFilePath);
|
7
app/MindWork AI Studio/Tools/Rust/PreviousFile.cs
Normal file
7
app/MindWork AI Studio/Tools/Rust/PreviousFile.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for selecting a file when a previous file was selected.
|
||||
/// </summary>
|
||||
/// <param name="FilePath">The path of the previous file.</param>
|
||||
public readonly record struct PreviousFile(string FilePath);
|
76
app/MindWork AI Studio/Tools/RustService.APIKeys.cs
Normal file
76
app/MindWork AI Studio/Tools/RustService.APIKeys.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to get the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the API key for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the API key for.</param>
|
||||
/// <param name="key">The API key to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key)
|
||||
{
|
||||
var encryptedKey = await this.encryptor!.Encrypt(key);
|
||||
var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the API key for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
120
app/MindWork AI Studio/Tools/RustService.App.cs
Normal file
120
app/MindWork AI Studio/Tools/RustService.App.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
||||
//
|
||||
// Note I: In the production environment, the Rust runtime is already running
|
||||
// and listening on the given port. In the development environment, the IDE
|
||||
// starts the Rust runtime in parallel with the .NET runtime. Since the
|
||||
// Rust runtime needs some time to start, we have to wait for it to be ready.
|
||||
//
|
||||
const int MAX_TRIES = 160;
|
||||
var tris = 0;
|
||||
var wait4Try = TimeSpan.FromMilliseconds(250);
|
||||
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
|
||||
while (tris++ < MAX_TRIES)
|
||||
{
|
||||
//
|
||||
// Note II: We use a new HttpClient instance for each try to avoid
|
||||
// .NET is caching the result. When we use the same HttpClient
|
||||
// instance, we would always get the same result (403 forbidden),
|
||||
// without even trying to connect to the Rust server.
|
||||
//
|
||||
|
||||
using var initialHttp = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
//
|
||||
// Note III: We have to create also a new HttpClientHandler instance
|
||||
// for each try to avoid .NET is caching the result. This is necessary
|
||||
// because it gets disposed when the HttpClient instance gets disposed.
|
||||
//
|
||||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
||||
{
|
||||
if(certificate is null)
|
||||
return false;
|
||||
|
||||
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return currentCertificateFingerprint == this.certificateFingerprint;
|
||||
}
|
||||
});
|
||||
|
||||
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
|
||||
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||
initialHttp.DefaultRequestHeaders.AddApiToken();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await initialHttp.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
|
||||
await Task.Delay(wait4Try);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||
var appPort = int.Parse(appPortContent);
|
||||
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
|
||||
return appPort;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
|
||||
Console.WriteLine(e.InnerException);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Failed to receive the app port from Rust runtime.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task AppIsReady()
|
||||
{
|
||||
const string URL = "/system/dotnet/ready";
|
||||
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
|
||||
try
|
||||
{
|
||||
var response = await this.http.GetAsync(URL);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetConfigDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/config");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetDataDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/data");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
50
app/MindWork AI Studio/Tools/RustService.Clipboard.cs
Normal file
50
app/MindWork AI Studio/Tools/RustService.Clipboard.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to copy the given text to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||
/// <param name="text">The text to copy to the clipboard.</param>
|
||||
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||
{
|
||||
var message = "Successfully copied the text to your clipboard";
|
||||
var iconColor = Color.Error;
|
||||
var severity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var encryptedText = await text.Encrypt(this.encryptor!);
|
||||
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
{
|
||||
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
iconColor = Color.Success;
|
||||
severity = Severity.Success;
|
||||
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
snackbar.Add(message, severity, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = iconColor;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
32
app/MindWork AI Studio/Tools/RustService.FileSystem.cs
Normal file
32
app/MindWork AI Studio/Tools/RustService.FileSystem.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||
{
|
||||
PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||
return new DirectorySelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
|
||||
public async Task<FileSelectionResponse> SelectFile(string title, string? initialFile = null)
|
||||
{
|
||||
PreviousFile? previousFile = initialFile is null ? null : new (initialFile);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");
|
||||
return new FileSelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
}
|
76
app/MindWork AI Studio/Tools/RustService.Secrets.cs
Normal file
76
app/MindWork AI Studio/Tools/RustService.Secrets.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to get the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the data for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetSecret(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the secret data due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the data for.</param>
|
||||
/// <param name="secretData">The data to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData)
|
||||
{
|
||||
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
||||
var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the secret data due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the data for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the secret data due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
40
app/MindWork AI Studio/Tools/RustService.Updates.cs
Normal file
40
app/MindWork AI Studio/Tools/RustService.Updates.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<UpdateResponse> CheckForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
||||
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to check for an update.");
|
||||
return new UpdateResponse
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await this.http.GetAsync("/updates/install", cts.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
@ -10,7 +8,7 @@ namespace AIStudio.Tools;
|
||||
/// <summary>
|
||||
/// Calling Rust functions.
|
||||
/// </summary>
|
||||
public sealed class RustService : IDisposable
|
||||
public sealed partial class RustService : IDisposable
|
||||
{
|
||||
private readonly HttpClient http;
|
||||
|
||||
@ -60,281 +58,6 @@ public sealed class RustService : IDisposable
|
||||
{
|
||||
this.encryptor = encryptionService;
|
||||
}
|
||||
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
||||
//
|
||||
// Note I: In the production environment, the Rust runtime is already running
|
||||
// and listening on the given port. In the development environment, the IDE
|
||||
// starts the Rust runtime in parallel with the .NET runtime. Since the
|
||||
// Rust runtime needs some time to start, we have to wait for it to be ready.
|
||||
//
|
||||
const int MAX_TRIES = 160;
|
||||
var tris = 0;
|
||||
var wait4Try = TimeSpan.FromMilliseconds(250);
|
||||
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
|
||||
while (tris++ < MAX_TRIES)
|
||||
{
|
||||
//
|
||||
// Note II: We use a new HttpClient instance for each try to avoid
|
||||
// .NET is caching the result. When we use the same HttpClient
|
||||
// instance, we would always get the same result (403 forbidden),
|
||||
// without even trying to connect to the Rust server.
|
||||
//
|
||||
|
||||
using var initialHttp = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
//
|
||||
// Note III: We have to create also a new HttpClientHandler instance
|
||||
// for each try to avoid .NET is caching the result. This is necessary
|
||||
// because it gets disposed when the HttpClient instance gets disposed.
|
||||
//
|
||||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
||||
{
|
||||
if(certificate is null)
|
||||
return false;
|
||||
|
||||
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return currentCertificateFingerprint == this.certificateFingerprint;
|
||||
}
|
||||
});
|
||||
|
||||
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
|
||||
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||
initialHttp.DefaultRequestHeaders.AddApiToken();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await initialHttp.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
|
||||
await Task.Delay(wait4Try);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||
var appPort = int.Parse(appPortContent);
|
||||
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
|
||||
return appPort;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
|
||||
Console.WriteLine(e.InnerException);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Failed to receive the app port from Rust runtime.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task AppIsReady()
|
||||
{
|
||||
const string URL = "/system/dotnet/ready";
|
||||
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
|
||||
try
|
||||
{
|
||||
var response = await this.http.GetAsync(URL);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetConfigDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/config");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetDataDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/data");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to copy the given text to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||
/// <param name="text">The text to copy to the clipboard.</param>
|
||||
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||
{
|
||||
var message = "Successfully copied the text to your clipboard";
|
||||
var iconColor = Color.Error;
|
||||
var severity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var encryptedText = await text.Encrypt(this.encryptor!);
|
||||
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
{
|
||||
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
iconColor = Color.Success;
|
||||
severity = Severity.Success;
|
||||
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
snackbar.Add(message, severity, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = iconColor;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UpdateResponse> CheckForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
||||
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to check for an update.");
|
||||
return new UpdateResponse
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await this.http.GetAsync("/updates/install", cts.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the API key for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the API key for.</param>
|
||||
/// <param name="key">The API key to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key)
|
||||
{
|
||||
var encryptedKey = await this.encryptor!.Encrypt(key);
|
||||
var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the API key for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||
{
|
||||
PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||
return new DirectorySelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
|
149
app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs
Normal file
149
app/MindWork AI Studio/Tools/Validation/DataSourceValidation.cs
Normal file
@ -0,0 +1,149 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
namespace AIStudio.Tools.Validation;
|
||||
|
||||
public sealed class DataSourceValidation
|
||||
{
|
||||
public Func<string> GetSecretStorageIssue { get; init; } = () => string.Empty;
|
||||
|
||||
public Func<string> GetPreviousDataSourceName { get; init; } = () => string.Empty;
|
||||
|
||||
public Func<IEnumerable<string>> GetUsedDataSourceNames { get; init; } = () => [];
|
||||
|
||||
public Func<AuthMethod> GetAuthMethod { get; init; } = () => AuthMethod.NONE;
|
||||
|
||||
public Func<bool> GetSelectedCloudEmbedding { get; init; } = () => false;
|
||||
|
||||
public Func<bool> GetTestedConnection { get; init; } = () => false;
|
||||
|
||||
public Func<bool> GetTestedConnectionResult { get; init; } = () => false;
|
||||
|
||||
public Func<IReadOnlyList<AuthMethod>> GetAvailableAuthMethods { get; init; } = () => [];
|
||||
|
||||
public string? ValidatingHostname(string hostname)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(hostname))
|
||||
return "Please enter a hostname, e.g., http://localhost:1234";
|
||||
|
||||
if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
|
||||
return "The hostname must start with either http:// or https://";
|
||||
|
||||
if(!Uri.TryCreate(hostname, UriKind.Absolute, out _))
|
||||
return "The hostname is not a valid HTTP(S) URL.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatePort(int port)
|
||||
{
|
||||
if(port is < 1 or > 65535)
|
||||
return "The port must be between 1 and 65535.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateUsername(string username)
|
||||
{
|
||||
if(this.GetAuthMethod() is not AuthMethod.USERNAME_PASSWORD)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(username))
|
||||
return "The username must not be empty.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingSecret(string secret)
|
||||
{
|
||||
var authMethod = this.GetAuthMethod();
|
||||
if(authMethod is AuthMethod.NONE or AuthMethod.KERBEROS)
|
||||
return null;
|
||||
|
||||
var secretStorageIssue = this.GetSecretStorageIssue();
|
||||
if(!string.IsNullOrWhiteSpace(secretStorageIssue))
|
||||
return secretStorageIssue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
return authMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Please enter your secure access token.",
|
||||
AuthMethod.USERNAME_PASSWORD => "Please enter your password.",
|
||||
|
||||
_ => "Please enter the secret necessary for authentication."
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingName(string dataSourceName)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(dataSourceName))
|
||||
return "The name must not be empty.";
|
||||
|
||||
if (dataSourceName.Length > 40)
|
||||
return "The name must not exceed 40 characters.";
|
||||
|
||||
var lowerName = dataSourceName.ToLowerInvariant();
|
||||
if(lowerName != this.GetPreviousDataSourceName() && this.GetUsedDataSourceNames().Contains(lowerName))
|
||||
return "The name is already used by another data source. Please choose a different name.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatePath(string path)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(path))
|
||||
return "The path must not be empty. Please select a directory.";
|
||||
|
||||
if(!Directory.Exists(path))
|
||||
return "The path does not exist. Please select a valid directory.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateFilePath(string filePath)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(filePath))
|
||||
return "The file path must not be empty. Please select a file.";
|
||||
|
||||
if(!File.Exists(filePath))
|
||||
return "The file does not exist. Please select a valid file.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateEmbeddingId(string embeddingId)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(embeddingId))
|
||||
return "Please select an embedding provider.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateUserAcknowledgedCloudEmbedding(bool value)
|
||||
{
|
||||
if(this.GetSelectedCloudEmbedding() && !value)
|
||||
return "Please acknowledge that you are aware of the cloud embedding implications.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateTestedConnection()
|
||||
{
|
||||
if(!this.GetTestedConnection())
|
||||
return "Please test the connection before saving.";
|
||||
|
||||
if(!this.GetTestedConnectionResult())
|
||||
return "The connection test failed. Please check the connection settings.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateAuthMethod(AuthMethod authMethod)
|
||||
{
|
||||
if(!this.GetAvailableAuthMethods().Contains(authMethod))
|
||||
return "Please select one valid authentication method.";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -205,6 +205,9 @@
|
||||
"type": "Transitive",
|
||||
"resolved": "0.16.9",
|
||||
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
|
||||
},
|
||||
"ericlientv1": {
|
||||
"type": "Project"
|
||||
}
|
||||
},
|
||||
"net8.0/osx-arm64": {}
|
||||
|
3
app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md
Normal file
3
app/MindWork AI Studio/wwwroot/changelog/v0.9.26.md
Normal file
@ -0,0 +1,3 @@
|
||||
# v0.9.26, build 201 (2025-01-xx xx:xx UTC)
|
||||
- Added the ability to configure local and remote (ERI) data sources in the settings as a preview feature behind the RAG feature flag.
|
||||
- Fixed a bug in the ERI server assistant that allowed an empty directory as a base directory for the code generation.
|
@ -270,4 +270,53 @@ pub struct PreviousDirectory {
|
||||
pub struct DirectorySelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_directory: String,
|
||||
}
|
||||
|
||||
/// Let the user select a file.
|
||||
#[post("/select/file?<title>", data = "<previous_file>")]
|
||||
pub fn select_file(_token: APIToken, title: &str, previous_file: Option<Json<PreviousFile>>) -> Json<FileSelectionResponse> {
|
||||
let file_path = match previous_file {
|
||||
Some(previous) => {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
FileDialogBuilder::new()
|
||||
.set_title(title)
|
||||
.set_directory(previous_path)
|
||||
.pick_file()
|
||||
},
|
||||
|
||||
None => {
|
||||
FileDialogBuilder::new()
|
||||
.set_title(title)
|
||||
.pick_file()
|
||||
},
|
||||
};
|
||||
|
||||
match file_path {
|
||||
Some(path) => {
|
||||
info!("User selected file: {path:?}");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: false,
|
||||
selected_file_path: path.to_str().unwrap().to_string(),
|
||||
})
|
||||
},
|
||||
|
||||
None => {
|
||||
info!("User cancelled file selection.");
|
||||
Json(FileSelectionResponse {
|
||||
user_cancelled: true,
|
||||
selected_file_path: String::from(""),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PreviousFile {
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FileSelectionResponse {
|
||||
user_cancelled: bool,
|
||||
selected_file_path: String,
|
||||
}
|
@ -85,6 +85,7 @@ pub fn start_runtime_api() {
|
||||
crate::app_window::check_for_update,
|
||||
crate::app_window::install_update,
|
||||
crate::app_window::select_directory,
|
||||
crate::app_window::select_file,
|
||||
crate::secret::get_secret,
|
||||
crate::secret::store_secret,
|
||||
crate::secret::delete_secret,
|
||||
|
Loading…
Reference in New Issue
Block a user