From c43820756e496cae2ddfe8798b9ccaaa5ba9b514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= <20603780+peerschuett@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:53:05 +0200 Subject: [PATCH] The LLM answers now with a final message if the maximum of tool calls is reached. --- .../Provider/BaseProvider.cs | 42 ++++++++++------ .../Provider/OpenAI/ProviderOpenAI.cs | 50 ++++++++++++------- .../ReadWebPageTool.cs | 6 +-- .../ToolCallingSystem/ToolSelectionRules.cs | 2 +- 4 files changed, 65 insertions(+), 35 deletions(-) diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index e52d4e47..cc3527e6 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1044,7 +1044,17 @@ public abstract class BaseProvider : IProvider, ISecretId var toolCallCount = 0; while (true) { - ChatCompletionAPIRequest requestDtoBase = await requestFactory(systemPrompt, apiParameters, providerTools); + var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS; + var requestSystemPrompt = finalResponseRequired + ? systemPrompt with + { + Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction()}", + } + : systemPrompt; + ChatCompletionAPIRequest requestDtoBase = await requestFactory( + requestSystemPrompt, + apiParameters, + finalResponseRequired ? null : providerTools); var requestDto = requestDtoBase with { Messages = [..requestDtoBase.Messages, ..internalMessages], @@ -1070,6 +1080,17 @@ public abstract class BaseProvider : IProvider, ISecretId yield break; } + if (finalResponseRequired) + { + await ResetToolRuntimeStatusAsync(); + if (!string.IsNullOrWhiteSpace(responseMessage.Content)) + yield return new ContentStreamChunk(responseMessage.Content, []); + else + yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []); + + yield break; + } + await ShowToolRuntimeStatusAsync(toolCalls .Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Function.Name)); @@ -1081,25 +1102,18 @@ public abstract class BaseProvider : IProvider, ISecretId foreach (var toolCall in toolCalls) { - toolCallCount++; - if (toolCallCount > ToolSelectionRules.MAX_TOOL_CALLS) + if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS) { - var limitMessage = ToolSelectionRules.GetMaxToolCallsLimitMessage(); - currentAssistantContent?.ToolInvocations.Add(new ToolInvocationTrace + var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); + internalMessages.Add(new ToolResultMessage { - Order = toolCallCount, - ToolId = toolCall.Function.Name, - ToolName = toolCall.Function.Name, + Content = finalResponseInstruction, ToolCallId = toolCall.Id, - Status = ToolInvocationTraceStatus.BLOCKED, - StatusMessage = limitMessage, - Result = limitMessage, }); - await ResetToolRuntimeStatusAsync(); - yield return new ContentStreamChunk(limitMessage, []); - yield break; + continue; } + toolCallCount++; var (toolContent, trace) = await toolExecutor.ExecuteAsync( toolCall.Id, toolCall.Function.Name, diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 8f2480c8..13d9e71f 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -326,13 +326,24 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur while (true) { + var finalResponseRequired = toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS; + 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()}", + }; + } + requestInput.AddRange(internalItems); + var requestDto = new ResponsesAPIRequest { Model = chatModel.Id, - Input = [..baseInput, ..internalItems], + Input = requestInput, Stream = false, Store = false, - Tools = providerTools, + Tools = finalResponseRequired ? [] : providerTools, AdditionalApiParameters = apiParameters, }; var response = await this.ExecuteResponsesRequest(requestDto, requestedSecret, token); @@ -342,6 +353,19 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur yield break; } + if (finalResponseRequired) + { + await ResetToolRuntimeStatusAsync(currentAssistantContent); + + var textOutput = response.GetTextOutput(); + if (!string.IsNullOrWhiteSpace(textOutput)) + yield return new ContentStreamChunk(textOutput, []); + else + yield return new ContentStreamChunk("The model did not return a final answer after completing the available tool calls.", []); + + yield break; + } + var functionCalls = response.GetFunctionCalls(); if (functionCalls.Count == 0) { @@ -364,26 +388,18 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur foreach (var functionCall in functionCalls) { - toolCallCount++; - if (toolCallCount > ToolSelectionRules.MAX_TOOL_CALLS) + if (toolCallCount >= ToolSelectionRules.MAX_TOOL_CALLS) { - var limitMessage = ToolSelectionRules.GetMaxToolCallsLimitMessage(); - currentAssistantContent?.ToolInvocations.Add(new ToolInvocationTrace + var finalResponseInstruction = ToolSelectionRules.GetMaxToolCallsFinalResponseInstruction(); + internalItems.Add(new ResponsesFunctionCallOutputItem { - Order = toolCallCount, - ToolId = functionCall.Name, - ToolName = functionCall.Name, - ToolCallId = functionCall.CallId, - Status = ToolInvocationTraceStatus.BLOCKED, - StatusMessage = limitMessage, - Result = limitMessage, + CallId = functionCall.CallId, + Output = finalResponseInstruction, }); - - await ResetToolRuntimeStatusAsync(currentAssistantContent); - yield return new ContentStreamChunk(limitMessage, []); - yield break; + continue; } + toolCallCount++; var (toolContent, trace) = await toolExecutor.ExecuteAsync( functionCall.CallId, functionCall.Name, diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs index 5d901b35..83586461 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs @@ -43,9 +43,9 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger fieldName switch { - "timeoutSeconds" => TB("Optional HTTP timeout for loading a web page in seconds."), - "maxContentCharacters" => TB("Optional global truncation limit for extracted characters returned to the model."), - ALLOWED_PRIVATE_HOSTS_SETTING => TB("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."), + "timeoutSeconds" => TB("(Optional) HTTP timeout for loading a web page in seconds."), + "maxContentCharacters" => TB("(Optional) Global truncation limit for extracted characters returned to the model."), + ALLOWED_PRIVATE_HOSTS_SETTING => TB("(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."), _ => TB(fieldDefinition.Description), }; diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs index 32770392..41999b2f 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs @@ -33,7 +33,7 @@ public static class ToolSelectionRules _ => ConfidenceLevel.NONE, }; - public static string GetMaxToolCallsLimitMessage() => $"Tool calling stopped because the maximum of {MAX_TOOL_CALLS} tool calls was reached."; + public static string GetMaxToolCallsFinalResponseInstruction() => $"The maximum of {MAX_TOOL_CALLS} tool calls has been reached. No more tools are available. Provide the best possible final answer to the user based on the tool results already available."; public static string BuildToolPolicyPrompt(IEnumerable definitions) {