diff --git a/samples/hosted-agent/dotnet/agent/Program.cs b/samples/hosted-agent/dotnet/agent/Program.cs index cca8996..c09a0a4 100644 --- a/samples/hosted-agent/dotnet/agent/Program.cs +++ b/samples/hosted-agent/dotnet/agent/Program.cs @@ -1,10 +1,15 @@ +// 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. +// Uses Microsoft Agent Framework with Microsoft Foundry. // Ready for deployment to Foundry Hosted Agent service. +#pragma warning disable CA2252 // AIProjectClient and Agents API require opting into preview features + using System.ComponentModel; using System.Globalization; using System.Text; + using Azure.AI.AgentServer.AgentFramework.Extensions; using Azure.AI.Projects; using Azure.Identity; @@ -14,11 +19,9 @@ // 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-5.4-mini"; Console.WriteLine($"Project Endpoint: {endpoint}"); Console.WriteLine($"Model Deployment: {deploymentName}"); - // Simulated hotel data for Seattle var seattleHotels = new[] { @@ -41,12 +44,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 @@ -67,17 +70,19 @@ string GetAvailableHotels( // Build response var result = new StringBuilder(); - result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); - result.AppendLine(); + result + .AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):") + .AppendLine(); foreach (var hotel in availableHotels) { var totalCost = hotel.PricePerNight * nights; - result.AppendLine($"**{hotel.Name}**"); - result.AppendLine($" Location: {hotel.Location}"); - result.AppendLine($" Rating: {hotel.Rating}/5"); - result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); - result.AppendLine(); + result + .AppendLine($"**{hotel.Name}**") + .AppendLine($" Location: {hotel.Location}") + .AppendLine($" Rating: {hotel.Rating}/5") + .AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})") + .AppendLine(); } return result.ToString(); @@ -88,14 +93,16 @@ 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); +// 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 agent = (await projectClient.CreateAIAgentAsync( - model: deploymentName, - name: "SeattleHotelAgent", - instructions: """ +// 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. When a user asks about hotels in Seattle: @@ -108,17 +115,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)], - clientFactory: client => client.AsBuilder() - .UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false) - .Build() - )) - .AsBuilder() - .UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false) - .Build(); - -Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088"); -await agent.RunAIAgentAsync(telemetrySourceName: "Agents"); + tools: [AIFunctionFactory.Create(GetAvailableHotels)]); + +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); diff --git a/samples/hosted-agent/dotnet/agent/{{SafeProjectName}}.csproj b/samples/hosted-agent/dotnet/agent/{{SafeProjectName}}.csproj index 8d96a1e..ff0c8e0 100644 --- a/samples/hosted-agent/dotnet/agent/{{SafeProjectName}}.csproj +++ b/samples/hosted-agent/dotnet/agent/{{SafeProjectName}}.csproj @@ -5,11 +5,34 @@ enable enable true + net10.0 + false - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/samples/hosted-agent/dotnet/workflow/Program.cs b/samples/hosted-agent/dotnet/workflow/Program.cs index 318f15e..cc1e331 100644 --- a/samples/hosted-agent/dotnet/workflow/Program.cs +++ b/samples/hosted-agent/dotnet/workflow/Program.cs @@ -1,166 +1,51 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.Agents.Persistent; +// This sample demonstrates a multi-agent workflow with Writer and Reviewer agents +// using Microsoft Foundry AIProjectClient and the Agent Framework WorkflowBuilder. + +#pragma warning disable CA2252 // AIProjectClient and Agents API require opting into preview features + 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-5.4-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.AsAIAgent().RunAIAgentAsync(); +} +finally +{ + // Cleanup server-side agents + await aiProjectClient.Agents.DeleteAgentAsync(writerAgent.Name); + await aiProjectClient.Agents.DeleteAgentAsync(reviewerAgent.Name); } diff --git a/samples/hosted-agent/dotnet/workflow/{{SafeProjectName}}.csproj b/samples/hosted-agent/dotnet/workflow/{{SafeProjectName}}.csproj index d884ae2..fdaf8d1 100644 --- a/samples/hosted-agent/dotnet/workflow/{{SafeProjectName}}.csproj +++ b/samples/hosted-agent/dotnet/workflow/{{SafeProjectName}}.csproj @@ -1,26 +1,46 @@ - + Exe net9.0 enable enable + net10.0 + false - - - - + + + + - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +