From ee6982c9d37243cd317a722140ce50e663135837 Mon Sep 17 00:00:00 2001 From: Yanan Wang <103942@smsassist.com> Date: Thu, 7 May 2026 11:07:19 -0500 Subject: [PATCH 1/3] fix(routing): improve OneStepForwardReasoner instruction parsing and add logging --- .../BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs index 7d396fbb2..b00f4a7e9 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs @@ -63,7 +63,9 @@ public async Task GetNextInstruction(Agent router, string m }; var response = await completion.GetChatCompletions(router, dialogs); - var inst = response.Content.JsonContent(); + var inst = (response.FunctionArgs ?? response.Content).JsonContent(); + var routingCtx = _services.GetRequiredService(); + _logger.LogInformation($"[OneStepForwardReasoner] ConversationId: {routingCtx.ConversationId}, MessageId: {messageId}, Next instruction: {response.FunctionArgs ?? response.Content}"); // Fix LLM malformed response await ReasonerHelper.FixMalformedResponse(_services, inst); From 9ab8a6ee8107e480f6050dc06c17875171ec9dc6 Mon Sep 17 00:00:00 2001 From: Yanan Wang <103942@smsassist.com> Date: Thu, 7 May 2026 13:37:39 -0500 Subject: [PATCH 2/3] Add comment for explaination --- .../BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs index b00f4a7e9..0a475ed4e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs @@ -63,6 +63,9 @@ public async Task GetNextInstruction(Agent router, string m }; var response = await completion.GetChatCompletions(router, dialogs); + // Due to format drift, LLMs may complete with finishReason=function_call (instruction in FunctionArgs) + // or finishReason=stop (instruction serialized as JSON in Content). + // Use FunctionArgs ?? Content to be compatible with both cases. var inst = (response.FunctionArgs ?? response.Content).JsonContent(); var routingCtx = _services.GetRequiredService(); _logger.LogInformation($"[OneStepForwardReasoner] ConversationId: {routingCtx.ConversationId}, MessageId: {messageId}, Next instruction: {response.FunctionArgs ?? response.Content}"); From 989037e3cc47a206c924b3077b56c59005641a2e Mon Sep 17 00:00:00 2001 From: Yanan Wang Date: Thu, 7 May 2026 19:31:55 -0500 Subject: [PATCH 3/3] fix(routing): force tool_choice=required in OneStepForwardReasoner to eliminate format drift --- .../Reasoning/OneStepForwardReasoner.cs | 40 +++++++++++++++---- .../Chat/ChatCompletionProvider.Chat.cs | 6 +++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs index 0a475ed4e..9a96cef72 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs @@ -15,6 +15,7 @@ limitations under the License. ******************************************************************************/ using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Reasoning; using BotSharp.Abstraction.Templating; @@ -61,14 +62,15 @@ public async Task GetNextInstruction(Agent router, string m MessageId = messageId } }; - var response = await completion.GetChatCompletions(router, dialogs); - // Due to format drift, LLMs may complete with finishReason=function_call (instruction in FunctionArgs) - // or finishReason=stop (instruction serialized as JSON in Content). - // Use FunctionArgs ?? Content to be compatible with both cases. - var inst = (response.FunctionArgs ?? response.Content).JsonContent(); - var routingCtx = _services.GetRequiredService(); - _logger.LogInformation($"[OneStepForwardReasoner] ConversationId: {routingCtx.ConversationId}, MessageId: {messageId}, Next instruction: {response.FunctionArgs ?? response.Content}"); + // Force tool_choice=required so the LLM always returns the instruction as a function call, + // eliminating format drift where the LLM completes with finishReason=stop and returns + // free text or JSON in Content instead of a structured function call. + var response = await GetChatCompletionsWithScopedState(completion, router, dialogs, "tool_choice", "required"); + + var inst = response.FunctionArgs?.JsonContent(); + _logger.LogInformation("[OneStepForwardReasoner] ConversationId: {ConversationId}, MessageId: {MessageId}, Next instruction: {Instruction}", + _services.GetRequiredService().ConversationId, messageId, response.FunctionArgs); // Fix LLM malformed response await ReasonerHelper.FixMalformedResponse(_services, inst); @@ -107,6 +109,30 @@ public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro return true; } + /// + /// Runs chat completion with a scoped conversation state that is set before the call + /// and guaranteed to be removed afterwards, even if the completion throws. + /// + private async Task GetChatCompletionsWithScopedState( + IChatCompletion completion, + Agent agent, + List dialogs, + string stateKey, + string stateValue) + { + var states = _services.GetRequiredService(); + states.SetState(stateKey, stateValue, source: StateSource.Application); + + try + { + return await completion.GetChatCompletions(agent, dialogs); + } + finally + { + states.RemoveState(stateKey); + } + } + private string GetNextStepPrompt(Agent router) { var template = router.Templates.First(x => x.Name == "reasoner.one-step-forward").Content; diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs index 45682e619..f0a4dd716 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs @@ -405,6 +405,12 @@ private async Task InnerGetChatCompletionsStreamingAsync(Agent } } + // Apply tool_choice only when tools are present; tool_choice is rejected by the API otherwise. + if (!options.Tools.IsNullOrEmpty() && _state.GetState("tool_choice") == "required") + { + options.ToolChoice = ChatToolChoice.CreateRequiredChoice(); + } + if (!string.IsNullOrEmpty(agent.Knowledges)) { messages.Add(new SystemChatMessage(agent.Knowledges));