diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIChatClient.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIChatClient.cs index ddaf6bd5929..0f4f1c290c6 100644 --- a/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIChatClient.cs @@ -214,6 +214,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( RunId = runId, Messages = messagesList.AsAGUIMessages(this._jsonSerializerOptions), State = state, + ForwardedProperties = ExtractForwardedPropertiesFromOptions(options), }; // Add tools if provided @@ -288,6 +289,18 @@ public async IAsyncEnumerable GetStreamingResponseAsync( return threadId; } + private static JsonElement ExtractForwardedPropertiesFromOptions(ChatOptions? options) + { + if (options?.AdditionalProperties is null || + !options.AdditionalProperties.TryGetValue("ag_ui_forwarded_properties", out object? forwardedProperties) || + forwardedProperties is not JsonElement jsonElement) + { + return default; + } + + return jsonElement; + } + // Extract the session id from the second last message's function call content additional properties private static string? ExtractTemporaryThreadId(List messagesList) { diff --git a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs index d5890bb5f20..856d7143b36 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AGUIChatClientTests.cs @@ -1395,6 +1395,44 @@ public async Task GetStreamingResponseAsync_WithNoStateDataContent_SendsEmptySta Assert.Null(captureHandler.CapturedState); } + [Fact] + public async Task GetStreamingResponseAsync_ForwardsAdditionalPropertiesInRunInputAsync() + { + // Arrange + var forwardedProperties = new { tenant = "contoso", conversation = "abc123" }; + + var captureHandler = new StateCapturingTestDelegatingHandler(); + captureHandler.AddResponse( + [ + new RunStartedEvent { ThreadId = "thread1", RunId = "run1" }, + new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" } + ]); + using HttpClient httpClient = new(captureHandler); + + var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options); + var options = new ChatOptions + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["ag_ui_forwarded_properties"] = JsonSerializer.SerializeToElement(forwardedProperties) + } + }; + List messages = [new ChatMessage(ChatRole.User, "Hello")]; + + // Act + await foreach (var _ in chatClient.GetStreamingResponseAsync(messages, options)) + { + // Just consume the stream + } + + // Assert + Assert.True(captureHandler.RequestWasMade); + Assert.NotNull(captureHandler.CapturedForwardedProperties); + Assert.Equal(JsonValueKind.Object, captureHandler.CapturedForwardedProperties.Value.ValueKind); + Assert.Equal("contoso", captureHandler.CapturedForwardedProperties.Value.GetProperty("tenant").GetString()); + Assert.Equal("abc123", captureHandler.CapturedForwardedProperties.Value.GetProperty("conversation").GetString()); + } + [Fact] public async Task GetStreamingResponseAsync_WithMalformedStateJson_ThrowsInvalidOperationExceptionAsync() { @@ -1730,6 +1768,7 @@ internal sealed class StateCapturingTestDelegatingHandler : DelegatingHandler public bool RequestWasMade { get; private set; } public JsonElement? CapturedState { get; private set; } + public JsonElement? CapturedForwardedProperties { get; private set; } public int CapturedMessageCount { get; private set; } public IReadOnlyList CapturedMessageCounts => this._capturedMessageCounts; @@ -1755,6 +1794,10 @@ protected override async Task SendAsync(HttpRequestMessage { this.CapturedState = input.State; } + if (input.ForwardedProperties.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null) + { + this.CapturedForwardedProperties = input.ForwardedProperties; + } this.CapturedMessageCount = input.Messages.Count(); this._capturedMessageCounts.Add(this.CapturedMessageCount); }