Skip to content

Commit 989037e

Browse files
author
Yanan Wang
committed
fix(routing): force tool_choice=required in OneStepForwardReasoner to eliminate format drift
1 parent 9ab8a6e commit 989037e

2 files changed

Lines changed: 39 additions & 7 deletions

File tree

src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
******************************************************************************/
1616

1717
using BotSharp.Abstraction.Infrastructures.Enums;
18+
using BotSharp.Abstraction.MLTasks;
1819
using BotSharp.Abstraction.Routing.Models;
1920
using BotSharp.Abstraction.Routing.Reasoning;
2021
using BotSharp.Abstraction.Templating;
@@ -61,14 +62,15 @@ public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string m
6162
MessageId = messageId
6263
}
6364
};
64-
var response = await completion.GetChatCompletions(router, dialogs);
6565

66-
// Due to format drift, LLMs may complete with finishReason=function_call (instruction in FunctionArgs)
67-
// or finishReason=stop (instruction serialized as JSON in Content).
68-
// Use FunctionArgs ?? Content to be compatible with both cases.
69-
var inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
70-
var routingCtx = _services.GetRequiredService<IRoutingContext>();
71-
_logger.LogInformation($"[OneStepForwardReasoner] ConversationId: {routingCtx.ConversationId}, MessageId: {messageId}, Next instruction: {response.FunctionArgs ?? response.Content}");
66+
// Force tool_choice=required so the LLM always returns the instruction as a function call,
67+
// eliminating format drift where the LLM completes with finishReason=stop and returns
68+
// free text or JSON in Content instead of a structured function call.
69+
var response = await GetChatCompletionsWithScopedState(completion, router, dialogs, "tool_choice", "required");
70+
71+
var inst = response.FunctionArgs?.JsonContent<FunctionCallFromLlm>();
72+
_logger.LogInformation("[OneStepForwardReasoner] ConversationId: {ConversationId}, MessageId: {MessageId}, Next instruction: {Instruction}",
73+
_services.GetRequiredService<IRoutingContext>().ConversationId, messageId, response.FunctionArgs);
7274

7375
// Fix LLM malformed response
7476
await ReasonerHelper.FixMalformedResponse(_services, inst);
@@ -107,6 +109,30 @@ public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro
107109
return true;
108110
}
109111

112+
/// <summary>
113+
/// Runs chat completion with a scoped conversation state that is set before the call
114+
/// and guaranteed to be removed afterwards, even if the completion throws.
115+
/// </summary>
116+
private async Task<RoleDialogModel> GetChatCompletionsWithScopedState(
117+
IChatCompletion completion,
118+
Agent agent,
119+
List<RoleDialogModel> dialogs,
120+
string stateKey,
121+
string stateValue)
122+
{
123+
var states = _services.GetRequiredService<IConversationStateService>();
124+
states.SetState(stateKey, stateValue, source: StateSource.Application);
125+
126+
try
127+
{
128+
return await completion.GetChatCompletions(agent, dialogs);
129+
}
130+
finally
131+
{
132+
states.RemoveState(stateKey);
133+
}
134+
}
135+
110136
private string GetNextStepPrompt(Agent router)
111137
{
112138
var template = router.Templates.First(x => x.Name == "reasoner.one-step-forward").Content;

src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,12 @@ private async Task<RoleDialogModel> InnerGetChatCompletionsStreamingAsync(Agent
405405
}
406406
}
407407

408+
// Apply tool_choice only when tools are present; tool_choice is rejected by the API otherwise.
409+
if (!options.Tools.IsNullOrEmpty() && _state.GetState("tool_choice") == "required")
410+
{
411+
options.ToolChoice = ChatToolChoice.CreateRequiredChoice();
412+
}
413+
408414
if (!string.IsNullOrEmpty(agent.Knowledges))
409415
{
410416
messages.Add(new SystemChatMessage(agent.Knowledges));

0 commit comments

Comments
 (0)