diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index fb38cc4f..22d79441 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -21,7 +21,7 @@ public sealed class ProviderAlibabaCloud(ILogger logger) : BaseProvider("https:/ public override string InstanceName { get; set; } = "AlibabaCloud"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 7ff631fd..a09564df 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -18,7 +18,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap public override string InstanceName { get; set; } = "Anthropic"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs index c42e131c..c74e13ea 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ResponseStreamLine.cs @@ -13,7 +13,7 @@ public readonly record struct ResponseStreamLine(string Type, int Index, Delta D public bool ContainsContent() => this != default && !string.IsNullOrWhiteSpace(this.Delta.Text); /// - public string GetContent() => this.Delta.Text; + public ContentStreamChunk GetContent() => new(this.Delta.Text, []); } /// diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 32c0e621..cc81ab3c 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -63,7 +63,7 @@ public abstract class BaseProvider : IProvider, ISecretId public abstract string InstanceName { get; set; } /// - public abstract IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default); + public abstract IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default); /// public abstract IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default); @@ -96,7 +96,7 @@ public abstract class BaseProvider : IProvider, ISecretId /// A function that builds the request. /// The cancellation token. /// The status object of the request. - protected async Task SendRequest(Func> requestBuilder, CancellationToken token = default) + private async Task SendRequest(Func> requestBuilder, CancellationToken token = default) { const int MAX_RETRIES = 6; const double RETRY_DELAY_SECONDS = 4; @@ -189,7 +189,7 @@ public abstract class BaseProvider : IProvider, ISecretId return new HttpRateLimitedStreamResult(true, false, string.Empty, response); } - protected async IAsyncEnumerable StreamChatCompletionInternal(string providerName, Func> requestBuilder, [EnumeratorCancellation] CancellationToken token = default) where T : struct, IResponseStreamLine + protected async IAsyncEnumerable StreamChatCompletionInternal(string providerName, Func> requestBuilder, [EnumeratorCancellation] CancellationToken token = default) where T : struct, IResponseStreamLine { StreamReader? streamReader = null; try diff --git a/app/MindWork AI Studio/Provider/ContentStreamChunk.cs b/app/MindWork AI Studio/Provider/ContentStreamChunk.cs new file mode 100644 index 00000000..c6b2e205 --- /dev/null +++ b/app/MindWork AI Studio/Provider/ContentStreamChunk.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Provider; + +/// +/// A chunk of content from a content stream, along with its associated sources. +/// +/// The text content of the chunk. +/// The list of sources associated with the chunk. +public sealed record ContentStreamChunk(string Content, IList Sources) +{ + /// + /// Implicit conversion to string. + /// + /// The content stream chunk. + /// The text content of the chunk. + public static implicit operator string(ContentStreamChunk chunk) => chunk.Content; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index 57f74f4c..c7ab556f 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -20,7 +20,7 @@ public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api public override string InstanceName { get; set; } = "DeepSeek"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 22164e18..880804e0 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -19,7 +19,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew public override string InstanceName { get; set; } = "Fireworks.ai"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs index b3832f51..e9da7a53 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs @@ -14,7 +14,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint public bool ContainsContent() => this != default && this.Choices.Count > 0; /// - public string GetContent() => this.Choices[0].Delta.Content; + public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []); } /// diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index ad41804d..b9a997d6 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -20,7 +20,7 @@ public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai public override string InstanceName { get; set; } = "GWDG SAIA"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index de1df964..7819614f 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -20,7 +20,7 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela public override string InstanceName { get; set; } = "Google Gemini"; /// - public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 30d81ed0..8729b1d5 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -20,7 +20,7 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o public override string InstanceName { get; set; } = "Groq"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index 09a95387..bc8a3832 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -20,7 +20,7 @@ public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://ap public override string InstanceName { get; set; } = "Helmholtz Blablador"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 659a8ca9..f0b312b9 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -25,7 +25,7 @@ public sealed class ProviderHuggingFace : BaseProvider public override string InstanceName { get; set; } = "HuggingFace"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index 86a60913..cede6ca4 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -27,7 +27,7 @@ public interface IProvider /// The settings manager instance to use. /// The cancellation token. /// The chat completion stream. - public IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default); + public IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, CancellationToken token = default); /// /// Starts an image completion stream. diff --git a/app/MindWork AI Studio/Provider/IResponseStreamLine.cs b/app/MindWork AI Studio/Provider/IResponseStreamLine.cs index b3e7c284..366b9884 100644 --- a/app/MindWork AI Studio/Provider/IResponseStreamLine.cs +++ b/app/MindWork AI Studio/Provider/IResponseStreamLine.cs @@ -12,5 +12,17 @@ public interface IResponseStreamLine /// Gets the content of the response line. /// /// The content of the response line. - public string GetContent(); + public ContentStreamChunk GetContent(); + + /// + /// Checks if the response line contains any sources. + /// + /// True when the response line contains sources, false otherwise. + public bool ContainsSources() => false; + + /// + /// Gets the sources of the response line. + /// + /// The sources of the response line. + public IList GetSources() => []; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/ISource.cs b/app/MindWork AI Studio/Provider/ISource.cs new file mode 100644 index 00000000..38f3505d --- /dev/null +++ b/app/MindWork AI Studio/Provider/ISource.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Provider; + +/// +/// Data model for a source used in the response. +/// +public interface ISource +{ + /// + /// The title of the source. + /// + public string Title { get; } + + /// + /// The URL of the source. + /// + public string URL { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index ed87d12f..db094210 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -18,7 +18,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. public override string InstanceName { get; set; } = "Mistral"; /// - public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index 983ab875..b06ce2e0 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -19,7 +19,7 @@ public class NoProvider : IProvider public Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult>([]); - public async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatChatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { await Task.FromResult(0); yield break; diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index b5f8f818..cc89d1b2 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -22,7 +22,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o public override string InstanceName { get; set; } = "OpenAI"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponseStreamLine.cs index 98b2b2d9..96f6fc46 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ResponseStreamLine.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ResponseStreamLine.cs @@ -15,7 +15,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint public bool ContainsContent() => this != default && this.Choices.Count > 0; /// - public string GetContent() => this.Choices[0].Delta.Content; + public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []); } /// diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index e578b7f0..8193f237 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -29,7 +29,7 @@ public sealed class ProviderPerplexity(ILogger logger) : BaseProvider("https://a public override string InstanceName { get; set; } = "Perplexity"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 3655fd15..db6766ac 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -18,7 +18,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam public override string InstanceName { get; set; } = "Self-hosted"; /// - public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true); diff --git a/app/MindWork AI Studio/Provider/Source.cs b/app/MindWork AI Studio/Provider/Source.cs new file mode 100644 index 00000000..d666e375 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Source.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Provider; + +/// +/// Data model for a source used in the response. +/// +/// The title of the source. +/// The URL of the source. +public record Source(string Title, string URL) : ISource; \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SourceExtensions.cs b/app/MindWork AI Studio/Provider/SourceExtensions.cs new file mode 100644 index 00000000..a99f213e --- /dev/null +++ b/app/MindWork AI Studio/Provider/SourceExtensions.cs @@ -0,0 +1,35 @@ +using System.Text; + +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Provider; + +public static class SourceExtensions +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SourceExtensions).Namespace, nameof(SourceExtensions)); + + /// + /// Converts a list of sources to a markdown-formatted string. + /// + /// The list of sources to convert. + /// A markdown-formatted string representing the sources. + public static string ToMarkdown(this IList sources) + { + var sb = new StringBuilder(); + sb.Append("## "); + sb.AppendLine(TB("Sources")); + + var sourceNum = 0; + foreach (var source in sources) + { + sb.Append($"- [{++sourceNum}] "); + sb.Append('['); + sb.Append(source.Title); + sb.Append("]("); + sb.Append(source.URL); + sb.AppendLine(")"); + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index 884c1007..9fc5ec90 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -20,7 +20,7 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v public override string InstanceName { get; set; } = "xAI"; /// - public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: var requestedSecret = await RUST_SERVICE.GetAPIKey(this); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md index d4936af5..f810950e 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.51.md @@ -2,6 +2,7 @@ - Added support for predefined chat templates in configuration plugins to help enterprises roll out consistent templates across the organization. - Added the ability to choose between automatic and manual update installation to the app settings (default is manual). - Added the ability to control the update installation behavior by configuration plugins. +- Added the option for LLM providers to return citations. - Improved memory usage in several areas of the app. - Improved plugin management for configuration plugins so that hot reload detects when a provider or chat template has been removed. - Improved the dialog for naming chats and workspaces to ensure valid inputs are entered.