-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Python: .NET: Fix .NET conversation memory in DevUI (#3484) #4294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
798d868
4be48e5
cee4f23
5222710
24f19ba
3bc06d0
571ddab
902d8e1
db8ffa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Text.Json; | ||
| using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; | ||
|
|
||
| /// <summary> | ||
| /// Converts stored <see cref="ItemResource"/> objects back to <see cref="ChatMessage"/> objects | ||
| /// for injecting conversation history into agent execution. | ||
| /// </summary> | ||
| internal static class ItemResourceConversions | ||
| { | ||
| /// <summary> | ||
| /// Converts a sequence of <see cref="ItemResource"/> items to a list of <see cref="ChatMessage"/> objects. | ||
| /// Only converts message, function call, and function result items. Other item types are skipped. | ||
| /// </summary> | ||
| public static List<ChatMessage> ToChatMessages(IEnumerable<ItemResource> items) | ||
| { | ||
| var messages = new List<ChatMessage>(); | ||
|
|
||
| foreach (var item in items) | ||
| { | ||
| switch (item) | ||
| { | ||
| case ResponsesUserMessageItemResource userMsg: | ||
| messages.Add(new ChatMessage(ChatRole.User, ConvertContents(userMsg.Content))); | ||
| break; | ||
|
|
||
| case ResponsesAssistantMessageItemResource assistantMsg: | ||
| messages.Add(new ChatMessage(ChatRole.Assistant, ConvertContents(assistantMsg.Content))); | ||
| break; | ||
|
|
||
| case ResponsesSystemMessageItemResource systemMsg: | ||
| messages.Add(new ChatMessage(ChatRole.System, ConvertContents(systemMsg.Content))); | ||
| break; | ||
|
|
||
| case ResponsesDeveloperMessageItemResource developerMsg: | ||
| messages.Add(new ChatMessage(new ChatRole("developer"), ConvertContents(developerMsg.Content))); | ||
| break; | ||
|
|
||
| case FunctionToolCallItemResource funcCall: | ||
| var arguments = ParseArguments(funcCall.Arguments); | ||
| messages.Add(new ChatMessage(ChatRole.Assistant, | ||
| [ | ||
| new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments) | ||
| ])); | ||
| break; | ||
|
|
||
| case FunctionToolCallOutputItemResource funcOutput: | ||
| messages.Add(new ChatMessage(ChatRole.Tool, | ||
| [ | ||
| new FunctionResultContent(funcOutput.CallId, funcOutput.Output) | ||
| ])); | ||
| break; | ||
|
|
||
| // Skip all other item types (reasoning, executor_action, web_search, etc.) | ||
| // They are not relevant for conversation context. | ||
| } | ||
| } | ||
|
|
||
| return messages; | ||
| } | ||
|
|
||
| private static List<AIContent> ConvertContents(List<ItemContent> contents) | ||
| { | ||
| var result = new List<AIContent>(); | ||
| foreach (var content in contents) | ||
| { | ||
| var aiContent = ItemContentConverter.ToAIContent(content); | ||
| if (aiContent is not null) | ||
| { | ||
| result.Add(aiContent); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| private static Dictionary<string, object?>? ParseArguments(string? argumentsJson) | ||
| { | ||
| if (string.IsNullOrEmpty(argumentsJson)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| using var doc = JsonDocument.Parse(argumentsJson); | ||
| var result = new Dictionary<string, object?>(); | ||
| foreach (var property in doc.RootElement.EnumerateObject()) | ||
| { | ||
| result[property.Name] = property.Value.ValueKind switch | ||
| { | ||
| JsonValueKind.String => property.Value.GetString(), | ||
| JsonValueKind.Number => property.Value.GetDouble(), | ||
| JsonValueKind.True => true, | ||
| JsonValueKind.False => false, | ||
| JsonValueKind.Null => null, | ||
| _ => property.Value.GetRawText() | ||
| }; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| catch (JsonException) | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -425,11 +425,28 @@ private async Task ExecuteResponseAsync(string responseId, ResponseState state, | |
| // Create agent invocation context | ||
| var context = new AgentInvocationContext(new IdGenerator(responseId: responseId, conversationId: state.Response?.Conversation?.Id)); | ||
|
|
||
| // Load conversation history if a conversation ID is provided | ||
| IReadOnlyList<Extensions.AI.ChatMessage>? conversationHistory = null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While this change does fix loading of chat history, the code is still broken for other scenarios. Happy to work with you get this fixed for all memory scenarios.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @westey-m . I agree. |
||
| if (this._conversationStorage is not null && request.Conversation?.Id is not null) | ||
| { | ||
| var itemsResult = await this._conversationStorage.ListItemsAsync( | ||
| request.Conversation.Id, | ||
| limit: 100, | ||
victordibia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| order: SortOrder.Ascending, | ||
| cancellationToken: linkedCts.Token).ConfigureAwait(false); | ||
|
|
||
| var history = ItemResourceConversions.ToChatMessages(itemsResult.Data); | ||
| if (history.Count > 0) | ||
| { | ||
| conversationHistory = history; | ||
| } | ||
| } | ||
|
|
||
| // Collect output items for conversation storage | ||
| List<ItemResource> outputItems = []; | ||
|
|
||
| // Execute using the injected executor | ||
| await foreach (var streamingEvent in this._executor.ExecuteAsync(context, request, linkedCts.Token).ConfigureAwait(false)) | ||
| await foreach (var streamingEvent in this._executor.ExecuteAsync(context, request, conversationHistory, linkedCts.Token).ConfigureAwait(false)) | ||
| { | ||
| state.AddStreamingEvent(streamingEvent); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to add
conversationHistorytoAgentInvocationContext?History sounds like an essential part of the conversation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree @westey-m 's suggestion to store/load AgentSession per conversation with a custom ChatHistoryProvider would address this as well, making the separate parameter unnecessary.
Will work with westey-m on this broader change, likely in a separate PR.