Compare commits

..
Author SHA1 Message Date
Shyju Krishnankutty ad4b732741 Adding ReflectExecutors method to Workflow. 2026-01-22 12:07:03 -08:00
945 changed files with 14402 additions and 37156 deletions
+5 -23
View File
@@ -1,34 +1,16 @@
{
"name": "C# (.NET)",
//"image": "mcr.microsoft.com/devcontainers/dotnet",
// Workaround for https://github.com/devcontainers/images/issues/1752
"build": {
"dockerfile": "dotnet.Dockerfile"
},
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
"features": {
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "2"
},
"ghcr.io/devcontainers/features/powershell:1": {
"version": "latest"
},
"ghcr.io/azure/azure-dev/azd:0": {
"version": "latest"
},
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "10.0",
"aspNetCoreRuntimeVersions": "10.0"
},
"ghcr.io/devcontainers/features/copilot-cli:1": {}
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
},
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"GitHub.vscode-github-actions",
"ms-dotnettools.csdevkit",
"vscode-icons-team.vscode-icons",
"ms-windows-ai-studio.windows-ai-studio"
-5
View File
@@ -1,5 +0,0 @@
FROM mcr.microsoft.com/devcontainers/universal:latest
# Remove Yarn repository with expired GPG key to prevent apt-get update failures
# Tracking issue: https://github.com/devcontainers/images/issues/1752
RUN rm -f /etc/apt/sources.list.d/yarn.list
-3
View File
@@ -2,6 +2,3 @@
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
python/packages/durabletask/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
-2
View File
@@ -14,8 +14,6 @@ Here are some general guidelines that apply to all code.
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
- All public methods and classes should have XML documentation comments.
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
- After adding or modifying code, run `dotnet format` to automatically fix any formatting errors.
### C# Sample Code Guidelines
+7 -7
View File
@@ -83,7 +83,7 @@ jobs:
dotnet
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
@@ -139,7 +139,7 @@ jobs:
popd
popd
rm -rf "$TEMP_DIR"
- name: Run Unit Tests
shell: bash
run: |
@@ -147,7 +147,7 @@ jobs:
for project in $UT_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
@@ -165,8 +165,8 @@ jobs:
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
- name: Log event name and matrix integration-tests
shell: bash
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
shell: bash
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
- name: Azure CLI Login
if: github.event_name != 'pull_request' && matrix.integration-tests
@@ -192,10 +192,10 @@ jobs:
for project in $INTEGRATION_TEST_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled"
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
else
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
fi
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
id: azure-functions-setup
- name: Test with pytest
timeout-minutes: 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 900 --retries 3 --retry-delay 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 600 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test core samples
timeout-minutes: 10
+5 -6
View File
@@ -209,14 +209,13 @@ WARP.md
**/tmpclaude*
# Azurite storage emulator files
*/__azurite_db_blob__.json*
*/__azurite_db_blob_extent__.json*
*/__azurite_db_queue__.json*
*/__azurite_db_queue_extent__.json*
*/__azurite_db_table__.json*
*/__azurite_db_blob__.json
*/__azurite_db_blob_extent__.json
*/__azurite_db_queue__.json
*/__azurite_db_queue_extent__.json
*/__azurite_db_table__.json
*/__blobstorage__/
*/__queuestorage__/
*/AzuriteConfig
# Azure Functions local settings
local.settings.json
-423
View File
@@ -1,423 +0,0 @@
---
status: accepted
contact: westey-m
date: 2025-01-21
deciders: sergeymenshykh, markwallace, rbarreto, westey-m, stephentoub
consulted: reubenbond
informed:
---
# Feature Collections
## Context and Problem Statement
When using agents, we often have cases where we want to pass some arbitrary services or data to an agent or some component in the agent execution stack.
These services or data are not necessarily known at compile time and can vary by the agent stack that the user has built.
E.g., there may be an agent decorator or chat client decorator that was added to the stack by the user, and an arbitrary payload needs to be passed to that decorator.
Since these payloads are related to components that are not integral parts of the agent framework, they cannot be added as strongly typed settings to the agent run options.
However, the payloads could be added to the agent run options as loosely typed 'features', that can be retrieved as needed.
In some cases certain classes of agents may support the same capability, but not all agents do.
Having the configuration for such a capability on the main abstraction would advertise the functionality to all users, even if their chosen agent does not support it.
The user may type test for certain agent types, and call overloads on the appropriate agent types, with the strongly typed configuration.
Having a feature collection though, would be an alternative way of passing such configuration, without needing to type check the agent type.
All agents that support the functionality would be able to check for the configuration and use it, simplifying the user code.
If the agent does not support the capability, that configuration would be ignored.
### Sample Scenario 1 - Per Run ChatMessageStore Override for hosting Libraries
We are building an agent hosting library, that can host any agent built using the agent framework.
Where an agent is not built on a service that uses in-service chat history storage, the hosting library wants to force the agent to use
the hosting library's chat history storage implementation.
This chat history storage implementation may be specifically tailored to the type of protocol that the hosting library uses, e.g. conversation id based storage or response id based storage.
The hosting library does not know what type of agent it is hosting, so it cannot provide a strongly typed parameter on the agent.
Instead, it adds the chat history storage implementation to a feature collection, and if the agent supports custom chat history storage, it retrieves the implementation from the feature collection and uses it.
```csharp
// Pseudo-code for an agent hosting library that supports conversation id based hosting.
public async Task<string> HandleConversationsBasedRequestAsync(AIAgent agent, string conversationId, string userInput)
{
var thread = await this._threadStore.GetOrCreateThread(conversationId);
// The hosting library can set a per-run chat message store via Features that only applies for that run.
// This message store will load and save messages under the conversation id provided.
ConversationsChatMessageStore messageStore = new(this._dbClient, conversationId);
var response = await agent.RunAsync(
userInput,
thread,
options: new AgentRunOptions()
{
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
});
await this._threadStore.SaveThreadAsync(conversationId, thread);
return response.Text;
}
// Pseudo-code for an agent hosting library that supports response id based hosting.
public async Task<(string responseMessage, string responseId)> HandleResponseIdBasedRequestAsync(AIAgent agent, string previousResponseId, string userInput)
{
var thread = await this._threadStore.GetOrCreateThreadAsync(previousResponseId);
// The hosting library can set a per-run chat message store via Features that only applies for that run.
// This message store will buffer newly added messages until explicitly saved after the run.
ResponsesChatMessageStore messageStore = new(this._dbClient, previousResponseId);
var response = await agent.RunAsync(
userInput,
thread,
options: new AgentRunOptions()
{
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
});
// Since the message store may not actually have been used at all (if the agent's underlying chat client requires service-based chat history storage),
// we may not have anything to save back to the database.
// We still want to generate a new response id though, so that we can save the updated thread state under that id.
// We should also use the same id to save any buffered messages in the message store if there are any.
var newResponseId = this.GenerateResponseId();
if (messageStore.HasBufferedMessages)
{
await messageStore.SaveBufferedMessagesAsync(newResponseId);
}
// Save the updated thread state under the new response id that was generated by the store.
await this._threadStore.SaveThreadAsync(newResponseId, thread);
return (response.Text, newResponseId);
}
```
### Sample Scenario 2 - Structured output
Currently our base abstraction does not support structured output, since the capability is not supported by all agents.
For those agents that don't support structured output, we could add an agent decorator that takes the response from the underlying agent, and applies structured output parsing on top of it via an additional LLM call.
If we add structured output configuration as a feature, then any agent that supports structured output could retrieve the configuration from the feature collection and apply it, and where it is not supported, the configuration would simply be ignored.
We could add a simple StructuredOutputAgentFeature that can be added to the list of features and also be used to return the generated structured output.
```csharp
internal class StructuredOutputAgentFeature
{
public Type? OutputType { get; set; }
public JsonSerializerOptions? SerializerOptions { get; set; }
public bool? UseJsonSchemaResponseFormat { get; set; }
// Contains the result of the structured output parsing request.
public ChatResponse? ChatResponse { get; set; }
}
```
We can add a simple decorator class that does the chat client invocation.
```csharp
public class StructuredOutputAgent : DelegatingAIAgent
{
private readonly IChatClient _chatClient;
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
: base(innerAgent)
{
this._chatClient = Throw.IfNull(chatClient);
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
// Run the inner agent first, to get back the text response we want to convert.
var response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
if (options?.Features?.TryGet<StructuredOutputAgentFeature>(out var responseFormatFeature) is true
&& responseFormatFeature.OutputType is not null)
{
// Create the chat options to request structured output.
ChatOptions chatOptions = new()
{
ResponseFormat = ChatResponseFormat.ForJsonSchema(responseFormatFeature.OutputType, responseFormatFeature.SerializerOptions)
};
// Invoke the chat client to transform the text output into structured data.
// The feature is updated with the result.
// The code can be simplified by adding a non-generic structured output GetResponseAsync
// overload that takes Type as input.
responseFormatFeature.ChatResponse = await this._chatClient.GetResponseAsync(
messages: new[]
{
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
new ChatMessage(ChatRole.User, response.Text)
},
options: chatOptions,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
return response;
}
}
```
Finally, we can add an extension method on `AIAgent` that can add the feature to the run options and check the feature for the structured output result and add the deserialized result to the response.
```csharp
public static async Task<AgentRunResponse<T>> RunAsync<T>(
this AIAgent agent,
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
// Create the structured output feature.
var structuredOutputFeature = new StructuredOutputAgentFeature();
structuredOutputFeature.OutputType = typeof(T);
structuredOutputFeature.UseJsonSchemaResponseFormat = useJsonSchemaResponseFormat;
// Run the agent.
options ??= new AgentRunOptions();
options.Features ??= new AgentFeatureCollection();
options.Features.Set(structuredOutputFeature);
var response = await agent.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
// Deserialize the JSON output.
if (structuredOutputFeature.ChatResponse is not null)
{
var typed = new ChatResponse<T>(structuredOutputFeature.ChatResponse, serializerOptions ?? AgentJsonUtilities.DefaultOptions);
return new AgentRunResponse<T>(response, typed.Result);
}
throw new InvalidOperationException("No structured output response was generated by the agent.");
}
```
We can then use the extension method with any agent that supports structured output or that has
been decorated with the `StructuredOutputAgent` decorator.
```csharp
agent = new StructuredOutputAgent(agent, chatClient);
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>([new ChatMessage(
ChatRole.User,
"Please provide information about John Smith, who is a 35-year-old software engineer.")]);
```
## Implementation Options
Three options were considered for implementing feature collections:
- **Option 1**: FeatureCollections similar to ASP.NET Core
- **Option 2**: AdditionalProperties Dictionary
- **Option 3**: IServiceProvider
Here are some comparisons about their suitability for our use case:
| Criteria | Feature Collection | Additional Properties | IServiceProvider |
|------------------|--------------------|-----------------------|------------------|
|Ease of use |✅ Good |❌ Bad |✅ Good |
|User familiarity |❌ Bad |✅ Good |✅ Good |
|Type safety |✅ Good |❌ Bad |✅ Good |
|Ability to modify registered options when progressing down the stack|✅ Supported|✅ Supported|❌ Not-Supported (IServiceProvider is read-only)|
|Already available in MEAI stack|❌ No|✅ Yes|❌ No|
|Ambiguity with existing AdditionalProperties|❌ Yes|✅ No|❌ Yes|
## IServiceProvider
Service Collections and Service Providers provide a very popular way to register and retrieve services by type and could be used as a way to pass features to agents and chat clients.
However, since IServiceProvider is read-only, it is not possible to modify the registered services when progressing down the execution stack.
E.g. an agent decorator cannot add additional services to the IServiceProvider passed to it when calling into the inner agent.
IServiceProvider also does not expose a way to list all services contained in it, making it difficult to copy services from one provider to another.
This lack of mutability makes IServiceProvider unsuitable for our use case, since we will not be able to use it to build sample scenario 2.
## AdditionalProperties dictionary
The AdditionalProperties dictionary is already available on various options classes in the agent framework as well as in the MEAI stack and
allows storing arbitrary key/value pairs, where the key is a string and the value is an object.
While FeatureCollection uses Type as a key, AdditionalProperties uses string keys.
This means that users need to agree on string keys to use for specific features, however it is also possible to use Type.FullName as a key by convention
to avoid key collisions, which is an easy convention to follow.
Since the value of AdditionalProperties is of type object, users need to cast the value to the expected type when retrieving it, which is also
a drawback, but when using the convention of using Type.FullName as a key, there is at least a clear expectation of what type to cast to.
```csharp
// Setting a feature
options.AdditionalProperties[typeof(MyFeature).FullName] = new MyFeature();
// Retrieving a feature
if (options.AdditionalProperties.TryGetValue(typeof(MyFeature).FullName, out var featureObj)
&& featureObj is MyFeature myFeature)
{
// Use myFeature
}
```
It would also be possible to add extension methods to simplify setting and getting features from AdditionalProperties.
Having a base class for features should help make this more feature rich.
```csharp
// Setting a feature, this can use Type.FullName as the key.
options.AdditionalProperties
.WithFeature(new MyFeature());
// Retrieving a feature, this can use Type.FullName as the key.
if (options.AdditionalProperties.TryGetFeature<MyFeature>(out var myFeature))
{
// Use myFeature
}
```
It would also be possible to add extension methods for a feature to simplify setting and getting features from AdditionalProperties.
```csharp
// Setting a feature
options.AdditionalProperties
.WithMyFeature(new MyFeature());
// Retrieving a feature
if (options.AdditionalProperties.TryGetMyFeature(out var myFeature))
{
// Use myFeature
}
```
## Feature Collection
If we choose the feature collection option, we need to decide on the design of the feature collection itself.
### Feature Collections extension points
We need to decide the set of actions that feature collections would be supported for. Here is the suggested list of actions:
**MAAI.AIAgent:**
1. GetNewThread
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
1. DeserializeThread
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
1. Run / RunStreaming
1. E.g. this would allow passing an override chat message store just for that run, or a desired schema for a structured output middleware component.
**MEAI.ChatClient:**
1. GetResponse / GetStreamingResponse
### Reconciling with existing AdditionalProperties
If we decide to add feature collections, separately from the existing AdditionalProperties dictionaries, we need to consider how to explain to users when to use each one.
One possible approach though is to have the one use the other under the hood.
AdditionalProperties could be stored as a feature in the feature collection.
Users would be able to retrieve additional properties from the feature collection, in addition to retrieving it via a dedicated AdditionalProperties property.
E.g. `features.Get<AdditionalPropertiesDictionary>()`
One challenge with this approach is that when setting a value in the AdditionalProperties dictionary, the feature collection would need to be created first if it does not already exist.
```csharp
public class AgentRunOptions
{
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
public IAgentFeatureCollection? Features { get; set; }
}
var options = new AgentRunOptions();
// This would need to create the feature collection first, if it does not already exist.
options.AdditionalProperties ??= new AdditionalPropertiesDictionary();
```
Since IAgentFeatureCollection is an interface, AgentRunOptions would need to have a concrete implementation of the interface to create, meaning that the user cannot decide.
It also means that if the user doesn't realise that AdditionalProperties is implemented using feature collections, they may set a value on AdditionalProperties, and then later overwrite the entire feature collection, losing the AdditionalProperties feature.
Options to avoid these issues:
1. Make `Features` readonly.
1. This would prevent the user from overwriting the feature collection after setting AdditionalProperties.
1. Since the user cannot set their own implementation of IAgentFeatureCollection, having an interface for it may not be necessary.
### Feature Collection Implementation
We have two options for implementing feature collections:
1. Create our own [IAgentFeatureCollection interface](https://github.com/microsoft/agent-framework/pull/2354/files#diff-9c42f3e60d70a791af9841d9214e038c6de3eebfc10e3997cb4cdffeb2f1246d) and [implementation](https://github.com/microsoft/agent-framework/pull/2354/files#diff-a435cc738baec500b8799f7f58c1538e3bb06c772a208afc2615ff90ada3f4ca).
2. Reuse the asp.net [IFeatureCollection interface](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/IFeatureCollection.cs) and [implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs).
#### Roll our own
Advantages:
Creating our own IAgentFeatureCollection interface and implementation has the advantage of being more clearly associated with the agent framework and allows us to
improve on some of the design decisions made in asp.net core's IFeatureCollection.
Drawbacks:
It would mean a different implementation to maintain and test.
#### Reuse asp.net IFeatureCollection
Advantages:
Reusing the asp.net IFeatureCollection has the advantage of being able to reuse the well-established and tested implementation from asp.net
core. Users who are using agents in an asp.net core application may be able to pass feature collections from asp.net core to the agent framework directly.
Drawbacks:
While the package name is `Microsoft.Extensions.Features`, the namespaces of the types are `Microsoft.AspNetCore.Http.Features`, which may create confusion for users of agent framework who are not building web applications or services.
Users may rightly ask: Why do I need to use a class from asp.net core when I'm not building a web application / service?
The current design has some design issues that would be good to avoid. E.g. it does not distinguish between a feature being "not set" and "null". Get returns both as null and there is no tryget method.
Since the [default implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs) also supports value types, it throws for null values of value types.
A TryGet method would be more appropriate.
## Feature Layering
One possible scenario when adding support for feature collections is to allow layering of features by scope.
The following levels of scope could be supported:
1. Application - Application wide features that apply to all agents / chat clients
2. Artifact (Agent / ChatClient) - Features that apply to all runs of a specific agent or chat client instance
3. Action (GetNewThread / Run / GetResponse) - Feature that apply to a single action only
When retrieving a feature from the collection, the search would start from the most specific scope (Action) and progress to the least specific scope (Application), returning the first matching feature found.
Introducing layering adds some challenges:
- There may be multiple feature collections at the same scope level, e.g. an Agent that uses a ChatClient where both have their own feature collections.
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or only use the agent feature collection in the agent (Application -> Agent -> Run), and the chat client feature collection in the chat client (Application -> ChatClient -> Run)?
- The appropriate base feature collection may change when progressing down the stack, e.g. when an Agent calls a ChatClient, the action feature collection stays the same, but the artifact feature collection changes.
- Who creates the feature collection hierarchy?
- Since the hierarchy changes as it progresses down the execution stack, and the caller can only pass in the action level feature collection, the callee needs to combine it with its own artifact level feature collection and the application level feature collection. Each action will need to build the appropriate feature collection hierarchy, at the start of its execution.
- For Artifact level features, it seems odd to pass them in as a bag of untyped features, when we are constructing a known artifact type and therefore can have typed settings.
- E.g. today we have a strongly typed setting on ChatClientAgentOptions to configure a ChatMessageStore for the agent.
- To avoid global statics for application level features, the user would need to pass in the application level feature collection to each artifact that they create.
- This would be very odd if the user also already has to strongly typed settings for each feature that they want to set at the artifact level.
### Layering Options
1. No layering - only a single feature collection is supported per action (the caller can still create a layered collection if desired, but the callee does not do any layering automatically).
1. Fallback is to any features configured on the artifact via strongly typed settings.
1. Full layering - support layering at all levels (Application -> Artifact -> Action).
1. Only apply applicable artifact level features when calling into that artifact.
1. Apply upstream artifact features when calling into downstream artifacts, e.g. Feature hierarchy in ChatClientAgent would be `Application -> Agent -> Run` and in ChatClient would be `Application -> ChatClient -> Agent -> Run` or `Application -> Agent -> ChatClient -> Run`
1. The user needs to provide the application level feature collection to each artifact that they create and artifact features are passed via strongly typed settings.
### Accessing application level features Options
We need to consider how application level features would be accessed if supported.
1. The user provides the application level feature collection to each artifact that the user constructs
1. Passing the application level feature collection to each artifact is tedious for the user.
1. There is a static application level feature collection that can be accessed globally.
1. Statics create issues with testing and isolation.
## Decisions
- Feature Collections Container: Use AdditionalProperties
- Feature Layering: No layering - only a single collection/dictionary is supported per action. Application layers can be added later if needed.
+4 -6
View File
@@ -37,7 +37,7 @@
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
@@ -89,7 +89,6 @@
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -108,9 +107,9 @@
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
@@ -144,7 +143,6 @@
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Toolset -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
-12
View File
@@ -65,7 +65,6 @@
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GitHubCopilot/Agent_With_GitHubCopilot.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
@@ -230,7 +229,6 @@
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
@@ -301,11 +299,6 @@
<File Path="../docs/decisions/0007-agent-filtering-middleware.md" />
<File Path="../docs/decisions/0008-python-subpackages.md" />
<File Path="../docs/decisions/0009-support-long-running-operations.md" />
<File Path="../docs/decisions/0010-ag-ui-support.md" />
<File Path="../docs/decisions/0011-create-get-agent-api.md" />
<File Path="../docs/decisions/0012-python-typeddict-options.md" />
<File Path="../docs/decisions/0013-python-get-response-simplification.md" />
<File Path="../docs/decisions/0014-feature-collections.md" />
<File Path="../docs/decisions/adr-short-template.md" />
<File Path="../docs/decisions/adr-template.md" />
<File Path="../docs/decisions/README.md" />
@@ -397,7 +390,6 @@
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
@@ -417,7 +409,6 @@
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
</Folder>
<Folder Name="/Tests/" />
@@ -427,7 +418,6 @@
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
@@ -442,7 +432,6 @@
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
@@ -459,7 +448,6 @@
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
</Folder>
</Solution>
-1
View File
@@ -6,7 +6,6 @@
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260128.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260128.1</PackageVersion>
<GitTag>1.0.0-preview.260128.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260121.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260121.1</PackageVersion>
<GitTag>1.0.0-preview.260121.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -42,7 +42,7 @@ public static class Program
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentSession session = await hostAgent.Agent!.GetNewSessionAsync(cancellationToken);
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
try
{
while (true)
@@ -61,7 +61,7 @@ public static class Program
break;
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
var agentResponse = await hostAgent.Agent!.RunAsync(message, thread, cancellationToken: cancellationToken);
foreach (var chatMessage in agentResponse.Messages)
{
Console.ForegroundColor = ConsoleColor.Cyan;
@@ -88,7 +88,7 @@ public static class Program
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentSession session = await agent.GetNewSessionAsync(cancellationToken);
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
@@ -112,23 +112,23 @@ public static class Program
// Call RunStreamingAsync to get streaming updates
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
var updates = new List<ChatResponseUpdate>();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, cancellationToken: cancellationToken))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
{
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
updates.Add(chatUpdate);
if (chatUpdate.ConversationId != null)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
}
// Display run started information from the first update
if (isFirstUpdate && sessionId != null && update.ResponseId != null)
if (isFirstUpdate && threadId != null && update.ResponseId != null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Session: {sessionId}, Run: {update.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -177,7 +177,7 @@ public static class Program
var lastUpdate = updates[^1];
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine($"[Run Ended - Session: {sessionId}, Run: {lastUpdate.ResponseId}]");
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
Console.ResetColor();
}
messages.Clear();
@@ -19,21 +19,21 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track function calls that should trigger state events
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Process contents: track function calls and emit state events for results
List<AIContent> stateEventsToEmit = new();
@@ -20,21 +20,21 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track the last emitted document state to avoid duplicates
string? lastEmittedDocument = null;
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Check if we're seeing a write_document tool call and emit predictive state
bool hasToolCall = false;
@@ -19,21 +19,21 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -64,7 +64,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
@@ -98,7 +98,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -36,7 +36,7 @@ var pirateAgentBuilder = builder.AddAIAgent(
chatClientServiceKey: "chat-model")
.WithAITool(new CustomAITool())
.WithAITool(new CustomFunctionTool())
.WithInMemorySessionStore();
.WithInMemoryThreadStore();
var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
{
@@ -28,13 +28,13 @@ internal sealed class A2AAgentClient : AgentClientBase
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count);
var (a2aClient, _) = this.ResolveClient(agentName);
var contextId = sessionId ?? Guid.NewGuid().ToString("N");
var contextId = threadId ?? Guid.NewGuid().ToString("N");
// Convert and send messages via A2A without try-catch in yield method
var results = new List<AgentResponseUpdate>();
@@ -16,13 +16,13 @@ internal abstract class AgentClientBase
/// </summary>
/// <param name="agentName">The name of the agent to run.</param>
/// <param name="messages">The messages to send to the agent.</param>
/// <param name="sessionId">Optional session identifier for conversation continuity.</param>
/// <param name="threadId">Optional thread identifier for conversation continuity.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An asynchronous enumerable of agent response updates.</returns>
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
CancellationToken cancellationToken = default);
/// <summary>
@@ -34,3 +34,16 @@ internal abstract class AgentClientBase
public virtual Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
=> Task.FromResult<AgentCard?>(null);
}
/// <summary>
/// Helper class to create a thread-like wrapper for agent clients.
/// </summary>
public class AgentClientThread
{
public string ThreadId { get; }
public AgentClientThread(string? threadId = null)
{
this.ThreadId = threadId ?? Guid.NewGuid().ToString("N");
}
}
@@ -19,7 +19,7 @@ internal sealed class OpenAIChatCompletionsAgentClient(HttpClient httpClient) :
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
OpenAIClientOptions options = new()
@@ -18,7 +18,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
OpenAIClientOptions options = new()
@@ -30,7 +30,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var chatOptions = new ChatOptions()
{
ConversationId = sessionId
ConversationId = threadId
};
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, chatOptions, cancellationToken: cancellationToken))
@@ -19,15 +19,15 @@ public static class FunctionTriggers
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentSession writerSession = await writer.GetNewSessionAsync();
AgentThread writerThread = await writer.GetNewThreadAsync();
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: writerSession);
thread: writerThread);
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
session: writerSession);
thread: writerThread);
return refined.Result.Text;
}
@@ -21,7 +21,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
@@ -1,11 +1,11 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentSession` across multiple calls to maintain conversational context
- Using the same `AgentThread` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- HTTP API integration for starting and monitoring orchestrations
@@ -21,7 +21,7 @@ public static class FunctionTriggers
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -31,7 +31,7 @@ public static class FunctionTriggers
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: spamSession);
thread: spamThread);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
@@ -43,7 +43,7 @@ public static class FunctionTriggers
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -53,7 +53,7 @@ public static class FunctionTriggers
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: emailSession);
thread: emailThread);
EmailResponse emailResponse = emailAssistantResponse.Result;
@@ -24,7 +24,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -32,7 +32,7 @@ public static class FunctionTriggers
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -81,7 +81,7 @@ public static class FunctionTriggers
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -20,7 +20,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("Writer");
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -28,7 +28,7 @@ public static class FunctionTriggers
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -102,7 +102,7 @@ public static class FunctionTriggers
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -47,7 +47,7 @@ internal sealed class Tools(ILogger<Tools> logger)
{
this._logger.LogInformation("Getting status for workflow instance: {InstanceId}", instanceId);
// Get the current agent context using the session-static property
// Get the current agent context using the thread-static property
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
instanceId,
includeDetails);
@@ -94,15 +94,15 @@ public sealed class FunctionTriggers
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
// Create a new agent session
AgentSession session = await agentProxy.GetNewSessionAsync(cancellationToken);
string agentSessionId = session.GetService<AgentSessionId>().ToString();
// Create a new agent thread
AgentThread thread = await agentProxy.GetNewThreadAsync(cancellationToken);
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
// Run the agent in the background (fire-and-forget)
DurableAgentRunOptions options = new() { IsFireAndForget = true };
await agentProxy.RunAsync(prompt, session, options, cancellationToken);
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
this._logger.LogInformation("Agent run started for session: {AgentSessionId}", agentSessionId);
@@ -65,9 +65,9 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
}
// Get session ID from the current session context, which is only available in the context of
// Get session ID from the current thread context, which is only available in the context of
// a durable agent execution.
string agentSessionId = context.CurrentSession.GetService<AgentSessionId>().ToString();
string agentSessionId = context.CurrentThread.GetService<AgentSessionId>().ToString();
string streamKey = GetStreamKey(agentSessionId);
IDatabase db = this._redis.GetDatabase();
@@ -60,8 +60,8 @@ Console.ResetColor();
Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):");
Console.WriteLine();
// Create a session for the conversation
AgentSession session = await agentProxy.GetNewSessionAsync();
// Create a thread for the conversation
AgentThread thread = await agentProxy.GetNewThreadAsync();
while (true)
{
@@ -85,7 +85,7 @@ while (true)
{
AgentResponse agentResponse = await agentProxy.RunAsync(
message: input,
session: session,
thread: thread,
cancellationToken: CancellationToken.None);
Console.WriteLine(agentResponse.Text);
@@ -33,7 +33,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
@@ -47,15 +47,15 @@ AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstr
static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentSession writerSession = await writer.GetNewSessionAsync();
AgentThread writerThread = await writer.GetNewThreadAsync();
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: writerSession);
thread: writerThread);
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
session: writerSession);
thread: writerThread);
return refined.Result.Text;
}
@@ -1,11 +1,11 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentSession` across multiple calls to maintain conversational context
- Using the same `AgentThread` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- Waiting for orchestration completion using `WaitForInstanceCompletionAsync`
@@ -56,7 +56,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
{
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent(SpamDetectionAgentName);
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -66,7 +66,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: spamSession);
thread: spamThread);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
@@ -78,7 +78,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent(EmailAssistantAgentName);
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -88,7 +88,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: emailSession);
thread: emailThread);
EmailResponse emailResponse = emailAssistantResponse.Result;
@@ -48,7 +48,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
{
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -56,7 +56,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}' in less than 300 words.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -105,7 +105,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -59,7 +59,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
{
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent(WriterAgentName);
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -67,7 +67,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -141,7 +141,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -203,7 +203,7 @@ static async Task<object> GetWorkflowStatusAsync(
[Description("The instance ID of the workflow to check")] string instanceId,
[Description("Whether to include detailed information")] bool includeDetails = true)
{
// Get the current agent context using the session-static property
// Get the current agent context using the thread-static property
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
instanceId,
includeDetails);
@@ -298,8 +298,8 @@ Console.ResetColor();
Console.WriteLine("Enter a topic for the Publisher agent to write about (or 'exit' to quit):");
Console.WriteLine();
// Create a session for the conversation
AgentSession session = await agentProxy.GetNewSessionAsync();
// Create a thread for the conversation
AgentThread thread = await agentProxy.GetNewThreadAsync();
using CancellationTokenSource cts = new();
Console.CancelKeyPress += (sender, e) =>
@@ -330,7 +330,7 @@ while (!cts.Token.IsCancellationRequested)
{
AgentResponse agentResponse = await agentProxy.RunAsync(
message: input,
session: session,
thread: thread,
cancellationToken: cts.Token);
Console.WriteLine(agentResponse.Text);
@@ -304,9 +304,9 @@ if (string.IsNullOrWhiteSpace(prompt) || prompt.Equals("exit", StringComparison.
return;
}
// Create a new agent session
AgentSession session = await agentProxy.GetNewSessionAsync();
AgentSessionId sessionId = session.GetService<AgentSessionId>();
// Create a new agent thread
AgentThread thread = await agentProxy.GetNewThreadAsync();
AgentSessionId sessionId = thread.GetService<AgentSessionId>();
string conversationId = sessionId.ToString();
Console.ForegroundColor = ConsoleColor.Green;
@@ -316,7 +316,7 @@ Console.ResetColor();
// Run the agent in the background
DurableAgentRunOptions options = new() { IsFireAndForget = true };
await agentProxy.RunAsync(prompt, session, options, CancellationToken.None);
await agentProxy.RunAsync(prompt, thread, options, CancellationToken.None);
bool streamCompleted = false;
while (!streamCompleted)
@@ -61,12 +61,12 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
DurableAgentContext context = DurableAgentContext.Current
?? throw new InvalidOperationException("DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
// Get conversation ID from the current session context, which is only available in the context of
// Get conversation ID from the current thread context, which is only available in the context of
// a durable agent execution.
string conversationId = context.CurrentSession.GetService<AgentSessionId>().ToString();
string conversationId = context.CurrentThread.GetService<AgentSessionId>().ToString();
if (string.IsNullOrEmpty(conversationId))
{
throw new InvalidOperationException("Unable to determine conversation ID from the current session.");
throw new InvalidOperationException("Unable to determine conversation ID from the current thread.");
}
string streamKey = GetStreamKey(conversationId);
@@ -16,10 +16,10 @@ AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = agentCard.AsAIAgent();
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run with a long-running task.
AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session);
AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", thread);
// Poll until the response is complete.
while (response.ContinuationToken is { } token)
@@ -28,7 +28,7 @@ while (response.ContinuationToken is { } token)
await Task.Delay(TimeSpan.FromSeconds(2));
// Continue with the token.
response = await agent.RunAsync(session, options: new AgentRunOptions { ContinuationToken = token });
response = await agent.RunAsync(thread, options: new AgentRunOptions { ContinuationToken = token });
}
// Display the result
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
description: "AG-UI Client Agent");
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -49,18 +49,18 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -84,7 +84,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
description: "AG-UI Client Agent");
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -49,18 +49,18 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -116,7 +116,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -33,7 +33,7 @@ AIAgent agent = chatClient.AsAIAgent(
description: "AG-UI Client Agent",
tools: frontendTools);
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -62,18 +62,18 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -109,7 +109,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -27,7 +27,7 @@ JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default;
ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions);
List<ChatMessage> messages = [];
AgentSession? session = null;
AgentThread? thread = null;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Ask a question (or type 'exit' to quit):");
@@ -52,7 +52,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
approvalResponses.Clear();
List<AgentResponseUpdate> chatResponseUpdates = [];
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, cancellationToken: default))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: default))
{
chatResponseUpdates.Add(update);
foreach (AIContent content in update.Contents)
@@ -24,17 +24,17 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -43,7 +43,7 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
// Run the inner agent and intercept any approval requests
await foreach (var update in this.InnerAgent.RunStreamingAsync(
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
processedMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions);
}
@@ -24,17 +24,17 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -43,7 +43,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
// Run the inner agent and intercept any approval requests
await foreach (var update in this.InnerAgent.RunStreamingAsync(
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
processedMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions);
}
@@ -30,7 +30,7 @@ JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
};
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful recipe assistant.")
@@ -65,21 +65,21 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
bool stateReceived = false;
Console.WriteLine();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
{
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Run Started - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"[Run Started - Thread: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -113,7 +113,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
// Display final state if received
@@ -37,18 +37,18 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
/// <inheritdoc />
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
/// <inheritdoc />
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -64,7 +64,7 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
messagesWithState.Add(stateMessage);
// Stream the response and update state when received
await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, session, options, cancellationToken))
await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, thread, options, cancellationToken))
{
// Check if this update contains a state snapshot
foreach (AIContent content in update.Contents)
@@ -19,17 +19,17 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
@@ -40,7 +40,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
state.ValueKind != JsonValueKind.Object)
{
// No state management requested, pass through to inner agent
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -58,7 +58,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
if (!hasProperties)
{
// Empty state - treat as no state
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -92,7 +92,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
// Collect all updates from first run
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
@@ -129,7 +129,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -128,7 +128,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var session = await agent.GetNewSessionAsync();
var thread = await agent.GetNewThreadAsync();
appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
@@ -176,7 +176,7 @@ using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = ses
Console.Write("Agent: ");
// Run the agent (this will create its own internal telemetry spans)
await foreach (var update in agent.RunStreamingAsync(userInput, session))
await foreach (var update in agent.RunStreamingAsync(userInput, thread))
{
Console.Write(update.Text);
}
@@ -31,8 +31,8 @@ AIAgent agent2 = await persistentAgentsClient.CreateAIAgentAsync(
instructions: JokerInstructions);
// You can then invoke the agent like any other AIAgent.
AgentSession session = await agent1.GetNewSessionAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
AgentThread thread = await agent1.GetNewThreadAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", thread));
// Cleanup for sample purposes.
await persistentAgentsClient.Administration.DeleteAgentAsync(agent1.Id);
@@ -40,11 +40,11 @@ var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
// Once you have the AIAgent, you can invoke it like any other AIAgent.
AgentSession session = await jokerAgentLatest.GetNewSessionAsync();
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", session));
AgentThread thread = await jokerAgentLatest.GetNewThreadAsync();
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", thread));
// This will use the same session to continue the conversation.
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", session));
// This will use the same thread to continue the conversation.
Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", thread));
// Cleanup by agent name removes both agent versions created.
aiProjectClient.Agents.DeleteAgent(existingJokerAgent.Name);
@@ -28,35 +28,35 @@ namespace SampleApp
{
public override string? Name => "UpperCaseParrotAgent";
public override ValueTask<AgentSession> GetNewSessionAsync(CancellationToken cancellationToken = default)
=> new(new CustomAgentSession());
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> new(new CustomAgentThread());
public override ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CustomAgentSession(serializedSession, jsonSerializerOptions));
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CustomAgentThread(serializedThread, jsonSerializerOptions));
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
// Create a session if the user didn't supply one.
session ??= await this.GetNewSessionAsync(cancellationToken);
// Create a thread if the user didn't supply one.
thread ??= await this.GetNewThreadAsync(cancellationToken);
if (session is not CustomAgentSession typedSession)
if (thread is not CustomAgentThread typedThread)
{
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
}
// Get existing messages from the store
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
var storeMessages = await typedSession.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
// Notify the thread of the input and output messages.
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
{
ResponseMessages = responseMessages
};
await typedSession.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
return new AgentResponse
{
@@ -66,29 +66,29 @@ namespace SampleApp
};
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create a session if the user didn't supply one.
session ??= await this.GetNewSessionAsync(cancellationToken);
// Create a thread if the user didn't supply one.
thread ??= await this.GetNewThreadAsync(cancellationToken);
if (session is not CustomAgentSession typedSession)
if (thread is not CustomAgentThread typedThread)
{
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
}
// Get existing messages from the store
var invokingContext = new ChatHistoryProvider.InvokingContext(messages);
var storeMessages = await typedSession.ChatHistoryProvider.InvokingAsync(invokingContext, cancellationToken);
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the session of the input and output messages.
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, storeMessages)
// Notify the thread of the input and output messages.
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
{
ResponseMessages = responseMessages
};
await typedSession.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
foreach (var message in responseMessages)
{
@@ -128,14 +128,14 @@ namespace SampleApp
});
/// <summary>
/// A session type for our custom agent that only supports in memory storage of messages.
/// A thread type for our custom agent that only supports in memory storage of messages.
/// </summary>
internal sealed class CustomAgentSession : InMemoryAgentSession
internal sealed class CustomAgentThread : InMemoryAgentThread
{
internal CustomAgentSession() { }
internal CustomAgentThread() { }
internal CustomAgentSession(JsonElement serializedSessionState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedSessionState, jsonSerializerOptions) { }
internal CustomAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
}
}
}
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.GitHub.Copilot\Microsoft.Agents.AI.GitHub.Copilot.csproj" />
</ItemGroup>
</Project>
@@ -1,51 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create a GitHub Copilot agent with shell command permissions.
using GitHub.Copilot.SDK;
using Microsoft.Agents.AI;
// Permission handler that prompts the user for approval
static Task<PermissionRequestResult> PromptPermission(PermissionRequest request, PermissionInvocation invocation)
{
Console.WriteLine($"\n[Permission Request: {request.Kind}]");
Console.Write("Approve? (y/n): ");
string? input = Console.ReadLine()?.Trim().ToUpperInvariant();
string kind = input is "Y" or "YES" ? "approved" : "denied-interactively-by-user";
return Task.FromResult(new PermissionRequestResult { Kind = kind });
}
// Create and start a Copilot client
await using CopilotClient copilotClient = new();
await copilotClient.StartAsync();
// Create an agent with a session config that enables permission handling
SessionConfig sessionConfig = new()
{
OnPermissionRequest = PromptPermission,
};
AIAgent agent = copilotClient.AsAIAgent(sessionConfig, ownsClient: true);
// Toggle between streaming and non-streaming modes
bool useStreaming = true;
string prompt = "List all files in the current directory";
Console.WriteLine($"User: {prompt}\n");
if (useStreaming)
{
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(prompt))
{
Console.Write(update);
}
Console.WriteLine();
}
else
{
AgentResponse response = await agent.RunAsync(prompt);
Console.WriteLine(response);
}
@@ -1,76 +0,0 @@
# Prerequisites
> **⚠️ WARNING: Container Recommendation**
>
> GitHub Copilot can execute tools and commands that may interact with your system. For safety, it is strongly recommended to run this sample in a containerized environment (e.g., Docker, Dev Container) to avoid unintended consequences to your machine.
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- GitHub Copilot CLI installed and available in your PATH (or provide a custom path)
## Setting up GitHub Copilot CLI
To use this sample, you need to have the GitHub Copilot CLI installed. You can install it by following the instructions at:
https://github.com/github/copilot-sdk
Once installed, ensure the `copilot` command is available in your PATH, or configure a custom path using `CopilotClientOptions`.
## Running the Sample
No additional environment variables are required if using default configuration. The sample will:
1. Create a GitHub Copilot client with default options
2. Create an AI agent using the Copilot SDK
3. Send a message to the agent
4. Display the response
Run the sample:
```powershell
dotnet run
```
## Advanced Usage
You can customize the agent by providing additional configuration:
```csharp
using GitHub.Copilot.SDK;
using Microsoft.Agents.AI;
// Create and start a Copilot client
await using CopilotClient copilotClient = new();
await copilotClient.StartAsync();
// Create session configuration with specific model
SessionConfig sessionConfig = new()
{
Model = "claude-opus-4.5",
Streaming = false
};
// Create an agent with custom configuration using the extension method
AIAgent agent = copilotClient.AsAIAgent(
sessionConfig,
ownsClient: true,
id: "my-copilot-agent",
name: "My Copilot Assistant",
description: "A helpful AI assistant powered by GitHub Copilot"
);
// Use the agent - ask it to write code for us
AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application");
Console.WriteLine(response);
```
## Streaming Responses
To get streaming responses:
```csharp
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a C# function to calculate Fibonacci numbers"))
{
Console.Write(update.Text);
}
```
@@ -33,8 +33,8 @@ AIAgent agent2 = await assistantClient.CreateAIAgentAsync(
instructions: JokerInstructions);
// You can invoke the agent like any other AIAgent.
AgentSession session = await agent1.GetNewSessionAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", session));
AgentThread thread = await agent1.GetNewThreadAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", thread));
// Cleanup for sample purposes.
await assistantClient.DeleteAssistantAsync(agent1.Id);
@@ -22,7 +22,6 @@ See the README.md for each sample for the prerequisites for that sample.
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
|[Creating an AIAgent with Azure OpenAI Responses](./Agent_With_AzureOpenAIResponses/)|This sample demonstrates how to create an AIAgent using Azure OpenAI Responses as the underlying inference service|
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GitHubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|[Creating an AIAgent with OpenAI Assistants](./Agent_With_OpenAIAssistants/)|This sample demonstrates how to create an AIAgent using OpenAI Assistants as the underlying inference service.</br>WARNING: The Assistants API is deprecated and will be shut down. For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration|
@@ -26,12 +26,12 @@ AIAgent agent = new AnthropicClient { APIKey = apiKey }
.AsAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
session = await agent.GetNewSessionAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", session))
thread = await agent.GetNewThreadAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
}
@@ -39,22 +39,22 @@ AIAgent agent = new AzureOpenAIClient(
collectionName: "chathistory",
vectorDimensions: 3072,
// Configure the scope values under which chat messages will be stored.
// In this case, we are using a fixed user ID and a unique session ID for each new session.
storageScope: new() { UserId = "UID1", SessionId = Guid.NewGuid().ToString() },
// In this case, we are using a fixed user ID and a unique thread ID for each new thread.
storageScope: new() { UserId = "UID1", ThreadId = new Guid().ToString() },
// Configure the scope which would be used to search for relevant prior messages.
// In this case, we are searching for any messages for the user across all sessions.
// In this case, we are searching for any messages for the user across all threads.
searchScope: new() { UserId = "UID1" }))
});
// Start a new session for the agent conversation.
AgentSession session = await agent.GetNewSessionAsync();
// Start a new thread for the agent conversation.
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with the session that stores conversation history in the vector store.
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session));
// Run the agent with the thread that stores conversation history in the vector store.
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", thread));
// Start a second session. Since we configured the search scope to be across all sessions for the user,
// Start a second thread. Since we configured the search scope to be across all threads for the user,
// the agent should remember that the user likes pirate jokes.
AgentSession? session2 = await agent.GetNewSessionAsync();
AgentThread thread2 = await agent.GetNewThreadAsync();
// Run the agent with the second session.
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2));
// Run the agent with the second thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", thread2));
@@ -2,7 +2,7 @@
// This sample shows how to use the Mem0Provider to persist and recall memories for an agent.
// The sample stores conversation messages in a Mem0 service and retrieves relevant memories
// for subsequent invocations, even across new sessions.
// for subsequent invocations, even across new threads.
using System.Net.Http.Headers;
using System.Text.Json;
@@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
// If each session should have its own Mem0 scope, you can create a new id per session here:
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
// In this case we are storing memories scoped by application and user instead so that memories are retained across threads.
? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" })
@@ -40,25 +40,25 @@ AIAgent agent = new AzureOpenAIClient(
: new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions))
});
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
// Clear any existing memories for this scope to demonstrate fresh behavior.
Mem0Provider mem0Provider = session.GetService<Mem0Provider>()!;
Mem0Provider mem0Provider = thread.GetService<Mem0Provider>()!;
await mem0Provider.ClearStoredMemoriesAsync();
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", thread));
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", thread));
Console.WriteLine("\nWaiting briefly for Mem0 to index the new memories...\n");
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", thread));
Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
JsonElement serializedSession = session.Serialize();
AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
Console.WriteLine("\n>> Serialize and deserialize the thread to demonstrate persisted state\n");
JsonElement serializedThread = thread.Serialize();
AgentThread restoredThread = await agent.DeserializeThreadAsync(serializedThread);
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredThread));
Console.WriteLine("\n>> Start a new session that shares the same Mem0 scope\n");
AgentSession newSession = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
Console.WriteLine("\n>> Start a new thread that shares the same Mem0 scope\n");
AgentThread newThread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newThread));
@@ -24,10 +24,10 @@ ChatClient chatClient = new AzureOpenAIClient(
.GetChatClient(deploymentName);
// Create the agent and provide a factory to add our custom memory component to
// all sessions created by the agent. Here each new memory component will have its own
// user info object, so each session will have its own memory.
// all threads created by the agent. Here each new memory component will have its own
// user info object, so each thread will have its own memory.
// In real world applications/services, where the user info would be persisted in a database,
// and preferably shared between multiple sessions used by the same user, ensure that the
// and preferably shared between multiple threads used by the same user, ensure that the
// factory reads the user id from the current context and scopes the memory component
// and its storage to that user id.
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
@@ -36,47 +36,47 @@ AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions))
});
// Create a new session for the conversation.
AgentSession session = await agent.GetNewSessionAsync();
// Create a new thread for the conversation.
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Use session with blank memory\n");
Console.WriteLine(">> Use thread with blank memory\n");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", session));
Console.WriteLine(await agent.RunAsync("My name is RuaidhrĂ­", session));
Console.WriteLine(await agent.RunAsync("I am 20 years old", session));
Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
Console.WriteLine(await agent.RunAsync("My name is RuaidhrĂ­", thread));
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));
// We can serialize the session. The serialized state will include the state of the memory component.
var sesionElement = session.Serialize();
// We can serialize the thread. The serialized state will include the state of the memory component.
var threadElement = thread.Serialize();
Console.WriteLine("\n>> Use deserialized session with previously created memories\n");
Console.WriteLine("\n>> Use deserialized thread with previously created memories\n");
// Later we can deserialize the session and continue the conversation with the previous memory component state.
var deserializedSession = await agent.DeserializeSessionAsync(sesionElement);
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedSession));
// Later we can deserialize the thread and continue the conversation with the previous memory component state.
var deserializedThread = await agent.DeserializeThreadAsync(threadElement);
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedThread));
Console.WriteLine("\n>> Read memories from memory component\n");
// It's possible to access the memory component via the session's GetService method.
var userInfo = deserializedSession.GetService<UserInfoMemory>()?.UserInfo;
// It's possible to access the memory component via the thread's GetService method.
var userInfo = deserializedThread.GetService<UserInfoMemory>()?.UserInfo;
// Output the user info that was captured by the memory component.
Console.WriteLine($"MEMORY - User Name: {userInfo?.UserName}");
Console.WriteLine($"MEMORY - User Age: {userInfo?.UserAge}");
Console.WriteLine("\n>> Use new session with previously created memories\n");
Console.WriteLine("\n>> Use new thread with previously created memories\n");
// It is also possible to set the memories in a memory component on an individual session.
// This is useful if we want to start a new session, but have it share the same memories as a previous session.
var newSession = await agent.GetNewSessionAsync();
if (userInfo is not null && newSession.GetService<UserInfoMemory>() is UserInfoMemory newSessionMemory)
// It is also possible to set the memories in a memory component on an individual thread.
// This is useful if we want to start a new thread, but have it share the same memories as a previous thread.
var newThread = await agent.GetNewThreadAsync();
if (userInfo is not null && newThread.GetService<UserInfoMemory>() is UserInfoMemory newThreadMemory)
{
newSessionMemory.UserInfo = userInfo;
newThreadMemory.UserInfo = userInfo;
}
// Invoke the agent and output the text result.
// This time the agent should remember the user's name and use it in the response.
Console.WriteLine(await agent.RunAsync("What is my name and age?", newSession));
Console.WriteLine(await agent.RunAsync("What is my name and age?", newThread));
namespace SampleApp
{
@@ -52,17 +52,17 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="session">The conversation session to continue with this invocation. If not provided, creates a new session. The session will be mutated with the provided messages and agent response.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatCompletion"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async Task<ChatCompletion> RunAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var response = await this.RunAsync(messages.AsChatMessages(), session, options, cancellationToken).ConfigureAwait(false);
var response = await this.RunAsync(messages.AsChatMessages(), thread, options, cancellationToken).ConfigureAwait(false);
return response.AsOpenAIChatCompletion();
}
@@ -71,26 +71,26 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
/// Run the agent streaming with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="session">The conversation session to continue with this invocation. If not provided, creates a new session. The session will be mutated with the provided messages and agent response.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatCompletion"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual IAsyncEnumerable<StreamingChatCompletionUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var response = this.RunStreamingAsync(messages.AsChatMessages(), session, options, cancellationToken);
var response = this.RunStreamingAsync(messages.AsChatMessages(), thread, options, cancellationToken);
return response.AsChatResponseUpdatesAsync().AsOpenAIStreamingChatCompletionUpdatesAsync(cancellationToken);
}
/// <inheritdoc/>
protected sealed override Task<AgentResponse> RunCoreAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, session, options, cancellationToken);
protected sealed override Task<AgentResponse> RunCoreAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, thread, options, cancellationToken);
/// <inheritdoc/>
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, session, options, cancellationToken);
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
@@ -52,17 +52,17 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="session">The conversation session to continue with this invocation. If not provided, creates a new session. The session will be mutated with the provided messages and agent response.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async Task<ResponseResult> RunAsync(
IEnumerable<ResponseItem> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var response = await this.RunAsync(messages.AsChatMessages(), session, options, cancellationToken).ConfigureAwait(false);
var response = await this.RunAsync(messages.AsChatMessages(), thread, options, cancellationToken).ConfigureAwait(false);
return response.AsOpenAIResponse();
}
@@ -71,17 +71,17 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// Run the agent streaming with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="session">The conversation session to continue with this invocation. If not provided, creates a new session. The session will be mutated with the provided messages and agent response.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async IAsyncEnumerable<StreamingResponseUpdate> RunStreamingAsync(
IEnumerable<ResponseItem> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var response = this.RunStreamingAsync(messages.AsChatMessages(), session, options, cancellationToken);
var response = this.RunStreamingAsync(messages.AsChatMessages(), thread, options, cancellationToken);
await foreach (var update in response.ConfigureAwait(false))
{
@@ -105,10 +105,10 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
}
/// <inheritdoc/>
protected sealed override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, session, options, cancellationToken);
protected sealed override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, thread, options, cancellationToken);
/// <inheritdoc/>
protected sealed override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, session, options, cancellationToken);
protected sealed override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to maintain conversation state using the OpenAIResponseClientAgent
// and AgentSession. By passing the same session to multiple agent invocations, the agent
// 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.
@@ -29,8 +29,8 @@ ClientResult createConversationResult = await conversationClient.CreateConversat
using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createConversationResult.GetRawResponse().Content.ToString());
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
// Create a session for the conversation - this enables conversation state management for subsequent turns
AgentSession session = await agent.GetNewSessionAsync(conversationId);
// Create a thread for the conversation - this enables conversation state management for subsequent turns
AgentThread thread = await agent.GetNewThreadAsync(conversationId);
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
@@ -38,22 +38,22 @@ Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
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 'session' and used in subsequent calls
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], session);
// 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], session);
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], session);
ChatCompletion thirdResponse = await agent.RunAsync([thirdMessage], thread);
Console.WriteLine($"Assistant: {thirdResponse.Content.Last().Text}\n");
Console.WriteLine("=== End of Conversation ===");
@@ -4,7 +4,7 @@ This sample demonstrates how to maintain conversation state across multiple turn
## What This Sample Shows
- **Conversation State Management**: Shows how to use `ConversationClient` and `AgentSession` to maintain conversation context across multiple agent invocations
- **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
@@ -24,22 +24,22 @@ ConversationClient conversationClient = openAIClient.GetConversationClient();
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
```
### AgentSession for Conversation State
### AgentThread for Conversation State
The `AgentSession` works with `ChatClientAgentRunOptions` to link the agent to a server-side conversation:
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 session for the conversation
AgentSession session = await agent.GetNewSessionAsync();
// Create a thread for the conversation
AgentThread thread = await agent.GetNewThreadAsync();
// First call links the session to the conversation
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], session, agentRunOptions);
// First call links the thread to the conversation
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions);
// Subsequent calls use the session without needing to pass options again
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], session);
// Subsequent calls use the thread without needing to pass options again
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
```
### Retrieving Conversation History
@@ -59,9 +59,9 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
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 Session**: Call `agent.GetNewSessionAsync()` to create a new conversation session
5. **Link Session to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the session - context is maintained
4. **Create a Thread**: Call `agent.GetNewThreadAsync()` 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
@@ -14,4 +14,4 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|[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.|
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentSession for context continuity.|
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentThread for context continuity.|
@@ -66,20 +66,20 @@ AIAgent agent = azureOpenAIClient
// Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(new InMemoryChatHistoryProvider(ctx.SerializedState, ctx.JsonSerializerOptions)
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
.WithAIContextProviderMessageRemoval()),
});
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
Console.WriteLine("\n>> Asking about shipping\n");
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", session));
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
Console.WriteLine("\n>> Asking about product care\n");
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", session));
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
// Produces some sample search documents.
// Each one contains a source name and link, which the agent can use to cite sources in its responses.
@@ -74,22 +74,22 @@ AIAgent agent = azureOpenAIClient
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
});
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about SK sessions\n");
Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread/session in Semantic Kernel?", session));
Console.WriteLine(">> Asking about SK threads\n");
Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread in Semantic Kernel?", thread));
// Here we are asking a very vague question when taken out of context,
// but since we are including previous messages in our search using RecentMessageMemoryLimit
// the RAG search should still produce useful results.
Console.WriteLine("\n>> Asking about AF sessions\n");
Console.WriteLine(await agent.RunAsync("and in Agent Framework?", session));
Console.WriteLine("\n>> Asking about AF threads\n");
Console.WriteLine(await agent.RunAsync("and in Agent Framework?", thread));
Console.WriteLine("\n>> Contrasting Approaches\n");
Console.WriteLine(await agent.RunAsync("Please contrast the two approaches", session));
Console.WriteLine(await agent.RunAsync("Please contrast the two approaches", thread));
Console.WriteLine("\n>> Asking about ancestry\n");
Console.WriteLine(await agent.RunAsync("What are the predecessors to the Agent Framework?", session));
Console.WriteLine(await agent.RunAsync("What are the predecessors to the Agent Framework?", thread));
static async Task UploadDataFromMarkdown(string markdownUrl, string sourceName, VectorStoreCollection<Guid, DocumentationChunk> vectorStoreCollection, int chunkSize, int overlap)
{
@@ -32,16 +32,16 @@ AIAgent agent = new AzureOpenAIClient(
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
});
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
Console.WriteLine("\n>> Asking about shipping\n");
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", session));
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
Console.WriteLine("\n>> Asking about product care\n");
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", session));
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
static Task<IEnumerable<TextSearchProvider.TextSearchResult>> MockSearchAsync(string query, CancellationToken cancellationToken)
{
@@ -43,16 +43,16 @@ AIAgent agent = await aiProjectClient
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
tools: [fileSearchTool]);
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", session));
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
Console.WriteLine("\n>> Asking about shipping\n");
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", session));
Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
Console.WriteLine("\n>> Asking about product care\n");
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", session));
Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
// Cleanup
await fileClient.DeleteFileAsync(uploadResult.Value.Id);
@@ -16,18 +16,18 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent with a multi-turn conversation, where the context is preserved in the session object.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread));
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the session object.
session = await agent.GetNewSessionAsync();
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session))
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the thread object.
thread = await agent.GetNewThreadAsync();
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
}
await foreach (var update in agent.RunStreamingAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session))
await foreach (var update in agent.RunStreamingAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread))
{
Console.WriteLine(update);
}
@@ -30,12 +30,12 @@ AIAgent agent = new AzureOpenAIClient(
.AsAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
// Call the agent and check if there are any user input requests to handle.
AgentSession session = await agent.GetNewSessionAsync();
var response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
AgentThread thread = await agent.GetNewThreadAsync();
var response = await agent.RunAsync("What is the weather like in Amsterdam?", thread);
var userInputRequests = response.UserInputRequests.ToList();
// For streaming use:
// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", session).ToListAsync();
// var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread).ToListAsync();
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
while (userInputRequests.Count > 0)
@@ -52,12 +52,12 @@ while (userInputRequests.Count > 0)
.ToList();
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputResponses, session);
response = await agent.RunAsync(userInputResponses, thread);
userInputRequests = response.UserInputRequests.ToList();
// For streaming use:
// updates = await agent.RunStreamingAsync(userInputResponses, session).ToListAsync();
// updates = await agent.RunStreamingAsync(userInputResponses, thread).ToListAsync();
// userInputRequests = updates.SelectMany(x => x.UserInputRequests).ToList();
}
@@ -18,24 +18,24 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Start a new session for the agent conversation.
AgentSession session = await agent.GetNewSessionAsync();
// Start a new thread for the agent conversation.
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with a new session.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Run the agent with a new thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the session state to a JsonElement, so it can be stored for later use.
JsonElement serializedSession = session.Serialize();
// Serialize the thread state to a JsonElement, so it can be stored for later use.
JsonElement serializedThread = thread.Serialize();
// Save the serialized session to a temporary file (for demonstration purposes).
// Save the serialized thread to a temporary file (for demonstration purposes).
string tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedSession));
await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedThread));
// Load the serialized session from the temporary file (for demonstration purposes).
JsonElement reloadedSerializedSession = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath));
// Load the serialized thread from the temporary file (for demonstration purposes).
JsonElement reloadedSerializedThread = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath));
// Deserialize the session state after loading from storage.
AgentSession resumedSession = await agent.DeserializeSessionAsync(reloadedSerializedSession);
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread);
// Run the agent again with the resumed session.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedSession));
// Run the agent again with the resumed thread.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -31,61 +31,61 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(
// Create a new ChatHistoryProvider for this agent that stores chat history in a vector store.
// Each session must get its own copy of the VectorChatHistoryProvider, since the provider
// also contains the id that the chat history is stored under.
new VectorChatHistoryProvider(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
});
// Start a new session for the agent conversation.
AgentSession session = await agent.GetNewSessionAsync();
// Start a new thread for the agent conversation.
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with the session that stores chat history in the vector store.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Run the agent with the thread that stores conversation history in the vector store.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the session state, so it can be stored for later use.
// Since the chat history is stored in the vector store, the serialized session
// Serialize the thread state, so it can be stored for later use.
// Since the chat history is stored in the vector store, the serialized thread
// only contains the guid that the messages are stored under in the vector store.
JsonElement serializedSession = session.Serialize();
JsonElement serializedThread = thread.Serialize();
Console.WriteLine("\n--- Serialized session ---\n");
Console.WriteLine(JsonSerializer.Serialize(serializedSession, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine("\n--- Serialized thread ---\n");
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
// The serialized session can now be saved to a database, file, or any other storage mechanism
// The serialized thread can now be saved to a database, file, or any other storage mechanism
// and loaded again later.
// Deserialize the session state after loading from storage.
AgentSession resumedSession = await agent.DeserializeSessionAsync(serializedSession);
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
// Run the agent with the session that stores chat history in the vector store a second time.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedSession));
// Run the agent with the thread that stores conversation history in the vector store a second time.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
// We can access the VectorChatHistoryProvider via the session's GetService method if we need to read the key under which chat history is stored.
var chatHistoryProvider = resumedSession.GetService<VectorChatHistoryProvider>()!;
Console.WriteLine($"\nSession is stored in vector store under key: {chatHistoryProvider.SessionDbKey}");
// We can access the VectorChatMessageStore via the thread's GetService method if we need to read the key under which threads are stored.
var messageStore = resumedThread.GetService<VectorChatMessageStore>()!;
Console.WriteLine($"\nThread is stored in vector store under key: {messageStore.ThreadDbKey}");
namespace SampleApp
{
/// <summary>
/// A sample implementation of <see cref="ChatHistoryProvider"/> that stores chat history in a vector store.
/// A sample implementation of <see cref="ChatMessageStore"/> that stores chat messages in a vector store.
/// </summary>
internal sealed class VectorChatHistoryProvider : ChatHistoryProvider
internal sealed class VectorChatMessageStore : ChatMessageStore
{
private readonly VectorStore _vectorStore;
public VectorChatHistoryProvider(VectorStore vectorStore, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
public VectorChatMessageStore(VectorStore vectorStore, JsonElement serializedStoreState, JsonSerializerOptions? jsonSerializerOptions = null)
{
this._vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
if (serializedState.ValueKind is JsonValueKind.String)
if (serializedStoreState.ValueKind is JsonValueKind.String)
{
// Here we can deserialize the session id so that we can access the same messages as before the suspension.
this.SessionDbKey = serializedState.Deserialize<string>();
// Here we can deserialize the thread id so that we can access the same messages as before the suspension.
this.ThreadDbKey = serializedStoreState.Deserialize<string>();
}
}
public string? SessionDbKey { get; private set; }
public string? ThreadDbKey { get; private set; }
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
@@ -94,7 +94,7 @@ namespace SampleApp
var records = await collection
.GetAsync(
x => x.SessionId == this.SessionDbKey, 10,
x => x.ThreadId == this.ThreadDbKey, 10,
new() { OrderBy = x => x.Descending(y => y.Timestamp) },
cancellationToken)
.ToListAsync(cancellationToken);
@@ -113,7 +113,7 @@ namespace SampleApp
return;
}
this.SessionDbKey ??= Guid.NewGuid().ToString("N");
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
@@ -124,17 +124,17 @@ namespace SampleApp
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{
Key = this.SessionDbKey + x.MessageId,
Key = this.ThreadDbKey + x.MessageId,
Timestamp = DateTimeOffset.UtcNow,
SessionId = this.SessionDbKey,
ThreadId = this.ThreadDbKey,
SerializedMessage = JsonSerializer.Serialize(x),
MessageText = x.Text
}), cancellationToken);
}
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
// We have to serialize the session id, so that on deserialization we can retrieve the messages using the same session id.
JsonSerializer.SerializeToElement(this.SessionDbKey);
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
JsonSerializer.SerializeToElement(this.ThreadDbKey);
/// <summary>
/// The data structure used to store chat history items in the vector store.
@@ -145,7 +145,7 @@ namespace SampleApp
public string? Key { get; set; }
[VectorStoreData]
public string? SessionId { get; set; }
public string? ThreadId { get; set; }
[VectorStoreData]
public DateTimeOffset? Timestamp { get; set; }
@@ -44,12 +44,12 @@ await host.RunAsync().ConfigureAwait(false);
/// </summary>
internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appLifetime) : IHostedService
{
private AgentSession? _session;
private AgentThread? _thread;
public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a session that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._session = await agent.GetNewSessionAsync(cancellationToken);
// Create a thread that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._thread = await agent.GetNewThreadAsync(cancellationToken);
_ = this.RunAsync(appLifetime.ApplicationStopping);
}
@@ -72,7 +72,7 @@ internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appL
}
// Stream the output to the console as it is generated.
await foreach (var update in agent.RunStreamingAsync(input, this._session, cancellationToken: cancellationToken))
await foreach (var update in agent.RunStreamingAsync(input, this._thread, cancellationToken: cancellationToken))
{
Console.Write(update);
}
@@ -22,9 +22,9 @@ ChatMessage message = new(ChatRole.User, [
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
]);
var session = await agent.GetNewSessionAsync();
var thread = await agent.GetNewThreadAsync();
await foreach (var update in agent.RunStreamingAsync(message, session))
await foreach (var update in agent.RunStreamingAsync(message, thread))
{
Console.WriteLine(update);
}
@@ -32,41 +32,41 @@ AIAgent agent = new AzureOpenAIClient(
// Enable background responses (only supported by {Azure}OpenAI Responses at this time).
AgentRunOptions options = new() { AllowBackgroundResponses = true };
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run.
AgentResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", session, options);
AgentResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", thread, options);
// Poll for background responses until complete.
while (response.ContinuationToken is not null)
{
PersistAgentState(session, response.ContinuationToken);
PersistAgentState(thread, response.ContinuationToken);
await Task.Delay(TimeSpan.FromSeconds(10));
var (restoredSession, continuationToken) = await RestoreAgentState(agent);
var (restoredThread, continuationToken) = await RestoreAgentState(agent);
options.ContinuationToken = continuationToken;
response = await agent.RunAsync(restoredSession, options);
response = await agent.RunAsync(restoredThread, options);
}
Console.WriteLine(response.Text);
void PersistAgentState(AgentSession? session, ResponseContinuationToken? continuationToken)
void PersistAgentState(AgentThread thread, ResponseContinuationToken? continuationToken)
{
stateStore["session"] = session!.Serialize();
stateStore["thread"] = thread.Serialize();
stateStore["continuationToken"] = JsonSerializer.SerializeToElement(continuationToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
async Task<(AgentSession Session, ResponseContinuationToken? ContinuationToken)> RestoreAgentState(AIAgent agent)
async Task<(AgentThread Thread, ResponseContinuationToken? ContinuationToken)> RestoreAgentState(AIAgent agent)
{
JsonElement serializedSession = stateStore["session"] ?? throw new InvalidOperationException("No serialized session found in state store.");
JsonElement serializedThread = stateStore["thread"] ?? throw new InvalidOperationException("No serialized thread found in state store.");
JsonElement? serializedToken = stateStore["continuationToken"];
AgentSession session = await agent.DeserializeSessionAsync(serializedSession);
AgentThread thread = await agent.DeserializeThreadAsync(serializedThread);
ResponseContinuationToken? continuationToken = (ResponseContinuationToken?)serializedToken?.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
return (session, continuationToken);
return (thread, continuationToken);
}
[Description("Researches relevant space facts and scientific information for writing a science fiction novel")]
@@ -45,7 +45,7 @@ var middlewareEnabledAgent = originalAgent
.Use(GuardrailMiddleware, null)
.Build();
var session = await middlewareEnabledAgent.GetNewSessionAsync();
var thread = await middlewareEnabledAgent.GetNewThreadAsync();
Console.WriteLine("\n\n=== Example 1: Wording Guardrail ===");
var guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful.");
@@ -65,7 +65,7 @@ var options = new ChatClientAgentRunOptions(new()
Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))]
});
var functionCallResponse = await middlewareEnabledAgent.RunAsync("What's the current time and the weather in Seattle?", session, options);
var functionCallResponse = await middlewareEnabledAgent.RunAsync("What's the current time and the weather in Seattle?", thread, options);
Console.WriteLine($"Function calling response: {functionCallResponse}");
// Special per-request middleware agent.
@@ -89,7 +89,7 @@ var response = await originalAgent // Using per-request middleware pipeline with
.Use(PerRequestFunctionCallingMiddleware)
.Use(ConsolePromptingApprovalMiddleware, null)
.Build()
.RunAsync("What's the current time and the weather in Seattle?", session, optionsWithApproval);
.RunAsync("What's the current time and the weather in Seattle?", thread, optionsWithApproval);
Console.WriteLine($"Per-request middleware response: {response}");
@@ -131,13 +131,13 @@ async ValueTask<object?> PerRequestFunctionCallingMiddleware(AIAgent agent, Func
}
// This middleware redacts PII information from input and output messages.
async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact PII information from input messages
var filteredMessages = FilterMessages(messages);
Console.WriteLine("Pii Middleware - Filtered Messages Pre-Run");
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken).ConfigureAwait(false);
var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken).ConfigureAwait(false);
// Redact PII information from output messages
response.Messages = FilterMessages(response.Messages);
@@ -171,7 +171,7 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
}
// This middleware enforces guardrails by redacting certain keywords from input and output messages.
async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact keywords from input messages
var filteredMessages = FilterMessages(messages);
@@ -179,7 +179,7 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
Console.WriteLine("Guardrail Middleware - Filtered messages Pre-Run");
// Proceed with the agent run
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken);
var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken);
// Redact keywords from output messages
response.Messages = FilterMessages(response.Messages);
@@ -208,9 +208,9 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
}
// This middleware handles Human in the loop console interaction for any user approval required during function calling.
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
var response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
var response = await innerAgent.RunAsync(messages, thread, options, cancellationToken);
var userInputRequests = response.UserInputRequests.ToList();
@@ -229,7 +229,7 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
})
.ToList();
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
response = await innerAgent.RunAsync(response.Messages, thread, options, cancellationToken);
userInputRequests = response.UserInputRequests.ToList();
}
@@ -24,26 +24,26 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(new InMemoryChatHistoryProvider(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions))
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions))
});
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Get the chat history to see how many messages are stored.
IList<ChatMessage>? chatHistory = session.GetService<IList<ChatMessage>>();
IList<ChatMessage>? chatHistory = thread.GetService<IList<ChatMessage>>();
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
// Invoke the agent a few more times.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a robot.", session));
Console.WriteLine(await agent.RunAsync("Tell me a joke about a robot.", thread));
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
Console.WriteLine(await agent.RunAsync("Tell me a joke about a lemur.", session));
Console.WriteLine(await agent.RunAsync("Tell me a joke about a lemur.", thread));
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
// At this point, the chat history has exceeded the limit and the original message will not exist anymore,
// so asking a follow up question about it will not work as expected.
Console.WriteLine(await agent.RunAsync("Tell me the joke about the pirate again, but add emojis and use the voice of a parrot.", session));
Console.WriteLine(await agent.RunAsync("Tell me the joke about the pirate again, but add emojis and use the voice of a parrot.", thread));
Console.WriteLine($"\nChat history has {chatHistory?.Count} messages.\n");
@@ -19,10 +19,10 @@ AIAgent agent = new AzureOpenAIClient(
// Enable background responses (only supported by OpenAI Responses at this time).
AgentRunOptions options = new() { AllowBackgroundResponses = true };
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run.
AgentResponse response = await agent.RunAsync("Write a very long novel about otters in space.", session, options);
AgentResponse response = await agent.RunAsync("Write a very long novel about otters in space.", thread, options);
// Poll until the response is complete.
while (response.ContinuationToken is { } token)
@@ -33,19 +33,19 @@ while (response.ContinuationToken is { } token)
// Continue with the token.
options.ContinuationToken = token;
response = await agent.RunAsync(session, options);
response = await agent.RunAsync(thread, options);
}
// Display the result.
Console.WriteLine(response.Text);
// Reset options and session for streaming.
// Reset options and thread for streaming.
options = new() { AllowBackgroundResponses = true };
session = await agent.GetNewSessionAsync();
thread = await agent.GetNewThreadAsync();
AgentResponseUpdate? lastReceivedUpdate = null;
// Start streaming.
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a very long novel about otters in space.", session, options))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a very long novel about otters in space.", thread, options))
{
// Output each update.
Console.Write(update.Text);
@@ -63,7 +63,7 @@ await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a ve
// Resume from interruption point.
options.ContinuationToken = lastReceivedUpdate?.ContinuationToken;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(session, options))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(thread, options))
{
// Output each update.
Console.Write(update.Text);
@@ -39,9 +39,9 @@ Console.WriteLine();
try
{
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
await foreach (var response in agent.RunStreamingAsync(Task, session))
await foreach (var response in agent.RunStreamingAsync(Task, thread))
{
Console.Write(response.Text);
}
@@ -12,9 +12,9 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.ObjectModel" />
<PackageReference Include="Microsoft.Agents.ObjectModel.Json" />
<PackageReference Include="Microsoft.Agents.ObjectModel.PowerFx" />
<PackageReference Include="Microsoft.Bot.ObjectModel" />
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
</ItemGroup>
<ItemGroup>
@@ -12,9 +12,9 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.ObjectModel" />
<PackageReference Include="Microsoft.Agents.ObjectModel.Json" />
<PackageReference Include="Microsoft.Agents.ObjectModel.PowerFx" />
<PackageReference Include="Microsoft.Bot.ObjectModel" />
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
</ItemGroup>
<ItemGroup>
@@ -45,7 +45,7 @@ AIAgent agent = new AzureOpenAIClient(
You manage a TODO list for the user. When the user has completed one of the tasks it can be removed from the TODO list. Only provide the list of TODO items if asked.
You remind users of upcoming calendar events when the user interacts with you.
""" },
ChatHistoryProviderFactory = (ctx, ct) => new ValueTask<ChatHistoryProvider>(new InMemoryChatHistoryProvider()
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore()
// Use WithAIContextProviderMessageRemoval, so that we don't store the messages from the AI context provider in the chat history.
// You may want to store these messages, depending on their content and your requirements.
.WithAIContextProviderMessageRemoval()),
@@ -58,20 +58,20 @@ AIAgent agent = new AzureOpenAIClient(
});
// Invoke the agent and output the text result.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("I need to pick up milk from the supermarket.", session) + "\n");
Console.WriteLine(await agent.RunAsync("I need to take Sally for soccer practice.", session) + "\n");
Console.WriteLine(await agent.RunAsync("I need to make a dentist appointment for Jimmy.", session) + "\n");
Console.WriteLine(await agent.RunAsync("I've taken Sally to soccer practice.", session) + "\n");
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("I need to pick up milk from the supermarket.", thread) + "\n");
Console.WriteLine(await agent.RunAsync("I need to take Sally for soccer practice.", thread) + "\n");
Console.WriteLine(await agent.RunAsync("I need to make a dentist appointment for Jimmy.", thread) + "\n");
Console.WriteLine(await agent.RunAsync("I've taken Sally to soccer practice.", thread) + "\n");
// We can serialize the session, and it will contain both the chat history and the data that each AI context provider serialized.
JsonElement serializedSession = session.Serialize();
// We can serialize the thread, and it will contain both the chat history and the data that each AI context provider serialized.
JsonElement serializedThread = thread.Serialize();
// Let's print it to console to show the contents.
Console.WriteLine(JsonSerializer.Serialize(serializedSession, options: new JsonSerializerOptions() { WriteIndented = true, IndentSize = 2 }) + "\n");
// The serialized session can be stored long term in a persistent store, but in this case we will just deserialize again and continue the conversation.
session = await agent.DeserializeSessionAsync(serializedSession);
Console.WriteLine(JsonSerializer.Serialize(serializedThread, options: new JsonSerializerOptions() { WriteIndented = true, IndentSize = 2 }) + "\n");
// The serialized thread can be stored long term in a persistent store, but in this case we will just deserialize again and continue the conversation.
thread = await agent.DeserializeThreadAsync(serializedThread);
Console.WriteLine(await agent.RunAsync("Considering my appointments, can you create a plan for my day that plans out when I should complete the items on my todo list?", session) + "\n");
Console.WriteLine(await agent.RunAsync("Considering my appointments, can you create a plan for my day that plans out when I should complete the items on my todo list?", thread) + "\n");
namespace SampleApp
{
@@ -12,9 +12,9 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.ObjectModel" />
<PackageReference Include="Microsoft.Agents.ObjectModel.Json" />
<PackageReference Include="Microsoft.Agents.ObjectModel.PowerFx" />
<PackageReference Include="Microsoft.Bot.ObjectModel" />
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
</ItemGroup>
<ItemGroup>
@@ -22,25 +22,25 @@ AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deplo
// Retrieve an AIAgent for the created server side agent version.
ChatClientAgent jokerAgent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, options);
// Invoke the agent with a multi-turn conversation, where the context is preserved in the session object.
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
// Create a conversation in the server
ProjectConversationsClient conversationsClient = aiProjectClient.GetProjectOpenAIClient().GetProjectConversationsClient();
ProjectConversation conversation = await conversationsClient.CreateProjectConversationAsync();
// Providing the conversation Id is not strictly necessary, but by not providing it no information will show up in the Foundry Project UI as conversations.
// Sessions that don't have a conversation Id will work based on the `PreviousResponseId`.
AgentSession session = await jokerAgent.GetNewSessionAsync(conversation.Id);
// Threads that doesn't have a conversation Id will work based on the `PreviousResponseId`.
AgentThread thread = await jokerAgent.GetNewThreadAsync(conversation.Id);
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", session));
Console.WriteLine(await jokerAgent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session));
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread));
Console.WriteLine(await jokerAgent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread));
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the session object.
session = await jokerAgent.GetNewSessionAsync(conversation.Id);
await foreach (AgentResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", session))
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the thread object.
thread = await jokerAgent.GetNewThreadAsync(conversation.Id);
await foreach (AgentResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
}
await foreach (AgentResponseUpdate update in jokerAgent.RunStreamingAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", session))
await foreach (AgentResponseUpdate update in jokerAgent.RunStreamingAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread))
{
Console.WriteLine(update);
}
@@ -54,6 +54,6 @@ The sample will:
When working with multi-turn conversations, there are two approaches:
- **With Conversation ID**: By passing a `conversation.Id` to `GetNewSessionAsync()`, the conversation will be visible in the Azure Foundry Project UI. This is useful for tracking and debugging conversations.
- **Without Conversation ID**: Sessions created without a conversation ID still work correctly, maintaining context via `PreviousResponseId`. However, these conversations may not appear in the Foundry UI.
- **With Conversation ID**: By passing a `conversation.Id` to `GetNewThreadAsync()`, the conversation will be visible in the Azure Foundry Project UI. This is useful for tracking and debugging conversations.
- **Without Conversation ID**: Threads created without a conversation ID still work correctly, maintaining context via `PreviousResponseId`. However, these conversations may not appear in the Foundry UI.
@@ -37,12 +37,12 @@ var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mod
var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentSession session = await existingAgent.GetNewSessionAsync();
Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", session));
AgentThread thread = await existingAgent.GetNewThreadAsync();
Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
session = await existingAgent.GetNewSessionAsync();
await foreach (AgentResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", session))
thread = await existingAgent.GetNewThreadAsync();
await foreach (AgentResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
}
@@ -32,8 +32,8 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mo
// Call the agent with approval-required function tools.
// The agent will request approval before invoking the function.
AgentSession session = await agent.GetNewSessionAsync();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", session);
AgentThread thread = await agent.GetNewThreadAsync();
AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", thread);
// Check if there are any user input requests (approvals needed).
List<UserInputRequestContent> userInputRequests = response.UserInputRequests.ToList();
@@ -53,7 +53,7 @@ while (userInputRequests.Count > 0)
.ToList();
// Pass the user input responses back to the agent for further processing.
response = await agent.RunAsync(userInputMessages, session);
response = await agent.RunAsync(userInputMessages, thread);
userInputRequests = response.UserInputRequests.ToList();
}
@@ -18,27 +18,27 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: JokerInstructions);
// Start a new session for the agent conversation.
AgentSession session = await agent.GetNewSessionAsync();
// Start a new thread for the agent conversation.
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with a new session.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// Run the agent with a new thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the session state to a JsonElement, so it can be stored for later use.
JsonElement serializedSession = session.Serialize();
// Serialize the thread state to a JsonElement, so it can be stored for later use.
JsonElement serializedThread = thread.Serialize();
// Save the serialized session to a temporary file (for demonstration purposes).
// Save the serialized thread to a temporary file (for demonstration purposes).
string tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedSession));
await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedThread));
// Load the serialized session from the temporary file (for demonstration purposes).
JsonElement reloadedSerializedSession = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath))!;
// Load the serialized thread from the temporary file (for demonstration purposes).
JsonElement reloadedSerializedThread = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath))!;
// Deserialize the session state after loading from storage.
AgentSession resumedSession = await agent.DeserializeSessionAsync(reloadedSerializedSession);
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread);
// Run the agent again with the resumed session.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedSession));
// Run the agent again with the resumed thread.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -38,12 +38,12 @@ AIAgent agent = (await aiProjectClient.CreateAIAgentAsync(name: JokerName, model
.Build();
// Invoke the agent and output the text result.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Invoke the agent with streaming support.
session = await agent.GetNewSessionAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", session))
thread = await agent.GetNewThreadAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
}
@@ -49,12 +49,12 @@ await host.RunAsync().ConfigureAwait(false);
/// </summary>
internal sealed class SampleService(AIProjectClient client, AIAgent agent, IHostApplicationLifetime appLifetime) : IHostedService
{
private AgentSession? _session;
private AgentThread? _thread;
public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a session that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._session = await agent.GetNewSessionAsync(cancellationToken);
// Create a thread that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._thread = await agent.GetNewThreadAsync(cancellationToken);
_ = this.RunAsync(appLifetime.ApplicationStopping);
}
@@ -77,7 +77,7 @@ internal sealed class SampleService(AIProjectClient client, AIAgent agent, IHost
}
// Stream the output to the console as it is generated.
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, this._session, cancellationToken: cancellationToken))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, this._thread, cancellationToken: cancellationToken))
{
Console.Write(update);
}
@@ -24,9 +24,9 @@ ChatMessage message = new(ChatRole.User, [
new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg")
]);
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(message, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(message, thread))
{
Console.WriteLine(update);
}
@@ -39,8 +39,8 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
tools: [weatherAgent.AsAIFunction()]);
// Invoke the agent and output the text result.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
// Cleanup by agent name removes the agent versions created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -49,7 +49,7 @@ AIAgent middlewareEnabledAgent = originalAgent
.Use(GuardrailMiddleware, null)
.Build();
AgentSession session = await middlewareEnabledAgent.GetNewSessionAsync();
AgentThread thread = await middlewareEnabledAgent.GetNewThreadAsync();
Console.WriteLine("\n\n=== Example 1: Wording Guardrail ===");
AgentResponse guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful.");
@@ -63,7 +63,7 @@ Console.WriteLine("\n\n=== Example 3: Agent function middleware ===");
// Agent function middleware support is limited to agents that wraps a upstream ChatClientAgent or derived from it.
AgentResponse functionCallResponse = await middlewareEnabledAgent.RunAsync("What's the current time and the weather in Seattle?", session);
AgentResponse functionCallResponse = await middlewareEnabledAgent.RunAsync("What's the current time and the weather in Seattle?", thread);
Console.WriteLine($"Function calling response: {functionCallResponse}");
// Special per-request middleware agent.
@@ -113,13 +113,13 @@ async ValueTask<object?> FunctionCallOverrideWeather(AIAgent agent, FunctionInvo
}
// This middleware redacts PII information from input and output messages.
async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact PII information from input messages
var filteredMessages = FilterMessages(messages);
Console.WriteLine("Pii Middleware - Filtered Messages Pre-Run");
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken).ConfigureAwait(false);
var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken).ConfigureAwait(false);
// Redact PII information from output messages
response.Messages = FilterMessages(response.Messages);
@@ -152,7 +152,7 @@ async Task<AgentResponse> PIIMiddleware(IEnumerable<ChatMessage> messages, Agent
}
// This middleware enforces guardrails by redacting certain keywords from input and output messages.
async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
// Redact keywords from input messages
var filteredMessages = FilterMessages(messages);
@@ -160,7 +160,7 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
Console.WriteLine("Guardrail Middleware - Filtered messages Pre-Run");
// Proceed with the agent run
var response = await innerAgent.RunAsync(filteredMessages, session, options, cancellationToken);
var response = await innerAgent.RunAsync(filteredMessages, thread, options, cancellationToken);
// Redact keywords from output messages
response.Messages = FilterMessages(response.Messages);
@@ -189,9 +189,9 @@ async Task<AgentResponse> GuardrailMiddleware(IEnumerable<ChatMessage> messages,
}
// This middleware handles Human in the loop console interaction for any user approval required during function calling.
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
AgentResponse response = await innerAgent.RunAsync(messages, thread, options, cancellationToken);
List<UserInputRequestContent> userInputRequests = response.UserInputRequests.ToList();
@@ -211,7 +211,7 @@ async Task<AgentResponse> ConsolePromptingApprovalMiddleware(IEnumerable<ChatMes
})
.ToList();
response = await innerAgent.RunAsync(response.Messages, session, options, cancellationToken);
response = await innerAgent.RunAsync(response.Messages, thread, options, cancellationToken);
userInputRequests = response.UserInputRequests.ToList();
}
@@ -42,8 +42,8 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
services: serviceProvider);
// Invoke the agent and output the text result.
AgentSession session = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me current time and weather in Seattle.", session));
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me current time and weather in Seattle.", thread));
// Cleanup by agent name removes the agent version created.
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
@@ -83,7 +83,7 @@ internal sealed class Program
AllowBackgroundResponses = true,
};
AgentSession session = await agent.GetNewSessionAsync();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage message = new(ChatRole.User, [
new TextContent("I need you to help me search for 'OpenAI news'. Please type 'OpenAI news' and submit the search. Once you see search results, the task is complete."),
@@ -93,7 +93,7 @@ internal sealed class Program
// Initial request with screenshot - start with Bing search page
Console.WriteLine("Starting computer automation session (initial screenshot: cua_browser_search.png)...");
AgentResponse response = await agent.RunAsync(message, session: session, options: runOptions);
AgentResponse response = await agent.RunAsync(message, thread: thread, options: runOptions);
// Main interaction loop
const int MaxIterations = 10;
@@ -113,7 +113,7 @@ internal sealed class Program
// Continue with the token.
runOptions.ContinuationToken = token;
response = await agent.RunAsync(session, runOptions);
response = await agent.RunAsync(thread, runOptions);
}
Console.WriteLine($"Agent response received (ID: {response.ResponseId})");
@@ -168,7 +168,7 @@ internal sealed class Program
// Follow-up message with action result and new screenshot
message = new(ChatRole.User, [content]);
response = await agent.RunAsync(message, session: session, options: runOptions);
response = await agent.RunAsync(message, thread: thread, options: runOptions);
}
}
}

Some files were not shown because too many files have changed in this diff Show More