mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-20 19:18:01 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9a65d9ca0 | ||
|
|
e4b1ea32fb | ||
|
|
e9975a5fbc | ||
|
|
2ef4d9d53f | ||
|
|
33a2728644 | ||
|
|
a54c0bdbbf | ||
|
|
d6521850e9 | ||
|
|
258bc7a338 | ||
|
|
187663bbf2 | ||
|
|
8060fc01dd | ||
|
|
b2ca49ab92 | ||
|
|
5e445f09fa | ||
|
|
6f5aecd92b | ||
|
|
317d3c9eec | ||
|
|
0acb3a20a2 | ||
|
|
3b93841982 | ||
|
|
38a45955fb | ||
|
|
caec3bfd2c | ||
|
|
12b3d6fc3d | ||
|
|
21927754b6 | ||
|
|
2131bf1817 | ||
|
|
8cbef49d89 | ||
|
|
cdf717ad5f | ||
|
|
02b6f526da | ||
|
|
a365539282 |
No files matched your search
@@ -370,7 +370,7 @@ jobs:
|
||||
|
||||
build_linux_arm64:
|
||||
name: Build app (linux-arm64)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: read_metadata
|
||||
env:
|
||||
SKIP: false # allows disabling this long-running job temporarily
|
||||
|
||||
@@ -147,3 +147,4 @@ orleans.codegen.cs
|
||||
**/.idea/**/dynamic.xml
|
||||
**/.idea/**/uiDesigner.xml
|
||||
**/.idea/**/dbnavigator.xml
|
||||
**/.vs
|
||||
@@ -0,0 +1,22 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: AI Studio
|
||||
message: >-
|
||||
When you want to cite AI Studio in your scientific work,
|
||||
please use these metadata.
|
||||
type: software
|
||||
authors:
|
||||
- given-names: Thorsten
|
||||
family-names: Sommer
|
||||
email: thorsten.sommer@dlr.de
|
||||
affiliation: Deutsches Zentrum für Luft- und Raumfahrt (DLR)
|
||||
orcid: 'https://orcid.org/0000-0002-3264-9934'
|
||||
- name: Open Source Community
|
||||
repository-code: 'https://github.com/MindWorkAI/AI-Studio'
|
||||
url: 'https://mindworkai.org/'
|
||||
keywords:
|
||||
- LLM
|
||||
- AI
|
||||
- Orchestration
|
||||
+1
-1
@@ -6,7 +6,7 @@ FSL-1.1-MIT
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2024 Thorsten Sommer
|
||||
Copyright 2025 Thorsten Sommer
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ Things we are currently working on:
|
||||
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
|
||||
|
||||
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
|
||||
- [x] ~~Define the [External Data API (EDI)](https://github.com/MindWorkAI/EDI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/EDI/pull/1))~~
|
||||
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
|
||||
- [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: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||
- [ ] App: Management of data sources (local & external data via [EDI](https://github.com/MindWorkAI/EDI))
|
||||
- [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))
|
||||
- [ ] 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)
|
||||
- [ ] App: Implement external embedding providers
|
||||
@@ -34,14 +35,14 @@ Other News:
|
||||
|
||||
Features we have recently released:
|
||||
|
||||
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
|
||||
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
|
||||
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
|
||||
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
|
||||
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
|
||||
- v0.9.16: Added workspace display options & improved the layout of the app window.
|
||||
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
|
||||
- v0.9.13: You can use `ollama` providers secured with API keys.
|
||||
- v0.9.12: Added a job posting assistant to the business category and improved grammar & spelling check and rewrite assistants.
|
||||
- v0.9.11: Added enforcement of minimal confidence levels & dark mode.
|
||||
|
||||
## What is AI Studio?
|
||||
|
||||
@@ -52,7 +53,7 @@ MindWork AI Studio is a desktop application available for macOS, Windows, and Li
|
||||
|
||||
**Key advantages:**
|
||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/).
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/).
|
||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
||||
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
||||
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API.
|
||||
@@ -78,7 +79,7 @@ Your support, whether big or small, keeps the wheels turning and is deeply appre
|
||||
## Planned Features
|
||||
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
|
||||
- **Integrating your data**: You should be able to integrate your data into AI Studio. For example, your PDF or Office files, or your Markdown notes.
|
||||
- **Integration of enterprise data:** Soon, it will also be possible to integrate data from the corporate network using an interface that we have specified ([External Data API](https://github.com/MindWorkAI/EDI), EDI for short).
|
||||
- **Integration of enterprise data:** Soon, it will also be possible to integrate data from the corporate network using an interface that we have specified ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short).
|
||||
- **Writing mode:** We want to integrate a writing mode that should support you in creating extensive works. We are thinking of comprehensive project proposals, tenders, or your next fantasy novel.
|
||||
- **Browser usage:** We're trying to offer the features from AI Studio to you in the browser via a plugin, so we could use spell-checking or rewriting text directly in the browser.
|
||||
- **Voice control:** You should be able to interact with the AI systems using your voice as well. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
|
||||
|
||||
@@ -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">
|
||||
<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/=LM/@EntryIndexedValue">LM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||
|
||||
@@ -106,7 +106,7 @@ public partial class AssistantAgenda : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputContent = string.Empty;
|
||||
this.contentLines.Clear();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<InnerScrolling HeaderHeight="6em">
|
||||
<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">
|
||||
@this.Description
|
||||
</MudText>
|
||||
@@ -35,11 +35,22 @@
|
||||
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
|
||||
</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)"/>
|
||||
}
|
||||
|
||||
@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>
|
||||
</ChildContent>
|
||||
|
||||
@@ -4,7 +4,10 @@ using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using MudBlazor.Utilities;
|
||||
|
||||
using RustService = AIStudio.Tools.RustService;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace AIStudio.Assistants;
|
||||
|
||||
@@ -55,7 +58,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
protected abstract void ResetFrom();
|
||||
protected abstract void ResetForm();
|
||||
|
||||
protected abstract bool MightPreselectValues();
|
||||
|
||||
@@ -68,6 +71,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
private protected virtual RenderFragment? Body => null;
|
||||
|
||||
protected virtual bool ShowResult => true;
|
||||
|
||||
protected virtual bool ShowEntireChatThread => false;
|
||||
|
||||
protected virtual bool AllowProfiles => true;
|
||||
|
||||
@@ -91,8 +96,10 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
protected MudForm? form;
|
||||
protected bool inputIsValid;
|
||||
protected Profile currentProfile = Profile.NO_PROFILE;
|
||||
|
||||
protected ChatThread? chatThread;
|
||||
|
||||
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
private ContentBlock? resultingContentBlock;
|
||||
private string[] inputIssues = [];
|
||||
private bool isProcessing;
|
||||
@@ -101,6 +108,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.formChangeTimer.AutoReset = false;
|
||||
this.formChangeTimer.Elapsed += async (_, _) =>
|
||||
{
|
||||
this.formChangeTimer.Stop();
|
||||
await this.OnFormChange();
|
||||
};
|
||||
|
||||
this.MightPreselectValues();
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
|
||||
@@ -161,22 +175,46 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
|
||||
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()
|
||||
{
|
||||
this.chatThread = new()
|
||||
{
|
||||
SelectedProvider = this.providerSettings.Id,
|
||||
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
|
||||
SystemPrompt = this.SystemPrompt,
|
||||
WorkspaceId = Guid.Empty,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Empty,
|
||||
Name = $"Assistant - {this.Title}",
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
|
||||
$"""
|
||||
{this.SystemPrompt}
|
||||
|
||||
{this.currentProfile.ToSystemPrompt()}
|
||||
""",
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
@@ -187,16 +225,12 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
this.chatThread = new()
|
||||
{
|
||||
SelectedProvider = this.providerSettings.Id,
|
||||
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
|
||||
SystemPrompt = this.SystemPrompt,
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = chatId,
|
||||
Name = name,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
|
||||
$"""
|
||||
{this.SystemPrompt}
|
||||
|
||||
{this.currentProfile.ToSystemPrompt()}
|
||||
""",
|
||||
Blocks = [],
|
||||
};
|
||||
|
||||
@@ -221,7 +255,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
return time;
|
||||
}
|
||||
|
||||
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
|
||||
protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false)
|
||||
{
|
||||
var aiText = new ContentText
|
||||
{
|
||||
@@ -236,6 +270,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
HideFromUser = hideContentFromUser,
|
||||
};
|
||||
|
||||
if (this.chatThread is not null)
|
||||
@@ -313,7 +348,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
|
||||
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
|
||||
|
||||
this.ResetFrom();
|
||||
this.ResetForm();
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
|
||||
|
||||
this.inputIsValid = false;
|
||||
@@ -341,6 +376,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
this.formChangeTimer.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Assistants.BiasDay;
|
||||
@@ -50,7 +50,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
||||
|
||||
protected override bool ShowReset => false;
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
if (!this.MightPreselectValues())
|
||||
{
|
||||
@@ -124,7 +124,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
||||
{
|
||||
var biasChat = new LoadChat
|
||||
{
|
||||
WorkspaceId = Workspaces.WORKSPACE_ID_BIAS,
|
||||
WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID,
|
||||
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
|
||||
};
|
||||
|
||||
@@ -147,7 +147,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore
|
||||
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
|
||||
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.BiasOfTheDayChatId = chatId;
|
||||
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
@@ -32,7 +32,7 @@ public partial class AssistantCoding : AssistantBaseCore
|
||||
|
||||
protected override Func<Task> SubmitAction => this.GetSupport;
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.codingContexts.Clear();
|
||||
this.compilerMessages = string.Empty;
|
||||
|
||||
@@ -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"/>
|
||||
<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">
|
||||
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class AssistantEMail : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputBulletPoints = string.Empty;
|
||||
this.bulletPointsLines.Clear();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public enum AllowedLLMProviders
|
||||
{
|
||||
NONE,
|
||||
|
||||
ANY,
|
||||
SELF_HOSTED,
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 Wikipedia’s 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
@@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public enum Auth
|
||||
{
|
||||
NONE,
|
||||
KERBEROS,
|
||||
USERNAME_PASSWORD,
|
||||
TOKEN,
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public enum ERIVersion
|
||||
{
|
||||
NONE,
|
||||
|
||||
V1,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public enum OperatingSystem
|
||||
{
|
||||
NONE,
|
||||
|
||||
WINDOWS,
|
||||
LINUX,
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public enum ProgrammingLanguages
|
||||
{
|
||||
NONE,
|
||||
|
||||
C,
|
||||
CPP,
|
||||
CSHARP,
|
||||
GO,
|
||||
JAVA,
|
||||
JAVASCRIPT,
|
||||
JULIA,
|
||||
MATLAB,
|
||||
PHP,
|
||||
PYTHON,
|
||||
RUST,
|
||||
|
||||
OTHER,
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
this.correctedText = string.Empty;
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class AssistantIconFinder : AssistantBaseCore
|
||||
|
||||
protected override Func<Task> SubmitAction => this.FindIcon;
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputContext = string.Empty;
|
||||
if (!this.MightPreselectValues())
|
||||
|
||||
@@ -59,7 +59,7 @@ public partial class AssistantJobPostings : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputEntryDate = string.Empty;
|
||||
this.inputValidUntil = string.Empty;
|
||||
|
||||
@@ -37,7 +37,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputLegalDocument = string.Empty;
|
||||
this.inputQuestions = string.Empty;
|
||||
|
||||
@@ -41,7 +41,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
if (!this.MightPreselectValues())
|
||||
|
||||
@@ -49,7 +49,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
this.rewrittenText = string.Empty;
|
||||
|
||||
@@ -60,7 +60,7 @@ public partial class AssistantSynonyms : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
this.inputContext = string.Empty;
|
||||
|
||||
@@ -40,7 +40,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
if(!this.MightPreselectValues())
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class AssistantTranslation : AssistantBaseCore
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
};
|
||||
|
||||
protected override void ResetFrom()
|
||||
protected override void ResetForm()
|
||||
{
|
||||
this.inputText = string.Empty;
|
||||
this.inputTextLastTranslation = string.Empty;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,6 +22,11 @@ public sealed record ChatThread
|
||||
/// </summary>
|
||||
public string SelectedProvider { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the profile selected for the chat thread.
|
||||
/// </summary>
|
||||
public string SelectedProfile { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
@@ -39,4 +46,96 @@ public sealed record ChatThread
|
||||
/// The content blocks of the chat thread.
|
||||
/// </summary>
|
||||
public List<ContentBlock> Blocks { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the system prompt for the chat thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The actual system prompt depends on the selected profile. If no profile is selected,
|
||||
/// the system prompt is returned as is. When a profile is selected, the system prompt
|
||||
/// is extended with the profile chosen.
|
||||
/// </remarks>
|
||||
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||
/// <param name="chatThread">The chat thread to prepare the system prompt for.</param>
|
||||
/// <param name="logger">The logger instance to use.</param>
|
||||
/// <returns>The prepared system prompt.</returns>
|
||||
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
|
||||
{
|
||||
//
|
||||
// Prepare the system prompt:
|
||||
//
|
||||
string systemPromptText;
|
||||
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
|
||||
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
else
|
||||
{
|
||||
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
else
|
||||
{
|
||||
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
else
|
||||
{
|
||||
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
|
||||
if(profile == default)
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
else
|
||||
{
|
||||
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
|
||||
systemPromptText = $"""
|
||||
{chatThread.SystemPrompt}
|
||||
|
||||
{profile.ToSystemPrompt()}
|
||||
""";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation(logMessage);
|
||||
return systemPromptText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a content block from this chat thread.
|
||||
/// </summary>
|
||||
/// <param name="content">The content block to remove.</param>
|
||||
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
|
||||
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
|
||||
/// which will not remove the previous user block if it is hidden from the user.</param>
|
||||
public void Remove(IContent content, bool removeForRegenerate = false)
|
||||
{
|
||||
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
|
||||
if(block is null)
|
||||
return;
|
||||
|
||||
//
|
||||
// Remove the previous user block if it is hidden from the user. Otherwise,
|
||||
// the experience might be confusing for the user.
|
||||
//
|
||||
// Explanation, using the ERI assistant as an example:
|
||||
// - The ERI assistant generates for every file a hidden user prompt.
|
||||
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
|
||||
// - Now, the user removes one AI response
|
||||
// - The hidden user prompt is still there, but the user can't see it.
|
||||
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
|
||||
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
|
||||
//
|
||||
if (block.Role is ChatRole.AI && !removeForRegenerate)
|
||||
{
|
||||
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
|
||||
var index = sortedBlocks.IndexOf(block);
|
||||
if (index > 0)
|
||||
{
|
||||
var previousBlock = sortedBlocks[index - 1];
|
||||
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
|
||||
this.Blocks.Remove(previousBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the block from the chat thread:
|
||||
this.Blocks.Remove(block);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,33 @@
|
||||
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard" />
|
||||
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="Edit" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="Edit" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="Removes this block" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="Copies the content to the clipboard" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/>
|
||||
</MudTooltip>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@@ -44,7 +70,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
|
||||
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using RustService = AIStudio.Tools.RustService;
|
||||
@@ -39,11 +41,38 @@ public partial class ContentBlockComponent : ComponentBase
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool IsLastContentBlock { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public bool IsSecondToLastBlock { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? RegenerateFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? EditLastUserBlockFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<bool> RegenerateEnabled { get; set; } = () => false;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private bool HideContent { get; set; }
|
||||
|
||||
@@ -115,4 +144,68 @@ public partial class ContentBlockComponent : ComponentBase
|
||||
}
|
||||
|
||||
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
||||
|
||||
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
|
||||
|
||||
private async Task RemoveBlock()
|
||||
{
|
||||
if (this.RemoveBlockFunc is null)
|
||||
return;
|
||||
|
||||
var remove = await this.DialogService.ShowMessageBox(
|
||||
"Remove Message",
|
||||
"Do you really want to remove this message?",
|
||||
"Yes, remove it",
|
||||
"No, keep it");
|
||||
|
||||
if (remove.HasValue && remove.Value)
|
||||
await this.RemoveBlockFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task RegenerateBlock()
|
||||
{
|
||||
if (this.RegenerateFunc is null)
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.AI)
|
||||
return;
|
||||
|
||||
var regenerate = await this.DialogService.ShowMessageBox(
|
||||
"Regenerate Message",
|
||||
"Do you really want to regenerate this message?",
|
||||
"Yes, regenerate it",
|
||||
"No, keep it");
|
||||
|
||||
if (regenerate.HasValue && regenerate.Value)
|
||||
await this.RegenerateFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task EditLastBlock()
|
||||
{
|
||||
if (this.EditLastBlockFunc is null)
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.USER)
|
||||
return;
|
||||
|
||||
await this.EditLastBlockFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task EditLastUserBlock()
|
||||
{
|
||||
if (this.EditLastUserBlockFunc is null)
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.USER)
|
||||
return;
|
||||
|
||||
var edit = await this.DialogService.ShowMessageBox(
|
||||
"Edit Message",
|
||||
"Do you really want to edit this message? In order to edit this message, the AI response will be deleted.",
|
||||
"Yes, remove the AI response and edit it",
|
||||
"No, keep it");
|
||||
|
||||
if (edit.HasValue && edit.Value)
|
||||
await this.EditLastUserBlockFunc(this.Content);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public sealed class ContentText : IContent
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
|
||||
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
|
||||
new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"),
|
||||
new (198, "v0.9.23, build 198 (2025-01-02 19:39 UTC)", "v0.9.23.md"),
|
||||
new (197, "v0.9.22, build 197 (2024-12-04 10:58 UTC)", "v0.9.22.md"),
|
||||
new (196, "v0.9.21, build 196 (2024-11-23 12:22 UTC)", "v0.9.21.md"),
|
||||
new (195, "v0.9.20, build 195 (2024-11-16 20:44 UTC)", "v0.9.20.md"),
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@using AIStudio.Chat
|
||||
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em" MinWidth="36em">
|
||||
<ChildContent>
|
||||
@if (this.ChatThread is not null)
|
||||
{
|
||||
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
|
||||
for (var i = 0; i < blocks.Count; i++)
|
||||
{
|
||||
var block = blocks[i];
|
||||
var isLastBlock = i == blocks.Count - 1;
|
||||
var isSecondLastBlock = i == blocks.Count - 2;
|
||||
@if (!block.HideFromUser)
|
||||
{
|
||||
<ContentBlockComponent
|
||||
Role="@block.Role"
|
||||
Type="@block.ContentType"
|
||||
Time="@block.Time"
|
||||
Content="@block.Content"
|
||||
RemoveBlockFunc="@this.RemoveBlock"
|
||||
IsLastContentBlock="@isLastBlock"
|
||||
IsSecondToLastBlock="@isSecondLastBlock"
|
||||
RegenerateFunc="@this.RegenerateBlock"
|
||||
RegenerateEnabled="@(() => this.IsProviderSelected)"
|
||||
EditLastBlockFunc="@this.EditLastBlock"
|
||||
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ChildContent>
|
||||
<FooterContent>
|
||||
<MudElement Style="flex: 0 0 auto;">
|
||||
<MudTextField
|
||||
T="string"
|
||||
@ref="@this.inputField"
|
||||
@bind-Text="@this.userInput"
|
||||
Variant="Variant.Outlined"
|
||||
AutoGrow="@true"
|
||||
Lines="3"
|
||||
MaxLines="12"
|
||||
Label="@this.InputLabel"
|
||||
Placeholder="@this.ProviderPlaceholder"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||
OnAdornmentClick="() => this.SendMessage()"
|
||||
ReadOnly="!this.IsProviderSelected || this.isStreaming"
|
||||
Immediate="@true"
|
||||
OnKeyUp="this.InputKeyEvent"
|
||||
UserAttributes="@USER_INPUT_ATTRIBUTES"
|
||||
Class="@this.UserInputClass"
|
||||
Style="@this.UserInputStyle"/>
|
||||
</MudElement>
|
||||
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
|
||||
@if (
|
||||
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
|
||||
{
|
||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
||||
</MudTooltip>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||
{
|
||||
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||
{
|
||||
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||
{
|
||||
<MudTooltip Text="Stop generation" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
|
||||
</MudToolBar>
|
||||
</FooterContent>
|
||||
</InnerScrolling>
|
||||
@@ -0,0 +1,711 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Parameter]
|
||||
public ChatThread? ChatThread { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<ChatThread?> ChatThreadChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Settings.Provider Provider { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Settings.Provider> ProviderChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<string> WorkspaceName { get; set; } = _ => { };
|
||||
|
||||
[Parameter]
|
||||
public Workspaces? Workspaces { get; set; }
|
||||
|
||||
[Inject]
|
||||
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
private Profile currentProfile = Profile.NO_PROFILE;
|
||||
private bool hasUnsavedChanges;
|
||||
private bool mustScrollToBottomAfterRender;
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
private byte scrollRenderCountdown;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private bool mustStoreChat;
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
private bool autoSaveEnabled;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
// Unfortunately, we need the input field reference to blur the focus away. Without
|
||||
// this, we cannot clear the input field.
|
||||
private MudTextField<string> inputField = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
||||
if (deferredContent is not null)
|
||||
{
|
||||
this.ChatThread = deferredContent;
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||
{
|
||||
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
|
||||
if (firstUserBlock is not null)
|
||||
{
|
||||
this.ChatThread.Name = firstUserBlock.Content switch
|
||||
{
|
||||
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
|
||||
_ => "Thread"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
this.autoSaveEnabled = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 4;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
||||
if (deferredLoading != default)
|
||||
{
|
||||
this.loadChat = deferredLoading;
|
||||
this.mustLoadChat = true;
|
||||
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
|
||||
}
|
||||
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && this.ChatThread is not null && this.mustStoreChat)
|
||||
{
|
||||
this.mustStoreChat = false;
|
||||
|
||||
if(this.Workspaces is not null)
|
||||
await this.Workspaces.StoreChat(this.ChatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
if (firstRender && this.mustLoadChat)
|
||||
{
|
||||
this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now.");
|
||||
this.mustLoadChat = false;
|
||||
this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
|
||||
|
||||
if(this.ChatThread is not null)
|
||||
{
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
}
|
||||
else
|
||||
this.Logger.LogWarning($"The chat '{this.loadChat.ChatId}' could not be loaded.");
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
if(this.mustScrollToBottomAfterRender)
|
||||
{
|
||||
if (--this.scrollRenderCountdown == 0)
|
||||
{
|
||||
await this.scrollingArea.ScrollToBottom();
|
||||
this.mustScrollToBottomAfterRender = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
|
||||
|
||||
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.Provider.InstanceName}', provider '{this.Provider.UsedLLMProvider.ToName()}')" : "Select a provider first";
|
||||
|
||||
private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0;
|
||||
|
||||
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
|
||||
|
||||
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||
|
||||
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||
|
||||
private string ExtractThreadName(string firstUserInput)
|
||||
{
|
||||
// We select the first 10 words of the user input:
|
||||
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var threadName = string.Join(' ', words.Take(10));
|
||||
|
||||
// If the thread name is empty, we use a default name:
|
||||
if (string.IsNullOrWhiteSpace(threadName))
|
||||
threadName = "Thread";
|
||||
|
||||
return threadName;
|
||||
}
|
||||
|
||||
private async Task ProfileWasChanged(Profile profile)
|
||||
{
|
||||
this.currentProfile = profile;
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
this.ChatThread = this.ChatThread with
|
||||
{
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
};
|
||||
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
this.hasUnsavedChanges = true;
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
var isEnter = key is "enter" or "numpadenter";
|
||||
|
||||
// Was a modifier key pressed as well?
|
||||
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
|
||||
|
||||
// Depending on the user's settings, might react to shortcuts:
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
|
||||
{
|
||||
case SendBehavior.ENTER_IS_SENDING:
|
||||
if (!isModifier && isEnter)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
|
||||
case SendBehavior.MODIFER_ENTER_IS_SENDING:
|
||||
if (isEnter && isModifier)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessage(bool reuseLastUserPrompt = false)
|
||||
{
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
// We need to blur the focus away from the input field
|
||||
// to be able to clear the field:
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
// Create a new chat thread if necessary:
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
this.ChatThread = new()
|
||||
{
|
||||
SelectedProvider = this.Provider.Id,
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = this.ExtractThreadName(this.userInput),
|
||||
Seed = this.RNG.Next(),
|
||||
Blocks = [],
|
||||
};
|
||||
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
|
||||
|
||||
// Update provider and profile:
|
||||
this.ChatThread.SelectedProvider = this.Provider.Id;
|
||||
this.ChatThread.SelectedProfile = this.currentProfile.Id;
|
||||
}
|
||||
|
||||
var time = DateTimeOffset.Now;
|
||||
if (!reuseLastUserPrompt)
|
||||
{
|
||||
//
|
||||
// Add the user message to the thread:
|
||||
//
|
||||
this.ChatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.USER,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
},
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Add the AI response to the thread:
|
||||
//
|
||||
var aiText = new ContentText
|
||||
{
|
||||
// We have to wait for the remote
|
||||
// for the content stream:
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.ChatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
});
|
||||
|
||||
// Clear the input field:
|
||||
this.userInput = string.Empty;
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 2;
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
|
||||
using (this.cancellationTokenSource = new())
|
||||
{
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.SettingsManager, this.Provider.Model, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
{
|
||||
if (this.cancellationTokenSource is not null)
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (!this.CanThreadBeSaved)
|
||||
return;
|
||||
|
||||
//
|
||||
// When the workspace component is visible, we store the chat
|
||||
// through the workspace component. The advantage of this is that
|
||||
// the workspace gets updated automatically when the chat is saved.
|
||||
//
|
||||
if (this.Workspaces is not null)
|
||||
await this.Workspaces.StoreChat(this.ChatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.ChatThread);
|
||||
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ChatThread is not null && deletePreviousChat)
|
||||
{
|
||||
string chatPath;
|
||||
if (this.ChatThread.WorkspaceId == Guid.Empty)
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString());
|
||||
else
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
|
||||
|
||||
if(this.Workspaces is null)
|
||||
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
else
|
||||
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
}
|
||||
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
|
||||
{
|
||||
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
|
||||
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
break;
|
||||
|
||||
default:
|
||||
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
|
||||
if(this.Provider == default)
|
||||
{
|
||||
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!useSameWorkspace)
|
||||
{
|
||||
this.ChatThread = null;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ChatThread = new()
|
||||
{
|
||||
SelectedProvider = this.Provider.Id,
|
||||
SelectedProfile = this.currentProfile.Id,
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Empty,
|
||||
Seed = this.RNG.Next(),
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
|
||||
this.userInput = string.Empty;
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
private async Task MoveChatToWorkspace()
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Please select the workspace where you want to move the chat to." },
|
||||
{ "SelectedWorkspace", this.ChatThread?.WorkspaceId },
|
||||
{ "ConfirmText", "Move chat" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = dialogResult.Data is Guid id ? id : Guid.Empty;
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Delete the chat from the current workspace or the temporary storage:
|
||||
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
|
||||
this.ChatThread!.WorkspaceId = workspaceId;
|
||||
await this.SaveThread();
|
||||
|
||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
this.currentWorkspaceName = this.ChatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 2;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
|
||||
this.ChatThread = null;
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
private async Task SelectProviderWhenLoadingChat()
|
||||
{
|
||||
var chatProvider = this.ChatThread?.SelectedProvider;
|
||||
var chatProfile = this.ChatThread?.SelectedProfile;
|
||||
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
|
||||
{
|
||||
default:
|
||||
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
|
||||
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
|
||||
break;
|
||||
|
||||
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
|
||||
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
|
||||
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
|
||||
if(this.Provider == default)
|
||||
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
// Try to select the profile:
|
||||
if (!string.IsNullOrWhiteSpace(chatProfile))
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile);
|
||||
if(this.currentProfile == default)
|
||||
this.currentProfile = Profile.NO_PROFILE;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleWorkspaceOverlay()
|
||||
{
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
|
||||
}
|
||||
|
||||
private async Task RemoveBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
this.ChatThread.Remove(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
await this.SaveThread();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RegenerateBlock(IContent aiBlock)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
await this.SendMessage(reuseLastUserPrompt: true);
|
||||
}
|
||||
|
||||
private Task EditLastUserBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (block is not ContentText textBlock)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var lastBlock = this.ChatThread.Blocks.Last();
|
||||
var lastBlockContent = lastBlock.Content;
|
||||
if(lastBlockContent is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task EditLastBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (block is not ContentText textBlock)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.ChatThread.Remove(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.RESET_CHAT_STATE:
|
||||
await this.ResetState();
|
||||
break;
|
||||
|
||||
case Event.CHAT_STREAMING_DONE:
|
||||
if(this.autoSaveEnabled)
|
||||
await this.SaveThread();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
|
||||
await this.LoadedChatChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IAsyncDisposable
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
if (this.cancellationTokenSource is not null)
|
||||
{
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
|
||||
this.cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@typeparam T
|
||||
@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">
|
||||
@foreach (var value in Enum.GetValues<T>())
|
||||
{
|
||||
|
||||
@@ -32,6 +32,9 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string? MinWidth { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private MainLayout MainLayout { get; set; } = null!;
|
||||
|
||||
@@ -71,7 +74,9 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0;" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0;";
|
||||
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth};";
|
||||
|
||||
private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0; {this.MinWidthStyle}" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0; {this.MinWidthStyle}";
|
||||
|
||||
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -31,15 +31,10 @@ public partial class Workspaces : ComponentBase
|
||||
[Parameter]
|
||||
public EventCallback<ChatThread> CurrentChatThreadChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task> LoadedChatWasChanged { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
[Parameter]
|
||||
public bool ExpandRootNodes { get; set; } = true;
|
||||
|
||||
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();
|
||||
|
||||
@@ -53,8 +48,6 @@ public partial class Workspaces : ComponentBase
|
||||
// - 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
|
||||
//
|
||||
|
||||
await this.EnsureBiasWorkspace();
|
||||
await this.LoadTreeItems();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
@@ -277,7 +270,7 @@ public partial class Workspaces : ComponentBase
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
return chat;
|
||||
@@ -329,7 +322,7 @@ public partial class Workspaces : ComponentBase
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,18 +401,6 @@ public partial class Workspaces : ComponentBase
|
||||
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)
|
||||
{
|
||||
if(workspacePath is null)
|
||||
@@ -487,7 +468,7 @@ public partial class Workspaces : ComponentBase
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await this.StoreChat(chat);
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
File renamed without changes.
+2
-2
@@ -8,7 +8,7 @@ using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class EmbeddingDialog : ComponentBase, ISecretId
|
||||
public partial class EmbeddingProviderDialog : ComponentBase, ISecretId
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
@@ -97,7 +97,7 @@ public partial class EmbeddingDialog : ComponentBase, ISecretId
|
||||
private readonly Encryption encryption = Program.ENCRYPTION;
|
||||
private readonly ProviderValidation providerValidation;
|
||||
|
||||
public EmbeddingDialog()
|
||||
public EmbeddingProviderDialog()
|
||||
{
|
||||
this.providerValidation = new()
|
||||
{
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -8,25 +8,39 @@
|
||||
<MudLayout>
|
||||
@if (!this.performingUpdate)
|
||||
{
|
||||
<MudDrawerContainer Class="mud-height-full absolute">
|
||||
<MudDrawer @bind-Open="@this.navBarOpen" MiniWidth="@NAVBAR_COLLAPSED_WIDTH" Width="@NAVBAR_EXPANDED_WIDTH" Elevation="1" Fixed="@true" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.EXPAND_ON_HOVER)" Color="Color.Default">
|
||||
@if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.EXPAND_ON_HOVER or NavBehavior.ALWAYS_EXPAND)
|
||||
{
|
||||
<MudDrawerContainer Class="mud-height-full absolute">
|
||||
<MudDrawer @bind-Open="@this.navBarOpen" MiniWidth="@NAVBAR_COLLAPSED_WIDTH" Width="@NAVBAR_EXPANDED_WIDTH" Elevation="1" Fixed="@true" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.EXPAND_ON_HOVER)" Color="Color.Default">
|
||||
<MudNavMenu>
|
||||
@foreach (var navBarItem in this.navItems)
|
||||
{
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">@navBarItem.Name</MudNavLink>
|
||||
}
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
</MudDrawerContainer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Width="4em" Class="mud-height-full absolute">
|
||||
<MudNavMenu>
|
||||
@foreach (var navBarItem in this.navItems)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.NEVER_EXPAND_USE_TOOLTIPS)
|
||||
{
|
||||
<MudTooltip Text="@navBarItem.Name" Placement="Placement.Right">
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">@navBarItem.Name</MudNavLink>
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">@navBarItem.Name</MudNavLink>
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color"/>
|
||||
}
|
||||
}
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
</MudDrawerContainer>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
|
||||
<MudMainContent Class="mud-height-full pt-1" Style="@this.PaddingLeft">
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0" />
|
||||
<PackageReference Include="MudBlazor" Version="7.15.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
|
||||
|
||||
@@ -35,7 +35,7 @@ public partial class About : ComponentBase
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2024 Thorsten Sommer
|
||||
Copyright 2025 Thorsten Sommer
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@attribute [Route(Routes.ASSISTANTS)]
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
@@ -41,6 +42,10 @@
|
||||
</MudText>
|
||||
<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"/>
|
||||
@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>
|
||||
|
||||
</InnerScrolling>
|
||||
@@ -1,5 +1,11 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Assistants : ComponentBase;
|
||||
public partial class Assistants : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
public SettingsManager SettingsManager { get; set; } = null!;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
@attribute [Route(Routes.CHAT)]
|
||||
@using AIStudio.Chat
|
||||
@using AIStudio.Settings.DataModel
|
||||
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
@@ -16,113 +14,81 @@
|
||||
</MudText>
|
||||
|
||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
|
||||
<MudStack Row="@true" Style="width: 100%; overflow: hidden;">
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR
|
||||
&& !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
||||
{
|
||||
@if (this.AreWorkspacesVisible)
|
||||
{
|
||||
<MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;">
|
||||
<StartContent>
|
||||
@if (this.AreWorkspacesHidden)
|
||||
{
|
||||
<MudPaper Class="border border-solid rounded-lg">
|
||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||
</MudTooltip>
|
||||
</MudPaper>
|
||||
}
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
@if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE)
|
||||
{
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
||||
{
|
||||
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em">
|
||||
<HeaderContent>
|
||||
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||
</MudTooltip>
|
||||
</HeaderContent>
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
else
|
||||
{
|
||||
<InnerScrolling HeaderHeight="12.3em" FillEntireHorizontalSpace="@true" Class="border border-solid rounded-lg" MinWidth="26em">
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
}
|
||||
}
|
||||
</StartContent>
|
||||
<EndContent>
|
||||
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.currentWorkspaceName = name" />
|
||||
|
||||
</EndContent>
|
||||
</MudSplitter>
|
||||
}
|
||||
else if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES && this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR)
|
||||
{
|
||||
<MudStack Row="@true" Style="width: 100%; overflow: hidden;">
|
||||
<MudPaper Class="border border-solid rounded-lg">
|
||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||
</MudTooltip>
|
||||
</MudPaper>
|
||||
}
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
@if ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible) || this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE)
|
||||
{
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
||||
{
|
||||
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
|
||||
<HeaderContent>
|
||||
<MudTooltip Text="Hide your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Size="Size.Large" Icon="@this.WorkspaceSidebarToggleIcon" OnClick="() => this.ToggleWorkspaceSidebar()"/>
|
||||
</MudTooltip>
|
||||
</HeaderContent>
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
else
|
||||
{
|
||||
<InnerScrolling HeaderHeight="12.3em" Class="border border-solid rounded-lg">
|
||||
<ChildContent>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
||||
</ChildContent>
|
||||
</InnerScrolling>
|
||||
}
|
||||
}
|
||||
}
|
||||
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em">
|
||||
<ChildContent>
|
||||
@if (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"/>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ChildContent>
|
||||
<FooterContent>
|
||||
<MudElement Style="flex: 0 0 auto;">
|
||||
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="@this.UserInputClass" Style="@this.UserInputStyle"/>
|
||||
</MudElement>
|
||||
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
|
||||
@if (
|
||||
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
|
||||
{
|
||||
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
||||
</MudTooltip>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||
{
|
||||
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||
{
|
||||
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" />
|
||||
</MudToolBar>
|
||||
</FooterContent>
|
||||
</InnerScrolling>
|
||||
</MudStack>
|
||||
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.currentWorkspaceName = name"/>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.currentWorkspaceName = name"/>
|
||||
}
|
||||
|
||||
@if (
|
||||
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
@@ -134,11 +100,11 @@
|
||||
<MudText Typo="Typo.h6" Class="mr-3">
|
||||
Your workspaces
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaceOverlay()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspacesOverlay()"/>
|
||||
</MudStack>
|
||||
</MudDrawerHeader>
|
||||
<MudDrawerContainer Class="ml-6">
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" />
|
||||
</MudDrawerContainer>
|
||||
</MudDrawer>
|
||||
}
|
||||
@@ -1,588 +1,95 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// The chat page.
|
||||
/// </summary>
|
||||
public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||
public partial class Chat : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Chat> Logger { get; init; } = null!;
|
||||
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
private AIStudio.Settings.Provider providerSettings;
|
||||
private Profile currentProfile = Profile.NO_PROFILE;
|
||||
private ChatThread? chatThread;
|
||||
private bool hasUnsavedChanges;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private AIStudio.Settings.Provider providerSettings;
|
||||
private bool workspaceOverlayVisible;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Workspaces? workspaces;
|
||||
private bool mustScrollToBottomAfterRender;
|
||||
private bool mustStoreChat;
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
private byte scrollRenderCountdown;
|
||||
private bool autoSaveEnabled;
|
||||
private double splitterPosition = 30;
|
||||
|
||||
// Unfortunately, we need the input field reference to blur the focus away. Without
|
||||
// this, we cannot clear the input field.
|
||||
private MudTextField<string> inputField = null!;
|
||||
private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
||||
if (deferredContent is not null)
|
||||
this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
|
||||
this.splitterSaveTimer.AutoReset = false;
|
||||
this.splitterSaveTimer.Elapsed += async (_, _) =>
|
||||
{
|
||||
this.chatThread = deferredContent;
|
||||
if (this.chatThread is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.chatThread.Name))
|
||||
{
|
||||
var firstUserBlock = this.chatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
|
||||
if (firstUserBlock is not null)
|
||||
{
|
||||
this.chatThread.Name = firstUserBlock.Content switch
|
||||
{
|
||||
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
|
||||
_ => "Thread"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
this.autoSaveEnabled = true;
|
||||
this.mustStoreChat = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 2;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
this.SettingsManager.ConfigurationData.Workspace.SplitterPosition = this.splitterPosition;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
};
|
||||
|
||||
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
||||
if (deferredLoading != default)
|
||||
{
|
||||
this.loadChat = deferredLoading;
|
||||
this.mustLoadChat = true;
|
||||
}
|
||||
|
||||
this.SelectProviderWhenLoadingChat();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && this.chatThread is not null && this.mustStoreChat)
|
||||
{
|
||||
this.mustStoreChat = false;
|
||||
|
||||
if(this.workspaces is not null)
|
||||
await this.workspaces.StoreChat(this.chatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.chatThread);
|
||||
|
||||
this.currentWorkspaceId = this.chatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
}
|
||||
|
||||
if (firstRender && this.mustLoadChat)
|
||||
{
|
||||
this.mustLoadChat = false;
|
||||
this.chatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
|
||||
|
||||
if(this.chatThread is not null)
|
||||
{
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
this.SelectProviderWhenLoadingChat();
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
if(this.mustScrollToBottomAfterRender)
|
||||
{
|
||||
if (--this.scrollRenderCountdown == 0)
|
||||
{
|
||||
await this.scrollingArea.ScrollToBottom();
|
||||
this.mustScrollToBottomAfterRender = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
|
||||
|
||||
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.providerSettings.InstanceName}', provider '{this.providerSettings.UsedLLMProvider.ToName()}')" : "Select a provider first";
|
||||
|
||||
private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0;
|
||||
|
||||
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
|
||||
|
||||
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.providerSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||
|
||||
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||
|
||||
private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
|
||||
|
||||
private void ProfileWasChanged(Profile profile)
|
||||
{
|
||||
this.currentProfile = profile;
|
||||
if(this.chatThread is null)
|
||||
return;
|
||||
private bool AreWorkspacesVisible => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& ((this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
|
||||
|| this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.SIDEBAR_ALWAYS_VISIBLE);
|
||||
|
||||
this.chatThread = this.chatThread with
|
||||
{
|
||||
SystemPrompt = $"""
|
||||
{SystemPrompts.DEFAULT}
|
||||
|
||||
{this.currentProfile.ToSystemPrompt()}
|
||||
"""
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
// We need to blur the focus away from the input field
|
||||
// to be able to clear the field:
|
||||
await this.inputField.BlurAsync();
|
||||
|
||||
// Create a new chat thread if necessary:
|
||||
var threadName = this.ExtractThreadName(this.userInput);
|
||||
|
||||
if (this.chatThread is null)
|
||||
{
|
||||
this.chatThread = new()
|
||||
{
|
||||
SelectedProvider = this.providerSettings.Id,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = threadName,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = $"""
|
||||
{SystemPrompts.DEFAULT}
|
||||
|
||||
{this.currentProfile.ToSystemPrompt()}
|
||||
""",
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.chatThread.Name))
|
||||
this.chatThread.Name = threadName;
|
||||
}
|
||||
|
||||
//
|
||||
// Add the user message to the thread:
|
||||
//
|
||||
var time = DateTimeOffset.Now;
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.USER,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
},
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
//
|
||||
// Add the AI response to the thread:
|
||||
//
|
||||
var aiText = new ContentText
|
||||
{
|
||||
// We have to wait for the remote
|
||||
// for the content stream:
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
});
|
||||
|
||||
// Clear the input field:
|
||||
this.userInput = string.Empty;
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 2;
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
this.hasUnsavedChanges = true;
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
var isEnter = key is "enter" or "numpadenter";
|
||||
|
||||
// Was a modifier key pressed as well?
|
||||
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
|
||||
|
||||
// Depending on the user's settings, might react to shortcuts:
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
|
||||
{
|
||||
case SendBehavior.ENTER_IS_SENDING:
|
||||
if (!isModifier && isEnter)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
|
||||
case SendBehavior.MODIFER_ENTER_IS_SENDING:
|
||||
if (isEnter && isModifier)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleWorkspaceOverlay()
|
||||
{
|
||||
this.workspaceOverlayVisible = !this.workspaceOverlayVisible;
|
||||
}
|
||||
private bool AreWorkspacesHidden => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
|
||||
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR
|
||||
&& !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
|
||||
|
||||
private async Task ToggleWorkspaceSidebar()
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible = !this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible;
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
{
|
||||
if(this.chatThread is null)
|
||||
return;
|
||||
|
||||
if (!this.CanThreadBeSaved)
|
||||
return;
|
||||
|
||||
//
|
||||
// When the workspace component is visible, we store the chat
|
||||
// through the workspace component. The advantage of this is that
|
||||
// the workspace gets updated automatically when the chat is saved.
|
||||
//
|
||||
if (this.workspaces is not null)
|
||||
await this.workspaces.StoreChat(this.chatThread);
|
||||
else
|
||||
await WorkspaceBehaviour.StoreChat(this.chatThread);
|
||||
|
||||
this.hasUnsavedChanges = false;
|
||||
private void SplitterChanged(double position)
|
||||
{
|
||||
this.splitterPosition = position;
|
||||
this.splitterSaveTimer.Stop();
|
||||
this.splitterSaveTimer.Start();
|
||||
}
|
||||
|
||||
private string ExtractThreadName(string firstUserInput)
|
||||
private void ToggleWorkspacesOverlay()
|
||||
{
|
||||
// We select the first 10 words of the user input:
|
||||
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var threadName = string.Join(' ', words.Take(10));
|
||||
|
||||
// If the thread name is empty, we use a default name:
|
||||
if (string.IsNullOrWhiteSpace(threadName))
|
||||
threadName = "Thread";
|
||||
|
||||
return threadName;
|
||||
}
|
||||
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chatThread is not null && deletePreviousChat)
|
||||
{
|
||||
string chatPath;
|
||||
if (this.chatThread.WorkspaceId == Guid.Empty)
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString());
|
||||
else
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString());
|
||||
|
||||
if(this.workspaces is null)
|
||||
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.chatThread.WorkspaceId, this.chatThread.ChatId, askForConfirmation: false);
|
||||
else
|
||||
await this.workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
}
|
||||
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
|
||||
{
|
||||
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
|
||||
default:
|
||||
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
|
||||
if(this.providerSettings == default)
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!useSameWorkspace)
|
||||
{
|
||||
this.chatThread = null;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chatThread = new()
|
||||
{
|
||||
SelectedProvider = this.providerSettings.Id,
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Empty,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = $"""
|
||||
{SystemPrompts.DEFAULT}
|
||||
|
||||
{this.currentProfile.ToSystemPrompt()}
|
||||
""",
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
|
||||
this.userInput = string.Empty;
|
||||
}
|
||||
|
||||
private async Task MoveChatToWorkspace()
|
||||
{
|
||||
if(this.chatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Please select the workspace where you want to move the chat to." },
|
||||
{ "SelectedWorkspace", this.chatThread?.WorkspaceId },
|
||||
{ "ConfirmText", "Move chat" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = dialogResult.Data is Guid id ? id : default;
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Delete the chat from the current workspace or the temporary storage:
|
||||
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.chatThread!.WorkspaceId, this.chatThread.ChatId, askForConfirmation: false);
|
||||
|
||||
this.chatThread!.WorkspaceId = workspaceId;
|
||||
await this.SaveThread();
|
||||
|
||||
this.currentWorkspaceId = this.chatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
{
|
||||
//
|
||||
// It should not happen that the workspace component is not loaded
|
||||
// because the workspace component is calling this method.
|
||||
//
|
||||
if(this.workspaces is null)
|
||||
return;
|
||||
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty;
|
||||
this.currentWorkspaceName = this.chatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
|
||||
this.SelectProviderWhenLoadingChat();
|
||||
|
||||
this.userInput = string.Empty;
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
{
|
||||
this.mustScrollToBottomAfterRender = true;
|
||||
this.scrollRenderCountdown = 2;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.chatThread = null;
|
||||
}
|
||||
|
||||
private void SelectProviderWhenLoadingChat()
|
||||
{
|
||||
var chatProvider = this.chatThread?.SelectedProvider;
|
||||
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
|
||||
{
|
||||
default:
|
||||
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
|
||||
break;
|
||||
|
||||
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
|
||||
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
|
||||
if(this.providerSettings == default)
|
||||
this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
|
||||
break;
|
||||
}
|
||||
this.workspaceOverlayVisible = !this.workspaceOverlayVisible;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private double ReadSplitterPosition => this.AreWorkspacesHidden ? 6 : this.splitterPosition;
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.RESET_CHAT_STATE:
|
||||
this.ResetState();
|
||||
break;
|
||||
|
||||
case Event.CHAT_STREAMING_DONE:
|
||||
if(this.autoSaveEnabled)
|
||||
await this.SaveThread();
|
||||
case Event.WORKSPACE_TOGGLE_OVERLAY:
|
||||
this.ToggleWorkspacesOverlay();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IAsyncDisposable
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public partial class Home : ComponentBase
|
||||
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
||||
[
|
||||
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
|
||||
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
|
||||
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
|
||||
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
|
||||
new("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
|
||||
new("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
|
||||
|
||||
@@ -327,11 +327,28 @@
|
||||
{
|
||||
<ConfigurationText OptionDescription="Preselect another programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" Icon="@Icons.Material.Filled.Code" Text="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence = selectedValue)"/>
|
||||
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
|
||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
|
||||
</MudPaper>
|
||||
</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">
|
||||
<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."/>
|
||||
|
||||
@@ -183,12 +183,12 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
|
||||
private async Task AddEmbeddingProvider()
|
||||
{
|
||||
var dialogParameters = new DialogParameters<EmbeddingDialog>
|
||||
var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
|
||||
{
|
||||
{ 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;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
@@ -205,7 +205,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
|
||||
private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<EmbeddingDialog>
|
||||
var dialogParameters = new DialogParameters<EmbeddingProviderDialog>
|
||||
{
|
||||
{ x => x.DataNum, embeddingProvider.Num },
|
||||
{ x => x.DataId, embeddingProvider.Id },
|
||||
@@ -218,7 +218,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{ 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;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
@@ -4,16 +4,12 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
public override string Id => LLMProviders.ANTHROPIC.ToName();
|
||||
@@ -21,7 +17,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
||||
public override string InstanceName { get; set; } = "Anthropic";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
@@ -52,86 +48,31 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
||||
}
|
||||
}).ToList()],
|
||||
|
||||
System = chatThread.SystemPrompt,
|
||||
System = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
MaxTokens = 4_096,
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(stream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Check for the end of the stream:
|
||||
if(line.StartsWith("event: message_stop", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
// Ignore any type except "content_block_delta":
|
||||
if(!line.Contains("\"content_block_delta\"", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
ResponseStreamLine anthropicResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
anthropicResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(anthropicResponse == default || string.IsNullOrWhiteSpace(anthropicResponse.Delta.Text))
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return anthropicResponse.Delta.Text;
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Anthropic", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -7,7 +7,14 @@ namespace AIStudio.Provider.Anthropic;
|
||||
/// <param name="Type">The type of the response line.</param>
|
||||
/// <param name="Index">The index of the response line.</param>
|
||||
/// <param name="Delta">The delta of the response line.</param>
|
||||
public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta);
|
||||
public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta) : IResponseStreamLine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this != default && !string.IsNullOrWhiteSpace(this.Delta.Text);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetContent() => this.Delta.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delta object of a response line.
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using RustService = AIStudio.Tools.RustService;
|
||||
|
||||
@@ -28,6 +33,11 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
protected static readonly RustService RUST_SERVICE;
|
||||
|
||||
protected static readonly Encryption ENCRYPTION;
|
||||
|
||||
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the base provider.
|
||||
@@ -51,7 +61,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
public abstract string InstanceName { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, CancellationToken token = default);
|
||||
public abstract IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
|
||||
@@ -74,4 +84,155 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
public string SecretName => this.InstanceName;
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
if(nextResponse.StatusCode is HttpStatusCode.BadRequest)
|
||||
{
|
||||
this.logger.LogError($"Failed request with status code {nextResponse.StatusCode} (message = '{nextResponse.ReasonPhrase}').");
|
||||
errorMessage = nextResponse.ReasonPhrase;
|
||||
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 || !string.IsNullOrWhiteSpace(errorMessage))
|
||||
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
|
||||
|
||||
return new HttpRateLimitedStreamResult(true, false, string.Empty, response);
|
||||
}
|
||||
|
||||
protected async IAsyncEnumerable<string> StreamChatCompletionInternal<T>(string providerName, Func<Task<HttpRequestMessage>> requestBuilder, [EnumeratorCancellation] CancellationToken token = default) where T : struct, IResponseStreamLine
|
||||
{
|
||||
StreamReader? streamReader = null;
|
||||
try
|
||||
{
|
||||
// Send the request using exponential backoff:
|
||||
var responseData = await this.SendRequest(requestBuilder, token);
|
||||
if(responseData.IsFailedAfterAllRetries)
|
||||
{
|
||||
this.logger.LogError($"The {providerName} chat completion failed: {responseData.ErrorMessage}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Open the response stream:
|
||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
streamReader = new StreamReader(providerStream);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
|
||||
if (streamReader is null)
|
||||
yield break;
|
||||
|
||||
// Read the stream, line by line:
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(streamReader.EndOfStream)
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogWarning($"Failed to read the end-of-stream state from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if the token is canceled:
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
this.logger.LogWarning($"The user canceled the chat completion for {providerName} '{this.InstanceName}'.");
|
||||
streamReader.Close();
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Read the next line:
|
||||
string? line;
|
||||
try
|
||||
{
|
||||
line = await streamReader.ReadLineAsync(token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip empty lines:
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
|
||||
T providerResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
providerResponse = JsonSerializer.Deserialize<T>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if (!providerResponse.ContainsContent())
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return providerResponse.GetContent();
|
||||
}
|
||||
|
||||
streamReader.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,12 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -23,7 +19,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
|
||||
public override string InstanceName { get; set; } = "Fireworks.ai";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
@@ -34,7 +30,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the Fireworks HTTP chat request:
|
||||
@@ -68,72 +64,21 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var fireworksStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(fireworksStream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
ResponseStreamLine fireworksResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
fireworksResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(fireworksResponse == default || fireworksResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return fireworksResponse.Choices[0].Delta.Content;
|
||||
// Set the content:
|
||||
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Fireworks", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -8,7 +8,14 @@ namespace AIStudio.Provider.Fireworks;
|
||||
/// <param name="Created">The timestamp of the response.</param>
|
||||
/// <param name="Model">The model used for the response.</param>
|
||||
/// <param name="Choices">The choices made by the AI.</param>
|
||||
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, IList<Choice> Choices);
|
||||
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, IList<Choice> Choices) : IResponseStreamLine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetContent() => this.Choices[0].Delta.Content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a choice made by the AI.
|
||||
|
||||
@@ -5,16 +5,12 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Google;
|
||||
|
||||
public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativelanguage.googleapis.com/v1beta/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -24,7 +20,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
||||
public override string InstanceName { get; set; } = "Google Gemini";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
@@ -35,7 +31,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the Google HTTP chat request:
|
||||
@@ -69,72 +65,21 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var geminiStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(geminiStream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
ResponseStreamLine geminiResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
geminiResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(geminiResponse == default || geminiResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return geminiResponse.Choices[0].Delta.Content;
|
||||
// Set the content:
|
||||
request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Google", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -5,16 +5,12 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Groq;
|
||||
|
||||
public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/openai/v1/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -24,7 +20,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
||||
public override string InstanceName { get; set; } = "Groq";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
@@ -35,7 +31,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
@@ -71,72 +67,21 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var groqStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(groqStream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
ResponseStreamLine groqResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
groqResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(groqResponse == default || groqResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return groqResponse.Choices[0].Delta.Content;
|
||||
// Set the content:
|
||||
request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Groq", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
@@ -23,9 +24,10 @@ public interface IProvider
|
||||
/// </summary>
|
||||
/// <param name="chatModel">The model to use for chat completion.</param>
|
||||
/// <param name="chatThread">The chat thread to continue.</param>
|
||||
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The chat completion stream.</returns>
|
||||
public IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, CancellationToken token = default);
|
||||
public IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Starts an image completion stream.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public interface IResponseStreamLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the response line contains any content.
|
||||
/// </summary>
|
||||
/// <returns>True when the response line contains content, false otherwise.</returns>
|
||||
public bool ContainsContent();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content of the response line.
|
||||
/// </summary>
|
||||
/// <returns>The content of the response line.</returns>
|
||||
public string GetContent();
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public enum LLMProviders
|
||||
ANTHROPIC = 2,
|
||||
MISTRAL = 3,
|
||||
GOOGLE = 7,
|
||||
X = 8,
|
||||
|
||||
FIREWORKS = 5,
|
||||
GROQ = 6,
|
||||
|
||||
@@ -5,6 +5,7 @@ using AIStudio.Provider.Groq;
|
||||
using AIStudio.Provider.Mistral;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
using AIStudio.Provider.X;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
@@ -26,6 +27,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => "Anthropic",
|
||||
LLMProviders.MISTRAL => "Mistral",
|
||||
LLMProviders.GOOGLE => "Google",
|
||||
LLMProviders.X => "xAI",
|
||||
|
||||
LLMProviders.GROQ => "Groq",
|
||||
LLMProviders.FIREWORKS => "Fireworks.ai",
|
||||
@@ -62,6 +64,8 @@ public static class LLMProvidersExtensions
|
||||
|
||||
LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
@@ -87,6 +91,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GROQ => false,
|
||||
LLMProviders.ANTHROPIC => false,
|
||||
LLMProviders.FIREWORKS => false,
|
||||
LLMProviders.X => false,
|
||||
|
||||
//
|
||||
// Self-hosted providers are treated as a special case anyway.
|
||||
@@ -128,6 +133,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = instanceName },
|
||||
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = instanceName },
|
||||
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
|
||||
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
|
||||
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
|
||||
@@ -150,6 +156,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => "https://console.mistral.ai/",
|
||||
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
|
||||
LLMProviders.X => "https://accounts.x.ai/sign-up",
|
||||
|
||||
LLMProviders.GROQ => "https://console.groq.com/",
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
|
||||
@@ -162,6 +169,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.OPEN_AI => "https://platform.openai.com/usage",
|
||||
LLMProviders.MISTRAL => "https://console.mistral.ai/usage/",
|
||||
LLMProviders.ANTHROPIC => "https://console.anthropic.com/settings/plans",
|
||||
LLMProviders.X => "https://console.x.ai/",
|
||||
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
|
||||
@@ -174,6 +182,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.OPEN_AI => true,
|
||||
LLMProviders.MISTRAL => true,
|
||||
LLMProviders.ANTHROPIC => true,
|
||||
LLMProviders.X => true,
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
@@ -217,6 +226,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => true,
|
||||
LLMProviders.ANTHROPIC => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
@@ -232,6 +242,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => true,
|
||||
LLMProviders.ANTHROPIC => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
|
||||
@@ -5,16 +5,12 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Mistral;
|
||||
|
||||
public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
public override string Id => LLMProviders.MISTRAL.ToName();
|
||||
@@ -22,7 +18,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
||||
public override string InstanceName { get; set; } = "Mistral";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
@@ -33,7 +29,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
||||
var systemPrompt = new RegularMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the Mistral HTTP chat request:
|
||||
@@ -70,72 +66,21 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
||||
SafePrompt = false,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var mistralStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(mistralStream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
ResponseStreamLine mistralResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
mistralResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(mistralResponse == default || mistralResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return mistralResponse.Choices[0].Delta.Content;
|
||||
// Set the content:
|
||||
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Mistral", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
@@ -18,7 +19,7 @@ public class NoProvider : IProvider
|
||||
|
||||
public Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
|
||||
|
||||
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
await Task.FromResult(0);
|
||||
yield break;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
@@ -12,11 +13,6 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// </summary>
|
||||
public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -26,18 +22,43 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
public override string InstanceName { get; set; } = "OpenAI";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Unfortunately, OpenAI changed the name of the system prompt based on the model.
|
||||
// All models that start with "o" (the omni aka reasoning models) and all GPT4o models
|
||||
// have the system prompt named "developer". All other models have the system prompt
|
||||
// named "system". We need to check this to get the correct system prompt.
|
||||
//
|
||||
// To complicate it even more: The early versions of reasoning models, which are released
|
||||
// before the 17th of December 2024, have no system prompt at all. We need to check this
|
||||
// as well.
|
||||
|
||||
// Apply the basic rule first:
|
||||
var systemPromptRole = chatModel.Id.StartsWith('o') || chatModel.Id.Contains("4o") ? "developer" : "system";
|
||||
|
||||
// Check if the model is an early version of the reasoning models:
|
||||
systemPromptRole = chatModel.Id switch
|
||||
{
|
||||
"o1-mini" => "user",
|
||||
"o1-mini-2024-09-12" => "user",
|
||||
"o1-preview" => "user",
|
||||
"o1-preview-2024-09-12" => "user",
|
||||
|
||||
_ => systemPromptRole,
|
||||
};
|
||||
|
||||
this.logger.LogInformation($"Using the system prompt role '{systemPromptRole}' for model '{chatModel.Id}'.");
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Role = systemPromptRole,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
@@ -55,7 +76,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
@@ -71,75 +92,23 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
FrequencyPenalty = 0f,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var openAIStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(openAIStream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
ResponseStreamLine openAIResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
openAIResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(openAIResponse == default || openAIResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return openAIResponse.Choices[0].Delta.Content;
|
||||
// Set the content:
|
||||
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("OpenAI", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -9,7 +9,14 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// <param name="Model">The model used for the response.</param>
|
||||
/// <param name="SystemFingerprint">The system fingerprint; together with the seed, this allows you to reproduce the response.</param>
|
||||
/// <param name="Choices">The choices made by the AI.</param>
|
||||
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<Choice> Choices);
|
||||
public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<Choice> Choices) : IResponseStreamLine
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this != default && this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetContent() => this.Choices[0].Delta.Content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data model for a choice made by the AI.
|
||||
|
||||
@@ -5,16 +5,12 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.SelfHosted;
|
||||
|
||||
public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostname) : BaseProvider($"{hostname}{host.BaseURL()}", logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
public override string Id => LLMProviders.SELF_HOSTED.ToName();
|
||||
@@ -22,7 +18,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
||||
public override string InstanceName { get; set; } = "Self-hosted";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true);
|
||||
@@ -31,7 +27,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.SystemPrompt,
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the OpenAI HTTP chat request:
|
||||
@@ -66,8 +62,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
||||
MaxTokens = -1,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
StreamReader? streamReader = default;
|
||||
try
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL());
|
||||
@@ -78,72 +73,11 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// 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 response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var providerStream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
streamReader = new StreamReader(providerStream);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
this.logger.LogError($"Failed to stream chat completion from self-hosted provider '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
|
||||
if (streamReader is not null)
|
||||
{
|
||||
// Read the stream, line by line:
|
||||
while (!streamReader.EndOfStream)
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if (token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
|
||||
ResponseStreamLine providerResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
providerResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if (providerResponse == default || providerResponse.Choices.Count == 0)
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return providerResponse.Choices[0].Delta.Content;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("self-hosted provider", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.X;
|
||||
|
||||
public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v1/", logger)
|
||||
{
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.X.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "xAI";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the system prompt:
|
||||
var systemPrompt = new Message
|
||||
{
|
||||
Role = "system",
|
||||
Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger),
|
||||
};
|
||||
|
||||
// Prepare the xAI HTTP chat request:
|
||||
var xChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
// - First of all the system prompt
|
||||
// - Then none-empty user and AI messages
|
||||
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
|
||||
Seed = chatThread.Seed,
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
async Task<HttpRequestMessage> RequestBuilder()
|
||||
{
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(xChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("xAI", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(["grok-"], token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<Model>>([]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<Model>>([]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> LoadModels(string[] prefixes, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
_ => await RUST_SERVICE.GetAPIKey(this) switch
|
||||
{
|
||||
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
|
||||
_ => null,
|
||||
}
|
||||
};
|
||||
|
||||
if (secretKey is null)
|
||||
return [];
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
var response = await this.httpClient.SendAsync(request, token);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
|
||||
//
|
||||
// The API does not return the alias model names, so we have to add them manually:
|
||||
// Right now, the only alias to add is `grok-2-latest`.
|
||||
//
|
||||
return modelResponse.Data.Where(model => prefixes.Any(prefix => model.Id.StartsWith(prefix, StringComparison.InvariantCulture)))
|
||||
.Concat([
|
||||
new Model
|
||||
{
|
||||
Id = "grok-2-latest",
|
||||
DisplayName = "Grok 2.0 (latest)",
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,6 @@ public sealed partial class Routes
|
||||
public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks";
|
||||
public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting";
|
||||
public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day";
|
||||
public const string ASSISTANT_ERI = "/assistant/eri";
|
||||
// ReSharper restore InconsistentNaming
|
||||
}
|
||||
@@ -57,6 +57,8 @@ public sealed class Data
|
||||
public DataTranslation Translation { get; init; } = new();
|
||||
|
||||
public DataCoding Coding { get; init; } = new();
|
||||
|
||||
public DataERI ERI { get; init; } = new();
|
||||
|
||||
public DataTextSummarizer TextSummarizer { get; init; } = new();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -21,4 +21,9 @@ public sealed class DataWorkspace
|
||||
/// Indicates whether the sidebar is currently visible.
|
||||
/// </summary>
|
||||
public bool IsSidebarVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the splitter between the chat and the workspaces.
|
||||
/// </summary>
|
||||
public double SplitterPosition { get; set; } = 30;
|
||||
}
|
||||
@@ -118,8 +118,10 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
}
|
||||
|
||||
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
|
||||
await File.WriteAllTextAsync(settingsPath, settingsJson);
|
||||
var tempFile = Path.GetTempFileName();
|
||||
await File.WriteAllTextAsync(tempFile, settingsJson);
|
||||
|
||||
File.Move(tempFile, settingsPath, true);
|
||||
this.logger.LogInformation("Stored the settings to the file system.");
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public enum Components
|
||||
MY_TASKS_ASSISTANT,
|
||||
JOB_POSTING_ASSISTANT,
|
||||
BIAS_DAY_ASSISTANT,
|
||||
ERI_ASSISTANT,
|
||||
|
||||
CHAT,
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public static class ComponentsExtensions
|
||||
public static bool AllowSendTo(this Components component) => component switch
|
||||
{
|
||||
Components.NONE => false,
|
||||
Components.ERI_ASSISTANT => false,
|
||||
Components.BIAS_DAY_ASSISTANT => false,
|
||||
|
||||
_ => true,
|
||||
@@ -27,6 +28,7 @@ public static class ComponentsExtensions
|
||||
Components.SYNONYMS_ASSISTANT => "Synonym Assistant",
|
||||
Components.MY_TASKS_ASSISTANT => "My Tasks Assistant",
|
||||
Components.JOB_POSTING_ASSISTANT => "Job Posting Assistant",
|
||||
Components.ERI_ASSISTANT => "ERI Server",
|
||||
|
||||
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.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.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : 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.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.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,
|
||||
|
||||
@@ -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.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.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,
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ public enum Event
|
||||
LOAD_CHAT,
|
||||
CHAT_STREAMING_DONE,
|
||||
|
||||
// Workspace events:
|
||||
WORKSPACE_LOADED_CHAT_CHANGED,
|
||||
WORKSPACE_TOGGLE_OVERLAY,
|
||||
|
||||
// Send events:
|
||||
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
||||
SEND_TO_ICON_FINDER_ASSISTANT,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -322,6 +322,19 @@ public sealed class RustService : IDisposable
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -117,4 +117,20 @@ public static class WorkspaceBehaviour
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -196,7 +196,7 @@ def update_dotnet_version []: nothing -> nothing {
|
||||
mut dotnet_sdk_version = $meta_lines.3
|
||||
mut dotnet_version = $meta_lines.4
|
||||
|
||||
let dotnet_data = (^dotnet --info) | collect | parse --regex '(?ms).?NET SDK:\s+Version:\s+(?P<sdkVersion>[0-9.]+).+Commit:\s+(?P<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?P<hostVersion>[0-9.]+).+Commit:\s+(?P<hostCommit>[a-zA-Z0-9]+)'
|
||||
let dotnet_data = (^dotnet --info) | collect | parse --regex '(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?P<sdkVersion>[0-9.]+).+Commit:\s+(?P<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?P<hostVersion>[0-9.]+).+Commit:\s+(?P<hostCommit>[a-zA-Z0-9]+)'
|
||||
let sdk_version = $dotnet_data.sdkVersion.0
|
||||
let host_version = $dotnet_data.hostVersion.0
|
||||
let sdkCommit = $dotnet_data.sdkCommit.0
|
||||
|
||||
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user