-
Notifications
You must be signed in to change notification settings - Fork 2.1k
.NET: Add Conversation State Sample (Step05) #2697
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8a3e5ea
Initial plan
Copilot 6ccc2e7
Add Agent_OpenAI_Step05_Conversation sample for conversation state ma…
Copilot 51599a1
Update Program.cs comment to accurately describe the sample
Copilot db69c99
Update the code to use the ConversationClient more in line with the s…
rogerbarreto 2801fb9
Apply suggestions from code review
rogerbarreto ad529b0
Changing sample to use ChatClientAgent and conversationId in GetNewTh…
rogerbarreto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
.../AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>net10.0</TargetFrameworks> | ||
|
|
||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
103 changes: 103 additions & 0 deletions
103
dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| // This sample demonstrates how to maintain conversation state using the OpenAIChatClientAgent | ||
| // and AgentThread. By passing the same thread to multiple agent invocations, the agent | ||
| // automatically maintains the conversation history, allowing the AI model to understand | ||
| // context from previous exchanges. | ||
|
|
||
| using System.ClientModel; | ||
| using System.ClientModel.Primitives; | ||
| using System.Text.Json; | ||
| using Microsoft.Agents.AI; | ||
| using Microsoft.Extensions.AI; | ||
| using OpenAI; | ||
| using OpenAI.Chat; | ||
| using OpenAI.Conversations; | ||
|
|
||
| string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); | ||
| string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; | ||
|
|
||
| // Create a ChatClient directly from OpenAIClient | ||
|
rogerbarreto marked this conversation as resolved.
Outdated
|
||
| OpenAIClient openAIClient = new(apiKey); | ||
| ConversationClient conversationClient = openAIClient.GetConversationClient(); | ||
|
|
||
| // Create an agent directly from the ChatClient using OpenAIChatClientAgent | ||
|
rogerbarreto marked this conversation as resolved.
Outdated
|
||
| OpenAIResponseClientAgent agent = new(openAIClient.GetOpenAIResponseClient(model), instructions: "You are a helpful assistant.", name: "ConversationAgent"); | ||
|
|
||
| ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}"))); | ||
|
|
||
| using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createConversationResult.GetRawResponse().Content.ToString()); | ||
| string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!; | ||
|
|
||
| // Set up agent run options with the conversation that was previously created | ||
| ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } }; | ||
|
|
||
| // Create a thread for the conversation - this enables conversation state management for subsequent turns | ||
| AgentThread thread = agent.GetNewThread(); | ||
|
|
||
| Console.WriteLine("=== Multi-turn Conversation Demo ===\n"); | ||
|
|
||
| // First turn: Ask about a topic | ||
| Console.WriteLine("User: What is the capital of France?"); | ||
| UserChatMessage firstMessage = new("What is the capital of France?"); | ||
|
|
||
| // After this call, the conversation state associated in the options is stored in 'thread' and used in subsequent calls | ||
| ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions); | ||
| Console.WriteLine($"Assistant: {firstResponse.Content.Last().Text}\n"); | ||
|
|
||
| // Second turn: Follow-up question that relies on conversation context | ||
| Console.WriteLine("User: What famous landmarks are located there?"); | ||
| UserChatMessage secondMessage = new("What famous landmarks are located there?"); | ||
|
|
||
| // No options with conversation id is needed here because 'thread' maintains the context | ||
| ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread); | ||
| Console.WriteLine($"Assistant: {secondResponse.Content.Last().Text}\n"); | ||
|
|
||
| // Third turn: Another follow-up that demonstrates context continuity | ||
| Console.WriteLine("User: How tall is the most famous one?"); | ||
| UserChatMessage thirdMessage = new("How tall is the most famous one?"); | ||
|
|
||
| // No options with conversation id is needed here because 'thread' maintains the context | ||
| ChatCompletion thirdResponse = await agent.RunAsync([thirdMessage], thread); | ||
| Console.WriteLine($"Assistant: {thirdResponse.Content.Last().Text}\n"); | ||
|
|
||
| Console.WriteLine("=== End of Conversation ==="); | ||
|
|
||
| // Show full conversation history | ||
| Console.WriteLine("Full Conversation History:"); | ||
| ClientResult getConversationResult = await conversationClient.GetConversationAsync(conversationId); | ||
|
|
||
| Console.WriteLine("Conversation created."); | ||
| Console.WriteLine($" Conversation ID: {conversationId}"); | ||
| Console.WriteLine(); | ||
|
|
||
| CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId); | ||
| foreach (ClientResult result in getConversationItemsResults.GetRawPages()) | ||
| { | ||
| Console.WriteLine("Message contents retrieved. Order is most recent first by default."); | ||
| using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString()); | ||
| foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray()) | ||
| { | ||
| string messageId = element.GetProperty("id"u8).ToString(); | ||
| string messageRole = element.GetProperty("role"u8).ToString(); | ||
| Console.WriteLine($" Message ID: {messageId}"); | ||
| Console.WriteLine($" Message Role: {messageRole}"); | ||
|
|
||
| foreach (var content in element.GetProperty("content").EnumerateArray()) | ||
| { | ||
| string messageContentText = content.GetProperty("text"u8).ToString(); | ||
| Console.WriteLine($" Message Text: {messageContentText}"); | ||
| } | ||
| Console.WriteLine(); | ||
| } | ||
| } | ||
|
|
||
| ClientResult deleteConversationResult = conversationClient.DeleteConversation(conversationId); | ||
| using JsonDocument deleteConversationResultAsJson = JsonDocument.Parse(deleteConversationResult.GetRawResponse().Content.ToString()); | ||
| bool deleted = deleteConversationResultAsJson.RootElement | ||
| .GetProperty("deleted"u8) | ||
| .GetBoolean(); | ||
|
|
||
| Console.WriteLine("Conversation deleted."); | ||
| Console.WriteLine($" Deleted: {deleted}"); | ||
| Console.WriteLine(); | ||
90 changes: 90 additions & 0 deletions
90
...mples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # Managing Conversation State with OpenAI | ||
|
|
||
| This sample demonstrates how to maintain conversation state across multiple turns using the Agent Framework with OpenAI's Conversation API. | ||
|
|
||
| ## What This Sample Shows | ||
|
|
||
| - **Conversation State Management**: Shows how to use `ConversationClient` and `AgentThread` to maintain conversation context across multiple agent invocations | ||
| - **Multi-turn Conversations**: Demonstrates follow-up questions that rely on context from previous messages in the conversation | ||
| - **Server-Side Storage**: Uses OpenAI's Conversation API to manage conversation history server-side, allowing the model to access previous messages without resending them | ||
| - **Conversation Lifecycle**: Demonstrates creating, retrieving, and deleting conversations | ||
|
|
||
| ## Key Concepts | ||
|
|
||
| ### ConversationClient for Server-Side Storage | ||
|
|
||
| The `ConversationClient` manages conversations on OpenAI's servers: | ||
|
|
||
| ```csharp | ||
| // Create a ConversationClient from OpenAIClient | ||
| OpenAIClient openAIClient = new(apiKey); | ||
| ConversationClient conversationClient = openAIClient.GetConversationClient(); | ||
|
|
||
| // Create a new conversation | ||
| ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}"))); | ||
| ``` | ||
|
|
||
| ### AgentThread for Conversation State | ||
|
|
||
| The `AgentThread` works with `ChatClientAgentRunOptions` to link the agent to a server-side conversation: | ||
|
|
||
| ```csharp | ||
| // Set up agent run options with the conversation ID | ||
| ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } }; | ||
|
|
||
| // Create a thread for the conversation | ||
| AgentThread thread = agent.GetNewThread(); | ||
|
|
||
| // First call links the thread to the conversation | ||
| ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions); | ||
|
|
||
| // Subsequent calls use the thread without needing to pass options again | ||
| ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread); | ||
| ``` | ||
|
|
||
| ### Retrieving Conversation History | ||
|
|
||
| You can retrieve the full conversation history from the server: | ||
|
|
||
| ```csharp | ||
| CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId); | ||
| foreach (ClientResult result in getConversationItemsResults.GetRawPages()) | ||
| { | ||
| // Process conversation items | ||
| } | ||
| ``` | ||
|
|
||
| ### How It Works | ||
|
|
||
| 1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key | ||
| 2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation | ||
| 3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions | ||
| 4. **Create a Thread**: Call `agent.GetNewThread()` to create a new conversation thread | ||
| 5. **Link Thread to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call | ||
| 6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the thread - context is maintained | ||
| 7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()` | ||
|
|
||
| ## Running the Sample | ||
|
|
||
| 1. Set the required environment variables: | ||
| ```powershell | ||
| $env:OPENAI_API_KEY = "your_api_key_here" | ||
| $env:OPENAI_MODEL = "gpt-4o-mini" | ||
| ``` | ||
|
|
||
| 2. Run the sample: | ||
| ```powershell | ||
| dotnet run | ||
| ``` | ||
|
|
||
| ## Expected Output | ||
|
|
||
| The sample demonstrates a three-turn conversation where each follow-up question relies on context from previous messages: | ||
|
|
||
| 1. First question asks about the capital of France | ||
| 2. Second question asks about landmarks "there" - requiring understanding of the previous answer | ||
| 3. Third question asks about "the most famous one" - requiring context from both previous turns | ||
|
|
||
| After the conversation, the sample retrieves and displays the full conversation history from the server, then cleans up by deleting the conversation. | ||
|
|
||
| This demonstrates that the conversation state is properly maintained across multiple agent invocations using OpenAI's server-side conversation storage. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.