chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to represent an A2A agent as a set of function tools, where each function tool
|
||||
// corresponds to a skill of the A2A agent, and register these function tools with another AI agent so
|
||||
// it can leverage the A2A agent's skills.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using A2A;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Get the agent card
|
||||
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
|
||||
|
||||
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
|
||||
AIAgent a2aAgent = agentCard.AsAIAgent();
|
||||
|
||||
// Create the main agent, and provide the a2a agent skills as a function tools.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(
|
||||
instructions: "You are a helpful assistant that helps people with travel planning.",
|
||||
tools: [.. CreateFunctionTools(a2aAgent, agentCard)]
|
||||
);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls"));
|
||||
|
||||
static IEnumerable<AIFunction> CreateFunctionTools(AIAgent a2aAgent, AgentCard agentCard)
|
||||
{
|
||||
foreach (var skill in agentCard.Skills)
|
||||
{
|
||||
// A2A agent skills don't have schemas describing the expected shape of their inputs and outputs.
|
||||
// Schemas can be beneficial for AI models to better understand the skill's contract, generate
|
||||
// the skill's input accordingly and to know what to expect in the skill's output.
|
||||
// However, the A2A specification defines properties such as name, description, tags, examples,
|
||||
// inputModes, and outputModes to provide context about the skill's purpose, capabilities, usage,
|
||||
// and supported MIME types. These properties are added to the function tool description to help
|
||||
// the model determine the appropriate shape of the skill's input and output.
|
||||
AIFunctionFactoryOptions options = new()
|
||||
{
|
||||
Name = FunctionNameSanitizer.Sanitize(skill.Name),
|
||||
Description = $$"""
|
||||
{
|
||||
"description": "{{skill.Description}}",
|
||||
"tags": "[{{string.Join(", ", skill.Tags ?? [])}}]",
|
||||
"examples": "[{{string.Join(", ", skill.Examples ?? [])}}]",
|
||||
"inputModes": "[{{string.Join(", ", skill.InputModes ?? [])}}]",
|
||||
"outputModes": "[{{string.Join(", ", skill.OutputModes ?? [])}}]"
|
||||
}
|
||||
""",
|
||||
};
|
||||
|
||||
yield return AIFunctionFactory.Create(RunAgentAsync, options);
|
||||
}
|
||||
|
||||
async Task<string> RunAgentAsync(string input, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await a2aAgent.RunAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response.Text;
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class FunctionNameSanitizer
|
||||
{
|
||||
public static string Sanitize(string name)
|
||||
{
|
||||
return InvalidNameCharsRegex().Replace(name, "_");
|
||||
}
|
||||
|
||||
[GeneratedRegex("[^0-9A-Za-z]+")]
|
||||
private static partial Regex InvalidNameCharsRegex();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# A2A Agent as Function Tools
|
||||
|
||||
This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent,
|
||||
and register these function tools with another AI agent so it can leverage the A2A agent's skills.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Access to the A2A agent host service
|
||||
|
||||
**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be
|
||||
spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:A2A_AGENT_HOST="https://your-a2a-agent-host" # Replace with your A2A agent host endpoint
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A AI agent.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Get the agent card
|
||||
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.CreateSessionAsync();
|
||||
|
||||
// AllowBackgroundResponses must be true so the server returns immediately with a continuation token
|
||||
// instead of blocking until the task is complete.
|
||||
AgentRunOptions options = new() { AllowBackgroundResponses = true };
|
||||
|
||||
// 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, options: options);
|
||||
|
||||
// Poll until the response is complete.
|
||||
while (response.ContinuationToken is { } token)
|
||||
{
|
||||
// Wait before polling again.
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
|
||||
// Continue with the token.
|
||||
response = await agent.RunAsync(session, options: new AgentRunOptions { ContinuationToken = token });
|
||||
}
|
||||
|
||||
// Display the result
|
||||
Console.WriteLine(response);
|
||||
@@ -0,0 +1,25 @@
|
||||
# Polling for A2A Agent Task Completion
|
||||
|
||||
This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A AI agent, following the background responses pattern.
|
||||
|
||||
The sample:
|
||||
|
||||
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
|
||||
- Sends a request to the agent that may take time to complete
|
||||
- Polls the agent at regular intervals using continuation tokens until a final response is received
|
||||
- Displays the final result
|
||||
|
||||
This pattern is useful when an AI model cannot complete a complex task in a single response and needs multiple rounds of processing.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- An A2A agent server running and accessible via HTTP
|
||||
|
||||
Set the following environment variable:
|
||||
|
||||
```powershell
|
||||
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when
|
||||
// creating an AIAgent from an A2A agent card using A2AClientOptions.PreferredBindings.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Get the agent card
|
||||
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
|
||||
|
||||
// Use A2AClientOptions to explicitly select the HTTP+JSON protocol binding.
|
||||
// This tells the A2A client factory to prefer the HTTP+JSON interface when the agent card
|
||||
// advertises multiple supported interfaces.
|
||||
A2AClientOptions options = new()
|
||||
{
|
||||
PreferredBindings = [ProtocolBindingNames.HttpJson]
|
||||
};
|
||||
|
||||
// To prefer JSON-RPC instead, use:
|
||||
// A2AClientOptions options = new()
|
||||
// {
|
||||
// PreferredBindings = [ProtocolBindingNames.JsonRpc]
|
||||
// };
|
||||
|
||||
// Create an instance of the AIAgent for an existing A2A agent, using the specified protocol binding.
|
||||
AIAgent agent = agentCard.AsAIAgent(options: options);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(response);
|
||||
@@ -0,0 +1,27 @@
|
||||
# A2A Agent Protocol Selection
|
||||
|
||||
This sample demonstrates how to select the A2A protocol binding when creating an `AIAgent` from an A2A agent card.
|
||||
|
||||
A2A agents can expose multiple interfaces with different protocol bindings (e.g., HTTP+JSON, JSON-RPC). By default, `AsAIAgent()` prefers HTTP+JSON with JSON-RPC as a fallback. This sample shows how to use `A2AClientOptions.PreferredBindings` to explicitly control which protocol binding is used.
|
||||
|
||||
The sample:
|
||||
|
||||
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
|
||||
- Configures `A2AClientOptions` to prefer the HTTP+JSON protocol binding
|
||||
- Creates an `AIAgent` from the resolved agent card using the specified binding
|
||||
- Sends a message to the agent and displays the response
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- An A2A agent server running and accessible via HTTP
|
||||
|
||||
**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md
|
||||
|
||||
Set the following environment variable:
|
||||
|
||||
```powershell
|
||||
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
|
||||
```
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens,
|
||||
// allowing recovery from stream interruptions without losing progress.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Get the agent card
|
||||
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.CreateSessionAsync();
|
||||
|
||||
ResponseContinuationToken? continuationToken = null;
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync("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))
|
||||
{
|
||||
// Saving the continuation token to be able to reconnect to the same response stream later.
|
||||
// Note: Continuation tokens are only returned for long-running tasks. If the underlying A2A agent
|
||||
// returns a message instead of a task, the continuation token will not be initialized.
|
||||
// A2A agents do not support stream resumption from a specific point in the stream,
|
||||
// but only reconnection to obtain the same response stream from the beginning.
|
||||
// So, A2A agents will return an initialized continuation token in the first update
|
||||
// representing the beginning of the stream, and it will be null in all subsequent updates.
|
||||
if (update.ContinuationToken is { } token)
|
||||
{
|
||||
continuationToken = token;
|
||||
}
|
||||
|
||||
// Imitating stream interruption
|
||||
break;
|
||||
}
|
||||
|
||||
// Reconnect to the same response stream using the continuation token obtained from the previous run.
|
||||
// As a first update, the agent will return an update representing the current state of the response at the moment of calling
|
||||
// RunStreamingAsync with the same continuation token, followed by other updates until the end of the stream is reached.
|
||||
if (continuationToken is not null)
|
||||
{
|
||||
await foreach (var update in agent.RunStreamingAsync(session, options: new() { ContinuationToken = continuationToken }))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(update.Text))
|
||||
{
|
||||
Console.WriteLine(update.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# A2A Agent Stream Reconnection
|
||||
|
||||
This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions without losing progress.
|
||||
|
||||
The sample:
|
||||
|
||||
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
|
||||
- Sends a request to the agent and begins streaming the response
|
||||
- Captures a continuation token from the stream for later reconnection
|
||||
- Simulates a stream interruption by breaking out of the streaming loop
|
||||
- Reconnects to the same response stream using the captured continuation token
|
||||
- Displays the response received after reconnection
|
||||
|
||||
This pattern is useful when network interruptions or other failures may disrupt an ongoing streaming response, and you need to recover and continue processing.
|
||||
|
||||
> **Note:** Continuation tokens are only available when the underlying A2A agent returns a task. If the agent returns a message instead, the continuation token will not be initialized and stream reconnection is not applicable.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- An A2A agent server running and accessible via HTTP
|
||||
|
||||
Set the following environment variable:
|
||||
|
||||
```powershell
|
||||
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent-to-Agent (A2A) Samples
|
||||
|
||||
These samples demonstrate how to work with Agent-to-Agent (A2A) specific features in the Agent Framework.
|
||||
|
||||
For other samples that demonstrate how to use AIAgent instances,
|
||||
see the [Getting Started With Agents](../Agents/README.md) samples.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
See the README.md for each sample for the prerequisites for that sample.
|
||||
|
||||
## Samples
|
||||
|
||||
|Sample|Description|
|
||||
|---|---|
|
||||
|[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.|
|
||||
|[A2A Agent Polling For Task Completion](./A2AAgent_PollingForTaskCompletion/)|This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A agent.|
|
||||
|[A2A Agent Stream Reconnection](./A2AAgent_StreamReconnection/)|This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions.|
|
||||
|[A2A Agent Protocol Selection](./A2AAgent_ProtocolSelection/)|This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when creating an AIAgent from an A2A agent card using A2AClientOptions.|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
To run the samples, navigate to the desired sample directory, e.g.
|
||||
|
||||
```powershell
|
||||
cd A2AAgent_AsFunctionTools
|
||||
```
|
||||
|
||||
Set the required environment variables as documented in the sample readme.
|
||||
If the variables are not set, you will be prompted for the values when running the samples.
|
||||
Execute the following command to build the sample:
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
```
|
||||
|
||||
Execute the following command to run the sample:
|
||||
|
||||
```powershell
|
||||
dotnet run --no-build
|
||||
```
|
||||
|
||||
Or just build and run in one step:
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Running the samples from Visual Studio
|
||||
|
||||
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
|
||||
|
||||
You will be prompted for any required environment variables if they are not already set.
|
||||
@@ -0,0 +1,310 @@
|
||||
# AG-UI Getting Started Samples
|
||||
|
||||
This directory contains samples that demonstrate how to build AG-UI (Agent UI Protocol) servers and clients using the Microsoft Agent Framework.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 9.0 or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (`az login`)
|
||||
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All samples require the following environment variables:
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
|
||||
```
|
||||
|
||||
For the client samples, you can optionally set:
|
||||
|
||||
```bash
|
||||
export AGUI_SERVER_URL="http://localhost:8888"
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
### Step01_GettingStarted
|
||||
|
||||
A basic AG-UI server and client that demonstrate the foundational concepts.
|
||||
|
||||
#### Server (`Step01_GettingStarted/Server`)
|
||||
|
||||
A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates:
|
||||
|
||||
- Creating an ASP.NET Core web application
|
||||
- Setting up an AG-UI server endpoint with `MapAGUIServer`
|
||||
- Creating an AI agent from an Azure OpenAI chat client
|
||||
- Streaming responses via Server-Sent Events (SSE)
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step01_GettingStarted/Server
|
||||
dotnet run --urls http://localhost:8888
|
||||
```
|
||||
|
||||
#### Client (`Step01_GettingStarted/Client`)
|
||||
|
||||
An interactive console client that connects to an AG-UI server. Demonstrates:
|
||||
|
||||
- Creating an AG-UI client with `AGUIChatClient`
|
||||
- Managing conversation threads
|
||||
- Streaming responses with `RunStreamingAsync`
|
||||
- Displaying colored console output for different content types
|
||||
- Supporting both interactive and automated modes
|
||||
|
||||
**Prerequisites:** The Step01_GettingStarted server (or any AG-UI server) must be running.
|
||||
|
||||
**Run the client:**
|
||||
|
||||
```bash
|
||||
cd Step01_GettingStarted/Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Type messages and press Enter to interact with the agent. Type `:q` or `quit` to exit.
|
||||
|
||||
### Step02_BackendTools
|
||||
|
||||
An AG-UI server with function tools that execute on the backend.
|
||||
|
||||
#### Server (`Step02_BackendTools/Server`)
|
||||
|
||||
Demonstrates:
|
||||
|
||||
- Creating function tools using `AIFunctionFactory.Create`
|
||||
- Using `[Description]` attributes for tool documentation
|
||||
- Defining explicit request/response types for type safety
|
||||
- Setting up JSON serialization contexts for source generation
|
||||
- Backend tool rendering (tools execute on the server)
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step02_BackendTools/Server
|
||||
dotnet run --urls http://localhost:8888
|
||||
```
|
||||
|
||||
#### Client (`Step02_BackendTools/Client`)
|
||||
|
||||
A client that works with the backend tools server. Try asking: "Find Italian restaurants in Seattle" or "Search for Mexican food in Portland".
|
||||
|
||||
**Run the client:**
|
||||
|
||||
```bash
|
||||
cd Step02_BackendTools/Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Step03_FrontendTools
|
||||
|
||||
Demonstrates frontend tool rendering (tools defined on client, executed on server).
|
||||
|
||||
#### Server (`Step03_FrontendTools/Server`)
|
||||
|
||||
A basic AG-UI server that accepts tool definitions from the client.
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step03_FrontendTools/Server
|
||||
dotnet run --urls http://localhost:8888
|
||||
```
|
||||
|
||||
#### Client (`Step03_FrontendTools/Client`)
|
||||
|
||||
A client that defines and sends tools to the server for execution.
|
||||
|
||||
**Run the client:**
|
||||
|
||||
```bash
|
||||
cd Step03_FrontendTools/Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Step04_HumanInLoop
|
||||
|
||||
Demonstrates human-in-the-loop approval workflows for sensitive operations. This sample includes both a server and client component.
|
||||
|
||||
#### Server (`Step04_HumanInLoop/Server`)
|
||||
|
||||
An AG-UI server that implements approval workflows. Demonstrates:
|
||||
|
||||
- Wrapping tools with `ApprovalRequiredAIFunction`
|
||||
- Converting `FunctionApprovalRequestContent` to approval requests
|
||||
- Middleware pattern with `ServerFunctionApprovalServerAgent`
|
||||
- Complete function call capture and restoration
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step04_HumanInLoop/Server
|
||||
dotnet run --urls http://localhost:8888
|
||||
```
|
||||
|
||||
#### Client (`Step04_HumanInLoop/Client`)
|
||||
|
||||
An interactive client that handles approval requests from the server. Demonstrates:
|
||||
|
||||
- Using `ServerFunctionApprovalClientAgent` middleware
|
||||
- Detecting `FunctionApprovalRequestContent`
|
||||
- Displaying approval details to users
|
||||
- Prompting for approval/rejection
|
||||
- Sending approval responses with `FunctionApprovalResponseContent`
|
||||
- Resuming conversation after approval
|
||||
|
||||
**Run the client:**
|
||||
|
||||
```bash
|
||||
cd Step04_HumanInLoop/Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Try asking the agent to perform sensitive operations like "Approve expense report EXP-12345".
|
||||
|
||||
### Step05_StateManagement
|
||||
|
||||
An AG-UI server and client that demonstrate state management with predictive updates.
|
||||
|
||||
#### Server (`Step05_StateManagement/Server`)
|
||||
|
||||
Demonstrates:
|
||||
|
||||
- Defining state schemas using C# records
|
||||
- Using `SharedStateAgent` middleware for state management
|
||||
- Streaming predictive state updates with `AgentState` content
|
||||
- Managing shared state between client and server
|
||||
- Using JSON serialization contexts for state types
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step05_StateManagement/Server
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The server runs on port 8888 by default.
|
||||
|
||||
#### Client (`Step05_StateManagement/Client`)
|
||||
|
||||
A client that displays and updates shared state from the server. Try asking: "Create a recipe for chocolate chip cookies" or "Suggest a pasta dish".
|
||||
|
||||
**Run the client:**
|
||||
|
||||
```bash
|
||||
cd Step05_StateManagement/Client
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## How AG-UI Works
|
||||
|
||||
### Server-Side
|
||||
|
||||
1. Client sends HTTP POST request with messages
|
||||
2. ASP.NET Core endpoint receives the request via `MapAGUIServer`
|
||||
3. Agent processes messages using Agent Framework
|
||||
4. Responses are streamed back as Server-Sent Events (SSE)
|
||||
|
||||
### Client-Side
|
||||
|
||||
1. `AGUIAgent` sends HTTP POST request to server
|
||||
2. Server responds with SSE stream
|
||||
3. Client parses events into `AgentResponseUpdate` objects
|
||||
4. Updates are displayed based on content type
|
||||
5. The client sends the full message history each turn (the stateless AG-UI client does not rely on a server-assigned `ConversationId`)
|
||||
|
||||
### Protocol Features
|
||||
|
||||
- **HTTP POST** for requests
|
||||
- **Server-Sent Events (SSE)** for streaming responses
|
||||
- **JSON** for event serialization
|
||||
- **Thread IDs** (read from the `RUN_STARTED` event's raw representation) for conversation context. `AGUIChatClient` is stateless and intentionally does not surface a `ConversationId`.
|
||||
- **Run IDs** (as `ResponseId`) for tracking individual executions
|
||||
|
||||
## Security considerations
|
||||
|
||||
`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.
|
||||
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
Ensure the server is running before starting the client:
|
||||
|
||||
```bash
|
||||
# Terminal 1
|
||||
cd AGUI_Step01_ServerBasic
|
||||
dotnet run --urls http://localhost:8888
|
||||
|
||||
# Terminal 2 (after server starts)
|
||||
cd AGUI_Step02_ClientBasic
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 8888 is already in use, choose a different port:
|
||||
|
||||
```bash
|
||||
# Server
|
||||
dotnet run --urls http://localhost:8889
|
||||
|
||||
# Client (set environment variable)
|
||||
export AGUI_SERVER_URL="http://localhost:8889"
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
Make sure you're authenticated with Azure:
|
||||
|
||||
```bash
|
||||
az login
|
||||
```
|
||||
|
||||
Verify you have the `Cognitive Services OpenAI Contributor` role on the Azure OpenAI resource.
|
||||
|
||||
### Missing Environment Variables
|
||||
|
||||
If you see "AZURE_OPENAI_ENDPOINT is not set" errors, ensure environment variables are set in your current shell session before running the samples.
|
||||
|
||||
### Streaming Not Working
|
||||
|
||||
Check that the client timeout is sufficient (default is 60 seconds). For long-running operations, you may need to increase the timeout in the client code.
|
||||
|
||||
## Next Steps
|
||||
|
||||
After completing these samples, explore more AG-UI capabilities:
|
||||
|
||||
### Currently Available in C#
|
||||
|
||||
The samples above demonstrate the AG-UI features currently available in C#:
|
||||
|
||||
- ✅ **Basic Server and Client**: Setting up AG-UI communication
|
||||
- ✅ **Backend Tool Rendering**: Function tools that execute on the server
|
||||
- ✅ **Streaming Responses**: Real-time Server-Sent Events
|
||||
- ✅ **State Management**: State schemas with predictive updates
|
||||
- ✅ **Human-in-the-Loop**: Approval workflows for sensitive operations
|
||||
|
||||
### Coming Soon to C#
|
||||
|
||||
The following advanced AG-UI features are available in the Python implementation and are planned for future C# releases:
|
||||
|
||||
- ⏳ **Generative UI**: Custom UI component generation
|
||||
- ⏳ **Advanced State Patterns**: Complex state synchronization scenarios
|
||||
|
||||
For the most up-to-date AG-UI features, see the [Python samples](../../../../python/samples/) for working examples.
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [AG-UI Overview](https://learn.microsoft.com/agent-framework/integrations/ag-ui/) - Complete AG-UI documentation
|
||||
- [Getting Started Tutorial](https://learn.microsoft.com/agent-framework/integrations/ag-ui/getting-started) - Step-by-step walkthrough
|
||||
- [Backend Tool Rendering](https://learn.microsoft.com/agent-framework/integrations/ag-ui/backend-tool-rendering) - Function tools tutorial
|
||||
- [Human-in-the-Loop](https://learn.microsoft.com/agent-framework/integrations/ag-ui/human-in-the-loop) - Approval workflows tutorial
|
||||
- [State Management](https://learn.microsoft.com/agent-framework/integrations/ag-ui/state-management) - State management tutorial
|
||||
- [Agent Framework Overview](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview) - Core framework concepts
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
|
||||
Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n");
|
||||
|
||||
// Create the AG-UI client agent
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent");
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get user input
|
||||
Console.Write("\nUser (:q or quit to exit): ");
|
||||
string? message = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Console.WriteLine("Request cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message is ":q" or "quit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, message));
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// Display streaming text content
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
if (content is TextContent textContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else if (content is ErrorContent errorContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[Error: {errorContent.Message}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"\nAn error occurred: {ex.Message}");
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7047;http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
|
||||
Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n");
|
||||
|
||||
// Create the AG-UI client agent
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent");
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get user input
|
||||
Console.Write("\nUser (:q or quit to exit): ");
|
||||
string? message = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Console.WriteLine("Request cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message is ":q" or "quit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, message));
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// Display streaming content
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionCallContent functionCallContent:
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}]");
|
||||
|
||||
// Display individual parameters
|
||||
if (functionCallContent.Arguments != null)
|
||||
{
|
||||
foreach (var kvp in functionCallContent.Arguments)
|
||||
{
|
||||
Console.WriteLine($" Parameter: {kvp.Key} = {kvp.Value}");
|
||||
}
|
||||
}
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionResultContent functionResultContent:
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"\n[Function Result - CallId: {functionResultContent.CallId}]");
|
||||
|
||||
if (functionResultContent.Exception != null)
|
||||
{
|
||||
Console.WriteLine($" Exception: {functionResultContent.Exception}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" Result: {functionResultContent.Result}");
|
||||
}
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ErrorContent errorContent:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[Error: {errorContent.Message}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"\nAn error occurred: {ex.Message}");
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Define the function tool
|
||||
[Description("Search for restaurants in a location.")]
|
||||
static RestaurantSearchResponse SearchRestaurants(
|
||||
[Description("The restaurant search request")] RestaurantSearchRequest request)
|
||||
{
|
||||
// Simulated restaurant data
|
||||
string cuisine = request.Cuisine == "any" ? "Italian" : request.Cuisine;
|
||||
|
||||
return new RestaurantSearchResponse
|
||||
{
|
||||
Location = request.Location,
|
||||
Cuisine = request.Cuisine,
|
||||
Results =
|
||||
[
|
||||
new RestaurantInfo
|
||||
{
|
||||
Name = "The Golden Fork",
|
||||
Cuisine = cuisine,
|
||||
Rating = 4.5,
|
||||
Address = $"123 Main St, {request.Location}"
|
||||
},
|
||||
new RestaurantInfo
|
||||
{
|
||||
Name = "Spice Haven",
|
||||
Cuisine = cuisine == "Italian" ? "Indian" : cuisine,
|
||||
Rating = 4.7,
|
||||
Address = $"456 Oak Ave, {request.Location}"
|
||||
},
|
||||
new RestaurantInfo
|
||||
{
|
||||
Name = "Green Leaf",
|
||||
Cuisine = "Vegetarian",
|
||||
Rating = 4.3,
|
||||
Address = $"789 Elm Rd, {request.Location}"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Get JsonSerializerOptions from the configured HTTP JSON options
|
||||
Microsoft.AspNetCore.Http.Json.JsonOptions jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
|
||||
|
||||
// Create tool with serializer options
|
||||
AITool[] tools =
|
||||
[
|
||||
AIFunctionFactory.Create(
|
||||
SearchRestaurants,
|
||||
serializerOptions: jsonOptions.SerializerOptions)
|
||||
];
|
||||
|
||||
// Create the AI agent with tools
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
ChatClientAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant with access to restaurant information.",
|
||||
tools: tools);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
// Define request/response types for the tool
|
||||
internal sealed class RestaurantSearchRequest
|
||||
{
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public string Cuisine { get; set; } = "any";
|
||||
}
|
||||
|
||||
internal sealed class RestaurantSearchResponse
|
||||
{
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
public RestaurantInfo[] Results { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class RestaurantInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
public double Rating { get; set; }
|
||||
public string Address { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context for source generation
|
||||
[JsonSerializable(typeof(RestaurantSearchRequest))]
|
||||
[JsonSerializable(typeof(RestaurantSearchResponse))]
|
||||
internal sealed partial class SampleJsonSerializerContext : JsonSerializerContext;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7047;http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
|
||||
Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n");
|
||||
|
||||
// Define a frontend function tool
|
||||
[Description("Get the user's current location from GPS.")]
|
||||
static string GetUserLocation()
|
||||
{
|
||||
// Access client-side GPS
|
||||
return "Amsterdam, Netherlands (52.37°N, 4.90°E)";
|
||||
}
|
||||
|
||||
// Create frontend tools
|
||||
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)];
|
||||
|
||||
// Create the AG-UI client agent with tools
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "agui-client",
|
||||
description: "AG-UI Client Agent",
|
||||
tools: frontendTools);
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant.")
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get user input
|
||||
Console.Write("\nUser (:q or quit to exit): ");
|
||||
string? message = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Console.WriteLine("Request cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message is ":q" or "quit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, message));
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// Display streaming content
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
if (content is TextContent textContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else if (content is FunctionCallContent functionCallContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Client Tool Call - Name: {functionCallContent.Name}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else if (content is FunctionResultContent functionResultContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"[Client Tool Result: {functionResultContent.Result}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else if (content is ErrorContent errorContent)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[Error: {errorContent.Message}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"\nAn error occurred: {ex.Message}");
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Create the AI agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7047;http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:5100";
|
||||
|
||||
// Connect to the AG-UI server
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
// Create agent
|
||||
ChatClientAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Use default JSON serializer options
|
||||
JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default;
|
||||
|
||||
// Wrap the agent with ServerFunctionApprovalClientAgent
|
||||
ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions);
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
AgentSession? session = null;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("Ask a question (or type 'exit' to quit):");
|
||||
Console.ResetColor();
|
||||
|
||||
string? input;
|
||||
while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, input));
|
||||
Console.WriteLine();
|
||||
|
||||
#pragma warning disable MEAI001
|
||||
List<AIContent> approvalResponses = [];
|
||||
|
||||
do
|
||||
{
|
||||
approvalResponses.Clear();
|
||||
|
||||
List<AgentResponseUpdate> chatResponseUpdates = [];
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, cancellationToken: default))
|
||||
{
|
||||
chatResponseUpdates.Add(update);
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case ToolApprovalRequestContent approvalRequest when approvalRequest.ToolCall is FunctionCallContent fcc:
|
||||
DisplayApprovalRequest(approvalRequest, fcc);
|
||||
|
||||
Console.Write($"\nApprove '{fcc.Name}'? (yes/no): ");
|
||||
string? userInput = Console.ReadLine();
|
||||
bool approved = userInput?.ToUpperInvariant() is "YES" or "Y";
|
||||
|
||||
ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
|
||||
|
||||
if (approvalRequest.AdditionalProperties != null)
|
||||
{
|
||||
approvalResponse.AdditionalProperties = [];
|
||||
foreach (var kvp in approvalRequest.AdditionalProperties)
|
||||
{
|
||||
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
approvalResponses.Add(approvalResponse);
|
||||
break;
|
||||
|
||||
case TextContent textContent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionCallContent functionCall:
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"[Tool Call - Name: {functionCall.Name}]");
|
||||
if (functionCall.Arguments is { } arguments)
|
||||
{
|
||||
Console.WriteLine($" Parameters: {JsonSerializer.Serialize(arguments)}");
|
||||
}
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case FunctionResultContent functionResult:
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"[Tool Result: {functionResult.Result}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ErrorContent error:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[Error: {error.Message}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AgentResponse response = chatResponseUpdates.ToAgentResponse();
|
||||
messages.AddRange(response.Messages);
|
||||
foreach (AIContent approvalResponse in approvalResponses)
|
||||
{
|
||||
messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse]));
|
||||
}
|
||||
}
|
||||
while (approvalResponses.Count > 0);
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
Console.WriteLine("\n");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine("Ask another question (or type 'exit' to quit):");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001
|
||||
static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("============================================================");
|
||||
Console.WriteLine("APPROVAL REQUIRED");
|
||||
Console.WriteLine("============================================================");
|
||||
Console.WriteLine($"Function: {fcc.Name}");
|
||||
|
||||
if (fcc.Arguments != null)
|
||||
{
|
||||
Console.WriteLine("Arguments:");
|
||||
foreach (var arg in fcc.Arguments)
|
||||
{
|
||||
Console.WriteLine($" {arg.Key} = {arg.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("============================================================");
|
||||
Console.ResetColor();
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that handles server function approval requests and responses.
|
||||
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
|
||||
/// and the server's request_approval tool call pattern.
|
||||
/// </summary>
|
||||
internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Process and transform approval messages, creating a new message list
|
||||
var processedMessages = ProcessOutgoingServerFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
|
||||
|
||||
// Run the inner agent and intercept any approval requests
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(
|
||||
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
|
||||
{
|
||||
return new FunctionResultContent(
|
||||
callId: approvalResponse.RequestId,
|
||||
result: JsonSerializer.SerializeToElement(
|
||||
new ApprovalResponse
|
||||
{
|
||||
ApprovalId = approvalResponse.RequestId,
|
||||
Approved = approvalResponse.Approved
|
||||
},
|
||||
jsonOptions));
|
||||
}
|
||||
|
||||
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
|
||||
{
|
||||
var result = new List<ChatMessage>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(messages[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
|
||||
{
|
||||
var result = new List<AIContent>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(contents[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ProcessOutgoingServerFunctionApprovals(
|
||||
List<ChatMessage> messages,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
List<ChatMessage>? result = null;
|
||||
|
||||
Dictionary<string, ToolApprovalRequestContent> approvalRequests = [];
|
||||
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var message = messages[messageIndex];
|
||||
List<AIContent>? transformedContents = null;
|
||||
|
||||
// Process each content item in the message
|
||||
HashSet<string> approvalCalls = [];
|
||||
for (var contentIndex = 0; contentIndex < message.Contents.Count; contentIndex++)
|
||||
{
|
||||
var content = message.Contents[contentIndex];
|
||||
|
||||
// Handle pending approval requests (transform to tool call)
|
||||
if (content is ToolApprovalRequestContent approvalRequest &&
|
||||
approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true &&
|
||||
originalFunction is FunctionCallContent original)
|
||||
{
|
||||
approvalRequests[approvalRequest.RequestId] = approvalRequest;
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
transformedContents.Add(original);
|
||||
}
|
||||
// Handle pending approval responses (transform to tool result)
|
||||
else if (content is ToolApprovalResponseContent approvalResponse &&
|
||||
approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest))
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions));
|
||||
approvalRequests.Remove(approvalResponse.RequestId);
|
||||
correspondingRequest.AdditionalProperties?.Remove("original_function");
|
||||
}
|
||||
// Skip historical approval content
|
||||
else if (content is FunctionCallContent { Name: "request_approval" } approvalCall)
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
approvalCalls.Add(approvalCall.CallId);
|
||||
}
|
||||
else if (content is FunctionResultContent functionResult &&
|
||||
approvalCalls.Contains(functionResult.CallId))
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
approvalCalls.Remove(functionResult.CallId);
|
||||
}
|
||||
else
|
||||
{
|
||||
transformedContents?.Add(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (transformedContents?.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (transformedContents != null)
|
||||
{
|
||||
// We made changes to contents, so use transformedContents
|
||||
var newMessage = new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
};
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
result.Add(newMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're already copying messages, so copy this unchanged message too
|
||||
result?.Add(message);
|
||||
}
|
||||
// If result is null, we haven't made any changes yet, so keep processing
|
||||
}
|
||||
|
||||
return result ?? messages;
|
||||
}
|
||||
|
||||
private static AgentResponseUpdate ProcessIncomingServerApprovalRequests(
|
||||
AgentResponseUpdate update,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
IList<AIContent>? updatedContents = null;
|
||||
for (var i = 0; i < update.Contents.Count; i++)
|
||||
{
|
||||
var content = update.Contents[i];
|
||||
if (content is FunctionCallContent { Name: "request_approval" } request)
|
||||
{
|
||||
updatedContents ??= [.. update.Contents];
|
||||
|
||||
// Serialize the function arguments as JsonElement
|
||||
ApprovalRequest? approvalRequest;
|
||||
if (request.Arguments?.TryGetValue("request", out var reqObj) == true &&
|
||||
reqObj is JsonElement je)
|
||||
{
|
||||
approvalRequest = (ApprovalRequest?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest)));
|
||||
}
|
||||
else
|
||||
{
|
||||
approvalRequest = null;
|
||||
}
|
||||
|
||||
if (approvalRequest == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize approval request.");
|
||||
}
|
||||
|
||||
var functionCallArgs = (Dictionary<string, object?>?)approvalRequest.FunctionArguments?
|
||||
.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
|
||||
|
||||
var approvalRequestContent = new ToolApprovalRequestContent(
|
||||
requestId: approvalRequest.ApprovalId,
|
||||
new FunctionCallContent(
|
||||
callId: approvalRequest.ApprovalId,
|
||||
name: approvalRequest.FunctionName,
|
||||
arguments: functionCallArgs));
|
||||
|
||||
approvalRequestContent.AdditionalProperties ??= [];
|
||||
approvalRequestContent.AdditionalProperties["original_function"] = content;
|
||||
|
||||
updatedContents[i] = approvalRequestContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContents is not null)
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
return new AgentResponseUpdate(new ChatResponseUpdate()
|
||||
{
|
||||
Role = chatUpdate.Role,
|
||||
Contents = updatedContents,
|
||||
MessageId = chatUpdate.MessageId,
|
||||
AuthorName = chatUpdate.AuthorName,
|
||||
CreatedAt = chatUpdate.CreatedAt,
|
||||
RawRepresentation = chatUpdate.RawRepresentation,
|
||||
ResponseId = chatUpdate.ResponseId,
|
||||
AdditionalProperties = chatUpdate.AdditionalProperties
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId,
|
||||
ContinuationToken = update.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
namespace ServerFunctionApproval
|
||||
{
|
||||
public sealed class ApprovalRequest
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("function_name")]
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
[JsonPropertyName("function_arguments")]
|
||||
public JsonElement? FunctionArguments { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ApprovalResponse
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("approved")]
|
||||
public required bool Approved { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.HttpLogging;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddHttpLogging(logging =>
|
||||
{
|
||||
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
|
||||
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
|
||||
logging.RequestBodyLogLimit = int.MaxValue;
|
||||
logging.ResponseBodyLogLimit = int.MaxValue;
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.UseHttpLogging();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Define approval-required tool
|
||||
[Description("Approve the expense report.")]
|
||||
static string ApproveExpenseReport(string expenseReportId)
|
||||
{
|
||||
return $"Expense report {expenseReportId} approved";
|
||||
}
|
||||
|
||||
// Get JsonSerializerOptions
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>().Value;
|
||||
|
||||
// Create approval-required tool
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))];
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
// Create base agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
ChatClient openAIChatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant in charge of approving expenses",
|
||||
tools: tools);
|
||||
|
||||
// Wrap with ServerFunctionApprovalAgent
|
||||
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
app.MapAGUIServer("/", agent);
|
||||
await app.RunAsync();
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5100",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7047;http://localhost:5100",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that handles function approval requests on the server side.
|
||||
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
|
||||
/// and the request_approval tool call pattern for client communication.
|
||||
/// </summary>
|
||||
internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Process and transform incoming approval responses from client, creating a new message list
|
||||
var processedMessages = ProcessIncomingFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
|
||||
|
||||
// Run the inner agent and intercept any approval requests
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(
|
||||
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
if (toolCall.Name != "request_approval" || toolCall.Arguments == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid request_approval tool call");
|
||||
}
|
||||
|
||||
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
|
||||
reqObj is JsonElement argsElement &&
|
||||
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
|
||||
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
||||
return new ToolApprovalRequestContent(
|
||||
requestId: request.ApprovalId,
|
||||
new FunctionCallContent(
|
||||
callId: request.ApprovalId,
|
||||
name: request.FunctionName,
|
||||
arguments: request.FunctionArguments));
|
||||
}
|
||||
|
||||
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
var approvalResponse = (result.Result is JsonElement je ?
|
||||
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result is string str ?
|
||||
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
||||
return approval.CreateResponse(approvalResponse.Approved);
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
|
||||
{
|
||||
var result = new List<ChatMessage>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(messages[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
|
||||
{
|
||||
var result = new List<AIContent>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(contents[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ProcessIncomingFunctionApprovals(
|
||||
List<ChatMessage> messages,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
List<ChatMessage>? result = null;
|
||||
|
||||
// Track approval ID to original call ID mapping
|
||||
_ = new Dictionary<string, string>();
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
|
||||
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var message = messages[messageIndex];
|
||||
List<AIContent>? transformedContents = null;
|
||||
for (int j = 0; j < message.Contents.Count; j++)
|
||||
{
|
||||
var content = message.Contents[j];
|
||||
if (content is FunctionCallContent { Name: "request_approval" } toolCall)
|
||||
{
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||
var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions);
|
||||
transformedContents.Add(approvalRequest);
|
||||
trackedRequestApprovalToolCalls[toolCall.CallId] = approvalRequest;
|
||||
result.Add(new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
});
|
||||
}
|
||||
else if (content is FunctionResultContent toolResult &&
|
||||
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
|
||||
{
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||
var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions);
|
||||
transformedContents.Add(approvalResponse);
|
||||
result.Add(new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
result?.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
return result ?? messages;
|
||||
}
|
||||
|
||||
private static AgentResponseUpdate ProcessOutgoingApprovalRequests(
|
||||
AgentResponseUpdate update,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
IList<AIContent>? updatedContents = null;
|
||||
for (var i = 0; i < update.Contents.Count; i++)
|
||||
{
|
||||
var content = update.Contents[i];
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall)
|
||||
{
|
||||
updatedContents ??= [.. update.Contents];
|
||||
var approvalId = request.RequestId;
|
||||
|
||||
var approvalData = new ApprovalRequest
|
||||
{
|
||||
ApprovalId = approvalId,
|
||||
FunctionName = functionCall.Name,
|
||||
FunctionArguments = functionCall.Arguments,
|
||||
Message = $"Approve execution of '{functionCall.Name}'?"
|
||||
};
|
||||
|
||||
updatedContents[i] = new FunctionCallContent(
|
||||
callId: approvalId,
|
||||
name: "request_approval",
|
||||
arguments: new Dictionary<string, object?> { ["request"] = approvalData });
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
}
|
||||
|
||||
if (updatedContents is not null)
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
// Yield a tool call update that represents the approval request
|
||||
return new AgentResponseUpdate(new ChatResponseUpdate()
|
||||
{
|
||||
Role = chatUpdate.Role,
|
||||
Contents = updatedContents,
|
||||
MessageId = chatUpdate.MessageId,
|
||||
AuthorName = chatUpdate.AuthorName,
|
||||
CreatedAt = chatUpdate.CreatedAt,
|
||||
RawRepresentation = chatUpdate.RawRepresentation,
|
||||
ResponseId = chatUpdate.ResponseId,
|
||||
AdditionalProperties = chatUpdate.AdditionalProperties
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId,
|
||||
ContinuationToken = update.ContinuationToken
|
||||
};
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ServerFunctionApproval
|
||||
{
|
||||
// Define approval models
|
||||
public sealed class ApprovalRequest
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("function_name")]
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
[JsonPropertyName("function_arguments")]
|
||||
public IDictionary<string, object?>? FunctionArguments { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ApprovalResponse
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("approved")]
|
||||
public required bool Approved { get; init; }
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(ApprovalRequest))]
|
||||
[JsonSerializable(typeof(ApprovalResponse))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
public sealed partial class ApprovalJsonContext : JsonSerializerContext;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<PackageReference Include="AGUI.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using RecipeClient;
|
||||
|
||||
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
|
||||
|
||||
Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n");
|
||||
|
||||
// Create the AG-UI client agent
|
||||
using HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "recipe-client",
|
||||
description: "AG-UI Recipe Client Agent");
|
||||
|
||||
// Wrap the base agent with state management
|
||||
JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
TypeInfoResolver = RecipeSerializerContext.Default
|
||||
};
|
||||
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful recipe assistant.")
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get user input
|
||||
Console.Write("\nUser (:q to quit, :state to show state): ");
|
||||
string? message = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Console.WriteLine("Request cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message is ":q" or "quit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.Equals(":state", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DisplayState(agent.State.Recipe);
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, message));
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
bool stateReceived = false;
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// Display streaming content
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.Write(textContent.Text);
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case DataContent dataContent when dataContent.MediaType == "application/json":
|
||||
// This is a state snapshot - the StatefulAgent has already updated the state
|
||||
stateReceived = true;
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n[State Snapshot Received]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ErrorContent errorContent:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[Error: {errorContent.Message}]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
|
||||
// Display final state if received
|
||||
if (stateReceived)
|
||||
{
|
||||
DisplayState(agent.State.Recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"\nAn error occurred: {ex.Message}");
|
||||
}
|
||||
|
||||
static void DisplayState(RecipeState? state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine("\n[No state available]");
|
||||
Console.ResetColor();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n" + new string('=', 60));
|
||||
Console.WriteLine("CURRENT STATE");
|
||||
Console.WriteLine(new string('=', 60));
|
||||
Console.ResetColor();
|
||||
|
||||
if (!string.IsNullOrEmpty(state.Title))
|
||||
{
|
||||
Console.WriteLine("\nRecipe:");
|
||||
Console.WriteLine($" Title: {state.Title}");
|
||||
if (!string.IsNullOrEmpty(state.Cuisine))
|
||||
{
|
||||
Console.WriteLine($" Cuisine: {state.Cuisine}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(state.SkillLevel))
|
||||
{
|
||||
Console.WriteLine($" Skill Level: {state.SkillLevel}");
|
||||
}
|
||||
|
||||
if (state.PrepTimeMinutes > 0)
|
||||
{
|
||||
Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes");
|
||||
}
|
||||
|
||||
if (state.CookTimeMinutes > 0)
|
||||
{
|
||||
Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes");
|
||||
}
|
||||
|
||||
if (state.Ingredients.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n Ingredients:");
|
||||
foreach (var ingredient in state.Ingredients)
|
||||
{
|
||||
Console.WriteLine($" - {ingredient}");
|
||||
}
|
||||
}
|
||||
|
||||
if (state.Steps.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n Steps:");
|
||||
for (int i = 0; i < state.Steps.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" {i + 1}. {state.Steps[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n" + new string('=', 60));
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
// State wrapper
|
||||
internal sealed class AgentState
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public RecipeState Recipe { get; set; } = new();
|
||||
}
|
||||
|
||||
// Recipe state model
|
||||
internal sealed class RecipeState
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cuisine")]
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<string> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("steps")]
|
||||
public List<string> Steps { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("prep_time_minutes")]
|
||||
public int PrepTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("cook_time_minutes")]
|
||||
public int CookTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context
|
||||
[JsonSerializable(typeof(AgentState))]
|
||||
[JsonSerializable(typeof(RecipeState))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace RecipeClient;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that manages client-side state and automatically attaches it to requests.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The state type.</typeparam>
|
||||
internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
||||
where TState : class, new()
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state.
|
||||
/// </summary>
|
||||
public TState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StatefulAgent{TState}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent to delegate to.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options for state serialization.</param>
|
||||
/// <param name="initialState">The initial state. If null, a new instance will be created.</param>
|
||||
public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
this.State = initialState ?? new TState();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Add state to messages
|
||||
List<ChatMessage> messagesWithState = [.. messages];
|
||||
|
||||
// Serialize the state using AgentState wrapper
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
this.State,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState)));
|
||||
DataContent stateContent = new(stateBytes, "application/json");
|
||||
ChatMessage stateMessage = new(ChatRole.System, [stateContent]);
|
||||
messagesWithState.Add(stateMessage);
|
||||
|
||||
// Stream the response and update state when received
|
||||
await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, session, options, cancellationToken))
|
||||
{
|
||||
// Check if this update contains a state snapshot
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
|
||||
{
|
||||
// Deserialize the state
|
||||
if (JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
|
||||
{
|
||||
this.State = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
using RecipeAssistant;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// Configure to listen on port 8888
|
||||
builder.WebHost.UseUrls("http://localhost:8888");
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// Get JsonSerializerOptions
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
|
||||
|
||||
// Create base agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "RecipeAgent",
|
||||
instructions: """
|
||||
You are a helpful recipe assistant. When users ask you to create or suggest a recipe,
|
||||
respond with a complete AgentState JSON object that includes:
|
||||
- recipe.title: The recipe name
|
||||
- recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese)
|
||||
- recipe.ingredients: Array of ingredient strings with quantities
|
||||
- recipe.steps: Array of cooking instruction strings
|
||||
- recipe.prep_time_minutes: Preparation time in minutes
|
||||
- recipe.cook_time_minutes: Cooking time in minutes
|
||||
- recipe.skill_level: One of "beginner", "intermediate", or "advanced"
|
||||
|
||||
Always include all fields in the response. Be creative and helpful.
|
||||
""");
|
||||
|
||||
// Wrap with state management middleware
|
||||
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7047;http://localhost:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RecipeAssistant;
|
||||
|
||||
// State wrapper
|
||||
internal sealed class AgentState
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public RecipeState Recipe { get; set; } = new();
|
||||
}
|
||||
|
||||
// Recipe state model
|
||||
internal sealed class RecipeState
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cuisine")]
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<string> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("steps")]
|
||||
public List<string> Steps { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("prep_time_minutes")]
|
||||
public int PrepTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("cook_time_minutes")]
|
||||
public int CookTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context
|
||||
[JsonSerializable(typeof(AgentState))]
|
||||
[JsonSerializable(typeof(RecipeState))]
|
||||
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
|
||||
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AspNetCore\Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Server;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace RecipeAssistant;
|
||||
|
||||
internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Check if the client sent state in the request
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions } chatRunOptions ||
|
||||
!chatOptions.TryGetRunAgentInput(out RunAgentInput? agentInput) ||
|
||||
agentInput.State is not { ValueKind: JsonValueKind.Object } state)
|
||||
{
|
||||
// No state management requested, pass through to inner agent
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Check if state has properties (not empty {})
|
||||
bool hasProperties = false;
|
||||
foreach (JsonProperty _ in state.EnumerateObject())
|
||||
{
|
||||
hasProperties = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasProperties)
|
||||
{
|
||||
// Empty state - treat as no state
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// First run: Generate structured state update
|
||||
var firstRunOptions = new ChatClientAgentRunOptions
|
||||
{
|
||||
ChatOptions = chatRunOptions.ChatOptions.Clone(),
|
||||
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
|
||||
ContinuationToken = chatRunOptions.ContinuationToken,
|
||||
ChatClientFactory = chatRunOptions.ChatClientFactory,
|
||||
};
|
||||
|
||||
// Configure JSON schema response format for structured state output
|
||||
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<AgentState>(
|
||||
schemaName: "AgentState",
|
||||
schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions");
|
||||
|
||||
// Add current state to the conversation - state is already a JsonElement
|
||||
ChatMessage stateUpdateMessage = new(
|
||||
ChatRole.System,
|
||||
[
|
||||
new TextContent("Here is the current state in JSON format:"),
|
||||
new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))),
|
||||
new TextContent("The new state is:")
|
||||
]);
|
||||
|
||||
var firstRunMessages = messages.Append(stateUpdateMessage);
|
||||
|
||||
// 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))
|
||||
{
|
||||
allUpdates.Add(update);
|
||||
|
||||
// Yield all non-text updates (tool calls, etc.)
|
||||
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
|
||||
if (hasNonTextContent)
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
// Try to deserialize the structured state response
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
// Serialize and emit as STATE_SNAPSHOT via DataContent
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
Contents = [new DataContent(stateBytes, "application/json")]
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Second run: Generate user-friendly summary
|
||||
var secondRunMessages = messages.Concat(response.Messages).Append(
|
||||
new ChatMessage(
|
||||
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))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (deserialized is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = deserialized;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Azure.Monitor.OpenTelemetry.Exporter;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Logs;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
#region Setup Telemetry
|
||||
|
||||
// Source name for this sample's custom ActivitySource and Meter; other instrumentation uses their own sources/categories.
|
||||
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
|
||||
const string ServiceName = "AgentOpenTelemetry";
|
||||
|
||||
// Configure OpenTelemetry for Aspire dashboard
|
||||
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4317";
|
||||
|
||||
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
|
||||
|
||||
// Create a resource to identify this service
|
||||
var resource = ResourceBuilder.CreateDefault()
|
||||
.AddService(ServiceName, serviceVersion: "1.0.0")
|
||||
.AddAttributes(new Dictionary<string, object>
|
||||
{
|
||||
["service.instance.id"] = Environment.MachineName,
|
||||
["deployment.environment"] = "development"
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Setup tracing with resource
|
||||
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||
.AddSource(SourceName) // Our custom activity source
|
||||
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
|
||||
{
|
||||
tracerProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
|
||||
}
|
||||
|
||||
using var tracerProvider = tracerProviderBuilder.Build();
|
||||
|
||||
// Setup metrics with resource and instrument name filtering
|
||||
using var meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
|
||||
.AddMeter(SourceName) // Our custom meter source
|
||||
.AddHttpClientInstrumentation() // HTTP client metrics
|
||||
.AddRuntimeInstrumentation() // .NET runtime metrics
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
||||
.Build();
|
||||
|
||||
// Setup structured logging with OpenTelemetry
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging(loggingBuilder => loggingBuilder
|
||||
.SetMinimumLevel(LogLevel.Debug)
|
||||
.AddOpenTelemetry(options =>
|
||||
{
|
||||
options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"));
|
||||
options.AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri(otlpEndpoint));
|
||||
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
|
||||
{
|
||||
options.AddAzureMonitorLogExporter(options => options.ConnectionString = applicationInsightsConnectionString);
|
||||
}
|
||||
options.IncludeScopes = true;
|
||||
options.IncludeFormattedMessage = true;
|
||||
}));
|
||||
|
||||
using var activitySource = new ActivitySource(SourceName);
|
||||
using var meter = new Meter(SourceName);
|
||||
|
||||
// Create custom metrics
|
||||
var interactionCounter = meter.CreateCounter<int>("agent_interactions_total", description: "Total number of agent interactions");
|
||||
var responseTimeHistogram = meter.CreateHistogram<double>("agent_response_time_seconds", description: "Agent response time in seconds");
|
||||
|
||||
#endregion
|
||||
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var appLogger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
Console.WriteLine("""
|
||||
=== OpenTelemetry Aspire Demo ===
|
||||
This demo shows OpenTelemetry integration with the Agent Framework.
|
||||
You can view the telemetry data in the Aspire Dashboard.
|
||||
Type your message and press Enter. Type 'exit' or empty message to quit.
|
||||
""");
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT environment variable is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// Log application startup
|
||||
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
|
||||
|
||||
[Description("Get the weather for a given location.")]
|
||||
static async Task<string> GetWeatherAsync([Description("The location to get the weather for.")] string location)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
return $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
}
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
|
||||
// Create the agent with the instrumented chat client
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||
name: "OpenTelemetryDemoAgent",
|
||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)],
|
||||
clientFactory: client => client
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation()
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
|
||||
.Build())
|
||||
.AsBuilder()
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
|
||||
.Build();
|
||||
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
|
||||
|
||||
// Create a parent span for the entire agent session
|
||||
using var sessionActivity = activitySource.StartActivity("Agent Session");
|
||||
Console.WriteLine($"Trace ID: {sessionActivity?.TraceId} ");
|
||||
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
sessionActivity?
|
||||
.SetTag("agent.name", "OpenTelemetryDemoAgent")
|
||||
.SetTag("session.id", sessionId)
|
||||
.SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
appLogger.LogInformation("Starting agent session with ID: {SessionId}", sessionId);
|
||||
using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" }))
|
||||
{
|
||||
var interactionCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("You (or 'exit' to quit): ");
|
||||
var userInput = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
appLogger.LogInformation("User requested to exit the session");
|
||||
break;
|
||||
}
|
||||
|
||||
interactionCount++;
|
||||
appLogger.LogInformation("Processing user interaction #{InteractionNumber}: {UserInput}", interactionCount, userInput);
|
||||
|
||||
// Create a child span for each individual interaction
|
||||
using var activity = activitySource.StartActivity("Agent Interaction");
|
||||
activity?
|
||||
.SetTag("user.input", userInput)
|
||||
.SetTag("agent.name", "OpenTelemetryDemoAgent")
|
||||
.SetTag("interaction.number", interactionCount);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
appLogger.LogDebug("Starting agent execution for interaction #{InteractionNumber}", interactionCount);
|
||||
Console.Write("Agent: ");
|
||||
|
||||
// Run the agent (this will create its own internal telemetry spans)
|
||||
await foreach (var update in agent.RunStreamingAsync(userInput, session))
|
||||
{
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
stopwatch.Stop();
|
||||
var responseTime = stopwatch.Elapsed.TotalSeconds;
|
||||
|
||||
// Record metrics (similar to Python example)
|
||||
interactionCounter.Add(1, new KeyValuePair<string, object?>("status", "success"));
|
||||
responseTimeHistogram.Record(responseTime,
|
||||
new KeyValuePair<string, object?>("status", "success"));
|
||||
|
||||
activity?.SetTag("response.success", true);
|
||||
|
||||
appLogger.LogInformation("Agent interaction #{InteractionNumber} completed successfully in {ResponseTime:F2} seconds",
|
||||
interactionCount, responseTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
Console.WriteLine();
|
||||
|
||||
stopwatch.Stop();
|
||||
var responseTime = stopwatch.Elapsed.TotalSeconds;
|
||||
|
||||
// Record error metrics
|
||||
interactionCounter.Add(1, new KeyValuePair<string, object?>("status", "error"));
|
||||
responseTimeHistogram.Record(responseTime,
|
||||
new KeyValuePair<string, object?>("status", "error"));
|
||||
|
||||
activity?
|
||||
.SetTag("response.success", false)
|
||||
.SetTag("error.message", ex.Message)
|
||||
.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
|
||||
appLogger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}",
|
||||
interactionCount, responseTime, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Add session summary to the parent span
|
||||
sessionActivity?
|
||||
.SetTag("session.total_interactions", interactionCount)
|
||||
.SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O"));
|
||||
|
||||
appLogger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount);
|
||||
} // End of logging scope
|
||||
|
||||
appLogger.LogInformation("OpenTelemetry Aspire Demo application shutting down");
|
||||
@@ -0,0 +1,229 @@
|
||||
# OpenTelemetry Aspire Demo with Microsoft Foundry
|
||||
|
||||
This demo showcases the integration of OpenTelemetry with the Microsoft Agent Framework using Microsoft Foundry and the .NET Aspire Dashboard for telemetry visualization.
|
||||
|
||||
## Overview
|
||||
|
||||
The demo consists of three main components:
|
||||
|
||||
1. **Aspire Dashboard** - Provides a web-based interface to visualize OpenTelemetry data
|
||||
2. **Console Application** - An interactive console application that demonstrates agent interactions with proper OpenTelemetry instrumentation
|
||||
3. **[Optional] Application Insights** - When the agent is deployed to a production environment, Application Insights can be used to monitor the agent performance.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Console App<br/>(Interactive)"] --> B["Agent Framework<br/>with OpenTel<br/>Instrumentation"]
|
||||
B --> C["Microsoft Foundry<br/>Project"]
|
||||
A --> D["Aspire Dashboard<br/>(OpenTelemetry Visualization)"]
|
||||
B --> D
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry project endpoint and model configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
- Docker installed (for running Aspire Dashboard)
|
||||
- [Optional] Application Insights and Grafana
|
||||
|
||||
## Configuration
|
||||
|
||||
### Microsoft Foundry Setup
|
||||
Set the following environment variables:
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<your-project>.services.ai.azure.com/api/projects/<your-project>"
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Foundry project.
|
||||
|
||||
### [Optional] Application Insights Setup
|
||||
Set the following environment variables:
|
||||
```powershell
|
||||
$env:APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=XXXX;IngestionEndpoint=https://XXXX.applicationinsights.azure.com/;LiveEndpoint=https://XXXXX.livediagnostics.monitor.azure.com/;ApplicationId=XXXXX"
|
||||
```
|
||||
|
||||
## Running the Demo
|
||||
|
||||
### Quick Start (Using Script)
|
||||
|
||||
The easiest way to run the demo is using the provided PowerShell script:
|
||||
|
||||
```powershell
|
||||
.\start-demo.ps1
|
||||
```
|
||||
|
||||
This script will automatically:
|
||||
- ✅ Check prerequisites (Docker, Foundry configuration)
|
||||
- 🔨 Build the console application
|
||||
- 🐳 Start the Aspire Dashboard via Docker (with anonymous access)
|
||||
- ⏳ Wait for dashboard to be ready (polls port until listening)
|
||||
- 🌐 Open your browser with the dashboard
|
||||
- 📊 Configure telemetry endpoints (http://localhost:4317)
|
||||
- 🎯 Start the interactive console application
|
||||
|
||||
### Manual Setup (Step by Step)
|
||||
|
||||
If you prefer to run the components manually:
|
||||
|
||||
#### Step 1: Start the Aspire Dashboard via Docker
|
||||
|
||||
```powershell
|
||||
docker run -d --name aspire-dashboard -p 4318:18888 -p 4317:18889 -e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true mcr.microsoft.com/dotnet/aspire-dashboard:latest
|
||||
```
|
||||
|
||||
#### Step 2: Access the Dashboard
|
||||
|
||||
Open your browser to: http://localhost:4318
|
||||
|
||||
#### Step 3: Run the Console Application
|
||||
|
||||
```powershell
|
||||
cd dotnet/demos/AgentOpenTelemetry
|
||||
$env:OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
dotnet run
|
||||
```
|
||||
|
||||
#### Interacting with the Console Application
|
||||
|
||||
You should see a welcome message like:
|
||||
|
||||
```
|
||||
=== OpenTelemetry Aspire Demo ===
|
||||
This demo shows OpenTelemetry integration with the Agent Framework.
|
||||
You can view the telemetry data in the Aspire Dashboard.
|
||||
Type your message and press Enter. Type 'exit' or empty message to quit.
|
||||
|
||||
You:
|
||||
```
|
||||
|
||||
1. Type your message and press Enter to interact with the AI agent
|
||||
2. The agent will respond, and you can continue the conversation
|
||||
3. Type `exit` to stop the application
|
||||
|
||||
**Note**: Make sure the Aspire Dashboard is running before starting the console application, as the telemetry data will be sent to the dashboard.
|
||||
|
||||
#### Step 4: Test the Integration
|
||||
|
||||
1. **Start the Aspire Dashboard** (if not already running)
|
||||
2. **Run the Console Application** in a separate terminal
|
||||
3. **Send a test message** like "Hello, how are you?"
|
||||
4. **Check the Aspire Dashboard** - you should see:
|
||||
- New traces appearing in the **Traces** tab
|
||||
- Each trace showing the complete agent interaction flow
|
||||
- Metrics in the **Metrics** tab showing token usage and duration
|
||||
- Logs in the **Structured Logs** tab with detailed information
|
||||
|
||||
## Viewing Telemetry Data in Aspire Dashboard
|
||||
|
||||
### Traces
|
||||
1. In the Aspire Dashboard, navigate to the **Traces** tab
|
||||
2. You'll see traces for each agent interaction
|
||||
3. Each trace contains:
|
||||
- An outer span for the entire agent interaction
|
||||
- Inner spans from the Agent Framework's OpenTelemetry instrumentation
|
||||
- Spans from HTTP calls to Microsoft Foundry
|
||||
|
||||
### Metrics
|
||||
1. Navigate to the **Metrics** tab
|
||||
2. View metrics related to:
|
||||
- Agent execution duration
|
||||
- Token usage (input/output tokens)
|
||||
- Request counts
|
||||
|
||||
### Logs
|
||||
1. Navigate to the **Structured Logs** tab
|
||||
2. Filter by the console application to see detailed logs
|
||||
3. Logs include information about user inputs, agent responses, and any errors
|
||||
|
||||
## [Optional] View Application Insights data in Grafana
|
||||
Besides the Aspire Dashboard and the Application Insights native UI, you can also use Grafana to visualize the telemetry data in Application Insights. There are two tailored dashboards for you to get started quickly:
|
||||
|
||||
### Agent Overview dashboard
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-agent>
|
||||

|
||||
|
||||
### Workflow Overview dashboard
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
|
||||

|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### OpenTelemetry Integration
|
||||
- **Automatic instrumentation** of Agent Framework operations
|
||||
- **Custom spans** for user interactions
|
||||
- **Proper span lifecycle management** (create → execute → close)
|
||||
- **Telemetry correlation** across the entire request flow
|
||||
|
||||
### Agent Framework Features
|
||||
- **ChatClientAgent** created from `AIProjectClient`
|
||||
- **OpenTelemetry wrapper** using `.WithOpenTelemetry()`
|
||||
- **Conversation threading** for multi-turn conversations
|
||||
- **Error handling** with telemetry correlation
|
||||
|
||||
### Aspire Dashboard Features
|
||||
- **Real-time telemetry visualization**
|
||||
- **Distributed tracing** across services
|
||||
- **Metrics and logging** integration
|
||||
- **Resource management** and monitoring
|
||||
|
||||
## Available Script
|
||||
|
||||
The demo includes a PowerShell script to make running the demo easy:
|
||||
|
||||
### `start-demo.ps1`
|
||||
Complete demo startup script that handles everything automatically.
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\start-demo.ps1 # Start the complete demo
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- **Automatic configuration detection** - Checks for Foundry configuration
|
||||
- **Project building** - Automatically builds projects before running
|
||||
- **Error handling** - Provides clear error messages if something goes wrong
|
||||
- **Multi-window support** - Opens dashboard in separate window for better experience
|
||||
- **Browser auto-launch** - Automatically opens the Aspire Dashboard in your browser
|
||||
- **Docker integration** - Uses Docker to run the Aspire Dashboard
|
||||
|
||||
**Docker Endpoints:**
|
||||
- **Aspire Dashboard**: `http://localhost:4318`
|
||||
- **OTLP Telemetry**: `http://localhost:4317`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Conflicts
|
||||
If you encounter port binding errors, try:
|
||||
1. Stop any existing Docker containers using the same ports (`docker stop aspire-dashboard`)
|
||||
2. Or kill any processes using the conflicting ports
|
||||
|
||||
### Authentication Issues
|
||||
- Ensure your Foundry project endpoint is correctly configured
|
||||
- Check that the environment variables are set in the correct terminal session
|
||||
- Verify you're logged in with Azure CLI (`az login`) and have access to the Foundry project
|
||||
- Ensure the `FOUNDRY_MODEL` value matches an enabled model in your Foundry project
|
||||
|
||||
### Build Issues
|
||||
- Ensure you're using .NET 10.0 SDK
|
||||
- Run `dotnet restore` if you encounter package restore issues
|
||||
- Check that all project references are correctly resolved
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
AgentOpenTelemetry/
|
||||
├── AgentOpenTelemetry.csproj # Project file with dependencies
|
||||
├── Program.cs # Main application with Foundry AIProjectClient agent integration
|
||||
├── start-demo.ps1 # PowerShell script to start the demo
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Experiment with different prompts to see various telemetry patterns
|
||||
- Explore the Aspire Dashboard's filtering and search capabilities
|
||||
- Try modifying the OpenTelemetry configuration to add custom metrics or spans
|
||||
- Integrate additional services to see distributed tracing in action
|
||||
@@ -0,0 +1,139 @@
|
||||
# OpenTelemetry Console Demo with Aspire Dashboard (Docker)
|
||||
# This script starts the Aspire Dashboard via Docker and the Console Application
|
||||
|
||||
Write-Host "Starting OpenTelemetry Console Demo..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if (!(Test-Path "AgentOpenTelemetry.csproj")) {
|
||||
Write-Host "Error: Please run this script from the AgentOpenTelemetry directory" -ForegroundColor Red
|
||||
Write-Host "Expected to find AgentOpenTelemetry.csproj file" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if Docker is running
|
||||
try {
|
||||
docker version | Out-Null
|
||||
Write-Host "Docker is running" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Docker is not running or not installed" -ForegroundColor Red
|
||||
Write-Host "Please start Docker Desktop and try again" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for Azure OpenAI configuration
|
||||
if ($env:AZURE_OPENAI_ENDPOINT) {
|
||||
Write-Host "Found Azure OpenAI endpoint: $($env:AZURE_OPENAI_ENDPOINT)" -ForegroundColor Green
|
||||
if ($env:AZURE_OPENAI_DEPLOYMENT_NAME) {
|
||||
Write-Host "Using deployment: $($env:AZURE_OPENAI_DEPLOYMENT_NAME)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Using default deployment: gpt-4o-mini" -ForegroundColor Cyan
|
||||
}
|
||||
} else {
|
||||
Write-Host "Warning: AZURE_OPENAI_ENDPOINT not found!" -ForegroundColor Yellow
|
||||
Write-Host "Please set the AZURE_OPENAI_ENDPOINT environment variable" -ForegroundColor Yellow
|
||||
Write-Host "Example: `$env:AZURE_OPENAI_ENDPOINT='https://your-resource.openai.azure.com/'" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Build console application
|
||||
Write-Host ""
|
||||
Write-Host "Building console application..." -ForegroundColor Cyan
|
||||
|
||||
$buildResult = dotnet build --verbosity quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Failed to build Console App" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Build completed successfully" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Starting Aspire Dashboard via Docker..." -ForegroundColor Cyan
|
||||
|
||||
# Stop any existing Aspire Dashboard container
|
||||
Write-Host "Stopping any existing Aspire Dashboard container..." -ForegroundColor Gray
|
||||
docker stop aspire-dashboard-afdemo 2>$null | Out-Null
|
||||
docker rm aspire-dashboard-afdemo 2>$null | Out-Null
|
||||
|
||||
# Start Aspire Dashboard in Docker daemon mode with fixed token
|
||||
Write-Host "Starting Aspire Dashboard container..." -ForegroundColor Green
|
||||
$fixedToken = "demo-token-12345"
|
||||
$dockerResult = docker run -d `
|
||||
--name aspire-dashboard-afdemo `
|
||||
-p 4318:18888 `
|
||||
-p 4317:18889 `
|
||||
-e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true `
|
||||
--restart unless-stopped `
|
||||
mcr.microsoft.com/dotnet/aspire-dashboard:latest
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Failed to start Aspire Dashboard container" -ForegroundColor Red
|
||||
Write-Host "Make sure Docker is running and try again" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Aspire Dashboard started successfully!" -ForegroundColor Green
|
||||
Write-Host "OTLP Endpoint: http://localhost:4318" -ForegroundColor Cyan
|
||||
|
||||
# Wait for dashboard to be ready by polling the port
|
||||
Write-Host "Waiting for dashboard to be ready..." -ForegroundColor Gray
|
||||
$maxWaitSeconds = 10
|
||||
$waitCount = 0
|
||||
$dashboardReady = $false
|
||||
|
||||
while ($waitCount -lt $maxWaitSeconds -and !$dashboardReady) {
|
||||
try {
|
||||
$tcpConnection = Test-NetConnection -ComputerName "localhost" -Port 4317 -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
|
||||
if ($tcpConnection) {
|
||||
$dashboardReady = $true
|
||||
Write-Host "Dashboard is ready! (took $waitCount seconds)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "." -NoNewline -ForegroundColor Gray
|
||||
Start-Sleep -Seconds 1
|
||||
$waitCount++
|
||||
}
|
||||
} catch {
|
||||
Write-Host "." -NoNewline -ForegroundColor Gray
|
||||
Start-Sleep -Seconds 1
|
||||
$waitCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dashboardReady) {
|
||||
Write-Host ""
|
||||
Write-Host "Dashboard port 4317 not responding after $maxWaitSeconds seconds" -ForegroundColor Yellow
|
||||
Write-Host " Continuing anyway - dashboard might still be starting..." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Open the dashboard in browser (anonymous access enabled)
|
||||
Write-Host "Opening dashboard in browser..." -ForegroundColor Green
|
||||
Write-Host "Dashboard URL: http://localhost:4318" -ForegroundColor Cyan
|
||||
Start-Process "http://localhost:4318"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Starting Console Application..." -ForegroundColor Cyan
|
||||
Write-Host "You can now interact with the AI agent!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Set the OTLP endpoint for the console application (Docker Aspire Dashboard)
|
||||
$otlpEndpoint = "http://localhost:4317"
|
||||
Write-Host "Using OTLP endpoint: $otlpEndpoint" -ForegroundColor Cyan
|
||||
|
||||
$env:OTEL_EXPORTER_OTLP_ENDPOINT = $otlpEndpoint
|
||||
|
||||
# Start the console application in the current window
|
||||
Write-Host ""
|
||||
Write-Host "Starting the console application..." -ForegroundColor Green
|
||||
Write-Host "Tip: The dashboard should now be open in your browser!" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
dotnet run --no-build
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Demo completed!" -ForegroundColor Green
|
||||
Write-Host "The Aspire Dashboard is still running in Docker." -ForegroundColor Gray
|
||||
Write-Host "You can view telemetry data in the browser tab that opened." -ForegroundColor Gray
|
||||
Write-Host "To stop the dashboard: docker stop aspire-dashboard-afdemo" -ForegroundColor Gray
|
||||
@@ -0,0 +1,98 @@
|
||||
# Creating an AIAgent with various providers
|
||||
|
||||
These samples show how to create an AIAgent instance using various providers,
|
||||
organized by provider. This is not an exhaustive list, but shows a variety of
|
||||
the more popular options.
|
||||
|
||||
For other samples that demonstrate how to use AIAgent instances,
|
||||
see the [Getting Started With Agents](../Agents/README.md) samples.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
See the README.md for each sample for the prerequisites for that sample.
|
||||
|
||||
## Providers
|
||||
|
||||
### [A2A](./a2a/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Agent with A2A](./a2a/Agent_With_A2A/) | Create an AIAgent for an existing A2A agent |
|
||||
|
||||
### [Anthropic](./anthropic/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Agent with Anthropic](./anthropic/Agent_With_Anthropic/) | Create an AIAgent using Anthropic Claude models |
|
||||
| [Running](./anthropic/Agent_Anthropic_Step01_Running/) | Basic Anthropic agent usage |
|
||||
| [Reasoning](./anthropic/Agent_Anthropic_Step02_Reasoning/) | Using Anthropic reasoning capabilities |
|
||||
| [Function Tools](./anthropic/Agent_Anthropic_Step03_UsingFunctionTools/) | Using function tools with Anthropic |
|
||||
| [Skills](./anthropic/Agent_Anthropic_Step04_UsingSkills/) | Using skills with Anthropic agents |
|
||||
|
||||
### [Azure](./azure/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Azure AI Project](./azure/Agent_With_AzureAIProject/) | Create a Foundry Project agent using the Azure.AI.Project SDK |
|
||||
| [Azure Foundry Model](./azure/Agent_With_AzureFoundryModel/) | Use any model deployed to Microsoft Foundry |
|
||||
| [Azure OpenAI ChatCompletion](./azure/Agent_With_AzureOpenAIChatCompletion/) | Create an AIAgent using Azure OpenAI ChatCompletion |
|
||||
| [Azure OpenAI Responses](./azure/Agent_With_AzureOpenAIResponses/) | Create an AIAgent using Azure OpenAI Responses |
|
||||
|
||||
### [Custom](./custom/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Custom Implementation](./custom/Agent_With_CustomImplementation/) | Create an AIAgent with a custom implementation |
|
||||
|
||||
### [Foundry](./foundry/)
|
||||
|
||||
See [foundry/README.md](./foundry/README.md) for the full list of Foundry agent samples,
|
||||
covering basics, function tools, structured output, middleware, MCP, code interpreter, and more.
|
||||
|
||||
### [GitHub Copilot](./github-copilot/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [GitHub Copilot](./github-copilot/Agent_With_GitHubCopilot/) | Create an AIAgent using GitHub Copilot SDK |
|
||||
|
||||
### [Google Gemini](./google-gemini/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Google Gemini](./google-gemini/Agent_With_GoogleGemini/) | Create an AIAgent using Google Gemini |
|
||||
|
||||
### [Ollama](./ollama/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [Ollama](./ollama/Agent_With_Ollama/) | Create an AIAgent using Ollama |
|
||||
|
||||
### [ONNX](./onnx/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [ONNX](./onnx/Agent_With_ONNX/) | Create an AIAgent using ONNX Runtime |
|
||||
|
||||
### [OpenAI](./openai/)
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [OpenAI ChatCompletion](./openai/Agent_With_OpenAIChatCompletion/) | Create an AIAgent using OpenAI ChatCompletion |
|
||||
| [OpenAI Responses](./openai/Agent_With_OpenAIResponses/) | Create an AIAgent using OpenAI Responses |
|
||||
| [Running](./openai/Agent_OpenAI_Step01_Running/) | Basic OpenAI agent usage |
|
||||
| [Reasoning](./openai/Agent_OpenAI_Step02_Reasoning/) | Using OpenAI reasoning capabilities |
|
||||
| [Create from ChatClient](./openai/Agent_OpenAI_Step03_CreateFromChatClient/) | Create agent from IChatClient |
|
||||
| [Create from Response Client](./openai/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/) | Create agent from OpenAI Response client |
|
||||
| [Conversation](./openai/Agent_OpenAI_Step05_Conversation/) | Multi-turn conversations with OpenAI |
|
||||
| [Code Interpreter](./openai/Agent_OpenAI_Step06_CodeInterpreterFileDownload/) | Code interpreter with file downloads |
|
||||
|
||||
## Running the samples
|
||||
|
||||
Navigate to a sample directory and run:
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Set the required environment variables as documented in each sample's README.
|
||||
If the variables are not set, you will be prompted for the values when running the samples.
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with an existing A2A agent.
|
||||
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
|
||||
|
||||
// Initialize an A2ACardResolver to get an A2A agent card.
|
||||
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
|
||||
|
||||
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
|
||||
AIAgent agent = await agentCardResolver.GetAIAgentAsync();
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(response);
|
||||
@@ -0,0 +1,34 @@
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Access to the A2A agent host service
|
||||
|
||||
**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:A2A_AGENT_HOST="https://your-a2a-agent-host" # Replace with your A2A agent host endpoint
|
||||
```
|
||||
|
||||
## Advanced scenario
|
||||
|
||||
This method can be used to create AI agents for A2A agents whose hosts support the [Direct Configuration / Private Discovery](https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#3-direct-configuration--private-discovery) discovery mechanism.
|
||||
|
||||
```csharp
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.A2A;
|
||||
|
||||
// Create an A2AClient pointing to your `echo` A2A agent endpoint
|
||||
A2AClient a2aClient = new(new Uri("https://your-a2a-agent-host/echo"));
|
||||
|
||||
// Create an AIAgent from the A2AClient
|
||||
AIAgent agent = a2aClient.AsAIAgent();
|
||||
|
||||
// Run the agent
|
||||
AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate.");
|
||||
Console.WriteLine(response);
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with Anthropic as the backend.
|
||||
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||
|
||||
AIAgent agent =
|
||||
new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
||||
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Running a simple agent with Anthropic
|
||||
|
||||
This sample demonstrates how to create and run a basic agent with Anthropic Claude models.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating an AI agent with Anthropic Claude
|
||||
- Running a simple agent with instructions
|
||||
- Managing agent lifecycle
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
|
||||
**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="your-anthropic-model" # Replace with your Anthropic model
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the Anthropic sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet\samples\02-agents\AgentProviders\anthropic
|
||||
dotnet run --project .\Agent_Anthropic_Step01_Running
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will:
|
||||
|
||||
1. Create an agent with Anthropic Claude
|
||||
2. Run the agent with a simple prompt
|
||||
3. Display the agent's response
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use an AI agent with reasoning capabilities.
|
||||
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Anthropic.Models.Messages;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||
var maxTokens = 4096;
|
||||
var thinkingTokens = 2048;
|
||||
|
||||
var agent = new AnthropicClient(new ClientOptions { ApiKey = apiKey })
|
||||
.AsAIAgent(
|
||||
model: model,
|
||||
clientFactory: (chatClient) => chatClient
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(
|
||||
options => options.RawRepresentationFactory = (_) => new MessageCreateParams()
|
||||
{
|
||||
Model = options.ModelId ?? model,
|
||||
MaxTokens = options.MaxOutputTokens ?? maxTokens,
|
||||
Messages = [],
|
||||
Thinking = new ThinkingConfigParam(new ThinkingConfigEnabled(budgetTokens: thinkingTokens))
|
||||
})
|
||||
.Build());
|
||||
|
||||
Console.WriteLine("1. Non-streaming:");
|
||||
var response = await agent.RunAsync("Solve this problem step by step: If a train travels 60 miles per hour and needs to cover 180 miles, how long will the journey take? Show your reasoning.");
|
||||
|
||||
Console.WriteLine("#### Start Thinking ####");
|
||||
Console.WriteLine($"\e[92m{string.Join("\n", response.Messages.SelectMany(m => m.Contents.OfType<TextReasoningContent>().Select(c => c.Text)))}\e[0m");
|
||||
Console.WriteLine("#### End Thinking ####");
|
||||
|
||||
Console.WriteLine("\n#### Final Answer ####");
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
Console.WriteLine("Token usage:");
|
||||
Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}, {string.Join(", ", response.Usage?.AdditionalCounts ?? [])}");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("2. Streaming");
|
||||
await foreach (var update in agent.RunStreamingAsync("Explain the theory of relativity in simple terms."))
|
||||
{
|
||||
foreach (var item in update.Contents)
|
||||
{
|
||||
if (item is TextReasoningContent reasoningContent)
|
||||
{
|
||||
Console.WriteLine($"\e[92m{reasoningContent.Text}\e[0m");
|
||||
}
|
||||
else if (item is TextContent textContent)
|
||||
{
|
||||
Console.WriteLine(textContent.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# Using reasoning with Anthropic agents
|
||||
|
||||
This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating an AI agent with Anthropic Claude extended thinking
|
||||
- Using reasoning capabilities for complex problem solving
|
||||
- Extracting thinking and response content from agent output
|
||||
- Managing agent lifecycle
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
- Access to Anthropic Claude models with extended thinking support
|
||||
|
||||
**Note**: This sample uses Anthropic Claude models with extended thinking. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="your-anthropic-model" # Replace with your Anthropic model
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the Anthropic sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet\samples\02-agents\AgentProviders\anthropic
|
||||
dotnet run --project .\Agent_Anthropic_Step02_Reasoning
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will:
|
||||
|
||||
1. Create an agent with Anthropic Claude extended thinking enabled
|
||||
2. Run the agent with a complex reasoning prompt
|
||||
3. Display the agent's thinking process
|
||||
4. Display the agent's final response
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use an agent with function tools.
|
||||
// It shows both non-streaming and streaming agent interactions using weather-related tools.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Anthropic;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||
|
||||
[Description("Get the weather for a given location.")]
|
||||
static string GetWeather([Description("The location to get the weather for.")] string location)
|
||||
=> $"The weather in {location} is cloudy with a high of 15°C.";
|
||||
|
||||
const string AssistantInstructions = "You are a helpful assistant that can get weather information.";
|
||||
const string AssistantName = "WeatherAssistant";
|
||||
|
||||
// Define the agent with function tools.
|
||||
AITool tool = AIFunctionFactory.Create(GetWeather);
|
||||
|
||||
// Get anthropic client to create agents.
|
||||
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.CreateSessionAsync();
|
||||
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", session));
|
||||
|
||||
// Streaming agent interaction with function tools.
|
||||
session = await agent.CreateSessionAsync();
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", session))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Using Function Tools with Anthropic agents
|
||||
|
||||
This sample demonstrates how to use function tools with Anthropic Claude agents, allowing agents to call custom functions to retrieve information.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating function tools using AIFunctionFactory
|
||||
- Passing function tools to an Anthropic Claude agent
|
||||
- Running agents with function tools (text output)
|
||||
- Running agents with function tools (streaming output)
|
||||
- Managing agent lifecycle
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
|
||||
**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="your-anthropic-model" # Replace with your Anthropic model
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the Anthropic sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet\samples\02-agents\AgentProviders\anthropic
|
||||
dotnet run --project .\Agent_Anthropic_Step03_UsingFunctionTools
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will:
|
||||
|
||||
1. Create an agent named "WeatherAssistant" with a GetWeather function tool
|
||||
2. Run the agent with a text prompt asking about weather
|
||||
3. The agent will invoke the GetWeather function tool to retrieve weather information
|
||||
4. Run the agent again with streaming to display the response as it's generated
|
||||
5. Clean up resources by deleting the agent
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use Anthropic-managed Skills with an AI agent.
|
||||
// Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
|
||||
// This sample shows how to:
|
||||
// 1. List available Anthropic-managed skills
|
||||
// 2. Use the pptx skill to create PowerPoint presentations
|
||||
// 3. Download and save generated files
|
||||
|
||||
using Anthropic;
|
||||
using Anthropic.Core;
|
||||
using Anthropic.Models.Beta;
|
||||
using Anthropic.Models.Beta.Files;
|
||||
using Anthropic.Models.Beta.Messages;
|
||||
using Anthropic.Models.Beta.Skills;
|
||||
using Anthropic.Services;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
|
||||
// Skills require Claude 4.5 models (Sonnet 4.5, Haiku 4.5, or Opus 4.5)
|
||||
string model = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-sonnet-4-5-20250929";
|
||||
|
||||
// Create the Anthropic client
|
||||
AnthropicClient anthropicClient = new() { ApiKey = apiKey };
|
||||
|
||||
// List available Anthropic-managed skills (optional - API may not be available in all regions)
|
||||
Console.WriteLine("Available Anthropic-managed skills:");
|
||||
try
|
||||
{
|
||||
SkillListPage skills = await anthropicClient.Beta.Skills.List(
|
||||
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
|
||||
|
||||
foreach (var skill in skills.Items)
|
||||
{
|
||||
Console.WriteLine($" {skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" (Skills listing not available: {ex.Message})");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Define the pptx skill - the SDK handles all beta flags and container configuration automatically
|
||||
// when using AsAITool(), so no manual RawRepresentationFactory configuration is needed.
|
||||
BetaSkillParams pptxSkill = new()
|
||||
{
|
||||
Type = BetaSkillParamsType.Anthropic,
|
||||
SkillID = "pptx",
|
||||
Version = "latest"
|
||||
};
|
||||
|
||||
// Create an agent with the pptx skill enabled.
|
||||
// Skills require extended thinking and higher max tokens for complex file generation.
|
||||
// The SDK's AsAITool() handles beta flags and container config automatically.
|
||||
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
|
||||
model: model,
|
||||
instructions: "You are a helpful agent for creating PowerPoint presentations.",
|
||||
tools: [pptxSkill.AsAITool()],
|
||||
clientFactory: (chatClient) => chatClient
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(options =>
|
||||
{
|
||||
options.RawRepresentationFactory = (_) => new MessageCreateParams()
|
||||
{
|
||||
Model = model,
|
||||
MaxTokens = 20000,
|
||||
Messages = [],
|
||||
Thinking = new BetaThinkingConfigParam(
|
||||
new BetaThinkingConfigEnabled(budgetTokens: 10000))
|
||||
};
|
||||
})
|
||||
.Build());
|
||||
|
||||
Console.WriteLine("Creating a presentation about renewable energy...\n");
|
||||
|
||||
// Run the agent with a request to create a presentation
|
||||
AgentResponse response = await agent.RunAsync("Create a simple 3-slide presentation about renewable energy sources. Include a title slide, a slide about solar energy, and a slide about wind energy.");
|
||||
|
||||
Console.WriteLine("#### Agent Response ####");
|
||||
Console.WriteLine(response.Text);
|
||||
|
||||
// Display any reasoning/thinking content
|
||||
List<TextReasoningContent> reasoningContents = response.Messages.SelectMany(m => m.Contents.OfType<TextReasoningContent>()).ToList();
|
||||
if (reasoningContents.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n#### Agent Reasoning ####");
|
||||
Console.WriteLine($"\e[92m{string.Join("\n", reasoningContents.Select(c => c.Text))}\e[0m");
|
||||
}
|
||||
|
||||
// Collect generated files from CodeInterpreterToolResultContent outputs
|
||||
List<HostedFileContent> hostedFiles = response.Messages
|
||||
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
|
||||
.Where(c => c.Outputs is not null)
|
||||
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
|
||||
.ToList();
|
||||
|
||||
if (hostedFiles.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n#### Generated Files ####");
|
||||
foreach (HostedFileContent file in hostedFiles)
|
||||
{
|
||||
Console.WriteLine($" FileId: {file.FileId}");
|
||||
|
||||
// Download the file using the Anthropic Files API
|
||||
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
|
||||
file.FileId,
|
||||
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
|
||||
|
||||
// Save the file to disk
|
||||
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
|
||||
using FileStream fileStream = File.Create(fileName);
|
||||
Stream contentStream = await fileResponse.ReadAsStream();
|
||||
await contentStream.CopyToAsync(fileStream);
|
||||
|
||||
Console.WriteLine($" Saved to: {fileName}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("\nToken usage:");
|
||||
Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}");
|
||||
if (response.Usage?.AdditionalCounts is not null)
|
||||
{
|
||||
Console.WriteLine($"Additional: {string.Join(", ", response.Usage.AdditionalCounts)}");
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# Using Anthropic Skills with agents
|
||||
|
||||
This sample demonstrates how to use Anthropic-managed Skills with AI agents. Skills are pre-built capabilities provided by Anthropic that can be used with the Claude API.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Listing available Anthropic-managed skills
|
||||
- Creating an AI agent with Anthropic Claude Skills support using the simplified `AsAITool()` approach
|
||||
- Using the pptx skill to create PowerPoint presentations
|
||||
- Downloading and saving generated files to disk
|
||||
- Handling agent responses with generated content
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
- Access to Anthropic Claude models with Skills support
|
||||
|
||||
**Note**: This sample uses Anthropic Claude models with Skills. Skills are a beta feature. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="your-anthropic-model" # Replace with your Anthropic model (e.g., claude-sonnet-4-5-20250929)
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the Anthropic sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet\samples\02-agents\AgentProviders\anthropic
|
||||
dotnet run --project .\Agent_Anthropic_Step04_UsingSkills
|
||||
```
|
||||
|
||||
## Available Anthropic Skills
|
||||
|
||||
Anthropic provides several managed skills that can be used with the Claude API:
|
||||
|
||||
- `pptx` - Create PowerPoint presentations
|
||||
- `xlsx` - Create Excel spreadsheets
|
||||
- `docx` - Create Word documents
|
||||
- `pdf` - Create and analyze PDF documents
|
||||
|
||||
You can list available skills using the Anthropic SDK:
|
||||
|
||||
```csharp
|
||||
SkillListPage skills = await anthropicClient.Beta.Skills.List(
|
||||
new SkillListParams { Source = "anthropic", Betas = [AnthropicBeta.Skills2025_10_02] });
|
||||
|
||||
foreach (var skill in skills.Items)
|
||||
{
|
||||
Console.WriteLine($"{skill.Source}: {skill.ID} (version: {skill.LatestVersion})");
|
||||
}
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
|
||||
The sample will:
|
||||
|
||||
1. List all available Anthropic-managed skills
|
||||
2. Create an agent with the pptx skill enabled
|
||||
3. Run the agent with a request to create a presentation
|
||||
4. Display the agent's response text
|
||||
5. Download any generated files and save them to disk
|
||||
6. Display token usage statistics
|
||||
|
||||
## Code highlights
|
||||
|
||||
### Simplified skill configuration
|
||||
|
||||
The Anthropic SDK handles all beta flags and container configuration automatically when using `AsAITool()`:
|
||||
|
||||
```csharp
|
||||
// Define the pptx skill
|
||||
BetaSkillParams pptxSkill = new()
|
||||
{
|
||||
Type = BetaSkillParamsType.Anthropic,
|
||||
SkillID = "pptx",
|
||||
Version = "latest"
|
||||
};
|
||||
|
||||
// Create an agent - the SDK handles beta flags automatically!
|
||||
ChatClientAgent agent = anthropicClient.Beta.AsAIAgent(
|
||||
model: model,
|
||||
instructions: "You are a helpful agent for creating PowerPoint presentations.",
|
||||
tools: [pptxSkill.AsAITool()]);
|
||||
```
|
||||
|
||||
**Note**: No manual `RawRepresentationFactory`, `Betas`, or `Container` configuration is needed. The SDK automatically adds the required beta headers (`skills-2025-10-02`, `code-execution-2025-08-25`) and configures the container with the skill.
|
||||
|
||||
### Handling generated files
|
||||
|
||||
Generated files are returned as `HostedFileContent` within `CodeInterpreterToolResultContent`:
|
||||
|
||||
```csharp
|
||||
// Collect generated files from response
|
||||
List<HostedFileContent> hostedFiles = response.Messages
|
||||
.SelectMany(m => m.Contents.OfType<CodeInterpreterToolResultContent>())
|
||||
.Where(c => c.Outputs is not null)
|
||||
.SelectMany(c => c.Outputs!.OfType<HostedFileContent>())
|
||||
.ToList();
|
||||
|
||||
// Download and save each file
|
||||
foreach (HostedFileContent file in hostedFiles)
|
||||
{
|
||||
using HttpResponse fileResponse = await anthropicClient.Beta.Files.Download(
|
||||
file.FileId,
|
||||
new FileDownloadParams { Betas = ["files-api-2025-04-14"] });
|
||||
|
||||
string fileName = $"presentation_{file.FileId.Substring(0, 8)}.pptx";
|
||||
await using FileStream fileStream = File.Create(fileName);
|
||||
Stream contentStream = await fileResponse.ReadAsStream();
|
||||
await contentStream.CopyToAsync(fileStream);
|
||||
}
|
||||
```
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0059</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Anthropic.Foundry" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use an AI agent with Anthropic as the backend.
|
||||
|
||||
using Anthropic;
|
||||
using Anthropic.Foundry;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
string deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_CHAT_MODEL_NAME") ?? "claude-haiku-4-5";
|
||||
|
||||
// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri
|
||||
// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions
|
||||
string? resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
|
||||
string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
|
||||
|
||||
const string JokerInstructions = "You are good at telling jokes.";
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
using AnthropicClient client = (resource is null)
|
||||
? new AnthropicClient() { ApiKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API
|
||||
: (apiKey is not null)
|
||||
? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication
|
||||
: new AnthropicFoundryClient(new AnthropicFoundryIdentityTokenCredentials(new DefaultAzureCredential(), resource, ["https://ai.azure.com/.default"])); // Otherwise, use Foundry with Azure TokenCredential authentication
|
||||
|
||||
AIAgent agent = client.AsAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
@@ -0,0 +1,54 @@
|
||||
# Creating an AIAgent with Anthropic
|
||||
|
||||
This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service.
|
||||
|
||||
The sample supports three deployment scenarios:
|
||||
|
||||
1. **Anthropic Public API** - Direct connection to Anthropic's public API
|
||||
2. **Microsoft Foundry with API Key** - Anthropic models deployed through Microsoft Foundry using API key authentication
|
||||
3. **Microsoft Foundry with Azure CLI** - Anthropic models deployed through Microsoft Foundry using Azure CLI credentials
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
|
||||
### For Anthropic Public API
|
||||
|
||||
- Anthropic API key
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
### For Microsoft Foundry with API Key
|
||||
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Anthropic API key
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
### For Microsoft Foundry with Azure CLI
|
||||
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Microsoft Foundry resource name (subdomain before .services.ai.azure.com)
|
||||
$env:ANTHROPIC_CHAT_MODEL_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
|
||||
```
|
||||
|
||||
**Note**: When using Microsoft Foundry with Azure CLI, make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Getting started with agents using Anthropic
|
||||
|
||||
The getting started with agents using Anthropic samples demonstrate the fundamental concepts and functionalities
|
||||
of single agents using Anthropic as the AI provider.
|
||||
|
||||
These samples use Anthropic Claude models as the AI provider and use ChatCompletion as the type of service.
|
||||
|
||||
For other samples that demonstrate how to create and configure each type of agent that come with the agent framework,
|
||||
see the [How to create an agent for each provider](../README.md) samples.
|
||||
|
||||
## Getting started with agents using Anthropic prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 8.0 SDK or later
|
||||
- Anthropic API key configured
|
||||
- User has access to Anthropic Claude models
|
||||
|
||||
**Note**: These samples use Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
|
||||
|
||||
## Using Anthropic with Microsoft Foundry
|
||||
|
||||
To use Anthropic with Microsoft Foundry, you can check the sample [providers/Agent_With_Anthropic](./Agent_With_Anthropic/README.md) for more details.
|
||||
|
||||
## Samples
|
||||
|
||||
|Sample|Description|
|
||||
|---|---|
|
||||
|[Running a simple agent](./Agent_Anthropic_Step01_Running/)|This sample demonstrates how to create and run a basic agent with Anthropic Claude|
|
||||
|[Using reasoning with an agent](./Agent_Anthropic_Step02_Reasoning/)|This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents|
|
||||
|[Using function tools with an agent](./Agent_Anthropic_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with an Anthropic Claude agent|
|
||||
|[Using Skills with an agent](./Agent_Anthropic_Step04_UsingSkills/)|This sample demonstrates how to use Anthropic-managed Skills (e.g., pptx) with an Anthropic Claude agent|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
To run the samples, navigate to the desired sample directory, e.g.
|
||||
|
||||
```powershell
|
||||
cd Agent_Anthropic_Step01_Running
|
||||
```
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
|
||||
```
|
||||
|
||||
If the variables are not set, you will be prompted for the values when running the samples.
|
||||
|
||||
Execute the following command to build the sample:
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
```
|
||||
|
||||
Execute the following command to run the sample:
|
||||
|
||||
```powershell
|
||||
dotnet run --no-build
|
||||
```
|
||||
|
||||
Or just build and run in one step:
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Running the samples from Visual Studio
|
||||
|
||||
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
|
||||
|
||||
You will be prompted for any required environment variables if they are not already set.
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0059</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use AI agents with Microsoft Foundry Agents as the backend.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// Get a client to create/retrieve/delete server side agents with Microsoft Foundry Agents.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Define the agent you want to create. (Prompt Agent in this case)
|
||||
var agentVersionCreationOptions = new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(model: deploymentName) { Instructions = "You are good at telling jokes." });
|
||||
// Azure.AI.Agents SDK creates and manages agent by name and versions.
|
||||
// You can create a server side agent version with the Azure.AI.Agents SDK client below.
|
||||
var createdAgentVersion = aiProjectClient.AgentAdministrationClient.CreateAgentVersion(agentName: JokerName, options: agentVersionCreationOptions);
|
||||
|
||||
// Note:
|
||||
// agentVersion.Id = "<agentName>:<versionNumber>",
|
||||
// agentVersion.Version = <versionNumber>,
|
||||
// agentVersion.Name = <agentName>
|
||||
|
||||
// You can use an AIAgent with an already created server side agent version.
|
||||
FoundryAgent existingJokerAgent = aiProjectClient.AsAIAgent(createdAgentVersion);
|
||||
|
||||
// You can also create another AIAgent version by providing the same name with a different definition.
|
||||
ProjectsAgentVersion newJokerAgentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
JokerName,
|
||||
new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition(model: deploymentName) { Instructions = "You are extremely hilarious at telling jokes." }));
|
||||
FoundryAgent newJokerAgent = aiProjectClient.AsAIAgent(newJokerAgentVersion);
|
||||
|
||||
// You can also get the AIAgent latest version just providing its name.
|
||||
ProjectsAgentRecord jokerAgentRecord = await aiProjectClient.AgentAdministrationClient.GetAgentAsync(JokerName);
|
||||
FoundryAgent jokerAgentLatest = aiProjectClient.AsAIAgent(jokerAgentRecord);
|
||||
ProjectsAgentVersion latestAgentVersion = jokerAgentRecord.GetLatestVersion();
|
||||
|
||||
// The AIAgent version can be accessed via the GetService method.
|
||||
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.CreateSessionAsync();
|
||||
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", session));
|
||||
|
||||
// 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));
|
||||
|
||||
// Cleanup by agent name removes both agent versions created.
|
||||
aiProjectClient.AgentAdministrationClient.DeleteAgent(existingJokerAgent.Name);
|
||||
@@ -0,0 +1,26 @@
|
||||
# New Foundry Agents
|
||||
|
||||
This sample demonstrates how to create an agent using the new Foundry Agents experience.
|
||||
|
||||
# Classic vs New Foundry Agents
|
||||
|
||||
Below is a comparison between the classic and new Foundry Agents approaches:
|
||||
|
||||
[Migration Guide](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/migrate?view=foundry)
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Microsoft Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Microsoft Foundry resource endpoint
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
|
||||
// You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in your Microsoft Foundry resource.
|
||||
// Note: Ensure that you pick a model that suits your needs. For example, if you want to use function calling, ensure that the model you pick supports function calling.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
|
||||
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "Phi-4-mini-instruct";
|
||||
|
||||
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Microsoft Foundry.
|
||||
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
|
||||
|
||||
// Create the OpenAI client with either an API key or Azure CLI credential.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
OpenAIClient client = string.IsNullOrWhiteSpace(apiKey)
|
||||
? new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions)
|
||||
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
|
||||
|
||||
AIAgent agent = client
|
||||
.GetChatClient(model)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
@@ -0,0 +1,34 @@
|
||||
## Overview
|
||||
|
||||
This sample shows how to use the OpenAI SDK to create and use a simple AI agent with any model hosted in Microsoft Foundry.
|
||||
|
||||
You could use models from Microsoft, OpenAI, DeepSeek, Hugging Face, Meta, xAI or any other model you have deployed in Microsoft Foundry.
|
||||
|
||||
**Note**: Ensure that you pick a model that suits your needs. For example, if you want to use function calling, ensure that the model you pick supports function calling.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry resource
|
||||
- A model deployment in your Microsoft Foundry resource. This example defaults to using the `Phi-4-mini-instruct` model,
|
||||
so if you want to use a different model, ensure that you set your `FOUNDRY_MODEL` environment
|
||||
variable to the name of your deployed model.
|
||||
- An API key or role based authentication to access the Microsoft Foundry resource
|
||||
|
||||
See [here](https://learn.microsoft.com/en-us/azure/ai-foundry/quickstarts/get-started-code?tabs=csharp) for more info on setting up these prerequisites
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
# Replace with your Microsoft Foundry resource endpoint
|
||||
# Ensure that you have the "/openai/v1/" path in the URL, since this is required when using the OpenAI SDK to access Microsoft Foundry models.
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://ai-foundry-<myresourcename>.services.ai.azure.com/openai/v1/"
|
||||
|
||||
# Optional, defaults to using Azure CLI for authentication if not provided
|
||||
$env:AZURE_OPENAI_API_KEY="************"
|
||||
|
||||
# Optional, defaults to Phi-4-mini-instruct
|
||||
$env:FOUNDRY_MODEL="Phi-4-mini-instruct"
|
||||
```
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with Azure OpenAI Chat Completion as the backend.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using OpenAI.Chat;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and use a simple AI agent with Azure OpenAI Responses as the backend.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
// You must dissable client side conversation storage for clients that support it
|
||||
AIAgent agent = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
// Create a responses based agent with "store"=false.
|
||||
// This means that chat history is managed locally by Agent Framework
|
||||
// instead of being stored in the service (default).
|
||||
AIAgent agentStoreFalse = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new DefaultAzureCredential())
|
||||
.GetResponsesClient()
|
||||
.AsIChatClientWithStoredOutputDisabled(model: deploymentName)
|
||||
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agentStoreFalse.RunAsync("Tell me a joke about a pirate."));
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Azure OpenAI service endpoint and deployment configured
|
||||
- Azure CLI installed and authenticated (for Azure credential authentication)
|
||||
|
||||
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
|
||||
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows all the required steps to create a fully custom agent implementation.
|
||||
// In this case the agent doesn't use AI at all, and simply parrots back the user input in upper case.
|
||||
// You can however, build a fully custom agent that uses AI in any way you want.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using SampleApp;
|
||||
|
||||
AIAgent agent = new UpperCaseParrotAgent();
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
// Invoke the agent with streaming support.
|
||||
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
|
||||
{
|
||||
Console.WriteLine(update);
|
||||
}
|
||||
|
||||
namespace SampleApp
|
||||
{
|
||||
// Custom agent that parrot's the user input back in upper case.
|
||||
internal sealed class UpperCaseParrotAgent : AIAgent
|
||||
{
|
||||
public override string? Name => "UpperCaseParrotAgent";
|
||||
|
||||
public readonly ChatHistoryProvider ChatHistoryProvider = new InMemoryChatHistoryProvider();
|
||||
|
||||
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
|
||||
=> new(new CustomAgentSession());
|
||||
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
|
||||
}
|
||||
|
||||
return new(JsonSerializer.SerializeToElement(typedSession, jsonSerializerOptions));
|
||||
}
|
||||
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> new(serializedState.Deserialize<CustomAgentSession>(jsonSerializerOptions)!);
|
||||
|
||||
protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a session if the user didn't supply one.
|
||||
session ??= await this.CreateSessionAsync(cancellationToken);
|
||||
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
|
||||
var userAndChatHistoryMessages = await this.ChatHistoryProvider.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(this, session, userAndChatHistoryMessages, responseMessages);
|
||||
await this.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
return new AgentResponse
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = Guid.NewGuid().ToString("N"),
|
||||
Messages = responseMessages
|
||||
};
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a session if the user didn't supply one.
|
||||
session ??= await this.CreateSessionAsync(cancellationToken);
|
||||
|
||||
if (session is not CustomAgentSession typedSession)
|
||||
{
|
||||
throw new ArgumentException($"The provided session is not of type {nameof(CustomAgentSession)}.", nameof(session));
|
||||
}
|
||||
|
||||
// Get existing messages from the store
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
|
||||
var userAndChatHistoryMessages = await this.ChatHistoryProvider.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(this, session, userAndChatHistoryMessages, responseMessages);
|
||||
await this.ChatHistoryProvider.InvokedAsync(invokedContext, cancellationToken);
|
||||
|
||||
foreach (var message in responseMessages)
|
||||
{
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
AgentId = this.Id,
|
||||
AuthorName = message.AuthorName,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = message.Contents,
|
||||
ResponseId = Guid.NewGuid().ToString("N"),
|
||||
MessageId = Guid.NewGuid().ToString("N")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string? agentName) => messages.Select(x =>
|
||||
{
|
||||
// Clone the message and update its author to be the agent.
|
||||
var messageClone = x.Clone();
|
||||
messageClone.Role = ChatRole.Assistant;
|
||||
messageClone.MessageId = Guid.NewGuid().ToString("N");
|
||||
messageClone.AuthorName = agentName;
|
||||
|
||||
// Clone and convert any text content to upper case.
|
||||
messageClone.Contents = x.Contents.Select(c => c switch
|
||||
{
|
||||
TextContent tc => new TextContent(tc.Text.ToUpperInvariant())
|
||||
{
|
||||
AdditionalProperties = tc.AdditionalProperties,
|
||||
Annotations = tc.Annotations,
|
||||
RawRepresentation = tc.RawRepresentation
|
||||
},
|
||||
_ => c
|
||||
}).ToList();
|
||||
|
||||
return messageClone;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// A session type for our custom agent that only supports in memory storage of messages.
|
||||
/// </summary>
|
||||
internal sealed class CustomAgentSession : AgentSession
|
||||
{
|
||||
internal CustomAgentSession()
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
internal CustomAgentSession(AgentSessionStateBag stateBag) : base(stateBag)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Agent with Custom Implementation
|
||||
|
||||
This sample demonstrates how to create a fully custom agent implementation without relying on external AI services.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample creates a simple "parrot" agent that:
|
||||
- Converts user input to uppercase
|
||||
- Supports both synchronous and streaming invocation modes
|
||||
- Demonstrates the complete implementation requirements for a custom agent
|
||||
|
||||
This pattern is useful when you need to:
|
||||
- Integrate with custom AI models or services
|
||||
- Create rule-based agents without AI
|
||||
- Build agents with specific custom logic
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create, use, and clean up a FoundryAgent backed by a server-side
|
||||
// versioned agent in Microsoft Foundry. It demonstrates the full lifecycle:
|
||||
// create agent version -> wrap as FoundryAgent -> run -> delete.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
const string JokerName = "JokerAgent";
|
||||
|
||||
// Create the AIProjectClient to manage server-side agents.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create a server-side agent version using the native SDK.
|
||||
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
JokerName,
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(model: deploymentName)
|
||||
{
|
||||
Instructions = "You are good at telling jokes.",
|
||||
}));
|
||||
|
||||
// Wrap the agent version as a FoundryAgent using the AsAIAgent extension.
|
||||
FoundryAgent agent = aiProjectClient.AsAIAgent(agentVersion);
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
|
||||
// Cleanup: deletes the agent and all its versions.
|
||||
await aiProjectClient.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Agent Step 00 - FoundryAgent Lifecycle
|
||||
|
||||
This sample demonstrates the full lifecycle of a `FoundryAgent` backed by a server-side versioned agent in Microsoft Foundry: create → run → delete.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Microsoft Foundry project endpoint
|
||||
- A model deployment name (defaults to `gpt-5.4-mini`)
|
||||
- An authenticated Azure identity (for example, sign in with `az login`)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
| --- | --- | --- |
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | Yes |
|
||||
| `FOUNDRY_MODEL` | Model deployment name | No (defaults to `gpt-5.4-mini`) |
|
||||
|
||||
## Running the sample
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/AgentProviders/foundry
|
||||
dotnet run --project .\Agent_Step00_FoundryAgentLifecycle
|
||||
```
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create and run a basic agent with AIProjectClient.AsAIAgent(...).
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent =
|
||||
new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "JokerAgent");
|
||||
|
||||
// Once you have the agent, you can invoke it like any other AIAgent.
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
|
||||
@@ -0,0 +1,56 @@
|
||||
# Creating and Running a Basic Agent with the Responses API
|
||||
|
||||
This sample demonstrates how to create and run a basic AI agent using the `ChatClientAgent`, which uses the Microsoft Foundry Responses API directly without creating server-side agent definitions.
|
||||
|
||||
## What this sample demonstrates
|
||||
|
||||
- Creating a `ChatClientAgent` with instructions and a model
|
||||
- Running a simple single-turn conversation
|
||||
- No server-side agent creation or cleanup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- Microsoft Foundry service endpoint and deployment configured
|
||||
- An authenticated Azure identity (for example, sign in with `az login`)
|
||||
|
||||
**Note**: This sample uses `DefaultAzureCredential`. `az login` is the easiest local development path, but Visual Studio, VS Code, and managed identity credentials also work when available.
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
|
||||
$env:FOUNDRY_MODEL="gpt-5.4-mini"
|
||||
```
|
||||
|
||||
## Run the sample
|
||||
|
||||
Navigate to the Foundry sample directory and run:
|
||||
|
||||
```powershell
|
||||
cd dotnet/samples/02-agents/AgentProviders/foundry
|
||||
dotnet run --project .\Agent_Step01_Basics
|
||||
```
|
||||
|
||||
## Alternative: Composable approach
|
||||
|
||||
You can also create the same agent by composing the underlying `IChatClient` directly. This gives you full control over the chat client pipeline:
|
||||
|
||||
```csharp
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
AIAgent agent = new ChatClientAgent(
|
||||
chatClient: aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClient().AsIChatClient(deploymentName),
|
||||
instructions: "You are good at telling jokes.",
|
||||
name: "JokerAgent");
|
||||
```
|
||||
|
||||
This approach is useful when you need to customize the chat client pipeline or swap providers (e.g., Anthropic, OpenAI) while keeping the same agent code.
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user