Skip to content

Commit dba92a2

Browse files
authored
Merge pull request SciSharp#1347 from ywang1110/master
fix(routing): force tool_choice=required in OneStepForwardReasoner to eliminate format drift
2 parents bca130e + 989037e commit dba92a2

2 files changed

Lines changed: 39 additions & 2 deletions

File tree

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

Lines changed: 33 additions & 2 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,9 +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-
var inst = response.Content.JsonContent<FunctionCallFromLlm>();
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);
6774

6875
// Fix LLM malformed response
6976
await ReasonerHelper.FixMalformedResponse(_services, inst);
@@ -102,6 +109,30 @@ public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro
102109
return true;
103110
}
104111

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+
105136
private string GetNextStepPrompt(Agent router)
106137
{
107138
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)