diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 9c21ef03..431a65ef 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1333,6 +1333,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, re -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" +-- Number of sources +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Number of sources" + -- Do you really want to edit this message? In order to edit this message, the AI response will be deleted. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you really want to edit this message? In order to edit this message, the AI response will be deleted." @@ -4615,6 +4618,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Introduction" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2183503084"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." + -- Let's get started UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started" @@ -4624,9 +4630,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog" -- Choose the provider and model best suited for your current task. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Choose the provider and model best suited for your current task." --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." - -- Quick Start Guide UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3002014720"] = "Quick Start Guide" @@ -4861,6 +4864,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un -- no model selected UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected" +-- Sources +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources" + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template" diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index f7d67759..e1cfcb90 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -1,6 +1,7 @@ @using AIStudio.Tools @using MudBlazor @using AIStudio.Components +@using AIStudio.Provider @inherits AIStudio.Components.MSGComponentBase @@ -15,6 +16,14 @@ + @if (this.Content.Sources.Count > 0) + { + + + + + + } @if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null) { @@ -72,6 +81,10 @@ else { + @if (textContent.Sources.Count > 0) + { + + } } } } diff --git a/app/MindWork AI Studio/Chat/ContentImage.cs b/app/MindWork AI Studio/Chat/ContentImage.cs index 6fcf7f1e..f37ba652 100644 --- a/app/MindWork AI Studio/Chat/ContentImage.cs +++ b/app/MindWork AI Studio/Chat/ContentImage.cs @@ -27,6 +27,9 @@ public sealed class ContentImage : IContent, IImageSource [JsonIgnore] public Func StreamingEvent { get; set; } = () => Task.CompletedTask; + /// + public List Sources { get; set; } = []; + /// public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default) { diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index 590bef35..a5f0ef4f 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -24,7 +24,7 @@ public sealed class ContentText : IContent public bool InitialRemoteWait { get; set; } /// - // [JsonIgnore] + [JsonIgnore] public bool IsStreaming { get; set; } /// @@ -35,6 +35,9 @@ public sealed class ContentText : IContent [JsonIgnore] public Func StreamingEvent { get; set; } = () => Task.CompletedTask; + /// + public List Sources { get; set; } = []; + /// public async Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default) { @@ -80,7 +83,7 @@ public sealed class ContentText : IContent this.InitialRemoteWait = true; // Iterate over the responses from the AI: - await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, settings, token)) + await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token)) { // When the user cancels the request, we stop the loop: if (token.IsCancellationRequested) @@ -91,7 +94,10 @@ public sealed class ContentText : IContent this.IsStreaming = true; // Add the response to the text: - this.Text += deltaText; + this.Text += contentStreamChunk; + + // Merge the sources: + this.Sources.MergeSources(contentStreamChunk.Sources); // Notify the UI that the content has changed, // depending on the energy saving mode: diff --git a/app/MindWork AI Studio/Chat/IContent.cs b/app/MindWork AI Studio/Chat/IContent.cs index c03f6574..fc71f760 100644 --- a/app/MindWork AI Studio/Chat/IContent.cs +++ b/app/MindWork AI Studio/Chat/IContent.cs @@ -37,6 +37,12 @@ public interface IContent /// [JsonIgnore] public Func StreamingDone { get; set; } + + /// + /// The provided sources, if any. + /// + [JsonIgnore] + public List Sources { get; set; } /// /// Uses the provider to create the content. diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 6af7ba5f..62008d86 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -126,12 +126,13 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId Id = this.DataId, InstanceName = this.DataInstanceName, UsedLLMProvider = this.DataLLMProvider, + Model = this.DataLLMProvider switch { - LLMProviders.FIREWORKS => new Model(this.dataManuallyModel, null), - LLMProviders.HUGGINGFACE => new Model(this.dataManuallyModel, null), + LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE => new Model(this.dataManuallyModel, null), _ => this.DataModel }, + IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, IsEnterpriseConfiguration = false, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, @@ -158,7 +159,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant(); // When using Fireworks or Hugging Face, we must copy the model name: - if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) + if (this.DataLLMProvider.IsLLMModelProvidedManually()) this.dataManuallyModel = this.DataModel.Id; // @@ -241,7 +242,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private string? ValidateManuallyModel(string manuallyModel) { - if ((this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) && string.IsNullOrWhiteSpace(manuallyModel)) + if (this.DataLLMProvider.IsLLMModelProvidedManually() && string.IsNullOrWhiteSpace(manuallyModel)) return T("Please enter a model name."); return null; diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index e9d76474..bdd46e06 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -31,7 +31,7 @@ public partial class Home : MSGComponentBase { this.itemsAdvantages = [ new(this.T("Free of charge"), this.T("The app is free to use, both for personal and commercial purposes.")), - new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")), + new(this.T("Independence"), this.T("You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.")), new(this.T("Assistants"), this.T("You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.")), new(this.T("Unrestricted usage"), this.T("Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.")), new(this.T("Cost-effective"), this.T("You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.")), diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index ca1832b3..1a6580f2 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1335,6 +1335,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Ja, neu -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Ja, entferne es" +-- Number of sources +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Anzahl der Quellen" + -- Do you really want to edit this message? In order to edit this message, the AI response will be deleted. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Möchten Sie diese Nachricht wirklich bearbeiten? Um die Nachricht zu bearbeiten, wird die Antwort der KI gelöscht." @@ -4617,6 +4620,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Einführung" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2183503084"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar." + -- Let's get started UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Los geht's" @@ -4626,9 +4632,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Letztes Änderungsproto -- Choose the provider and model best suited for your current task. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Wählen Sie den Anbieter und das Modell aus, die am besten zu ihrer aktuellen Aufgabe passen." --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "Sie sind an keinen einzelnen Anbieter gebunden. Stattdessen können Sie den Anbieter wählen, der am besten zu ihren Bedürfnissen passt. Derzeit unterstützen wir OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face und selbst gehostete Modelle mit vLLM, llama.cpp, ollama, LM Studio, Groq oder Fireworks. Für Wissenschaftler und Mitarbeiter von Forschungseinrichtungen unterstützen wir auch die KI-Dienste von Helmholtz und GWDG. Diese sind über föderierte Anmeldungen wie eduGAIN für alle 18 Helmholtz-Zentren, die Max-Planck-Gesellschaft, die meisten deutschen und viele internationale Universitäten verfügbar." - -- Quick Start Guide UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3002014720"] = "Schnellstart-Anleitung" @@ -4863,6 +4866,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un -- no model selected UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgewählt" +-- Sources +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Quellen" + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a4d628c2..a5087d4d 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1335,6 +1335,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1603883875"] = "Yes, re -- Yes, remove it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1820166585"] = "Yes, remove it" +-- Number of sources +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1848978959"] = "Number of sources" + -- Do you really want to edit this message? In order to edit this message, the AI response will be deleted. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2018431076"] = "Do you really want to edit this message? In order to edit this message, the AI response will be deleted." @@ -4617,6 +4620,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1702902297"] = "Introduction" -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1892426825"] = "Vision" +-- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. +UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2183503084"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Perplexity, Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." + -- Let's get started UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2331588413"] = "Let's get started" @@ -4626,9 +4632,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2348849647"] = "Last Changelog" -- Choose the provider and model best suited for your current task. UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2588488920"] = "Choose the provider and model best suited for your current task." --- You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities. -UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T2900280782"] = "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT5, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, Alibaba Cloud (Qwen), Hugging Face, and self-hosted models using vLLM, llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities." - -- Quick Start Guide UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T3002014720"] = "Quick Start Guide" @@ -4863,6 +4866,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::LLMPROVIDERSEXTENSIONS::T3424652889"] = "Un -- no model selected UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected" +-- Sources +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SOURCEEXTENSIONS::T2730980305"] = "Sources" + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template" 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/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs index 118d68aa..f23cd876 100644 --- a/app/MindWork AI Studio/Provider/LLMProviders.cs +++ b/app/MindWork AI Studio/Provider/LLMProviders.cs @@ -14,6 +14,7 @@ public enum LLMProviders X = 8, DEEP_SEEK = 11, ALIBABA_CLOUD = 12, + PERPLEXITY = 14, FIREWORKS = 5, GROQ = 6, diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index 3dd9abe8..ad9a8f2f 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -9,6 +9,7 @@ using AIStudio.Provider.Helmholtz; using AIStudio.Provider.HuggingFace; using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; +using AIStudio.Provider.Perplexity; using AIStudio.Provider.SelfHosted; using AIStudio.Provider.X; using AIStudio.Settings; @@ -38,6 +39,7 @@ public static class LLMProvidersExtensions LLMProviders.X => "xAI", LLMProviders.DEEP_SEEK => "DeepSeek", LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud", + LLMProviders.PERPLEXITY => "Perplexity", LLMProviders.GROQ => "Groq", LLMProviders.FIREWORKS => "Fireworks.ai", @@ -86,6 +88,8 @@ public static class LLMProvidersExtensions LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + + LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), @@ -121,6 +125,7 @@ public static class LLMProvidersExtensions LLMProviders.GWDG => false, LLMProviders.DEEP_SEEK => false, LLMProviders.HUGGINGFACE => false, + LLMProviders.PERPLEXITY => false, // // Self-hosted providers are treated as a special case anyway. @@ -165,6 +170,7 @@ public static class LLMProvidersExtensions LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName }, LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName }, LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud(logger) { InstanceName = instanceName }, + LLMProviders.PERPLEXITY => new ProviderPerplexity(logger) { InstanceName = instanceName }, LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName }, LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName }, @@ -194,6 +200,7 @@ public static class LLMProvidersExtensions LLMProviders.X => "https://accounts.x.ai/sign-up", LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up", LLMProviders.ALIBABA_CLOUD => "https://account.alibabacloud.com/register/intl_register.htm", + LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api", LLMProviders.GROQ => "https://console.groq.com/", LLMProviders.FIREWORKS => "https://fireworks.ai/login", @@ -216,6 +223,7 @@ public static class LLMProvidersExtensions LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing", LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage", LLMProviders.ALIBABA_CLOUD => "https://usercenter2-intl.aliyun.com/billing", + LLMProviders.PERPLEXITY => "https://www.perplexity.ai/account/api/", LLMProviders.HUGGINGFACE => "https://huggingface.co/settings/billing", _ => string.Empty, @@ -232,6 +240,7 @@ public static class LLMProvidersExtensions LLMProviders.GOOGLE => true, LLMProviders.DEEP_SEEK => true, LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, LLMProviders.HUGGINGFACE => true, _ => false, @@ -278,6 +287,7 @@ public static class LLMProvidersExtensions LLMProviders.X => true, LLMProviders.DEEP_SEEK => true, LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, @@ -299,6 +309,7 @@ public static class LLMProvidersExtensions LLMProviders.X => true, LLMProviders.DEEP_SEEK => true, LLMProviders.ALIBABA_CLOUD => true, + LLMProviders.PERPLEXITY => true, LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, 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 new file mode 100644 index 00000000..8193f237 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -0,0 +1,148 @@ +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.Perplexity; + +public sealed class ProviderPerplexity(ILogger logger) : BaseProvider("https://api.perplexity.ai/", logger) +{ + private static readonly Model[] KNOWN_MODELS = + [ + new("sonar", "Sonar"), + new("sonar-pro", "Sonar Pro"), + new("sonar-reasoning", "Sonar Reasoning"), + new("sonar-reasoning-pro", "Sonar Reasoning Pro"), + new("sonar-deep-research", "Sonar Deep Research"), + ]; + + #region Implementation of IProvider + + /// + public override string Id => LLMProviders.PERPLEXITY.ToName(); + + /// + public override string InstanceName { get; set; } = "Perplexity"; + + /// + 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); + if(!requestedSecret.Success) + yield break; + + // Prepare the system prompt: + var systemPrompt = new Message + { + Role = "system", + Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger), + }; + + // Prepare the Perplexity HTTP chat request: + var perplexityChatRequest = JsonSerializer.Serialize(new ChatRequest + { + Model = chatModel.Id, + + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => text.Text, + _ => string.Empty, + } + }).ToList()], + Stream = true, + }, JSON_SERIALIZER_OPTIONS); + + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(perplexityChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + await foreach (var content in this.StreamChatCompletionInternal("Perplexity", RequestBuilder, token)) + yield return content; + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) + { + yield break; + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + /// + public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return this.LoadModels(); + } + + /// + public override Task> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(Enumerable.Empty()); + } + + /// + public override Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(Enumerable.Empty()); + } + + public override IReadOnlyCollection GetModelCapabilities(Model model) + { + var modelName = model.Id.ToLowerInvariant().AsSpan(); + + if(modelName.IndexOf("reasoning") is not -1 || + modelName.IndexOf("deep-research") is not -1) + return + [ + Capability.TEXT_INPUT, + Capability.MULTIPLE_IMAGE_INPUT, + + Capability.TEXT_OUTPUT, + Capability.IMAGE_OUTPUT, + + Capability.ALWAYS_REASONING, + ]; + + return + [ + Capability.TEXT_INPUT, + Capability.MULTIPLE_IMAGE_INPUT, + + Capability.TEXT_OUTPUT, + Capability.IMAGE_OUTPUT, + ]; + } + + #endregion + + private Task> LoadModels() => Task.FromResult>(KNOWN_MODELS); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs new file mode 100644 index 00000000..e8956a06 --- /dev/null +++ b/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Provider.Perplexity; + +/// +/// Data model for a line in the response stream, for streaming completions. +/// +/// The id of the response. +/// The object describing the response. +/// The timestamp of the response. +/// The model used for the response. +/// The system fingerprint; together with the seed, this allows you to reproduce the response. +/// The choices made by the AI. +public readonly record struct ResponseStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList Choices, IList SearchResults) : IResponseStreamLine +{ + /// + public bool ContainsContent() => this != default && this.Choices.Count > 0; + + /// + public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.GetSources()); + + /// + public bool ContainsSources() => this != default && this.SearchResults.Count > 0; + + /// + public IList GetSources() => this.SearchResults.Cast().ToList(); +} + +/// +/// Data model for a choice made by the AI. +/// +/// The index of the choice. +/// The delta text of the choice. +public readonly record struct Choice(int Index, Delta Delta); + +/// +/// The delta text of a choice. +/// +/// The content of the delta text. +public readonly record struct Delta(string Content); + +/// +/// Data model for a search result. +/// +/// The title of the search result. +/// The URL of the search result. +public sealed record SearchResult(string Title, string URL) : Source(Title, URL); 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..ce208612 --- /dev/null +++ b/app/MindWork AI Studio/Provider/SourceExtensions.cs @@ -0,0 +1,47 @@ +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(); + } + + /// + /// Merges a list of added sources into an existing list of sources, avoiding duplicates based on URL and Title. + /// + /// The existing list of sources to merge into. + /// The list of sources to add. + public static void MergeSources(this IList sources, IList addedSources) + { + foreach (var addedSource in addedSources) + if (sources.All(s => s.URL != addedSource.URL && s.Title != addedSource.Title)) + sources.Add((Source)addedSource); + } +} \ 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/app.css b/app/MindWork AI Studio/wwwroot/app.css index a0d9e860..07572473 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -140,4 +140,9 @@ .no-elevation { box-shadow: none !important; +} + +.sources-card-header { + top: 0em !important; + left: 2.2em !important; } \ No newline at end of file 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..bbff8e6b 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,8 @@ - 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 stream citations or sources. +- Added support for citations to the chat interface. This feature is invisible unless an LLM model is streaming citations or sources. - 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.