mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-20 14:17:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a66d88c989 | ||
|
|
2e84f10a97 | ||
|
|
dbc2b4f6bc | ||
|
|
9d2b63bbaa | ||
|
|
277dc73468 | ||
|
|
ccac2b04ac | ||
|
|
0c4b94c527 | ||
|
|
37a9046be4 | ||
|
|
b984b4432e | ||
|
|
5e3b632eee | ||
|
|
99dac520cb | ||
|
|
e21e30ce8c | ||
|
|
5a077e1bdb | ||
|
|
8dd5b3bab8 | ||
|
|
03431b85b4 | ||
|
|
9699754ec7 | ||
|
|
64656d3605 | ||
|
|
9bd79bd3a0 | ||
|
|
bfc9f2ea1d | ||
|
|
96e6372fcd | ||
|
|
f01cf498e2 | ||
|
|
77d427610b | ||
|
|
7c59aa11fe | ||
|
|
98810ce884 | ||
|
|
657fda4961 | ||
|
|
f4780939fc | ||
|
|
954cf44939 | ||
|
|
e973b51f75 | ||
|
|
c9474c55b7 | ||
|
|
c0b7016c3a | ||
|
|
78315195cb | ||
|
|
6040e88dad | ||
|
|
ad3d8cbb3a | ||
|
|
c836bd7f33 |
No files matched your search
+4
-4
@@ -2,13 +2,13 @@
|
|||||||
* @MindWorkAI/maintainer
|
* @MindWorkAI/maintainer
|
||||||
|
|
||||||
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
||||||
/.github/ @MindWorkAI/release
|
/.github/ @MindWorkAI/release @SommerEngineering
|
||||||
|
|
||||||
# The release team is responsible for the update directory:
|
|
||||||
/.updates/ @MindWorkAI/release
|
|
||||||
|
|
||||||
# Our Rust experts are responsible for the Rust codebase:
|
# Our Rust experts are responsible for the Rust codebase:
|
||||||
/runtime/ @MindWorkAI/rust-experts
|
/runtime/ @MindWorkAI/rust-experts
|
||||||
|
|
||||||
# Our .NET experts are responsible for the .NET codebase:
|
# Our .NET experts are responsible for the .NET codebase:
|
||||||
/app/ @MindWorkAI/net-experts
|
/app/ @MindWorkAI/net-experts
|
||||||
|
|
||||||
|
# The source code rules must be reviewed by the release team:
|
||||||
|
/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering
|
||||||
@@ -12,16 +12,16 @@ Things we are currently working on:
|
|||||||
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
|
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
|
||||||
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
|
||||||
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259))~~
|
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||||
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
|
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
|
||||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||||
- [ ] App: Implement external embedding providers
|
- [ ] App: Implement external embedding providers
|
||||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||||
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
|
||||||
- [ ] App: Implement the continuous process of vectorizing data
|
- [ ] App: Implement the continuous process of vectorizing data
|
||||||
- [ ] App: Define a common retrieval context interface for the integration of RAG processes in chats
|
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
|
||||||
- [ ] App: Define a common augmentation interface for the integration of RAG processes in chats
|
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
|
||||||
- [ ] App: Integrate data sources in chats
|
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||||
|
|
||||||
|
|
||||||
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
|
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
|
||||||
@@ -56,7 +56,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
|||||||
|
|
||||||
**Key advantages:**
|
**Key advantages:**
|
||||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
- **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, o1, 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/).
|
- **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, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, 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/). For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
|
||||||
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
||||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
- **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.
|
- **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.
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,10 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -2,9 +2,7 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ERIClients", "ERIClients", "{5C2AF789-287B-4FCB-B675-7273D8CD4579}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERIClientV1", "ERIClientV1\ERIClientV1.csproj", "{9E35A273-0FA6-4BD5-8880-A1DDAC106926}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -16,12 +14,11 @@ Global
|
|||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{9E35A273-0FA6-4BD5-8880-A1DDAC106926} = {5C2AF789-287B-4FCB-B675-7273D8CD4579}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
@@ -2,9 +2,15 @@
|
|||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=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/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
@@ -1,13 +1,31 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
// ReSharper disable MemberCanBePrivate.Global
|
||||||
|
|
||||||
namespace AIStudio.Agents;
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : IAgent
|
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : IAgent
|
||||||
{
|
{
|
||||||
|
protected static readonly ContentBlock EMPTY_BLOCK = new()
|
||||||
|
{
|
||||||
|
Content = null,
|
||||||
|
ContentType = ContentType.NONE,
|
||||||
|
Role = ChatRole.AGENT,
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected DataSourceService DataSourceService { get; init; } = dataSourceService;
|
||||||
|
|
||||||
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||||
|
|
||||||
protected ThreadSafeRandom RNG { get; init; } = rng;
|
protected ThreadSafeRandom RNG { get; init; } = rng;
|
||||||
@@ -60,24 +78,30 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
|||||||
Blocks = [],
|
Blocks = [],
|
||||||
};
|
};
|
||||||
|
|
||||||
protected DateTimeOffset AddUserRequest(ChatThread thread, string request)
|
protected UserRequest AddUserRequest(ChatThread thread, string request)
|
||||||
{
|
{
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
var lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = request,
|
||||||
|
};
|
||||||
|
|
||||||
thread.Blocks.Add(new ContentBlock
|
thread.Blocks.Add(new ContentBlock
|
||||||
{
|
{
|
||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = lastUserPrompt,
|
||||||
{
|
|
||||||
Text = request,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return time;
|
return new()
|
||||||
|
{
|
||||||
|
Time = time,
|
||||||
|
UserPrompt = lastUserPrompt,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task AddAIResponseAsync(ChatThread thread, DateTimeOffset time)
|
protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time)
|
||||||
{
|
{
|
||||||
if(this.ProviderSettings is null)
|
if(this.ProviderSettings is null)
|
||||||
return;
|
return;
|
||||||
@@ -103,6 +127,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.SettingsManager, providerSettings.Model, thread);
|
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), providerSettings.Model, lastUserPrompt, thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
|
||||||
|
{
|
||||||
|
private readonly List<ContentBlock> answers = new();
|
||||||
|
|
||||||
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Type Type => Type.SYSTEM;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "Data Source Selection";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string JobDescription =>
|
||||||
|
"""
|
||||||
|
You receive a system and a user prompt, as well as a list of possible data sources as input.
|
||||||
|
Your task is to select the appropriate data sources for the given task. You may choose none,
|
||||||
|
one, or multiple sources, depending on what best fits the system and user prompt. You need
|
||||||
|
to estimate and assess which source, based on its description, might be helpful in
|
||||||
|
processing the prompts.
|
||||||
|
|
||||||
|
Your response is a JSON list in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.87},
|
||||||
|
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.54}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
|
||||||
|
1.0 (you are absolutely certain that this source is needed).
|
||||||
|
|
||||||
|
The JSON schema is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"reason",
|
||||||
|
"confidence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When no data source is needed, you return an empty JSON list `[]`. You do not ask any
|
||||||
|
follow-up questions. You do not address the user. Your response consists solely of
|
||||||
|
the JSON list.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string SystemPrompt(string availableDataSources) => $"""
|
||||||
|
{this.JobDescription}
|
||||||
|
|
||||||
|
{availableDataSources}
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source selection agent does not work with context. Use
|
||||||
|
/// the process input method instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The chat thread without any changes.</returns>
|
||||||
|
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
|
||||||
|
{
|
||||||
|
if (input.Content is not ContentText text)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(text.InitialRemoteWait || text.IsStreaming)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(text.Text))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(!additionalData.TryGetValue("availableDataSources", out var availableDataSources) || string.IsNullOrWhiteSpace(availableDataSources))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
var thread = this.CreateChatThread(this.SystemPrompt(availableDataSources));
|
||||||
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
|
var answer = thread.Blocks[^1];
|
||||||
|
|
||||||
|
this.answers.Add(answer);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
|
||||||
|
|
||||||
|
// <inheritdoc />
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetAnswers() => this.answers;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
logger.LogInformation("The AI should select the appropriate data sources.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Which LLM provider should the agent use?
|
||||||
|
//
|
||||||
|
|
||||||
|
// We start with the provider currently selected by the user:
|
||||||
|
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true);
|
||||||
|
|
||||||
|
// Assign the provider settings to the agent:
|
||||||
|
logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
|
||||||
|
this.ProviderSettings = agentProvider;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Prepare the current system and user prompts as input for the agent:
|
||||||
|
//
|
||||||
|
var lastPromptContent = lastPrompt switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
|
||||||
|
// Image prompts may be empty, e.g., when the image is too large:
|
||||||
|
ContentImage image => await image.AsBase64(token),
|
||||||
|
|
||||||
|
// Other content types are not supported yet:
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lastPromptContent))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The last prompt is empty. The AI cannot select data sources.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Prepare the allowed data sources as input for the agent:
|
||||||
|
//
|
||||||
|
var additionalData = new Dictionary<string, string>();
|
||||||
|
logger.LogInformation("Preparing the list of allowed data sources for the agent to choose from.");
|
||||||
|
|
||||||
|
// Notice: We do not dispose the Rust service here. The Rust service is a singleton
|
||||||
|
// and will be disposed when the application shuts down:
|
||||||
|
var rustService = Program.SERVICE_PROVIDER.GetService<RustService>()!;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("The following data sources are available for selection:");
|
||||||
|
foreach (var ds in dataSources.AllowedDataSources)
|
||||||
|
{
|
||||||
|
switch (ds)
|
||||||
|
{
|
||||||
|
case DataSourceLocalDirectory localDirectory:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSourceLocalFile localFile:
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IERIDataSource eriDataSource:
|
||||||
|
var eriServerDescription = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Call the ERI server to get the server description:
|
||||||
|
//
|
||||||
|
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
|
||||||
|
var authResponse = await eriClient.AuthenticateAsync(eriDataSource, rustService, token);
|
||||||
|
if (authResponse.Successful)
|
||||||
|
{
|
||||||
|
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
|
||||||
|
if (serverDescriptionResponse.Successful)
|
||||||
|
{
|
||||||
|
eriServerDescription = serverDescriptionResponse.Data.Description;
|
||||||
|
|
||||||
|
// Remove all line breaks from the description:
|
||||||
|
eriServerDescription = eriServerDescription.Replace("\n", " ").Replace("\r", " ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to retrieve the server description from the ERI data source '{eriDataSource.Name}'. Message: {serverDescriptionResponse.Message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning($"Was not able to authenticate with the ERI data source '{eriDataSource.Name}'. Message: {authResponse.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogWarning($"The ERI data source '{eriDataSource.Name}' is not available. Thus, we cannot retrieve the server description. Error: {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Append the ERI data source to the list. Use the server description if available:
|
||||||
|
//
|
||||||
|
if (string.IsNullOrWhiteSpace(eriServerDescription))
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source");
|
||||||
|
else
|
||||||
|
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source, description='{eriServerDescription}'");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Prepared the list of allowed data sources for the agent.");
|
||||||
|
additionalData.Add("availableDataSources", sb.ToString());
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4. Let the agent select the data sources:
|
||||||
|
//
|
||||||
|
var prompt = $"""
|
||||||
|
The system prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{lastPromptContent}
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Call the agent:
|
||||||
|
var aiResponse = await this.ProcessInput(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = new ContentText
|
||||||
|
{
|
||||||
|
Text = prompt,
|
||||||
|
},
|
||||||
|
}, additionalData);
|
||||||
|
|
||||||
|
if(aiResponse.Content is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aiResponse)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// 5. Parse the agent response:
|
||||||
|
//
|
||||||
|
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// What we expect is a JSON list of SelectedDataSource objects:
|
||||||
|
//
|
||||||
|
var selectedDataSourcesJson = textContent.Text;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We know how bad LLM may be in generating JSON without surrounding text.
|
||||||
|
// Thus, we expect the worst and try to extract the JSON list from the text:
|
||||||
|
//
|
||||||
|
var json = ExtractJson(selectedDataSourcesJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var aiSelectedDataSources = JsonSerializer.Deserialize<List<SelectedDataSource>>(json, JSON_SERIALIZER_OPTIONS);
|
||||||
|
return aiSelectedDataSources ?? [];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case { ContentType: ContentType.TEXT }:
|
||||||
|
logger.LogWarning("The agent answered with an unexpected inner content type.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
case { ContentType: ContentType.NONE }:
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return [];
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the JSON list from the given text. The text may contain additional
|
||||||
|
/// information around the JSON list. The method tries to extract the JSON list
|
||||||
|
/// from the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Algorithm: The method searches for the first line that contains only a '[' character.
|
||||||
|
/// Then, it searches for the first line that contains only a ']' character. The method
|
||||||
|
/// returns the text between these two lines (including the brackets). When the method
|
||||||
|
/// cannot find the JSON list, it returns an empty string.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// This overload is using strings instead of spans. We can use this overload in any
|
||||||
|
/// async method. Thus, it is a wrapper around the span-based method. Yes, we are losing
|
||||||
|
/// the memory efficiency of the span-based method, but we still gain the performance
|
||||||
|
/// of the span-based method: the entire search algorithm is span-based.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="text">The text that may contain the JSON list.</param>
|
||||||
|
/// <returns>The extracted JSON list.</returns>
|
||||||
|
private static string ExtractJson(string text) => ExtractJson(text.AsSpan()).ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the JSON list from the given text. The text may contain additional
|
||||||
|
/// information around the JSON list. The method tries to extract the JSON list
|
||||||
|
/// from the text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Algorithm: The method searches for the first line that contains only a '[' character.
|
||||||
|
/// Then, it searches for the first line that contains only a ']' character. The method
|
||||||
|
/// returns the text between these two lines (including the brackets). When the method
|
||||||
|
/// cannot find the JSON list, it returns an empty string.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="text">The text that may contain the JSON list.</param>
|
||||||
|
/// <returns>The extracted JSON list.</returns>
|
||||||
|
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> text)
|
||||||
|
{
|
||||||
|
var startIndex = -1;
|
||||||
|
var endIndex = -1;
|
||||||
|
var foundStart = false;
|
||||||
|
var foundEnd = false;
|
||||||
|
var lineStart = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i <= text.Length; i++)
|
||||||
|
{
|
||||||
|
// Handle the end of the line or the end of the text:
|
||||||
|
if (i == text.Length || text[i] == '\n')
|
||||||
|
{
|
||||||
|
if (IsCharacterAloneInLine(text, lineStart, i, '[') && !foundStart)
|
||||||
|
{
|
||||||
|
startIndex = lineStart;
|
||||||
|
foundStart = true;
|
||||||
|
}
|
||||||
|
else if (IsCharacterAloneInLine(text, lineStart, i, ']') && foundStart && !foundEnd)
|
||||||
|
{
|
||||||
|
endIndex = i;
|
||||||
|
foundEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart && foundEnd)
|
||||||
|
{
|
||||||
|
// Adjust endIndex for slicing, ensuring it's within bounds:
|
||||||
|
return text.Slice(startIndex, Math.Min(text.Length, endIndex + 1) - startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadOnlySpan<char>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCharacterAloneInLine(ReadOnlySpan<char> text, int lineStart, int lineEnd, char character)
|
||||||
|
{
|
||||||
|
for (var i = lineStart; i < lineEnd; i++)
|
||||||
|
if (!char.IsWhiteSpace(text[i]) && text[i] != character)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.RAG;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalContextValidation> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
|
||||||
|
{
|
||||||
|
#region Overrides of AgentBase
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Type Type => Type.WORKER;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "Retrieval Context Validation";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string JobDescription =>
|
||||||
|
"""
|
||||||
|
You receive a system and user prompt as well as a retrieval context as input. Your task is to decide whether this
|
||||||
|
retrieval context is helpful in processing the prompts or not. You respond with the decision (true or false),
|
||||||
|
your reasoning, and your confidence in this decision.
|
||||||
|
|
||||||
|
Your response is only one JSON object in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"decision": true, "reason": "Why did you choose this source?", "confidence": 0.87}
|
||||||
|
```
|
||||||
|
|
||||||
|
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
|
||||||
|
1.0 (you are absolutely certain that this retrieval context is needed).
|
||||||
|
|
||||||
|
The JSON schema is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"decision": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"decision",
|
||||||
|
"reason",
|
||||||
|
"confidence"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You do not ask any follow-up questions. You do not address the user. Your response consists solely of
|
||||||
|
that one JSON object.
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string SystemPrompt(string retrievalContext) => $"""
|
||||||
|
{this.JobDescription}
|
||||||
|
|
||||||
|
{retrievalContext}
|
||||||
|
""";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Settings.Provider? ProviderSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The retrieval context validation agent does not work with context. Use
|
||||||
|
/// the process input method instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The chat thread without any changes.</returns>
|
||||||
|
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
|
||||||
|
{
|
||||||
|
if (input.Content is not ContentText text)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(text.InitialRemoteWait || text.IsStreaming)
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(text.Text))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
if(!additionalData.TryGetValue("retrievalContext", out var retrievalContext) || string.IsNullOrWhiteSpace(retrievalContext))
|
||||||
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
|
var thread = this.CreateChatThread(this.SystemPrompt(retrievalContext));
|
||||||
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
|
return thread.Blocks[^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We do not provide any context. This agent will process many retrieval contexts.
|
||||||
|
/// This would block a huge amount of memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An empty list.</returns>
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We do not provide any answers. This agent will process many retrieval contexts.
|
||||||
|
/// This would block a huge amount of memory.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An empty list.</returns>
|
||||||
|
public override IReadOnlyCollection<ContentBlock> GetAnswers() => [];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the LLM provider for the agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When you have to call the validation in parallel for many retrieval contexts,
|
||||||
|
/// you can set the provider once and then call the validation method in parallel.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="provider">The current LLM provider. When the user doesn't preselect an agent provider, the agent uses this provider.</param>
|
||||||
|
public void SetLLMProvider(IProvider provider)
|
||||||
|
{
|
||||||
|
// We start with the provider currently selected by the user:
|
||||||
|
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, provider.Id, true);
|
||||||
|
|
||||||
|
// Assign the provider settings to the agent:
|
||||||
|
logger.LogInformation($"The agent for the retrieval context validation uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
|
||||||
|
this.ProviderSettings = agentProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate all retrieval contexts against the last user and the system prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastPrompt">The last user prompt.</param>
|
||||||
|
/// <param name="chatThread">The chat thread.</param>
|
||||||
|
/// <param name="retrievalContexts">All retrieval contexts to validate.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The validation results.</returns>
|
||||||
|
public async Task<IReadOnlyList<RetrievalContextValidationResult>> ValidateRetrievalContextsAsync(IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
// Check if the retrieval context validation is enabled:
|
||||||
|
if (!this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
logger.LogInformation($"Validating {retrievalContexts.Count:###,###,###,###} retrieval contexts.");
|
||||||
|
|
||||||
|
// Prepare the list of validation tasks:
|
||||||
|
var validationTasks = new List<Task<RetrievalContextValidationResult>>(retrievalContexts.Count);
|
||||||
|
|
||||||
|
// Read the number of parallel validations:
|
||||||
|
var numParallelValidations = 3;
|
||||||
|
if(this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions)
|
||||||
|
numParallelValidations = this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.NumParallelValidations;
|
||||||
|
|
||||||
|
numParallelValidations = Math.Max(1, numParallelValidations);
|
||||||
|
|
||||||
|
// Use a semaphore to limit the number of parallel validations:
|
||||||
|
using var semaphore = new SemaphoreSlim(numParallelValidations);
|
||||||
|
foreach (var retrievalContext in retrievalContexts)
|
||||||
|
{
|
||||||
|
// Wait for an available slot in the semaphore:
|
||||||
|
await semaphore.WaitAsync(token);
|
||||||
|
|
||||||
|
// Start the next validation task:
|
||||||
|
validationTasks.Add(this.ValidateRetrievalContextAsync(lastPrompt, chatThread, retrievalContext, token, semaphore));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all validation tasks to complete:
|
||||||
|
return await Task.WhenAll(validationTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the retrieval context against the last user and the system prompt.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Probably, you have a lot of retrieval contexts to validate. In this case, you
|
||||||
|
/// can call this method in parallel for each retrieval context. You might use
|
||||||
|
/// the ValidateRetrievalContextsAsync method to validate all retrieval contexts.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="lastPrompt">The last user prompt.</param>
|
||||||
|
/// <param name="chatThread">The chat thread.</param>
|
||||||
|
/// <param name="retrievalContext">The retrieval context to validate.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <param name="semaphore">The optional semaphore to limit the number of parallel validations.</param>
|
||||||
|
/// <returns>The validation result.</returns>
|
||||||
|
public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsync(IContent lastPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Check if the validation was canceled. This could happen when the user
|
||||||
|
// canceled the validation process or when the validation process took
|
||||||
|
// too long:
|
||||||
|
//
|
||||||
|
if(token.IsCancellationRequested)
|
||||||
|
return new(false, "The validation was canceled.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Prepare the current system and user prompts as input for the agent:
|
||||||
|
//
|
||||||
|
var lastPromptContent = lastPrompt switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
|
||||||
|
// Image prompts may be empty, e.g., when the image is too large:
|
||||||
|
ContentImage image => await image.AsBase64(token),
|
||||||
|
|
||||||
|
// Other content types are not supported yet:
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(lastPromptContent))
|
||||||
|
{
|
||||||
|
logger.LogWarning("The last prompt is empty. The AI cannot validate the retrieval context.");
|
||||||
|
return new(false, "The last prompt was empty.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Prepare the retrieval context for the agent:
|
||||||
|
//
|
||||||
|
var additionalData = new Dictionary<string, string>();
|
||||||
|
var markdownRetrievalContext = await retrievalContext.AsMarkdown(token: token);
|
||||||
|
additionalData.Add("retrievalContext", markdownRetrievalContext);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Let the agent validate the retrieval context:
|
||||||
|
//
|
||||||
|
var prompt = $"""
|
||||||
|
The system prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{chatThread.SystemPrompt}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user prompt is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{lastPromptContent}
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Call the agent:
|
||||||
|
var aiResponse = await this.ProcessInput(new ContentBlock
|
||||||
|
{
|
||||||
|
Time = DateTimeOffset.UtcNow,
|
||||||
|
ContentType = ContentType.TEXT,
|
||||||
|
Role = ChatRole.USER,
|
||||||
|
Content = new ContentText
|
||||||
|
{
|
||||||
|
Text = prompt,
|
||||||
|
},
|
||||||
|
}, additionalData);
|
||||||
|
|
||||||
|
if (aiResponse.Content is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (aiResponse)
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4. Parse the agent response:
|
||||||
|
//
|
||||||
|
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// What we expect is one JSON object:
|
||||||
|
//
|
||||||
|
var validationJson = textContent.Text;
|
||||||
|
|
||||||
|
//
|
||||||
|
// We know how bad LLM may be in generating JSON without surrounding text.
|
||||||
|
// Thus, we expect the worst and try to extract the JSON list from the text:
|
||||||
|
//
|
||||||
|
var json = ExtractJson(validationJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<RetrievalContextValidationResult>(json, JSON_SERIALIZER_OPTIONS);
|
||||||
|
return result with { RetrievalContext = retrievalContext };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
|
||||||
|
return new(false, "The agent answered with an invalid or unexpected JSON format.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case { ContentType: ContentType.TEXT }:
|
||||||
|
logger.LogWarning("The agent answered with an unexpected inner content type.");
|
||||||
|
return new(false, "The agent answered with an unexpected inner content type.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
case { ContentType: ContentType.NONE }:
|
||||||
|
logger.LogWarning("The agent did not return a response.");
|
||||||
|
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
|
||||||
|
return new(false, $"The agent answered with an unexpected content type '{aiResponse.ContentType}'.", 1.0f, retrievalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Release the semaphore slot:
|
||||||
|
semaphore?.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wrapper around the span version, because we need to call this method from an async context.
|
||||||
|
private static string ExtractJson(string text) => ExtractJson(text.AsSpan()).ToString();
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// 1. Expect the best case ;-)
|
||||||
|
//
|
||||||
|
if (CheckJsonObjectStart(input))
|
||||||
|
return ExtractJsonPart(input);
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Okay, we have some garbage before the
|
||||||
|
// JSON object. We expected that...
|
||||||
|
//
|
||||||
|
for (var index = 0; index < input.Length; index++)
|
||||||
|
{
|
||||||
|
if (input[index] is '{' && CheckJsonObjectStart(input[index..]))
|
||||||
|
return ExtractJsonPart(input[index..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckJsonObjectStart(ReadOnlySpan<char> area)
|
||||||
|
{
|
||||||
|
char[] expectedSymbols = ['{', '"', 'd'];
|
||||||
|
var symbolIndex = 0;
|
||||||
|
|
||||||
|
foreach (var c in area)
|
||||||
|
{
|
||||||
|
if (symbolIndex >= expectedSymbols.Length)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (char.IsWhiteSpace(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (c == expectedSymbols[symbolIndex++])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> ExtractJsonPart(ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
var insideString = false;
|
||||||
|
for (var index = 0; index < input.Length; index++)
|
||||||
|
{
|
||||||
|
if (input[index] is '"')
|
||||||
|
{
|
||||||
|
insideString = !insideString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insideString)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (input[index] is '}')
|
||||||
|
return input[..++index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
namespace AIStudio.Agents;
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, rng)
|
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, dataSourceService, rng)
|
||||||
{
|
{
|
||||||
private static readonly ContentBlock EMPTY_BLOCK = new()
|
|
||||||
{
|
|
||||||
Content = null,
|
|
||||||
ContentType = ContentType.NONE,
|
|
||||||
Role = ChatRole.AGENT,
|
|
||||||
Time = DateTimeOffset.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<ContentBlock> context = new();
|
private readonly List<ContentBlock> context = new();
|
||||||
private readonly List<ContentBlock> answers = new();
|
private readonly List<ContentBlock> answers = new();
|
||||||
|
|
||||||
@@ -72,8 +65,8 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
|
|||||||
return EMPTY_BLOCK;
|
return EMPTY_BLOCK;
|
||||||
|
|
||||||
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
|
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
|
||||||
var time = this.AddUserRequest(thread, text.Text);
|
var userRequest = this.AddUserRequest(thread, text.Text);
|
||||||
await this.AddAIResponseAsync(thread, time);
|
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
|
||||||
|
|
||||||
var answer = thread.Blocks[^1];
|
var answer = thread.Blocks[^1];
|
||||||
this.answers.Add(answer);
|
this.answers.Add(answer);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using AIStudio.Tools.RAG;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a retrieval context validation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Decision">Whether the retrieval context is useful or not.</param>
|
||||||
|
/// <param name="Reason">The reason for the decision.</param>
|
||||||
|
/// <param name="Confidence">The confidence of the decision.</param>
|
||||||
|
/// <param name="RetrievalContext">The retrieval context that was validated.</param>
|
||||||
|
public readonly record struct RetrievalContextValidationResult(bool Decision, string Reason, float Confidence, IRetrievalContext? RetrievalContext) : IConfidence;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a selected data source, chosen by the agent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The data source ID.</param>
|
||||||
|
/// <param name="Reason">The reason for selecting the data source.</param>
|
||||||
|
/// <param name="Confidence">The confidence of the agent in the selection.</param>
|
||||||
|
public readonly record struct SelectedDataSource(string Id, string Reason, float Confidence) : IConfidence;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using AIStudio.Chat;
|
||||||
|
|
||||||
|
namespace AIStudio.Agents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The created user request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time when the request was created.
|
||||||
|
/// </summary>
|
||||||
|
public required DateTimeOffset Time { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user prompt.
|
||||||
|
/// </summary>
|
||||||
|
public required IContent UserPrompt { get; init; }
|
||||||
|
}
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||||
{
|
{
|
||||||
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (this.AllowProfiles && this.ShowProfileSelection)
|
@if (this.AllowProfiles && this.ShowProfileSelection)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
using MudBlazor.Utilities;
|
using MudBlazor.Utilities;
|
||||||
|
|
||||||
using RustService = AIStudio.Tools.RustService;
|
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace AIStudio.Assistants;
|
namespace AIStudio.Assistants;
|
||||||
@@ -28,6 +28,9 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
[Inject]
|
[Inject]
|
||||||
protected RustService RustService { get; init; } = null!;
|
protected RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected DataSourceService DataSourceService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected NavigationManager NavigationManager { get; init; } = null!;
|
protected NavigationManager NavigationManager { get; init; } = null!;
|
||||||
|
|
||||||
@@ -97,6 +100,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
protected bool inputIsValid;
|
protected bool inputIsValid;
|
||||||
protected Profile currentProfile = Profile.NO_PROFILE;
|
protected Profile currentProfile = Profile.NO_PROFILE;
|
||||||
protected ChatThread? chatThread;
|
protected ChatThread? chatThread;
|
||||||
|
protected IContent? lastUserPrompt;
|
||||||
|
|
||||||
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
|
||||||
|
|
||||||
@@ -242,16 +246,18 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
|
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
|
||||||
{
|
{
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
this.lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = request,
|
||||||
|
};
|
||||||
|
|
||||||
this.chatThread!.Blocks.Add(new ContentBlock
|
this.chatThread!.Blocks.Add(new ContentBlock
|
||||||
{
|
{
|
||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
HideFromUser = hideContentFromUser,
|
HideFromUser = hideContentFromUser,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = this.lastUserPrompt,
|
||||||
{
|
|
||||||
Text = request,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return time;
|
return time;
|
||||||
@@ -287,7 +293,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
|
||||||
|
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ namespace AIStudio.Assistants.ERI;
|
|||||||
|
|
||||||
public static class AllowedLLMProvidersExtensions
|
public static class AllowedLLMProvidersExtensions
|
||||||
{
|
{
|
||||||
public static string Description(this AllowedLLMProviders provider)
|
public static string Description(this AllowedLLMProviders provider) => provider switch
|
||||||
{
|
{
|
||||||
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.NONE => "Please select what kind of LLM provider are allowed for this data source",
|
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
|
||||||
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"
|
_ => "Unknown option was selected"
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
@using MudExtensions
|
@using MudExtensions
|
||||||
@inherits AssistantBaseCore
|
@inherits AssistantBaseCore
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
|
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
|
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
|
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.
|
significantly reduce the hallucination of the LLM in knowledge questions.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1">
|
<MudText Typo="Typo.body1">
|
||||||
<b>Related links:</b>
|
<b>Related links:</b>
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
ERI server presets
|
ERI server presets
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudJustifiedText 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
|
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.
|
you are responsible for multiple ERI servers.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
|
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
|
||||||
{
|
{
|
||||||
@@ -59,26 +59,26 @@ else
|
|||||||
|
|
||||||
@if(this.AreServerPresetsBlocked)
|
@if(this.AreServerPresetsBlocked)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.body1" Class="mb-3 mt-3">
|
<MudJustifiedText 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.
|
Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
|
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
|
||||||
Auto save
|
Auto save
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudJustifiedText 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
|
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
|
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?
|
automatically saved. Would you like this?
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
@if(this.AreServerPresetsBlocked)
|
@if(this.AreServerPresetsBlocked)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudJustifiedText 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.
|
Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
}
|
}
|
||||||
|
|
||||||
<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" />
|
<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" />
|
||||||
@@ -204,18 +204,18 @@ else
|
|||||||
Embedding settings
|
Embedding settings
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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
|
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
|
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
|
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
|
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.
|
need embeddings.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-3">
|
<MudJustifiedText 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
|
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.
|
or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
@if (!this.IsNoneERIServerSelected)
|
@if (!this.IsNoneERIServerSelected)
|
||||||
{
|
{
|
||||||
@@ -258,12 +258,12 @@ else
|
|||||||
Data retrieval settings
|
Data retrieval settings
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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.
|
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 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
|
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.
|
the process that best suits their circumstances.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
@if (!this.IsNoneERIServerSelected)
|
@if (!this.IsNoneERIServerSelected)
|
||||||
{
|
{
|
||||||
@@ -299,13 +299,13 @@ else
|
|||||||
Add Retrieval Process
|
Add Retrieval Process
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-1">
|
<MudJustifiedText 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
|
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.
|
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
|
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
|
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.
|
the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<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"/>
|
<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"/>
|
||||||
|
|
||||||
@@ -313,17 +313,17 @@ else
|
|||||||
Provider selection for generation
|
Provider selection for generation
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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.
|
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.
|
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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
|
<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.
|
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
|
<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.
|
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||||
|
|
||||||
@@ -331,19 +331,19 @@ else
|
|||||||
Write code to file system
|
Write code to file system
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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
|
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
|
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.
|
AI has made in which files.
|
||||||
</MudText>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudText Typo="Typo.body1" Class="mb-2">
|
<MudJustifiedText 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
|
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
|
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,
|
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 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.
|
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>
|
</MudJustifiedText>
|
||||||
|
|
||||||
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
|
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
|
||||||
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />
|
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />
|
||||||
@@ -7,7 +7,7 @@ public static class ERIVersionExtensions
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var url = version.SpecificationURL();
|
var url = version.SpecificationURL();
|
||||||
var response = await httpClient.GetAsync(url);
|
using var response = await httpClient.GetAsync(url);
|
||||||
return await response.Content.ReadAsStringAsync();
|
return await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public enum ChatRole
|
|||||||
USER,
|
USER,
|
||||||
AI,
|
AI,
|
||||||
AGENT,
|
AGENT,
|
||||||
|
RAG,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
@@ -27,6 +30,16 @@ public sealed record ChatThread
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SelectedProfile { get; set; } = string.Empty;
|
public string SelectedProfile { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source options for this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public DataSourceOptions DataSourceOptions { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The AI-selected data sources for this chat thread.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -138,4 +151,46 @@ public sealed record ChatThread
|
|||||||
// Remove the block from the chat thread:
|
// Remove the block from the chat thread:
|
||||||
this.Blocks.Remove(block);
|
this.Blocks.Remove(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms this chat thread to an ERI chat thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The ERI chat thread.</returns>
|
||||||
|
public async Task<Tools.ERIClient.DataModel.ChatThread> ToERIChatThread(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Transform the content blocks:
|
||||||
|
//
|
||||||
|
var contentBlocks = new List<Tools.ERIClient.DataModel.ContentBlock>(this.Blocks.Count);
|
||||||
|
foreach (var block in this.Blocks)
|
||||||
|
{
|
||||||
|
var (contentData, contentType) = block.Content switch
|
||||||
|
{
|
||||||
|
ContentImage image => (await image.AsBase64(token), Tools.ERIClient.DataModel.ContentType.IMAGE),
|
||||||
|
ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT),
|
||||||
|
|
||||||
|
_ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN),
|
||||||
|
};
|
||||||
|
|
||||||
|
contentBlocks.Add(new Tools.ERIClient.DataModel.ContentBlock
|
||||||
|
{
|
||||||
|
Role = block.Role switch
|
||||||
|
{
|
||||||
|
ChatRole.AI => Role.AI,
|
||||||
|
ChatRole.USER => Role.USER,
|
||||||
|
ChatRole.AGENT => Role.AGENT,
|
||||||
|
ChatRole.SYSTEM => Role.SYSTEM,
|
||||||
|
ChatRole.NONE => Role.NONE,
|
||||||
|
|
||||||
|
_ => Role.UNKNOW,
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = contentData,
|
||||||
|
Type = contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -78,9 +78,9 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ContentType.IMAGE:
|
case ContentType.IMAGE:
|
||||||
if (this.Content is ContentImage imageContent)
|
if (this.Content is ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent)
|
||||||
{
|
{
|
||||||
<MudImage Src="@imageContent.URL"/>
|
<MudImage Src="@imageContent.Source"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
using RustService = AIStudio.Tools.RustService;
|
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an image inside the chat.
|
/// Represents an image inside the chat.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ContentImage : IContent
|
public sealed class ContentImage : IContent, IImageSource
|
||||||
{
|
{
|
||||||
#region Implementation of IContent
|
#region Implementation of IContent
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ public sealed class ContentImage : IContent
|
|||||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
|
public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -37,12 +36,15 @@ public sealed class ContentImage : IContent
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The URL of the image.
|
/// The type of the image source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string URL { get; set; } = string.Empty;
|
/// <remarks>
|
||||||
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
|
/// </remarks>
|
||||||
|
public required ContentImageSource SourceType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The local path of the image.
|
/// The image source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LocalPath { get; set; } = string.Empty;
|
public required string Source { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public enum ContentImageSource
|
||||||
|
{
|
||||||
|
URL,
|
||||||
|
LOCAL_PATH,
|
||||||
|
BASE64,
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.RAG.RAGProcesses;
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
@@ -35,17 +36,27 @@ public sealed class ContentText : IContent
|
|||||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
|
public async Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if(chatThread is null)
|
if(chatThread is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Call the RAG process. Right now, we only have one RAG process:
|
||||||
|
if (lastPrompt is not null)
|
||||||
|
{
|
||||||
|
var rag = new AISrcSelWithRetCtxVal();
|
||||||
|
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
|
||||||
|
}
|
||||||
|
|
||||||
// Store the last time we got a response. We use this later
|
// Store the last time we got a response. We use this later
|
||||||
// to determine whether we should notify the UI about the
|
// to determine whether we should notify the UI about the
|
||||||
// new content or not. Depends on the energy saving mode
|
// new content or not. Depends on the energy saving mode
|
||||||
// the user chose.
|
// the user chose.
|
||||||
var last = DateTimeOffset.Now;
|
var last = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// Get the settings manager:
|
||||||
|
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
|
||||||
|
|
||||||
// Start another thread by using a task to uncouple
|
// Start another thread by using a task to uncouple
|
||||||
// the UI thread from the AI processing:
|
// the UI thread from the AI processing:
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
|
||||||
|
|
||||||
namespace AIStudio.Chat;
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
@@ -42,5 +41,16 @@ public interface IContent
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uses the provider to create the content.
|
/// Uses the provider to create the content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
|
public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the corresponding ERI content type.
|
||||||
|
/// </summary>
|
||||||
|
public Tools.ERIClient.DataModel.ContentType ToERIContentType => this switch
|
||||||
|
{
|
||||||
|
ContentText => Tools.ERIClient.DataModel.ContentType.TEXT,
|
||||||
|
ContentImage => Tools.ERIClient.DataModel.ContentType.IMAGE,
|
||||||
|
|
||||||
|
_ => Tools.ERIClient.DataModel.ContentType.UNKNOWN,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public interface IImageSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the image source.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Is the image source a URL, a local file path, a base64 string, etc.?
|
||||||
|
/// </remarks>
|
||||||
|
public ContentImageSource SourceType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The image source.
|
||||||
|
/// </summary>
|
||||||
|
public string Source { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace AIStudio.Chat;
|
||||||
|
|
||||||
|
public static class IImageSourceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read the image content as a base64 string.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The images are directly converted to base64 strings. The maximum
|
||||||
|
/// size of the image is around 10 MB. If the image is larger, the method
|
||||||
|
/// returns an empty string.
|
||||||
|
///
|
||||||
|
/// As of now, this method does no sort of image processing. LLMs usually
|
||||||
|
/// do not work with arbitrary image sizes. In the future, we might have
|
||||||
|
/// to resize the images before sending them to the model.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="image">The image source.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// <returns>The image content as a base64 string; might be empty.</returns>
|
||||||
|
public static async Task<string> AsBase64(this IImageSource image, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
switch (image.SourceType)
|
||||||
|
{
|
||||||
|
case ContentImageSource.BASE64:
|
||||||
|
return image.Source;
|
||||||
|
|
||||||
|
case ContentImageSource.URL:
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
if(response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// Read the length of the content:
|
||||||
|
var lengthBytes = response.Content.Headers.ContentLength;
|
||||||
|
if(lengthBytes > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync(token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ContentImageSource.LOCAL_PATH:
|
||||||
|
if(File.Exists(image.Source))
|
||||||
|
{
|
||||||
|
// Read the content length:
|
||||||
|
var length = new FileInfo(image.Source).Length;
|
||||||
|
if(length > 10_000_000)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var bytes = await File.ReadAllBytesAsync(image.Source, token);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"),
|
||||||
|
new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"),
|
||||||
|
new (204, "v0.9.29, build 204 (2025-02-24 13:48 UTC)", "v0.9.29.md"),
|
||||||
|
new (203, "v0.9.28, build 203 (2025-02-09 16:33 UTC)", "v0.9.28.md"),
|
||||||
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
|
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
|
||||||
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
|
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
|
||||||
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
|
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase
|
|||||||
|
|
||||||
private async Task ReadLogAsync()
|
private async Task ReadLogAsync()
|
||||||
{
|
{
|
||||||
var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
|
||||||
this.LogContent = await response.Content.ReadAsStringAsync();
|
this.LogContent = await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
|
||||||
{
|
{
|
||||||
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||||
@@ -108,6 +108,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
|
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
|
||||||
|
|
||||||
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||||
|
}
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
</FooterContent>
|
</FooterContent>
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
@@ -3,6 +3,7 @@ using AIStudio.Dialogs;
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
@@ -40,9 +41,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
[Inject]
|
[Inject]
|
||||||
private IDialogService DialogService { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private DataSourceService DataSourceService { get; init; } = null!;
|
||||||
|
|
||||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
|
private DataSourceSelection? dataSourceSelectionComponent;
|
||||||
|
private DataSourceOptions earlyDataSourceOptions = new();
|
||||||
private Profile currentProfile = Profile.NO_PROFILE;
|
private Profile currentProfile = Profile.NO_PROFILE;
|
||||||
private bool hasUnsavedChanges;
|
private bool hasUnsavedChanges;
|
||||||
private bool mustScrollToBottomAfterRender;
|
private bool mustScrollToBottomAfterRender;
|
||||||
@@ -66,21 +72,40 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
// Apply the filters for the message bus:
|
||||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
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:
|
// Configure the spellchecking for the user input:
|
||||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
|
|
||||||
|
// Get the preselected profile:
|
||||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check for deferred messages of the kind 'SEND_TO_CHAT',
|
||||||
|
// aka the user sends an assistant result to the chat:
|
||||||
|
//
|
||||||
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
|
||||||
if (deferredContent is not null)
|
if (deferredContent is not null)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Yes, the user sent an assistant result to the chat.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Use chat thread sent by the user:
|
||||||
this.ChatThread = deferredContent;
|
this.ChatThread = deferredContent;
|
||||||
this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
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);
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
|
|
||||||
|
// We know already that the chat thread is not null,
|
||||||
|
// but we have to check it again for the nullability
|
||||||
|
// for the compiler:
|
||||||
if (this.ChatThread is not null)
|
if (this.ChatThread is not null)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Check if the chat thread has a name. If not, we
|
||||||
|
// generate the name now:
|
||||||
|
//
|
||||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||||
{
|
{
|
||||||
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
|
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
|
||||||
@@ -94,12 +119,24 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to apply the standard chat data source options:
|
||||||
|
//
|
||||||
|
if (this.SettingsManager.ConfigurationData.Chat.SendToChatDataSourceBehavior is SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS)
|
||||||
|
this.ChatThread.DataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to store the chat automatically:
|
||||||
|
//
|
||||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
{
|
{
|
||||||
this.autoSaveEnabled = true;
|
this.autoSaveEnabled = true;
|
||||||
this.mustStoreChat = true;
|
this.mustStoreChat = true;
|
||||||
|
|
||||||
// Ensure the workspace exists:
|
//
|
||||||
|
// When a standard workspace is used, we have to ensure
|
||||||
|
// that the workspace is available:
|
||||||
|
//
|
||||||
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
|
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
|
||||||
await WorkspaceBehaviour.EnsureERIServerWorkspace();
|
await WorkspaceBehaviour.EnsureERIServerWorkspace();
|
||||||
|
|
||||||
@@ -108,14 +145,38 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// No, the user did not send an assistant result to the chat.
|
||||||
|
//
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if the user wants to show the latest message after loading:
|
||||||
|
//
|
||||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// We cannot scroll to the bottom right now because the
|
||||||
|
// chat component is not rendered yet. We have to wait for
|
||||||
|
// the rendering process to finish. Thus, we set a flag
|
||||||
|
// to scroll to the bottom after the rendering process.:
|
||||||
|
//
|
||||||
this.mustScrollToBottomAfterRender = true;
|
this.mustScrollToBottomAfterRender = true;
|
||||||
this.scrollRenderCountdown = 4;
|
this.scrollRenderCountdown = 4;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check if another component deferred the loading of a chat.
|
||||||
|
//
|
||||||
|
// This is used, e.g., for the bias-of-the-day component:
|
||||||
|
// when the bias for this day was already produced, the bias
|
||||||
|
// component sends a message to the chat component to load
|
||||||
|
// the chat with the bias:
|
||||||
|
//
|
||||||
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
|
||||||
if (deferredLoading != default)
|
if (deferredLoading != default)
|
||||||
{
|
{
|
||||||
@@ -124,6 +185,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
|
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// When for whatever reason we have a chat thread, we have to
|
||||||
|
// ensure that the corresponding workspace id is set and the
|
||||||
|
// workspace name is loaded:
|
||||||
|
//
|
||||||
if (this.ChatThread is not null)
|
if (this.ChatThread is not null)
|
||||||
{
|
{
|
||||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
@@ -131,6 +197,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.WorkspaceName(this.currentWorkspaceName);
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select the correct provider:
|
||||||
await this.SelectProviderWhenLoadingChat();
|
await this.SelectProviderWhenLoadingChat();
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
@@ -204,6 +271,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
|
||||||
|
|
||||||
|
private void ApplyStandardDataSourceOptions()
|
||||||
|
{
|
||||||
|
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||||
|
this.earlyDataSourceOptions = chatDefaultOptions;
|
||||||
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
|
||||||
|
}
|
||||||
|
|
||||||
private string ExtractThreadName(string firstUserInput)
|
private string ExtractThreadName(string firstUserInput)
|
||||||
{
|
{
|
||||||
// We select the first 10 words of the user input:
|
// We select the first 10 words of the user input:
|
||||||
@@ -231,8 +305,43 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
|
||||||
|
{
|
||||||
|
if (this.ChatThread is null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return this.ChatThread.AISelectedDataSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataSourceOptions GetCurrentDataSourceOptions()
|
||||||
|
{
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
return this.ChatThread.DataSourceOptions;
|
||||||
|
|
||||||
|
return this.earlyDataSourceOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetCurrentDataSourceOptions(DataSourceOptions updatedOptions)
|
||||||
|
{
|
||||||
|
if (this.ChatThread is not null)
|
||||||
|
{
|
||||||
|
this.hasUnsavedChanges = true;
|
||||||
|
this.ChatThread.DataSourceOptions = updatedOptions;
|
||||||
|
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||||
|
{
|
||||||
|
await this.SaveThread();
|
||||||
|
this.hasUnsavedChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.earlyDataSourceOptions = updatedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||||
{
|
{
|
||||||
|
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||||
|
this.dataSourceSelectionComponent.Hide();
|
||||||
|
|
||||||
this.hasUnsavedChanges = true;
|
this.hasUnsavedChanges = true;
|
||||||
var key = keyEvent.Code.ToLowerInvariant();
|
var key = keyEvent.Code.ToLowerInvariant();
|
||||||
|
|
||||||
@@ -276,6 +385,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
SystemPrompt = SystemPrompts.DEFAULT,
|
SystemPrompt = SystemPrompts.DEFAULT,
|
||||||
WorkspaceId = this.currentWorkspaceId,
|
WorkspaceId = this.currentWorkspaceId,
|
||||||
ChatId = Guid.NewGuid(),
|
ChatId = Guid.NewGuid(),
|
||||||
|
DataSourceOptions = this.earlyDataSourceOptions,
|
||||||
Name = this.ExtractThreadName(this.userInput),
|
Name = this.ExtractThreadName(this.userInput),
|
||||||
Seed = this.RNG.Next(),
|
Seed = this.RNG.Next(),
|
||||||
Blocks = [],
|
Blocks = [],
|
||||||
@@ -295,8 +405,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
IContent? lastUserPrompt;
|
||||||
if (!reuseLastUserPrompt)
|
if (!reuseLastUserPrompt)
|
||||||
{
|
{
|
||||||
|
lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
Text = this.userInput,
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Add the user message to the thread:
|
// Add the user message to the thread:
|
||||||
//
|
//
|
||||||
@@ -305,10 +421,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = lastUserPrompt,
|
||||||
{
|
|
||||||
Text = this.userInput,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save the chat:
|
// Save the chat:
|
||||||
@@ -319,6 +432,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
lastUserPrompt = this.ChatThread.Blocks.Last(x => x.Role is ChatRole.USER).Content;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Add the AI response to the thread:
|
// Add the AI response to the thread:
|
||||||
@@ -360,7 +475,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.SettingsManager, this.Provider.Model, this.ChatThread, this.cancellationTokenSource.Token);
|
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancellationTokenSource = null;
|
this.cancellationTokenSource = null;
|
||||||
@@ -374,6 +489,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
// Disable the stream state:
|
// Disable the stream state:
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
|
|
||||||
|
// Update the UI:
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +524,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||||
|
// about possible data loss:
|
||||||
|
//
|
||||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters
|
var dialogParameters = new DialogParameters
|
||||||
@@ -420,6 +541,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete the previous chat when desired and necessary:
|
||||||
|
//
|
||||||
if (this.ChatThread is not null && deletePreviousChat)
|
if (this.ChatThread is not null && deletePreviousChat)
|
||||||
{
|
{
|
||||||
string chatPath;
|
string chatPath;
|
||||||
@@ -434,10 +558,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset our state:
|
||||||
|
//
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
this.userInput = string.Empty;
|
this.userInput = string.Empty;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset the LLM provider considering the user's settings:
|
||||||
|
//
|
||||||
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
|
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
|
||||||
{
|
{
|
||||||
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
|
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
|
||||||
@@ -456,8 +586,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset the chat thread or create a new one:
|
||||||
|
//
|
||||||
if (!useSameWorkspace)
|
if (!useSameWorkspace)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// When the user wants to start a new chat outside the current workspace,
|
||||||
|
// we have to reset the workspace id and the workspace name. Also, we have
|
||||||
|
// to reset the chat thread:
|
||||||
|
//
|
||||||
this.ChatThread = null;
|
this.ChatThread = null;
|
||||||
this.currentWorkspaceId = Guid.Empty;
|
this.currentWorkspaceId = Guid.Empty;
|
||||||
this.currentWorkspaceName = string.Empty;
|
this.currentWorkspaceName = string.Empty;
|
||||||
@@ -465,6 +603,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// When the user wants to start a new chat in the same workspace, we have to
|
||||||
|
// reset the chat thread only. The workspace id and the workspace name remain
|
||||||
|
// the same:
|
||||||
|
//
|
||||||
this.ChatThread = new()
|
this.ChatThread = new()
|
||||||
{
|
{
|
||||||
SelectedProvider = this.Provider.Id,
|
SelectedProvider = this.Provider.Id,
|
||||||
@@ -478,7 +621,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userInput = string.Empty;
|
// Now, we have to reset the data source options as well:
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
|
|
||||||
|
// Notify the parent component about the change:
|
||||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,12 +684,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
|
||||||
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
|
||||||
this.WorkspaceName(this.currentWorkspaceName);
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.currentWorkspaceId = Guid.Empty;
|
this.currentWorkspaceId = Guid.Empty;
|
||||||
this.currentWorkspaceName = string.Empty;
|
this.currentWorkspaceName = string.Empty;
|
||||||
this.WorkspaceName(this.currentWorkspaceName);
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.SelectProviderWhenLoadingChat();
|
await this.SelectProviderWhenLoadingChat();
|
||||||
@@ -567,6 +715,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
this.WorkspaceName(this.currentWorkspaceName);
|
this.WorkspaceName(this.currentWorkspaceName);
|
||||||
|
|
||||||
this.ChatThread = null;
|
this.ChatThread = null;
|
||||||
|
this.ApplyStandardDataSourceOptions();
|
||||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@using AIStudio.Provider
|
@using AIStudio.Provider
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
|
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
|
||||||
@if (this.Mode is ConfidenceInfoMode.ICON)
|
@if (this.Mode is PopoverTriggerMode.ICON)
|
||||||
{
|
{
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<MudText Typo="Typo.h5">Confidence Card</MudText>
|
<MudText Typo="Typo.h5">Confidence Card</MudText>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent>
|
<MudCardContent Style="max-height: 50vh; overflow: auto;">
|
||||||
<MudText Typo="Typo.h6">Description</MudText>
|
<MudText Typo="Typo.h6">Description</MudText>
|
||||||
<MudMarkdown Value="@this.currentConfidence.Description"/>
|
<MudMarkdown Value="@this.currentConfidence.Description"/>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace AIStudio.Components;
|
|||||||
public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
|
public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
|
||||||
{
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public ConfidenceInfoMode Mode { get; set; } = ConfidenceInfoMode.BUTTON;
|
public PopoverTriggerMode Mode { get; set; } = PopoverTriggerMode.BUTTON;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public LLMProviders LLMProvider { get; set; }
|
public LLMProviders LLMProvider { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
|
||||||
@@ -47,8 +49,12 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private IEnumerable<ConfigurationSelectData<string>> FilteredData()
|
private IEnumerable<ConfigurationSelectData<string>> FilteredData()
|
||||||
{
|
{
|
||||||
|
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
|
||||||
|
yield return new("Use app default", string.Empty);
|
||||||
|
|
||||||
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
|
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
|
||||||
foreach (var providerId in this.Data)
|
foreach (var providerId in this.Data)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using AIStudio.Agents;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A data structure to combine the data source and the underlying AI decision.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DataSourceAgentSelected
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data source.
|
||||||
|
/// </summary>
|
||||||
|
public required IDataSource DataSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The AI decision, which led to the selection of the data source.
|
||||||
|
/// </summary>
|
||||||
|
public required SelectedDataSource AIDecision { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the data source is part of the final selection for the RAG process.
|
||||||
|
/// </summary>
|
||||||
|
public bool Selected { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
@using AIStudio.Settings
|
||||||
|
|
||||||
|
@if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE)
|
||||||
|
{
|
||||||
|
<div class="d-flex">
|
||||||
|
<MudTooltip Text="Select the data sources you want to use here." Placement="Placement.Top">
|
||||||
|
@if (this.PopoverTriggerMode is PopoverTriggerMode.ICON)
|
||||||
|
{
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())">
|
||||||
|
Select data sources
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudPopover Open="@this.showDataSourceSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<PreviewPrototype/>
|
||||||
|
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
|
||||||
|
@if (this.waitingForDataSources)
|
||||||
|
{
|
||||||
|
<MudSkeleton Width="30%" Height="42px;"/>
|
||||||
|
<MudSkeleton Width="80%"/>
|
||||||
|
<MudSkeleton Width="100%"/>
|
||||||
|
}
|
||||||
|
else if (this.showDataSourceSelection)
|
||||||
|
{
|
||||||
|
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="Yes, I want to use data sources." LabelOff="No, I don't want to use data sources." ValueChanged="@this.EnabledChanged"/>
|
||||||
|
@if (this.areDataSourcesEnabled)
|
||||||
|
{
|
||||||
|
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||||
|
{
|
||||||
|
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
|
||||||
|
{
|
||||||
|
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||||
|
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
|
||||||
|
@foreach (var source in this.availableDataSources)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
@source.Name
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</MudField>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
|
||||||
|
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
|
||||||
|
@foreach (var source in this.availableDataSources)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
@source.Name
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</ExpansionPanel>
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
|
||||||
|
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
|
||||||
|
@foreach (var source in this.DataSourcesAISelected)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
<ChildContent>
|
||||||
|
<MudText Typo="Typo.body1">
|
||||||
|
@source.DataSource.Name
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
|
||||||
|
<MudJustifiedText Typo="Typo.body2">
|
||||||
|
@this.GetAIReasoning(source)
|
||||||
|
</MudJustifiedText>
|
||||||
|
</ChildContent>
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</ExpansionPanel>
|
||||||
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions>
|
||||||
|
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideDataSourceSelection())">
|
||||||
|
Close
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
</MudPopover>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 mb-8 mt-3 border-dashed border rounded-lg">
|
||||||
|
<PreviewPrototype/>
|
||||||
|
<MudText Typo="Typo.h5">Data Source Selection</MudText>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.ConfigurationHeaderMessage))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1">
|
||||||
|
@this.ConfigurationHeaderMessage
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="Yes, I want to use data sources." LabelOff="No, I don't want to use data sources." ValueChanged="@this.EnabledChanged"/>
|
||||||
|
@if (this.areDataSourcesEnabled)
|
||||||
|
{
|
||||||
|
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
|
||||||
|
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
|
||||||
|
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
|
||||||
|
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
|
||||||
|
@foreach (var source in this.availableDataSources)
|
||||||
|
{
|
||||||
|
<MudListItem Value="@source">
|
||||||
|
@source.Name
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
</MudField>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, IDisposable
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public DataSourceSelectionMode SelectionMode { get; set; } = DataSourceSelectionMode.SELECTION_MODE;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public PopoverTriggerMode PopoverTriggerMode { get; set; } = PopoverTriggerMode.BUTTON;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string PopoverButtonClasses { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required AIStudio.Settings.Provider LLMProvider { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required DataSourceOptions DataSourceOptions { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<DataSourceOptions> DataSourceOptionsChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<DataSourceAgentSelected> DataSourcesAISelected { get; set; } = [];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ConfigurationHeaderMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool AutoSaveAppSettings { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private MessageBus MessageBus { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private DataSourceService DataSourceService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<DataSourceSelection> Logger { get; init; } = null!;
|
||||||
|
|
||||||
|
private bool internalChange;
|
||||||
|
private bool showDataSourceSelection;
|
||||||
|
private bool waitingForDataSources = true;
|
||||||
|
private IReadOnlyList<IDataSource> availableDataSources = [];
|
||||||
|
private IReadOnlyCollection<IDataSource> selectedDataSources = [];
|
||||||
|
private bool aiBasedSourceSelection;
|
||||||
|
private bool aiBasedValidation;
|
||||||
|
private bool areDataSourcesEnabled;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.MessageBus.RegisterComponent(this);
|
||||||
|
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED, Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Load the settings:
|
||||||
|
//
|
||||||
|
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||||
|
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||||
|
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||||
|
this.waitingForDataSources = this.areDataSourcesEnabled;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Preselect the data sources. Right now, we cannot filter
|
||||||
|
// the data sources. Later, when the component is shown, we
|
||||||
|
// will filter the data sources.
|
||||||
|
//
|
||||||
|
// Right before the preselection would be used to kick off the
|
||||||
|
// RAG process, we will filter the data sources as well.
|
||||||
|
//
|
||||||
|
var preselectedSources = new List<IDataSource>(this.DataSourceOptions.PreselectedDataSourceIds.Count);
|
||||||
|
foreach (var preselectedDataSourceId in this.DataSourceOptions.PreselectedDataSourceIds)
|
||||||
|
{
|
||||||
|
var dataSource = this.SettingsManager.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == preselectedDataSourceId);
|
||||||
|
if (dataSource is not null)
|
||||||
|
preselectedSources.Add(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedDataSources = preselectedSources;
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (!this.internalChange)
|
||||||
|
{
|
||||||
|
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||||
|
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||||
|
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.SelectionMode)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// In selection mode, we have to load & filter the data sources
|
||||||
|
// when the component is shown:
|
||||||
|
//
|
||||||
|
case DataSourceSelectionMode.SELECTION_MODE:
|
||||||
|
|
||||||
|
//
|
||||||
|
// For external changes, we have to reload & filter
|
||||||
|
// the data sources:
|
||||||
|
//
|
||||||
|
if (this.showDataSourceSelection && !this.internalChange)
|
||||||
|
await this.LoadAndApplyFilters();
|
||||||
|
else
|
||||||
|
this.internalChange = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
//
|
||||||
|
// In configuration mode, we have to load all data sources:
|
||||||
|
//
|
||||||
|
case DataSourceSelectionMode.CONFIGURATION_MODE:
|
||||||
|
this.availableDataSources = this.SettingsManager.ConfigurationData.DataSources;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
|
||||||
|
|
||||||
|
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
|
||||||
|
|
||||||
|
private string GetAIReasoning(DataSourceAgentSelected source) => $"AI reasoning (confidence {source.AIDecision.Confidence:P0}): {source.AIDecision.Reason}";
|
||||||
|
|
||||||
|
public void ChangeOptionWithoutSaving(DataSourceOptions options, IReadOnlyList<DataSourceAgentSelected>? aiSelectedDataSources = null)
|
||||||
|
{
|
||||||
|
this.DataSourceOptions = options;
|
||||||
|
this.DataSourcesAISelected = aiSelectedDataSources ?? [];
|
||||||
|
|
||||||
|
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
|
||||||
|
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
|
||||||
|
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
|
||||||
|
this.selectedDataSources = this.SettingsManager.ConfigurationData.DataSources.Where(ds => this.DataSourceOptions.PreselectedDataSourceIds.Contains(ds.Id)).ToList();
|
||||||
|
this.waitingForDataSources = false;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remark: We do not apply the filters here. This is done later
|
||||||
|
// when either the parameters are changed or just before the
|
||||||
|
// RAG process is started (outside of this component).
|
||||||
|
//
|
||||||
|
// In fact, when we apply the filters here, multiple calls
|
||||||
|
// to the filter method would be made. We would get conflicts.
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVisible => this.showDataSourceSelection;
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
this.showDataSourceSelection = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAndApplyFilters()
|
||||||
|
{
|
||||||
|
if(this.DataSourceOptions.DisableDataSources)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.waitingForDataSources = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
// Load the data sources:
|
||||||
|
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.selectedDataSources);
|
||||||
|
this.availableDataSources = sources.AllowedDataSources;
|
||||||
|
this.selectedDataSources = sources.SelectedDataSources;
|
||||||
|
this.waitingForDataSources = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnabledChanged(bool state)
|
||||||
|
{
|
||||||
|
this.areDataSourcesEnabled = state;
|
||||||
|
this.DataSourceOptions.DisableDataSources = !this.areDataSourcesEnabled;
|
||||||
|
|
||||||
|
await this.LoadAndApplyFilters();
|
||||||
|
await this.OptionsChanged();
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AutoModeChanged(bool state)
|
||||||
|
{
|
||||||
|
this.aiBasedSourceSelection = state;
|
||||||
|
this.DataSourceOptions.AutomaticDataSourceSelection = this.aiBasedSourceSelection;
|
||||||
|
|
||||||
|
await this.OptionsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidationModeChanged(bool state)
|
||||||
|
{
|
||||||
|
this.aiBasedValidation = state;
|
||||||
|
this.DataSourceOptions.AutomaticValidation = this.aiBasedValidation;
|
||||||
|
|
||||||
|
await this.OptionsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectionChanged(IReadOnlyCollection<IDataSource>? chosenDataSources)
|
||||||
|
{
|
||||||
|
this.selectedDataSources = chosenDataSources ?? [];
|
||||||
|
this.DataSourceOptions.PreselectedDataSourceIds = this.selectedDataSources.Select(ds => ds.Id).ToList();
|
||||||
|
|
||||||
|
await this.OptionsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OptionsChanged()
|
||||||
|
{
|
||||||
|
this.internalChange = true;
|
||||||
|
|
||||||
|
await this.DataSourceOptionsChanged.InvokeAsync(this.DataSourceOptions);
|
||||||
|
|
||||||
|
if(this.AutoSaveAppSettings)
|
||||||
|
await this.SettingsManager.StoreSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleDataSourceSelection()
|
||||||
|
{
|
||||||
|
this.showDataSourceSelection = !this.showDataSourceSelection;
|
||||||
|
if (this.showDataSourceSelection)
|
||||||
|
await this.LoadAndApplyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideDataSourceSelection() => this.showDataSourceSelection = false;
|
||||||
|
|
||||||
|
#region Implementation of IMessageBusReceiver
|
||||||
|
|
||||||
|
public string ComponentName => nameof(ConfidenceInfo);
|
||||||
|
|
||||||
|
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||||
|
{
|
||||||
|
switch (triggeredEvent)
|
||||||
|
{
|
||||||
|
case Event.COLOR_THEME_CHANGED:
|
||||||
|
this.showDataSourceSelection = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.RAG_AUTO_DATA_SOURCES_SELECTED:
|
||||||
|
if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources)
|
||||||
|
this.DataSourcesAISelected = aiSelectedDataSources;
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||||
|
{
|
||||||
|
return Task.FromResult<TResult?>(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.MessageBus.Unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public enum DataSourceSelectionMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user is selecting data sources for, e.g., the chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In this case, we have to filter the data sources based on the
|
||||||
|
/// selected provider and check security requirements.
|
||||||
|
/// </remarks>
|
||||||
|
SELECTION_MODE,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user is configuring the default data sources, e.g., for the chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In this case, all data sources are available for selection.
|
||||||
|
/// They get filtered later based on the selected provider and
|
||||||
|
/// security requirements.
|
||||||
|
/// </remarks>
|
||||||
|
CONFIGURATION_MODE,
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ public partial class MudTextList : ComponentBase
|
|||||||
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
|
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Class { get; set; } = "";
|
public string Class { get; set; } = string.Empty;
|
||||||
|
|
||||||
private string Classes => $"mud-text-list {this.Class}";
|
private string Classes => $"mud-text-list {this.Class}";
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
namespace AIStudio.Components;
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
public enum ConfidenceInfoMode
|
public enum PopoverTriggerMode
|
||||||
{
|
{
|
||||||
BUTTON,
|
BUTTON,
|
||||||
ICON,
|
ICON,
|
||||||
@@ -15,10 +15,13 @@ public partial class ProfileSelection : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string MarginLeft { get; set; } = "ml-3";
|
public string MarginLeft { get; set; } = "ml-3";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string MarginRight { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; init; } = null!;
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
private string MarginClass => $"{this.MarginLeft}";
|
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
|
||||||
|
|
||||||
private async Task SelectionChanged(Profile profile)
|
private async Task SelectionChanged(Profile profile)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@using AIStudio.Settings
|
@using AIStudio.Settings
|
||||||
|
|
||||||
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
|
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined">
|
||||||
@foreach (var provider in this.GetAvailableProviders())
|
@foreach (var provider in this.GetAvailableProviders())
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@provider"/>
|
<MudSelectItem Value="@provider"/>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
using AIStudio.Assistants;
|
using AIStudio.Assistants;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
@@ -29,6 +31,7 @@ public partial class ProviderSelection : ComponentBase
|
|||||||
await this.ProviderSettingsChanged.InvokeAsync(provider);
|
await this.ProviderSettingsChanged.InvokeAsync(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
|
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
|
||||||
{
|
{
|
||||||
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);
|
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);
|
||||||
|
|||||||
@@ -59,11 +59,7 @@ public partial class ReadWebContent : ComponentBase
|
|||||||
if(this.PreselectContentCleanerAgent)
|
if(this.PreselectContentCleanerAgent)
|
||||||
this.useContentCleanerAgent = true;
|
this.useContentCleanerAgent = true;
|
||||||
|
|
||||||
if (this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)
|
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_TEXT_CONTENT_CLEANER, this.ProviderSettings.Id, true);
|
||||||
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider);
|
|
||||||
else
|
|
||||||
this.providerSettings = this.ProviderSettings;
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
@inherits SettingsPanelBase
|
||||||
|
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.SelectAll" HeaderText="Agent: Data Source Selection Options">
|
||||||
|
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
|
Use Case: this agent is used to select the appropriate data sources for the current prompt.
|
||||||
|
</MudText>
|
||||||
|
<ConfigurationOption OptionDescription="Preselect data source selection options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions = updatedState)" OptionHelp="When enabled, you can preselect some agent options. This is might be useful when you prefer a LLM."/>
|
||||||
|
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider = selectedValue)"/>
|
||||||
|
</MudPaper>
|
||||||
|
</ExpansionPanel>
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Components.Settings;
|
||||||
|
|
||||||
|
public partial class SettingsPanelAgentDataSourceSelection : SettingsPanelBase;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
@inherits SettingsPanelBase
|
||||||
|
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Assessment" HeaderText="Agent: Retrieval Context Validation Options">
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-3">
|
||||||
|
Use Case: this agent is used to validate any retrieval context of any retrieval process. Perhaps there are many of these
|
||||||
|
retrieval contexts and you want to validate them all. Therefore, you might want to use a cheap and fast LLM for this
|
||||||
|
job. When using a local or self-hosted LLM, look for a small (e.g. 3B) and fast model.
|
||||||
|
</MudText>
|
||||||
|
<ConfigurationOption OptionDescription="Enable the retrieval context validation agent?" LabelOn="The validation agent is enabled" LabelOff="No validation is performed" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation = updatedState)" OptionHelp="When enabled, the retrieval context validation agent will check each retrieval context of any retrieval process, whether a context makes sense for the given prompt."/>
|
||||||
|
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||||
|
<ConfigurationOption OptionDescription="Preselect retrieval context validation options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions = updatedState)" OptionHelp="When enabled, you can preselect some agent options. This is might be useful when you prefer a LLM."/>
|
||||||
|
<ConfigurationSlider T="int" OptionDescription="How many validation agents should work simultaneously?" Min="1" Max="100" Step="1" Unit="agents" Value="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.NumParallelValidations)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.NumParallelValidations = updatedValue)" OptionHelp="More active agents also mean that a corresponding number of requests are made simultaneously. Some providers limit the number of requests per minute. When you are unsure, choose a low setting between 1 to 6 agents."/>
|
||||||
|
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider = selectedValue)"/>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
</ExpansionPanel>
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Components.Settings;
|
||||||
|
|
||||||
|
public partial class SettingsPanelAgentRetrievalContextValidation : SettingsPanelBase;
|
||||||
@@ -19,6 +19,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
|
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
|
||||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
|
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@using AIStudio.Settings
|
@using AIStudio.Settings
|
||||||
|
@using AIStudio.Settings.DataModel
|
||||||
@inherits SettingsPanelBase
|
@inherits SettingsPanelBase
|
||||||
|
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Chat" HeaderText="Chat Options">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Chat" HeaderText="Chat Options">
|
||||||
@@ -9,7 +10,13 @@
|
|||||||
|
|
||||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||||
<ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/>
|
<ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/>
|
||||||
<ConfigurationProviderSelection Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
|
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
|
||||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for chats?"/>
|
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for chats?"/>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<DataSourceSelection SelectionMode="DataSourceSelectionMode.CONFIGURATION_MODE" AutoSaveAppSettings="@true" @bind-DataSourceOptions="@this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions" ConfigurationHeaderMessage="You can set default data sources and options for new chats. You can change these settings later for each individual chat."/>
|
||||||
|
<ConfigurationSelect OptionDescription="Apply default data source option when sending assistant results to chat" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.SendToChatDataSourceBehavior)" Data="@ConfigurationSelectDataFactory.GetSendToChatDataSourceBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.SendToChatDataSourceBehavior = selectedValue)" OptionHelp="Do you want to apply the default data source options when sending assistant results to chat?"/>
|
||||||
|
}
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
@using AIStudio.Settings
|
|
||||||
@using AIStudio.Settings.DataModel
|
@using AIStudio.Settings.DataModel
|
||||||
@inherits SettingsPanelBase
|
@inherits SettingsPanelBase
|
||||||
|
|
||||||
@@ -37,12 +36,7 @@
|
|||||||
<MudTd>@this.GetEmbeddingName(context)</MudTd>
|
<MudTd>@this.GetEmbeddingName(context)</MudTd>
|
||||||
|
|
||||||
<MudTd Style="text-align: left;">
|
<MudTd Style="text-align: left;">
|
||||||
@if (context is IERIDataSource)
|
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"/>
|
||||||
{
|
|
||||||
@* <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Info" Class="ma-2" OnClick="() => this.ShowInformation(context)"> *@
|
|
||||||
@* Show Information *@
|
|
||||||
@* </MudButton> *@
|
|
||||||
}
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)">
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditDataSource(context)">
|
||||||
Edit
|
Edit
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using AIStudio.Dialogs;
|
using AIStudio.Dialogs;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
using ERI_Client.V1;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
@@ -216,10 +215,37 @@ public partial class SettingsPanelDataSources : SettingsPanelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ShowInformation(IDataSource dataSource)
|
private async Task ShowInformation(IDataSource dataSource)
|
||||||
{
|
{
|
||||||
#warning Implement the information dialog for ERI data sources.
|
switch (dataSource)
|
||||||
return Task.CompletedTask;
|
{
|
||||||
|
case DataSourceLocalFile localFile:
|
||||||
|
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileInfoDialog>
|
||||||
|
{
|
||||||
|
{ x => x.DataSource, localFile },
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.DialogService.ShowAsync<DataSourceLocalFileInfoDialog>("Local File Data Source Information", localFileDialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSourceLocalDirectory localDirectory:
|
||||||
|
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryInfoDialog>
|
||||||
|
{
|
||||||
|
{ x => x.DataSource, localDirectory },
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.DialogService.ShowAsync<DataSourceLocalDirectoryInfoDialog>("Local Directory Data Source Information", localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DataSourceERI_V1 eriV1DataSource:
|
||||||
|
var eriV1DialogParameters = new DialogParameters<DataSourceERI_V1InfoDialog>
|
||||||
|
{
|
||||||
|
{ x => x.DataSource, eriV1DataSource },
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.DialogService.ShowAsync<DataSourceERI_V1InfoDialog>("ERI v1 Data Source Information", eriV1DialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateDataSources()
|
private async Task UpdateDataSources()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
using AIStudio.Dialogs;
|
using AIStudio.Dialogs;
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
@@ -26,6 +28,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private async Task AddLLMProvider()
|
private async Task AddLLMProvider()
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ProviderDialog>
|
var dialogParameters = new DialogParameters<ProviderDialog>
|
||||||
@@ -48,6 +51,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
|||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
|
private async Task EditLLMProvider(AIStudio.Settings.Provider provider)
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters<ProviderDialog>
|
var dialogParameters = new DialogParameters<ProviderDialog>
|
||||||
@@ -82,6 +86,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
|||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider)
|
private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider)
|
||||||
{
|
{
|
||||||
var dialogParameters = new DialogParameters
|
var dialogParameters = new DialogParameters
|
||||||
@@ -112,6 +117,7 @@ public partial class SettingsPanelProviders : SettingsPanelBase
|
|||||||
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||||
private async Task UpdateProviders()
|
private async Task UpdateProviders()
|
||||||
{
|
{
|
||||||
this.AvailableLLMProviders.Clear();
|
this.AvailableLLMProviders.Clear();
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
ReadOnly="@true"
|
||||||
|
Label="@this.Label"
|
||||||
|
Text="@this.Value"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@this.Icon"
|
||||||
|
UserAttributes="@USER_INPUT_ATTRIBUTES" />
|
||||||
|
|
||||||
|
@if (this.ShowingCopyButton)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@this.ClipboardTooltip">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Size="Size.Medium" OnClick="@(() => this.CopyToClipboard(this.Value))"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class TextInfoLine : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ClipboardTooltipSubject { get; set; } = "the text";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowingCopyButton { get; set; } = true;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ISnackbar Snackbar { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Configure the spellchecking for the user input:
|
||||||
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
|
private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
|
||||||
|
|
||||||
|
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<MudStack Row="@true" AlignItems="AlignItems.Start">
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
ReadOnly="@true"
|
||||||
|
Label="@this.Label"
|
||||||
|
Text="@this.Value"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Lines="3"
|
||||||
|
MaxLines="@this.MaxLines"
|
||||||
|
AutoGrow="@true"
|
||||||
|
Style="@this.GetColor()"
|
||||||
|
UserAttributes="@USER_INPUT_ATTRIBUTES" />
|
||||||
|
|
||||||
|
@if (this.ShowingCopyButton)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@this.ClipboardTooltip">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Size="Size.Medium" OnClick="@(() => this.CopyToClipboard(this.Value))"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class TextInfoLines : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ClipboardTooltipSubject { get; set; } = "the text";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int MaxLines { get; set; } = 30;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowingCopyButton { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public TextColor Color { get; set; } = TextColor.DEFAULT;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ISnackbar Snackbar { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Configure the spellchecking for the user input:
|
||||||
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
|
private string ClipboardTooltip => $"Copy {this.ClipboardTooltipSubject} to the clipboard";
|
||||||
|
|
||||||
|
private async Task CopyToClipboard(string content) => await this.RustService.CopyText2Clipboard(this.Snackbar, content);
|
||||||
|
|
||||||
|
private string GetColor()
|
||||||
|
{
|
||||||
|
var htmlColorCode = this.Color.GetHTMLColor(this.SettingsManager);
|
||||||
|
if(string.IsNullOrWhiteSpace(htmlColorCode))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return $"color: {htmlColorCode} !important;";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
@using AIStudio.Tools.ERIClient.DataModel
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h5">
|
||||||
|
Common data source information
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.NetworkCheck" Label="ERI server hostname" Value="@this.DataSource.Hostname" ClipboardTooltipSubject="the ERI server hostname"/>
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="ERI server port" Value="@this.Port" ClipboardTooltipSubject="the ERI server port"/>
|
||||||
|
|
||||||
|
@if (!this.IsConnectionEncrypted())
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||||
|
Please note: the connection to the ERI v1 server is not encrypted. This means that all
|
||||||
|
data sent to the server is transmitted in plain text. Please ask the ERI server administrator
|
||||||
|
to enable encryption.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||||
|
{
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="Username" Value="@this.DataSource.Username" ClipboardTooltipSubject="the username"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLines Label="Server description" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="the server description"/>
|
||||||
|
<TextInfoLines Label="Security requirements of the data provider" MaxLines="3" Value="@this.securityRequirements.Explain()" ClipboardTooltipSubject="the security requirements of the data provider"/>
|
||||||
|
<TextInfoLines Label="Your security policy" MaxLines="3" Value="@this.DataSource.SecurityPolicy.ToInfoText()" Color="@this.DataSource.SecurityPolicy.GetColor()" ClipboardTooltipSubject="your security policy"/>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h5" Class="mt-6">
|
||||||
|
Retrieval information
|
||||||
|
</MudText>
|
||||||
|
@if (!this.retrievalInfoformation.Any())
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Info" Class="mb-3">
|
||||||
|
The data source does not provide any retrieval information.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudExpansionPanels Class="mb-3">
|
||||||
|
@for (var index = 0; index < this.retrievalInfoformation.Count; index++)
|
||||||
|
{
|
||||||
|
var info = this.retrievalInfoformation[index];
|
||||||
|
<ExpansionPanel HeaderText="@this.RetrievalName(info)" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="index == 0">
|
||||||
|
<TextInfoLines Label="Description" MaxLines="14" Value="@info.Description" ClipboardTooltipSubject="the retrieval description"/>
|
||||||
|
<TextInfoLines Label="Parameters" MaxLines="14" Value="@this.RetrievalParameters(info)" ClipboardTooltipSubject="the retrieval parameters"/>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(info.Link))
|
||||||
|
{
|
||||||
|
<MudButton Href="@info.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
|
||||||
|
Open web link, show more information
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6" Class="mt-3">
|
||||||
|
Embeddings
|
||||||
|
</MudText>
|
||||||
|
@if (!info.Embeddings.Any())
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Info" Class="mb-3">
|
||||||
|
The data source does not provide any embedding information.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudExpansionPanels>
|
||||||
|
@for (var embeddingIndex = 0; embeddingIndex < info.Embeddings.Count; embeddingIndex++)
|
||||||
|
{
|
||||||
|
var embedding = info.Embeddings[embeddingIndex];
|
||||||
|
<ExpansionPanel HeaderText="@embedding.EmbeddingName" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="embeddingIndex == 0">
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.FormatShapes" Label="Type" Value="@embedding.EmbeddingType" ClipboardTooltipSubject="the embedding type"/>
|
||||||
|
<TextInfoLines Label="Description" MaxLines="14" Value="@embedding.Description" ClipboardTooltipSubject="the embedding description"/>
|
||||||
|
<TextInfoLines Label="When to use" MaxLines="3" Value="@embedding.UsedWhen" ClipboardTooltipSubject="when is the embedding used"/>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(embedding.Link))
|
||||||
|
{
|
||||||
|
<MudButton Href="@embedding.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
|
||||||
|
Open web link, show more information
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</ExpansionPanel>
|
||||||
|
}
|
||||||
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
|
</ExpansionPanel>
|
||||||
|
}
|
||||||
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Issues IssuesData="@this.dataIssues"/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
@if (this.IsOperationInProgress)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="ml-5 mr-5"/>
|
||||||
|
}
|
||||||
|
<MudButton OnClick="@this.GetERIMetadata" Variant="Variant.Filled" Color="Color.Info">Reload</MudButton>
|
||||||
|
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using AIStudio.Assistants.ERI;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
|
||||||
|
|
||||||
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
|
public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposable, ISecretId
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public DataSourceERI_V1 DataSource { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.eriServerTasks.Add(this.GetERIMetadata());
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource cts = new();
|
||||||
|
private readonly List<Task> eriServerTasks = new();
|
||||||
|
private readonly List<string> dataIssues = [];
|
||||||
|
|
||||||
|
private string serverDescription = string.Empty;
|
||||||
|
private ProviderType securityRequirements = ProviderType.NONE;
|
||||||
|
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
||||||
|
|
||||||
|
private bool IsOperationInProgress { get; set; } = true;
|
||||||
|
|
||||||
|
private bool IsConnectionEncrypted() => this.DataSource.Hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
|
||||||
|
|
||||||
|
private string RetrievalName(RetrievalInfo retrievalInfo)
|
||||||
|
{
|
||||||
|
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
|
||||||
|
var hasName = !string.IsNullOrWhiteSpace(retrievalInfo.Name);
|
||||||
|
|
||||||
|
if (hasId && hasName)
|
||||||
|
return $"[{retrievalInfo.Id}] {retrievalInfo.Name}";
|
||||||
|
|
||||||
|
if (hasId)
|
||||||
|
return $"[{retrievalInfo.Id}] Unnamed retrieval process";
|
||||||
|
|
||||||
|
return hasName ? retrievalInfo.Name : "Unnamed retrieval process";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string RetrievalParameters(RetrievalInfo retrievalInfo)
|
||||||
|
{
|
||||||
|
var parameters = retrievalInfo.ParametersDescription;
|
||||||
|
if (parameters is null || parameters.Count == 0)
|
||||||
|
return "This retrieval process has no parameters.";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var (paramName, description) in parameters)
|
||||||
|
{
|
||||||
|
sb.Append("Parameter: ");
|
||||||
|
sb.AppendLine(paramName);
|
||||||
|
sb.AppendLine(description);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetERIMetadata()
|
||||||
|
{
|
||||||
|
this.dataIssues.Clear();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.IsOperationInProgress = true;
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
|
||||||
|
if(client is null)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add("Failed to connect to the ERI v1 server. The server is not supported.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService);
|
||||||
|
if (!loginResult.Successful)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add(loginResult.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataSourceInfo = await client.GetDataSourceInfoAsync(this.cts.Token);
|
||||||
|
if (!dataSourceInfo.Successful)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add(dataSourceInfo.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.serverDescription = dataSourceInfo.Data.Description;
|
||||||
|
|
||||||
|
var securityRequirementsResult = await client.GetSecurityRequirementsAsync(this.cts.Token);
|
||||||
|
if (!securityRequirementsResult.Successful)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add(securityRequirementsResult.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.securityRequirements = securityRequirementsResult.Data.AllowedProviderType;
|
||||||
|
|
||||||
|
var retrievalInfoResult = await client.GetRetrievalInfoAsync(this.cts.Token);
|
||||||
|
if (!retrievalInfoResult.Successful)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add(retrievalInfoResult.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.retrievalInfoformation = retrievalInfoResult.Data ?? [];
|
||||||
|
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.dataIssues.Add($"Failed to connect to the ERI v1 server. The message was: {e.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.IsOperationInProgress = false;
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
this.cts.Cancel();
|
||||||
|
this.MudDialog.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Implementation of ISecretId
|
||||||
|
|
||||||
|
public string SecretId => this.DataSource.Id;
|
||||||
|
|
||||||
|
public string SecretName => this.DataSource.Name;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.cts.CancelAsync();
|
||||||
|
await Task.WhenAll(this.eriServerTasks);
|
||||||
|
|
||||||
|
this.cts.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@using ERI_Client.V1
|
@using AIStudio.Settings.DataModel
|
||||||
|
@using AIStudio.Tools.ERIClient.DataModel
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||||
@@ -109,6 +110,13 @@
|
|||||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="@this.dataSecurityPolicy" Text="@this.dataSecurityPolicy.ToSelectionText()" Label="Your security policy" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateSecurityPolicy">
|
||||||
|
@foreach (var policy in Enum.GetValues<DataSourceSecurity>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@policy">@policy.ToSelectionText()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
</MudForm>
|
</MudForm>
|
||||||
<Issues IssuesData="@this.dataIssues"/>
|
<Issues IssuesData="@this.dataIssues"/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
using AIStudio.Assistants.ERI;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
using AIStudio.Tools.Validation;
|
using AIStudio.Tools.Validation;
|
||||||
|
|
||||||
using ERI_Client.V1;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
@@ -43,8 +45,9 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
|||||||
private string[] dataIssues = [];
|
private string[] dataIssues = [];
|
||||||
private string dataSecretStorageIssue = string.Empty;
|
private string dataSecretStorageIssue = string.Empty;
|
||||||
private string dataEditingPreviousInstanceName = string.Empty;
|
private string dataEditingPreviousInstanceName = string.Empty;
|
||||||
private HttpClient? httpClient;
|
|
||||||
private List<AuthMethod> availableAuthMethods = [];
|
private List<AuthMethod> availableAuthMethods = [];
|
||||||
|
private DataSourceSecurity dataSecurityPolicy;
|
||||||
|
private SecurityRequirements dataSourceSecurityRequirements;
|
||||||
private bool connectionTested;
|
private bool connectionTested;
|
||||||
private bool connectionSuccessfulTested;
|
private bool connectionSuccessfulTested;
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
|||||||
GetTestedConnection = () => this.connectionTested,
|
GetTestedConnection = () => this.connectionTested,
|
||||||
GetTestedConnectionResult = () => this.connectionSuccessfulTested,
|
GetTestedConnectionResult = () => this.connectionSuccessfulTested,
|
||||||
GetAvailableAuthMethods = () => this.availableAuthMethods,
|
GetAvailableAuthMethods = () => this.availableAuthMethods,
|
||||||
|
GetSecurityRequirements = () => this.dataSourceSecurityRequirements,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +99,7 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
|||||||
this.dataPort = this.DataSource.Port;
|
this.dataPort = this.DataSource.Port;
|
||||||
this.dataAuthMethod = this.DataSource.AuthMethod;
|
this.dataAuthMethod = this.DataSource.AuthMethod;
|
||||||
this.dataUsername = this.DataSource.Username;
|
this.dataUsername = this.DataSource.Username;
|
||||||
|
this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
|
||||||
|
|
||||||
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
||||||
{
|
{
|
||||||
@@ -147,6 +152,7 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
|||||||
AuthMethod = this.dataAuthMethod,
|
AuthMethod = this.dataAuthMethod,
|
||||||
Username = this.dataUsername,
|
Username = this.dataUsername,
|
||||||
Type = DataSourceType.ERI_V1,
|
Type = DataSourceType.ERI_V1,
|
||||||
|
SecurityPolicy = this.dataSecurityPolicy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,31 +173,60 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.httpClient = new HttpClient
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(14));
|
||||||
|
var dataSource = new DataSourceERI_V1
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri($"{this.dataHostname}:{this.dataPort}"),
|
Hostname = this.dataHostname,
|
||||||
Timeout = TimeSpan.FromSeconds(5),
|
Port = this.dataPort
|
||||||
};
|
};
|
||||||
|
|
||||||
using (this.httpClient)
|
using var client = ERIClientFactory.Get(ERIVersion.V1, dataSource);
|
||||||
|
if(client is null)
|
||||||
{
|
{
|
||||||
var client = new Client(this.httpClient);
|
await this.form.Validate();
|
||||||
var authSchemes = await client.GetAuthMethodsAsync();
|
|
||||||
if (authSchemes is null)
|
|
||||||
{
|
|
||||||
await this.form.Validate();
|
|
||||||
|
|
||||||
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||||
this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server did not respond.";
|
this.dataIssues[^1] = "Failed to connect to the ERI v1 server. The server is not supported.";
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
this.availableAuthMethods = authSchemes.Select(n => n.AuthMethod).ToList();
|
|
||||||
|
|
||||||
this.connectionTested = true;
|
|
||||||
this.connectionSuccessfulTested = true;
|
|
||||||
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authSchemes = await client.GetAuthMethodsAsync(cts.Token);
|
||||||
|
if (!authSchemes.Successful)
|
||||||
|
{
|
||||||
|
await this.form.Validate();
|
||||||
|
|
||||||
|
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||||
|
this.dataIssues[^1] = authSchemes.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.availableAuthMethods = authSchemes.Data!.Select(n => n.AuthMethod).ToList();
|
||||||
|
|
||||||
|
var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService, cts.Token);
|
||||||
|
if (!loginResult.Successful)
|
||||||
|
{
|
||||||
|
await this.form.Validate();
|
||||||
|
|
||||||
|
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||||
|
this.dataIssues[^1] = loginResult.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityRequirementsRequest = await client.GetSecurityRequirementsAsync(cts.Token);
|
||||||
|
if (!securityRequirementsRequest.Successful)
|
||||||
|
{
|
||||||
|
await this.form.Validate();
|
||||||
|
|
||||||
|
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||||
|
this.dataIssues[^1] = securityRequirementsRequest.Message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataSourceSecurityRequirements = securityRequirementsRequest.Data;
|
||||||
|
|
||||||
|
this.connectionTested = true;
|
||||||
|
this.connectionSuccessfulTested = true;
|
||||||
|
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||||
@@ -60,6 +62,13 @@
|
|||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="@this.dataSecurityPolicy" Text="@this.dataSecurityPolicy.ToSelectionText()" Label="Your security policy" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateSecurityPolicy">
|
||||||
|
@foreach (var policy in Enum.GetValues<DataSourceSecurity>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@policy">@policy.ToSelectionText()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
</MudForm>
|
</MudForm>
|
||||||
<Issues IssuesData="@this.dataIssues"/>
|
<Issues IssuesData="@this.dataIssues"/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public partial class DataSourceLocalDirectoryDialog : ComponentBase
|
|||||||
private bool dataUserAcknowledgedCloudEmbedding;
|
private bool dataUserAcknowledgedCloudEmbedding;
|
||||||
private string dataEmbeddingId = string.Empty;
|
private string dataEmbeddingId = string.Empty;
|
||||||
private string dataPath = string.Empty;
|
private string dataPath = string.Empty;
|
||||||
|
private DataSourceSecurity dataSecurityPolicy;
|
||||||
|
|
||||||
// We get the form reference from Blazor code to validate it manually:
|
// We get the form reference from Blazor code to validate it manually:
|
||||||
private MudForm form = null!;
|
private MudForm form = null!;
|
||||||
@@ -75,6 +76,7 @@ public partial class DataSourceLocalDirectoryDialog : ComponentBase
|
|||||||
this.dataName = this.DataSource.Name;
|
this.dataName = this.DataSource.Name;
|
||||||
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
||||||
this.dataPath = this.DataSource.Path;
|
this.dataPath = this.DataSource.Path;
|
||||||
|
this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@@ -102,6 +104,7 @@ public partial class DataSourceLocalDirectoryDialog : ComponentBase
|
|||||||
Type = DataSourceType.LOCAL_DIRECTORY,
|
Type = DataSourceType.LOCAL_DIRECTORY,
|
||||||
EmbeddingId = this.dataEmbeddingId,
|
EmbeddingId = this.dataEmbeddingId,
|
||||||
Path = this.dataPath,
|
Path = this.dataPath,
|
||||||
|
SecurityPolicy = this.dataSecurityPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task Store()
|
private async Task Store()
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.FolderOpen" Label="Path" Value="@this.DataSource.Path" ClipboardTooltipSubject="this path"/>
|
||||||
|
@if (!this.IsDirectoryAvailable)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||||
|
The directory chosen for the data source does not exist anymore. Please edit the data source and correct the path.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||||
|
The directory chosen for the data source exists.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Layers" Label="Embedding name" Value="@this.embeddingProvider.Name" ClipboardTooltipSubject="the embedding name"/>
|
||||||
|
@if (this.IsCloudEmbedding)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||||
|
The embedding runs in the cloud. All your data from the folder '@this.DataSource.Path' and all its subdirectories
|
||||||
|
will be sent to the cloud.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||||
|
The embedding runs locally or in your organization. Your data is not sent to the cloud.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLines Label="Your security policy" MaxLines="3" Value="@this.DataSource.SecurityPolicy.ToInfoText()" Color="@this.DataSource.SecurityPolicy.GetColor()" ClipboardTooltipSubject="your security policy"/>
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="Number of files" Value="@this.NumberFilesInDirectory" ClipboardTooltipSubject="the number of files in the directory"/>
|
||||||
|
<TextInfoLines Label="Files list" MaxLines="14" Value="@this.directoryFiles.ToString()" ClipboardTooltipSubject="the files list"/>
|
||||||
|
@if (this.directorySizeNumFiles > 100)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Warning" Class="mb-3">
|
||||||
|
For performance reasons, only the first 100 files are shown. The directory contains @this.NumberFilesInDirectory files in total.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="Total directory size" Value="@this.directorySizeBytes.FileSize()" ClipboardTooltipSubject="the total directory size"/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
@if (this.IsOperationInProgress)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="ml-5 mr-5"/>
|
||||||
|
}
|
||||||
|
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
|
public partial class DataSourceLocalDirectoryInfoDialog : ComponentBase, IAsyncDisposable
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public DataSourceLocalDirectory DataSource { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
private readonly Timer refreshTimer = new(TimeSpan.FromSeconds(1.6))
|
||||||
|
{
|
||||||
|
AutoReset = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
|
||||||
|
this.directoryInfo = new DirectoryInfo(this.DataSource.Path);
|
||||||
|
|
||||||
|
if (this.directoryInfo.Exists)
|
||||||
|
{
|
||||||
|
this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token);
|
||||||
|
this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged);
|
||||||
|
this.refreshTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private const int MAX_FILES_TO_SHOW = 100;
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource cts = new();
|
||||||
|
|
||||||
|
private EmbeddingProvider embeddingProvider;
|
||||||
|
private DirectoryInfo directoryInfo = null!;
|
||||||
|
private long directorySizeBytes;
|
||||||
|
private long directorySizeNumFiles;
|
||||||
|
private readonly StringBuilder directoryFiles = new();
|
||||||
|
private Task directorySizeTask = Task.CompletedTask;
|
||||||
|
|
||||||
|
private bool IsOperationInProgress { get; set; } = true;
|
||||||
|
|
||||||
|
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
|
||||||
|
|
||||||
|
private bool IsDirectoryAvailable => this.directoryInfo.Exists;
|
||||||
|
|
||||||
|
private void UpdateFileList(string file)
|
||||||
|
{
|
||||||
|
this.directoryFiles.Append("- ");
|
||||||
|
this.directoryFiles.AppendLine(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDirectorySize(long size)
|
||||||
|
{
|
||||||
|
this.directorySizeBytes = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles;
|
||||||
|
|
||||||
|
private void DirectoryOperationDone()
|
||||||
|
{
|
||||||
|
this.refreshTimer.Stop();
|
||||||
|
this.IsOperationInProgress = false;
|
||||||
|
this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NumberFilesInDirectory => $"{this.directorySizeNumFiles:###,###,###,###}";
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
this.cts.Cancel();
|
||||||
|
this.MudDialog.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Implementation of IDisposable
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.cts.CancelAsync();
|
||||||
|
await this.directorySizeTask;
|
||||||
|
|
||||||
|
this.cts.Dispose();
|
||||||
|
this.refreshTimer.Stop();
|
||||||
|
this.refreshTimer.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||||
@@ -59,6 +60,13 @@
|
|||||||
</MudJustifiedText>
|
</MudJustifiedText>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="@this.dataSecurityPolicy" Text="@this.dataSecurityPolicy.ToSelectionText()" Label="Your security policy" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateSecurityPolicy">
|
||||||
|
@foreach (var policy in Enum.GetValues<DataSourceSecurity>())
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@policy">@policy.ToSelectionText()</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
</MudForm>
|
</MudForm>
|
||||||
<Issues IssuesData="@this.dataIssues"/>
|
<Issues IssuesData="@this.dataIssues"/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public partial class DataSourceLocalFileDialog : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private SettingsManager SettingsManager { get; init; } = null!;
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||||
|
|
||||||
private readonly DataSourceValidation dataSourceValidation;
|
private readonly DataSourceValidation dataSourceValidation;
|
||||||
@@ -43,6 +42,7 @@ public partial class DataSourceLocalFileDialog : ComponentBase
|
|||||||
private bool dataUserAcknowledgedCloudEmbedding;
|
private bool dataUserAcknowledgedCloudEmbedding;
|
||||||
private string dataEmbeddingId = string.Empty;
|
private string dataEmbeddingId = string.Empty;
|
||||||
private string dataFilePath = string.Empty;
|
private string dataFilePath = string.Empty;
|
||||||
|
private DataSourceSecurity dataSecurityPolicy;
|
||||||
|
|
||||||
// We get the form reference from Blazor code to validate it manually:
|
// We get the form reference from Blazor code to validate it manually:
|
||||||
private MudForm form = null!;
|
private MudForm form = null!;
|
||||||
@@ -76,6 +76,7 @@ public partial class DataSourceLocalFileDialog : ComponentBase
|
|||||||
this.dataName = this.DataSource.Name;
|
this.dataName = this.DataSource.Name;
|
||||||
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
this.dataEmbeddingId = this.DataSource.EmbeddingId;
|
||||||
this.dataFilePath = this.DataSource.FilePath;
|
this.dataFilePath = this.DataSource.FilePath;
|
||||||
|
this.dataSecurityPolicy = this.DataSource.SecurityPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@@ -103,6 +104,7 @@ public partial class DataSourceLocalFileDialog : ComponentBase
|
|||||||
Type = DataSourceType.LOCAL_FILE,
|
Type = DataSourceType.LOCAL_FILE,
|
||||||
EmbeddingId = this.dataEmbeddingId,
|
EmbeddingId = this.dataEmbeddingId,
|
||||||
FilePath = this.dataFilePath,
|
FilePath = this.dataFilePath,
|
||||||
|
SecurityPolicy = this.dataSecurityPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task Store()
|
private async Task Store()
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Tag" Label="Data source name" Value="@this.DataSource.Name" ClipboardTooltipSubject="the data source name"/>
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.FolderOpen" Label="File path" Value="@this.DataSource.FilePath" ClipboardTooltipSubject="this path"/>
|
||||||
|
@if (!this.IsFileAvailable)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||||
|
The file chosen for the data source does not exist anymore. Please edit the data source and choose another file or correct the path.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||||
|
The file chosen for the data source exists.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.Layers" Label="Embedding name" Value="@this.embeddingProvider.Name" ClipboardTooltipSubject="the embedding name"/>
|
||||||
|
@if (this.IsCloudEmbedding)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Error" Class="mb-3">
|
||||||
|
The embedding runs in the cloud. All your data within the
|
||||||
|
file '@this.DataSource.FilePath' will be sent to the cloud.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||||
|
The embedding runs locally or in your organization. Your data is not sent to the cloud.
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextInfoLines Label="Your security policy" MaxLines="3" Value="@this.DataSource.SecurityPolicy.ToInfoText()" Color="@this.DataSource.SecurityPolicy.GetColor()" ClipboardTooltipSubject="your security policy"/>
|
||||||
|
<TextInfoLine Icon="@Icons.Material.Filled.SquareFoot" Label="File size" Value="@this.FileSize" ClipboardTooltipSubject="the file size"/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
|
public partial class DataSourceLocalFileInfoDialog : ComponentBase
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public DataSourceLocalFile DataSource { get; set; }
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId);
|
||||||
|
this.fileInfo = new FileInfo(this.DataSource.FilePath);
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private EmbeddingProvider embeddingProvider;
|
||||||
|
private FileInfo fileInfo = null!;
|
||||||
|
|
||||||
|
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
|
||||||
|
|
||||||
|
private bool IsFileAvailable => this.fileInfo.Exists;
|
||||||
|
|
||||||
|
private string FileSize => this.fileInfo.FileSize();
|
||||||
|
|
||||||
|
private void Close() => this.MudDialog.Close();
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
using AIStudio.Tools.Validation;
|
using AIStudio.Tools.Validation;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
using AIStudio.Tools.Validation;
|
using AIStudio.Tools.Validation;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||||
using RustService = AIStudio.Tools.RustService;
|
|
||||||
|
|
||||||
namespace AIStudio.Dialogs;
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
@@ -137,7 +137,9 @@ public partial class ProviderDialog : ComponentBase, ISecretId
|
|||||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||||
|
|
||||||
// Load the used instance names:
|
// Load the used instance names:
|
||||||
|
#pragma warning disable MWAIS0001
|
||||||
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList();
|
||||||
|
#pragma warning restore MWAIS0001
|
||||||
|
|
||||||
// When editing, we need to load the data:
|
// When editing, we need to load the data:
|
||||||
if(this.IsEditing)
|
if(this.IsEditing)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Components;
|
|||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
using RustService = AIStudio.Tools.RustService;
|
|
||||||
|
|
||||||
namespace AIStudio.Layout;
|
namespace AIStudio.Layout;
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,14 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
|
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
|
||||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
|
||||||
<PackageReference Include="MudBlazor" Version="7.15.0" />
|
<PackageReference Include="MudBlazor" Version="7.16.0" />
|
||||||
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
|
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
|
||||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ERIClientV1\ERIClientV1.csproj" />
|
<ProjectReference Include="..\SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Read the meta data file -->
|
<!-- Read the meta data file -->
|
||||||
|
|||||||
@@ -27,6 +27,38 @@
|
|||||||
<Changelog/>
|
<Changelog/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Book" HeaderText="Logbook">
|
||||||
|
<MudText Typo="Typo.h4">
|
||||||
|
Explanation
|
||||||
|
</MudText>
|
||||||
|
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||||
|
AI Studio creates a log file at startup, in which events during startup are recorded. After startup,
|
||||||
|
another log file is created that records all events that occur during the use of the app. This
|
||||||
|
includes any errors that may occur. Depending on when an error occurs (at startup or during use),
|
||||||
|
the contents of these log files can be helpful for troubleshooting. Sensitive information such as
|
||||||
|
passwords is not included in the log files.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||||
|
By clicking on the respective path, the path is copied to the clipboard. You might open these files
|
||||||
|
with a text editor to view their contents.
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4">
|
||||||
|
Startup log file
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string" Class="mb-3">
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogStartupPath" OnClick="() => this.CopyStartupLogPath()"/>
|
||||||
|
</MudList>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4">
|
||||||
|
Usage log file
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string" Class="mb-3">
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogAppPath" OnClick="() => this.CopyAppLogPath()"/>
|
||||||
|
</MudList>
|
||||||
|
</ExpansionPanel>
|
||||||
|
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Engineering" HeaderText="Motivation">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Engineering" HeaderText="Motivation">
|
||||||
<Motivation/>
|
<Motivation/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace AIStudio.Pages;
|
namespace AIStudio.Pages;
|
||||||
@@ -9,6 +12,12 @@ public partial class About : ComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private MessageBus MessageBus { get; init; } = null!;
|
private MessageBus MessageBus { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ISnackbar Snackbar { get; init; } = null!;
|
||||||
|
|
||||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||||
|
|
||||||
@@ -26,6 +35,28 @@ public partial class About : ComponentBase
|
|||||||
|
|
||||||
private static string TauriVersion => $"Tauri: v{META_DATA.TauriVersion}";
|
private static string TauriVersion => $"Tauri: v{META_DATA.TauriVersion}";
|
||||||
|
|
||||||
|
private GetLogPathsResponse logPaths;
|
||||||
|
|
||||||
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
|
private async Task CopyStartupLogPath()
|
||||||
|
{
|
||||||
|
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyAppLogPath()
|
||||||
|
{
|
||||||
|
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogAppPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
this.logPaths = await this.RustService.GetLogPaths();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private const string LICENSE = """
|
private const string LICENSE = """
|
||||||
# Functional Source License, Version 1.1, MIT Future License
|
# Functional Source License, Version 1.1, MIT Future License
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ public partial class Home : ComponentBase
|
|||||||
private async Task ReadLastChangeAsync()
|
private async Task ReadLastChangeAsync()
|
||||||
{
|
{
|
||||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
||||||
var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
|
using var response = await this.HttpClient.GetAsync($"changelog/{latest.Filename}");
|
||||||
this.LastChangeContent = await response.Content.ReadAsStringAsync();
|
this.LastChangeContent = await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
||||||
[
|
[
|
||||||
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
|
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, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), 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, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."),
|
||||||
new("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
|
new("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
|
||||||
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
|
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("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."),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@attribute [Route(Routes.SETTINGS)]
|
@attribute [Route(Routes.SETTINGS)]
|
||||||
@using AIStudio.Components.Settings
|
@using AIStudio.Components.Settings
|
||||||
|
@using AIStudio.Settings.DataModel
|
||||||
|
|
||||||
<div class="inner-scrolling-context">
|
<div class="inner-scrolling-context">
|
||||||
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
|
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
|
||||||
@@ -7,8 +8,12 @@
|
|||||||
<InnerScrolling>
|
<InnerScrolling>
|
||||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||||
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
|
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders" />
|
||||||
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders" />
|
|
||||||
<SettingsPanelDataSources AvailableLLMProvidersFunc="() => this.availableLLMProviders" AvailableEmbeddingsFunc="() => this.availableEmbeddingProviders" @bind-AvailableDataSources="@this.availableDataSources" />
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<SettingsPanelEmbeddings AvailableLLMProvidersFunc="() => this.availableLLMProviders" @bind-AvailableEmbeddingProviders="@this.availableEmbeddingProviders"/>
|
||||||
|
<SettingsPanelDataSources AvailableLLMProvidersFunc="() => this.availableLLMProviders" AvailableEmbeddingsFunc="() => this.availableEmbeddingProviders" @bind-AvailableDataSources="@this.availableDataSources"/>
|
||||||
|
}
|
||||||
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
@@ -16,7 +21,12 @@
|
|||||||
<SettingsPanelIconFinder AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelIconFinder AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelTranslation AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelTranslation AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelCoding AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelCoding AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelERIServer AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
|
||||||
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<SettingsPanelERIServer AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||||
|
}
|
||||||
|
|
||||||
<SettingsPanelTextSummarizer AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelTextSummarizer AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelAgenda AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelAgenda AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelGrammarSpelling AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelGrammarSpelling AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
@@ -27,6 +37,13 @@
|
|||||||
<SettingsPanelSynonyms AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelSynonyms AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelMyTasks AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelMyTasks AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
<SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelAssistantBias AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
|
|
||||||
|
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||||
|
{
|
||||||
|
<SettingsPanelAgentDataSourceSelection AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||||
|
<SettingsPanelAgentRetrievalContextValidation AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
|
||||||
|
}
|
||||||
|
|
||||||
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="() => this.availableLLMProviders" />
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace AIStudio.Pages;
|
|||||||
|
|
||||||
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||||
{
|
{
|
||||||
|
[Inject]
|
||||||
|
private SettingsManager SettingsManager { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private MessageBus MessageBus { get; init; } = null!;
|
private MessageBus MessageBus { get; init; } = null!;
|
||||||
|
|
||||||
|
|||||||
@@ -2,64 +2,101 @@
|
|||||||
|
|
||||||
<div class="inner-scrolling-context">
|
<div class="inner-scrolling-context">
|
||||||
<MudText Typo="Typo.h3" Class="mb-2">Supporters</MudText>
|
<MudText Typo="Typo.h3" Class="mb-2">Supporters</MudText>
|
||||||
|
|
||||||
<InnerScrolling>
|
<InnerScrolling>
|
||||||
|
|
||||||
<div class="border-solid border-2 rounded-lg pa-3 mb-6">
|
<MudExpansionPanels>
|
||||||
<MudText Typo="Typo.h4" Class="mb-2">Our Titans</MudText>
|
|
||||||
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3">
|
|
||||||
In this section, we highlight the titan supporters of MindWork AI Studio. Titans are prestigious companies that provide significant support to our mission.
|
|
||||||
</MudText>
|
|
||||||
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3">
|
|
||||||
For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
|
|
||||||
</MudText>
|
|
||||||
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.Business" Variant="Variant.Filled" Target="_blank">
|
|
||||||
Become our first Titan
|
|
||||||
</MudButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MudGrid Justify="Justify.Center" Spacing="2" Style="width: 100%;" Class="ma-0">
|
<ExpansionPanel HeaderText="Financial Support" IsExpanded="@true" HeaderIcon="@Icons.Material.Filled.AttachMoney">
|
||||||
<MudItem Class="pa-0 pr-2" xs="6">
|
<div class="border-solid border-2 rounded-lg pa-3 mb-6">
|
||||||
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
<MudText Typo="Typo.h4" Class="mb-2">Our Titans</MudText>
|
||||||
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
|
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Large" class="mr-2"/>
|
In this section, we highlight the titan supporters of MindWork AI Studio. Titans are prestigious companies that provide significant support to our mission.
|
||||||
Individual Contributors
|
|
||||||
</MudText>
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;" Class="mb-3">
|
||||||
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.People" Variant="Variant.Filled" Target="_blank">
|
For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
|
||||||
Become a contributor
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h6" Class="mt-8">
|
|
||||||
The first 10 supporters who make a monthly contribution:
|
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudList T="string">
|
|
||||||
<Supporter Name="richard-stanton" Type="SupporterType.INDIVIDUAL" URL="https://github.com/richard-stanton" Acknowledgment="Thank you, Richard, for being the first."/>
|
|
||||||
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thank you, Peer, for your courage in being the second person to support the project financially."/>
|
|
||||||
</MudList>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.h6" Class="mt-8">
|
|
||||||
The first 10 supporters who make a one-time contribution:
|
|
||||||
</MudText>
|
|
||||||
<MudList T="string">
|
|
||||||
<Supporter Name="Bessieres" Type="SupporterType.INDIVIDUAL" URL="https://github.com/Bessieres" Acknowledgment="Thank you for being the first to contribute a one-time donation."/>
|
|
||||||
</MudList>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6" Class="pa-0 pl-2">
|
|
||||||
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Business" Size="Size.Large" class="mr-2"/>
|
|
||||||
Business Contributors
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.Business" Variant="Variant.Filled" Target="_blank">
|
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.Business" Variant="Variant.Filled" Target="_blank">
|
||||||
Become a contributor
|
Become our first Titan
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudPaper>
|
</div>
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
|
<MudGrid Justify="Justify.Center" Spacing="2" Style="width: 100%;" Class="ma-0">
|
||||||
|
<MudItem Class="pa-0 pr-2" xs="6">
|
||||||
|
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Large" class="mr-2"/>
|
||||||
|
Individual Contributors
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.People" Variant="Variant.Filled" Target="_blank">
|
||||||
|
Become a contributor
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6" Class="mt-8">
|
||||||
|
The first 10 supporters who make a monthly contribution:
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string">
|
||||||
|
<Supporter Name="richard-stanton" Type="SupporterType.INDIVIDUAL" URL="https://github.com/richard-stanton" Acknowledgment="Thank you, Richard, for being the first."/>
|
||||||
|
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thank you, Peer, for your courage in being the second person to support the project financially."/>
|
||||||
|
<Supporter Name="donework" Type="SupporterType.INDIVIDUAL" URL="https://github.com/donework" Acknowledgment="Thanks Dominic for being the third supporter."/>
|
||||||
|
</MudList>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6" Class="mt-8">
|
||||||
|
The first 10 supporters who make a one-time contribution:
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string">
|
||||||
|
<Supporter Name="Bessieres" Type="SupporterType.INDIVIDUAL" URL="https://github.com/Bessieres" Acknowledgment="Thank you for being the first to contribute a one-time donation."/>
|
||||||
|
</MudList>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem xs="6" Class="pa-0 pl-2">
|
||||||
|
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Business" Size="Size.Large" class="mr-2"/>
|
||||||
|
Business Contributors
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudButton Href="https://github.com/sponsors/MindWorkAI" StartIcon="@Icons.Material.Filled.Business" Variant="Variant.Filled" Target="_blank">
|
||||||
|
Become a contributor
|
||||||
|
</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</ExpansionPanel>
|
||||||
|
|
||||||
|
<ExpansionPanel HeaderText="Content Contributors" HeaderIcon="@Icons.Material.Filled.Code">
|
||||||
|
|
||||||
|
<MudGrid Justify="Justify.Center" Spacing="2" Style="width: 100%;" Class="ma-0">
|
||||||
|
<MudItem Class="pa-0 pr-2" xs="6">
|
||||||
|
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Code" Size="Size.Large" class="mr-2"/>
|
||||||
|
Code Contributions
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudList T="string">
|
||||||
|
<Supporter Name="DevNullx64" Type="SupporterType.INDIVIDUAL" URL="https://github.com/DevNullx64" Acknowledgment="Thanks Luc for your build script contribution."/>
|
||||||
|
<Supporter Name="SolsticeSpectrum" Type="SupporterType.INDIVIDUAL" URL="https://github.com/SolsticeSpectrum" Acknowledgment="Thanks for your build script contribution."/>
|
||||||
|
</MudList>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<MudItem Class="pa-0 pr-2" xs="6">
|
||||||
|
<MudPaper Elevation="3" Class="border-solid border rounded-lg pa-3">
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-3 d-flex align-start">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Image" Size="Size.Large" class="mr-2"/>
|
||||||
|
Moderation, Design, Wiki, and Documentation
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
<MudList T="string">
|
||||||
|
<Supporter Name="KeSah001" Type="SupporterType.INDIVIDUAL" URL="https://github.com/KeSah001" Acknowledgment="Thank you very much, Kerstin, for taking care of creating the Wiki."/>
|
||||||
|
</MudList>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</ExpansionPanel>
|
||||||
|
|
||||||
|
</MudExpansionPanels>
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,17 +106,19 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
|||||||
};
|
};
|
||||||
|
|
||||||
var time = DateTimeOffset.Now;
|
var time = DateTimeOffset.Now;
|
||||||
|
var lastUserPrompt = new ContentText
|
||||||
|
{
|
||||||
|
// We use the maximum 160 characters from the end of the text:
|
||||||
|
Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
|
||||||
|
};
|
||||||
|
|
||||||
this.chatThread.Blocks.Clear();
|
this.chatThread.Blocks.Clear();
|
||||||
this.chatThread.Blocks.Add(new ContentBlock
|
this.chatThread.Blocks.Add(new ContentBlock
|
||||||
{
|
{
|
||||||
Time = time,
|
Time = time,
|
||||||
ContentType = ContentType.TEXT,
|
ContentType = ContentType.TEXT,
|
||||||
Role = ChatRole.USER,
|
Role = ChatRole.USER,
|
||||||
Content = new ContentText
|
Content = lastUserPrompt,
|
||||||
{
|
|
||||||
// We use the maximum 160 characters from the end of the text:
|
|
||||||
Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var aiText = new ContentText
|
var aiText = new ContentText
|
||||||
@@ -137,7 +139,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
|||||||
this.isStreaming = true;
|
this.isStreaming = true;
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
|
||||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||||
this.suggestion = aiText.Text;
|
this.suggestion = aiText.Text;
|
||||||
|
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ internal sealed class Program
|
|||||||
public static RustService RUST_SERVICE = null!;
|
public static RustService RUST_SERVICE = null!;
|
||||||
public static Encryption ENCRYPTION = null!;
|
public static Encryption ENCRYPTION = null!;
|
||||||
public static string API_TOKEN = null!;
|
public static string API_TOKEN = null!;
|
||||||
|
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||||
|
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
@@ -115,7 +116,10 @@ internal sealed class Program
|
|||||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||||
builder.Services.AddSingleton<SettingsManager>();
|
builder.Services.AddSingleton<SettingsManager>();
|
||||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||||
|
builder.Services.AddSingleton<DataSourceService>();
|
||||||
builder.Services.AddTransient<HTMLParser>();
|
builder.Services.AddTransient<HTMLParser>();
|
||||||
|
builder.Services.AddTransient<AgentDataSourceSelection>();
|
||||||
|
builder.Services.AddTransient<AgentRetrievalContextValidation>();
|
||||||
builder.Services.AddTransient<AgentTextContentCleaner>();
|
builder.Services.AddTransient<AgentTextContentCleaner>();
|
||||||
builder.Services.AddHostedService<UpdateService>();
|
builder.Services.AddHostedService<UpdateService>();
|
||||||
builder.Services.AddHostedService<TemporaryChatService>();
|
builder.Services.AddHostedService<TemporaryChatService>();
|
||||||
@@ -143,12 +147,22 @@ internal sealed class Program
|
|||||||
// Execute the builder to get the app:
|
// Execute the builder to get the app:
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Get a program logger:
|
||||||
|
var programLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
programLogger.LogInformation("Starting the AI Studio server.");
|
||||||
|
|
||||||
|
// Store the service provider (DI). We need it later for some classes,
|
||||||
|
// which are not part of the request pipeline:
|
||||||
|
SERVICE_PROVIDER = app.Services;
|
||||||
|
|
||||||
// Initialize the encryption service:
|
// Initialize the encryption service:
|
||||||
|
programLogger.LogInformation("Initializing the encryption service.");
|
||||||
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
|
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
|
||||||
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
|
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
|
||||||
var encryptionInitializer = encryption.Initialize();
|
var encryptionInitializer = encryption.Initialize();
|
||||||
|
|
||||||
// Set the logger for the Rust service:
|
// Set the logger for the Rust service:
|
||||||
|
programLogger.LogInformation("Initializing the Rust service.");
|
||||||
var rustLogger = app.Services.GetRequiredService<ILogger<RustService>>();
|
var rustLogger = app.Services.GetRequiredService<ILogger<RustService>>();
|
||||||
rust.SetLogger(rustLogger);
|
rust.SetLogger(rustLogger);
|
||||||
rust.SetEncryptor(encryption);
|
rust.SetEncryptor(encryption);
|
||||||
@@ -156,6 +170,7 @@ internal sealed class Program
|
|||||||
RUST_SERVICE = rust;
|
RUST_SERVICE = rust;
|
||||||
ENCRYPTION = encryption;
|
ENCRYPTION = encryption;
|
||||||
|
|
||||||
|
programLogger.LogInformation("Initialize internal file system.");
|
||||||
app.Use(Redirect.HandlerContentAsync);
|
app.Use(Redirect.HandlerContentAsync);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -175,9 +190,21 @@ internal sealed class Program
|
|||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
var serverTask = app.RunAsync();
|
var serverTask = app.RunAsync();
|
||||||
|
programLogger.LogInformation("Server was started successfully.");
|
||||||
|
|
||||||
await encryptionInitializer;
|
await encryptionInitializer;
|
||||||
await rust.AppIsReady();
|
await rust.AppIsReady();
|
||||||
|
programLogger.LogInformation("The AI Studio server is ready.");
|
||||||
|
|
||||||
|
TaskScheduler.UnobservedTaskException += (sender, taskArgs) =>
|
||||||
|
{
|
||||||
|
programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'.");
|
||||||
|
taskArgs.SetObserved();
|
||||||
|
};
|
||||||
|
|
||||||
await serverTask;
|
await serverTask;
|
||||||
|
|
||||||
|
RUST_SERVICE.Dispose();
|
||||||
|
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -37,6 +38,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
|||||||
ChatRole.USER => "user",
|
ChatRole.USER => "user",
|
||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
@@ -86,21 +88,15 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new[]
|
var additionalModels = new[]
|
||||||
{
|
{
|
||||||
new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (latest)"),
|
new Model("claude-3-7-sonnet-latest", "Claude 3.7 Sonnet (Latest)"),
|
||||||
new Model("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet (20. June 2024)"),
|
new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (Latest)"),
|
||||||
new Model("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet (22. October 2024)"),
|
new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (Latest)"),
|
||||||
|
new Model("claude-3-opus-latest", "Claude 3 Opus (Latest)"),
|
||||||
|
};
|
||||||
|
|
||||||
new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (latest)"),
|
return this.LoadModels(token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||||
new Model("claude-3-5-heiku-20241022", "Claude 3.5 Haiku (22. October 2024)"),
|
|
||||||
|
|
||||||
new Model("claude-3-opus-20240229", "Claude 3.0 Opus (29. February 2024)"),
|
|
||||||
new Model("claude-3-opus-latest", "Claude 3.0 Opus (latest)"),
|
|
||||||
|
|
||||||
new Model("claude-3-sonnet-20240229", "Claude 3.0 Sonnet (29. February 2024)"),
|
|
||||||
new Model("claude-3-haiku-20240307", "Claude 3.0 Haiku (7. March 2024)"),
|
|
||||||
}.AsEnumerable());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -116,4 +112,37 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private async Task<IEnumerable<Model>> LoadModels(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 [];
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models?limit=100");
|
||||||
|
|
||||||
|
// Set the authorization header:
|
||||||
|
request.Headers.Add("x-api-key", secretKey);
|
||||||
|
|
||||||
|
// Set the Anthropic version:
|
||||||
|
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||||
|
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(JSON_SERIALIZER_OPTIONS, token);
|
||||||
|
return modelResponse.Data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,7 @@ using System.Text.Json;
|
|||||||
|
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
using RustService = AIStudio.Tools.RustService;
|
|
||||||
|
|
||||||
namespace AIStudio.Provider;
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
@@ -103,9 +102,14 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
{
|
{
|
||||||
using var request = await requestBuilder();
|
using var request = await requestBuilder();
|
||||||
|
|
||||||
|
//
|
||||||
// Send the request with the ResponseHeadersRead option.
|
// Send the request with the ResponseHeadersRead option.
|
||||||
// This allows us to read the stream as soon as the headers are received.
|
// This allows us to read the stream as soon as the headers are received.
|
||||||
// This is important because we want to stream the responses.
|
// This is important because we want to stream the responses.
|
||||||
|
//
|
||||||
|
// Please notice: We do not dispose the response here. The caller is responsible
|
||||||
|
// for disposing the response object. This is important because the response
|
||||||
|
// object is used to read the stream.
|
||||||
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
if (nextResponse.IsSuccessStatusCode)
|
if (nextResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ public sealed record Confidence
|
|||||||
Description = "The provider operates its service from the USA and is subject to **US jurisdiction**. In case of suspicion, authorities in the USA can access your data. However, **your data is not used for training** purposes.",
|
Description = "The provider operates its service from the USA and is subject to **US jurisdiction**. In case of suspicion, authorities in the USA can access your data. However, **your data is not used for training** purposes.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static readonly Confidence CHINA_NO_TRAINING = new()
|
||||||
|
{
|
||||||
|
Level = ConfidenceLevel.MODERATE,
|
||||||
|
Description = "The provider operates its service from China. In case of suspicion, authorities in the respective countries of operation may access your data. However, **your data is not used for training** purposes.",
|
||||||
|
};
|
||||||
|
|
||||||
public static readonly Confidence GDPR_NO_TRAINING = new()
|
public static readonly Confidence GDPR_NO_TRAINING = new()
|
||||||
{
|
{
|
||||||
Level = ConfidenceLevel.MEDIUM,
|
Level = ConfidenceLevel.MEDIUM,
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
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.DeepSeek;
|
||||||
|
|
||||||
|
public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api.deepseek.com/", logger)
|
||||||
|
{
|
||||||
|
#region Implementation of IProvider
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => LLMProviders.DEEP_SEEK.ToName();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string InstanceName { get; set; } = "DeepSeek";
|
||||||
|
|
||||||
|
/// <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 DeepSeek HTTP chat request:
|
||||||
|
var deepSeekChatRequest = 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.RAG => "assistant",
|
||||||
|
ChatRole.SYSTEM => "system",
|
||||||
|
|
||||||
|
_ => "user",
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = n.Content switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
_ => string.Empty,
|
||||||
|
}
|
||||||
|
}).ToList()],
|
||||||
|
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(deepSeekChatRequest, Encoding.UTF8, "application/json");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Helmholtz", 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(token, apiKeyProvisional);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task<IEnumerable<Model>> LoadModels(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 [];
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||||
|
return modelResponse.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
|
|||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
ChatRole.SYSTEM => "system",
|
ChatRole.SYSTEM => "system",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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.GWDG;
|
||||||
|
|
||||||
|
public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai.academiccloud.de/v1/", logger)
|
||||||
|
{
|
||||||
|
#region Implementation of IProvider
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => LLMProviders.GWDG.ToName();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string InstanceName { get; set; } = "GWDG SAIA";
|
||||||
|
|
||||||
|
/// <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 GWDG HTTP chat request:
|
||||||
|
var gwdgChatRequest = 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.RAG => "assistant",
|
||||||
|
ChatRole.SYSTEM => "system",
|
||||||
|
|
||||||
|
_ => "user",
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = n.Content switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
_ => string.Empty,
|
||||||
|
}
|
||||||
|
}).ToList()],
|
||||||
|
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(gwdgChatRequest, Encoding.UTF8, "application/json");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("GWDG", 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 async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||||
|
return models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||||
|
return models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task<IEnumerable<Model>> LoadModels(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 [];
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||||
|
return modelResponse.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
|||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
ChatRole.SYSTEM => "system",
|
ChatRole.SYSTEM => "system",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
@@ -136,8 +137,8 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
|||||||
if (secretKey is null)
|
if (secretKey is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
|
using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
|
||||||
var response = await this.httpClient.SendAsync(request, token);
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
|
||||||
if(!response.IsSuccessStatusCode)
|
if(!response.IsSuccessStatusCode)
|
||||||
return default;
|
return default;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
|||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
ChatRole.SYSTEM => "system",
|
ChatRole.SYSTEM => "system",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
@@ -127,10 +128,10 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
|||||||
if (secretKey is null)
|
if (secretKey is null)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
var response = await this.httpClient.SendAsync(request, token);
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
if(!response.IsSuccessStatusCode)
|
if(!response.IsSuccessStatusCode)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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.Helmholtz;
|
||||||
|
|
||||||
|
public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://api.helmholtz-blablador.fz-juelich.de/v1/", logger)
|
||||||
|
{
|
||||||
|
#region Implementation of IProvider
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => LLMProviders.HELMHOLTZ.ToName();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string InstanceName { get; set; } = "Helmholtz Blablador";
|
||||||
|
|
||||||
|
/// <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 Helmholtz HTTP chat request:
|
||||||
|
var helmholtzChatRequest = 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.RAG => "assistant",
|
||||||
|
ChatRole.SYSTEM => "system",
|
||||||
|
|
||||||
|
_ => "user",
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = n.Content switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
_ => string.Empty,
|
||||||
|
}
|
||||||
|
}).ToList()],
|
||||||
|
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(helmholtzChatRequest, Encoding.UTF8, "application/json");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Helmholtz", 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 async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||||
|
return models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) &&
|
||||||
|
!model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||||
|
return models.Where(model =>
|
||||||
|
model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task<IEnumerable<Model>> LoadModels(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 [];
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||||
|
return modelResponse.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,13 @@ public enum LLMProviders
|
|||||||
MISTRAL = 3,
|
MISTRAL = 3,
|
||||||
GOOGLE = 7,
|
GOOGLE = 7,
|
||||||
X = 8,
|
X = 8,
|
||||||
|
DEEP_SEEK = 11,
|
||||||
|
|
||||||
FIREWORKS = 5,
|
FIREWORKS = 5,
|
||||||
GROQ = 6,
|
GROQ = 6,
|
||||||
|
|
||||||
SELF_HOSTED = 4,
|
SELF_HOSTED = 4,
|
||||||
|
|
||||||
|
HELMHOLTZ = 9,
|
||||||
|
GWDG = 10,
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using AIStudio.Provider.Anthropic;
|
using AIStudio.Provider.Anthropic;
|
||||||
|
using AIStudio.Provider.DeepSeek;
|
||||||
using AIStudio.Provider.Fireworks;
|
using AIStudio.Provider.Fireworks;
|
||||||
using AIStudio.Provider.Google;
|
using AIStudio.Provider.Google;
|
||||||
using AIStudio.Provider.Groq;
|
using AIStudio.Provider.Groq;
|
||||||
|
using AIStudio.Provider.GWDG;
|
||||||
|
using AIStudio.Provider.Helmholtz;
|
||||||
using AIStudio.Provider.Mistral;
|
using AIStudio.Provider.Mistral;
|
||||||
using AIStudio.Provider.OpenAI;
|
using AIStudio.Provider.OpenAI;
|
||||||
using AIStudio.Provider.SelfHosted;
|
using AIStudio.Provider.SelfHosted;
|
||||||
@@ -28,12 +31,16 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.MISTRAL => "Mistral",
|
LLMProviders.MISTRAL => "Mistral",
|
||||||
LLMProviders.GOOGLE => "Google",
|
LLMProviders.GOOGLE => "Google",
|
||||||
LLMProviders.X => "xAI",
|
LLMProviders.X => "xAI",
|
||||||
|
LLMProviders.DEEP_SEEK => "DeepSeek",
|
||||||
|
|
||||||
LLMProviders.GROQ => "Groq",
|
LLMProviders.GROQ => "Groq",
|
||||||
LLMProviders.FIREWORKS => "Fireworks.ai",
|
LLMProviders.FIREWORKS => "Fireworks.ai",
|
||||||
|
|
||||||
LLMProviders.SELF_HOSTED => "Self-hosted",
|
LLMProviders.SELF_HOSTED => "Self-hosted",
|
||||||
|
|
||||||
|
LLMProviders.HELMHOLTZ => "Helmholtz Blablador",
|
||||||
|
LLMProviders.GWDG => "GWDG SAIA",
|
||||||
|
|
||||||
_ => "Unknown",
|
_ => "Unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,8 +73,13 @@ public static class LLMProvidersExtensions
|
|||||||
|
|
||||||
LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").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.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||||
|
|
||||||
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||||
|
|
||||||
|
LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||||
|
LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||||
|
|
||||||
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,6 +96,7 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.OPEN_AI => true,
|
LLMProviders.OPEN_AI => true,
|
||||||
LLMProviders.MISTRAL => true,
|
LLMProviders.MISTRAL => true,
|
||||||
LLMProviders.GOOGLE => true,
|
LLMProviders.GOOGLE => true,
|
||||||
|
LLMProviders.HELMHOLTZ => true,
|
||||||
|
|
||||||
//
|
//
|
||||||
// Providers that do not support embeddings:
|
// Providers that do not support embeddings:
|
||||||
@@ -92,6 +105,8 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.ANTHROPIC => false,
|
LLMProviders.ANTHROPIC => false,
|
||||||
LLMProviders.FIREWORKS => false,
|
LLMProviders.FIREWORKS => false,
|
||||||
LLMProviders.X => false,
|
LLMProviders.X => false,
|
||||||
|
LLMProviders.GWDG => false,
|
||||||
|
LLMProviders.DEEP_SEEK => false,
|
||||||
|
|
||||||
//
|
//
|
||||||
// Self-hosted providers are treated as a special case anyway.
|
// Self-hosted providers are treated as a special case anyway.
|
||||||
@@ -134,12 +149,16 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = instanceName },
|
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = instanceName },
|
||||||
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
|
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
|
||||||
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
|
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
|
||||||
|
LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName },
|
||||||
|
|
||||||
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
|
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
|
||||||
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
|
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
|
||||||
|
|
||||||
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName },
|
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName },
|
||||||
|
|
||||||
|
LLMProviders.HELMHOLTZ => new ProviderHelmholtz(logger) { InstanceName = instanceName },
|
||||||
|
LLMProviders.GWDG => new ProviderGWDG(logger) { InstanceName = instanceName },
|
||||||
|
|
||||||
_ => new NoProvider(),
|
_ => new NoProvider(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -157,10 +176,14 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
|
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
|
||||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
|
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
|
||||||
LLMProviders.X => "https://accounts.x.ai/sign-up",
|
LLMProviders.X => "https://accounts.x.ai/sign-up",
|
||||||
|
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up",
|
||||||
|
|
||||||
LLMProviders.GROQ => "https://console.groq.com/",
|
LLMProviders.GROQ => "https://console.groq.com/",
|
||||||
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
|
LLMProviders.FIREWORKS => "https://fireworks.ai/login",
|
||||||
|
|
||||||
|
LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab",
|
||||||
|
LLMProviders.GWDG => "https://docs.hpc.gwdg.de/services/saia/index.html#api-request",
|
||||||
|
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,6 +196,7 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
|
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
|
||||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
|
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
|
||||||
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
|
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
|
||||||
|
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage",
|
||||||
|
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
@@ -186,6 +210,7 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.GROQ => true,
|
LLMProviders.GROQ => true,
|
||||||
LLMProviders.FIREWORKS => true,
|
LLMProviders.FIREWORKS => true,
|
||||||
LLMProviders.GOOGLE => true,
|
LLMProviders.GOOGLE => true,
|
||||||
|
LLMProviders.DEEP_SEEK => true,
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
@@ -227,9 +252,12 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.ANTHROPIC => true,
|
LLMProviders.ANTHROPIC => true,
|
||||||
LLMProviders.GOOGLE => true,
|
LLMProviders.GOOGLE => true,
|
||||||
LLMProviders.X => true,
|
LLMProviders.X => true,
|
||||||
|
LLMProviders.DEEP_SEEK => true,
|
||||||
|
|
||||||
LLMProviders.GROQ => true,
|
LLMProviders.GROQ => true,
|
||||||
LLMProviders.FIREWORKS => true,
|
LLMProviders.FIREWORKS => true,
|
||||||
|
LLMProviders.HELMHOLTZ => true,
|
||||||
|
LLMProviders.GWDG => true,
|
||||||
|
|
||||||
LLMProviders.SELF_HOSTED => host is Host.OLLAMA,
|
LLMProviders.SELF_HOSTED => host is Host.OLLAMA,
|
||||||
|
|
||||||
@@ -243,9 +271,12 @@ public static class LLMProvidersExtensions
|
|||||||
LLMProviders.ANTHROPIC => true,
|
LLMProviders.ANTHROPIC => true,
|
||||||
LLMProviders.GOOGLE => true,
|
LLMProviders.GOOGLE => true,
|
||||||
LLMProviders.X => true,
|
LLMProviders.X => true,
|
||||||
|
LLMProviders.DEEP_SEEK => true,
|
||||||
|
|
||||||
LLMProviders.GROQ => true,
|
LLMProviders.GROQ => true,
|
||||||
LLMProviders.FIREWORKS => true,
|
LLMProviders.FIREWORKS => true,
|
||||||
|
LLMProviders.HELMHOLTZ => true,
|
||||||
|
LLMProviders.GWDG => true,
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
|||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
ChatRole.SYSTEM => "system",
|
ChatRole.SYSTEM => "system",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
@@ -138,10 +139,10 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
|||||||
if (secretKey is null)
|
if (secretKey is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
var response = await this.httpClient.SendAsync(request, token);
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
if(!response.IsSuccessStatusCode)
|
if(!response.IsSuccessStatusCode)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
|||||||
ChatRole.USER => "user",
|
ChatRole.USER => "user",
|
||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
ChatRole.SYSTEM => systemPromptRole,
|
ChatRole.SYSTEM => systemPromptRole,
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
@@ -154,10 +155,10 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
|||||||
if (secretKey is null)
|
if (secretKey is null)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
var response = await this.httpClient.SendAsync(request, token);
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
if(!response.IsSuccessStatusCode)
|
if(!response.IsSuccessStatusCode)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,11 @@ public static class HostExtensions
|
|||||||
|
|
||||||
public static string BaseURL(this Host host) => host switch
|
public static string BaseURL(this Host host) => host switch
|
||||||
{
|
{
|
||||||
Host.LM_STUDIO => "/v1/",
|
|
||||||
Host.LLAMACPP => "/v1/",
|
|
||||||
Host.OLLAMA => "/v1/",
|
|
||||||
|
|
||||||
_ => "/v1/",
|
_ => "/v1/",
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ChatURL(this Host host) => host switch
|
public static string ChatURL(this Host host) => host switch
|
||||||
{
|
{
|
||||||
Host.LM_STUDIO => "chat/completions",
|
|
||||||
Host.LLAMACPP => "chat/completions",
|
|
||||||
Host.OLLAMA => "chat/completions",
|
|
||||||
|
|
||||||
_ => "chat/completions",
|
_ => "chat/completions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
|||||||
ChatRole.AI => "assistant",
|
ChatRole.AI => "assistant",
|
||||||
ChatRole.AGENT => "assistant",
|
ChatRole.AGENT => "assistant",
|
||||||
ChatRole.SYSTEM => "system",
|
ChatRole.SYSTEM => "system",
|
||||||
|
ChatRole.RAG => "assistant",
|
||||||
|
|
||||||
_ => "user",
|
_ => "user",
|
||||||
},
|
},
|
||||||
@@ -154,11 +155,11 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
if(secretKey is not null)
|
if(secretKey is not null)
|
||||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
|
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional);
|
||||||
|
|
||||||
var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
|
using var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token);
|
||||||
if(!lmStudioResponse.IsSuccessStatusCode)
|
if(!lmStudioResponse.IsSuccessStatusCode)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 223 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user