Revert "Merge"

This reverts commit abb1e3f52d.
This commit is contained in:
Peer Schütt 2025-03-12 10:04:22 +01:00
parent a6b7357385
commit 8ac40a489b
41 changed files with 131 additions and 394 deletions

View File

@ -302,7 +302,7 @@ public abstract partial class AssistantBase<TSettings> : ComponentBase, IMessage
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();

View File

@ -292,7 +292,6 @@ public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
- You consider the security of the implementation by applying the Security by Design principle.
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
create a separate code block with its file path and name as chapter title.
- Important: The JSON objects of the API messages use camel case for the data field names.
""");
return sb.ToString();

View File

@ -12,6 +12,7 @@ public enum ChatRole
USER,
AI,
AGENT,
RAG,
}
/// <summary>

View File

@ -40,16 +40,6 @@ public sealed record ChatThread
/// </summary>
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
/// <summary>
/// The augmented data for this chat thread. Will be inserted into the system prompt.
/// </summary>
public string AugmentedData { get; set; } = string.Empty;
/// <summary>
/// The data security to use, derived from the data sources used so far.
/// </summary>
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
/// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary>
@ -84,48 +74,31 @@ public sealed record ChatThread
/// <returns>The prepared system prompt.</returns>
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
{
var isAugmentedDataAvailable = !string.IsNullOrWhiteSpace(chatThread.AugmentedData);
var systemPromptWithAugmentedData = isAugmentedDataAvailable switch
{
true => $"""
{chatThread.SystemPrompt}
{chatThread.AugmentedData}
""",
false => chatThread.SystemPrompt,
};
if(isAugmentedDataAvailable)
logger.LogInformation("Augmented data is available for the chat thread.");
else
logger.LogInformation("No augmented data is available for the chat thread.");
//
// Prepare the system prompt:
//
string systemPromptText;
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
systemPromptText = systemPromptWithAugmentedData;
systemPromptText = chatThread.SystemPrompt;
else
{
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
systemPromptText = systemPromptWithAugmentedData;
systemPromptText = chatThread.SystemPrompt;
else
{
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = systemPromptWithAugmentedData;
systemPromptText = chatThread.SystemPrompt;
else
{
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
if(profile == default)
systemPromptText = systemPromptWithAugmentedData;
systemPromptText = chatThread.SystemPrompt;
else
{
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
{chatThread.SystemPrompt}
{profile.ToSystemPrompt()}
""";

View File

@ -1,58 +0,0 @@
using AIStudio.Provider.SelfHosted;
using AIStudio.Settings.DataModel;
namespace AIStudio.Chat;
public static class ChatThreadExtensions
{
/// <summary>
/// Checks if the specified provider is allowed for the chat thread.
/// </summary>
/// <remarks>
/// We don't check if the provider is allowed to use the data sources of the chat thread.
/// That kind of check is done in the RAG process itself.<br/><br/>
///
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
/// from the data sources is stored in the chat thread. This means we must check if the
/// selected provider is allowed to use this thread's data.
/// </remarks>
/// <param name="chatThread">The chat thread to check.</param>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
{
// No chat thread available means we have a new chat. That's fine:
if (chatThread is null)
return true;
// The chat thread is available, but the data security is not specified.
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
// That's fine as well:
if (chatThread.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
return true;
//
// Is the provider self-hosted?
//
var isSelfHostedProvider = provider switch
{
ProviderSelfHosted => true,
AIStudio.Settings.Provider p => p.IsSelfHosted,
_ => false,
};
//
// Check the chat data security against the selected provider:
//
return isSelfHostedProvider switch
{
// The provider is self-hosted -- we can use any data source:
true => true,
// The provider is not self-hosted -- it depends on the data security of the chat thread:
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}
}

View File

@ -27,7 +27,7 @@
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" OnClick="@this.RegenerateBlock"/>
</MudTooltip>
}
@if (this.RemoveBlockFunc is not null)

View File

@ -28,7 +28,7 @@ public sealed class ContentImage : IContent, IImageSource
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
{
throw new NotImplementedException();
}

View File

@ -36,31 +36,16 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
public async Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
{
if(chatThread is null)
return new();
if(!chatThread.IsLLMProviderAllowed(provider))
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
logger.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
return chatThread;
}
return;
// Call the RAG process. Right now, we only have one RAG process:
if (lastPrompt is not null)
{
try
{
var rag = new AISrcSelWithRetCtxVal();
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
}
catch (Exception e)
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
logger.LogError(e, "Skipping the RAG process due to an error.");
}
var rag = new AISrcSelWithRetCtxVal();
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
}
// Store the last time we got a response. We use this later
@ -122,7 +107,6 @@ public sealed class ContentText : IContent
// Inform the UI that the streaming is done:
await this.StreamingDone();
return chatThread;
}
#endregion

View File

@ -41,7 +41,7 @@ public interface IContent
/// <summary>
/// Uses the provider to create the content.
/// </summary>
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, 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.

View File

@ -13,7 +13,6 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"),
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"),

View File

@ -24,7 +24,7 @@
IsLastContentBlock="@isLastBlock"
IsSecondToLastBlock="@isSecondLastBlock"
RegenerateFunc="@this.RegenerateBlock"
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
RegenerateEnabled="@(() => this.IsProviderSelected)"
EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
}
@ -46,7 +46,7 @@
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Send"
OnAdornmentClick="() => this.SendMessage()"
Disabled="@this.IsInputForbidden()"
ReadOnly="!this.IsProviderSelected || this.isStreaming"
Immediate="@true"
OnKeyUp="this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"
@ -113,14 +113,6 @@
{
<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()"/>
}
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
{
<MudTooltip Text="The selected provider is not allowed in this chat due to data security reasons." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
</MudTooltip>
}
<MudIconButton />
</MudToolBar>
</FooterContent>
</InnerScrolling>

View File

@ -275,9 +275,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
this.earlyDataSourceOptions = chatDefaultOptions;
if(this.ChatThread is not null)
this.ChatThread.DataSourceOptions = chatDefaultOptions;
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
}
@ -340,20 +337,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.earlyDataSourceOptions = updatedOptions;
}
private bool IsInputForbidden()
{
if (!this.IsProviderSelected)
return true;
if(this.isStreaming)
return true;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return true;
return false;
}
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
{
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
@ -388,9 +371,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (!this.IsProviderSelected)
return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
// We need to blur the focus away from the input field
// to be able to clear the field:
await this.inputField.BlurAsync();
@ -495,7 +475,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, 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;
@ -793,9 +773,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.ChatThread is null)
return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
this.hasUnsavedChanges = true;
this.StateHasChanged();

View File

@ -42,49 +42,52 @@
else
{
<MudExpansionPanels Class="mb-3">
<ExpansionPanel HeaderText="@this.RetrievalName(this.selectedRetrievalInfo)" HeaderIcon="@Icons.Material.Filled.Info">
<TextInfoLines Label="Description" MaxLines="14" Value="@this.selectedRetrievalInfo.Description" ClipboardTooltipSubject="the retrieval description"/>
<TextInfoLines Label="Parameters" MaxLines="14" Value="@this.RetrievalParameters(this.selectedRetrievalInfo)" ClipboardTooltipSubject="the retrieval parameters"/>
@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(this.selectedRetrievalInfo.Link))
{
<MudButton Href="@this.selectedRetrievalInfo.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
Open web link, show more information
</MudButton>
}
@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>
@* ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract *@
@if (this.selectedRetrievalInfo.Embeddings is null || !this.selectedRetrievalInfo.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 < this.selectedRetrievalInfo.Embeddings.Count; embeddingIndex++)
{
var embedding = this.selectedRetrievalInfo.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"/>
<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>
@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>
}

View File

@ -42,7 +42,6 @@ public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposabl
private string serverDescription = string.Empty;
private ProviderType securityRequirements = ProviderType.NONE;
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
private RetrievalInfo selectedRetrievalInfo;
private bool IsOperationInProgress { get; set; } = true;
@ -131,7 +130,7 @@ public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposabl
}
this.retrievalInfoformation = retrievalInfoResult.Data ?? [];
this.selectedRetrievalInfo = this.retrievalInfoformation.FirstOrDefault(x => x.Id == this.DataSource.SelectedRetrievalId);
this.StateHasChanged();
}
catch (Exception e)

View File

@ -110,24 +110,10 @@
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
}
@if (this.availableRetrievalProcesses.Count > 0)
{
<MudSelect @bind-Value="@this.dataSelectedRetrievalProcess" Text="@this.dataSelectedRetrievalProcess.Name" Label="Select one retrieval process" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateRetrievalProcess">
@foreach (var retrievalProcess in this.availableRetrievalProcesses)
{
<MudSelectItem Value="@retrievalProcess">
@retrievalProcess.Name
</MudSelectItem>
}
</MudSelect>
}
<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>
<MudSelectItem Value="@policy">@policy.ToSelectionText()</MudSelectItem>
}
</MudSelect>

View File

@ -8,8 +8,6 @@ using AIStudio.Tools.Validation;
using Microsoft.AspNetCore.Components;
using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
// ReSharper disable InconsistentNaming
namespace AIStudio.Dialogs;
@ -61,8 +59,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
private int dataPort;
private AuthMethod dataAuthMethod;
private string dataUsername = string.Empty;
private List<RetrievalInfo> availableRetrievalProcesses = [];
private RetrievalInfo dataSelectedRetrievalProcess;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
@ -95,9 +91,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
// When editing, we need to load the data:
if(this.IsEditing)
{
//
// Assign the data to the form fields:
//
this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
this.dataNum = this.DataSource.Num;
this.dataId = this.DataSource.Id;
@ -121,12 +114,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
await this.form.Validate();
}
}
// Load the data:
await this.TestConnection();
// Select the retrieval process:
this.dataSelectedRetrievalProcess = this.availableRetrievalProcesses.FirstOrDefault(n => n.Id == this.DataSource.SelectedRetrievalId);
}
await base.OnInitializedAsync();
@ -166,7 +153,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
Username = this.dataUsername,
Type = DataSourceType.ERI_V1,
SecurityPolicy = this.dataSecurityPolicy,
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
};
}
@ -238,18 +224,6 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
this.dataSourceSecurityRequirements = securityRequirementsRequest.Data;
var retrievalInfoRequest = await client.GetRetrievalInfoAsync(cts.Token);
if (!retrievalInfoRequest.Successful)
{
await this.form.Validate();
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
this.dataIssues[^1] = retrievalInfoRequest.Message;
return;
}
this.availableRetrievalProcesses = retrievalInfoRequest.Data ?? [];
this.connectionTested = true;
this.connectionSuccessfulTested = true;
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");

View File

@ -46,7 +46,7 @@
<ItemGroup>
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.74" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
<PackageReference Include="MudBlazor" Version="7.16.0" />
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />

View File

@ -78,7 +78,6 @@
<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."/>
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thanks for your contributions to the project."/>
</MudList>
</MudPaper>
</MudItem>

View File

@ -139,7 +139,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
this.isStreaming = true;
this.StateHasChanged();
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
this.suggestion = aiText.Text;
this.isStreaming = false;

View File

@ -38,6 +38,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -49,6 +49,7 @@ public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.RAG => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",

View File

@ -49,6 +49,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -49,6 +49,7 @@ public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.RAG => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",

View File

@ -50,6 +50,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -50,6 +50,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -49,6 +49,7 @@ public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://ap
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.RAG => "assistant",
ChatRole.SYSTEM => "system",
_ => "user",

View File

@ -48,6 +48,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -76,6 +76,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.RAG => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",

View File

@ -46,6 +46,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -50,6 +50,7 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => "system",
ChatRole.RAG => "assistant",
_ => "user",
},

View File

@ -51,9 +51,6 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
/// <inheritdoc />
public ERIVersion Version { get; init; } = ERIVersion.V1;
/// <inheritdoc />
public string SelectedRetrievalId { get; init; } = string.Empty;
/// <inheritdoc />
public async Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
{
@ -77,7 +74,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
Thread = await thread.ToERIChatThread(token),
MaxMatches = 10,
RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId,
RetrievalProcessId = null, // The ERI server selects the retrieval process when multiple processes are available
Parameters = null, // The ERI server selects useful default parameters
};
@ -100,7 +97,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
Links = eriContext.Links,
Category = eriContext.Type.ToRetrievalContentCategory(),
MatchedText = eriContext.MatchedContent,
DataSourceName = this.Name,
DataSourceName = eriContext.Name,
SurroundingContent = eriContext.SurroundingContent,
});
break;
@ -114,7 +111,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
Source = eriContext.MatchedContent,
Category = eriContext.Type.ToRetrievalContentCategory(),
SourceType = ContentImageSource.BASE64,
DataSourceName = this.Name,
DataSourceName = eriContext.Name,
});
break;

View File

@ -29,9 +29,4 @@ public interface IERIDataSource : IExternalDataSource
/// The ERI specification to use.
/// </summary>
public ERIVersion Version { get; init; }
/// <summary>
/// The ID of the selected retrieval process.
/// </summary>
public string SelectedRetrievalId { get; init; }
}

View File

@ -9,10 +9,9 @@ public abstract class ERIClientBase(string baseAddress) : IDisposable
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = null,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),

View File

@ -66,8 +66,22 @@ public sealed class AugmentationOne : IAugmentationProcess
// Let's convert all retrieval contexts to Markdown:
await retrievalContexts.AsMarkdown(sb, token);
// Add the augmented data to the chat thread:
chatThread.AugmentedData = sb.ToString();
//
// Append the entire augmentation to the chat thread,
// just before the user prompt:
//
chatThread.Blocks.Insert(chatThread.Blocks.Count - 1, new()
{
Role = ChatRole.RAG,
Time = DateTimeOffset.UtcNow,
ContentType = ContentType.TEXT,
HideFromUser = true,
Content = new ContentText
{
Text = sb.ToString(),
}
});
return chatThread;
}

View File

@ -1,7 +1,6 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.RAG.AugmentationProcesses;
using AIStudio.Tools.RAG.DataSourceSelectionProcesses;
using AIStudio.Tools.Services;
@ -39,30 +38,6 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
// makes sense to proceed with the RAG process:
var proceedWithRAG = true;
//
// We read the last block in the chat thread. We need to re-arrange
// the order of blocks later, after the augmentation process takes
// place:
//
if(chatThread.Blocks.Count == 0)
{
logger.LogError("The chat thread is empty. Skipping the RAG process.");
return chatThread;
}
if (chatThread.Blocks.Last().Role != ChatRole.AI)
{
logger.LogError("The last block in the chat thread is not the AI block. There is something wrong with the chat thread. Skipping the RAG process.");
return chatThread;
}
//
// At this point in time, the chat thread contains already the
// last block, which is the waiting AI block. We need to remove
// this block before we call some parts of the RAG process:
//
var chatThreadWithoutWaitingAIBlock = chatThread with { Blocks = chatThread.Blocks[..^1] };
//
// When the user wants to bind data sources to the chat, we
// have to check if the data sources are available for the
@ -97,56 +72,6 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
logger.LogWarning("No data sources are selected. The RAG process is skipped.");
proceedWithRAG = false;
}
else
{
var previousDataSecurity = chatThread.DataSecurity;
//
// Update the data security of the chat thread. We consider the current data security
// of the chat thread and the data security of the selected data sources:
//
var dataSecurityRestrictedToSelfHosted = selectedDataSources.Any(x => x.SecurityPolicy is DataSourceSecurity.SELF_HOSTED);
chatThread.DataSecurity = dataSecurityRestrictedToSelfHosted switch
{
//
//
// Case: the data sources which are selected have a security policy
// of SELF_HOSTED (at least one data source).
//
// When the policy was already set to ALLOW_ANY, we restrict it
// to SELF_HOSTED.
//
true => DataSourceSecurity.SELF_HOSTED,
//
// Case: the data sources which are selected have a security policy
// of ALLOW_ANY (none of the data sources has a SELF_HOSTED policy).
//
// When the policy was already set to SELF_HOSTED, we must keep that.
//
false => chatThread.DataSecurity switch
{
//
// When the policy was not specified yet, we set it to ALLOW_ANY.
//
DataSourceSecurity.NOT_SPECIFIED => DataSourceSecurity.ALLOW_ANY,
DataSourceSecurity.ALLOW_ANY => DataSourceSecurity.ALLOW_ANY,
//
// When the policy was already set to SELF_HOSTED, we must keep that.
// This is important since the thread might already contain data
// from a data source with a SELF_HOSTED policy.
//
DataSourceSecurity.SELF_HOSTED => DataSourceSecurity.SELF_HOSTED,
// Default case: we use the current data security of the chat thread.
_ => chatThread.DataSecurity,
}
};
if (previousDataSecurity != chatThread.DataSecurity)
logger.LogInformation($"The data security of the chat thread was updated from '{previousDataSecurity}' to '{chatThread.DataSecurity}'.");
}
//
// Trigger the retrieval part of the (R)AG process:
@ -159,7 +84,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
//
var retrievalTasks = new List<Task<IReadOnlyList<IRetrievalContext>>>(selectedDataSources.Count);
foreach (var dataSource in selectedDataSources)
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastPrompt, chatThreadWithoutWaitingAIBlock, token));
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastPrompt, chatThread, token));
//
// Wait for all retrieval tasks to finish:

View File

@ -93,14 +93,6 @@ public sealed class DataSourceValidation
return null;
}
public string? ValidateRetrievalProcess(RetrievalInfo retrievalInfo)
{
if(retrievalInfo == default)
return "Please select one retrieval process.";
return null;
}
public string? ValidatingName(string dataSourceName)
{
if(string.IsNullOrWhiteSpace(dataSourceName))

View File

@ -18,9 +18,9 @@
},
"HtmlAgilityPack": {
"type": "Direct",
"requested": "[1.11.74, )",
"resolved": "1.11.74",
"contentHash": "q0wRGbegtr4sZXjCNoV3OeRLTOcTNJQKiO9etNVSKPoTo33unmSK8Ahg36C4jIg/Hd3aw8YnTQjtKpBy+wlOpg=="
"requested": "[1.11.72, )",
"resolved": "1.11.72",
"contentHash": "RNLgXxTFdIGFI+o5l8c2aJ2L5StIRn9Uv8HKR76p7QP4ZUL26wzpWUCWh08xWUdkL2kocl+Xhv6VUu0rA1npVg=="
},
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
@ -207,6 +207,6 @@
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
}
},
"net8.0/win-x64": {}
"net8.0/osx-x64": {}
}
}

View File

@ -1,16 +1,2 @@
# v0.9.32, build 207 (2025-03-08 20:15 UTC)
- Added the "Community & Code" section to the about page. It includes links to the GitHub repositories and the project website.
- Added the retrieval process section for ERI data sources in the configuration dialog. Like all RAG functions, this is hidden behind the RAG feature flag in the app settings due to its preview status.
- Improved data security by preventing the use of cloud LLMs after confidential data has been retrieved previously.
- Improved the ERI client to expect JSON responses and send JSON requests using camel case.
- Improved the ERI client to raise an error when the server responds with additional JSON data that is not expected.
- Improved the error handling in the ERI data source info dialog in cases where servers respond with an invalid message.
- Improved the error handling for the entire RAG process.
- Improved chat thread persistence after modifications through the RAG process.
- Improved the augmentation and generation part of RAG by passing the augmented data into the system prompt.
- Fixed the chat thread we use for the data retrieval by removing the last block, which is meant to be for the final AI answer.
- Fixed the data source name for ERI data sources when performing data retrieval.
- Fixed the default data source selection when replacing the current chat with a new one.
- Fixed the state of the re-generate button in the chat thread, when no provider is selected or the data security is preventing the use of cloud LLMs.
- Updated the code contributions section in the supporter page. Thanks `peerschuett` for being our next contributor.
- Upgraded code dependencies.
# v0.9.32, build 207 (2025-03-xx xx:xx UTC)
- Added the Community & Code section to the About page. It includes links to the GitHub repositories and the project homepage.

View File

@ -1 +0,0 @@
# v0.9.33, build 208 (2025-03-xx xx:xx UTC)

View File

@ -1,9 +1,9 @@
0.9.32
2025-03-08 20:15:02 UTC
207
0.9.31
2025-03-03 15:33:03 UTC
206
8.0.113 (commit 8f216348dc)
8.0.13 (commit eba546b0f0)
1.85.0 (commit 4d91de4e4)
7.16.0
1.8.1
5a726577ca2, release
2e84f10a977, release

53
runtime/Cargo.lock generated
View File

@ -377,12 +377,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.16"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"shlex",
]
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
[[package]]
name = "cesu8"
@ -2184,9 +2181,9 @@ dependencies = [
[[package]]
name = "keyring"
version = "3.6.2"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b"
checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833"
dependencies = [
"byteorder",
"dbus-secret-service",
@ -2217,9 +2214,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libdbus-sys"
@ -2264,9 +2261,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "loom"
@ -2350,7 +2347,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mindwork-ai-studio"
version = "0.9.32"
version = "0.9.31"
dependencies = [
"aes",
"arboard",
@ -2368,7 +2365,6 @@ dependencies = [
"rand_chacha 0.9.0",
"rcgen",
"reqwest 0.12.12",
"ring",
"rocket",
"serde",
"serde_json",
@ -2762,9 +2758,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.20.3"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "open"
@ -3561,14 +3557,15 @@ dependencies = [
[[package]]
name = "ring"
version = "0.17.13"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
@ -3876,18 +3873,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@ -4031,12 +4028,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@ -4728,9 +4719,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.44.0"
version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
dependencies = [
"backtrace",
"bytes",
@ -4745,9 +4736,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",