diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index c9a4a0a0ab..8edd5f847d 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -88,24 +88,24 @@ - + - + - + - + - + @@ -129,6 +129,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj new file mode 100644 index 0000000000..eeda3eef6f --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs new file mode 100644 index 0000000000..9f81a27dda --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Program.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to maintain conversation state using the OpenAIResponseClientAgent +// 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 ConversationClient directly from OpenAIClient +OpenAIClient openAIClient = new(apiKey); +ConversationClient conversationClient = openAIClient.GetConversationClient(); + +// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent +ChatClientAgent agent = new(openAIClient.GetOpenAIResponseClient(model).AsIChatClient(), 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()!; + +// Create a thread for the conversation - this enables conversation state management for subsequent turns +AgentThread thread = agent.GetNewThread(conversationId); + +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); +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?"); + +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?"); + +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(); diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md new file mode 100644 index 0000000000..c279ba2c17 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/README.md @@ -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. diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md b/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md index 6f2c77f39b..019af7f2b6 100644 --- a/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md @@ -13,4 +13,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th |[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent with native OpenAI SDK types. Shows both regular and streaming invocation of the agent.| |[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.| |[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.| -|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.| \ No newline at end of file +|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.| +|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentThread for context continuity.| \ No newline at end of file