2025-02-17 11:33:34 +00:00
|
|
|
using AIStudio.Components;
|
2025-01-02 13:50:54 +00:00
|
|
|
using AIStudio.Settings;
|
2025-02-15 14:41:12 +00:00
|
|
|
using AIStudio.Settings.DataModel;
|
2025-02-17 13:12:46 +00:00
|
|
|
using AIStudio.Tools.ERIClient.DataModel;
|
2025-01-02 13:50:54 +00:00
|
|
|
|
2024-05-04 09:11:09 +00:00
|
|
|
namespace AIStudio.Chat;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Data structure for a chat thread.
|
|
|
|
/// </summary>
|
2024-08-18 19:48:35 +00:00
|
|
|
public sealed record ChatThread
|
2024-05-04 09:11:09 +00:00
|
|
|
{
|
2024-07-13 08:37:57 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The unique identifier of the chat thread.
|
|
|
|
/// </summary>
|
|
|
|
public Guid ChatId { get; init; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The unique identifier of the workspace.
|
|
|
|
/// </summary>
|
|
|
|
public Guid WorkspaceId { get; set; }
|
|
|
|
|
2024-11-23 12:04:02 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Specifies the provider selected for the chat thread.
|
|
|
|
/// </summary>
|
|
|
|
public string SelectedProvider { get; set; } = string.Empty;
|
|
|
|
|
2025-01-02 13:50:54 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Specifies the profile selected for the chat thread.
|
|
|
|
/// </summary>
|
|
|
|
public string SelectedProfile { get; set; } = string.Empty;
|
|
|
|
|
2025-02-15 14:41:12 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The data source options for this chat thread.
|
|
|
|
/// </summary>
|
|
|
|
public DataSourceOptions DataSourceOptions { get; set; } = new();
|
|
|
|
|
2025-02-17 11:33:34 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The AI-selected data sources for this chat thread.
|
|
|
|
/// </summary>
|
|
|
|
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
|
|
|
|
|
2025-03-08 12:56:38 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The augmented data for this chat thread. Will be inserted into the system prompt.
|
|
|
|
/// </summary>
|
|
|
|
public string AugmentedData { get; set; } = string.Empty;
|
|
|
|
|
2025-03-08 19:13:08 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The data security to use, derived from the data sources used so far.
|
|
|
|
/// </summary>
|
|
|
|
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
|
|
|
|
|
2024-05-04 09:11:09 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
|
|
|
/// </summary>
|
2024-07-13 08:37:57 +00:00
|
|
|
public string Name { get; set; } = string.Empty;
|
2024-05-04 09:11:09 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The seed for the chat thread. Some providers use this to generate deterministic results.
|
|
|
|
/// </summary>
|
2024-07-13 08:37:57 +00:00
|
|
|
public int Seed { get; init; }
|
2024-05-04 09:11:09 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The current system prompt for the chat thread.
|
|
|
|
/// </summary>
|
2024-07-13 08:37:57 +00:00
|
|
|
public string SystemPrompt { get; init; } = string.Empty;
|
|
|
|
|
2024-05-04 09:11:09 +00:00
|
|
|
/// <summary>
|
|
|
|
/// The content blocks of the chat thread.
|
|
|
|
/// </summary>
|
2024-07-13 08:37:57 +00:00
|
|
|
public List<ContentBlock> Blocks { get; init; } = [];
|
2025-01-02 13:50:54 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Prepares the system prompt for the chat thread.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// The actual system prompt depends on the selected profile. If no profile is selected,
|
|
|
|
/// the system prompt is returned as is. When a profile is selected, the system prompt
|
|
|
|
/// is extended with the profile chosen.
|
|
|
|
/// </remarks>
|
|
|
|
/// <param name="settingsManager">The settings manager instance to use.</param>
|
|
|
|
/// <param name="chatThread">The chat thread to prepare the system prompt for.</param>
|
|
|
|
/// <param name="logger">The logger instance to use.</param>
|
|
|
|
/// <returns>The prepared system prompt.</returns>
|
|
|
|
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
|
|
|
|
{
|
2025-03-08 12:56:38 +00:00
|
|
|
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.");
|
|
|
|
|
2025-01-02 13:50:54 +00:00
|
|
|
//
|
|
|
|
// Prepare the system prompt:
|
|
|
|
//
|
|
|
|
string systemPromptText;
|
|
|
|
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
|
|
|
|
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
|
2025-03-08 12:56:38 +00:00
|
|
|
systemPromptText = systemPromptWithAugmentedData;
|
2025-01-02 13:50:54 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
|
2025-03-08 12:56:38 +00:00
|
|
|
systemPromptText = systemPromptWithAugmentedData;
|
2025-01-02 13:50:54 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
|
2025-03-08 12:56:38 +00:00
|
|
|
systemPromptText = systemPromptWithAugmentedData;
|
2025-01-02 13:50:54 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
|
|
|
|
if(profile == default)
|
2025-03-08 12:56:38 +00:00
|
|
|
systemPromptText = systemPromptWithAugmentedData;
|
2025-01-02 13:50:54 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
|
|
|
|
systemPromptText = $"""
|
2025-03-08 12:56:38 +00:00
|
|
|
{systemPromptWithAugmentedData}
|
2025-01-02 13:50:54 +00:00
|
|
|
|
|
|
|
{profile.ToSystemPrompt()}
|
|
|
|
""";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.LogInformation(logMessage);
|
|
|
|
return systemPromptText;
|
|
|
|
}
|
2025-01-03 20:18:27 +00:00
|
|
|
|
2025-01-03 17:01:22 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Removes a content block from this chat thread.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="content">The content block to remove.</param>
|
2025-01-03 20:18:27 +00:00
|
|
|
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
|
|
|
|
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
|
|
|
|
/// which will not remove the previous user block if it is hidden from the user.</param>
|
|
|
|
public void Remove(IContent content, bool removeForRegenerate = false)
|
2025-01-03 17:01:22 +00:00
|
|
|
{
|
|
|
|
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
|
|
|
|
if(block is null)
|
|
|
|
return;
|
2025-01-03 20:18:27 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Remove the previous user block if it is hidden from the user. Otherwise,
|
|
|
|
// the experience might be confusing for the user.
|
|
|
|
//
|
|
|
|
// Explanation, using the ERI assistant as an example:
|
|
|
|
// - The ERI assistant generates for every file a hidden user prompt.
|
|
|
|
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
|
|
|
|
// - Now, the user removes one AI response
|
|
|
|
// - The hidden user prompt is still there, but the user can't see it.
|
|
|
|
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
|
|
|
|
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
|
|
|
|
//
|
|
|
|
if (block.Role is ChatRole.AI && !removeForRegenerate)
|
|
|
|
{
|
|
|
|
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
|
|
|
|
var index = sortedBlocks.IndexOf(block);
|
|
|
|
if (index > 0)
|
|
|
|
{
|
|
|
|
var previousBlock = sortedBlocks[index - 1];
|
|
|
|
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
|
|
|
|
this.Blocks.Remove(previousBlock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the block from the chat thread:
|
2025-01-03 17:01:22 +00:00
|
|
|
this.Blocks.Remove(block);
|
|
|
|
}
|
2025-02-17 13:12:46 +00:00
|
|
|
|
|
|
|
/// <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 };
|
|
|
|
}
|
2024-05-04 09:11:09 +00:00
|
|
|
}
|