AI-Studio/app/MindWork AI Studio/Chat/ContentText.cs

217 lines
8.4 KiB
C#
Raw Normal View History

using System.Text;
using System.Text.Json.Serialization;
2024-05-04 09:11:09 +00:00
using AIStudio.Provider;
using AIStudio.Settings;
2025-02-17 15:51:26 +00:00
using AIStudio.Tools.RAG.RAGProcesses;
2024-05-04 09:11:09 +00:00
namespace AIStudio.Chat;
/// <summary>
/// Text content in the chat.
/// </summary>
public sealed class ContentText : IContent
{
private static readonly ILogger<ContentText> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ContentText>();
2024-05-04 09:11:09 +00:00
/// <summary>
/// The minimum time between two streaming events, when the user
/// enables the energy saving mode.
/// </summary>
private static readonly TimeSpan MIN_TIME = TimeSpan.FromSeconds(3);
#region Implementation of IContent
/// <inheritdoc />
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public bool InitialRemoteWait { get; set; }
/// <inheritdoc />
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public bool IsStreaming { get; set; }
/// <inheritdoc />
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public List<Source> Sources { get; set; } = [];
/// <inheritdoc />
2025-12-28 15:50:36 +00:00
public List<FileAttachment> FileAttachments { get; set; } = [];
2024-05-04 09:11:09 +00:00
/// <inheritdoc />
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)
2024-05-04 09:11:09 +00:00
{
if(chatThread is null)
return new();
if(!chatThread.IsLLMProviderAllowed(provider))
{
LOGGER.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
return chatThread;
}
2025-02-17 15:51:26 +00:00
// Call the RAG process. Right now, we only have one RAG process:
if (lastUserPrompt is not null)
{
2025-03-08 10:14:20 +00:00
try
{
var rag = new AISrcSelWithRetCtxVal();
chatThread = await rag.ProcessAsync(provider, lastUserPrompt, chatThread, token);
2025-03-08 10:14:20 +00:00
}
catch (Exception e)
{
LOGGER.LogError(e, "Skipping the RAG process due to an error.");
2025-03-08 10:14:20 +00:00
}
}
2025-02-17 15:51:26 +00:00
// Store the last time we got a response. We use this later
2024-05-04 09:11:09 +00:00
// to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode
// the user chose.
var last = DateTimeOffset.Now;
2025-02-17 15:51:26 +00:00
// Get the settings manager:
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
2024-09-01 18:10:03 +00:00
// Start another thread by using a task to uncouple
2024-05-04 09:11:09 +00:00
// the UI thread from the AI processing:
await Task.Run(async () =>
{
// We show the waiting animation until we get the first response:
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
2024-05-04 09:11:09 +00:00
{
// When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested)
break;
// Stop the waiting animation:
this.InitialRemoteWait = false;
this.IsStreaming = true;
// Add the response to the text:
this.Text += contentStreamChunk;
// Merge the sources:
this.Sources.MergeSources(contentStreamChunk.Sources);
2024-05-04 09:11:09 +00:00
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
2024-08-05 19:12:52 +00:00
switch (settings.ConfigurationData.App.IsSavingEnergy)
2024-05-04 09:11:09 +00:00
{
// Energy saving mode is off. We notify the UI
// as fast as possible -- no matter the odds:
case false:
await this.StreamingEvent();
break;
// Energy saving mode is on. We notify the UI
// only when the time between two events is
// greater than the minimum time:
case true when now - last > MIN_TIME:
last = now;
await this.StreamingEvent();
break;
}
}
// Stop the waiting animation (in case the loop
2024-09-01 18:10:03 +00:00
// was stopped, or no content was received):
2024-05-04 09:11:09 +00:00
this.InitialRemoteWait = false;
this.IsStreaming = false;
}, token);
2025-06-10 12:32:24 +00:00
this.Text = this.Text.RemoveThinkTags().Trim();
2024-05-04 09:11:09 +00:00
// Inform the UI that the streaming is done:
await this.StreamingDone();
return chatThread;
2024-05-04 09:11:09 +00:00
}
2025-05-24 10:27:00 +00:00
/// <inheritdoc />
2025-05-24 17:11:28 +00:00
public IContent DeepClone() => new ContentText
2025-05-24 10:27:00 +00:00
{
2025-05-24 17:11:28 +00:00
Text = this.Text,
InitialRemoteWait = this.InitialRemoteWait,
IsStreaming = this.IsStreaming,
Sources = [..this.Sources],
FileAttachments = [..this.FileAttachments],
2025-05-24 17:11:28 +00:00
};
2025-05-24 10:27:00 +00:00
2024-05-04 09:11:09 +00:00
#endregion
2025-12-28 15:50:36 +00:00
public async Task<string> PrepareTextContentForAI()
{
var sb = new StringBuilder();
sb.AppendLine(this.Text);
if(this.FileAttachments.Count > 0)
{
2025-12-28 15:50:36 +00:00
// Get the list of existing documents:
var existingDocuments = this.FileAttachments.Where(x => x.Type is FileAttachmentType.DOCUMENT && x.Exists).ToList();
// Log warning for missing files:
var missingDocuments = this.FileAttachments.Except(existingDocuments).Where(x => x.Type is FileAttachmentType.DOCUMENT).ToList();
if (missingDocuments.Count > 0)
foreach (var missingDocument in missingDocuments)
LOGGER.LogWarning("File attachment no longer exists and will be skipped: '{MissingDocument}'.", missingDocument.FilePath);
// Only proceed if there are existing, allowed documents:
if (existingDocuments.Count > 0)
{
// Check Pandoc availability once before processing file attachments
var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false);
if (!pandocState.IsAvailable)
LOGGER.LogWarning("File attachments could not be processed because Pandoc is not available.");
else if (!pandocState.CheckWasSuccessful)
LOGGER.LogWarning("File attachments could not be processed because the Pandoc version check failed.");
else
{
sb.AppendLine();
sb.AppendLine("The following files are attached to this message:");
2025-12-28 15:50:36 +00:00
foreach(var document in existingDocuments)
{
2025-12-28 15:50:36 +00:00
if (document.IsForbidden)
{
LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath);
continue;
}
sb.AppendLine();
sb.AppendLine("---------------------------------------");
2025-12-28 15:50:36 +00:00
sb.AppendLine($"File path: {document.FilePath}");
sb.AppendLine("File content:");
sb.AppendLine("````");
2025-12-28 15:50:36 +00:00
sb.AppendLine(await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue));
sb.AppendLine("````");
}
2025-12-30 17:30:32 +00:00
var numImages = this.FileAttachments.Count(x => x is { IsImage: true, Exists: true });
if (numImages > 0)
{
sb.AppendLine();
sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. ");
sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly.");
}
}
}
}
return sb.ToString();
}
2024-05-04 09:11:09 +00:00
/// <summary>
/// The text content.
/// </summary>
public string Text { get; set; } = string.Empty;
}