Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 25 additions & 33 deletions samples/hosted-agent/dotnet/agent/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

// Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle.
// Uses Microsoft Agent Framework with Azure AI Foundry.
// Ready for deployment to Foundry Hosted Agent service.

using System.ComponentModel;
using System.Globalization;
using System.Text;
using System.ClientModel.Primitives;

using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
Expand All @@ -16,7 +17,7 @@
// Get configuration from environment variables
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4.1-mini";
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
Console.WriteLine($"Project Endpoint: {endpoint}");
Console.WriteLine($"Model Deployment: {deploymentName}");
// Simulated hotel data for Seattle
Expand All @@ -41,12 +42,12 @@ string GetAvailableHotels(
// Parse dates
if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn))
{
return $"Error parsing check-in date. Please use YYYY-MM-DD format.";
return "Error parsing check-in date. Please use YYYY-MM-DD format.";
}

if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut))
{
return $"Error parsing check-out date. Please use YYYY-MM-DD format.";
return "Error parsing check-out date. Please use YYYY-MM-DD format.";
}

// Validate dates
Expand Down Expand Up @@ -88,29 +89,15 @@ string GetAvailableHotels(
}
}

// Create chat client using AIProjectClient to get the OpenAI connection from the project
var credential = new DefaultAzureCredential();
AIProjectClient projectClient = new AIProjectClient(new Uri(endpoint), credential);

// Get the OpenAI connection from the project
ClientConnection connection = projectClient.GetConnection(typeof(AzureOpenAIClient).FullName!);

if (!connection.TryGetLocatorAsUri(out Uri? openAiEndpoint) || openAiEndpoint is null)
{
throw new InvalidOperationException("Failed to get OpenAI endpoint from project connection.");
}
openAiEndpoint = new Uri($"https://{openAiEndpoint.Host}");
Console.WriteLine($"OpenAI Endpoint: {openAiEndpoint}");
// 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());

var chatClient = new AzureOpenAIClient(openAiEndpoint, credential)
.GetChatClient(deploymentName)
.AsIChatClient()
.AsBuilder()
.UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false)
.Build();

var agent = new ChatClientAgent(chatClient,
// Create Foundry agent with hotel search tool
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(
name: "SeattleHotelAgent",
model: deploymentName,
instructions: """
You are a helpful travel assistant specializing in finding hotels in Seattle, Washington.

Expand All @@ -124,13 +111,18 @@ 5. Offer to help with additional questions about the hotels or Seattle
Be conversational and helpful. If users ask about things outside of Seattle hotels,
politely let them know you specialize in Seattle hotel recommendations.
""",
tools: [AIFunctionFactory.Create(GetAvailableHotels)])
.AsBuilder()
.UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false)
.Build();
tools: [AIFunctionFactory.Create(GetAvailableHotels)]);

Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088");
await agent.RunAIAgentAsync(telemetrySourceName: "Agents");
try
{
Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088");
await agent.RunAIAgentAsync(telemetrySourceName: "Agents");
}
finally
{
// Cleanup server-side agent
await aiProjectClient.Agents.DeleteAgentAsync(agent.Name);
}

// Hotel record for simulated data
record Hotel(string Name, int PricePerNight, double Rating, string Location);
internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location);
26 changes: 25 additions & 1 deletion samples/hosted-agent/dotnet/agent/{{SafeProjectName}}.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<TargetFrameworks>net10.0</TargetFrameworks>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.6" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.8" />
<PackageReference Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.2.0-preview.1.26063.2" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
187 changes: 35 additions & 152 deletions samples/hosted-agent/dotnet/workflow/Program.cs
Original file line number Diff line number Diff line change
@@ -1,166 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.

using Azure.AI.Agents.Persistent;
// This sample demonstrates a multi-agent workflow with Writer and Reviewer agents
// using Azure AI Foundry AIProjectClient and the Agent Framework WorkflowBuilder.

using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.Core;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace {{SafeProjectName}};

internal static class Program
{
private static TracerProvider? s_tracerProvider;

private static async Task Main(string[] args)
{
try
{
// Enable OpenTelemetry tracing for visualization
ConfigureObservability();

await RunAsync().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine($"Critical error: {e}");
}
}

private static async ValueTask RunAsync()
{
// Build configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();

var endpoint =
configuration["PROJECT_ENDPOINT"]
?? throw new InvalidOperationException(
"PROJECT_ENDPOINT is required. Set it in appsettings.Development.json for local development or as PROJECT_ENDPOINT environment variable for production");
var deployment =
configuration["MODEL_DEPLOYMENT_NAME"]
?? throw new InvalidOperationException(
"MODEL_DEPLOYMENT_NAME is required. Set it in appsettings.Development.json for local development or as MODEL_DEPLOYMENT_NAME environment variable for containers");

Console.WriteLine($"Using Azure AI endpoint: {endpoint}");
Console.WriteLine($"Using model deployment: {deployment}");

// Create credential - use ManagedIdentityCredential if MSI_ENDPOINT exists, otherwise DefaultAzureCredential
TokenCredential credential = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSI_ENDPOINT"))
? new DefaultAzureCredential()
: new ManagedIdentityCredential();

// Create separate PersistentAgentsClient for each agent
var writerClient = new PersistentAgentsClient(endpoint, credential);
var reviewerClient = new PersistentAgentsClient(endpoint, credential);
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";

(ChatClientAgent agent, string id)? writer = null;
(ChatClientAgent agent, string id)? reviewer = null;
Console.WriteLine($"Using Azure AI endpoint: {endpoint}");
Console.WriteLine($"Using model deployment: {deploymentName}");

try
{
// Create Foundry agents with separate clients
writer = await CreateAgentAsync(
writerClient,
deployment,
"Writer",
"You are an excellent content writer. You create new content and edit contents based on the feedback."
);
reviewer = await CreateAgentAsync(
reviewerClient,
deployment,
"Reviewer",
"You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content. Provide the feedback in the most concise manner possible."
);
Console.WriteLine();
// 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());

var workflow = new WorkflowBuilder(writer.Value.agent)
.AddEdge(writer.Value.agent, reviewer.Value.agent)
.WithOutputFrom(reviewer.Value.agent)
.Build();
// Create Foundry agents
AIAgent writerAgent = await aiProjectClient.CreateAIAgentAsync(
name: "Writer",
model: deploymentName,
instructions: "You are an excellent content writer. You create new content and edit contents based on the feedback.");

Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088");
await workflow.AsAgent().RunAIAgentAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error running workflow: {ex.Message}");
throw;
}
finally
{
// Clean up all resources
await CleanupAsync(writerClient, writer?.id);
await CleanupAsync(reviewerClient, reviewer?.id);
AIAgent reviewerAgent = await aiProjectClient.CreateAIAgentAsync(
name: "Reviewer",
model: deploymentName,
instructions: "You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content. Provide the feedback in the most concise manner possible.");

if (credential is IDisposable disposable)
{
disposable.Dispose();
}
}
}

private static async Task<(ChatClientAgent agent, string id)> CreateAgentAsync(
PersistentAgentsClient client,
string model,
string name,
string instructions)
{
var agentMetadata = await client.Administration.CreateAgentAsync(
model: model,
name: name,
instructions: instructions
);

var chatClient = client.AsIChatClient(agentMetadata.Value.Id);
return (new ChatClientAgent(chatClient), agentMetadata.Value.Id);
}

private static async Task CleanupAsync(PersistentAgentsClient client, string? agentId)
{
if (string.IsNullOrEmpty(agentId))
{
return;
}

try
{
await client.Administration.DeleteAgentAsync(agentId);
}
catch (Exception e)
{
Console.WriteLine($"Cleanup failed for agent {agentId}: {e.Message}");
}
}

private static void ConfigureObservability()
{
var otlpEndpoint =
Environment.GetEnvironmentVariable("OTLP_ENDPOINT") ?? "http://localhost:4319";

var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService("WorkflowSample");

s_tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("Microsoft.Agents.AI.*") // All agent framework sources
.SetSampler(new AlwaysOnSampler()) // Ensure all traces are sampled
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
})
.Build();
try
{
var workflow = new WorkflowBuilder(writerAgent)
.AddEdge(writerAgent, reviewerAgent)
.Build();

Console.WriteLine($"OpenTelemetry configured. OTLP endpoint: {otlpEndpoint}");
}
Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088");
await workflow.AsAgent().RunAIAgentAsync();
}
finally
{
// Cleanup server-side agents
await aiProjectClient.Agents.DeleteAgentAsync(writerAgent.Name);
await aiProjectClient.Agents.DeleteAgentAsync(reviewerAgent.Name);
}
Loading