Add ERI server assistant (#231)

This commit is contained in:
Thorsten Sommer 2025-01-01 15:49:27 +01:00 committed by GitHub
parent cdf717ad5f
commit 8cbef49d89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 3581 additions and 153 deletions

View File

@ -10,6 +10,7 @@ Things we are currently working on:
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~ - [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~ - [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ - [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [ ] App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) - [ ] App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI))
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)

View File

@ -1,5 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>

View File

@ -106,7 +106,7 @@ public partial class AssistantAgenda : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputContent = string.Empty; this.inputContent = string.Empty;
this.contentLines.Clear(); this.contentLines.Clear();

View File

@ -6,7 +6,7 @@
<InnerScrolling HeaderHeight="6em"> <InnerScrolling HeaderHeight="6em">
<ChildContent> <ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" Class="pr-2"> <MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6"> <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description @this.Description
</MudText> </MudText>
@ -35,11 +35,22 @@
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3"> <div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div> </div>
@if (this.ShowResult && this.resultingContentBlock is not null) @if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null)
{ {
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/> <ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
} }
@if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null)
{
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
{
@if (!block.HideFromUser)
{
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
}
}
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3"> <div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div> </div>
</ChildContent> </ChildContent>

View File

@ -4,7 +4,10 @@ using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor.Utilities;
using RustService = AIStudio.Tools.RustService; using RustService = AIStudio.Tools.RustService;
using Timer = System.Timers.Timer;
namespace AIStudio.Assistants; namespace AIStudio.Assistants;
@ -55,7 +58,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
_ => string.Empty, _ => string.Empty,
}; };
protected abstract void ResetFrom(); protected abstract void ResetForm();
protected abstract bool MightPreselectValues(); protected abstract bool MightPreselectValues();
@ -69,6 +72,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected virtual bool ShowResult => true; protected virtual bool ShowResult => true;
protected virtual bool ShowEntireChatThread => false;
protected virtual bool AllowProfiles => true; protected virtual bool AllowProfiles => true;
protected virtual bool ShowProfileSelection => true; protected virtual bool ShowProfileSelection => true;
@ -91,8 +96,10 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected MudForm? form; protected MudForm? form;
protected bool inputIsValid; protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE; protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread; protected ChatThread? chatThread;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
private ContentBlock? resultingContentBlock; private ContentBlock? resultingContentBlock;
private string[] inputIssues = []; private string[] inputIssues = [];
private bool isProcessing; private bool isProcessing;
@ -101,6 +108,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.formChangeTimer.AutoReset = false;
this.formChangeTimer.Elapsed += async (_, _) =>
{
this.formChangeTimer.Stop();
await this.OnFormChange();
};
this.MightPreselectValues(); this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
@ -162,6 +176,34 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return null; return null;
} }
private void TriggerFormChange(FormFieldChangedEventArgs _)
{
this.formChangeTimer.Stop();
this.formChangeTimer.Start();
}
/// <summary>
/// This method is called after any form field has changed.
/// </summary>
/// <remarks>
/// This method is called after a delay of 1.6 seconds. This is to prevent
/// the method from being called too often. This method is called after
/// the user has stopped typing or selecting options.
/// </remarks>
protected virtual Task OnFormChange() => Task.CompletedTask;
/// <summary>
/// Add an issue to the UI.
/// </summary>
/// <param name="issue">The issue to add.</param>
protected void AddInputIssue(string issue)
{
Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1);
this.inputIssues[^1] = issue;
this.inputIsValid = false;
this.StateHasChanged();
}
protected void CreateChatThread() protected void CreateChatThread()
{ {
this.chatThread = new() this.chatThread = new()
@ -221,7 +263,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return time; return time;
} }
protected async Task<string> AddAIResponseAsync(DateTimeOffset time) protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false)
{ {
var aiText = new ContentText var aiText = new ContentText
{ {
@ -236,6 +278,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
Role = ChatRole.AI, Role = ChatRole.AI,
Content = aiText, Content = aiText,
HideFromUser = hideContentFromUser,
}; };
if (this.chatThread is not null) if (this.chatThread is not null)
@ -313,7 +356,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
await this.JsRuntime.ClearDiv(RESULT_DIV_ID); await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID); await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom(); this.ResetForm();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false; this.inputIsValid = false;
@ -341,6 +384,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
public void Dispose() public void Dispose()
{ {
this.MessageBus.Unregister(this); this.MessageBus.Unregister(this);
this.formChangeTimer.Dispose();
} }
#endregion #endregion

View File

@ -1,6 +1,6 @@
using System.Text; using System.Text;
using AIStudio.Components; using AIStudio.Chat;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BiasDay; namespace AIStudio.Assistants.BiasDay;
@ -50,7 +50,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
protected override bool ShowReset => false; protected override bool ShowReset => false;
protected override void ResetFrom() protected override void ResetForm()
{ {
if (!this.MightPreselectValues()) if (!this.MightPreselectValues())
{ {
@ -124,7 +124,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
{ {
var biasChat = new LoadChat var biasChat = new LoadChat
{ {
WorkspaceId = Workspaces.WORKSPACE_ID_BIAS, WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID,
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId, ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
}; };
@ -147,7 +147,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] : BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias); BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias);
var chatId = this.CreateChatThread(Workspaces.WORKSPACE_ID_BIAS, this.biasOfTheDay.Name); var chatId = this.CreateChatThread(KnownWorkspaces.BIAS_WORKSPACE_ID, this.biasOfTheDay.Name);
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id; this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id;
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId; this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId;
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now); this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);

View File

@ -32,7 +32,7 @@ public partial class AssistantCoding : AssistantBaseCore
protected override Func<Task> SubmitAction => this.GetSupport; protected override Func<Task> SubmitAction => this.GetSupport;
protected override void ResetFrom() protected override void ResetForm()
{ {
this.codingContexts.Clear(); this.codingContexts.Clear();
this.compilerMessages = string.Empty; this.compilerMessages = string.Empty;

View File

@ -1,5 +1,5 @@
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3"> <MudStack Row="@true" Class="mb-3">
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense"> <MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var language in Enum.GetValues<CommonCodingLanguages>()) @foreach (var language in Enum.GetValues<CommonCodingLanguages>())
{ {

View File

@ -33,7 +33,7 @@ public partial class AssistantEMail : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputBulletPoints = string.Empty; this.inputBulletPoints = string.Empty;
this.bulletPointsLines.Clear(); this.bulletPointsLines.Clear();

View File

@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum AllowedLLMProviders
{
NONE,
ANY,
SELF_HOSTED,
}

View File

@ -0,0 +1,16 @@
namespace AIStudio.Assistants.ERI;
public static class AllowedLLMProvidersExtensions
{
public static string Description(this AllowedLLMProviders provider)
{
return provider switch
{
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
_ => "Unknown option was selected"
};
}
}

View File

@ -0,0 +1,349 @@
@attribute [Route(Routes.ASSISTANT_ERI)]
@using AIStudio.Settings.DataModel
@using MudExtensions
@inherits AssistantBaseCore
<MudText Typo="Typo.body1" Class="mb-3">
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
all pages using an embedding method. All of Wikipedias data would remain with Wikipedia, including the
vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to
significantly reduce the hallucination of the LLM in knowledge questions.
</MudText>
<MudText Typo="Typo.body1">
<b>Related links:</b>
</MudText>
<MudList T="string" Class="mb-6">
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://github.com/MindWorkAI/ERI">ERI repository with example implementation in .NET and C#</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://mindworkai.org/swagger-ui.html">Interactive documentation aka Swagger UI</MudListItem>
</MudList>
<PreviewPrototype/>
<div class="mb-6"></div>
<MudText Typo="Typo.h4" Class="mb-3">
ERI server presets
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if
you are responsible for multiple ERI servers.
</MudText>
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
{
<MudText Typo="Typo.body1" Class="mb-3">
You have not yet added any ERI server presets.
</MudText>
}
else
{
<MudList Disabled="@this.AreServerPresetsBlocked" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged">
@foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers)
{
<MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server">
@server.ServerName
</MudListItem>
}
</MudList>
}
<MudStack Row="@true" Class="mt-1">
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
Add ERI server preset
</MudButton>
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Primary">
Delete this server preset
</MudButton>
</MudStack>
@if(this.AreServerPresetsBlocked)
{
<MudText Typo="Typo.body1" Class="mb-3 mt-3">
Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings.
</MudText>
}
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
Auto save
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to
regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be
automatically saved. Would you like this?
</MudText>
@if(this.AreServerPresetsBlocked)
{
<MudText Typo="Typo.body1" Class="mb-3">
Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings.
</MudText>
}
<MudTextSwitch Label="Should we automatically save any input made?" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="Yes, please save my inputs" LabelOff="No, I will enter everything again or configure it manually in the settings" />
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Common ERI server settings
</MudText>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="ERI server name" HelperText="Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio." Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="ERI server description" HelperText="Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio." Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Programming language" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
@foreach (var language in Enum.GetValues<ProgrammingLanguages>())
{
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="ERI specification version" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion">
@foreach (var version in Enum.GetValues<ERIVersion>())
{
<MudSelectItem Value="@version">@version</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@(!this.selectedERIVersion.WasSpecificationSelected() || this.IsNoneERIServerSelected)" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> Download specification
</MudButton>
</MudStack>
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Data source settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="Data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged">
@foreach (var dataSource in Enum.GetValues<DataSources>())
{
<MudSelectItem Value="@dataSource">@dataSource.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedDataSource is DataSources.CUSTOM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="Describe your data source" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
@if(this.selectedDataSource > DataSources.FILE_SYSTEM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceProductName" Label="Data source: product name" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
}
@if (this.NeedHostnamePort())
{
<div class="mb-3">
<MudStack Row="@true">
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceHostname" Label="Data source: hostname" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudNumericField Disabled="@this.IsNoneERIServerSelected" Label="Data source: port" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/>
</MudStack>
@if (this.dataSourcePort < 1024)
{
<MudText Typo="Typo.body2">
<b>Warning:</b> Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
</MudText>
}
</div>
}
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Authentication settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-1">
<MudSelectExtended
T="Auth"
Disabled="@this.IsNoneERIServerSelected"
ShrinkLabel="@true"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionAuthText"
SelectedValues="@this.selectedAuthenticationMethods"
Validation="@this.ValidateAuthenticationMethods"
SelectedValuesChanged="@this.AuthenticationMethodWasChanged"
Label="Authentication method(s)"
Variant="Variant.Outlined"
Margin="Margin.Dense">
@foreach (var authMethod in Enum.GetValues<Auth>())
{
<MudSelectItemExtended Value="@authMethod">@authMethod.Name()</MudSelectItemExtended>
}
</MudSelectExtended>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</MudStack>
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
{
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="Operating system on which your ERI will run" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1">
@foreach (var os in Enum.GetValues<OperatingSystem>())
{
<MudSelectItem Value="@os">@os.Name()</MudSelectItem>
}
</MudSelect>
}
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Data protection settings
</MudText>
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="Allowed LLM providers for this data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1">
@foreach (var option in Enum.GetValues<AllowedLLMProviders>())
{
<MudSelectItem Value="@option">@option.Description()</MudSelectItem>
}
</MudSelect>
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Embedding settings
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector
space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't
have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You
store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't
need embeddings.
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries
or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
</MudText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.embeddings" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Type</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.EmbeddingName</MudTd>
<MudTd>@context.EmbeddingType</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.embeddings.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No embedding methods configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding">
Add Embedding Method
</MudButton>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Data retrieval settings
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process.
You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows
you to test with beta users which process works better. Or you might generally want to give users the choice so they can select
the process that best suits their circumstances.
</MudText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.retrievalProcesses" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditRetrievalProcess(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.retrievalProcesses.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No retrieval process configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess">
Add Retrieval Process
</MudButton>
<MudText Typo="Typo.body1" Class="mb-1">
You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text
mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here.
It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these
libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe
the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
</MudText>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="(Optional) Additional libraries" HelperText="Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them." Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Provider selection for generation
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task.
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
<b>Important:</b> The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number
of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.
<b>However, generating all the files takes a certain amount of time.</b> Local or self-hosted models may work without these limitations
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
</MudText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Write code to file system
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created
outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the
AI has made in which files.
</MudText>
<MudText Typo="Typo.body1" Class="mb-2">
When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All
other files you have created remain. Then, the AI generates the new files. <b>But beware:</b> It may happen that the AI generates a
file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore,
you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization,
you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.
</MudText>
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server"/>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum Auth
{
NONE,
KERBEROS,
USERNAME_PASSWORD,
TOKEN,
}

View File

@ -0,0 +1,26 @@
namespace AIStudio.Assistants.ERI;
public static class AuthExtensions
{
public static string Name(this Auth auth) => auth switch
{
Auth.NONE => "No login necessary: useful for public data sources",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users",
Auth.USERNAME_PASSWORD => "Login by username and password: simple to implement and to operate, useful for few users; easy to use for users",
Auth.TOKEN => "Login by token: simple to implement and to operate, useful for few users; unusual for many users",
_ => "Unknown login method"
};
public static string ToPrompt(this Auth auth) => auth switch
{
Auth.NONE => "No login is necessary, the data source is public.",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.",
Auth.USERNAME_PASSWORD => "Login by username and password.",
Auth.TOKEN => "Login by static token per user.",
_ => string.Empty,
};
}

View File

@ -0,0 +1,15 @@
namespace AIStudio.Assistants.ERI;
public enum DataSources
{
NONE,
CUSTOM,
FILE_SYSTEM,
OBJECT_STORAGE,
KEY_VALUE_STORE,
DOCUMENT_STORE,
RELATIONAL_DATABASE,
GRAPH_DATABASE,
}

View File

@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
public static class DataSourcesExtensions
{
public static string Name(this DataSources dataSource) => dataSource switch
{
DataSources.NONE => "No data source selected",
DataSources.CUSTOM => "Custom description",
DataSources.FILE_SYSTEM => "File system (local or network share)",
DataSources.OBJECT_STORAGE => "Object storage, like Amazon S3, MinIO, etc.",
DataSources.KEY_VALUE_STORE => "Key-Value store, like Redis, etc.",
DataSources.DOCUMENT_STORE => "Document store, like MongoDB, etc.",
DataSources.RELATIONAL_DATABASE => "Relational database, like MySQL, PostgreSQL, etc.",
DataSources.GRAPH_DATABASE => "Graph database, like Neo4j, ArangoDB, etc.",
_ => "Unknown data source"
};
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Assistants.ERI;
public enum ERIVersion
{
NONE,
V1,
}

View File

@ -0,0 +1,27 @@
namespace AIStudio.Assistants.ERI;
public static class ERIVersionExtensions
{
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
{
try
{
var url = version.SpecificationURL();
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
catch
{
return string.Empty;
}
}
public static string SpecificationURL(this ERIVersion version)
{
var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
}

View File

@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Represents information about the used embedding for a data source.
/// </summary>
/// <param name="EmbeddingType">What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
/// Embedding," "Graph Embedding," etc.</param>
/// <param name="EmbeddingName">Name the embedding used. This can be a library, a framework, or the name of the used
/// algorithm.</param>
/// <param name="Description">A short description of the embedding. Describe what the embedding is doing.</param>
/// <param name="UsedWhen">Describe when the embedding is used. For example, when the user prompt contains certain
/// keywords, or anytime?</param>
/// <param name="Link">A link to the embedding's documentation or the source code. Might be null.</param>
public readonly record struct EmbeddingInfo(
string EmbeddingType,
string EmbeddingName,
string Description,
string UsedWhen,
string? Link);

View File

@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum OperatingSystem
{
NONE,
WINDOWS,
LINUX,
}

View File

@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public static class OperatingSystemExtensions
{
public static string Name(this OperatingSystem os) => os switch
{
OperatingSystem.NONE => "No operating system specified",
OperatingSystem.WINDOWS => "Windows",
OperatingSystem.LINUX => "Linux",
_ => "Unknown operating system"
};
}

View File

@ -0,0 +1,20 @@
namespace AIStudio.Assistants.ERI;
public enum ProgrammingLanguages
{
NONE,
C,
CPP,
CSHARP,
GO,
JAVA,
JAVASCRIPT,
JULIA,
MATLAB,
PHP,
PYTHON,
RUST,
OTHER,
}

View File

@ -0,0 +1,24 @@
namespace AIStudio.Assistants.ERI;
public static class ProgrammingLanguagesExtensions
{
public static string Name(this ProgrammingLanguages language) => language switch
{
ProgrammingLanguages.NONE => "No programming language selected",
ProgrammingLanguages.C => "C",
ProgrammingLanguages.CPP => "C++",
ProgrammingLanguages.CSHARP => "C#",
ProgrammingLanguages.GO => "Go",
ProgrammingLanguages.JAVA => "Java",
ProgrammingLanguages.JAVASCRIPT => "JavaScript",
ProgrammingLanguages.JULIA => "Julia",
ProgrammingLanguages.MATLAB => "MATLAB",
ProgrammingLanguages.PHP => "PHP",
ProgrammingLanguages.PYTHON => "Python",
ProgrammingLanguages.RUST => "Rust",
ProgrammingLanguages.OTHER => "Other",
_ => "Unknown"
};
}

View File

@ -0,0 +1,18 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Information about a retrieval process, which this data source implements.
/// </summary>
/// <param name="Name">The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".</param>
/// <param name="Description">A short description of the retrieval process. What kind of retrieval process is it?</param>
/// <param name="Link">A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.</param>
/// <param name="ParametersDescription">A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.</param>
/// <param name="Embeddings">A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.</param>
public readonly record struct RetrievalInfo(
string Name,
string Description,
string? Link,
Dictionary<string, string>? ParametersDescription,
List<EmbeddingInfo>? Embeddings);

View File

@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public sealed class RetrievalParameter
{
/// <summary>
/// The name of the parameter.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the parameter.
/// </summary>
public string Description { get; set; } = string.Empty;
}

View File

@ -48,7 +48,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
this.correctedText = string.Empty; this.correctedText = string.Empty;

View File

@ -33,7 +33,7 @@ public partial class AssistantIconFinder : AssistantBaseCore
protected override Func<Task> SubmitAction => this.FindIcon; protected override Func<Task> SubmitAction => this.FindIcon;
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputContext = string.Empty; this.inputContext = string.Empty;
if (!this.MightPreselectValues()) if (!this.MightPreselectValues())

View File

@ -59,7 +59,7 @@ public partial class AssistantJobPostings : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputEntryDate = string.Empty; this.inputEntryDate = string.Empty;
this.inputValidUntil = string.Empty; this.inputValidUntil = string.Empty;

View File

@ -37,7 +37,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputLegalDocument = string.Empty; this.inputLegalDocument = string.Empty;
this.inputQuestions = string.Empty; this.inputQuestions = string.Empty;

View File

@ -41,7 +41,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
if (!this.MightPreselectValues()) if (!this.MightPreselectValues())

View File

@ -49,7 +49,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
this.rewrittenText = string.Empty; this.rewrittenText = string.Empty;

View File

@ -60,7 +60,7 @@ public partial class AssistantSynonyms : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
this.inputContext = string.Empty; this.inputContext = string.Empty;

View File

@ -40,7 +40,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
if(!this.MightPreselectValues()) if(!this.MightPreselectValues())

View File

@ -36,7 +36,7 @@ public partial class AssistantTranslation : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
}; };
protected override void ResetFrom() protected override void ResetForm()
{ {
this.inputText = string.Empty; this.inputText = string.Empty;
this.inputTextLastTranslation = string.Empty; this.inputTextLastTranslation = string.Empty;

View File

@ -0,0 +1,7 @@
namespace AIStudio.Chat;
public static class KnownWorkspaces
{
public static readonly Guid BIAS_WORKSPACE_ID = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
public static readonly Guid ERI_SERVER_WORKSPACE_ID = Guid.Parse("8ec09cd3-9da7-4736-b245-2d8b67fc342f");
}

View File

@ -1,7 +1,7 @@
@typeparam T @typeparam T
@inherits EnumSelectionBase @inherits EnumSelectionBase
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3"> <MudStack Row="@true" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection"> <MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection">
@foreach (var value in Enum.GetValues<T>()) @foreach (var value in Enum.GetValues<T>())
{ {

View File

@ -0,0 +1,16 @@
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.Directory"
Label="@this.Label"
ReadOnly="@true"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenDirectoryDialog">
Choose Directory
</MudButton>
</MudStack>

View File

@ -0,0 +1,60 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class SelectDirectory : ComponentBase
{
[Parameter]
public string Directory { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> DirectoryChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string DirectoryDialogTitle { get; set; } = "Select Directory";
[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 InternalDirectoryChanged(string directory)
{
this.Directory = directory;
this.DirectoryChanged.InvokeAsync(directory);
}
private async Task OpenDirectoryDialog()
{
var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory);
this.Logger.LogInformation($"The user selected the directory '{response.SelectedDirectory}'.");
if (!response.UserCancelled)
this.InternalDirectoryChanged(response.SelectedDirectory);
}
}

View File

@ -39,8 +39,6 @@ public partial class Workspaces : ComponentBase
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
public static readonly Guid WORKSPACE_ID_BIAS = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
private readonly List<TreeItemData<ITreeItem>> treeItems = new(); private readonly List<TreeItemData<ITreeItem>> treeItems = new();
#region Overrides of ComponentBase #region Overrides of ComponentBase
@ -53,8 +51,6 @@ public partial class Workspaces : ComponentBase
// - Those initial tree items cannot have children // - Those initial tree items cannot have children
// - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item // - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item
// //
await this.EnsureBiasWorkspace();
await this.LoadTreeItems(); await this.LoadTreeItems();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -408,18 +404,6 @@ public partial class Workspaces : ComponentBase
await this.LoadTreeItems(); await this.LoadTreeItems();
} }
private async Task EnsureBiasWorkspace()
{
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", WORKSPACE_ID_BIAS.ToString());
if(Path.Exists(workspacePath))
return;
Directory.CreateDirectory(workspacePath);
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, "Bias of the Day", Encoding.UTF8);
}
private async Task DeleteWorkspace(string? workspacePath) private async Task DeleteWorkspace(string? workspacePath)
{ {
if(workspacePath is null) if(workspacePath is null)

View File

@ -0,0 +1,109 @@
<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.DataEmbeddingName"
Label="Embedding Name"
HelperText="The name of the embedding method."
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Label"
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Counter="26"
MaxLength="26"
Immediate="@true"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataEmbeddingType"
Label="Embedding Type"
HelperText="What kind of embedding is used. For example, Transformer Embedding, Contextual Word Embedding, Graph Embedding, etc."
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Extension"
AdornmentColor="Color.Info"
Counter="56"
MaxLength="56"
Validation="@this.ValidateType"
Immediate="@true"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudList T="@string" Class="mb-3">
<MudListItem Icon="@Icons.Material.Filled.Link" Href="https://en.wikipedia.org/wiki/Word_embedding" Target="_blank" Text="See Wikipedia for more information about word embeddings"/>
<MudListItem Icon="@Icons.Material.Filled.Link" Href="https://en.wikipedia.org/wiki/Knowledge_graph_embedding" Target="_blank" Text="See Wikipedia for more information about knowledge graph embeddings"/>
</MudList>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataDescription"
Label="Embedding Description"
HelperText="A short description of the embedding method."
Lines="3"
AutoGrow="@true"
MaxLines="6"
Immediate="@true"
Variant="Variant.Outlined"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Extension"
AdornmentColor="Color.Info"
Validation="@this.ValidateDescription"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataUsedWhen"
Label="Used when"
HelperText="When is this embedding used? When you define multiple embeddings, it is helpful to know when to use which one."
Lines="3"
AutoGrow="@true"
MaxLines="6"
Immediate="@true"
Variant="Variant.Outlined"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Assessment"
AdornmentColor="Color.Info"
Validation="@this.ValidateUsedWhen"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataLink"
Label="Embedding Link"
HelperText="A link to the embedding, e.g., to the model, the source code, the paper, it's Wikipedia page, etc."
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Link"
AdornmentColor="Color.Info"
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>

View File

@ -0,0 +1,144 @@
using AIStudio.Assistants.ERI;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class EmbeddingMethodDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The user chosen embedding name.
/// </summary>
[Parameter]
public string DataEmbeddingName { get; set; } = string.Empty;
/// <summary>
/// The user chosen embedding type.
/// </summary>
[Parameter]
public string DataEmbeddingType { get; set; } = string.Empty;
/// <summary>
/// The embedding description.
/// </summary>
[Parameter]
public string DataDescription { get; set; } = string.Empty;
/// <summary>
/// When is the embedding used?
/// </summary>
[Parameter]
public string DataUsedWhen { get; set; } = string.Empty;
/// <summary>
/// A link to the embedding documentation or the source code. Might be null, which means no link is provided.
/// </summary>
[Parameter]
public string DataLink { get; set; } = string.Empty;
/// <summary>
/// The embedding method names that are already used. The user must choose a unique name.
/// </summary>
[Parameter]
public IReadOnlyList<string> UsedEmbeddingMethodNames { get; set; } = new List<string>();
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
private bool dataIsValid;
private string[] dataIssues = [];
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
private EmbeddingInfo CreateEmbeddingInfo() => new(this.DataEmbeddingType, this.DataEmbeddingName, this.DataDescription, this.DataUsedWhen, this.DataLink);
#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();
}
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 string? ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "The embedding name must not be empty. Please name the embedding.";
if (name.Length > 26)
return "The embedding name must not be longer than 26 characters.";
if (this.UsedEmbeddingMethodNames.Contains(name))
return $"The embedding method name '{name}' is already used. Please choose a unique name.";
return null;
}
private string? ValidateType(string type)
{
if (string.IsNullOrWhiteSpace(type))
return "The embedding type must not be empty. Please specify the embedding type.";
if (type.Length > 56)
return "The embedding type must not be longer than 56 characters.";
return null;
}
private string? ValidateDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))
return "The description must not be empty. Please describe the embedding method.";
return null;
}
private string? ValidateUsedWhen(string usedWhen)
{
if (string.IsNullOrWhiteSpace(usedWhen))
return "Please describe when the embedding is used. Might be anytime or when certain keywords are present, etc.";
return null;
}
private async Task Store()
{
await this.form.Validate();
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
var embeddingInfo = this.CreateEmbeddingInfo();
this.MudDialog.Close(DialogResult.Ok(embeddingInfo));
}
private void Cancel() => this.MudDialog.Cancel();
}

View File

@ -8,7 +8,7 @@ using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Dialogs; namespace AIStudio.Dialogs;
public partial class EmbeddingDialog : ComponentBase, ISecretId public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
{ {
[CascadingParameter] [CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!; private MudDialogInstance MudDialog { get; set; } = null!;
@ -97,7 +97,7 @@ public partial class EmbeddingDialog : ComponentBase, ISecretId
private readonly Encryption encryption = Program.ENCRYPTION; private readonly Encryption encryption = Program.ENCRYPTION;
private readonly ProviderValidation providerValidation; private readonly ProviderValidation providerValidation;
public EmbeddingDialog() public EmbeddingProviderDialog()
{ {
this.providerValidation = new() this.providerValidation = new()
{ {

View File

@ -0,0 +1,213 @@
@using AIStudio.Assistants.ERI
@using MudExtensions
<MudDialog>
<DialogContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
<MudText Typo="Typo.h5" Class="mb-3">
General Information
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
Please provide some general information about your retrieval process first. This data may be
displayed to the users.
</MudText>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataName"
Label="Retrieval Process Name"
HelperText="The name of your retrieval process."
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Label"
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Counter="26"
MaxLength="26"
Immediate="@true"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataDescription"
Label="Retrieval Process Description"
HelperText="A short description of the retrieval process."
Lines="3"
AutoGrow="@true"
MaxLines="6"
Immediate="@true"
Variant="Variant.Outlined"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Extension"
AdornmentColor="Color.Info"
Validation="@this.ValidateDescription"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataLink"
Label="Retrieval Process Link"
HelperText="A link to the retrieval process, e.g., the source code, the paper, it's Wikipedia page, etc. Make sense for common retrieval processes. Leave empty if not applicable."
Class="mb-6"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Link"
AdornmentColor="Color.Info"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudText Typo="Typo.h5" Class="mb-3">
Retrieval Process Parameters
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
You may want to parameterize your retrieval process. However, this is optional. You can specify any
parameters that can be set by the user or the system during the call. Nevertheless, you should use
sensible default values in your code so that users are not forced to set the parameters manually.
</MudText>
<MudStack Row="@true" Spacing="6" AlignItems="AlignItems.Start" StretchItems="StretchItems.None">
@* The left side of the stack is another stack to show the list *@
<MudStack Row="@false" AlignItems="AlignItems.Start" StretchItems="StretchItems.None">
@if (this.retrievalParameters.Count > 0)
{
<MudList T="RetrievalParameter" Class="mb-1" @bind-SelectedValue="@this.selectedParameter">
@foreach (var parameter in this.retrievalParameters)
{
<MudListItem T="RetrievalParameter" Icon="@Icons.Material.Filled.Tune" Value="@parameter">
@parameter.Name
</MudListItem>
}
</MudList>
}
<MudButton OnClick="@this.AddRetrievalProcessParameter" Variant="Variant.Filled" Color="Color.Primary" Class="mt-1">
Add Parameter
</MudButton>
</MudStack>
@* The right side of the stack is another stack to display the parameter's data *@
<MudStack Row="@false" AlignItems="AlignItems.Stretch" StretchItems="StretchItems.End" Class="pa-3 mb-8 border-solid border rounded-lg">
@if (this.selectedParameter is null)
{
@if(this.retrievalParameters.Count == 0)
{
<MudText>
Add a parameter first, then select it to edit.
</MudText>
}
else
{
<MudText>
Select a parameter to show and edit it.
</MudText>
}
}
else
{
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.selectedParameter.Name"
Label="Parameter Name"
HelperText="The parameter name. It must be unique within the retrieval process."
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Label"
AdornmentColor="Color.Info"
Counter="26"
MaxLength="26"
Validation="@this.ValidateParameterName"
Immediate="@true"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.selectedParameter.Description"
Label="Parameter Description"
HelperText="A short description of the parameter. What data type is it? What is it used for? What are the possible values?"
Lines="3"
AutoGrow="@true"
MaxLines="6"
Immediate="@true"
Variant="Variant.Outlined"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Extension"
AdornmentColor="Color.Info"
Validation="@this.ValidateParameterDescription"
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
<MudStack Row="@true">
<MudButton OnClick="@this.RemoveRetrievalProcessParameter" Variant="Variant.Filled" Color="Color.Secondary">
Delete this parameter
</MudButton>
</MudStack>
}
</MudStack>
</MudStack>
<MudText Typo="Typo.h5" Class="mb-3">
Embeddings
</MudText>
@if(this.AvailableEmbeddings.Count == 0)
{
<MudText Typo="Typo.body1" Class="mb-3">
Currently, you have not defined any embedding methods. If your retrieval process does not require embedding, you can ignore this part.
Otherwise, you can define one or more embedding methods in the previous view to assign them to your retrieval process here.
</MudText>
}
else
{
<MudText Typo="Typo.body1" Class="mb-3">
Here you can select which embedding methods are used for this retrieval process. Embeddings are optional;
if your retrieval process works without embedding, you can ignore this part. You can only choose the embedding
methods you have previously defined.
</MudText>
<MudSelectExtended
T="EmbeddingInfo"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionText"
SelectedValues="@this.DataEmbeddings"
SelectedValuesChanged="@this.EmbeddingsChanged"
Strict="@true"
Margin="Margin.Dense"
Label="Embeddings methods"
ShrinkLabel="@true"
Class="mb-3"
Variant="Variant.Outlined"
HelperText="Optional. Select the embedding methods that are used for this retrieval process.">
@foreach (var embedding in this.AvailableEmbeddings)
{
<MudSelectItemExtended Value="@embedding">
@embedding.EmbeddingName
</MudSelectItemExtended>
}
</MudSelectExtended>
}
</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>

View File

@ -0,0 +1,213 @@
using AIStudio.Assistants.ERI;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class RetrievalProcessDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The user chosen retrieval process name.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// The retrieval process description.
/// </summary>
[Parameter]
public string DataDescription { get; set; } = string.Empty;
/// <summary>
/// A link to the retrieval process documentation, paper, Wikipedia article, or the source code.
/// </summary>
[Parameter]
public string DataLink { get; set; } = string.Empty;
/// <summary>
/// A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
/// and the value is a description of the parameter. Although each parameter will be sent as a string,
/// the description should indicate the expected type and range, e.g., 0.0 to 1.0 for a float parameter.
/// </summary>
[Parameter]
public Dictionary<string, string> DataParametersDescription { get; set; } = new();
/// <summary>
/// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.
/// </summary>
[Parameter]
public HashSet<EmbeddingInfo> DataEmbeddings { get; set; } = new();
/// <summary>
/// The available embeddings for the user to choose from.
/// </summary>
[Parameter]
public IReadOnlyList<EmbeddingInfo> AvailableEmbeddings { get; set; } = new List<EmbeddingInfo>();
/// <summary>
/// The retrieval process names that are already used. The user must choose a unique name.
/// </summary>
[Parameter]
public IReadOnlyList<string> UsedRetrievalProcessNames { get; set; } = new List<string>();
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
private bool dataIsValid;
private string[] dataIssues = [];
private List<RetrievalParameter> retrievalParameters = new();
private RetrievalParameter? selectedParameter;
private uint nextParameterId = 1;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
private RetrievalInfo CreateRetrievalInfo() => new(this.DataName, this.DataDescription, this.DataLink, this.retrievalParameters.ToDictionary(parameter => parameter.Name, parameter => parameter.Description), this.DataEmbeddings.ToList());
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Convert the parameters:
this.retrievalParameters = this.DataParametersDescription.Select(pair => new RetrievalParameter { Name = pair.Key, Description = pair.Value }).ToList();
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 string? ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "The retrieval process name must not be empty. Please name your retrieval process.";
if (name.Length > 26)
return "The retrieval process name must not be longer than 26 characters.";
if (this.UsedRetrievalProcessNames.Contains(name))
return $"The retrieval process name '{name}' must be unique. Please choose a different name.";
return null;
}
private string? ValidateDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))
return "The description must not be empty. Please describe the retrieval process.";
return null;
}
private void AddRetrievalProcessParameter()
{
this.retrievalParameters.Add(new() { Name = $"New Parameter {this.nextParameterId++}", Description = string.Empty });
}
private string? ValidateParameterName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "The parameter name must not be empty. Please name the parameter.";
if(name.Length > 26)
return "The parameter name must not be longer than 26 characters.";
if (this.retrievalParameters.Count(parameter => parameter.Name == name) > 1)
return $"The parameter name '{name}' must be unique. Please choose a different name.";
return null;
}
private string? ValidateParameterDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))
return $"The parameter description must not be empty. Please describe the parameter '{this.selectedParameter?.Name}'. What data type is it? What is it used for? What are the possible values?";
return null;
}
private string? ValidateParameter(RetrievalParameter parameter)
{
if(this.ValidateParameterName(parameter.Name) is { } nameIssue)
return nameIssue;
if (string.IsNullOrWhiteSpace(parameter.Description))
return $"The parameter description must not be empty. Please describe the parameter '{parameter.Name}'. What data type is it? What is it used for? What are the possible values?";
return null;
}
private void RemoveRetrievalProcessParameter()
{
if (this.selectedParameter is not null)
this.retrievalParameters.Remove(this.selectedParameter);
this.selectedParameter = null;
}
private string GetMultiSelectionText(List<EmbeddingInfo> selectedEmbeddings)
{
if(selectedEmbeddings.Count == 0)
return "No embedding methods selected.";
if(selectedEmbeddings.Count == 1)
return "You have selected 1 embedding method.";
return $"You have selected {selectedEmbeddings.Count} embedding methods.";
}
private void EmbeddingsChanged(IEnumerable<EmbeddingInfo>? updatedEmbeddings)
{
if(updatedEmbeddings is null)
this.DataEmbeddings = new();
else
this.DataEmbeddings = updatedEmbeddings.ToHashSet();
}
private async Task Store()
{
await this.form.Validate();
foreach (var parameter in this.retrievalParameters)
{
if (this.ValidateParameter(parameter) is { } issue)
{
this.dataIsValid = false;
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
this.dataIssues[^1] = issue;
}
}
// When the data is not valid, we don't store it:
if (!this.dataIsValid || this.dataIssues.Any())
return;
var retrievalInfo = this.CreateRetrievalInfo();
this.MudDialog.Close(DialogResult.Ok(retrievalInfo));
}
private void Cancel() => this.MudDialog.Cancel();
}

View File

@ -1,3 +1,4 @@
@using AIStudio.Settings.DataModel
@attribute [Route(Routes.ASSISTANTS)] @attribute [Route(Routes.ASSISTANTS)]
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <MudText Typo="Typo.h3" Class="mb-2 mr-3">
@ -41,6 +42,10 @@
</MudText> </MudText>
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3"> <MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
<AssistantBlock Name="Coding" Description="Get coding and debugging support from a LLM." Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/> <AssistantBlock Name="Coding" Description="Get coding and debugging support from a LLM." Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
<AssistantBlock Name="ERI Server" Description="Generate an ERI server to integrate business systems." Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
}
</MudStack> </MudStack>
</InnerScrolling> </InnerScrolling>

View File

@ -1,5 +1,11 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages; namespace AIStudio.Pages;
public partial class Assistants : ComponentBase; public partial class Assistants : ComponentBase
{
[Inject]
public SettingsManager SettingsManager { get; set; } = null!;
}

View File

@ -85,6 +85,13 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
{ {
this.autoSaveEnabled = true; this.autoSaveEnabled = true;
this.mustStoreChat = true; this.mustStoreChat = true;
// Ensure the workspace exists:
if(this.chatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureERIServerWorkspace();
else if (this.chatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureBiasWorkspace();
} }
} }

View File

@ -333,6 +333,23 @@
</MudPaper> </MudPaper>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.PrivateConnectivity" HeaderText="Assistant: ERI Server Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect ERI server options?" LabelOn="ERI server options are preselected" LabelOff="No ERI server options are preselected" State="@(() => this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.ERI.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect some ERI server options."/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.ERI.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ERI.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.ERI.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ERI.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
<MudText Typo="Typo.body1" Class="mb-3">
Most ERI server options can be customized and saved directly in the ERI server assistant.
For this, the ERI server assistant has an auto-save function.
</MudText>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.PrivateConnectivity" Color="Color.Default" Href="@Routes.ASSISTANT_ERI">
Switch to ERI server assistant
</MudButton>
</MudPaper>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextSnippet" HeaderText="Assistant: Text Summarizer Options"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.TextSnippet" HeaderText="Assistant: Text Summarizer Options">
<ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/> <ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg"> <MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">

View File

@ -183,12 +183,12 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
private async Task AddEmbeddingProvider() private async Task AddEmbeddingProvider()
{ {
var dialogParameters = new DialogParameters<EmbeddingDialog> var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
{ {
{ x => x.IsEditing, false }, { x => x.IsEditing, false },
}; };
var dialogReference = await this.DialogService.ShowAsync<EmbeddingDialog>("Add Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<EmbeddingProviderDialog>("Add Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;
@ -205,7 +205,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider) private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider)
{ {
var dialogParameters = new DialogParameters<EmbeddingDialog> var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
{ {
{ x => x.DataNum, embeddingProvider.Num }, { x => x.DataNum, embeddingProvider.Num },
{ x => x.DataId, embeddingProvider.Id }, { x => x.DataId, embeddingProvider.Id },
@ -218,7 +218,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{ x => x.DataHost, embeddingProvider.Host }, { x => x.DataHost, embeddingProvider.Host },
}; };
var dialogReference = await this.DialogService.ShowAsync<EmbeddingDialog>("Edit Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await this.DialogService.ShowAsync<EmbeddingProviderDialog>("Edit Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result; var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled)
return; return;

View File

@ -59,6 +59,8 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
Stream = true, Stream = true,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "messages"); var request = new HttpRequestMessage(HttpMethod.Post, "messages");
@ -70,14 +72,19 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
// Set the content: // Set the content:
request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Anthropic chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var stream = await response.Content.ReadAsStreamAsync(token); var stream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(stream); var streamReader = new StreamReader(stream);

View File

@ -74,4 +74,47 @@ public abstract class BaseProvider : IProvider, ISecretId
public string SecretName => this.InstanceName; public string SecretName => this.InstanceName;
#endregion #endregion
/// <summary>
/// Sends a request and handles rate limiting by exponential backoff.
/// </summary>
/// <param name="requestBuilder">A function that builds the request.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The status object of the request.</returns>
protected async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken token = default)
{
const int MAX_RETRIES = 6;
const double RETRY_DELAY_SECONDS = 4;
var retry = 0;
var response = default(HttpResponseMessage);
var errorMessage = string.Empty;
while (retry++ < MAX_RETRIES)
{
using var request = await requestBuilder();
// Send the request with the ResponseHeadersRead option.
// This allows us to read the stream as soon as the headers are received.
// This is important because we want to stream the responses.
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
if (nextResponse.IsSuccessStatusCode)
{
response = nextResponse;
break;
}
errorMessage = nextResponse.ReasonPhrase;
var timeSeconds = Math.Pow(RETRY_DELAY_SECONDS, retry + 1);
if(timeSeconds > 90)
timeSeconds = 90;
this.logger.LogDebug($"Failed request with status code {nextResponse.StatusCode} (message = '{errorMessage}'). Retrying in {timeSeconds:0.00} seconds.");
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token);
}
if(retry >= MAX_RETRIES)
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
return new HttpRateLimitedStreamResult(true, false, string.Empty, response);
}
} }

View File

@ -68,6 +68,8 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
Stream = true, Stream = true,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
@ -76,14 +78,19 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
// Set the content: // Set the content:
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Fireworks chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var fireworksStream = await response.Content.ReadAsStreamAsync(token); var fireworksStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(fireworksStream); var streamReader = new StreamReader(fireworksStream);

View File

@ -69,6 +69,8 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
Stream = true, Stream = true,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
@ -77,14 +79,19 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
// Set the content: // Set the content:
request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Google chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var geminiStream = await response.Content.ReadAsStreamAsync(token); var geminiStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(geminiStream); var streamReader = new StreamReader(geminiStream);

View File

@ -71,6 +71,8 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
Stream = true, Stream = true,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
@ -79,14 +81,19 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
// Set the content: // Set the content:
request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Groq chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var groqStream = await response.Content.ReadAsStreamAsync(token); var groqStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(groqStream); var streamReader = new StreamReader(groqStream);

View File

@ -70,6 +70,8 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
SafePrompt = false, SafePrompt = false,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
@ -78,14 +80,19 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
// Set the content: // Set the content:
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Mistral chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var mistralStream = await response.Content.ReadAsStreamAsync(token); var mistralStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(mistralStream); var streamReader = new StreamReader(mistralStream);

View File

@ -74,6 +74,8 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
FrequencyPenalty = 0f, FrequencyPenalty = 0f,
}, JSON_SERIALIZER_OPTIONS); }, JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
@ -82,14 +84,19 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
// Set the content: // Set the content:
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"OpenAI chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var openAIStream = await response.Content.ReadAsStreamAsync(token); var openAIStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
var streamReader = new StreamReader(openAIStream); var streamReader = new StreamReader(openAIStream);

View File

@ -68,6 +68,8 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
StreamReader? streamReader = default; StreamReader? streamReader = default;
try try
{
async Task<HttpRequestMessage> RequestBuilder()
{ {
// Build the HTTP post request: // Build the HTTP post request:
var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL()); var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL());
@ -78,14 +80,19 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
// Set the content: // Set the content:
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
return request;
}
// Send the request with the ResponseHeadersRead option. // Send the request using exponential backoff:
// This allows us to read the stream as soon as the headers are received. using var responseData = await this.SendRequest(RequestBuilder, token);
// This is important because we want to stream the responses. if(responseData.IsFailedAfterAllRetries)
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); {
this.logger.LogError($"Self-hosted provider's chat completion failed: {responseData.ErrorMessage}");
yield break;
}
// Open the response stream: // Open the response stream:
var providerStream = await response.Content.ReadAsStreamAsync(token); var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
// Add a stream reader to read the stream, line by line: // Add a stream reader to read the stream, line by line:
streamReader = new StreamReader(providerStream); streamReader = new StreamReader(providerStream);

View File

@ -24,5 +24,6 @@ public sealed partial class Routes
public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks"; public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks";
public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting"; public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting";
public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day"; public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day";
public const string ASSISTANT_ERI = "/assistant/eri";
// ReSharper restore InconsistentNaming // ReSharper restore InconsistentNaming
} }

View File

@ -58,6 +58,8 @@ public sealed class Data
public DataCoding Coding { get; init; } = new(); public DataCoding Coding { get; init; } = new();
public DataERI ERI { get; init; } = new();
public DataTextSummarizer TextSummarizer { get; init; } = new(); public DataTextSummarizer TextSummarizer { get; init; } = new();
public DataTextContentCleaner TextContentCleaner { get; init; } = new(); public DataTextContentCleaner TextContentCleaner { get; init; } = new();

View File

@ -0,0 +1,36 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataERI
{
/// <summary>
/// Should we automatically save any input made in the ERI assistant?
/// </summary>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>
/// Preselect any ERI options?
/// </summary>
public bool PreselectOptions { get; set; } = true;
/// <summary>
/// Data for the ERI servers.
/// </summary>
public List<DataERIServer> ERIServers { get; set; } = new();
/// <summary>
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
/// <summary>
/// Which coding provider should be preselected?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}

View File

@ -0,0 +1,113 @@
using AIStudio.Assistants.ERI;
using OperatingSystem = AIStudio.Assistants.ERI.OperatingSystem;
namespace AIStudio.Settings.DataModel;
public sealed class DataERIServer
{
/// <summary>
/// Preselect the server name?
/// </summary>
public string ServerName { get; set; } = string.Empty;
/// <summary>
/// Preselect the server description?
/// </summary>
public string ServerDescription { get; set; } = string.Empty;
/// <summary>
/// Preselect the ERI version?
/// </summary>
public ERIVersion ERIVersion { get; set; } = ERIVersion.NONE;
/// <summary>
/// Preselect the language for implementing the ERI?
/// </summary>
public ProgrammingLanguages ProgrammingLanguage { get; set; }
/// <summary>
/// Do you want to preselect any other language?
/// </summary>
public string OtherProgrammingLanguage { get; set; } = string.Empty;
/// <summary>
/// Preselect a data source?
/// </summary>
public DataSources DataSource { get; set; }
/// <summary>
/// Do you want to preselect a product name for the data source?
/// </summary>
public string DataSourceProductName { get; set; } = string.Empty;
/// <summary>
/// Do you want to preselect any other data source?
/// </summary>
public string OtherDataSource { get; set; } = string.Empty;
/// <summary>
/// Do you want to preselect a hostname for the data source?
/// </summary>
public string DataSourceHostname { get; set; } = string.Empty;
/// <summary>
/// Do you want to preselect a port for the data source?
/// </summary>
public int? DataSourcePort { get; set; }
/// <summary>
/// Did the user type the port number?
/// </summary>
public bool UserTypedPort { get; set; } = false;
/// <summary>
/// Preselect any authentication methods?
/// </summary>
public HashSet<Auth> AuthMethods { get; set; } = [];
/// <summary>
/// Do you want to preselect any authentication description?
/// </summary>
public string AuthDescription { get; set; } = string.Empty;
/// <summary>
/// Do you want to preselect an operating system? This is necessary when SSO with Kerberos is used.
/// </summary>
public OperatingSystem OperatingSystem { get; set; } = OperatingSystem.NONE;
/// <summary>
/// Do you want to preselect which LLM providers are allowed?
/// </summary>
public AllowedLLMProviders AllowedLLMProviders { get; set; } = AllowedLLMProviders.NONE;
/// <summary>
/// Do you want to predefine any embedding information?
/// </summary>
public List<EmbeddingInfo> EmbeddingInfos { get; set; } = new();
/// <summary>
/// Do you want to predefine any retrieval information?
/// </summary>
public List<RetrievalInfo> RetrievalInfos { get; set; } = new();
/// <summary>
/// Do you want to preselect any additional libraries?
/// </summary>
public string AdditionalLibraries { get; set; } = string.Empty;
/// <summary>
/// Do you want to write all generated code to the filesystem?
/// </summary>
public bool WriteToFilesystem { get; set; }
/// <summary>
/// The base directory where to write the generated code to.
/// </summary>
public string BaseDirectory { get; set; } = string.Empty;
/// <summary>
/// We save which files were generated previously.
/// </summary>
public List<string> PreviouslyGeneratedFiles { get; set; } = new();
}

View File

@ -17,6 +17,7 @@ public enum Components
MY_TASKS_ASSISTANT, MY_TASKS_ASSISTANT,
JOB_POSTING_ASSISTANT, JOB_POSTING_ASSISTANT,
BIAS_DAY_ASSISTANT, BIAS_DAY_ASSISTANT,
ERI_ASSISTANT,
CHAT, CHAT,
} }

View File

@ -8,6 +8,7 @@ public static class ComponentsExtensions
public static bool AllowSendTo(this Components component) => component switch public static bool AllowSendTo(this Components component) => component switch
{ {
Components.NONE => false, Components.NONE => false,
Components.ERI_ASSISTANT => false,
Components.BIAS_DAY_ASSISTANT => false, Components.BIAS_DAY_ASSISTANT => false,
_ => true, _ => true,
@ -27,6 +28,7 @@ public static class ComponentsExtensions
Components.SYNONYMS_ASSISTANT => "Synonym Assistant", Components.SYNONYMS_ASSISTANT => "Synonym Assistant",
Components.MY_TASKS_ASSISTANT => "My Tasks Assistant", Components.MY_TASKS_ASSISTANT => "My Tasks Assistant",
Components.JOB_POSTING_ASSISTANT => "Job Posting Assistant", Components.JOB_POSTING_ASSISTANT => "Job Posting Assistant",
Components.ERI_ASSISTANT => "ERI Server",
Components.CHAT => "New Chat", Components.CHAT => "New Chat",
@ -68,6 +70,7 @@ public static class ComponentsExtensions
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
_ => default, _ => default,
}; };
@ -87,6 +90,7 @@ public static class ComponentsExtensions
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : default,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default,
@ -101,6 +105,7 @@ public static class ComponentsExtensions
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) : default, Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) : default, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProfile) : default,
Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) : default, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) : default,

View File

@ -0,0 +1,23 @@
namespace AIStudio.Tools;
/// <summary>
/// The result of a rate-limited HTTP stream.
/// </summary>
/// <param name="IsFailedAfterAllRetries">True, when the stream failed after all retries.</param>
/// <param name="ErrorMessage">The error message which we might show to the user.</param>
/// <param name="Response">The response from the server.</param>
public readonly record struct HttpRateLimitedStreamResult(
bool IsSuccessful,
bool IsFailedAfterAllRetries,
string ErrorMessage,
HttpResponseMessage? Response) : IDisposable
{
#region IDisposable
public void Dispose()
{
this.Response?.Dispose();
}
#endregion
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for selecting a directory.
/// </summary>
/// <param name="UserCancelled">Was the directory selection canceled?</param>
/// <param name="SelectedDirectory">The selected directory, if any.</param>
public readonly record struct DirectorySelectionResponse(bool UserCancelled, string SelectedDirectory);

View File

@ -0,0 +1,7 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for selecting a directory when a previous directory was selected.
/// </summary>
/// <param name="Path">The path of the previous directory.</param>
public readonly record struct PreviousDirectory(string Path);

View File

@ -323,6 +323,19 @@ public sealed class RustService : IDisposable
return state; 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
public void Dispose() public void Dispose()

View File

@ -117,4 +117,20 @@ public static class WorkspaceBehaviour
Directory.Delete(chatDirectory, true); Directory.Delete(chatDirectory, true);
} }
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
{
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
if(Path.Exists(workspacePath))
return;
Directory.CreateDirectory(workspacePath);
var workspaceNamePath = Path.Join(workspacePath, "name");
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
}
public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day");
public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers");
} }

View File

@ -207,6 +207,6 @@
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ==" "contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
} }
}, },
"net8.0/osx-x64": {} "net8.0/osx-arm64": {}
} }
} }

View File

@ -0,0 +1,6 @@
# v0.9.23, build 198 (2024-12-xx xx:xx UTC)
- Added an ERI server coding assistant as a preview feature behind the RAG feature flag. This helps you implement an ERI server to gain access to, e.g., your enterprise data from within AI Studio.
- Improved provider requests by handling rate limits by retrying requests.
- Improved the creation of the "the bias of the day" workspace; create that workspace only when the bias of the day feature is used.
- Fixed layout issues when selecting `other` items (e.g., programming languages).
- Fixed a bug about the bias of the day workspace when the workspace component was hidden.

View File

@ -0,0 +1,531 @@
{
"openapi": "3.0.1",
"info": {
"title": "ERI - (E)xternal (R)etrieval (I)nterface",
"description": "This API serves as a contract between LLM tools like AI Studio and any external data sources for RAG\n(retrieval-augmented generation). The tool, e.g., AI Studio acts as the client (the augmentation and\ngeneration parts) and the data sources act as the server (the retrieval part). The data\nsources implement some form of data retrieval and return a suitable context to the LLM tool.\nThe LLM tool, in turn, handles the integration of appropriate LLMs (augmentation & generation).\nData sources can be document or graph databases, or even a file system, for example. They\nwill likely implement an appropriate retrieval process by using some kind of embedding.\nHowever, this API does not inherently require any embedding, as data processing is\nimplemented decentralized by the data sources.",
"version": "v1"
},
"paths": {
"/auth/methods": {
"get": {
"tags": [
"Authentication"
],
"description": "Get the available authentication methods.",
"operationId": "GetAuthMethods",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AuthScheme"
}
}
}
}
}
}
}
},
"/auth": {
"post": {
"tags": [
"Authentication"
],
"description": "Authenticate with the data source to get a token for further requests.",
"operationId": "Authenticate",
"parameters": [
{
"name": "authMethod",
"in": "query",
"required": true,
"schema": {
"$ref": "#/components/schemas/AuthMethod"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
}
}
}
},
"/dataSource": {
"get": {
"tags": [
"Data Source"
],
"description": "Get information about the data source.",
"operationId": "GetDataSourceInfo",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DataSourceInfo"
}
}
}
}
}
}
},
"/embedding/info": {
"get": {
"tags": [
"Embedding"
],
"description": "Get information about the used embedding(s).",
"operationId": "GetEmbeddingInfo",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EmbeddingInfo"
}
}
}
}
}
}
}
},
"/retrieval/info": {
"get": {
"tags": [
"Retrieval"
],
"description": "Get information about the retrieval processes implemented by this data source.",
"operationId": "GetRetrievalInfo",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RetrievalInfo"
}
}
}
}
}
}
}
},
"/retrieval": {
"post": {
"tags": [
"Retrieval"
],
"description": "Retrieve information from the data source.",
"operationId": "Retrieve",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RetrievalRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Context"
}
}
}
}
}
}
}
},
"/security/requirements": {
"get": {
"tags": [
"Security"
],
"description": "Get the security requirements for this data source.",
"operationId": "GetSecurityRequirements",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SecurityRequirements"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AuthField": {
"enum": [
"NONE",
"USERNAME",
"PASSWORD",
"TOKEN",
"KERBEROS_TICKET"
],
"type": "string",
"description": "An authentication field."
},
"AuthFieldMapping": {
"type": "object",
"properties": {
"authField": {
"$ref": "#/components/schemas/AuthField"
},
"fieldName": {
"type": "string",
"description": "The field name in the authentication request.",
"nullable": true
}
},
"additionalProperties": false,
"description": "The mapping between an AuthField and the field name in the authentication request."
},
"AuthMethod": {
"enum": [
"NONE",
"KERBEROS",
"USERNAME_PASSWORD",
"TOKEN"
],
"type": "string"
},
"AuthResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True, when the authentication was successful."
},
"token": {
"type": "string",
"description": "The token to use for further requests.",
"nullable": true
},
"message": {
"type": "string",
"description": "When the authentication was not successful, this contains the reason.",
"nullable": true
}
},
"additionalProperties": false,
"description": "The response to an authentication request."
},
"AuthScheme": {
"type": "object",
"properties": {
"authMethod": {
"$ref": "#/components/schemas/AuthMethod"
},
"authFieldMappings": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AuthFieldMapping"
},
"description": "A list of field mappings for the authentication method. The client must know,\r\n e.g., how the password field is named in the request.",
"nullable": true
}
},
"additionalProperties": false,
"description": "Describes one authentication scheme for this data source."
},
"ChatThread": {
"type": "object",
"properties": {
"contentBlocks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContentBlock"
},
"description": "The content blocks in this chat thread.",
"nullable": true
}
},
"additionalProperties": false,
"description": "A chat thread, which is a list of content blocks."
},
"ContentBlock": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The content of the block. Remember that images and other media are base64 encoded.",
"nullable": true
},
"role": {
"$ref": "#/components/schemas/Role"
},
"type": {
"$ref": "#/components/schemas/ContentType"
}
},
"additionalProperties": false,
"description": "A block of content of a chat thread."
},
"ContentType": {
"enum": [
"NONE",
"UNKNOWN",
"TEXT",
"IMAGE",
"VIDEO",
"AUDIO",
"SPEECH"
],
"type": "string",
"description": "The type of content."
},
"Context": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the source, e.g., a document name, database name,\r\n collection name, etc.",
"nullable": true
},
"category": {
"type": "string",
"description": "What are the contents of the source? For example, is it a\r\n dictionary, a book chapter, business concept, a paper, etc.",
"nullable": true
},
"path": {
"type": "string",
"description": "The path to the content, e.g., a URL, a file path, a path in a\r\n graph database, etc.",
"nullable": true
},
"type": {
"$ref": "#/components/schemas/ContentType"
},
"matchedContent": {
"type": "string",
"description": "The content that matched the user prompt. For text, you\r\n return the matched text and, e.g., three words before and after it.",
"nullable": true
},
"surroundingContent": {
"type": "array",
"items": {
"type": "string"
},
"description": "The surrounding content of the matched content.\r\n For text, you may return, e.g., one sentence or paragraph before and after\r\n the matched content.",
"nullable": true
},
"links": {
"type": "array",
"items": {
"type": "string"
},
"description": "Links to related content, e.g., links to Wikipedia articles,\r\n links to sources, etc.",
"nullable": true
}
},
"additionalProperties": false,
"description": "Matching context returned by the data source as a result of a retrieval request."
},
"DataSourceInfo": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the data source, e.g., \"Internal Organization Documents.\"",
"nullable": true
},
"description": {
"type": "string",
"description": "A short description of the data source. What kind of data does it contain?\r\n What is the data source used for?",
"nullable": true
}
},
"additionalProperties": false,
"description": "Information about the data source."
},
"EmbeddingInfo": {
"type": "object",
"properties": {
"embeddingType": {
"type": "string",
"description": "What kind of embedding is used. For example, \"Transformer Embedding,\" \"Contextual Word\r\n Embedding,\" \"Graph Embedding,\" etc.",
"nullable": true
},
"embeddingName": {
"type": "string",
"description": "Name the embedding used. This can be a library, a framework, or the name of the used\r\n algorithm.",
"nullable": true
},
"description": {
"type": "string",
"description": "A short description of the embedding. Describe what the embedding is doing.",
"nullable": true
},
"usedWhen": {
"type": "string",
"description": "Describe when the embedding is used. For example, when the user prompt contains certain\r\n keywords, or anytime?",
"nullable": true
},
"link": {
"type": "string",
"description": "A link to the embedding's documentation or the source code. Might be null.",
"nullable": true
}
},
"additionalProperties": false,
"description": "Represents information about the used embedding for this data source. The purpose of this information is to give the\r\ninterested user an idea of what kind of embedding is used and what it does."
},
"ProviderType": {
"enum": [
"NONE",
"ANY",
"SELF_HOSTED"
],
"type": "string",
"description": "Known types of providers that can process data."
},
"RetrievalInfo": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer.",
"nullable": true
},
"name": {
"type": "string",
"description": "The name of the retrieval process, e.g., \"Keyword-Based Wikipedia Article Retrieval\".",
"nullable": true
},
"description": {
"type": "string",
"description": "A short description of the retrieval process. What kind of retrieval process is it?",
"nullable": true
},
"link": {
"type": "string",
"description": "A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.",
"nullable": true
},
"parametersDescription": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "A dictionary that describes the parameters of the retrieval process. The key is the parameter name,\r\n and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the\r\n expected type and range, e.g., 0.0 to 1.0 for a float parameter.",
"nullable": true
},
"embeddings": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EmbeddingInfo"
},
"description": "A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.",
"nullable": true
}
},
"additionalProperties": false,
"description": "Information about a retrieval process, which this data source implements."
},
"RetrievalRequest": {
"type": "object",
"properties": {
"latestUserPrompt": {
"type": "string",
"description": "The latest user prompt that AI Studio received.",
"nullable": true
},
"latestUserPromptType": {
"$ref": "#/components/schemas/ContentType"
},
"thread": {
"$ref": "#/components/schemas/ChatThread"
},
"retrievalProcessId": {
"type": "string",
"description": "Optional. The ID of the retrieval process that the data source should use.\r\n When null, the data source chooses an appropriate retrieval process. Selecting a retrieval process is optional\r\n for AI Studio users. Most users do not specify a retrieval process.",
"nullable": true
},
"parameters": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "A dictionary of parameters that the data source should use for the retrieval process.\r\n Although each parameter will be sent as a string, the retrieval process specifies the expected type and range.",
"nullable": true
},
"maxMatches": {
"type": "integer",
"description": "The maximum number of matches that the data source should return. AI Studio uses\r\n any value below 1 to indicate that the data source should return as many matches as appropriate.",
"format": "int32"
}
},
"additionalProperties": false,
"description": "The retrieval request sent by AI Studio."
},
"Role": {
"enum": [
"NONE",
"UNKNOW",
"SYSTEM",
"USER",
"AI",
"AGENT"
],
"type": "string",
"description": "Possible roles of any chat thread."
},
"SecurityRequirements": {
"type": "object",
"properties": {
"allowedProviderType": {
"$ref": "#/components/schemas/ProviderType"
}
},
"additionalProperties": false,
"description": "Represents the security requirements for this data source."
}
},
"securitySchemes": {
"ERI_Token": {
"type": "apiKey",
"description": "Enter the ERI token yielded by the authentication process at /auth.",
"name": "token",
"in": "header"
}
}
},
"security": [
{
"ERI_Token": [ ]
}
]
}

View File

@ -9,7 +9,7 @@ authors = ["Thorsten Sommer"]
tauri-build = { version = "1.5", features = [] } tauri-build = { version = "1.5", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open"] } tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -2,11 +2,13 @@ use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use log::{error, info, warn}; use log::{error, info, warn};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rocket::get; use rocket::{get, post};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::serde::Serialize; use rocket::serde::Serialize;
use serde::Deserialize;
use tauri::updater::UpdateResponse; use tauri::updater::UpdateResponse;
use tauri::{Manager, Window}; use tauri::{Manager, Window};
use tauri::api::dialog::blocking::FileDialogBuilder;
use tokio::time; use tokio::time;
use crate::api_token::APIToken; use crate::api_token::APIToken;
use crate::dotnet::stop_dotnet_server; use crate::dotnet::stop_dotnet_server;
@ -220,3 +222,52 @@ pub async fn install_update(_token: APIToken) {
}, },
} }
} }
/// Let the user select a directory.
#[post("/select/directory?<title>", data = "<previous_directory>")]
pub fn select_directory(_token: APIToken, title: &str, previous_directory: Option<Json<PreviousDirectory>>) -> Json<DirectorySelectionResponse> {
let folder_path = match previous_directory {
Some(previous) => {
let previous_path = previous.path.as_str();
FileDialogBuilder::new()
.set_title(title)
.set_directory(previous_path)
.pick_folder()
},
None => {
FileDialogBuilder::new()
.set_title(title)
.pick_folder()
},
};
match folder_path {
Some(path) => {
info!("User selected directory: {path:?}");
Json(DirectorySelectionResponse {
user_cancelled: false,
selected_directory: path.to_str().unwrap().to_string(),
})
},
None => {
info!("User cancelled directory selection.");
Json(DirectorySelectionResponse {
user_cancelled: true,
selected_directory: String::from(""),
})
},
}
}
#[derive(Clone, Deserialize)]
pub struct PreviousDirectory {
path: String,
}
#[derive(Serialize)]
pub struct DirectorySelectionResponse {
user_cancelled: bool,
selected_directory: String,
}

View File

@ -84,6 +84,7 @@ pub fn start_runtime_api() {
crate::clipboard::set_clipboard, crate::clipboard::set_clipboard,
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::secret::get_secret, crate::secret::get_secret,
crate::secret::store_secret, crate::secret::store_secret,
crate::secret::delete_secret, crate::secret::delete_secret,