diff --git a/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs index 4b7ed309..b0f784ff 100644 --- a/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs +++ b/app/MindWork AI Studio/Assistants/Builder/LuaResponseParseError.cs @@ -17,7 +17,7 @@ public enum LuaResponseParseError public static class LuaResponseParseErrorExtension { - private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseErrorExtension).Namespace, nameof(LuaResponseParseErrorExtension)); + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseError).Namespace, nameof(LuaResponseParseError)); public static string GetMessage(this LuaResponseParseError parseError, string technicalDetails) => parseError switch { diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 96c943db..29f7dcaf 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -9097,9 +9097,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T9397 -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Global shortcut" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Toggle voice recording" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." @@ -9217,12 +9223,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro -- Sources provided by the AI UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI" +-- Sources used by tools +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T535360212"] = "Sources used by tools" + -- Tool UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Tool" -- Tool description UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Tool description" +-- Please select an LLM provider. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1110311702"] = "Please select an LLM provider." + +-- The selected model does not support tool calling. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1960397623"] = "The selected model does not support tool calling." + +-- Tool calling for this provider is not implemented yet. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T3776963202"] = "Tool calling for this provider is not implemented yet." + -- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T1410249500"] = "(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication." @@ -9358,6 +9376,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- SearXNG URL UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG URL" +-- Using tools: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T2834986024"] = "Using tools: {0}" + +-- Using tool: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T4185351801"] = "Using tool: {0}" + -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation" @@ -9499,11 +9523,17 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Please select a model." +-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Are you sure you want to delete the chat '{0}' in the workspace '{1}'?" + -- Unnamed workspace UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed workspace" -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" +-- Are you sure you want to delete the temporary chat '{0}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Are you sure you want to delete the temporary chat '{0}'?" + -- Unnamed chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat" diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index cd3bf46a..8138a0ff 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -148,11 +148,6 @@ @invocation.StatusMessage } - @T("Result") - - @this.GetToolInvocationResult(invocation) - - @T("Arguments") @if (invocation.Arguments.Count == 0) { @@ -169,6 +164,18 @@ } } + + @T("Result") + + @if (invocation.JsonResult is not null) + { + + } + else + { + @this.GetToolInvocationResult(invocation) + } + } } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 32ee1cd9..d9bafdc4 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -217,6 +217,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable hash.Add(invocation.Status); hash.Add(invocation.StatusMessage); hash.Add(invocation.Result); + hash.Add(invocation.JsonResult is not null); hash.Add(invocation.Arguments.Count); foreach (var argument in invocation.Arguments) { diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index eafea313..20420fe6 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -266,6 +266,7 @@ public sealed class ContentText : IContent StatusMessage = x.StatusMessage, Arguments = new Dictionary(x.Arguments, StringComparer.Ordinal), Result = x.Result, + JsonResult = x.JsonResult?.DeepClone(), })], }; diff --git a/app/MindWork AI Studio/Components/JsonTreeView.razor b/app/MindWork AI Studio/Components/JsonTreeView.razor new file mode 100644 index 00000000..c2526674 --- /dev/null +++ b/app/MindWork AI Studio/Components/JsonTreeView.razor @@ -0,0 +1,26 @@ +@using MudBlazor + + + + @if (item.Value is JsonTreeNode node) + { + + + + @node.Text + + + + } + + diff --git a/app/MindWork AI Studio/Components/JsonTreeView.razor.cs b/app/MindWork AI Studio/Components/JsonTreeView.razor.cs new file mode 100644 index 00000000..2dbf056b --- /dev/null +++ b/app/MindWork AI Studio/Components/JsonTreeView.razor.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +using Microsoft.AspNetCore.Components; + +using MudBlazor; + +namespace AIStudio.Components; + +public partial class JsonTreeView : ComponentBase +{ + [Parameter] + public JsonNode? Value { get; set; } + + private IReadOnlyCollection> items = []; + + protected override void OnParametersSet() + { + this.items = [CreateTreeItem("$", this.Value)]; + } + + private static TreeItemData CreateTreeItem(string label, JsonNode? value) + { + var children = CreateChildren(value); + return new TreeItemData + { + Expanded = false, + Expandable = children.Count > 0, + Value = new JsonTreeNode + { + Text = $"{label}: {FormatValue(value)}", + Icon = GetIcon(value), + Expandable = children.Count > 0, + }, + Children = children, + }; + } + + private static List> CreateChildren(JsonNode? value) => value switch + { + JsonObject jsonObject => jsonObject + .Select(property => CreateTreeItem(JsonSerializer.Serialize(property.Key), property.Value)) + .ToList(), + JsonArray jsonArray => jsonArray + .Select((item, index) => CreateTreeItem($"[{index}]", item)) + .ToList(), + _ => [], + }; + + private static string FormatValue(JsonNode? value) => value switch + { + JsonObject jsonObject when jsonObject.Count == 0 => "{}", + JsonObject => "{...}", + JsonArray jsonArray when jsonArray.Count == 0 => "[]", + JsonArray => "[...]", + null => "null", + _ => value.ToJsonString(), + }; + + private static string GetIcon(JsonNode? value) => value switch + { + JsonObject => Icons.Material.Filled.DataObject, + JsonArray => Icons.Material.Filled.DataArray, + _ => Icons.Material.Filled.Code, + }; + + private sealed class JsonTreeNode + { + public string Text { get; init; } = string.Empty; + + public string Icon { get; init; } = string.Empty; + + public bool Expandable { get; init; } + } +} 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 29b48f23..b8302e91 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 @@ -3619,7 +3619,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397 UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Werkzeugeinstellungen" -- This tool has been disabled by your organization. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3794167684"] = "Dieses Tool wurde von Ihrer Organisation deaktiviert." +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3794167684"] = "Dieses Werkzeug wurde von Ihrer Organisation deaktiviert." -- Status UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status" @@ -3739,7 +3739,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Erforderl UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Schließen" -- This tool has been disabled by your organization. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3794167684"] = "Dieses Tool wurde von Ihrer Organisation deaktiviert." +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3794167684"] = "Dieses Werkzeug wurde von Ihrer Organisation deaktiviert." -- No tools are available in this context. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "Keine Werkzeuge sind in diesem Kontext verfügbar." @@ -9099,9 +9099,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T9397 -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "Die globale Tastenkombination konnte nicht registriert werden. Die vorherige Tastenkombination bleibt aktiv." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Globale Tastenkombination" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "Die Änderung der globalen Tastenkombination wurde abgebrochen. Die vorherige Tastenkombination bleibt aktiv." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Sprachaufnahme umschalten" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "Der konfigurierte Transkriptionsanbieter konnte nicht erstellt werden." @@ -9219,12 +9225,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Von den Dat -- Sources provided by the AI UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Von der KI bereitgestellte Quellen" +-- Sources used by tools +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T535360212"] = "Quellen, die von Werkzeugen verwendet werden" + -- Tool UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Werkzeug" -- Tool description UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Werkzeugbeschreibung" +-- Please select an LLM provider. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1110311702"] = "Bitte wählen Sie einen LLM-Anbieter aus." + +-- The selected model does not support tool calling. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1960397623"] = "Das ausgewählte Modell unterstützt keine Werkzeug-Nutzung." + +-- Tool calling for this provider is not implemented yet. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T3776963202"] = "Werkzeug-Aufruf für diesen Anbieter ist noch nicht implementiert." + -- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T1410249500"] = "(Optional) Host-Whitelist für private oder VPN-Webseiten. Aus Sicherheitsgründen dürfen private oder VPN-Webseiten standardmäßig nicht gelesen werden. Trennen Sie Host-Muster durch Kommas, z. B. example.de, *.example.de. Erlaubte private Hosts erfordern einen Anbieter mit hoher Zuverlässigkeit. Für erlaubte interne Hosts versucht AI Studio außerdem automatisch die Standard-Anmeldung des Betriebssystems, wenn der Server mit integrierter Authentifizierung antwortet." @@ -9360,6 +9378,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- SearXNG URL UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG-URL" +-- Using tools: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T2834986024"] = "Verwendung von Tools: {0}" + +-- Using tool: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T4185351801"] = "Verwendetes Tool: {0}" + -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc-Installation" @@ -9501,11 +9525,17 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Bitte wählen Sie ein Modell aus." +-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Möchtest du den Chat '{0}' im Arbeitsbereich '{1}' wirklich löschen?" + -- Unnamed workspace UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannter Arbeitsbereich" -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen" +-- Are you sure you want to delete the temporary chat '{0}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Möchtest du den temporären Chat '{0}' wirklich löschen?" + -- Unnamed chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unbenannter Chat" 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 acd26b0a..816c7c58 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 @@ -9099,9 +9099,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T9397 -- The global shortcut could not be registered. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." +-- Global shortcut +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2637055764"] = "Global shortcut" + -- The global shortcut change was cancelled. The previous shortcut remains active. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." +-- Toggle voice recording +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T40517664"] = "Toggle voice recording" + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." @@ -9219,12 +9225,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4174900468"] = "Sources pro -- Sources provided by the AI UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T4261248356"] = "Sources provided by the AI" +-- Sources used by tools +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SOURCEEXTENSIONS::T535360212"] = "Sources used by tools" + -- Tool UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T3517012711"] = "Tool" -- Tool description UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::ITOOLIMPLEMENTATION::T4056470505"] = "Tool description" +-- Please select an LLM provider. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1110311702"] = "Please select an LLM provider." + +-- The selected model does not support tool calling. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T1960397623"] = "The selected model does not support tool calling." + +-- Tool calling for this provider is not implemented yet. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T3776963202"] = "Tool calling for this provider is not implemented yet." + -- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T1410249500"] = "(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication." @@ -9360,6 +9378,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- SearXNG URL UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T993547568"] = "SearXNG URL" +-- Using tools: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T2834986024"] = "Using tools: {0}" + +-- Using tool: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLRUNTIMESTATUS::T4185351801"] = "Using tool: {0}" + -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::TOOLS::USERFILE::T185447014"] = "Pandoc Installation" @@ -9501,11 +9525,17 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T497939286"] = -- Please select a model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::PROVIDERVALIDATION::T818893091"] = "Please select a model." +-- Are you sure you want to delete the chat '{0}' in the workspace '{1}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1016188706"] = "Are you sure you want to delete the chat '{0}' in the workspace '{1}'?" + -- Unnamed workspace UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed workspace" -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" +-- Are you sure you want to delete the temporary chat '{0}'? +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Are you sure you want to delete the temporary chat '{0}'?" + -- Unnamed chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat" diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 56bb80b2..32d4d68e 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1034,13 +1034,16 @@ public abstract class BaseProvider : IProvider, ISecretId var internalMessages = new List(); var toolCallCount = 0; + var toolResultCharacterCount = 0L; + var toolSources = new List(); while (true) { - var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS; + var finalResponseInstruction = ToolSelectionRules.GetToolCallsUnavailableInstruction(toolCallCount, toolResultCharacterCount); + var finalResponseRequired = finalResponseInstruction is not null; var requestSystemPrompt = finalResponseRequired ? systemPrompt with { - Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction()}", + Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{finalResponseInstruction}", } : systemPrompt; ChatCompletionAPIRequest requestDtoBase = await requestFactory( @@ -1051,6 +1054,10 @@ public abstract class BaseProvider : IProvider, ISecretId { Messages = [..requestDtoBase.Messages, ..internalMessages], Stream = false, + ParallelToolCalls = requestDtoBase.Tools is not null && + chatModel.Id.Contains("gpt-oss", StringComparison.InvariantCultureIgnoreCase) + ? false + : requestDtoBase.ParallelToolCalls, }; var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token); var responseMessage = response?.Choices.FirstOrDefault()?.Message; @@ -1065,9 +1072,9 @@ public abstract class BaseProvider : IProvider, ISecretId { await ResetToolRuntimeStatusAsync(); if (!string.IsNullOrWhiteSpace(responseMessage.Content)) - yield return new ContentStreamChunk(responseMessage.Content, []); + yield return new ContentStreamChunk(responseMessage.Content, [..toolSources]); else if (toolCallCount > 0) - yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", []); + yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", [..toolSources]); yield break; } @@ -1076,9 +1083,9 @@ public abstract class BaseProvider : IProvider, ISecretId { await ResetToolRuntimeStatusAsync(); if (!string.IsNullOrWhiteSpace(responseMessage.Content)) - yield return new ContentStreamChunk(responseMessage.Content, []); + yield return new ContentStreamChunk(responseMessage.Content, [..toolSources]); else - yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []); + yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", [..toolSources]); yield break; } @@ -1091,24 +1098,25 @@ public abstract class BaseProvider : IProvider, ISecretId internalMessages.Add(new AssistantToolCallMessage { Content = responseMessage.Content, + ReasoningContent = responseMessage.ReasoningContent, ToolCalls = toolCalls, }); foreach (var toolCall in toolCalls) { - if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS) + var toolCallsUnavailableInstruction = ToolSelectionRules.GetToolCallsUnavailableInstruction(toolCallCount, toolResultCharacterCount); + if (toolCallsUnavailableInstruction is not null) { - var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); internalMessages.Add(new ToolResultMessage { - Content = finalResponseInstruction, + Content = toolCallsUnavailableInstruction, ToolCallId = toolCall.Id, }); continue; } toolCallCount++; - var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync( + var (toolContent, trace, requiredProviderConfidence, sources) = await toolExecutor.ExecuteAsync( toolCall.Id, toolCall.Function.Name, toolCall.Function.Arguments, @@ -1116,8 +1124,10 @@ public abstract class BaseProvider : IProvider, ISecretId this.Provider.GetConfidence(settingsManager).Level, toolCallCount, token); + toolResultCharacterCount += toolContent.Length; chatThread.RequireProviderConfidence(requiredProviderConfidence); + toolSources.MergeSources(sources); currentAssistantContent?.ToolInvocations.Add(trace); internalMessages.Add(new ToolResultMessage { diff --git a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs index 340d1e74..693b40bd 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace AIStudio.Provider.OpenAI; public sealed record AssistantToolCallMessage : IMessageBase @@ -6,5 +8,8 @@ public sealed record AssistantToolCallMessage : IMessageBase public string? Content { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ReasoningContent { get; init; } + public IList ToolCalls { get; init; } = []; } diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs index cb8ff196..205194e4 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs @@ -6,5 +6,7 @@ public sealed record ChatCompletionResponseMessage public string? Content { get; init; } + public string? ReasoningContent { get; init; } + public IList? ToolCalls { get; init; } } diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 8dd3b24f..1cba21d4 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -335,16 +335,19 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur // reasoning items emitted alongside function calls. var internalItems = new List(); var toolCallCount = 0; + var toolResultCharacterCount = 0L; + var toolSources = new List(); while (true) { - var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS; + var finalResponseInstruction = ToolSelectionRules.GetToolCallsUnavailableInstruction(toolCallCount, toolResultCharacterCount); + var finalResponseRequired = finalResponseInstruction is not null; var requestInput = new List(baseInput); if (finalResponseRequired && requestInput.FirstOrDefault() is TextMessage systemPrompt) { requestInput[0] = systemPrompt with { - Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction()}", + Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{finalResponseInstruction}", }; } requestInput.AddRange(internalItems); @@ -365,15 +368,17 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur yield break; } + toolSources.MergeSources(response.GetSources()); + if (finalResponseRequired) { await ResetToolRuntimeStatusAsync(currentAssistantContent); var textOutput = response.GetTextOutput(); if (!string.IsNullOrWhiteSpace(textOutput)) - yield return new ContentStreamChunk(textOutput, []); + yield return new ContentStreamChunk(textOutput, [..toolSources]); else - yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []); + yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", [..toolSources]); yield break; } @@ -385,9 +390,9 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur var textOutput = response.GetTextOutput(); if (!string.IsNullOrWhiteSpace(textOutput)) - yield return new ContentStreamChunk(textOutput, []); + yield return new ContentStreamChunk(textOutput, [..toolSources]); else if (toolCallCount > 0) - yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", []); + yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", [..toolSources]); yield break; } @@ -402,19 +407,19 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur foreach (var functionCall in functionCalls) { - if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS) + var toolCallsUnavailableInstruction = ToolSelectionRules.GetToolCallsUnavailableInstruction(toolCallCount, toolResultCharacterCount); + if (toolCallsUnavailableInstruction is not null) { - var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); internalItems.Add(new ResponsesFunctionCallOutputItem { CallId = functionCall.CallId, - Output = finalResponseInstruction, + Output = toolCallsUnavailableInstruction, }); continue; } toolCallCount++; - var (toolContent, trace, requiredProviderConfidence) = await toolExecutor.ExecuteAsync( + var (toolContent, trace, requiredProviderConfidence, sources) = await toolExecutor.ExecuteAsync( functionCall.CallId, functionCall.Name, functionCall.Arguments, @@ -422,8 +427,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur providerConfidence, toolCallCount, token); + toolResultCharacterCount += toolContent.Length; chatThread.RequireProviderConfidence(requiredProviderConfidence); + toolSources.MergeSources(sources); currentAssistantContent?.ToolInvocations.Add(trace); internalItems.Add(new ResponsesFunctionCallOutputItem { diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs index 69682e22..875553da 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesResponse.cs @@ -35,19 +35,35 @@ public sealed record ResponsesResponse return string.Concat(this.Output .Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal)) .SelectMany(ReadContentItems) - .Where(x => ReadString(x, "type").Equals("output_text", StringComparison.Ordinal)) - .Select(x => ReadString(x, "text"))); + .Select(x => ReadString(x, "type") switch + { + "output_text" => ReadString(x, "text"), + "refusal" => ReadString(x, "refusal"), + _ => string.Empty, + })); } + public IReadOnlyList GetSources() => this.Output + .Where(x => ReadString(x, "type").Equals("message", StringComparison.Ordinal)) + .SelectMany(ReadContentItems) + .SelectMany(x => ReadArrayItems(x, "annotations")) + .Where(x => ReadString(x, "type").Equals("url_citation", StringComparison.Ordinal)) + .Select(x => new Source(ReadString(x, "title"), ReadString(x, "url"), SourceOrigin.LLM)) + .Where(x => !string.IsNullOrWhiteSpace(x.Title) && !string.IsNullOrWhiteSpace(x.URL)) + .ToList(); + private static IEnumerable ReadContentItems(JsonElement outputItem) + => ReadArrayItems(outputItem, "content"); + + private static IEnumerable ReadArrayItems(JsonElement item, string propertyName) { - if (outputItem.ValueKind is not JsonValueKind.Object || - !outputItem.TryGetProperty("content", out var content) || - content.ValueKind is not JsonValueKind.Array) + if (item.ValueKind is not JsonValueKind.Object || + !item.TryGetProperty(propertyName, out var array) || + array.ValueKind is not JsonValueKind.Array) yield break; - foreach (var contentItem in content.EnumerateArray()) - yield return contentItem; + foreach (var arrayItem in array.EnumerateArray()) + yield return arrayItem; } private static string ReadString(JsonElement item, string propertyName) diff --git a/app/MindWork AI Studio/Tools/ISource.cs b/app/MindWork AI Studio/Tools/ISource.cs index b3963699..3b479619 100644 --- a/app/MindWork AI Studio/Tools/ISource.cs +++ b/app/MindWork AI Studio/Tools/ISource.cs @@ -16,7 +16,7 @@ public interface ISource public string URL { get; } /// - /// The origin of the source, whether it was provided by the AI or by the RAG process. + /// The origin of the source. /// public SourceOrigin Origin { get; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index 3be5319e..6799a651 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -213,10 +213,12 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private async Task GetShortcutDescription(Shortcut shortcutId) { var language = await this.settingsManager.GetActiveLanguagePlugin(); + string T(string fallbackEN) => I18N.I.GetText(language, fallbackEN, typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)); + return shortcutId switch { - Shortcut.VOICE_RECORDING_TOGGLE => I18N.I.GetText(language, "Toggle voice recording", typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)), - _ => I18N.I.GetText(language, "Global shortcut", typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService)), + Shortcut.VOICE_RECORDING_TOGGLE => T("Toggle voice recording"), + _ => T("Global shortcut"), }; } @@ -262,4 +264,4 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv } private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/SourceExtensions.cs b/app/MindWork AI Studio/Tools/SourceExtensions.cs index 660f5d90..7e414e39 100644 --- a/app/MindWork AI Studio/Tools/SourceExtensions.cs +++ b/app/MindWork AI Studio/Tools/SourceExtensions.cs @@ -88,6 +88,7 @@ public static partial class SourceExtensions { var sb = new StringBuilder(); var ragSources = new List(); + var toolSources = new List(); var sourceNum = 0; var addedLLMHeaders = false; foreach (var source in sources) @@ -110,32 +111,54 @@ public static partial class SourceExtensions AppendMarkdownLink(sb, source.Title, source.URL); sb.AppendLine(); break; + + case SourceOrigin.TOOL: + toolSources.Add(source); + break; } } - - if(ragSources.Count == 0) - return sb.ToString(); - - sb.AppendLine(); - sb.Append("## "); - sb.AppendLine(TB("Sources provided by the data providers")); - - foreach (var source in ragSources) + + if(toolSources.Count > 0) { - sb.Append($"- [{++sourceNum}] "); - AppendMarkdownLink(sb, source.Title, source.URL); - sb.AppendLine(); + if(sb.Length > 0) + sb.AppendLine(); + + sb.Append("## "); + sb.AppendLine(TB("Sources used by tools")); + + foreach (var source in toolSources) + { + sb.Append($"- [{++sourceNum}] "); + AppendMarkdownLink(sb, source.Title, source.URL); + sb.AppendLine(); + } } - + + if(ragSources.Count > 0) + { + if(sb.Length > 0) + sb.AppendLine(); + + sb.Append("## "); + sb.AppendLine(TB("Sources provided by the data providers")); + + foreach (var source in ragSources) + { + sb.Append($"- [{++sourceNum}] "); + AppendMarkdownLink(sb, source.Title, 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) + public static void MergeSources(this IList sources, IEnumerable addedSources) { foreach (var addedSource in addedSources) if (sources.All(s => s.URL != addedSource.URL && s.Title != addedSource.Title)) @@ -144,4 +167,4 @@ public static partial class SourceExtensions [GeneratedRegex(@"^\[(?