2025-12-10 12:48:13 +00:00
using System.Text ;
2024-07-13 08:37:57 +00:00
using System.Text.Json.Serialization ;
2024-05-04 09:11:09 +00:00
using AIStudio.Provider ;
using AIStudio.Settings ;
2026-04-09 06:57:06 +00:00
using AIStudio.Tools ;
using AIStudio.Tools.PluginSystem ;
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
{
2025-09-25 17:47:18 +00:00
private static readonly ILogger < ContentText > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ContentText > ( ) ;
2026-04-09 06:57:06 +00:00
private static string TB ( string fallbackEN ) = > I18N . I . T ( fallbackEN , typeof ( ContentText ) . Namespace , nameof ( ContentText ) ) ;
2025-09-25 17:47:18 +00:00
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 />
2024-07-13 08:37:57 +00:00
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public bool InitialRemoteWait { get ; set ; }
/// <inheritdoc />
2025-08-31 12:27:35 +00:00
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public bool IsStreaming { get ; set ; }
/// <inheritdoc />
2024-07-13 08:37:57 +00:00
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public Func < Task > StreamingDone { get ; set ; } = ( ) = > Task . CompletedTask ;
2024-07-13 08:37:57 +00:00
/// <inheritdoc />
[JsonIgnore]
2024-05-04 09:11:09 +00:00
public Func < Task > StreamingEvent { get ; set ; } = ( ) = > Task . CompletedTask ;
2025-08-31 12:27:35 +00:00
/// <inheritdoc />
public List < Source > Sources { get ; set ; } = [ ] ;
2025-12-10 12:48:13 +00:00
/// <inheritdoc />
2025-12-28 15:50:36 +00:00
public List < FileAttachment > FileAttachments { get ; set ; } = [ ] ;
2025-08-31 12:27:35 +00:00
2024-05-04 09:11:09 +00:00
/// <inheritdoc />
2025-09-25 17:47:18 +00:00
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 )
2026-04-09 06:57:06 +00:00
{
await this . CompleteWithoutStreaming ( ) ;
2025-03-08 12:56:38 +00:00
return new ( ) ;
2026-04-09 06:57:06 +00:00
}
2025-02-17 11:33:34 +00:00
2025-03-08 19:13:08 +00:00
if ( ! chatThread . IsLLMProviderAllowed ( provider ) )
{
2025-09-25 17:47:18 +00:00
LOGGER . LogError ( "The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process." ) ;
2026-04-09 06:57:06 +00:00
await this . CompleteWithoutStreaming ( ) ;
return chatThread ;
}
if ( ! await this . CheckSelectedModelAvailability ( provider , chatModel , token ) )
{
await this . CompleteWithoutStreaming ( ) ;
2025-03-08 19:13:08 +00:00
return chatThread ;
}
2025-02-17 15:51:26 +00:00
// Call the RAG process. Right now, we only have one RAG process:
2025-09-25 17:47:18 +00:00
if ( lastUserPrompt is not null )
2025-02-15 14:41:12 +00:00
{
2025-03-08 10:14:20 +00:00
try
{
var rag = new AISrcSelWithRetCtxVal ( ) ;
2025-09-25 17:47:18 +00:00
chatThread = await rag . ProcessAsync ( provider , lastUserPrompt , chatThread , token ) ;
2025-03-08 10:14:20 +00:00
}
catch ( Exception e )
{
2025-09-25 17:47:18 +00:00
LOGGER . LogError ( e , "Skipping the RAG process due to an error." ) ;
2025-03-08 10:14:20 +00:00
}
2025-02-15 14:41:12 +00:00
}
2025-02-17 15:51:26 +00:00
2024-11-13 19:33:52 +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:
2025-08-31 12:27:35 +00:00
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:
2025-08-31 12:27:35 +00:00
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 ( ) ;
2025-03-08 12:56:38 +00:00
return chatThread ;
2024-05-04 09:11:09 +00:00
}
2026-04-09 06:57:06 +00:00
private async Task CompleteWithoutStreaming ( )
{
this . InitialRemoteWait = false ;
this . IsStreaming = false ;
await this . StreamingDone ( ) ;
}
private static bool ModelsMatch ( Model modelA , Model modelB )
{
var idA = modelA . Id . Trim ( ) ;
var idB = modelB . Id . Trim ( ) ;
return string . Equals ( idA , idB , StringComparison . OrdinalIgnoreCase ) ;
}
private async Task < bool > CheckSelectedModelAvailability ( IProvider provider , Model chatModel , CancellationToken token = default )
{
if ( chatModel . IsSystemModel )
return true ;
if ( string . IsNullOrWhiteSpace ( chatModel . Id ) )
{
LOGGER . LogWarning ( "Skipping AI request because model ID is null or white space." ) ;
return false ;
}
IEnumerable < Model > loadedModels ;
try
{
loadedModels = await provider . GetTextModels ( token : token ) ;
}
catch ( OperationCanceledException )
{
return false ;
}
catch ( Exception e )
{
LOGGER . LogWarning ( e , "Skipping selected model availability check for '{ProviderInstanceName}' (provider={ProviderType}) because the model list could not be loaded." , provider . InstanceName , provider . Provider ) ;
return true ;
}
var availableModels = loadedModels . Where ( model = > ! string . IsNullOrWhiteSpace ( model . Id ) ) . ToList ( ) ;
if ( availableModels . Count = = 0 )
{
LOGGER . LogWarning ( "Skipping AI request because there are no models available from '{ProviderInstanceName}' (provider={ProviderType})." , provider . InstanceName , provider . Provider ) ;
return false ;
}
if ( availableModels . Any ( model = > ModelsMatch ( model , chatModel ) ) )
return true ;
var message = string . Format (
TB ( "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings." ) ,
chatModel . Id ,
provider . InstanceName ,
provider . Provider ) ;
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . CloudOff , message ) ) ;
LOGGER . LogWarning ( "Skipping AI request because model '{ModelId}' is not available from '{ProviderInstanceName}' (provider={ProviderType})." , chatModel . Id , provider . InstanceName , provider . Provider ) ;
return false ;
}
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 ,
2025-12-10 12:48:13 +00:00
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-10 12:48:13 +00:00
2025-12-28 15:50:36 +00:00
public async Task < string > PrepareTextContentForAI ( )
2025-12-10 12:48:13 +00:00
{
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 )
2025-12-10 12:48:13 +00:00
{
2025-12-10 16:29:20 +00:00
// 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
2025-12-10 12:48:13 +00:00
{
sb . AppendLine ( ) ;
2025-12-10 16:29:20 +00:00
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-10 16:29:20 +00:00
{
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 ;
}
2025-12-10 16:29:20 +00:00
sb . AppendLine ( ) ;
sb . AppendLine ( "---------------------------------------" ) ;
2025-12-28 15:50:36 +00:00
sb . AppendLine ( $"File path: {document.FilePath}" ) ;
2025-12-10 16:29:20 +00:00
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 ) ) ;
2025-12-10 16:29:20 +00:00
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." ) ;
}
2025-12-10 12:48:13 +00:00
}
}
}
2025-12-10 16:29:20 +00:00
2025-12-10 12:48:13 +00:00
return sb . ToString ( ) ;
}
2024-05-04 09:11:09 +00:00
/// <summary>
/// The text content.
/// </summary>
public string Text { get ; set ; } = string . Empty ;
2026-04-09 06:57:06 +00:00
}