Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,9 +62,15 @@ public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string m
MessageId = messageId
}
};
var response = await completion.GetChatCompletions(router, dialogs);

var inst = response.Content.JsonContent<FunctionCallFromLlm>();
// 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<FunctionCallFromLlm>();
_logger.LogInformation("[OneStepForwardReasoner] ConversationId: {ConversationId}, MessageId: {MessageId}, Next instruction: {Instruction}",
_services.GetRequiredService<IRoutingContext>().ConversationId, messageId, response.FunctionArgs);

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

/// <summary>
/// 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.
/// </summary>
private async Task<RoleDialogModel> GetChatCompletionsWithScopedState(
IChatCompletion completion,
Agent agent,
List<RoleDialogModel> dialogs,
string stateKey,
string stateValue)
{
var states = _services.GetRequiredService<IConversationStateService>();
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ private async Task<RoleDialogModel> 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));
Expand Down
Loading