mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-05 11:29:06 +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
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
@ -52,7 +52,7 @@ else
|
|||||||
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
|
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
|
||||||
Add ERI server preset
|
Add ERI server preset
|
||||||
</MudButton>
|
</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
|
Delete this server preset
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@ -346,4 +346,4 @@ else
|
|||||||
</MudText>
|
</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" />
|
<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;
|
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)
|
private string GetMultiSelectionAuthText(List<Auth> selectedValues)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
Text="@this.Directory"
|
Text="@this.Directory"
|
||||||
Label="@this.Label"
|
Label="@this.Label"
|
||||||
ReadOnly="@true"
|
ReadOnly="@true"
|
||||||
|
Validation="@this.Validation"
|
||||||
Adornment="Adornment.Start"
|
Adornment="Adornment.Start"
|
||||||
AdornmentIcon="@Icons.Material.Filled.Folder"
|
AdornmentIcon="@Icons.Material.Filled.Folder"
|
||||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||||
|
@ -21,6 +21,9 @@ public partial class SelectDirectory : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string DirectoryDialogTitle { get; set; } = "Select Directory";
|
public string DirectoryDialogTitle { get; set; } = "Select Directory";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<string, string?> Validation { get; set; } = _ => null;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; init; } = null!;
|
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();
|
var modelName = provider.Model.ToString();
|
||||||
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
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()
|
private async Task AddEmbeddingProvider()
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
|
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()
|
private async Task Store()
|
||||||
{
|
{
|
||||||
await this.form.Validate();
|
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:
|
// When the data is not valid, we don't store it:
|
||||||
if (!this.dataIsValid)
|
if (!this.dataIsValid)
|
||||||
|
@ -53,6 +53,10 @@
|
|||||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ERIClientV1\ERIClientV1.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Read the meta data file -->
|
<!-- Read the meta data file -->
|
||||||
<Target Name="ReadMetaData" BeforeTargets="BeforeBuild">
|
<Target Name="ReadMetaData" BeforeTargets="BeforeBuild">
|
||||||
<Error Text="The ../../metadata.txt file was not found!" Condition="!Exists('../../metadata.txt')" />
|
<Error Text="The ../../metadata.txt file was not found!" Condition="!Exists('../../metadata.txt')" />
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||||
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
|
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
|
||||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" />
|
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" />
|
||||||
|
<SettingsPanelDataSources AvailableLLMProvidersFunc="() => this.availableLLMProviders" AvailableEmbeddingsFunc="() => this.availableEmbeddingProviders" @bind-AvailableDataSources="@this.availableDataSources" />
|
||||||
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelChat 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>> availableLLMProviders = new();
|
||||||
private List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
private List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
||||||
|
private List<ConfigurationSelectData<string>> availableDataSources = new();
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@ public sealed class Data
|
|||||||
/// A collection of embedding providers configured.
|
/// A collection of embedding providers configured.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<EmbeddingProvider> EmbeddingProviders { get; init; } = [];
|
public List<EmbeddingProvider> EmbeddingProviders { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of data sources configured.
|
||||||
|
/// </summary>
|
||||||
|
public List<IDataSource> DataSources { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of configured profiles.
|
/// List of configured profiles.
|
||||||
@ -41,6 +46,11 @@ public sealed class Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public uint NextEmbeddingNum { get; set; } = 1;
|
public uint NextEmbeddingNum { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next data source number to use.
|
||||||
|
/// </summary>
|
||||||
|
public uint NextDataSourceNum { get; set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The next profile number to use.
|
/// The next profile number to use.
|
||||||
/// </summary>
|
/// </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.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Tools.Rust;
|
|
||||||
|
|
||||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||||
|
|
||||||
namespace AIStudio.Tools;
|
namespace AIStudio.Tools;
|
||||||
@ -10,7 +8,7 @@ namespace AIStudio.Tools;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calling Rust functions.
|
/// Calling Rust functions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RustService : IDisposable
|
public sealed partial class RustService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
|
|
||||||
@ -60,281 +58,6 @@ public sealed class RustService : IDisposable
|
|||||||
{
|
{
|
||||||
this.encryptor = encryptionService;
|
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
|
#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",
|
"type": "Transitive",
|
||||||
"resolved": "0.16.9",
|
"resolved": "0.16.9",
|
||||||
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
|
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
|
||||||
|
},
|
||||||
|
"ericlientv1": {
|
||||||
|
"type": "Project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net8.0/osx-arm64": {}
|
"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 {
|
pub struct DirectorySelectionResponse {
|
||||||
user_cancelled: bool,
|
user_cancelled: bool,
|
||||||
selected_directory: String,
|
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::check_for_update,
|
||||||
crate::app_window::install_update,
|
crate::app_window::install_update,
|
||||||
crate::app_window::select_directory,
|
crate::app_window::select_directory,
|
||||||
|
crate::app_window::select_file,
|
||||||
crate::secret::get_secret,
|
crate::secret::get_secret,
|
||||||
crate::secret::store_secret,
|
crate::secret::store_secret,
|
||||||
crate::secret::delete_secret,
|
crate::secret::delete_secret,
|
||||||
|
Loading…
Reference in New Issue
Block a user