Skip to content

Commit 83a59d4

Browse files
authored
.Net Agents - Fix Function Call Handling for Streaming (#9652)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Fixes: #9638 `System.ArgumentException: An item with the same key has already been added. ` - Duplicate key added when processing function result. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> The processing loop for assistant streaming is selecting completed steps which is resulting in over-processing that violates the state-tracking. This was due to leveraging the existing utiility method `GetRunStepsAsync`. I removed this method in favor of inline invocation since the processing for Streaming and Non-Streaming have distinct considerations. Also, as the SDK has evolved (paging removed), the utility method isn't adding much value. > Note: Was able to reproduce reported issue by setting `ParallelToolCallsEnabled = false` on `OpenAIAssistant_Streaming` demo and verify fix. Existing approach was able to handle parallel function calls and function calls on different steps adequetly. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent 4283cf2 commit 83a59d4

File tree

1 file changed

+9
-18
lines changed

1 file changed

+9
-18
lines changed

dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs

+9-18
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ public static async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync(Assist
194194
throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}");
195195
}
196196

197-
IReadOnlyList<RunStep> steps = await GetRunStepsAsync(client, run, cancellationToken).ConfigureAwait(false);
197+
RunStep[] steps = await client.GetRunStepsAsync(run.ThreadId, run.Id, cancellationToken: cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false);
198198

199199
// Is tool action required?
200200
if (run.Status == RunStatus.RequiresAction)
@@ -475,11 +475,14 @@ public static async IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamin
475475

476476
if (run.Status == RunStatus.RequiresAction)
477477
{
478-
IReadOnlyList<RunStep> steps = await GetRunStepsAsync(client, run, cancellationToken).ConfigureAwait(false);
478+
RunStep[] activeSteps =
479+
await client.GetRunStepsAsync(run.ThreadId, run.Id, cancellationToken: cancellationToken)
480+
.Where(step => step.Status == RunStepStatus.InProgress)
481+
.ToArrayAsync(cancellationToken).ConfigureAwait(false);
479482

480483
// Capture map between the tool call and its associated step
481484
Dictionary<string, string> toolMap = [];
482-
foreach (RunStep step in steps)
485+
foreach (RunStep step in activeSteps)
483486
{
484487
foreach (RunStepToolCall stepDetails in step.Details.ToolCalls)
485488
{
@@ -488,7 +491,7 @@ public static async IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamin
488491
}
489492

490493
// Execute functions in parallel and post results at once.
491-
FunctionCallContent[] functionCalls = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray();
494+
FunctionCallContent[] functionCalls = activeSteps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray();
492495
if (functionCalls.Length > 0)
493496
{
494497
// Emit function-call content
@@ -504,7 +507,7 @@ public static async IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamin
504507
ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults);
505508
asyncUpdates = client.SubmitToolOutputsToRunStreamingAsync(run.ThreadId, run.Id, toolOutputs, cancellationToken);
506509

507-
foreach (RunStep step in steps)
510+
foreach (RunStep step in activeSteps)
508511
{
509512
stepFunctionResults.Add(step.Id, functionResults.Where(result => step.Id == toolMap[result.CallId!]).ToArray());
510513
}
@@ -560,18 +563,6 @@ await RetrieveMessageAsync(
560563
logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run?.Id ?? "Failed", threadId);
561564
}
562565

563-
private static async Task<IReadOnlyList<RunStep>> GetRunStepsAsync(AssistantClient client, ThreadRun run, CancellationToken cancellationToken)
564-
{
565-
List<RunStep> steps = [];
566-
567-
await foreach (RunStep step in client.GetRunStepsAsync(run.ThreadId, run.Id, cancellationToken: cancellationToken).ConfigureAwait(false))
568-
{
569-
steps.Add(step);
570-
}
571-
572-
return steps;
573-
}
574-
575566
private static ChatMessageContent GenerateMessageContent(string? assistantName, ThreadMessage message, RunStep? completedStep = null)
576567
{
577568
AuthorRole role = new(message.Role.ToString());
@@ -788,7 +779,7 @@ private static ChatMessageContent GenerateFunctionCallContent(string agentName,
788779
return functionCallContent;
789780
}
790781

791-
private static ChatMessageContent GenerateFunctionResultContent(string agentName, FunctionResultContent[] functionResults, RunStep completedStep)
782+
private static ChatMessageContent GenerateFunctionResultContent(string agentName, IEnumerable<FunctionResultContent> functionResults, RunStep completedStep)
792783
{
793784
ChatMessageContent functionResultContent = new(AuthorRole.Tool, content: null)
794785
{

0 commit comments

Comments
 (0)