diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml index eb365c2982..c0da7d36b2 100644 --- a/.github/.linkspector.yml +++ b/.github/.linkspector.yml @@ -20,6 +20,7 @@ ignorePatterns: - pattern: "https://your-resource.openai.azure.com/" - pattern: "http://host.docker.internal" - pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/" + - pattern: "https:\/\/dotnet.microsoft.com\/download" # excludedDirs: # Folders which include links to localhost, since it's not ignored with regular expressions baseUrl: https://github.com/microsoft/agent-framework/ diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d5773ee9d9..0f105d4a80 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -286,6 +286,8 @@ + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile new file mode 100644 index 0000000000..fc3d3a1a5b --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile @@ -0,0 +1,20 @@ +# Build the application +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# Copy files from the current directory on the host to the working directory in the container +COPY . . + +RUN dotnet restore +RUN dotnet build -c Release --no-restore +RUN dotnet publish -c Release --no-build -o /app -f net10.0 + +# Run the application +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app + +# Copy everything needed to run the app from the "build" stage. +COPY --from=build /app . + +EXPOSE 8088 +ENTRYPOINT ["dotnet", "FoundryMultiAgent.dll"] diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj new file mode 100644 index 0000000000..b2fb41ac5e --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj @@ -0,0 +1,76 @@ + + + Exe + net10.0 + enable + enable + + + 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 + + + + + + PreserveNewest + + + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs new file mode 100644 index 0000000000..138efb0096 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +// 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.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; + +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"; + +Console.WriteLine($"Using Azure AI endpoint: {endpoint}"); +Console.WriteLine($"Using model deployment: {deploymentName}"); + +// 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 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."); + +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."); + +try +{ + var workflow = new WorkflowBuilder(writerAgent) + .AddEdge(writerAgent, reviewerAgent) + .Build(); + + 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); +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md new file mode 100644 index 0000000000..314320880b --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md @@ -0,0 +1,168 @@ +**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). + +Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. + +Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. + +Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. + +# What this sample demonstrates + +This sample demonstrates a **key advantage of code-based hosted agents**: + +- **Multi-agent workflows** - Orchestrate multiple agents working together + +Code-based agents can execute **any C# code** you write. This sample includes a Writer-Reviewer workflow where two agents collaborate: a Writer creates content and a Reviewer provides feedback. + +The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/) and can be deployed to Microsoft Foundry. + +## How It Works + +### Multi-Agent Workflow + +In [Program.cs](Program.cs), the sample creates two agents using `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package: + +- **Writer** - An agent that creates and edits content based on feedback +- **Reviewer** - An agent that provides actionable feedback on the content + +The `WorkflowBuilder` from the [Microsoft.Agents.AI.Workflows](https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/) package connects these agents in a sequential flow: + +1. The Writer receives the initial request and generates content +2. The Reviewer evaluates the content and provides feedback +3. Both agent responses are output to the user + +### Agent Hosting + +The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/), +which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Running the Agent Locally + +### Prerequisites + +Before running this sample, ensure you have: + +1. **Azure AI Foundry Project** + - Project created. + - Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`) + - Note your project endpoint URL and model deployment name + > **Note**: You can right-click the project in the Microsoft Foundry VS Code extension and select `Copy Project Endpoint URL` to get the endpoint. + +2. **Azure CLI** + - Installed and authenticated + - Run `az login` and verify with `az account show` + - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) + +3. **.NET 10.0 SDK or later** + - Verify your version: `dotnet --version` + - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) + +### Environment Variables + +Set the following environment variables: + +**PowerShell:** + +```powershell +# Replace with your actual values +$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +**Bash:** + +```bash +export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +### Running the Sample + +To run the agent, execute the following command in your terminal: + +```bash +dotnet restore +dotnet build +dotnet run +``` + +This will start the hosted agent locally on `http://localhost:8088/`. + +### Interacting with the Agent + +**VS Code:** + +1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. +2. Execute the following commands to start the containerized hosted agent. + ```bash + dotnet restore + dotnet build + dotnet run + ``` +3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "Create a slogan for a new electric SUV that is affordable and fun to drive." +4. Review the agent's response in the playground interface. + +> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. + +**PowerShell (Windows):** + +```powershell +$body = @{ + input = "Create a slogan for a new electric SUV that is affordable and fun to drive" + stream = $false +} | ConvertTo-Json + +Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" +``` + +**Bash/curl (Linux/macOS):** + +```bash +curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ + -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive","stream":false}' +``` + +You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. + +The Writer agent will generate content based on your prompt, and the Reviewer agent will provide feedback on the output. + +## Deploying the Agent to Microsoft Foundry + +**Preparation (required)** + +Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. + +To deploy the hosted agent: + +1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. + +2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. + +3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. + +**What the deploy flow does for you:** + +- Creates or obtains an Azure Container Registry for the target project. +- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). +- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). +- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. + +## MSI Configuration in the Azure Portal + +This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. + +To configure the Managed Identity: + +1. In the Azure Portal, open the Foundry Project. +2. Select "Access control (IAM)" from the left-hand menu. +3. Click "Add" and choose "Add role assignment". +4. In the role selection, search for and select "Azure AI User", then click "Next". +5. For "Assign access to", choose "Managed identity". +6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". +7. Click "Review + assign" to complete the assignment. +8. Allow a few minutes for the role assignment to propagate before running the application. + +## Additional Resources + +- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) +- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml new file mode 100644 index 0000000000..70b82abf7c --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml + +name: FoundryMultiAgent +displayName: "Foundry Multi-Agent Workflow" +description: > + A multi-agent workflow featuring a Writer and Reviewer that collaborate + to create and refine content using Azure AI Foundry PersistentAgentsClient. +metadata: + authors: + - Microsoft Agent Framework Team + tags: + - Azure AI AgentServer + - Microsoft Agent Framework + - Multi-Agent Workflow + - Writer-Reviewer + - Content Creation +template: + kind: hosted + name: FoundryMultiAgent + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: MODEL_DEPLOYMENT_NAME + value: gpt-4o-mini +resources: + - name: "gpt-4o-mini" + kind: model + id: gpt-4o-mini diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json new file mode 100644 index 0000000000..b6b1c77b85 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json @@ -0,0 +1,4 @@ +{ + "AZURE_AI_PROJECT_ENDPOINT": "https://.services.ai.azure.com/api/projects/", + "MODEL_DEPLOYMENT_NAME": "gpt-4o-mini" +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http new file mode 100644 index 0000000000..2fcdb2499e --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http @@ -0,0 +1,34 @@ +@host = http://localhost:8088 +@endpoint = {{host}}/responses + +### Health Check +GET {{host}}/readiness + +### Simple string input - Content creation request +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "Create a slogan for a new electric SUV that is affordable and fun to drive", + "stream": false +} + +### Explicit input format +POST {{endpoint}} +Content-Type: application/json + +{ + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Write a short product description for a smart water bottle that tracks hydration" + } + ] + } + ], + "stream": false +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile new file mode 100644 index 0000000000..0d1141cc69 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile @@ -0,0 +1,20 @@ +# Build the application +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# Copy files from the current directory on the host to the working directory in the container +COPY . . + +RUN dotnet restore +RUN dotnet build -c Release --no-restore +RUN dotnet publish -c Release --no-build -o /app -f net10.0 + +# Run the application +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app + +# Copy everything needed to run the app from the "build" stage. +COPY --from=build /app . + +EXPOSE 8088 +ENTRYPOINT ["dotnet", "FoundrySingleAgent.dll"] diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj new file mode 100644 index 0000000000..756f3d30ee --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj @@ -0,0 +1,67 @@ + + + Exe + net10.0 + enable + enable + + + 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/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs new file mode 100644 index 0000000000..759636bcc0 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs @@ -0,0 +1,128 @@ +// 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 Azure.AI.AgentServer.AgentFramework.Extensions; +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +// 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-4o-mini"; +Console.WriteLine($"Project Endpoint: {endpoint}"); +Console.WriteLine($"Model Deployment: {deploymentName}"); +// Simulated hotel data for Seattle +var seattleHotels = new[] +{ + new Hotel("Contoso Suites", 189, 4.5, "Downtown"), + new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"), + new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"), + new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"), + new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"), + new Hotel("Relecloud Hotel", 99, 3.8, "University District"), +}; + +[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")] +string GetAvailableHotels( + [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, + [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, + [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) +{ + try + { + // 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."; + } + + 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."; + } + + // Validate dates + if (checkOut <= checkIn) + { + return "Error: Check-out date must be after check-in date."; + } + + var nights = (checkOut - checkIn).Days; + + // Filter hotels by price + var availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); + + if (availableHotels.Count == 0) + { + return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; + } + + // Build response + var result = new StringBuilder(); + result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); + result.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(); + } + + return result.ToString(); + } + catch (Exception ex) + { + return $"Error processing request. Details: {ex.Message}"; + } +} + +// 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 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: + 1. Ask for their check-in and check-out dates if not provided + 2. Ask about their budget preferences if not mentioned + 3. Use the GetAvailableHotels tool to find available options + 4. Present the results in a friendly, informative way + 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)]); + +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 +internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md new file mode 100644 index 0000000000..31f3fc1a9d --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md @@ -0,0 +1,167 @@ +**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). + +Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. + +Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. + +Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. + +# What this sample demonstrates + +This sample demonstrates a **key advantage of code-based hosted agents**: + +- **Local C# tool execution** - Run custom C# methods as agent tools + +Code-based agents can execute **any C# code** you write. This sample includes a Seattle Hotel Agent with a `GetAvailableHotels` tool that searches for available hotels based on check-in/check-out dates and budget preferences. + +The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme) and can be deployed to Microsoft Foundry. + +## How It Works + +### Local Tools Integration + +In [Program.cs](Program.cs), the agent uses `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package to create a Foundry agent with a local C# method (`GetAvailableHotels`) that simulates a hotel availability API. This demonstrates how code-based agents can execute custom server-side logic that prompt agents cannot access. + +The tool accepts: + +- **checkInDate** - Check-in date in YYYY-MM-DD format +- **checkOutDate** - Check-out date in YYYY-MM-DD format +- **maxPrice** - Maximum price per night in USD (optional, defaults to $500) + +### Agent Hosting + +The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme), +which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Running the Agent Locally + +### Prerequisites + +Before running this sample, ensure you have: + +1. **Azure AI Foundry Project** + - Project created. + - Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`) + - Note your project endpoint URL and model deployment name + +2. **Azure CLI** + - Installed and authenticated + - Run `az login` and verify with `az account show` + - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) + +3. **.NET 10.0 SDK or later** + - Verify your version: `dotnet --version` + - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) + +### Environment Variables + +Set the following environment variables (matching `agent.yaml`): + +- `AZURE_AI_PROJECT_ENDPOINT` - Your Azure AI Foundry project endpoint URL (required) +- `MODEL_DEPLOYMENT_NAME` - The deployment name for your chat model (defaults to `gpt-4o-mini`) + +**PowerShell:** + +```powershell +# Replace with your actual values +$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +**Bash:** + +```bash +export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +### Running the Sample + +To run the agent, execute the following command in your terminal: + +```bash +dotnet restore +dotnet build +dotnet run +``` + +This will start the hosted agent locally on `http://localhost:8088/`. + +### Interacting with the Agent + +**VS Code:** + +1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. +2. Execute the following commands to start the containerized hosted agent. + + ```bash + dotnet restore + dotnet build + dotnet run + ``` + +3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night." +4. The agent will use the GetAvailableHotels tool to search for available hotels matching your criteria. + +> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. + +**PowerShell (Windows):** + +```powershell +$body = @{ + input = "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under `$200 per night" + stream = $false +} | ConvertTo-Json + +Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" +``` + +**Bash/curl (Linux/macOS):** + +```bash +curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ + -d '{"input": "Find me hotels in Seattle for March 20-23, 2025 under $200 per night","stream":false}' +``` + +You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. + +The agent will use the `GetAvailableHotels` tool to search for available hotels matching your criteria. + +## Deploying the Agent to Microsoft Foundry + +**Preparation (required)** + +Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. + +To deploy the hosted agent: + +1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. +2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. +3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. + +**What the deploy flow does for you:** + +- Creates or obtains an Azure Container Registry for the target project. +- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). +- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). +- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. + +## MSI Configuration in the Azure Portal + +This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. + +To configure the Managed Identity: + +1. In the Azure Portal, open the Foundry Project. +2. Select "Access control (IAM)" from the left-hand menu. +3. Click "Add" and choose "Add role assignment". +4. In the role selection, search for and select "Azure AI User", then click "Next". +5. For "Assign access to", choose "Managed identity". +6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". +7. Click "Review + assign" to complete the assignment. +8. Allow a few minutes for the role assignment to propagate before running the application. + +## Additional Resources + +- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) +- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml new file mode 100644 index 0000000000..100defd112 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml + +name: FoundrySingleAgent +displayName: "Foundry Single Agent with Local Tools" +description: > + A travel assistant agent that helps users find hotels in Seattle. + Demonstrates local C# tool execution - a key advantage of code-based + hosted agents over prompt agents. +metadata: + authors: + - Microsoft Agent Framework Team + tags: + - Azure AI AgentServer + - Microsoft Agent Framework + - Local Tools + - Travel Assistant + - Hotel Search +template: + kind: hosted + name: FoundrySingleAgent + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: MODEL_DEPLOYMENT_NAME + value: gpt-4o-mini +resources: + - name: "gpt-4o-mini" + kind: model + id: gpt-4o-mini \ No newline at end of file diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http new file mode 100644 index 0000000000..4f2e87e097 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http @@ -0,0 +1,52 @@ +@host = http://localhost:8088 +@endpoint = {{host}}/responses + +### Health Check +GET {{host}}/readiness + +### Simple hotel search - budget under $200 +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night", + "stream": false +} + +### Hotel search with higher budget +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "Find me hotels in Seattle for March 20-23, 2025 under $250 per night", + "stream": false +} + +### Ask for recommendations without dates (agent should ask for clarification) +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "What hotels do you recommend in Seattle?", + "stream": false +} + +### Explicit input format +POST {{endpoint}} +Content-Type: application/json + +{ + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "I'm looking for a hotel in Seattle from 2025-04-01 to 2025-04-05, my budget is $150 per night maximum" + } + ] + } + ], + "stream": false +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/README.md b/dotnet/samples/05-end-to-end/HostedAgents/README.md index f7a3bdc94b..f2d32f3c4d 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/README.md +++ b/dotnet/samples/05-end-to-end/HostedAgents/README.md @@ -12,6 +12,8 @@ These samples demonstrate how to build and host AI agents using the [Azure AI Ag | [`AgentWithHostedMCP`](./AgentWithHostedMCP/) | Hosted MCP server tool (Microsoft Learn search) | | [`AgentWithTextSearchRag`](./AgentWithTextSearchRag/) | RAG with `TextSearchProvider` (Contoso Outdoors) | | [`AgentsInWorkflows`](./AgentsInWorkflows/) | Sequential workflow pipeline (translation chain) | +| [`FoundryMultiAgent`](./FoundryMultiAgent/) | Multi-agent Writer-Reviewer workflow using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | +| [`FoundrySingleAgent`](./FoundrySingleAgent/) | Single agent with local C# tool execution (hotel search) using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | ## Common Prerequisites @@ -38,9 +40,9 @@ Most samples require one or more of these environment variables: |----------|---------|-------------| | `AZURE_OPENAI_ENDPOINT` | Most samples | Your Azure OpenAI resource endpoint URL | | `AZURE_OPENAI_DEPLOYMENT_NAME` | Most samples | Chat model deployment name (defaults to `gpt-4o-mini`) | -| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools | Azure AI Foundry project endpoint | +| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Azure AI Foundry project endpoint | | `MCP_TOOL_CONNECTION_ID` | AgentWithTools | Foundry MCP tool connection name | -| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools | Chat model deployment name (defaults to `gpt-4o-mini`) | +| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Chat model deployment name (defaults to `gpt-4o-mini`) | See each sample's README for the specific variables required. diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs index 4c0aeef5bb..40e79f8af5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs @@ -133,7 +133,7 @@ private async Task TestWorkflowEndToEndActivitiesAsync(string executionEnvironme activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event"); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync() { await this.TestWorkflowEndToEndActivitiesAsync("Default");