From 9b166f0e72215c50ee1214d07de7f2188958c989 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:13:30 +0100 Subject: [PATCH 1/7] .NET: Migrate 6 hosted-agent samples to source (ZIP) deploy Extend the source (ZIP) deploy pattern established for Hosted-ChatClientAgent to Hosted-LocalTools, Hosted-Workflow-Simple, Hosted-TextRag, Hosted-Observability, Hosted-Files and Hosted-FoundryAgent. Each gains an azure.yaml (codeConfiguration/remote_build, ASPNETCORE_URLS, model env) and the canonical .agentignore, a self-contained csproj (single target, CPM opt-out, explicit published package versions, AgentFrameworkVersion), a Program.cs that drops the shared contributor scaffolding for DefaultAzureCredential, an updated .env.example and README, and drops the container-mode files (agent.yaml, agent.manifest.yaml, Dockerfile, Dockerfile.contributor). LocalTools, Workflow-Simple, TextRag, Observability and Files were verified deploying live via remote_build; Workflow-Simple returns a workflow runtime error at invoke that is unrelated to the deploy mode. --- .../responses/Hosted-Files/.agentignore | 30 +++ .../responses/Hosted-Files/.dockerignore | 6 - .../responses/Hosted-Files/.env.example | 18 +- .../responses/Hosted-Files/Dockerfile | 17 -- .../Hosted-Files/Dockerfile.contributor | 19 -- .../responses/Hosted-Files/HostedFiles.csproj | 52 ++-- .../responses/Hosted-Files/Program.cs | 103 +++----- .../responses/Hosted-Files/README.md | 235 +++++++++++------- .../Hosted-Files/agent.manifest.yaml | 30 --- .../responses/Hosted-Files/agent.yaml | 9 - .../responses/Hosted-Files/azure.yaml | 40 +++ .../Hosted-FoundryAgent/.agentignore | 30 +++ .../Hosted-FoundryAgent/.env.example | 15 +- .../responses/Hosted-FoundryAgent/Dockerfile | 17 -- .../Dockerfile.contributor | 19 -- .../HostedFoundryAgent.csproj | 52 ++-- .../responses/Hosted-FoundryAgent/Program.cs | 39 ++- .../responses/Hosted-FoundryAgent/README.md | 204 ++++++++------- .../Hosted-FoundryAgent/agent.manifest.yaml | 28 --- .../responses/Hosted-FoundryAgent/agent.yaml | 9 - .../responses/Hosted-FoundryAgent/azure.yaml | 39 +++ .../responses/Hosted-LocalTools/.agentignore | 30 +++ .../responses/Hosted-LocalTools/.env.example | 18 +- .../responses/Hosted-LocalTools/Dockerfile | 17 -- .../Hosted-LocalTools/Dockerfile.contributor | 19 -- .../Hosted-LocalTools/HostedLocalTools.csproj | 56 +++-- .../responses/Hosted-LocalTools/Program.cs | 50 ++-- .../responses/Hosted-LocalTools/README.md | 219 ++++++++++------ .../Hosted-LocalTools/agent.manifest.yaml | 29 --- .../responses/Hosted-LocalTools/agent.yaml | 9 - .../responses/Hosted-LocalTools/azure.yaml | 46 ++++ .../Hosted-Observability/.agentignore | 30 +++ .../Hosted-Observability/.dockerignore | 6 - .../Hosted-Observability/.env.example | 23 +- .../responses/Hosted-Observability/Dockerfile | 17 -- .../Dockerfile.contributor | 19 -- .../HostedObservability.csproj | 52 ++-- .../responses/Hosted-Observability/Program.cs | 47 ++-- .../responses/Hosted-Observability/README.md | 214 ++++++++++------ .../Hosted-Observability/agent.manifest.yaml | 34 --- .../responses/Hosted-Observability/agent.yaml | 14 -- .../responses/Hosted-Observability/azure.yaml | 41 +++ .../responses/Hosted-TextRag/.agentignore | 30 +++ .../responses/Hosted-TextRag/.env.example | 18 +- .../responses/Hosted-TextRag/Dockerfile | 17 -- .../Hosted-TextRag/Dockerfile.contributor | 19 -- .../Hosted-TextRag/HostedTextRag.csproj | 54 ++-- .../responses/Hosted-TextRag/Program.cs | 49 ++-- .../responses/Hosted-TextRag/README.md | 207 +++++++++------ .../Hosted-TextRag/agent.manifest.yaml | 30 --- .../responses/Hosted-TextRag/agent.yaml | 9 - .../responses/Hosted-TextRag/azure.yaml | 40 +++ .../Hosted-Workflow-Simple/.agentignore | 30 +++ .../Hosted-Workflow-Simple/.env.example | 18 +- .../Hosted-Workflow-Simple/Dockerfile | 17 -- .../Dockerfile.contributor | 18 -- .../HostedWorkflowSimple.csproj | 56 +++-- .../Hosted-Workflow-Simple/Program.cs | 51 ++-- .../Hosted-Workflow-Simple/README.md | 204 +++++++++------ .../agent.manifest.yaml | 29 --- .../Hosted-Workflow-Simple/agent.yaml | 9 - .../Hosted-Workflow-Simple/azure.yaml | 40 +++ 62 files changed, 1645 insertions(+), 1301 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.dockerignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/azure.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore deleted file mode 100644 index cf85b06faa..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -**/bin -**/obj -**/.vs -**/.vscode -.env -*.user diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example index 04335e65b8..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example @@ -1,5 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile deleted file mode 100644 index 82f5e1b85c..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedFiles.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor deleted file mode 100644 index 7a34f9361d..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-files . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-files -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-files -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedFiles.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj index f1abd849d2..e983585e10 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj @@ -1,19 +1,41 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedFiles HostedFiles - $(NoWarn); + 204facfe-a494-4273-a330-3ffa649bb789 + 1.15.0-preview.260722.1 - - - + + + + + @@ -24,18 +46,6 @@ - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs index 83837a076e..e230749792 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs @@ -9,19 +9,18 @@ // // * Session files (per-session $HOME volume) — files uploaded at runtime via the alpha // Azure.AI.Projects.AgentSessionFiles SDK. Live at $HOME inside the per-session -// container, which the platform sets to /home/session by default -// (container-image-spec.md line 127, "If you use the session files API, $HOME is -// also the base path for those operations"). +// container, which the platform sets to /home/session by default. // // Each source is exposed via a separate tool pair, each rooted at its own directory. // Tools take a fileName, not a path: Path.GetFileName strips any directory components, -// then a canonicalize + StartsWith(root) check enforces the boundary. The model cannot -// be tricked into reading /etc/passwd or any path outside its tool's root, even via -// indirect prompt injection in an uploaded file. +// then a canonicalize + StartsWith(root) check enforces the boundary. +// +// This sample is deployed to Foundry directly from source (code / ZIP upload), so the +// platform builds and runs your code with no container image. // // Required environment variables: -// FOUNDRY_PROJECT_ENDPOINT - Foundry project endpoint -// FOUNDRY_MODEL - Model deployment name (default: gpt-4o) +// FOUNDRY_PROJECT_ENDPOINT - Foundry project endpoint +// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o) // // Optional: // AGENT_NAME - Agent name (default: hosted-files) @@ -32,46 +31,46 @@ using System.ComponentModel; using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -// Bypass SampleEnvironment alias (which prompts on missing env vars) for optional values. -string? GetOptionalEnv(string key) => System.Environment.GetEnvironmentVariable(key); - -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = GetOptionalEnv("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); + +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-files"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +var credential = new DefaultAzureCredential(); // ── File roots (canonicalized once) ────────────────────────────────────────── // Bundled root: where csproj lands at runtime. // In the container that resolves to /app/resources/. string bundledRoot = Path.GetFullPath( - GetOptionalEnv("BUNDLED_FILES_DIR") + System.Environment.GetEnvironmentVariable("BUNDLED_FILES_DIR") ?? Path.Combine(AppContext.BaseDirectory, "resources")); // Session root: the per-session $HOME volume mounted by the Foundry platform. -// Files uploaded via AgentSessionFiles.UploadSessionFileAsync(sessionStoragePath: "foo") -// land at $HOME/foo per container-image-spec.md line 172. string sessionRoot = Path.GetFullPath( - GetOptionalEnv("HOME") + System.Environment.GetEnvironmentVariable("HOME") ?? "/home/session"); // ── Tools: bundled files (image-baked, /app/resources/) ────────────────────── @@ -106,7 +105,7 @@ string SafeListNames(string root) } return string.Join( - Environment.NewLine, + System.Environment.NewLine, Directory.EnumerateFiles(root).Select(Path.GetFileName)); } catch (Exception ex) @@ -167,7 +166,7 @@ string SafeRead(string root, string fileName, string scope) both first. Always read the file before answering; do not guess. Quote numbers and figures verbatim from the file. """, - name: GetOptionalEnv("AGENT_NAME") ?? "hosted-files", + name: agentName, description: "Hosted agent that answers questions over bundled (image-baked) and session-uploaded files via two scoped tool pairs.", tools: [ @@ -177,51 +176,15 @@ numbers and figures verbatim from the file. AIFunctionFactory.Create(ReadSessionFile), ]); +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); -/// -/// A for local Docker debugging only. -/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable -/// once at startup. This should NOT be used in production. -/// -/// Generate a token on your host and pass it to the container: -/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ... -/// -internal sealed class DevTemporaryTokenCredential : TokenCredential -{ - private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; - private readonly string? _token; - - public DevTemporaryTokenCredential() - { - this._token = System.Environment.GetEnvironmentVariable(EnvironmentVariable); - } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => this.GetAccessToken(); - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(this.GetAccessToken()); - - private AccessToken GetAccessToken() - { - if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") - { - throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); - } - - return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); - } -} +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md index 2ab61b55f2..467cecb234 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md @@ -1,156 +1,201 @@ -# Hosted-Files +# Hosted-Files -A hosted agent that demonstrates **two distinct file knowledge sources** through scoped, security-hardened tools: +A hosted agent that answers questions over two file sources through scoped, path-safe tools: bundled files baked into the upload (read from /app/resources/) and per-session files uploaded at runtime (read from the session HOME volume). -- **Bundled files** (image-baked) — files the author packages with the agent at build time. Live at `/app/resources/` inside the container, copied from this project's [`resources/`](./resources/) folder via the csproj `` rule. -- **Session files** (per-session `$HOME` volume) — files the user uploads at runtime via the alpha `Azure.AI.Projects.AgentSessionFiles` SDK. Live at `$HOME` inside the per-session container. The Foundry platform sets `HOME=/home/session` by default and roots the session-files API there per [`container-image-spec.md` line 172](https://github.com/microsoft/foundrysdk-specs/blob/main/specs/agents/hosted_agents/container-spec/docs/container-image-spec.md): *"If you use the session files API, `$HOME` is also the base path for those operations; any paths given in those API endpoints will be relative to `$HOME`."* - -## Tool surface - -Each source is exposed via its own tool pair, rooted at its own directory. The model picks by intent. - -| Tool | Source | Root | -|------|--------|------| -| `ListBundledFiles` | Bundled (image-baked) | `/app/resources/` | -| `ReadBundledFile` | Bundled (image-baked) | `/app/resources/` | -| `ListSessionFiles` | Session-uploaded | `$HOME` (`/home/session`) | -| `ReadSessionFile` | Session-uploaded | `$HOME` (`/home/session`) | - -## Security model — distinct tools, distinct sandboxes - -Each tool takes a `fileName` (no directory components allowed) and enforces three layers of defence inside the implementation: - -1. **`Path.GetFileName(input)`** strips any directory parts from the model-supplied name. `"../../etc/passwd"` becomes `"passwd"`. -2. **`Path.GetFullPath(Combine(root, name))`** canonicalises the path. -3. **`fullPath.StartsWith(root + DirectorySeparatorChar)`** rejects anything that resolves outside the tool's root. - -Failures return a controlled `"File '' not found in ."` rather than throwing or exposing the canonical path. - -This is why the agent has four narrowly-scoped tools instead of a single `ReadFile(path)`: - -- **Smaller per-tool attack surface.** Each tool has one purpose, one root, and no path-typed parameter. Even a buggy implementation can only leak its own directory. -- **Cross-boundary access is impossible by schema.** A prompt-injection attempt to make the bundled tool read a session path (or vice versa) does not even compile in the tool schema the model sees. -- **Read-only, non-recursive listing.** No write tools, no glob, no `..`. - -## Companion - -[`Using-Samples/SessionFilesClient`](../Using-Samples/SessionFilesClient/) — a thin chat REPL (same shape as [`SimpleAgent`](../Using-Samples/SimpleAgent/)) that points at the deployed Hosted-Files endpoint via `FoundryAgent` and lets you ask questions whose answers come from either file source. - -## Live proof of the session-files contract - -The end-to-end alpha-SDK round trip (client uploads via `AgentSessionFiles.UploadSessionFileAsync` → file arrives at `$HOME/` inside the per-session container → agent's `ReadSessionFile` tool reads it → response quotes the verbatim contents) is exercised live by [`SessionFilesHostedAgentTests.UploadedFile_IsReadByHostedAgentAsync`](../../../../../tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs) against the matching `session-files` scenario in the integration test container. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: exposes scoped, path-safe tools over bundled and session files, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedFiles.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env`: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o +AZURE_TOKEN_CREDENTIALS=dev ``` > `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -```bash +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. + +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files -AGENT_NAME=hosted-files dotnet run +az login +dotnet run ``` The agent starts on `http://localhost:8088`. -## Try it from the SessionFilesClient REPL +**Terminal 2 — chat with it (code-first REPL):** -### Bundled files (works against any deployment, including local) +PowerShell: -```bash -cd ../Using-Samples/SessionFilesClient -$env:AGENT_ENDPOINT = "http://localhost:8088" -$env:AGENT_NAME = "hosted-files" -dotnet run - -You> What is the total revenue in the contoso file? -Agent> The contoso file reports total revenue of "$1,482.6M". +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-files" +dotnet run -- --local ``` -The agent calls `ListBundledFiles`, sees `contoso_q1_2026_report.txt`, calls `ReadBundledFile("contoso_q1_2026_report.txt")` (which resolves under `/app/resources/`), and quotes the figure verbatim. +Bash: -### Session files (against a deployed agent) +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-files" +dotnet run -- --local +``` -Upload a file to a specific session via `azd ai agent files upload` or via the alpha `AgentSessionFiles` SDK (see the integration test for the SDK call), then ask the agent about it. The agent's `ReadSessionFile` tool reads from `$HOME` and surfaces the content the same way. +Try: `List the bundled files and summarize the Contoso Q1 2026 report.` -## Running with Docker +## Deploy to Foundry (source / ZIP) -This project uses `ProjectReference`, so use `Dockerfile.contributor` which takes a pre-published output: +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -docker build -f Dockerfile.contributor -t hosted-files . +### Step 1: create the working directory and enter it + +PowerShell: -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-files \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-files +```powershell +$work = Join-Path $env:TEMP "hosted-files-work" +mkdir $work +cd $work ``` -The bundled `resources/` folder is part of the published output and ships inside the image. +### Step 2: scaffold the project -## Deploying to Foundry (azd spec) +`azd ai agent init` copies the sample into a subfolder named `hosted-files` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -Initialize an `azd` project from this sample's manifest: +PowerShell: -```bash -mkdir hosted-files && cd hosted-files -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -Then deploy: +### Step 3: provision and deploy -```bash +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-files +azd provision azd deploy +azd ai agent invoke "List the bundled files and summarize the Contoso Q1 2026 report." ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -```bash -azd env set AGENT_NAME hosted-files -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +### Step 4: clean up + +``` +azd down +``` + +Then delete the working directory. + +## Deploy your local framework changes (contributors) + +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. + +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: + +PowerShell: + +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-files ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +Bash: ---- +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-files +``` -## NuGet package users +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. -If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor` and switch the `ProjectReference` entries in `HostedFiles.csproj` to `PackageReference` (commented section in the csproj). +## Troubleshooting -## Adding more bundled files +**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -Drop additional text files into [`resources/`](./resources/). The csproj `` rule picks them up on the next `dotnet build` / `docker build`. +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: -## Overrides +``` +azd ai agent invoke --new-conversation "Hello!" +``` -| Env var | Purpose | Default | -|---------|---------|---------| -| `BUNDLED_FILES_DIR` | Override the bundled-files root the tools read from. | `/resources` (`/app/resources/` in container) | -| `HOME` | The per-session sandbox volume root the session-files tools read from. Set by the Foundry platform; can be overridden for local testing. | `/home/session` | +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml deleted file mode 100644 index cccf64b828..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-files -displayName: "Hosted Files Agent" - -description: > - A hosted agent that answers questions over a small set of files bundled - with its container image (under /app/resources/). Two local C# function - tools (ListFiles, ReadFile) surface the bundled file contents to the model. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Bundled Files - - Local Tools - - Agent Framework - -template: - name: hosted-files - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml deleted file mode 100644 index 96dccbcf54..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-files -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml new file mode 100644 index 0000000000..6d55c7c7dd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-files +services: + ai-project: + host: azure.ai.project + hosted-files: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedFiles.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent that answers questions over bundled (image-baked) and session-uploaded files via scoped, path-safe tools. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-files + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example index aaeb71a9e4..9c2b83e0a7 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example @@ -1,5 +1,14 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -AGENT_NAME= -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile deleted file mode 100644 index eda1f7e1e9..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor deleted file mode 100644 index 2b6a2dbbc4..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-foundry-agent . -# docker run --rm -p 8088:8088 -e AGENT_NAME= -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-foundry-agent -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj index 9343bf4a4b..4ca073ff47 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -1,33 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedFoundryAgent HostedFoundryAgent - $(NoWarn); + 9c3949a1-1d2f-453e-abeb-0643b1b95e88 + 1.15.0-preview.260722.1 - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs index 9ca5a28762..5a4f5aa41e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs @@ -1,32 +1,34 @@ // Copyright (c) Microsoft. All rights reserved. +// Hosted Foundry Agent - wraps an existing Foundry-managed (prompt) agent definition and serves it +// over the Responses protocol as a hosted agent. The managed agent is retrieved by name. It is +// deployed to Foundry directly from source (code / ZIP upload), so the platform builds and runs your +// code with no container image. + using Azure.AI.Projects; using Azure.AI.Projects.Agents; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI.Foundry; using Microsoft.Agents.AI.Foundry.Hosting; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.")); -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// The Foundry-managed agent to wrap, by name. When hosted, the platform injects AGENT_NAME as this +// hosted agent's own name, so the hosted agent and the managed agent it wraps share a name. +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); -var aiProjectClient = new AIProjectClient(projectEndpoint, credential); +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); // Retrieve the Foundry-managed agent by name (latest version). ProjectsAgentRecord agentRecord = await aiProjectClient @@ -34,16 +36,11 @@ FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord); -// Host the agent as a Foundry Hosted Agent using the Responses API. +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md index 63bad68348..cbd2bb59eb 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md @@ -1,161 +1,197 @@ -# Hosted-FoundryAgent +# Hosted-FoundryAgent -A hosted agent that delegates to a **Foundry-managed agent definition**. Instead of defining the model, instructions, and tools inline in code, this sample retrieves an existing agent registered in the Foundry platform via `AIProjectClient.AsAIAgent(agentRecord)` and hosts it using the Responses protocol. +A hosted agent that wraps an existing Foundry-managed (prompt) agent definition, retrieves it by name, and serves it over the Responses protocol. Use this when you already have a prompt agent in your project and want to host it with the Agent Framework hosting pipeline. -This is the **Foundry hosting** pattern — the agent's behavior is configured in the platform (via Foundry UI, CLI, or API), and this server simply wraps and serves it. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a **registered agent** (created via Foundry UI, CLI, or API) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: retrieves an existing Foundry-managed agent by name and hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedFoundryAgent.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -You also need to set `AGENT_NAME` — the name of the Foundry-managed agent to host. This is injected automatically by the Foundry platform when deployed. For local development, pass it as an environment variable. +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -## Running directly (contributors) +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -This project uses `ProjectReference` to build against the local Agent Framework source. +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -```bash +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent -AGENT_NAME= dotnet run +az login +dotnet run ``` -The agent will start on `http://localhost:8088`. +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Using the Azure Developer CLI: +PowerShell: -```bash -azd ai agent invoke --local "Hello!" +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-foundry-agent" +dotnet run -- --local ``` -Or with curl (specifying the agent name explicitly): +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "Hello!", "model": ""}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-foundry-agent" +dotnet run -- --local ``` -## Running with Docker +Try: `Hello!` -Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output. +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime (Linux Alpine) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-foundry-agent . +```powershell +$work = Join-Path $env:TEMP "hosted-foundry-agent-work" +mkdir $work +cd $work ``` -### 3. Run the container +### Step 2: scaffold the project -Generate a bearer token on your host and pass it to the container: +`azd ai agent init` copies the sample into a subfolder named `hosted-foundry-agent` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -```bash -# Generate token (expires in ~1 hour) -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -# Run with token -docker run --rm -p 8088:8088 \ - -e AGENT_NAME= \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-foundry-agent -``` -> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration. +PowerShell: -### 4. Test it +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml" -Using the Azure Developer CLI: - -```bash -azd ai agent invoke --local "Hello!" +azd auth login +azd ai agent init -m $sample ``` -Or with curl (specifying the agent name explicitly): +### Step 3: provision and deploy -```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "Hello!", "model": ""}' -``` +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. -## Deploying to Foundry (azd spec) +``` +cd hosted-foundry-agent +azd provision +azd deploy +azd ai agent invoke "Hello!" +``` -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -Initialize an `azd` project from this sample's manifest: +### Step 4: clean up -```bash -mkdir hosted-foundry-agent && cd hosted-foundry-agent -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml +``` +azd down ``` -Then deploy: +Then delete the working directory. -```bash -azd deploy -``` +## Deploy your local framework changes (contributors) -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -```bash -azd env set AGENT_NAME hosted-foundry-agent -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o -``` +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +PowerShell: -## NuGet package users +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-foundry-agent +``` -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative. +Bash: -## How it differs from Hosted-ChatClientAgent +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-foundry-agent +``` -| | Hosted-ChatClientAgent | Hosted-FoundryAgent | -|---|---|---| -| **Agent definition** | Inline in code (`AsAIAgent(model, instructions)`) | Managed in Foundry platform (`AsAIAgent(agentRecord)`) | -| **Model/instructions** | Set in `Program.cs` | Set in Foundry UI/CLI/API | -| **Tools** | Defined in code | Configured in the platform | -| **Use case** | Full control over agent behavior | Platform-managed agent with centralized config | +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml deleted file mode 100644 index c7e46b4d78..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-foundry-agent -displayName: "Hosted Foundry Agent" - -description: > - A simple general-purpose AI assistant hosted as a Foundry Hosted Agent, - backed by a Foundry-managed agent definition. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Streaming - - Agent Framework - -template: - name: hosted-foundry-agent - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml deleted file mode 100644 index 732a4a7e2e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-foundry-agent -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml new file mode 100644 index 0000000000..4b839833d9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-foundry-agent +services: + ai-project: + host: azure.ai.project + hosted-foundry-agent: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedFoundryAgent.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent backed by an existing Foundry-managed (prompt) agent definition, retrieved by name and served over the Responses protocol. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-foundry-agent + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example index 04335e65b8..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example @@ -1,5 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile deleted file mode 100644 index 1b72fcd93f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedLocalTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor deleted file mode 100644 index 65f920824a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-local-tools . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-tools -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-tools -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedLocalTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj index ee301430fb..b3b353e321 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj @@ -1,33 +1,47 @@ - + + + + + + false + + + - net10.0 + + net10.0 + enable enable - false HostedLocalTools HostedLocalTools - $(NoWarn); + 682a0481-5544-45e7-ad2b-88e335985c64 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs index d2671ddb19..174df75bf9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs @@ -1,36 +1,36 @@ // Copyright (c) Microsoft. All rights reserved. -// Seattle Hotel Agent - A hosted agent with local C# function tools. -// Demonstrates how to define and wire local tools that the LLM can invoke, -// a key advantage of code-based hosted agents over prompt agents. +// Seattle Hotel Agent - a hosted agent with local C# function tools. Demonstrates how to define +// and wire local tools that the LLM can invoke, a key advantage of code-based hosted agents over +// prompt agents. It is deployed to Foundry directly from source (code / ZIP upload), so the +// platform builds and runs your code with no container image. using System.ComponentModel; using System.Globalization; using System.Text; using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); + +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools"; // ── Hotel data ─────────────────────────────────────────────────────────────── @@ -94,7 +94,11 @@ string GetAvailableHotels( // ── Create and host the agent ──────────────────────────────────────────────── -AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) .AsAIAgent( model: deploymentName, instructions: """ @@ -110,23 +114,23 @@ 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. """, - name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools", + name: agentName, description: "Seattle hotel search agent with local function tools", tools: [AIFunctionFactory.Create(GetAvailableHotels)]); +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; + // ── Types ──────────────────────────────────────────────────────────────────── internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md index 95718065f3..6a368a8da5 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md @@ -1,155 +1,222 @@ -# Hosted-LocalTools +# Hosted-LocalTools -A hosted agent with **local C# function tools** for hotel search. Demonstrates how to define and wire local tools that the LLM can invoke — a key advantage of code-based hosted agents over prompt agents. +A Seattle hotel search agent hosted as a Foundry Hosted Agent using the Responses protocol. The agent is created inline via `AIProjectClient.AsAIAgent(...)` and given a local C# function tool (`GetAvailableHotels`) that the model can invoke, a key advantage of code-based hosted agents over prompt agents. It is served with `AddFoundryResponses` / `MapFoundryResponses`. -The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` tool that searches a mock hotel database by dates and budget. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: defines the `GetAvailableHotels` tool, builds the agent, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through `env`. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedLocalTools.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -This project uses `ProjectReference` to build against the local Agent Framework source. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character "»" in variable name` when a mark +> is present. PowerShell's `Set-Content -Encoding UTF8BOM` adds one; use `-Encoding utf8NoBOM`. -```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools -AGENT_NAME=hosted-local-tools dotnet run -``` +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential` (the pattern the hosted platform +> expects, where a managed identity is injected). On a developer machine with no managed identity, +> `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, `169.254.169.254`) and +> blocks for a long time on the network timeout before every model call, so requests appear to +> hang. Setting `AZURE_TOKEN_CREDENTIALS=dev` restricts `DefaultAzureCredential` to developer +> credentials (Azure CLI, Visual Studio, `azd`) and skips the managed-identity probe. This variable +> is only for local runs; the deployed agent in Foundry uses the platform-injected managed identity. -The agent will start on `http://localhost:8088`. +## Run and test locally -### Test it +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it +using Agent Framework components, see the sibling [`Using-Samples`](../Using-Samples/) REPLs. -Using the Azure Developer CLI: +**Terminal 1 — host the agent:** -```bash -azd ai agent invoke --local "Find me a hotel in Seattle for Dec 20-25 under $200/night" ``` - -Or with curl: - -```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "Find me a hotel in Seattle for Dec 20-25 under $200/night", "model": "hosted-local-tools"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools +az login +dotnet run ``` -## Running with Docker +The agent starts on `http://localhost:8088`. -Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. +**Terminal 2 — chat with it (code-first REPL):** -### 1. Publish for the container runtime (Linux Alpine) +PowerShell: -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-local-tools" +dotnet run -- --local ``` -### 2. Build the Docker image +Bash: ```bash -docker build -f Dockerfile.contributor -t hosted-local-tools . +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-local-tools" +dotnet run -- --local ``` -### 3. Run the container +Try: `Find me hotels in Seattle from 2026-09-01 to 2026-09-03 under $200 per night.` -Generate a bearer token on your host and pass it to the container: +## Deploy to Foundry (source / ZIP) -```bash -# Generate token (expires in ~1 hour) -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -# Run with token -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-local-tools \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-local-tools -``` +### Step 1: create the working directory and enter it + +PowerShell: -### 4. Test it +```powershell +$work = Join-Path $env:TEMP "hosted-local-tools-work" +mkdir $work +cd $work +``` -Using the Azure Developer CLI: +Bash: ```bash -azd ai agent invoke --local "What hotels are available in Seattle for next weekend?" +WORK="${TMPDIR:-/tmp}/hosted-local-tools-work" +mkdir -p "$WORK" +cd "$WORK" ``` -## How local tools work +### Step 2: scaffold the project -The agent has a single tool `GetAvailableHotels` defined as a C# method with `[Description]` attributes. The LLM decides when to call it based on the user's request: +`azd ai agent init` copies the sample into a subfolder named after the top-level `name:` in +`azure.yaml`, which is `hosted-local-tools`, and writes the adopted `azure.yaml` and the `azd` +environment there. It prompts you to pick the Foundry project; `-d` is the name of an existing +model deployment in that project. -| Parameter | Type | Description | -|-----------|------|-------------| -| `checkInDate` | string | Check-in date (YYYY-MM-DD) | -| `checkOutDate` | string | Check-out date (YYYY-MM-DD) | -| `maxPrice` | int | Max price per night in USD (default: 500) | +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing. +PowerShell: -## Deploying to Foundry (azd spec) +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml" -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +azd auth login +azd ai agent init -m $sample -d +``` -Initialize an `azd` project from this sample's manifest: +Bash: ```bash -mkdir hosted-local-tools && cd hosted-local-tools -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml +SAMPLE="/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml" + +azd auth login +azd ai agent init -m "$SAMPLE" -d ``` -Then deploy: +### Step 3: provision and deploy -```bash +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-local-tools +azd provision azd deploy +azd ai agent invoke "Find me hotels in Seattle under $200 a night." ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. + +### Step 4: clean up -```bash -azd env set AGENT_NAME hosted-local-tools -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o ``` +azd down +``` + +Then delete the working directory. + +## Deploy your local framework changes (contributors) -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. ---- +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -## NuGet package users +PowerShell: + +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-local-tools +``` + +Bash: + +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-local-tools +``` -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedLocalTools.csproj` for the `PackageReference` alternative. +Then continue with step 3. See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml deleted file mode 100644 index 935d509054..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-local-tools -displayName: "Seattle Hotel 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: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Local Tools - - Agent Framework - -template: - name: hosted-local-tools - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml deleted file mode 100644 index ee253cfb67..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-local-tools -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml new file mode 100644 index 0000000000..4eac0e69e0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml @@ -0,0 +1,46 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-local-tools +services: + ai-project: + host: azure.ai.project + hosted-local-tools: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedLocalTools.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + # + # ${AZURE_AI_MODEL_DEPLOYMENT_NAME} reads the model deployment `azd ai agent init` recorded + # in the active azd environment. Without it the container falls back to the default model + # name hardcoded in Program.cs, which may not exist in the target project. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A Seattle hotel search agent hosted as a Foundry Hosted Agent, demonstrating local C# function tools the model can invoke. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + - Agent Framework + name: hosted-local-tools + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.dockerignore deleted file mode 100644 index 37739c9e09..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -.env -bin/ -obj/ -.vs/ -.vscode/ -*.user diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example index 46900211af..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/.env.example @@ -1,12 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= -ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential -# Capture prompt / completion / tool argument content on GenAI spans. -OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. +ASPNETCORE_URLS=http://+:8088 -# Uncomment and set to send local-run telemetry to Application Insights. -# When the agent runs inside Foundry this value is injected automatically. -#APPLICATIONINSIGHTS_CONNECTION_STRING= +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile deleted file mode 100644 index 61b22468d1..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedObservability.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile.contributor deleted file mode 100644 index 768e01addc..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-observability . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-observability -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-observability -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedObservability.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/HostedObservability.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/HostedObservability.csproj index edd5e4be7d..452b29f12b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/HostedObservability.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/HostedObservability.csproj @@ -1,33 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedObservability HostedObservability - $(NoWarn); + f401dcb9-9636-4c3d-ab4a-4fb68773a1a1 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs index c48e603823..29633c92be 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/Program.cs @@ -3,33 +3,34 @@ // Hosted Observability Agent - demonstrates that the Foundry hosting pipeline // emits OpenTelemetry traces, metrics and logs with no extra wiring required. // Two small tools are included so a request produces a span tree covering -// agent invocation, the chat call, and tool execution. +// agent invocation, the chat call, and tool execution. It is deployed to Foundry +// directly from source (code / ZIP upload), so the platform builds and runs your +// code with no container image. using System.ComponentModel; using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); + +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-observability"; // ── Tools ──────────────────────────────────────────────────────────────────── @@ -51,26 +52,30 @@ string GetWeather( // and the OTLP exporter is registered by Azure.AI.AgentServer.Core's // AddAgentHostTelemetry(). No additional observability wiring is required. -AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) .AsAIAgent( model: deploymentName, instructions: "You are a friendly assistant. Keep your answers brief.", - name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-observability", + name: agentName, description: "A hosted agent that demonstrates Foundry observability.", tools: [ AIFunctionFactory.Create(GetCurrentLocation), AIFunctionFactory.Create(GetWeather), ]); +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); + +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md index 987d768e5e..8e33435b8d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md @@ -1,151 +1,201 @@ -# Hosted-Observability +# Hosted-Observability -A hosted [Agent Framework](https://github.com/microsoft/agent-framework) agent that demonstrates how the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring. +A hosted agent that demonstrates the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring. Two small tools are included so a request produces a span tree covering agent invocation, the chat call, and tool execution. -The agent has two small tools, `GetCurrentLocation` and `GetWeather`, so an end-to-end run produces a span tree covering agent invocation, the underlying chat call, and tool execution. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. -## How it works - -### Instrumentation is on by default +## Prerequisites -Unlike the Python SDK, the .NET hosting library is instrumented by default. `AddFoundryResponses(agent)` automatically wraps the agent with `OpenTelemetryAgent` (see `Microsoft.Agents.AI.Foundry.Hosting.ServiceCollectionExtensions.ApplyOpenTelemetry`) and the OTLP exporter pipeline is registered by `Azure.AI.AgentServer.Core`'s `AddAgentHostTelemetry()`. There is no `ENABLE_INSTRUMENTATION` flag to set. +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. +- Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` -### Sensitive content +## Files -Prompt, completion and tool argument content are omitted from spans by default. Set the OpenTelemetry standard environment variable to capture them: +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: defines two tools, hosts it with the Responses protocol; telemetry is emitted automatically by the hosting pipeline. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedObservability.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | -```env -OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true -``` - -This is the .NET equivalent of the Python sample's `ENABLE_SENSITIVE_DATA`. It is read by `OpenTelemetryAgent.EnableSensitiveData`. +## Configuration -### Where the telemetry goes +Copy the template and fill in your project endpoint: -Foundry injects `APPLICATIONINSIGHTS_CONNECTION_STRING` when the agent runs in the hosted environment, so traces, metrics and logs flow to Application Insights with no code change. To send telemetry from a local run, set the connection string yourself in `.env`. +PowerShell: -## Prerequisites - -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) -- Azure CLI logged in (`az login`) +```powershell +copy .env.example .env +``` -## Configuration +Bash: ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -```bash +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. + +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability -AGENT_NAME=hosted-observability dotnet run +az login +dotnet run ``` The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -```bash -azd ai agent invoke --local "What is the current weather where I am?" +PowerShell: + +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-observability" +dotnet run -- --local ``` -Or with curl: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "What is the current weather where I am?", "model": "hosted-observability"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-observability" +dotnet run -- --local ``` -## Expected span tree - -A single request produces approximately the following spans: +Try: `What is the weather where I am?` -| Span | Source | -|------|--------| -| `invoke_agent` | Outer span emitted by the Azure AI AgentServer hosting SDK | -| `agent_invoke ` | Emitted by `OpenTelemetryAgent` for each agent invocation | -| `chat ` | Emitted by the underlying `IChatClient` for each model call | -| `execute_tool ` | Emitted for each invocation of `GetCurrentLocation` / `GetWeather` | +## Deploy to Foundry (source / ZIP) -See the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for the attributes captured on each span. +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -## Running with Docker +### Step 1: create the working directory and enter it -This project uses `ProjectReference` to the local Agent Framework source, so use `Dockerfile.contributor` with a pre-published output: +PowerShell: -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -docker build -f Dockerfile.contributor -t hosted-observability . - -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-observability \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-observability +```powershell +$work = Join-Path $env:TEMP "hosted-observability-work" +mkdir $work +cd $work ``` -## Deploying to Foundry and viewing traces +### Step 2: scaffold the project -Once deployed, telemetry flows to the Application Insights instance attached to your Foundry project. In the Foundry UI, the **Traces** tab next to **Playground** lists conversations and lets you drill into the span tree for any request. +`azd ai agent init` copies the sample into a subfolder named `hosted-observability` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -## Deploying to Foundry (azd spec) +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +PowerShell: -Initialize an `azd` project from this sample's manifest: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml" -```bash -mkdir hosted-observability && cd hosted-observability -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml +azd auth login +azd ai agent init -m $sample -d ``` -Then deploy: +### Step 3: provision and deploy -```bash +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-observability +azd provision azd deploy +azd ai agent invoke "What is the weather where I am?" ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -```bash -azd env set AGENT_NAME hosted-observability -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +### Step 4: clean up + +``` +azd down ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +Then delete the working directory. + +## Deploy your local framework changes (contributors) + +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. + +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: ---- +PowerShell: -## NuGet package users +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-observability +``` + +Bash: + +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-observability +``` -If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedObservability.csproj` for the `PackageReference` alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml deleted file mode 100644 index 87b6aa55f6..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-observability -displayName: "Hosted Observability Agent" - -description: > - A hosted Agent Framework agent that demonstrates how the Foundry hosting - pipeline emits OpenTelemetry traces, metrics and logs to Application Insights - with no extra wiring required. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Observability - - OpenTelemetry - - Agent Framework - -template: - name: hosted-observability - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi - environment_variables: - # Capture prompt / completion / tool argument content on GenAI spans. - - name: OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT - value: "true" -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.yaml deleted file mode 100644 index 6fd75029b4..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-observability -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi -environment_variables: - # Capture prompt / completion / tool argument content on GenAI spans. - # See https://opentelemetry.io/docs/specs/semconv/gen-ai/ for the standard env var. - - name: OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT - value: "true" diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml new file mode 100644 index 0000000000..d5c7dcc495 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-observability +services: + ai-project: + host: azure.ai.project + hosted-observability: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedObservability.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: true + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent that demonstrates the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-observability + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example index 04335e65b8..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example @@ -1,5 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile deleted file mode 100644 index 062d0f4f7e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedTextRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor deleted file mode 100644 index 9a90c74335..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-text-rag . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-text-rag -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-text-rag -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedTextRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj index 837da626a3..a103967b69 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj @@ -1,35 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedTextRag HostedTextRag - $(NoWarn); + 39b2c04e-bcf6-42d7-aac3-ed5a24a3af75 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs index 492b5d8c87..1eaaf73a91 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs @@ -2,33 +2,33 @@ // This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG) // capabilities to a hosted agent. The provider runs a search against an external knowledge base -// before each model invocation and injects the results into the model context. +// before each model invocation and injects the results into the model context. It is deployed to +// Foundry directly from source (code / ZIP upload), so the platform builds and runs your code with +// no container image. using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; -using OpenAI.Chat; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); + +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-text-rag"; TextSearchProviderOptions textSearchOptions = new() { @@ -36,10 +36,14 @@ RecentMessageMemoryLimit = 6, }; -AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) .AsAIAgent(new ChatClientAgentOptions { - Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-text-rag", + Name = agentName, ChatOptions = new ChatOptions { ModelId = deploymentName, @@ -48,20 +52,19 @@ AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)] }); -// Host the agent as a Foundry Hosted Agent using the Responses API. +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; + // ── Mock search function ───────────────────────────────────────────────────── // In production, replace this with a real search provider (e.g., Azure AI Search). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md index f38a7a4a94..aad4c1a1af 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md @@ -1,158 +1,201 @@ -# Hosted-TextRag +# Hosted-TextRag -A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities using `TextSearchProvider`. The agent grounds its answers in product documentation by running a search before each model invocation, then citing the source in its response. +A retrieval-augmented (RAG) support agent hosted as a Foundry Hosted Agent using the Responses protocol. A TextSearchProvider runs a search against a knowledge base before each model call and injects the results into the context, then the agent is hosted with AddFoundryResponses / MapFoundryResponses. -This sample demonstrates how to add knowledge grounding to a hosted agent without requiring an external search index — using a mock search function that can be replaced with Azure AI Search or any other provider. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: wires a TextSearchProvider (mock search) into the agent context, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedTextRag.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN= +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -This project uses `ProjectReference` to build against the local Agent Framework source. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -```bash +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag -AGENT_NAME=hosted-text-rag dotnet run +az login +dotnet run ``` -The agent will start on `http://localhost:8088`. +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Using the Azure Developer CLI: +PowerShell: -```bash -azd ai agent invoke --local "What is your return policy?" -azd ai agent invoke --local "How long does shipping take?" -azd ai agent invoke --local "How do I clean my tent?" +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-text-rag" +dotnet run -- --local ``` -Or with curl: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "What is your return policy?", "model": "hosted-text-rag"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-text-rag" +dotnet run -- --local ``` -## Running with Docker +Try: `What is your return policy?` -Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime (Linux Alpine) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-text-rag . +```powershell +$work = Join-Path $env:TEMP "hosted-text-rag-work" +mkdir $work +cd $work ``` -### 3. Run the container +### Step 2: scaffold the project -Generate a bearer token on your host and pass it to the container: +`azd ai agent init` copies the sample into a subfolder named `hosted-text-rag` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -```bash -# Generate token (expires in ~1 hour) -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) - -# Run with token -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-text-rag \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-text-rag +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -### 4. Test it +PowerShell: -Using the Azure Developer CLI: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/azure.yaml" -```bash -azd ai agent invoke --local "What is your return policy?" +azd auth login +azd ai agent init -m $sample -d ``` -## How RAG works in this sample +### Step 3: provision and deploy -The `TextSearchProvider` runs a mock search **before each model invocation**: +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. -| User query contains | Search result injected | -|---|---| -| "return" or "refund" | Contoso Outdoors Return Policy | -| "shipping" | Contoso Outdoors Shipping Guide | -| "tent" or "fabric" | TrailRunner Tent Care Instructions | +``` +cd hosted-text-rag +azd provision +azd deploy +azd ai agent invoke "What is your return policy?" +``` -The model receives the search results as additional context and cites the source in its response. In production, replace `MockSearchAsync` with a call to Azure AI Search or your preferred search provider. +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -## Deploying to Foundry (azd spec) +### Step 4: clean up -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +``` +azd down +``` -Initialize an `azd` project from this sample's manifest: +Then delete the working directory. -```bash -mkdir hosted-text-rag && cd hosted-text-rag -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml -``` +## Deploy your local framework changes (contributors) -Then deploy: +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -```bash -azd deploy -``` +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +PowerShell: -```bash -azd env set AGENT_NAME hosted-text-rag -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-text-rag ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). - ---- +Bash: -## NuGet package users +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-text-rag +``` -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedTextRag.csproj` for the `PackageReference` alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml deleted file mode 100644 index 4e8330a025..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-text-rag -displayName: "Hosted Text RAG Agent" - -description: > - A support specialist agent for Contoso Outdoors with RAG capabilities. - Uses TextSearchProvider to ground answers in product documentation - before each model invocation. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - RAG - - Text Search - - Agent Framework - -template: - name: hosted-text-rag - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml deleted file mode 100644 index a73a702d9c..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-text-rag -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/azure.yaml new file mode 100644 index 0000000000..166fe4d08a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/azure.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-text-rag +services: + ai-project: + host: azure.ai.project + hosted-text-rag: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedTextRag.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A retrieval-augmented (RAG) support agent hosted as a Foundry Hosted Agent, using TextSearchProvider to inject search results into the model context before each call. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-text-rag + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example index 04335e65b8..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example @@ -1,5 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile deleted file mode 100644 index 5d6888e222..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedWorkflowSimple.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor deleted file mode 100644 index 17a924237f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor +++ /dev/null @@ -1,18 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local source, which means a standard -# multi-stage Docker build cannot resolve dependencies outside this folder. -# Pre-publish the app targeting the container runtime and copy the output: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-workflow-simple . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-workflow-simple -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflow-simple -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedWorkflowSimple.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj index 7b19925d84..e679a7543e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj @@ -1,37 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedWorkflowSimple HostedWorkflowSimple - $(NoWarn); + 02bc4c01-6d49-46d3-816f-0886f62a520e + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs index 6ed5f33601..662b30c4a4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs @@ -2,37 +2,40 @@ // Translation Chain Workflow Agent — demonstrates how to compose multiple AI agents // into a sequential workflow pipeline. Three translation agents are connected: -// English → French → Spanish → English, showing how agents can be orchestrated -// as workflow executors in a hosted agent. +// French → Spanish → English, showing how agents can be orchestrated as workflow +// executors in a hosted agent. It is deployed to Foundry directly from source +// (code / ZIP upload), so the platform builds and runs your code with no container image. using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); -// Create a chat client from the Foundry project -IChatClient chatClient = new AIProjectClient(new Uri(endpoint), credential) +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-workflow-simple"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +IChatClient chatClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) .GetProjectOpenAIClient() .GetChatClient(deploymentName) .AsIChatClient(); @@ -47,19 +50,17 @@ .AddEdge(frenchAgent, spanishAgent) .AddEdge(spanishAgent, englishAgent) .Build() - .AsAIAgent( - name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-workflows"); + .AsAIAgent(name: agentName); -// Host the workflow agent as a Foundry Hosted Agent using the Responses API. +// Host the workflow agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); + +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md index 643e59f67a..1d4fd09395 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md @@ -1,151 +1,201 @@ -# Hosted-Workflow-Simple +# Hosted-Workflow-Simple -A hosted agent that demonstrates **multi-agent workflow orchestration**. Three translation agents are composed into a sequential pipeline: English → French → Spanish → English, showing how agents can be chained as workflow executors using `WorkflowBuilder`. +A translation-chain workflow hosted as a Foundry Hosted Agent using the Responses protocol. Three translation agents are composed as sequential workflow executors (French then Spanish then English) via WorkflowBuilder, then hosted with AddFoundryResponses / MapFoundryResponses. + +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `hosted-workflow-simple`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: builds three translation agents, composes them into a sequential workflow, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedWorkflowSimple.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=hosted-workflow-simple +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -```bash +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. + +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple -AGENT_NAME=hosted-workflow-simple dotnet run +az login +dotnet run ``` -The agent will start on `http://localhost:8088`. +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Using the Azure Developer CLI: +PowerShell: -```bash -azd ai agent invoke --local "The quick brown fox jumps over the lazy dog" +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-workflow-simple" +dotnet run -- --local ``` -Or with curl: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "The quick brown fox jumps over the lazy dog", "model": "hosted-workflow-simple"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-workflow-simple" +dotnet run -- --local ``` -The text will be translated through the chain: English → French → Spanish → English. +Try: `Translate this text: The weather is beautiful today.` -## Running with Docker +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-workflow-simple . +```powershell +$work = Join-Path $env:TEMP "hosted-workflow-simple-work" +mkdir $work +cd $work ``` -### 3. Run the container +### Step 2: scaffold the project -```bash -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +`azd ai agent init` copies the sample into a subfolder named `hosted-workflow-simple` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-workflow-simple \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-workflow-simple +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -### 4. Test it +PowerShell: -```bash -azd ai agent invoke --local "Hello, how are you today?" +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -## How the workflow works +### Step 3: provision and deploy + +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. ``` -Input text - │ - ▼ -┌─────────────┐ ┌──────────────┐ ┌──────────────┐ -│ French Agent │ → │ Spanish Agent │ → │ English Agent │ -│ (translate) │ │ (translate) │ │ (translate) │ -└─────────────┘ └──────────────┘ └──────────────┘ - │ - ▼ - Final output - (back in English) +cd hosted-workflow-simple +azd provision +azd deploy +azd ai agent invoke "Translate this text: The weather is beautiful today." ``` -Each agent in the chain receives the output of the previous agent. The final result demonstrates how meaning is preserved (or subtly shifted) through multiple translation hops. +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. + +### Step 4: clean up + +``` +azd down +``` -## Deploying to Foundry (azd spec) +Then delete the working directory. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +## Deploy your local framework changes (contributors) -Initialize an `azd` project from this sample's manifest: +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -```bash -mkdir hosted-workflows && cd hosted-workflows -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml -``` +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -Then deploy: +PowerShell: -```bash -azd deploy +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-workflow-simple ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +Bash: ```bash -azd env set AGENT_NAME hosted-workflow-simple -azd env set FOUNDRY_MODEL hosted-workflow-simple +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-workflow-simple ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). - ---- - -## NuGet package users - -Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowSimple.csproj` for the `PackageReference` alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml deleted file mode 100644 index 9b1d3d10ab..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-workflows -displayName: "Translation Chain Workflow Agent" - -description: > - A workflow agent that performs sequential translation through multiple languages. - Translates text from English to French, then to Spanish, and finally back to English, - demonstrating how AI agents can be composed as workflow executors. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Workflows - - Agent Framework - -template: - name: hosted-workflows - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml deleted file mode 100644 index fad8a33470..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-workflow-simple -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/azure.yaml new file mode 100644 index 0000000000..67b02cd4cd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/azure.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-workflow-simple +services: + ai-project: + host: azure.ai.project + hosted-workflow-simple: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedWorkflowSimple.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A translation-chain workflow (English -> French -> Spanish -> English) hosted as a Foundry Hosted Agent, composing multiple agents as workflow executors. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-workflow-simple + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file From d95fda3970f896cd1850c416db91f9b1a7a11bf6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:28:43 +0100 Subject: [PATCH 2/7] .NET: Migrate Hosted-Invocations-EchoAgent and Hosted-LocalCodeAct to source (ZIP) deploy EchoAgent (Invocations protocol) and LocalCodeAct migrated to the zip/code-deploy pattern (azure.yaml, .agentignore, self-contained csproj, README, container files removed). EchoAgent maps /readiness explicitly because the Invocations SDK does not auto-map it. Both verified live via remote_build on a Foundry project; LocalCodeAct's execute_code ran server-side (compute 21+21 -> 42). --- .../Hosted-Invocations-EchoAgent/.agentignore | 30 +++ .../Hosted-Invocations-EchoAgent/.env.example | 5 +- .../Hosted-Invocations-EchoAgent/Dockerfile | 17 -- .../Dockerfile.contributor | 19 -- .../Hosted-Invocations-EchoAgent.csproj | 32 --- .../HostedInvocationsEchoAgent.csproj | 43 ++++ .../Hosted-Invocations-EchoAgent/Program.cs | 5 + .../Hosted-Invocations-EchoAgent/README.md | 133 ++++++----- .../agent.manifest.yaml | 27 --- .../Hosted-Invocations-EchoAgent/agent.yaml | 9 - .../Hosted-Invocations-EchoAgent/azure.yaml | 39 +++ .../Hosted-LocalCodeAct/.agentignore | 30 +++ .../Hosted-LocalCodeAct/.env.example | 19 +- .../responses/Hosted-LocalCodeAct/Dockerfile | 23 -- .../Dockerfile.contributor | 24 -- .../HostedLocalCodeAct.csproj | 57 +++-- .../responses/Hosted-LocalCodeAct/Program.cs | 53 +++-- .../responses/Hosted-LocalCodeAct/README.md | 222 ++++++++++-------- .../Hosted-LocalCodeAct/agent.manifest.yaml | 30 --- .../responses/Hosted-LocalCodeAct/agent.yaml | 9 - .../responses/Hosted-LocalCodeAct/azure.yaml | 40 ++++ 21 files changed, 469 insertions(+), 397 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example index 46a6ae748c..50275877c9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example @@ -1,2 +1,3 @@ -ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +# Foundry project endpoint is not required by this echo sample (no model call). +# Local development only: bind to the readiness port the Foundry runtime probes. +ASPNETCORE_URLS=http://+:8088 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile deleted file mode 100644 index 24585dec12..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor deleted file mode 100644 index 91a403c26c..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Abstractions source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent . -# docker run --rm -p 8088:8088 hosted-invocations-echo-agent -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj deleted file mode 100644 index a0b9e2e0d8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net10.0 - enable - enable - false - HostedInvocationsEchoAgent - HostedInvocationsEchoAgent - $(NoWarn); - - - - - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj new file mode 100644 index 0000000000..32b0ba2065 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj @@ -0,0 +1,43 @@ + + + + + + false + + + + + + net10.0 + + enable + enable + HostedInvocationsEchoAgent + HostedInvocationsEchoAgent + 27e5c7df-546c-477b-ab05-d1e070a1b78a + 1.15.0 + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs index d5944560ae..3b1f53813b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs @@ -19,6 +19,11 @@ var app = builder.Build(); +// The Foundry hosted runtime probes GET /readiness before routing invocations to the container. +// The Invocations SDK does not map that route (unlike the Responses SDK), so map it explicitly; +// without it every invoke fails with HTTP 424 session_not_ready. +app.MapGet("/readiness", () => Results.Ok()); + // Map the Invocations protocol endpoints: // POST /invocations — invoke the agent // GET /invocations/{id} — get result (not used by this sample) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md index 404f88d074..eafb9fb7ca 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md @@ -1,104 +1,113 @@ # Hosted-Invocations-EchoAgent -A minimal echo agent hosted as a Foundry Hosted Agent using the **Invocations protocol**. The agent reads the request body as plain text, passes it through a custom `EchoAIAgent`, and writes the echoed text back in the response. No LLM or Azure credentials are required. +A minimal agent that echoes the user's input back, hosted as a Foundry Hosted Agent over the **Invocations protocol**. No LLM or external service is required, so it is the simplest way to see the hosting pipeline end to end. -## Prerequisites +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +## How it works -## Configuration +- `EchoAIAgent.cs` — a tiny `AIAgent` that returns `Echo: `; no model call. +- `EchoInvocationHandler.cs` — an `InvocationHandler` that reads the request body as plain text, runs the agent, and writes the response back as `text/plain`. +- `Program.cs` — registers the agent and the Invocations SDK (`AddInvocationsServer` / `MapInvocationsServer`), and maps `GET /readiness`. -Copy the template: +> **Readiness note:** unlike the Responses SDK, the Invocations SDK does **not** auto-map the +> `GET /readiness` route the Foundry runtime probes before routing calls. `Program.cs` maps it +> explicitly; without it every invoke fails with HTTP 424 `session_not_ready`. -```bash -cp .env.example .env -``` +## Files -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +| File | Purpose | +|------|---------| +| `Program.cs` | Registers the echo agent and the Invocations server, maps `/readiness`. | +| `EchoAIAgent.cs` | The echo agent (no LLM). | +| `EchoInvocationHandler.cs` | Reads the request body, runs the agent, writes `text/plain`. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy) and the `invocations` protocol. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedInvocationsEchoAgent.csproj` | Self-contained project: single target framework and explicit package versions. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | -## Running directly (contributors) +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An **existing** Foundry project (no model deployment is needed for this echo sample). +- Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` -This project uses `ProjectReference` to build against the local Agent Framework source. +## Run and test locally -```bash +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent dotnet run ``` -The agent will start on `http://localhost:8088`. +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — invoke it** (the Invocations protocol takes a plain-text body and returns `text/plain`): -```bash -curl -X POST http://localhost:8088/invocations \ - -H "Content-Type: text/plain" \ - -d "Hello, world!" -``` +PowerShell: -Expected response: - -``` -Echo: Hello, world! +```powershell +(Invoke-WebRequest -Uri http://localhost:8088/invocations -Method POST -Body "Hello!").Content ``` -## Running with Docker - -Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output. - -### 1. Publish for the container runtime (Linux Alpine) +Bash: ```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +curl -X POST http://localhost:8088/invocations -d "Hello!" ``` -### 2. Build the Docker image +You get back `Echo: Hello!`. -```bash -docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent . -``` +## Deploy to Foundry (source / ZIP) -### 3. Run the container +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -docker run --rm -p 8088:8088 hosted-invocations-echo-agent -``` +```powershell +$work = Join-Path $env:TEMP "hosted-invocations-echo-work" +mkdir $work +cd $work -### 4. Test it +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml" +azd auth login +azd ai agent init -m $sample -```bash -curl -X POST http://localhost:8088/invocations \ - -H "Content-Type: text/plain" \ - -d "Hello from Docker!" +cd hosted-invocations-echo-agent +azd provision +azd deploy ``` -## Deploying to Foundry (azd spec) +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. - -Initialize an `azd` project from this sample's manifest: +Invoke the deployed agent with a bearer token (the Invocations endpoint returns `text/plain`): ```bash -mkdir hosted-invocations-echo-agent && cd hosted-invocations-echo-agent -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml +TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +curl -X POST "/agents/hosted-invocations-echo-agent/endpoint/protocols/invocations?api-version=v1" \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "Hello!" ``` -Then deploy: +Clean up with `azd down`, then delete the working directory. -```bash -azd deploy -``` +## Deploy your local framework changes (contributors) -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload. To ship a local framework build instead, run the helper between `azd ai agent init` and +`azd provision`: -```bash -azd env set AGENT_NAME hosted-invocations-echo-agent -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-invocations-echo-agent ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). - ---- - -## NuGet package users +See the +[`Hosted-ChatClientAgent`](../../responses/Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation. -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `Hosted-Invocations-EchoAgent.csproj` for the `PackageReference` alternative. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml deleted file mode 100644 index 09e4b0f885..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-invocations-echo-agent -displayName: "Hosted Invocations Echo Agent" - -description: > - A minimal echo agent hosted as a Foundry Hosted Agent using the Invocations - protocol. Reads the request body as plain text, echoes it back in the response. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Invocations Protocol - - Agent Framework - -template: - name: hosted-invocations-echo-agent - kind: hosted - protocols: - - protocol: invocations - version: 1.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml deleted file mode 100644 index 001a19f0ac..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-invocations-echo-agent -protocols: - - protocol: invocations - version: 1.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml new file mode 100644 index 0000000000..ac46ae92ba --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-invocations-echo-agent +services: + ai-project: + host: azure.ai.project + hosted-invocations-echo-agent: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedInvocationsEchoAgent.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A minimal agent that echoes the user input, hosted as a Foundry Hosted Agent over the Invocations protocol. No LLM or external service is required. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-invocations-echo-agent + protocols: + - protocol: invocations + version: 1.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example index c5423f93f7..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/.env.example @@ -1,6 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential -LOCAL_CODEACT_PYTHON=python3 + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile deleted file mode 100644 index 1d201d0472..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator. -RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 \ - && rm -rf /var/lib/apt/lists/* - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENV LOCAL_CODEACT_PYTHON=python3 -ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor deleted file mode 100644 index c27fb0ab41..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Dockerfile.contributor +++ /dev/null @@ -1,24 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry and -# Microsoft.Agents.AI.LocalCodeAct sources, which means a standard multi-stage -# Docker build cannot resolve dependencies outside this folder. Instead, pre-publish -# the app targeting the container runtime and copy the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-local-codeact . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-codeact -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-codeact -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator. -RUN apk add --no-cache python3 - -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENV LOCAL_CODEACT_PYTHON=python3 -ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj index 0c2a417ff7..fc6fe1f703 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj @@ -1,33 +1,44 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false - $(NoWarn); + HostedLocalCodeAct + HostedLocalCodeAct + dd574e14-75e7-41f0-8ee6-b7c62d6952cb + 1.17.0-preview.260804.1 - - - + + + + + + - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs index fa1c921fb9..bc53b8d6b1 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/Program.cs @@ -3,36 +3,47 @@ // Hosted Local CodeAct sample. Wires Microsoft.Agents.AI.LocalCodeAct into a // Foundry hosted agent. The model only sees a single `execute_code` tool; // `compute` and `fetch_data` are registered as sandbox-only host tools that -// generated Python reaches via `await call_tool(...)`. This mirrors the Python -// `foundry_hosted_agent.py` sample for the local-codeact package. +// generated Python reaches via `await call_tool(...)`. It is deployed to Foundry +// directly from source (code / ZIP upload), so the platform builds and runs your +// code with no container image. // // SECURITY: LocalCodeAct executes LLM-generated Python in the agent process. // Only deploy this sample to an externally sandboxed environment such as a // Foundry hosted-agent container. +// +// RUNTIME: this sample runs generated Python with a Python interpreter. The +// hosted dotnet_10 runtime image does not ship Python, so the `execute_code` +// tool only works where a Python interpreter is present (for example a local +// run, or a container/base image that includes Python). using System.ComponentModel; using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.LocalCodeAct; using Microsoft.Extensions.AI; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -string pythonExecutable = Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON") - ?? (OperatingSystem.IsWindows() ? "python.exe" : "python3"); -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +// Environment variables can arrive set but blank: azd substitutes an empty string when the azd +// environment does not define the variable referenced from azure.yaml. An empty string is not +// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK. +var deploymentName = FirstNonBlank( + System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"), + System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"), + "gpt-4o"); + +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-codeact"; + +var pythonExecutable = System.Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON") + ?? (OperatingSystem.IsWindows() ? "python.exe" : "python3"); // ── Sandbox-only tools (model never sees these directly) ───────────────────── @@ -87,10 +98,14 @@ static IReadOnlyList> FetchData( // ── Build the hosted agent ─────────────────────────────────────────────────── -AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()) .AsAIAgent(new ChatClientAgentOptions { - Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-codeact", + Name = agentName, Description = "Hosted CodeAct agent with sandbox-only compute and fetch_data tools.", ChatOptions = new ChatOptions { @@ -105,15 +120,15 @@ direct tool calls. The sandbox exposes `compute` and `fetch_data` via `call_tool AIContextProviders = [codeAct], }); +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); + +// Returns the first candidate that has an actual value, ignoring null and blank entries. +static string FirstNonBlank(params string?[] candidates) => + Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md index e58149f4de..e708ac71d5 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md @@ -1,173 +1,201 @@ # Hosted-LocalCodeAct -A hosted agent that uses [`Microsoft.Agents.AI.LocalCodeAct`](../../../../../src/Microsoft.Agents.AI.LocalCodeAct/README.md) -to give the model a single `execute_code` tool. Two sandbox-only host tools, -`compute` and `fetch_data`, are registered on `LocalCodeActProvider` and are -reachable from inside generated Python via `await call_tool(...)` — never as -direct LLM tool calls. +A hosted CodeAct agent using the Responses protocol. The model sees a single execute_code tool and reaches sandbox-only compute and fetch_data host tools from generated Python via call_tool(...). SECURITY: LocalCodeAct executes LLM-generated Python in the agent process, so only deploy it to an externally sandboxed environment such as a Foundry hosted-agent container. -This mirrors the Python -[`foundry_hosted_agent.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/local_codeact/samples/foundry_hosted_agent.py) -sample for the `agent-framework-local-codeact` package. - -> **⚠️ Security:** LocalCodeAct executes LLM-generated Python in the agent -> process. The package is not a sandbox — it relies on the Foundry hosted-agent -> container (or another externally sandboxed environment) for process, -> filesystem, and network isolation. Do not run this outside of a sandbox. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- Python 3 available on `PATH` (used by `LocalCodeActProvider` to execute the - embedded runner and validator). Override with the `LOCAL_CODEACT_PYTHON` - environment variable if you need a specific interpreter path. -- A Foundry project with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: registers sandbox-only compute/fetch_data tools with a LocalCodeActProvider, exposes a single execute_code tool, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedLocalCodeAct.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration Copy the template and fill in your project endpoint: +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: + ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -LOCAL_CODEACT_PYTHON=python3 +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -This project uses `ProjectReference` to build against the local Agent Framework -source, including the `Microsoft.Agents.AI.LocalCodeAct` package. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -```bash +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct -AGENT_NAME=hosted-local-codeact dotnet run +az login +dotnet run ``` -The agent will start on `http://localhost:8088`. +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Using the Azure Developer CLI: +PowerShell: -```bash -azd ai agent invoke --local "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...)." +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-local-codeact" +dotnet run -- --local ``` -Or with curl: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...).", "model": "hosted-local-codeact"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-local-codeact" +dotnet run -- --local ``` -## Running with Docker +Try: `Use compute to add 21 and 21, then tell me the result.` -Since this project uses `ProjectReference`, use `Dockerfile.contributor` which -takes a pre-published output. The image installs Python 3 so the embedded -runner and validator scripts can execute. +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime (Linux Alpine) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-local-codeact . +```powershell +$work = Join-Path $env:TEMP "hosted-local-codeact-work" +mkdir $work +cd $work ``` -### 3. Run the container - -Generate a bearer token on your host and pass it to the container: +### Step 2: scaffold the project -```bash -# Generate token (expires in ~1 hour) -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +`azd ai agent init` copies the sample into a subfolder named `hosted-local-codeact` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -# Run with token -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-local-codeact \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-local-codeact +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -### 4. Test it +PowerShell: -```bash -azd ai agent invoke --local "Fetch all users and print the admins." +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -## How CodeAct works here +### Step 3: provision and deploy + +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-local-codeact +azd provision +azd deploy +azd ai agent invoke "Use compute to add 21 and 21, then tell me the result." +``` -`LocalCodeActProvider` is registered as an `AIContextProvider`. On every run it -injects: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -- A single `execute_code` tool that the model can call with a Python snippet. -- CodeAct instructions that teach the model to use `await call_tool(...)` for - the provider-owned host tools, rather than asking for direct tool calls. +### Step 4: clean up -The provider-owned host tools in this sample: +``` +azd down +``` -| Tool | Description | -|------|-------------| -| `compute(operation, a, b)` | Math operation: `add`, `subtract`, `multiply`, `divide`. | -| `fetch_data(table)` | Returns rows from a simulated `users` or `products` table. | +Then delete the working directory. -`execute_code` runs the generated Python in a separate Python process governed -by `ProcessExecutionLimits` (5 second timeout in this sample) and the -default-on AST allow-list validator that rejects disallowed imports, builtins, -and dynamic-eval constructs before execution. +## Deploy your local framework changes (contributors) -## Deploying to Foundry (azd spec) +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent -spec (`agent.yaml`) for deployment to Foundry. +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -Initialize an `azd` project from this sample's manifest: +PowerShell: -```bash -mkdir hosted-local-codeact && cd hosted-local-codeact -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-local-codeact ``` -Then deploy: +Bash: ```bash -azd deploy +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-local-codeact ``` -## NuGet package users - -If you are consuming the Agent Framework as a NuGet package (not building from -source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See -the commented section in `HostedLocalCodeAct.csproj` for the `PackageReference` -alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml deleted file mode 100644 index 21ffda6c9e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-local-codeact -displayName: "Hosted Local CodeAct Agent" - -description: > - A hosted agent that uses the CodeAct pattern via - Microsoft.Agents.AI.LocalCodeAct. The model only sees an `execute_code` - tool and orchestrates `compute` and `fetch_data` sandbox-only host tools - via `await call_tool(...)` from inside generated Python. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Local CodeAct - - Agent Framework - -template: - name: hosted-local-codeact - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.5" - memory: 1Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml deleted file mode 100644 index 1076d72c39..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-local-codeact -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.5" - memory: 1Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml new file mode 100644 index 0000000000..7b460029df --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-local-codeact +services: + ai-project: + host: azure.ai.project + hosted-local-codeact: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedLocalCodeAct.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted CodeAct agent: the model sees a single execute_code tool and reaches sandbox-only compute/fetch_data host tools from generated Python. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-local-codeact + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file From f7bba0da05bd26c019120ea64a97cd5dec1258dd Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:42:22 +0100 Subject: [PATCH 3/7] .NET: Migrate remaining hosted-agent samples to source (ZIP) deploy Migrate Hosted-McpTools, Hosted-MemoryAgent, Hosted-AgentSkills, Hosted-AzureSearchRag, Hosted-Toolbox, Hosted-Toolbox-AuthPaths and Hosted-ToolboxMcpSkills to the zip/code-deploy pattern (azure.yaml with codeConfiguration + sample-specific env passthrough, canonical .agentignore, self-contained csproj, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Also restore the Hosted-Invocations-EchoAgent csproj filename the solution references. McpTools verified live via remote_build against the public Microsoft Learn MCP server; the memory/search/toolbox/skills samples build locally and deploy via remote_build but need their external resources (memory store, search index, toolbox connections, skills) provisioned to exercise end to end. --- ...oj => Hosted-Invocations-EchoAgent.csproj} | 0 .../responses/Hosted-AgentSkills/.agentignore | 30 ++ .../responses/Hosted-AgentSkills/.env.example | 27 +- .../responses/Hosted-AgentSkills/Dockerfile | 26 -- .../Hosted-AgentSkills/Dockerfile.contributor | 22 -- .../HostedAgentSkills.csproj | 59 ++-- .../responses/Hosted-AgentSkills/Program.cs | 20 +- .../responses/Hosted-AgentSkills/README.md | 216 +++++++++----- .../Hosted-AgentSkills/agent.manifest.yaml | 41 --- .../responses/Hosted-AgentSkills/agent.yaml | 14 - .../responses/Hosted-AgentSkills/azure.yaml | 42 +++ .../Hosted-AzureSearchRag/.agentignore | 30 ++ .../Hosted-AzureSearchRag/.env.example | 25 +- .../Hosted-AzureSearchRag/Dockerfile | 17 -- .../Dockerfile.contributor | 23 -- .../HostedAzureSearchRag.csproj | 56 ++-- .../Hosted-AzureSearchRag/Program.cs | 84 +----- .../responses/Hosted-AzureSearchRag/README.md | 262 ++++++++--------- .../Hosted-AzureSearchRag/agent.manifest.yaml | 31 -- .../Hosted-AzureSearchRag/agent.yaml | 9 - .../Hosted-AzureSearchRag/azure.yaml | 42 +++ .../responses/Hosted-McpTools/.agentignore | 30 ++ .../responses/Hosted-McpTools/.env.example | 18 +- .../responses/Hosted-McpTools/Dockerfile | 17 -- .../Hosted-McpTools/Dockerfile.contributor | 18 -- .../Hosted-McpTools/HostedMcpTools.csproj | 54 ++-- .../responses/Hosted-McpTools/Program.cs | 45 ++- .../responses/Hosted-McpTools/README.md | 201 ++++++++----- .../Hosted-McpTools/agent.manifest.yaml | 30 -- .../responses/Hosted-McpTools/agent.yaml | 9 - .../responses/Hosted-McpTools/azure.yaml | 40 +++ .../responses/Hosted-MemoryAgent/.agentignore | 30 ++ .../responses/Hosted-MemoryAgent/.env.example | 27 +- .../responses/Hosted-MemoryAgent/Dockerfile | 26 -- .../Hosted-MemoryAgent/Dockerfile.contributor | 22 -- .../HostedMemoryAgent.csproj | 52 ++-- .../responses/Hosted-MemoryAgent/Program.cs | 20 +- .../responses/Hosted-MemoryAgent/README.md | 230 ++++++++------- .../Hosted-MemoryAgent/agent.manifest.yaml | 31 -- .../responses/Hosted-MemoryAgent/agent.yaml | 9 - .../responses/Hosted-MemoryAgent/azure.yaml | 42 +++ .../Hosted-Toolbox-AuthPaths/.agentignore | 30 ++ .../Hosted-Toolbox-AuthPaths/.env.example | 26 +- .../Hosted-Toolbox-AuthPaths/Dockerfile | 17 -- .../Dockerfile.contributor | 21 -- .../Hosted-Toolbox-AuthPaths.csproj | 52 ++-- .../Hosted-Toolbox-AuthPaths/Program.cs | 55 +--- .../Hosted-Toolbox-AuthPaths/README.md | 275 +++++++++--------- .../agent.manifest.yaml | 48 --- .../Hosted-Toolbox-AuthPaths/agent.yaml | 9 - .../Hosted-Toolbox-AuthPaths/azure.yaml | 41 +++ .../responses/Hosted-Toolbox/.agentignore | 30 ++ .../responses/Hosted-Toolbox/.env.example | 26 +- .../responses/Hosted-Toolbox/Dockerfile | 17 -- .../Hosted-Toolbox/Dockerfile.contributor | 21 -- .../Hosted-Toolbox/HostedToolbox.csproj | 52 ++-- .../responses/Hosted-Toolbox/Program.cs | 22 +- .../responses/Hosted-Toolbox/README.md | 190 ++++++++---- .../Hosted-Toolbox/agent.manifest.yaml | 47 --- .../responses/Hosted-Toolbox/agent.yaml | 9 - .../responses/Hosted-Toolbox/azure.yaml | 41 +++ .../Hosted-ToolboxMcpSkills/.agentignore | 30 ++ .../Hosted-ToolboxMcpSkills/.env.example | 20 +- .../Hosted-ToolboxMcpSkills/Dockerfile | 26 -- .../Dockerfile.contributor | 18 -- .../HostedToolboxMcpSkills.csproj | 57 ++-- .../Hosted-ToolboxMcpSkills/Program.cs | 17 +- .../Hosted-ToolboxMcpSkills/README.md | 204 ++++++++----- .../agent.manifest.yaml | 43 --- .../Hosted-ToolboxMcpSkills/agent.yaml | 14 - .../Hosted-ToolboxMcpSkills/azure.yaml | 41 +++ 71 files changed, 1820 insertions(+), 1706 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/{HostedInvocationsEchoAgent.csproj => Hosted-Invocations-EchoAgent.csproj} (100%) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/azure.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/azure.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/HostedInvocationsEchoAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example index 26d0b95f8c..506721e182 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/.env.example @@ -1,10 +1,21 @@ -FOUNDRY_PROJECT_ENDPOINT= +# Foundry project endpoint (shape: https:///api/projects/) +FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AGENT_NAME=hosted-agent-skills -SKILL_NAMES=support-style,escalation-policy -# Set to true to provision sample skills to Foundry on startup (first-run convenience). -# In production, skills are provisioned externally — leave this unset or false. + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev + +# Set to true to upload the bundled sample skills on startup; SKILL_NAMES selects which to load. PROVISION_SAMPLE_SKILLS=true -AZURE_BEARER_TOKEN=DefaultAzureCredential \ No newline at end of file +SKILL_NAMES= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile deleted file mode 100644 index 58e30874a6..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Dockerfile for end-users consuming the Agent Framework via NuGet packages. -# -# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container, -# which only succeeds when the project references its dependencies via PackageReference (see the -# commented-out section in HostedAgentSkills.csproj). Contributors building from the -# agent-framework repository source must use Dockerfile.contributor instead because -# ProjectReference dependencies live outside this folder and cannot be restored from inside -# this build context. -# -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile.contributor deleted file mode 100644 index b988d322ae..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile.contributor +++ /dev/null @@ -1,22 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-agent-skills . -# docker run --rm -p 8088:8088 \ -# -e AGENT_NAME=hosted-agent-skills \ -# -e HOSTED_USER_ISOLATION_KEY=alice \ -# --env-file .env hosted-agent-skills -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"] \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj index 3d522e84b6..11e4d306a8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj @@ -1,40 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedAgentSkills HostedAgentSkills - $(NoWarn);MEAI001;OPENAI001;AAIP001 + 3ebf91b7-0ecf-4568-8ab9-841ff2132601 + 1.15.0-preview.260722.1 - - - - - - - - - - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs index 9d80fa0d1e..41103979b3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Program.cs @@ -22,10 +22,8 @@ using System.ClientModel; using Azure.AI.Projects; using Azure.AI.Projects.Agents; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; @@ -33,10 +31,10 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +string endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES") +string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; +string skillNames = System.Environment.GetEnvironmentVariable("SKILL_NAMES") ?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy)."); string[] requestedSkills = skillNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -60,9 +58,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. // Use a chained credential: try a temporary dev token first (for local Docker debugging), // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); AIProjectClient projectClient = new(new Uri(endpoint), credential); ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAgentSkills(); @@ -73,7 +69,7 @@ // out of the box without a separate setup step. Set PROVISION_SAMPLE_SKILLS=true to enable. string sourceSkillsDir = Path.Combine(AppContext.BaseDirectory, "skills"); bool provisionEnabled = string.Equals( - Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase); + System.Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase); if (provisionEnabled && Directory.Exists(sourceSkillsDir)) { await EnsureSkillsProvisionedAsync(skillsClient, sourceSkillsDir, requestedSkills); @@ -94,7 +90,7 @@ AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions { - Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills", + Name = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills", ChatOptions = new ChatOptions { ModelId = deploymentName, @@ -116,10 +112,6 @@ var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md index 31379c8c63..44fe288aae 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md @@ -1,135 +1,201 @@ -# What this sample demonstrates +# Hosted-AgentSkills -An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through the Skills REST API, and downloaded by the agent on boot so updates ship without code changes. +A hosted agent that uploads and consumes Foundry Skills (SKILL.md) via the Skills REST API. Set PROVISION_SAMPLE_SKILLS=true to have the sample upload its bundled skills on startup, and SKILL_NAMES to select which to load. -## How It Works +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. -### Authoring skills +## Prerequisites -Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/): +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. +- Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` -| Skill | Purpose | -|---|---| -| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. | -| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. | +## Files -Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response. +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: provisions and loads Foundry Skills, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedAgentSkills.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | -> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import. +## Configuration -### Uploading skills +Copy the template and fill in your project endpoint: -The sample includes a convenience provisioning step that checks whether each skill exists in Foundry and uploads it if not, gated behind the `PROVISION_SAMPLE_SKILLS=true` env var. **In production, skill provisioning is an external concern** — it is NOT the hosted agent's responsibility. A real deployment pipeline would provision skills separately (e.g., via a CI/CD step, a CLI script, or a management portal). +PowerShell: -The provisioning uses `ProjectAgentSkills.CreateSkillFromPackageAsync(directoryPath)` from the `Azure.AI.Projects.Agents` SDK. The method packages the `SKILL.md` file as a ZIP and uploads it to Foundry. +```powershell +copy .env.example .env +``` -### Downloading skills at agent startup +Bash: -[`Program.cs`](Program.cs) reads the comma-separated `SKILL_NAMES` env var and for each skill name downloads the ZIP archive from Foundry via `ProjectAgentSkills.DownloadSkillAsync(name)`, then unpacks it into a **separate runtime directory** at `downloaded_skills//` (kept distinct from the static `skills/` source folder). +```bash +cp .env.example .env +``` -An [`AgentSkillsProvider`](../../../../../src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs) is then built over `downloaded_skills/` and attached to the agent as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern: +```env +FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +ASPNETCORE_URLS=http://+:8088 +AZURE_TOKEN_CREDENTIALS=dev +``` -1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill). -2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required. +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -> **Note:** This sample supports instruction-only and resource-based skills. If your downloaded skills contain scripts, add a script runner when constructing the `AgentSkillsProvider`. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -### Agent Hosting +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the Responses API hosting layer (`AddFoundryResponses` / `MapFoundryResponses`). +## Run and test locally -## Prerequisites +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. -- A Foundry project with a deployed model (e.g., `gpt-4o`) -- Azure CLI logged in (`az login`) +**Terminal 1 — host the agent:** -### Required RBAC +``` +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills +az login +dotnet run +``` + +The agent starts on `http://localhost:8088`. + +**Terminal 2 — chat with it (code-first REPL):** -Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills and downloading them. +PowerShell: -## Running the Agent Host +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-agent-skills" +dotnet run -- --local +``` -Set the required environment variables and run the sample with `dotnet run`: +Bash: ```bash -export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" -export FOUNDRY_MODEL="gpt-4o" -export SKILL_NAMES="support-style,escalation-policy" -export PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-agent-skills" +dotnet run -- --local ``` -Or in PowerShell: +Try: `Introduce yourself using your configured skills.` + +## Deploy to Foundry (source / ZIP) + +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. + +### Step 1: create the working directory and enter it + +PowerShell: ```powershell -$env:SKILL_NAMES="support-style,escalation-policy" -$env:PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry +$work = Join-Path $env:TEMP "hosted-agent-skills-work" +mkdir $work +cd $work ``` -You can also place these in a `.env` file next to `Program.cs` — see [`.env.example`](.env.example). +### Step 2: scaffold the project -On startup you should see: +`azd ai agent init` copies the sample into a subfolder named `hosted-agent-skills` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -```text -Skill 'support-style' already exists in Foundry. -Skill 'escalation-policy' already exists in Foundry. -Downloading skill 'support-style' from Foundry... -Downloading skill 'escalation-policy' from Foundry... +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -The downloaded `SKILL.md` files land under `downloaded_skills//SKILL.md` next to the published output. This directory is recreated from scratch on every run, so deleting it manually is never necessary. - -## Interacting with the agent +PowerShell: -> Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml" -```bash -curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}' -curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}' +azd auth login +azd ai agent init -m $sample -d ``` -| Prompt mentions | Skill that should drive the response | -|---|---| -| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. | -| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. | +### Step 3: provision and deploy -Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list). +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. -## Deploying the Agent to Foundry +``` +cd hosted-agent-skills +azd provision +azd deploy +azd ai agent invoke "Introduce yourself using your configured skills." +``` -When deploying to Foundry, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml): +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. + +### Step 4: clean up -```bash -azd env set SKILL_NAMES "support-style,escalation-policy" +``` +azd down ``` -The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup. +Then delete the working directory. -> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The provisioning step must have been run against the same Foundry project before the agent can download the skills. +## Deploy your local framework changes (contributors) -### Deploying to Foundry (azd spec) +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -Initialize an `azd` project from this sample's manifest: +PowerShell: -```bash -mkdir hosted-agent-skills && cd hosted-agent-skills -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-agent-skills ``` -Then deploy: +Bash: ```bash -azd deploy +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-agent-skills ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. -```bash -azd env set AGENT_NAME hosted-agent-skills -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +## Troubleshooting + +**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** + +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: + +``` +azd ai agent invoke --new-conversation "Hello!" ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml deleted file mode 100644 index 6c8ac5f4ea..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-agent-skills -displayName: "Hosted Agent Skills" - -description: > - An Agent Framework agent that downloads its behavioral guidelines from the Foundry - Skills REST API at startup, demonstrating how to decouple behavioral guidelines - (tone, escalation policy, etc.) from agent code using AgentSkillsProvider. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Agent Framework - - Agent Skills - - Foundry Skills - -template: - name: hosted-agent-skills - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi - environment_variables: - - name: FOUNDRY_MODEL - value: "{{FOUNDRY_MODEL}}" - - name: SKILL_NAMES - value: "{{SKILL_NAMES}}" -parameters: - properties: - - name: SKILL_NAMES - secret: false - description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy) -resources: - - kind: model - id: gpt-4.1-mini - name: FOUNDRY_MODEL diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml deleted file mode 100644 index f544c2fbea..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-agent-skills -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi -environment_variables: - - name: FOUNDRY_MODEL - value: ${FOUNDRY_MODEL} - - name: SKILL_NAMES - value: ${SKILL_NAMES} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml new file mode 100644 index 0000000000..36758f8090 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-agent-skills +services: + ai-project: + host: azure.ai.project + hosted-agent-skills: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedAgentSkills.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + SKILL_NAMES: ${SKILL_NAMES} + PROVISION_SAMPLE_SKILLS: ${PROVISION_SAMPLE_SKILLS} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent that uploads and consumes Foundry Skills (SKILL.md) via the Skills REST API. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-agent-skills + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example index fe9adaaad8..814f9e44c8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/.env.example @@ -1,8 +1,21 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= -FOUNDRY_MODEL=gpt-4o -AZURE_SEARCH_ENDPOINT= -AZURE_SEARCH_INDEX_NAME=contoso-outdoors -AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential -AZURE_BEARER_TOKEN_SEARCH=DefaultAzureCredential + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev + +# Azure AI Search index (must be pre-provisioned; your identity needs read access). +AZURE_SEARCH_ENDPOINT=https://.search.windows.net +AZURE_SEARCH_INDEX_NAME= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile deleted file mode 100644 index a9c045eeaa..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile.contributor deleted file mode 100644 index a900a4fdac..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Dockerfile.contributor +++ /dev/null @@ -1,23 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-azure-search-rag . -# docker run --rm -p 8088:8088 \ -# -e AGENT_NAME=hosted-azure-search-rag \ -# -e AZURE_BEARER_TOKEN_FOUNDRY=$AZURE_BEARER_TOKEN_FOUNDRY \ -# -e AZURE_BEARER_TOKEN_SEARCH=$AZURE_BEARER_TOKEN_SEARCH \ -# --env-file .env hosted-azure-search-rag -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj index 8f676ffa56..5d71f597a1 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/HostedAzureSearchRag.csproj @@ -1,36 +1,44 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedAzureSearchRag HostedAzureSearchRag - $(NoWarn); + 6f2c3d71-5cbd-48fa-9a66-d807dadfb310 + 1.15.0-preview.260722.1 - - - - + + + + + + - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs index d955fc95ac..12509839ea 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/Program.cs @@ -14,7 +14,6 @@ using Azure.Search.Documents; using Azure.Search.Documents.Models; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; @@ -23,13 +22,13 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +string projectEndpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; +string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -string searchEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT") +string searchEndpoint = System.Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT") ?? throw new InvalidOperationException("AZURE_SEARCH_ENDPOINT is not set."); -string searchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME") +string searchIndexName = System.Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME") ?? throw new InvalidOperationException("AZURE_SEARCH_INDEX_NAME is not set."); // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. @@ -39,9 +38,7 @@ // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in // production). The dev credential is scope aware so a single instance serves both Foundry and // Azure AI Search clients (each Azure SDK client requests a token for its own audience). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); // Connect to the pre-provisioned search index. The caller is expected to have created the // index and populated it with documents matching the schema (id / content / sourceName / @@ -57,7 +54,7 @@ AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential) .AsAIAgent(new ChatClientAgentOptions { - Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-azure-search-rag", + Name = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-azure-search-rag", ChatOptions = new ChatOptions { ModelId = deploymentName, @@ -74,10 +71,6 @@ var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); @@ -108,68 +101,3 @@ return results; }; - -/// -/// A scope aware for local Docker debugging only. -/// Reads pre-fetched bearer tokens from environment variables, dispensing the right token -/// based on the requested scope: -/// -/// ai.azure.com scopes -> AZURE_BEARER_TOKEN_FOUNDRY -/// search.azure.com scopes -> AZURE_BEARER_TOKEN_SEARCH -/// -/// For any other scope, throws so a chained -/// credential will fall through. This should NOT be used in production: tokens expire (~1 hour) -/// and cannot be refreshed. -/// -/// Generate the tokens on your host and pass them to the container: -/// -/// export AZURE_BEARER_TOKEN_FOUNDRY=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -/// export AZURE_BEARER_TOKEN_SEARCH=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv) -/// docker run -e AZURE_BEARER_TOKEN_FOUNDRY -e AZURE_BEARER_TOKEN_SEARCH ... -/// -/// -internal sealed class DevTemporaryTokenCredential : TokenCredential -{ - private const string FoundryEnvironmentVariable = "AZURE_BEARER_TOKEN_FOUNDRY"; - private const string SearchEnvironmentVariable = "AZURE_BEARER_TOKEN_SEARCH"; - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => Resolve(requestContext.Scopes); - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(Resolve(requestContext.Scopes)); - - private static AccessToken Resolve(IReadOnlyList scopes) - { - string? envVar = null; - foreach (var scope in scopes) - { - if (scope.Contains("search.azure.com", StringComparison.OrdinalIgnoreCase)) - { - envVar = SearchEnvironmentVariable; - break; - } - - if (scope.Contains("ai.azure.com", StringComparison.OrdinalIgnoreCase)) - { - envVar = FoundryEnvironmentVariable; - break; - } - } - - if (envVar is null) - { - throw new CredentialUnavailableException( - $"DevTemporaryTokenCredential cannot serve scopes [{string.Join(", ", scopes)}]; falling through."); - } - - var token = Environment.GetEnvironmentVariable(envVar); - if (string.IsNullOrEmpty(token) || string.Equals(token, "DefaultAzureCredential", StringComparison.Ordinal)) - { - throw new CredentialUnavailableException( - $"{envVar} environment variable is not set; falling through to next credential."); - } - - return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md index b68f7c3a87..bd91ad8c77 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md @@ -1,221 +1,201 @@ -# Hosted-AzureSearchRag +# Hosted-AzureSearchRag -A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities backed by **Azure AI Search**. The agent grounds its answers in product documentation by running a keyword search against an Azure AI Search index before each model invocation, then citing the source in its response. +A hosted RAG agent grounded in an Azure AI Search index. Requires AZURE_SEARCH_ENDPOINT and AZURE_SEARCH_INDEX_NAME pointing at a pre-provisioned search index your identity can read. -This sample is the Azure AI Search counterpart to `Hosted-TextRag`. Where `Hosted-TextRag` uses a mock in-process search function, this sample talks to a real Azure AI Search index that is provisioned out of band (see "Provisioning the search index" below). +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) -- An Azure AI Search service ([create one](https://learn.microsoft.com/azure/search/search-create-service-portal)) -- **A pre-provisioned search index** with the schema and content described in the next section +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` -### Required RBAC +## Files -Your identity (or the Managed Identity running the container in production) needs: +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: queries an Azure AI Search index for grounding context, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedAzureSearchRag.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | -- **Azure AI User** on the Foundry project scope -- **Search Index Data Reader** on the Azure AI Search service (the sample only reads from the index) - -## Provisioning the search index (one time) - -The sample assumes the search index already exists and contains documents the agent can retrieve from. Provision it once via the Azure Portal, the [REST API](https://learn.microsoft.com/azure/search/search-how-to-create-search-index), or the snippet below. - -### Index schema +## Configuration -| Field | Type | Attributes | -|---|---|---| -| `id` | `Edm.String` | key, filterable | -| `content` | `Edm.String` | searchable (full-text) | -| `sourceName` | `Edm.String` | retrievable, filterable | -| `sourceLink` | `Edm.String` | retrievable | +Copy the template and fill in your project endpoint: -### Example: provision and seed via Azure CLI + REST +PowerShell: -```bash -SEARCH_ENDPOINT="https://.search.windows.net" -INDEX_NAME="contoso-outdoors" -TOKEN=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv) - -# 1. Create the index. -curl -X PUT "$SEARCH_ENDPOINT/indexes/$INDEX_NAME?api-version=2024-07-01" \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{ - "name": "contoso-outdoors", - "fields": [ - { "name": "id", "type": "Edm.String", "key": true, "filterable": true, "searchable": false, "retrievable": true }, - { "name": "content", "type": "Edm.String", "key": false, "filterable": false, "searchable": true, "retrievable": true, "analyzer": "standard.lucene" }, - { "name": "sourceName", "type": "Edm.String", "key": false, "filterable": true, "searchable": false, "retrievable": true }, - { "name": "sourceLink", "type": "Edm.String", "key": false, "filterable": false, "searchable": false, "retrievable": true } - ] - }' - -# 2. Upload three Contoso Outdoors documents matching the queries below. -curl -X POST "$SEARCH_ENDPOINT/indexes/$INDEX_NAME/docs/index?api-version=2024-07-01" \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{ - "value": [ - { "@search.action": "mergeOrUpload", "id": "return-policy", "sourceName": "Contoso Outdoors Return Policy", "sourceLink": "https://contoso.com/policies/returns", "content": "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection. As a thank you, every accepted return ships back with a complimentary Contoso TrailRunner sticker pack, item code TR-CANARY-7821." }, - { "@search.action": "mergeOrUpload", "id": "shipping-guide", "sourceName": "Contoso Outdoors Shipping Guide", "sourceLink": "https://contoso.com/help/shipping", "content": "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout. Use promo code SHIP-CANARY-4493 at checkout for a one-time free overnight upgrade on your first order." }, - { "@search.action": "mergeOrUpload", "id": "tent-care", "sourceName": "TrailRunner Tent Care Instructions", "sourceLink": "https://contoso.com/manuals/trailrunner-tent", "content": "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating. Replacement waterproofing kits are stocked under SKU TENT-CANARY-9067." } - ] - }' -``` - -You can also point the sample at any existing index that exposes the four fields above; the sample reads `content`, `sourceName`, and `sourceLink` as projected by the search results. - -## Configuration +```powershell +copy .env.example .env +``` -Copy the template and fill in your endpoints: +Bash: ```bash cp .env.example .env ``` -Edit `.env`: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ -FOUNDRY_MODEL=gpt-4o -AZURE_SEARCH_ENDPOINT=https://.search.windows.net -AZURE_SEARCH_INDEX_NAME=contoso-outdoors -AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential -AZURE_BEARER_TOKEN_SEARCH=DefaultAzureCredential +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -This project uses `ProjectReference` to build against the local Agent Framework source. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -```bash +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag -AGENT_NAME=hosted-azure-search-rag dotnet run +az login +dotnet run ``` -The agent will start on `http://localhost:8088`. The sample assumes the search index has already been provisioned and seeded (see "Provisioning the search index" above). +The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Using the Azure Developer CLI: +PowerShell: -```bash -azd ai agent invoke --local "What is your return policy?" -azd ai agent invoke --local "How long does shipping take?" -azd ai agent invoke --local "How do I clean my tent?" +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-azure-search-rag" +dotnet run -- --local ``` -Or with curl: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "What is your return policy?", "model": "hosted-azure-search-rag"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-azure-search-rag" +dotnet run -- --local ``` -## Running with Docker +Try: `What does the indexed documentation say about returns?` -Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime (Linux Alpine) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-azure-search-rag . +```powershell +$work = Join-Path $env:TEMP "hosted-azure-search-rag-work" +mkdir $work +cd $work ``` -### 3. Run the container - -Generate two bearer tokens on your host (one per audience) and pass them to the container. A single Azure AD token has only one `aud` claim, so Foundry and Azure AI Search require separate tokens. +### Step 2: scaffold the project -```bash -# Generate tokens (each expires in ~1 hour) -export AZURE_BEARER_TOKEN_FOUNDRY=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -export AZURE_BEARER_TOKEN_SEARCH=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv) +`azd ai agent init` copies the sample into a subfolder named `hosted-azure-search-rag` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -# Run with both tokens -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-azure-search-rag \ - -e AZURE_BEARER_TOKEN_FOUNDRY=$AZURE_BEARER_TOKEN_FOUNDRY \ - -e AZURE_BEARER_TOKEN_SEARCH=$AZURE_BEARER_TOKEN_SEARCH \ - --env-file .env \ - hosted-azure-search-rag +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -### 4. Test it +PowerShell: -Using the Azure Developer CLI: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml" -```bash -azd ai agent invoke --local "What is your return policy?" +azd auth login +azd ai agent init -m $sample -d ``` -## How RAG works in this sample +### Step 3: provision and deploy -The `TextSearchProvider` runs a keyword search against the configured Azure AI Search index **before each model invocation**. When the index is seeded with the three Contoso Outdoors documents from the provisioning section above: +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-azure-search-rag +azd provision +azd deploy +azd ai agent invoke "What does the indexed documentation say about returns?" +``` -| User query mentions | Search result injected | -|---|---| -| "return", "refund" | Contoso Outdoors Return Policy (canary token: `TR-CANARY-7821`) | -| "shipping", "promo" | Contoso Outdoors Shipping Guide (canary token: `SHIP-CANARY-4493`) | -| "tent", "fabric" | TrailRunner Tent Care Instructions (canary token: `TENT-CANARY-9067`) | +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -The model receives the top three search results as additional context and cites the source in its response. Each seeded document includes a unique `*-CANARY-*` token that does not exist in any model training data, so the integration tests can prove an answer was grounded in retrieved content (not fabricated from training) by asking for the canary and asserting it appears in the response. +### Step 4: clean up -Replace the seed documents (or point the sample at an existing index with your own content) to ground the agent in your own knowledge base. +``` +azd down +``` -## Deploying to Foundry (azd spec) +Then delete the working directory. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +## Deploy your local framework changes (contributors) -Initialize an `azd` project from this sample's manifest: +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -```bash -mkdir hosted-azure-search-rag && cd hosted-azure-search-rag -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml -``` +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -Then deploy: +PowerShell: -```bash -azd deploy +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-azure-search-rag ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +Bash: ```bash -azd env set AGENT_NAME hosted-azure-search-rag -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-azure-search-rag ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). - ---- - -## NuGet package users - -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedAzureSearchRag.csproj` for the `PackageReference` alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml deleted file mode 100644 index 3c263dc84e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-azure-search-rag -displayName: "Hosted Azure AI Search RAG Agent" - -description: > - A support specialist agent for Contoso Outdoors with RAG capabilities backed by - Azure AI Search. Uses TextSearchProvider with a SearchClient adapter to ground - answers in product documentation indexed in Azure AI Search before each model - invocation. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - RAG - - Azure AI Search - - Agent Framework - -template: - name: hosted-azure-search-rag - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.yaml deleted file mode 100644 index 1c0c1cdb33..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-azure-search-rag -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml new file mode 100644 index 0000000000..4694ee240e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-azure-search-rag +services: + ai-project: + host: azure.ai.project + hosted-azure-search-rag: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedAzureSearchRag.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + AZURE_SEARCH_ENDPOINT: ${AZURE_SEARCH_ENDPOINT} + AZURE_SEARCH_INDEX_NAME: ${AZURE_SEARCH_INDEX_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted RAG agent grounded in an Azure AI Search index. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-azure-search-rag + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example index 04335e65b8..857e3b584e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example @@ -1,5 +1,17 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile deleted file mode 100644 index fe7fceb685..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedMcpTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor deleted file mode 100644 index 51c8c347d8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor +++ /dev/null @@ -1,18 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local source, which means a standard -# multi-stage Docker build cannot resolve dependencies outside this folder. -# Pre-publish the app targeting the container runtime and copy the output: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-mcp-tools . -# docker run --rm -p 8088:8088 -e AGENT_NAME=mcp-tools -e GITHUB_PAT=$GITHUB_PAT -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-mcp-tools -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedMcpTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj index 7801e43532..0621a4ac36 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj @@ -1,34 +1,44 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedMcpTools HostedMcpTools - $(NoWarn); + 2bfa4cec-b6ef-4e75-b2ca-8f7947cb6419 + 1.15.0-preview.260722.1 - - - - + + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs index 67d66d5d52..0b303d2b60 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs @@ -6,40 +6,38 @@ // McpClient, discovers tools, and handles tool invocations locally within the agent process. // // 2. SERVER-SIDE MCP: The agent declares a HostedMcpServerTool for the same MCP server which -// delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API). -// The provider calls the MCP server on behalf of the agent — no local connection needed. +// delegates tool discovery and invocation to the LLM provider (Responses API). The provider +// calls the MCP server on behalf of the agent — no local connection needed. // -// Both patterns use the Microsoft Learn MCP server to illustrate the architectural difference: -// client-side tools are resolved and invoked by the agent, while server-side tools are resolved -// and invoked by the LLM provider. +// Both patterns use the public Microsoft Learn MCP server. It is deployed to Foundry directly from +// source (code / ZIP upload), so the platform builds and runs your code with no container image. #pragma warning disable MEAI001 // HostedMcpServerTool is experimental using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; -// Load .env file if present (for local development) +// Load a local .env file when present (local development only). In Foundry the +// platform injects the required environment variables at runtime. Env.TraversePath().Load(); -var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.")); -var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -// 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. -// Use a chained credential: try a temporary dev token first (for local Docker debugging), -// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var deployment = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") + ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") + ?? "gpt-4o"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful +// consideration in production. Consider a specific credential (for example +// ManagedIdentityCredential) to avoid latency, unintended credential probing, and +// fallback security risks. +var credential = new DefaultAzureCredential(); // ── Client-side MCP: Microsoft Learn (local resolution) ────────────────────── // Connect directly to the MCP server. The agent discovers and invokes tools locally. @@ -70,6 +68,8 @@ // The agent has access to tools from both MCP patterns simultaneously. List allTools = [.. clientTools.Cast(), serverTool]; +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-mcp-tools"; + AIAgent agent = new AIProjectClient(projectEndpoint, credential) .AsAIAgent( model: deployment, @@ -78,20 +78,15 @@ You are a helpful developer assistant with access to Microsoft Learn documentati Use the available tools to search and retrieve documentation. Be concise and provide direct answers with relevant links. """, - name: "mcp-tools", + name: agentName, description: "Developer assistant with dual-layer MCP tools (client-side and server-side)", tools: allTools); -// Host the agent as a Foundry Hosted Agent using the Responses API. +// Host the agent using the Responses protocol. var builder = WebApplication.CreateBuilder(args); builder.Services.AddFoundryResponses(agent); var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); - app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md index a67d31c388..c88c13722d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md @@ -1,130 +1,201 @@ -# Hosted-McpTools +# Hosted-McpTools -A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**: +A hosted agent with dual-layer MCP tools against the public Microsoft Learn MCP server: client-side (McpClient, resolved in-process) and server-side (HostedMcpServerTool, resolved by the LLM provider). -1. **Client-side MCP (Microsoft Learn)** — The agent connects directly to the Microsoft Learn MCP server via `McpClient`, discovers tools, and handles tool invocations locally within the agent process. - -2. **Server-side MCP (Microsoft Learn)** — The agent declares a `HostedMcpServerTool` which delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API). The provider calls the MCP server on behalf of the agent with no local connection needed. - -## How the two MCP patterns differ - -| | Client-side MCP | Server-side MCP | -|---|---|---| -| **Connection** | Agent connects to MCP server directly | LLM provider connects to MCP server | -| **Tool invocation** | Handled by the agent process | Handled by the Responses API | -| **Auth** | Agent manages credentials | Provider manages credentials | -| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup | -| **Example** | Microsoft Learn (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) | +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: connects to the Microsoft Learn MCP server client-side and declares a server-side HostedMcpServerTool, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedMcpTools.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration -Copy the template and fill in your values: +Copy the template and fill in your project endpoint: + +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: ```bash cp .env.example .env ``` -Edit `.env`: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ -FOUNDRY_MODEL=gpt-4o +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +ASPNETCORE_URLS=http://+:8088 +AZURE_TOKEN_CREDENTIALS=dev ``` -## Running directly (contributors) +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -```bash +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. + +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. + +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. + +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools +az login dotnet run ``` -### Test it +The agent starts on `http://localhost:8088`. -Using the Azure Developer CLI: +**Terminal 2 — chat with it (code-first REPL):** -```bash -# Uses GitHub MCP (client-side) -azd ai agent invoke --local "Search for the agent-framework repository on GitHub" +PowerShell: -# Uses Microsoft Learn MCP (server-side) -azd ai agent invoke --local "How do I create an Azure storage account using az cli?" +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-mcp-tools" +dotnet run -- --local ``` -## Running with Docker - -### 1. Publish for the container runtime +Bash: ```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-mcp-tools" +dotnet run -- --local ``` -### 2. Build and run +Try: `Search Microsoft Learn: what is Azure AI Foundry Agent Service?` -```bash -docker build -f Dockerfile.contributor -t hosted-mcp-tools . +## Deploy to Foundry (source / ZIP) + +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. + +### Step 1: create the working directory and enter it -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +PowerShell: -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=mcp-tools \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-mcp-tools +```powershell +$work = Join-Path $env:TEMP "hosted-mcp-tools-work" +mkdir $work +cd $work ``` -## Deploying to Foundry (azd spec) +### Step 2: scaffold the project + +`azd ai agent init` copies the sample into a subfolder named `hosted-mcp-tools` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. + +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +PowerShell: -Initialize an `azd` project from this sample's manifest: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml" -```bash -mkdir mcp-tools && cd mcp-tools -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml +azd auth login +azd ai agent init -m $sample -d ``` -Then deploy: +### Step 3: provision and deploy -```bash +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-mcp-tools +azd provision azd deploy +azd ai agent invoke "Search Microsoft Learn: what is Azure AI Foundry Agent Service?" ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. + +### Step 4: clean up -```bash -azd env set AGENT_NAME mcp-tools -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o ``` +azd down +``` + +Then delete the working directory. -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +## Deploy your local framework changes (contributors) ---- +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -## NuGet package users +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative. +PowerShell: -## Related samples +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-mcp-tools +``` + +Bash: + +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-mcp-tools +``` -- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — connects to a single Foundry Toolbox via the AF Foundry hosting bridge (`AddFoundryToolboxes` + `FoundryAITool.CreateHostedMcpToolbox`). -- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as `Hosted-Toolbox/`, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml deleted file mode 100644 index 546a7452ee..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: mcp-tools -displayName: "MCP Tools Agent" - -description: > - A developer assistant demonstrating dual-layer MCP integration: - client-side GitHub MCP tools handled by the agent and server-side - Microsoft Learn MCP tools delegated to the LLM provider. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Agent Framework - - MCP - - Model Context Protocol - -template: - name: mcp-tools - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml deleted file mode 100644 index fcc2015d08..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: mcp-tools -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml new file mode 100644 index 0000000000..8f80c46c3c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-mcp-tools +services: + ai-project: + host: azure.ai.project + hosted-mcp-tools: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedMcpTools.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent with dual-layer MCP tools (client-side McpClient and server-side HostedMcpServerTool) against the public Microsoft Learn MCP server. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-mcp-tools + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example index a1b87ffb56..6f2791c52e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/.env.example @@ -1,8 +1,21 @@ -FOUNDRY_PROJECT_ENDPOINT= +# Foundry project endpoint (shape: https:///api/projects/) +FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-4o -AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002 -AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample -AGENT_NAME=hosted-memory-agent -AZURE_BEARER_TOKEN=DefaultAzureCredential \ No newline at end of file + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev + +# Memory store and embedding deployment (must already exist in your project). +AZURE_AI_MEMORY_STORE_ID= +AZURE_AI_EMBEDDING_DEPLOYMENT_NAME= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile deleted file mode 100644 index 661d8069ed..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Dockerfile for end-users consuming the Agent Framework via NuGet packages. -# -# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container, -# which only succeeds when the project references its dependencies via PackageReference (see the -# commented-out section in HostedMemoryAgent.csproj). Contributors building from the -# agent-framework repository source must use Dockerfile.contributor instead because -# ProjectReference dependencies live outside this folder and cannot be restored from inside -# this build context. -# -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedMemoryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile.contributor deleted file mode 100644 index e23a38022a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Dockerfile.contributor +++ /dev/null @@ -1,22 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-memory-agent . -# docker run --rm -p 8088:8088 \ -# -e AGENT_NAME=hosted-memory-agent \ -# -e HOSTED_USER_ISOLATION_KEY=alice \ -# --env-file .env hosted-memory-agent -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedMemoryAgent.dll"] \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/HostedMemoryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/HostedMemoryAgent.csproj index 6386d6d9e3..29b7940391 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/HostedMemoryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/HostedMemoryAgent.csproj @@ -1,33 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedMemoryAgent HostedMemoryAgent - $(NoWarn);MEAI001;OPENAI001 + 7933e8df-ba8d-4e62-b419-6d34d53de308 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs index 6db1f5675a..571c8522fb 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs @@ -14,10 +14,8 @@ // the FoundryMemoryProviderScope, partitioning memories per user. using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry; using Microsoft.Agents.AI.Foundry.Hosting; @@ -26,21 +24,19 @@ // Load .env file if present (for local development). Env.TraversePath().Load(); -var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.")); -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-memory-agent"; -var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; -var embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002"; -var memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "hosted-memory-sample"; +var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-memory-agent"; +var deployment = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o"; +var embeddingDeployment = System.Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002"; +var memoryStoreName = System.Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "hosted-memory-sample"; // 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. // Use a chained credential: try a temporary dev token first (for local Docker debugging), // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in foundry). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); AIProjectClient projectClient = new(projectEndpoint, credential); @@ -87,9 +83,5 @@ var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md index c2dd294db8..61021b87eb 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md @@ -1,175 +1,201 @@ -# Hosted-MemoryAgent +# Hosted-MemoryAgent -A hosted Foundry agent that uses **FoundryMemoryProvider** to remember user-private details across -requests and across sessions, scoped per end user via the Foundry platform's user identity. The -agent plays a friendly travel assistant: tell it about your trip, ask follow-up questions in a new -session, and it recalls what it learned about you. +A hosted agent with long-term memory backed by a Foundry Memory store and an embedding deployment. Requires AZURE_AI_MEMORY_STORE_ID and AZURE_AI_EMBEDDING_DEPLOYMENT_NAME to be set to resources that exist in your project. -This sample exists to demonstrate two things together: - -1. How to host an agent that consumes a `Microsoft.Extensions.AI.AIContextProvider` (specifically - `FoundryMemoryProvider`) under the Foundry Responses hosting layer. -2. How the `HostedSessionContext` flows from the Foundry platform user-identity header - (`x-agent-user-id`) through the `HostedSessionIsolationKeyProvider` into the provider's - `stateInitializer`, so memories are partitioned per user automatically. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with at least one chat model deployment and one embedding model deployment +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: wires a Foundry memory provider (memory store + embedding deployment) into the agent, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedMemoryAgent.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration -Copy the template and fill in your values: +Copy the template and fill in your project endpoint: + +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: ```bash cp .env.example .env ``` -Required: - ```env -FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ -FOUNDRY_MODEL=gpt-4o -AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002 -AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample -AGENT_NAME=hosted-memory-agent +FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +AZURE_TOKEN_CREDENTIALS=dev ``` > `.env` is gitignored. The `.env.example` template is checked in as a reference. -## How memory scoping works +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -| Layer | Source of the user identity | -|---|---| -| Inbound request | The Foundry platform sets the `x-agent-user-id` header on every request. | -| Hosting layer | `AgentFrameworkResponseHandler` resolves a `HostedSessionIsolationKeyProvider` from DI and calls `GetKeysAsync(context, request, ct)`. The default implementation reads `context.PlatformContext.UserIdKey`. | -| Session | The handler stores the resolved value on the session as a `HostedSessionContext` on the first request, and validates it on every subsequent request that resumes the same conversation (mismatch returns 403). | -| Memory provider | The sample's `stateInitializer` reads `session.GetHostedContext().UserId` and uses it as the `FoundryMemoryProviderScope`. Memories are partitioned per user. | +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -This sample scopes memory per user via `HostedFoundryMemoryProviderScopes.PerUser()`, which requires a -resolved user identity — a request with none throws. So locally you **must** send an `x-agent-user-id` -request header (vary it to simulate distinct users); the default `HostedSessionIsolationKeyProvider` -reads it exactly as it reads the platform-injected value. On the Foundry platform the header is always -present, so no local provider registration is needed. +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -## Running directly (contributors) +## Run and test locally -This project uses `ProjectReference` to build against the local Agent Framework source. +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. -```bash +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent +az login dotnet run ``` The agent starts on `http://localhost:8088`. -### Test it +**Terminal 2 — chat with it (code-first REPL):** -Per-user memories require an identity. Send an `x-agent-user-id` header to scope the call to a user -(locally you set it yourself; on the platform it is set for you): +PowerShell: -```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -H "x-agent-user-id: alice" \ - -d '{"input": "Hi! My name is Taylor and I am planning a hiking trip to Patagonia in November.", "model": "hosted-memory-agent"}' +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-memory-agent" +dotnet run -- --local ``` -Wait a few seconds for memory extraction, then ask a follow-up using the response id from the -previous call as `previous_response_id`: +Bash: ```bash -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -H "x-agent-user-id: alice" \ - -d '{"input": "What do you already know about my upcoming trip?", "previous_response_id": "", "model": "hosted-memory-agent"}' +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-memory-agent" +dotnet run -- --local ``` -## Running with Docker +Try: `Remember that my favorite color is teal.` -Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies -outside this folder. Use `Dockerfile.contributor` which takes a pre-published output. +## Deploy to Foundry (source / ZIP) -### 1. Publish for the container runtime (Linux Alpine) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` +### Step 1: create the working directory and enter it -### 2. Build the Docker image +PowerShell: -```bash -docker build -f Dockerfile.contributor -t hosted-memory-agent . +```powershell +$work = Join-Path $env:TEMP "hosted-memory-agent-work" +mkdir $work +cd $work ``` -### 3. Run the container +### Step 2: scaffold the project -```bash -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +`azd ai agent init` copies the sample into a subfolder named `hosted-memory-agent` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-memory-agent \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-memory-agent +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ``` -### 4. Smoke test the running container - -A scripted smoke test that exercises memory recall and per-user isolation is provided at -`scripts/smoke.ps1`. From the sample folder: +PowerShell: ```powershell -pwsh ./scripts/smoke.ps1 +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -The script publishes the project, builds the image, runs a **single** container, and drives two users -(alice, bob) against it by varying the `x-agent-user-id` request header. It asserts that each user -only sees their own memories, and exits non-zero on failure. +### Step 3: provision and deploy -## Deploying to Foundry (azd spec) +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +``` +cd hosted-memory-agent +azd provision +azd deploy +azd ai agent invoke "Remember that my favorite color is teal." +``` -Initialize an `azd` project from this sample's manifest: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -```bash -mkdir hosted-memory-agent && cd hosted-memory-agent -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml +### Step 4: clean up + +``` +azd down ``` -Then deploy: +Then delete the working directory. -```bash -azd deploy +## Deploy your local framework changes (contributors) + +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. + +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: + +PowerShell: + +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-memory-agent ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +Bash: ```bash -azd env set AGENT_NAME hosted-memory-agent -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-memory-agent ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ---- +## Troubleshooting -## NuGet package users +**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -If you are consuming the Agent Framework as a NuGet package (not building from source), use the -standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in -`HostedMemoryAgent.csproj` for the `PackageReference` alternative. +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: -## How it differs from sibling samples +``` +azd ai agent invoke --new-conversation "Hello!" +``` -| | Hosted-ChatClientAgent | Hosted-MemoryAgent | -|---|---|---| -| **Agent definition** | Inline (`AsAIAgent(model, instructions)`) | Inline, plus `AIContextProviders = [memoryProvider]` | -| **State** | None beyond the conversation history | Per-user memories persisted in Foundry Memory | -| **Identity** | Not used | Required: `HostedSessionContext.UserId` flows into the memory scope | -| **Local dev** | Works with no identity header (per-user isolation not triggered) | Requires an `x-agent-user-id` header (memory is per-user); vary it to simulate distinct users | \ No newline at end of file +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml deleted file mode 100644 index 8c733262d1..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-memory-agent -displayName: "Hosted Memory Agent" - -description: > - A travel-assistant hosted agent that uses FoundryMemoryProvider to remember user-private - preferences and details across sessions. Memory is scoped per end user via the Foundry - platform's isolation key headers. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Streaming - - Agent Framework - - Memory - - Foundry Memory - -template: - name: hosted-memory-agent - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.yaml deleted file mode 100644 index 8b5e602ef7..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-memory-agent -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml new file mode 100644 index 0000000000..821022f435 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-memory-agent +services: + ai-project: + host: azure.ai.project + hosted-memory-agent: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedMemoryAgent.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + AZURE_AI_MEMORY_STORE_ID: ${AZURE_AI_MEMORY_STORE_ID} + AZURE_AI_EMBEDDING_DEPLOYMENT_NAME: ${AZURE_AI_EMBEDDING_DEPLOYMENT_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent with long-term memory backed by a Foundry Memory store and an embedding deployment. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-memory-agent + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example index d32068c053..f0742d3aff 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/.env.example @@ -1,16 +1,20 @@ -# Foundry project endpoint (auto-injected in hosted containers). -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +# Foundry project endpoint (shape: https:///api/projects/) +FOUNDRY_PROJECT_ENDPOINT= -# Model deployment name. Must exist in the Foundry project above. +# Model deployment name in your Foundry project. AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o -# Name of the Foundry Toolbox you provisioned in the portal (see README.md). -TOOLBOX_NAME=auth-paths-toolbox +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. +ASPNETCORE_URLS=http://+:8088 -# Agent name advertised over the wire. Must be unique if running side-by-side with -# other Hosted-* samples (e.g. Hosted-Toolbox), otherwise the REPL client cannot -# disambiguate which agent to chat with. -AGENT_NAME=hosted-toolbox-auth-paths-agent +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev -# Application Insights connection string (auto-injected in hosted containers; optional locally). -# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... +# Foundry Toolbox whose tools require per-user OAuth consent. +TOOLBOX_NAME= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile deleted file mode 100644 index b803098b6a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolboxAuthPaths.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor deleted file mode 100644 index bbeb4a098b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Dockerfile.contributor +++ /dev/null @@ -1,21 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local source, which means a standard -# multi-stage Docker build cannot resolve dependencies outside this folder. -# Pre-publish the app targeting the container runtime and copy the output: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-toolbox-auth-paths . -# docker run --rm -p 8088:8088 \ -# -e AGENT_NAME=hosted-toolbox-auth-paths-agent \ -# -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ -# --env-file .env hosted-toolbox-auth-paths -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolboxAuthPaths.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj index da484bb9e1..1ca68f453f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj @@ -1,33 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedToolboxAuthPaths HostedToolboxAuthPaths - $(NoWarn);OPENAI001 + 9c28c80c-ee05-4383-9b9d-aa0efbf5a198 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs index b9366bcdaa..b50ecb91ac 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Program.cs @@ -35,7 +35,6 @@ using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; @@ -48,18 +47,16 @@ // When deployed, only (1) is available; the AF-repo sample convention to set (2) at // deploy time fails silently because the platform reserves all FOUNDRY_* env-var names // and rejects them at agent-create time. Read both, prefer the platform-injected one. -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") - ?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") +string endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") + ?? System.Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException( "Neither FOUNDRY_PROJECT_ENDPOINT (platform-injected in hosted runtime) " + "nor AZURE_AI_PROJECT_ENDPOINT (local-dev convention) is set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; -string toolboxName = Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "auth-paths-toolbox"; -string agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-auth-paths-agent"; +string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; +string toolboxName = System.Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "auth-paths-toolbox"; +string agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-auth-paths-agent"; -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); // Notes on toolbox wiring — there are two ways to attach a Foundry Toolbox to an agent: // - Server-side "baked-in" (what this sample uses): calling AddFoundryToolboxes(credential, name) @@ -99,47 +96,7 @@ and explain which upstream service answered when you respond. var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry -// uses so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); // ── DevTemporaryTokenCredential ─────────────────────────────────────────────── - -/// -/// A for local Docker debugging only. -/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable -/// once at startup. This should NOT be used in production. -/// -/// Generate a token on your host and pass it to the container: -/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) -/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ... -/// -internal sealed class DevTemporaryTokenCredential : TokenCredential -{ - private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; - private readonly string? _token; - - public DevTemporaryTokenCredential() - { - this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); - } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => this.GetAccessToken(); - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(this.GetAccessToken()); - - private AccessToken GetAccessToken() - { - if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") - { - throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); - } - - return new AccessToken(this._token, DateTimeOffset.MaxValue); - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md index ca869e687b..f17ae69442 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md @@ -1,206 +1,201 @@ -# Hosted Toolbox — Authentication Paths +# Hosted-Toolbox-AuthPaths -A hosted Foundry agent backed by a single Foundry Toolbox that bundles MCP tools using **three different authentication paths**. The educational surface lives in the toolbox configuration (which you provision in the Foundry portal) and in this README — the agent code itself is identical to the existing [`Hosted-Toolbox/`](../Hosted-Toolbox/) sample. +A hosted agent demonstrating Foundry Toolbox auth paths (per-user OAuth consent) at runtime. Requires TOOLBOX_NAME set to a toolbox whose tools require delegated user consent. -Drive the agent across the auth paths with the shared [`Using-Samples/SimpleAgent/`](../Using-Samples/SimpleAgent/) REPL client, pointed at this agent. For the **OAuth user-consent** path (#4 below), use the dedicated [`Using-Samples/Hosted-Toolbox-AuthPaths-Client/`](../Using-Samples/Hosted-Toolbox-AuthPaths-Client/) REPL, which detects the consent request, **prints the consent link** and waits for you to press Enter once you have signed in, then re-sends. It never auto-opens a browser, so it works in headless, SSH, and container shells. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. -## What this sample teaches - -| Aspect | This sample | Existing siblings | -|---|---|---| -| Toolbox marker pattern | `FoundryAITool.CreateHostedMcpToolbox(name)` + `AddFoundryToolboxes(credential, name)` | Same as [`Hosted-Toolbox/`](../Hosted-Toolbox/) | -| Tools per toolbox | **Three MCP tools, each with a different auth method** | `Hosted-Toolbox/`: typically one demo tool | -| Consumption | Server-side (Foundry resolves the marker) | Same | -| Client | Shared [`Using-Samples/SimpleAgent/`](../Using-Samples/SimpleAgent/) REPL, pointed at this agent | `Hosted-Toolbox/`: any client | +## Prerequisites -Related samples: -- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — simpler single-tool toolbox. -- [`Hosted-McpTools/`](../Hosted-McpTools/) — contrasts client-side `McpClient` vs server-side `HostedMcpServerTool` for non-toolbox MCP servers. +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. +- Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` -## Authentication-path matrix +## Files -The sample's purpose is to enumerate every authentication path a Foundry toolbox can drive, so each path appears alongside the others. Pick the ones your scenario needs — each connection in a toolbox is independent. +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: consumes a Foundry Toolbox whose tools require per-user OAuth consent, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedToolboxAuthPaths.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | -| # | Auth method | MCP target | Connection `authType` | What flows where | When to pick this | -|---|---|---|---|---|---| -| 1 | **Key-based via project connection** | GitHub MCP at `https://api.githubcopilot.com/mcp` | `CustomKeys` | A PAT stored as `Authorization: Bearer ` lives in the Foundry connection. The toolbox proxy reads it server-side and injects on every MCP call. | The upstream service only accepts API keys or PATs. | -| 2 | **Microsoft Entra — agent identity** | Any Azure Cognitive Services MCP endpoint your project can reach (e.g., Language service MCP) | `AgenticIdentityToken` | Foundry mints an Entra token for the agent's own identity (`instance_identity` in the new agent object model), scoped to the connection's `audience`, and forwards it to the MCP server. The agent identity must hold the required role (typically `Cognitive Services User`) on the target resource. | Per-agent least-privilege access to Entra-protected services. Recommended default for new agents. | -| 3 | **Inline `Authorization` (anti-pattern)** | `https://gitmcp.io/Azure/azure-rest-api-specs` | none | A literal bearer string lives on the toolbox tool entry's `authorization` field. **Do not do this in production** — there's no rotation, no secret store, no per-user identity. Shown for completeness. | Local-dev or public MCP servers that accept any (or no) bearer. | -| 4 | **OAuth — per-user consent (delegated)** | Any per-user OAuth-protected MCP target (e.g. delegated Microsoft Graph, a Logic Apps connector) | `OAuth` connection | The first call for a user has no stored token, so the proxy returns `CONSENT_REQUIRED`. The agent surfaces an `oauth_consent_request` with a consent link and marks the response `incomplete`. The user consents out of band; the proxy then stores their delegated token (bound to the user, not the conversation) and performs the on-behalf-of exchange on every subsequent call. | The tool must act **as the end user** against a downstream that requires delegated consent. | +## Configuration -> **Path #4 needs the OAuth-aware client.** The shared `SimpleAgent/` REPL ignores the consent request and the call simply stays incomplete. Use [`Using-Samples/Hosted-Toolbox-AuthPaths-Client/`](../Using-Samples/Hosted-Toolbox-AuthPaths-Client/) instead — it prints the consent link, waits for you to press Enter after you have signed in, then re-sends the prompt. The user's token never touches the container or the client; consent and the OBO exchange happen entirely between the user, the identity provider, and the toolbox proxy. +Copy the template and fill in your project endpoint: -## Prerequisites +PowerShell: -### 0. (Path #2 only) Identify an Entra-authenticated MCP target - -Path #2 requires an MCP server that accepts Microsoft Entra tokens. Any **Azure Cognitive Services** resource that exposes an MCP endpoint works — they all accept Entra ID tokens and gate access via standard RBAC. +```powershell +copy .env.example .env +``` -The reference walkthrough below uses an **Azure Language service** MCP endpoint: +Bash: +```bash +cp .env.example .env ``` -https://.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview + +```env +FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +ASPNETCORE_URLS=http://+:8088 +AZURE_TOKEN_CREDENTIALS=dev ``` -Substitute any other Cognitive Services MCP endpoint you have. If your project has none, omit tool #2 from your toolbox — the remaining two paths still work. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -#### RBAC for path #2 +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -Grant the **`Cognitive Services User`** role on the target resource to the agent's instance identity. Find it on the agent ARM resource (Azure portal → your agent → JSON view) at `instance_identity.principal_id`. This is the principal the Foundry proxy uses when minting tokens for `AgenticIdentityToken` connections. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -```powershell -$lang = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/" +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -az role assignment create ` - --assignee-object-id ` - --assignee-principal-type ServicePrincipal ` - --role "Cognitive Services User" ` - --scope $lang -``` +## Run and test locally -Repeat for any additional Cognitive Services resources the agent identity needs to call. +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. -> The RBAC grant requires `Microsoft.Authorization/roleAssignments/write` on the target scope. In many enterprise subscriptions this needs a PIM JIT activation. +**Terminal 1 — host the agent:** -### 1. Foundry project + Azure AI User role +``` +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths +az login +dotnet run +``` -- An active Microsoft Foundry project ([create one](https://learn.microsoft.com/en-us/azure/foundry/how-to/create-projects)). -- The **Azure AI User** role on the project assigned to: - - The developer (you) creating the toolbox. - - The agent identity for tool invocation. +The agent starts on `http://localhost:8088`. -### 2. Create the project connections +**Terminal 2 — chat with it (code-first REPL):** -The Entra-based connection (path #2) is not available in the Foundry portal connection wizard today. Create it via ARM REST: +PowerShell: ```powershell -$armToken = az account get-access-token --query accessToken -o tsv -$h = @{ Authorization = "Bearer $armToken"; "Content-Type" = "application/json" } -$proj = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/" -$lang = "https://.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview" - -# Path 2 — agent identity -$body2 = @{ properties = @{ - category = "RemoteTool"; target = $lang - authType = "AgenticIdentityToken"; audience = "https://cognitiveservices.azure.com" - isSharedToAll = $false -}} | ConvertTo-Json -Depth 5 -az rest --method PUT --headers "Content-Type=application/json" ` - --url "https://management.azure.com$proj/connections/lang-mcp-agent-id?api-version=2025-04-01-preview" ` - --body $body2 +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths" +dotnet run -- --local ``` -Connection summary: - -| Connection name (used by the toolbox) | `category` | `authType` | `audience` | -|---|---|---|---| -| `github-mcp-key` | `CustomKeys` | `CustomKeys` | n/a (key value carries `Authorization: Bearer `) | -| `lang-mcp-agent-id` | `RemoteTool` | `AgenticIdentityToken` | `https://cognitiveservices.azure.com` | +Bash: -Path #3 (`gitmcp.io`) needs no connection — the auth lives on the toolbox tool entry itself. +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-toolbox-auth-paths" +dotnet run -- --local +``` -The `audience` value is the token resource identifier of the target service — for any Cognitive Services resource it is `https://cognitiveservices.azure.com`. For other Azure services consult [Agent identity — runtime token exchange](https://learn.microsoft.com/azure/foundry/agents/concepts/agent-identity#runtime-token-exchange). +Try: `Use a toolbox tool that requires my consent.` -### 3. Create the toolbox +## Deploy to Foundry (source / ZIP) -In the Foundry portal → Tools → Add Toolbox. Name it `auth-paths-toolbox` (or whatever you prefer; export the name as `TOOLBOX_NAME`). Add three MCP tool entries: +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -| Tool `server_label` | `server_url` | Auth | -|---|---|---| -| `github_pat` | `https://api.githubcopilot.com/mcp` | `project_connection_id: github-mcp-key` | -| `lang_agent` | Your Language service MCP URL | `project_connection_id: lang-mcp-agent-id` | -| `gitmcp_inline` | `https://gitmcp.io/Azure/azure-rest-api-specs` | `authorization: "Bearer demo-only-not-real"` (no `project_connection_id`) | +### Step 1: create the working directory and enter it -Each entry should also carry: +PowerShell: -- `require_approval: never` (this sample is focused on auth, not approval flows; see [`ToolCallingApprovalHostedAgentFixture.cs`](../../../../../tests/Foundry.Hosting.IntegrationTests/Fixtures/ToolCallingApprovalHostedAgentFixture.cs) for that concern). -- A tight `allowed_tools` list. GitHub MCP exposes ~50 tools; restrict to what you actually want the model to invoke. For example: `github_pat` → `["search_issues", "list_pull_requests"]`. **Every name in `allowed_tools` must match a real tool on the upstream server** — an unknown name (e.g., `get_issue`, which GitHub MCP does not expose) makes the whole source fail enumeration. See the partial-failure note below. +```powershell +$work = Join-Path $env:TEMP "hosted-toolbox-auth-paths-work" +mkdir $work +cd $work +``` -### Sidebar — what the toolbox-creation code looks like +### Step 2: scaffold the project -This sample assumes the toolbox already exists; it does not provision one programmatically. For an end-to-end code example of toolbox creation from a publisher script (suitable for a CI/CD pipeline), see [`02-agents/AgentProviders/foundry/Agent_Step25_FoundryToolboxMcp/Program.cs`](../../../../02-agents/AgentProviders/foundry/Agent_Step25_FoundryToolboxMcp/Program.cs) — its `CreateSampleToolboxAsync` helper uses `AgentAdministrationClient.GetAgentToolboxes().CreateToolboxVersionAsync(...)` and is the canonical pattern. +`azd ai agent init` copies the sample into a subfolder named `hosted-toolbox-auth-paths` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -## Run the agent +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -Set environment variables (or copy `.env.example` to `.env` and fill it in): +PowerShell: ```powershell -$env:AZURE_AI_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/" -$env:AZURE_AI_MODEL_DEPLOYMENT_NAME = "gpt-4o" -$env:TOOLBOX_NAME = "auth-paths-toolbox" +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -Locally, the `Foundry.Hosting` package reads `AZURE_AI_PROJECT_ENDPOINT` as a fallback when `FOUNDRY_PROJECT_ENDPOINT` is absent. In the hosted Foundry runtime, the platform auto-injects `FOUNDRY_PROJECT_ENDPOINT` and the package builds the toolbox proxy URL as `{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1` per [`tools-integration-spec.md`](https://github.com/microsoft/AgentSchema/blob/main/specs/agents/hosted_agents/container-spec/docs/tools-integration-spec.md) §2–§3. +### Step 3: provision and deploy -Then sign in (`az login`) and start the server: +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. -```powershell -dotnet run --tl:off +``` +cd hosted-toolbox-auth-paths +azd provision +azd deploy +azd ai agent invoke "Use a toolbox tool that requires my consent." ``` -The server logs at `http://localhost:8088/`. In Development it also maps the per-agent OpenAI route shape (`MapDevTemporaryLocalAgentEndpoint()`), so the shared `SimpleAgent` REPL client can reach it through `AsAIAgent(agentEndpoint)` — the only supported way to consume a hosted Foundry agent. In a separate terminal: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -**Against the local dev server** (point the client at localhost; the `{project}` segment is a wildcard the server ignores): +### Step 4: clean up -```powershell -cd ../Using-Samples/SimpleAgent -$env:AZURE_AI_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local" -$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent" -dotnet run --tl:off ``` - -**Against a deployed agent** (point the client at the real project endpoint and the deployed agent name): - -```powershell -cd ../Using-Samples/SimpleAgent -$env:AZURE_AI_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/" -$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent" -dotnet run --tl:off +azd down ``` -Either way the client derives the per-agent endpoint URL (`{AZURE_AI_PROJECT_ENDPOINT}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`) and consumes the agent via `AsAIAgent(agentEndpoint)`. Run `az login` first so the client can mint a bearer token. +Then delete the working directory. -> **Parallel-run warning**: `Hosted-Toolbox/` and other `Hosted-*` samples default to the same port (8088) and the same agent name slot. Always set a unique `AGENT_NAME` (this sample defaults to `hosted-toolbox-auth-paths-agent`) and stop other hosted samples before starting this one. +## Deploy your local framework changes (contributors) -## Sample prompts +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. -One per auth path so each tool gets exercised at least once: +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -``` -List the latest 3 issues in microsoft/agent-framework. # path #1 — GitHub MCP (key) -Detect the language of "Bonjour le monde". # path #2 — Language MCP (agent identity) -What's the latest API version for Microsoft.CognitiveServices? # path #3 — gitmcp.io (inline Authorization) -Send a test email to myself. # path #4 — OAuth user consent (use the OAuth client) -``` - -> Path #4 triggers the consent flow on first use. Run it from [`Using-Samples/Hosted-Toolbox-AuthPaths-Client/`](../Using-Samples/Hosted-Toolbox-AuthPaths-Client/), not `SimpleAgent/`. +PowerShell: -## Troubleshooting / partial-failure semantics - -`AddFoundryToolboxes` resolves the toolbox at startup by listing its tools via MCP `tools/list`. For **hard** errors this enumeration is **all-or-nothing**: if *any* single tool source fails to enumerate (a bad `allowed_tools` name, a rejected key or Entra token, an unreachable upstream), the Foundry toolbox proxy returns a top-level JSON-RPC error (`-32007`) instead of a partial list, the hosting package marks the toolbox startup as failed, `/readiness` returns 503, and *every* invoke against the agent returns **HTTP 424** — even for the auth paths that are configured correctly. So one misconfigured connection or one bad `allowed_tools` entry bricks the whole agent at startup. Get each source enumerating cleanly before deploying. +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-toolbox-auth-paths +``` -**Exception — OAuth consent (path #4) does not brick the container.** When a source fails enumeration purely because it needs per-user OAuth consent (`CONSENT_REQUIRED`), the hosting package keeps the container **healthy and routable**: `/readiness` stays 200 and the consent requirement is surfaced per-request as an `oauth_consent_request` with a consent link. The user consents (via the [`Hosted-Toolbox-AuthPaths-Client/`](../Using-Samples/Hosted-Toolbox-AuthPaths-Client/) REPL), re-sends, and enumeration is retried so the tool becomes available. A *mix* of `CONSENT_REQUIRED` and any non-consent error is still treated as a hard failure (consent alone cannot make enumeration succeed). Symptoms per auth path: +Bash: -| Symptom | Likely cause | -|---|---| -| **All invokes return HTTP 424 ("Failed Dependency")** | One or more tool sources failed `tools/list` at startup (see all-or-nothing note above). Common causes: an `allowed_tools` name that does not exist on the upstream server, or an Entra connection whose token is rejected. Reproduce by calling the toolbox `tools/list` directly with your own token — a `-32007` top-level error names the failing source. | -| **HTTP 401 "audience is incorrect"** | The connection's `audience` field is missing or does not match the OAuth resource identifier the target service accepts. For Cognitive Services targets, set `audience: "https://cognitiveservices.azure.com"`. | -| **HTTP 401 / 403 "principal does not have access"** | Path #1: PAT expired or scope insufficient. Path #2: the agent's instance identity is missing the required role on the target resource. | -| **Container reports zero tools but startup succeeded** | `FoundryToolboxService.StartAsync` caches the `tools/list` result at startup. If a connection or RBAC grant changed after the container started, force a fresh container (re-deploy the agent version) — the cache won't pick up the change until then. | -| **HTTP 404 from a tool call** | Toolbox name mismatch (`TOOLBOX_NAME` vs the name in the portal), or the toolbox was deleted. | -| **Server logs a warning "Neither FOUNDRY_PROJECT_ENDPOINT nor AZURE_AI_PROJECT_ENDPOINT is set; toolbox support is disabled"** | Local dev without the env var set. The agent will load with zero tools and respond as if it has none. Set `AZURE_AI_PROJECT_ENDPOINT` (local-dev fallback) or `FOUNDRY_PROJECT_ENDPOINT` to your project endpoint. | -| **Tools appear but model never invokes them** | `instructions:` in `Program.cs` may not surface what each tool is for. Tighten the `allowed_tools` lists and rephrase prompts to mention the upstream service by name. | -| **`azd ai agent invoke` returns `404 not_found: Conversation '' not found`** | `azd` saves the session and conversation per agent and reuses them on the next invoke. Once the agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server. Pass `--new-conversation` (and `--new-session` if it persists) to start a fresh one. | +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-toolbox-auth-paths +``` -## Region and model compatibility +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. -Foundry Toolboxes have region constraints; some tool types are limited to specific models. This sample defaults to `gpt-4o`, which works in all supported regions. For the full matrix, see the [Foundry tools compatibility matrix](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox#region-and-model-compatibility). +## Troubleshooting -## Anti-pattern note for path #3 +**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -Inline `authorization` on a toolbox tool entry stores credentials **inside the toolbox definition**. There is no rotation, no per-user scoping, no secret-store integration. Use it only for: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: -- Public MCP servers that ignore the bearer (the `gitmcp.io` case demonstrated here). -- Local development against a test MCP server with a throwaway token. +``` +azd ai agent invoke --new-conversation "Hello!" +``` -For everything else use `project_connection_id` and let the platform inject credentials. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml deleted file mode 100644 index 38a2cbaab5..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.manifest.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-toolbox-auth-paths -displayName: "Hosted Toolbox - Authentication Paths" - -description: > - A hosted agent demonstrating three MCP-tool authentication paths in a single - Foundry Toolbox: API key via project connection, Microsoft Entra agent - identity, and inline Authorization - (anti-pattern). The toolbox itself is - provisioned out of band; see this sample's README for the portal walkthrough. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Agent Framework - - Foundry Toolbox - - Authentication - - MCP - -template: - name: hosted-toolbox-auth-paths - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi - environment_variables: - - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - - name: TOOLBOX_NAME - value: "{{TOOLBOX_NAME}}" -parameters: - properties: - - name: TOOLBOX_NAME - type: string - default: "auth-paths-toolbox" - description: "Name of the Foundry Toolbox to load at runtime." -resources: - - kind: model - id: gpt-4o - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - - kind: toolbox - name: "{{TOOLBOX_NAME}}" - tools: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml deleted file mode 100644 index e534003689..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-toolbox-auth-paths -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/azure.yaml new file mode 100644 index 0000000000..834d0f4c44 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-toolbox-auth-paths +services: + ai-project: + host: azure.ai.project + hosted-toolbox-auth-paths: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedToolboxAuthPaths.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + TOOLBOX_NAME: ${TOOLBOX_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent demonstrating Foundry Toolbox auth paths (per-user OAuth consent) at runtime. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-toolbox-auth-paths + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.env.example index ad065ab0d3..8e3d0ddfce 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/.env.example @@ -1,16 +1,20 @@ -# Foundry project endpoint (auto-injected in hosted containers). -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +# Foundry project endpoint (shape: https:///api/projects/) +FOUNDRY_PROJECT_ENDPOINT= -# Model deployment name. Must exist in the Foundry project above. +# Model deployment name in your Foundry project. AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o -# Name of the Foundry Toolbox you provisioned in the portal (see README.md). -TOOLBOX_NAME=my-toolset +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. +ASPNETCORE_URLS=http://+:8088 -# Agent name advertised over the wire. Must be unique if running side-by-side with -# other Hosted-* samples (e.g. Hosted-Toolbox-AuthPaths), otherwise the REPL client -# cannot disambiguate which agent to chat with. -AGENT_NAME=hosted-toolbox-agent +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev -# Application Insights connection string (auto-injected in hosted containers; optional locally). -# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... +# Foundry Toolbox the agent consumes at runtime (must exist in your project). +TOOLBOX_NAME= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile deleted file mode 100644 index 2a71c19858..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolbox.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile.contributor deleted file mode 100644 index cd01987e40..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Dockerfile.contributor +++ /dev/null @@ -1,21 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local source, which means a standard -# multi-stage Docker build cannot resolve dependencies outside this folder. -# Pre-publish the app targeting the container runtime and copy the output: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-toolbox . -# docker run --rm -p 8088:8088 \ -# -e AGENT_NAME=hosted-toolbox-agent \ -# -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ -# --env-file .env hosted-toolbox -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolbox.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj index 84749d8029..94da86c575 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj @@ -1,33 +1,43 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedToolbox HostedToolbox - $(NoWarn); + 5d5195de-e14f-4130-bdad-102a5ef3f592 + 1.15.0-preview.260722.1 - - - + + + + + - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs index 8bee27b9b2..28dc7fb983 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/Program.cs @@ -25,33 +25,29 @@ // per tools-integration-spec.md §2–§3. using Azure.AI.Projects; -using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; // Load .env file if present (for local development) Env.TraversePath().Load(); -string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") - ?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") +string endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") + ?? System.Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException( "Neither FOUNDRY_PROJECT_ENDPOINT (platform-injected in hosted runtime) " + "nor AZURE_AI_PROJECT_ENDPOINT (local-dev convention) is set."); -string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") - ?? Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; -string toolboxName = Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "my-toolset"; +string deploymentName = System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") + ?? System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; +string toolboxName = System.Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? "my-toolset"; // 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. // Use a chained credential: try a temporary dev token first (for local Docker debugging), // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); // ── Create agent ───────────────────────────────────────────────────────────── @@ -63,7 +59,7 @@ You are a helpful assistant with access to tools provided by the Foundry Toolbox Use the available tools to answer user questions. If a tool is not available for a request, let the user know clearly. """, - name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent", + name: System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent", description: "Hosted agent backed by Foundry Toolbox MCP tools"); // ── Build the host ──────────────────────────────────────────────────────────── @@ -82,10 +78,6 @@ Use the available tools to answer user questions. var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md index a825bf576d..66b67a5b07 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md @@ -1,125 +1,201 @@ # Hosted-Toolbox -A hosted Foundry agent that loads tools from a single Foundry Toolbox via the AF Foundry hosting bridge. +A hosted agent that consumes tools from a Foundry Toolbox (Foundry Toolset) at runtime. Requires TOOLBOX_NAME set to a toolbox that exists in your project and that the agent identity can access. -`AddFoundryToolboxes(credential, name)` registers a `FoundryToolboxService` that connects to the Foundry Toolboxes MCP proxy at startup, discovers the toolbox's bundled tools via `tools/list`, and makes them available to the agent on every request. The agent code does nothing per request; the toolbox is baked in on the server. - -This is the minimal toolbox intro. For a richer walkthrough where a single toolbox bundles three MCP tools each authenticated differently, see [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/). +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-4o`) and a Toolbox configured +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: pre-registers a Foundry Toolbox and serves its tools, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedToolbox.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration -Copy the template and fill in your values: +Copy the template and fill in your project endpoint: + +PowerShell: ```powershell -Copy-Item .env.example .env +copy .env.example .env ``` -Edit `.env`: +Bash: + +```bash +cp .env.example .env +``` ```env -AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o -TOOLBOX_NAME=my-toolset +ASPNETCORE_URLS=http://+:8088 +AZURE_TOKEN_CREDENTIALS=dev ``` -Configuration notes: +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -- `AZURE_AI_PROJECT_ENDPOINT` (local-dev) or `FOUNDRY_PROJECT_ENDPOINT` (auto-injected in hosted containers). -- `AZURE_AI_MODEL_DEPLOYMENT_NAME` (default `gpt-4o`). -- `TOOLBOX_NAME` (default `my-toolset`). Use `TOOLBOX_NAME`, not `FOUNDRY_TOOLBOX_NAME`: all `FOUNDRY_*` env-var names are reserved by the Foundry platform and rejected at agent-create, so a `FOUNDRY_*`-named value would not survive deployment. +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -The `Foundry.Hosting` package builds the toolbox proxy URL from `FOUNDRY_PROJECT_ENDPOINT` as `{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1` per [`tools-integration-spec.md`](https://github.com/microsoft/AgentSchema/blob/main/specs/agents/hosted_agents/container-spec/docs/tools-integration-spec.md) §2–§3. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -## Running directly (contributors) +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -```powershell +## Run and test locally + +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. + +**Terminal 1 — host the agent:** + +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox -dotnet run --tl:off +az login +dotnet run ``` -### Test it +The agent starts on `http://localhost:8088`. -Using the Azure Developer CLI: +**Terminal 2 — chat with it (code-first REPL):** + +PowerShell: ```powershell -azd ai agent invoke --local "What tools do you have available, and what can they do?" +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-toolbox" +dotnet run -- --local ``` -## Running with Docker - -### 1. Publish for the container runtime +Bash: -```powershell -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-toolbox" +dotnet run -- --local ``` -### 2. Build and run +Try: `List the tools you have available.` -```powershell -docker build -f Dockerfile.contributor -t hosted-toolbox . +## Deploy to Foundry (source / ZIP) + +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. + +### Step 1: create the working directory and enter it -$env:AZURE_BEARER_TOKEN = (az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +PowerShell: -docker run --rm -p 8088:8088 ` - -e AGENT_NAME=hosted-toolbox-agent ` - -e AZURE_BEARER_TOKEN=$env:AZURE_BEARER_TOKEN ` - --env-file .env ` - hosted-toolbox +```powershell +$work = Join-Path $env:TEMP "hosted-toolbox-work" +mkdir $work +cd $work ``` -## Deploying to Foundry (azd spec) +### Step 2: scaffold the project -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +`azd ai agent init` copies the sample into a subfolder named `hosted-toolbox` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -Initialize an `azd` project from this sample's manifest: +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` + +PowerShell: ```powershell -mkdir hosted-toolbox; cd hosted-toolbox -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.manifest.yaml +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/azure.yaml" + +azd auth login +azd ai agent init -m $sample -d ``` -Then deploy: +### Step 3: provision and deploy -```powershell +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-toolbox +azd provision azd deploy +azd ai agent invoke "List the tools you have available." ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -```powershell -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o -azd env set TOOLBOX_NAME my-toolset +### Step 4: clean up + +``` +azd down ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +Then delete the working directory. + +## Deploy your local framework changes (contributors) + +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. ---- +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: -## NuGet package users +PowerShell: -Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedToolbox.csproj` for the `PackageReference` alternative. +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-toolbox +``` -## Related samples +Bash: + +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-toolbox +``` -- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as this sample, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL. -- [`Hosted-McpTools/`](../Hosted-McpTools/) — contrasts client-side `McpClient` vs server-side `HostedMcpServerTool` for non-toolbox MCP servers. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.manifest.yaml deleted file mode 100644 index abcacd0724..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.manifest.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-toolbox -displayName: "Hosted Toolbox" - -description: > - A hosted agent that loads its tools from a single Foundry Toolbox via the - AF Foundry hosting bridge. AddFoundryToolboxes(credential, name) connects to the Foundry - Toolboxes MCP proxy at startup and exposes the toolbox's bundled tools to the - agent on every request. The toolbox itself is provisioned out of band; see this - sample's README for the portal walkthrough. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Agent Framework - - Foundry Toolbox - - MCP - -template: - name: hosted-toolbox - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi - environment_variables: - - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - - name: TOOLBOX_NAME - value: "{{TOOLBOX_NAME}}" -parameters: - properties: - - name: TOOLBOX_NAME - type: string - default: "my-toolset" - description: "Name of the Foundry Toolbox to load at runtime." -resources: - - kind: model - id: gpt-4o - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - - kind: toolbox - name: "{{TOOLBOX_NAME}}" - tools: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.yaml deleted file mode 100644 index 60ac73e6d8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-toolbox -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/azure.yaml new file mode 100644 index 0000000000..3f9a1930d8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-toolbox +services: + ai-project: + host: azure.ai.project + hosted-toolbox: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedToolbox.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + TOOLBOX_NAME: ${TOOLBOX_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent that consumes tools from a Foundry Toolbox (Foundry Toolset) at runtime. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-toolbox + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example index 064a6ce629..a9258982d8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example @@ -1,6 +1,20 @@ +# Foundry project endpoint (shape: https:///api/projects/) FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o + +# Local development only. Bind the app to the port Foundry probes for readiness, which is the +# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind +# it themselves, so this only matters while the project is pinned to an older published package. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-5 + +# Local development only. Restrict DefaultAzureCredential to developer credentials +# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this, +# on a machine with no managed identity DefaultAzureCredential hangs for a long time +# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in +# Foundry, where the platform-injected managed identity is used. +AZURE_TOKEN_CREDENTIALS=dev + +# Foundry Toolbox with attached MCP skills. TOOLBOX_NAME= -AZURE_BEARER_TOKEN=DefaultAzureCredential diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile deleted file mode 100644 index d04bf72711..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Dockerfile for end-users consuming the Agent Framework via NuGet packages. -# -# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container, -# which only succeeds when the project references its dependencies via PackageReference (see the -# commented-out section in HostedToolboxMcpSkills.csproj). Contributors building from the -# agent-framework repository source must use Dockerfile.contributor instead because -# ProjectReference dependencies live outside this folder and cannot be restored from inside -# this build context. -# -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor deleted file mode 100644 index a01cc1e38c..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor +++ /dev/null @@ -1,18 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local source, which means a standard -# multi-stage Docker build cannot resolve dependencies outside this folder. -# Pre-publish the app targeting the container runtime and copy the output: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-toolbox-mcp-skills . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-toolbox-mcp-skills -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-toolbox-mcp-skills -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj index d4c4155baf..09ae2d837b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj @@ -1,36 +1,45 @@ - + + + + + + false + + + - net10.0 + net10.0 + enable enable - false HostedToolboxMcpSkills HostedToolboxMcpSkills - $(NoWarn); + 466c2502-39a9-4548-89b5-e92e14cd7fa3 + 1.15.0-preview.260722.1 - - - - + + + + + + + - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs index 9d67a11383..a3143afdb4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs @@ -23,7 +23,6 @@ using Azure.Core; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using ModelContextProtocol.Client; @@ -31,10 +30,10 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -var projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") +var projectEndpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); -var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5"; -var toolboxName = Environment.GetEnvironmentVariable("TOOLBOX_NAME") +var deployment = System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5"; +var toolboxName = System.Environment.GetEnvironmentVariable("TOOLBOX_NAME") ?? throw new InvalidOperationException("TOOLBOX_NAME is not set."); // Build the Toolbox MCP URL from the project endpoint and toolbox name. @@ -45,9 +44,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. // Use a chained credential: try a temporary dev token first (for local Docker debugging), // then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). -TokenCredential credential = new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential()); +var credential = new DefaultAzureCredential(); // ── Connect to the Foundry Toolbox MCP endpoint ───────────────────────────── // Create an HttpClient that attaches a fresh Foundry bearer token to every request. @@ -78,7 +75,7 @@ AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential) .AsAIAgent(new ChatClientAgentOptions { - Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-mcp-skills", + Name = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-mcp-skills", Description = "Hosted agent with MCP skills discovered from a Foundry Toolbox", ChatOptions = new() { @@ -95,10 +92,6 @@ var app = builder.Build(); app.MapFoundryResponses(); -// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses -// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). -// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. -app.MapDevTemporaryLocalAgentEndpoint(); app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md index 4695dd937d..7eaeb76981 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md @@ -1,145 +1,201 @@ -# Hosted-ToolboxMcpSkills +# Hosted-ToolboxMcpSkills -A hosted agent that discovers **MCP-based skills from a Foundry Toolbox** and makes them available to the agent using `AgentSkillsProviderBuilder.UseMcpSkills(mcpClient)`. +A hosted agent that discovers MCP-based skills attached to a Foundry Toolbox and serves them at runtime. Requires TOOLBOX_NAME set to a toolbox with MCP skills. NOTE: this sample references Microsoft.Agents.AI.Mcp, which is not part of the contributor local-feed closure, so the contributor local-framework flow does not apply here. -The `AgentSkillsProvider` is attached to the agent as a context provider and implements the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern. When the agent is prompted, it discovers available skills in the Foundry Toolbox via the provider: - -1. **Advertise** - skill names and descriptions are injected into the system prompt so the agent knows what is available. -2. **Load** - when the agent decides a skill is relevant, it retrieves the full skill body with detailed instructions via the provider. -3. **Read resources** - if a skill includes supplementary content (reference documents, assets), the agent reads them on demand via the provider. - -This way the full skill body and resources are only loaded when the agent actually needs them, reducing token usage. +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET. ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- A Foundry project with a deployed model (e.g., `gpt-5`) -- A Foundry Toolbox already configured with skills provisioned +- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`). + This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and + a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick + the project, and takes the deployment name as the `-d` argument. - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` + +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | The agent: discovers MCP skills attached to a Foundry Toolbox, hosts it with the Responses protocol. | +| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedToolboxMcpSkills.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). | ## Configuration -Copy the template and fill in your values: +Copy the template and fill in your project endpoint: + +PowerShell: + +```powershell +copy .env.example .env +``` + +Bash: ```bash cp .env.example .env ``` -Edit `.env` and set your Foundry project endpoint and toolbox name: - ```env FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development -FOUNDRY_MODEL=gpt-5 -TOOLBOX_NAME=my-toolbox +AZURE_TOKEN_CREDENTIALS=dev ``` -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. The `.env.example` template is checked in as a reference. -## Running directly (contributors) +> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent +> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters +> while this project is pinned to an older published package. -This project uses `ProjectReference` to build against the local Agent Framework source. +> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file +> during `azd ai agent init` and fails with `unexpected character` when a mark is present. -```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills -dotnet run -``` +> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`. +> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no +> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, +> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev` +> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe. +> Only for local runs; the deployed agent uses the platform-injected managed identity. -The agent will start on `http://localhost:8088`. +## Run and test locally -### Test it +Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it, +see the sibling [`Using-Samples`](../Using-Samples/) REPLs. -Using the Azure Developer CLI: +**Terminal 1 — host the agent:** -```bash -azd ai agent invoke --local "What skills do you have available?" +``` +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills +az login +dotnet run ``` -## Running with Docker +The agent starts on `http://localhost:8088`. -Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. +**Terminal 2 — chat with it (code-first REPL):** -### 1. Publish for the container runtime (Linux Alpine) +PowerShell: -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +```powershell +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-mcp-skills" +dotnet run -- --local ``` -### 2. Build the Docker image +Bash: ```bash -docker build -f Dockerfile.contributor -t hosted-toolbox-mcp-skills . +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent +export AZURE_AI_AGENT_NAME="hosted-toolbox-mcp-skills" +dotnet run -- --local ``` -### 3. Run the container +Try: `List the MCP skills available from the toolbox.` -Generate a bearer token on your host and pass it to the container: +## Deploy to Foundry (source / ZIP) -```bash -# Generate token (expires in ~1 hour) -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +`azd` scaffolds the project into a working folder, so every step below runs from an **empty +directory outside the repository**, and `-m` points at this sample's `azure.yaml`. -# Run with token -docker run --rm -p 8088:8088 \ - -e AGENT_NAME=hosted-toolbox-mcp-skills \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-toolbox-mcp-skills -``` +### Step 1: create the working directory and enter it -> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration. +PowerShell: -### 4. Test it +```powershell +$work = Join-Path $env:TEMP "hosted-toolbox-mcp-skills-work" +mkdir $work +cd $work +``` -Using the Azure Developer CLI: +### Step 2: scaffold the project -```bash -azd ai agent invoke --local "What skills do you have available?" -``` +`azd ai agent init` copies the sample into a subfolder named `hosted-toolbox-mcp-skills` (the top-level `name:` +in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts +you to pick the Foundry project; `-d` is the name of an existing model deployment in that project. -## Deploying to Foundry (azd spec) +``` +azd env get-values +azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME +``` -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +PowerShell: -Initialize an `azd` project from this sample's manifest: +```powershell +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/azure.yaml" -```bash -mkdir hosted-toolbox-mcp-skills && cd hosted-toolbox-mcp-skills -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml +azd auth login +azd ai agent init -m $sample -d ``` -Then deploy: +### Step 3: provision and deploy -```bash +Contributors changing the Agent Framework source: do the extra step in +[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now, +before the commands below. Everyone else can ignore it. + +``` +cd hosted-toolbox-mcp-skills +azd provision azd deploy +azd ai agent invoke "List the MCP skills available from the toolbox." ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` +in `azure.yaml`). No Dockerfile, no container registry. -```bash -azd env set AGENT_NAME hosted-toolbox-mcp-skills -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-5 +### Step 4: clean up + +``` +azd down ``` -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). +Then delete the working directory. + +## Deploy your local framework changes (contributors) + +**Skip this section unless you are changing the Agent Framework itself.** The project restores the +**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the +upload, so editing framework source in this repository changes nothing about the deployed agent. + +The helper script packs your local framework source into NuGet packages and puts them **inside the +upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above, +**between step 2 and step 3**: ---- +PowerShell: -## NuGet package users +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-toolbox-mcp-skills +``` + +Bash: + +```bash +cd "$WORK" +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-toolbox-mcp-skills +``` -If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedToolboxMcpSkills.csproj` for the `PackageReference` alternative. +See the +[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) +README for the full explanation of what the script changes and why. ## Troubleshooting **`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted, +that conversation no longer exists on the server. Start a fresh one: ``` azd ai agent invoke --new-conversation "Hello!" ``` -Add `--new-session` as well if the failure persists. +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml deleted file mode 100644 index 1ef15df3be..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml +++ /dev/null @@ -1,43 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: hosted-toolbox-mcp-skills -displayName: "Hosted Toolbox MCP Skills Agent" - -description: > - A hosted agent that discovers MCP-based skills from a Foundry Toolbox - and makes them available to the agent via the agent skills provider. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Agent Framework - - MCP - - Model Context Protocol - - Agent Skills - - Foundry Toolbox - - Foundry Toolbox Skills - -template: - name: hosted-toolbox-mcp-skills - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi - environment_variables: - - name: FOUNDRY_MODEL - value: "{{FOUNDRY_MODEL}}" - - name: TOOLBOX_NAME - value: "{{TOOLBOX_NAME}}" -parameters: - properties: - - name: TOOLBOX_NAME - secret: false - description: Name of the Foundry Toolbox to connect to for MCP skill discovery -resources: - - kind: model - id: gpt-5 - name: FOUNDRY_MODEL diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml deleted file mode 100644 index b3760fdc69..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: hosted-toolbox-mcp-skills -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi -environment_variables: - - name: FOUNDRY_MODEL - value: ${FOUNDRY_MODEL} - - name: TOOLBOX_NAME - value: ${TOOLBOX_NAME} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/azure.yaml new file mode 100644 index 0000000000..c7aaa13d8c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/azure.yaml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-toolbox-mcp-skills +services: + ai-project: + host: azure.ai.project + hosted-toolbox-mcp-skills: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedToolboxMcpSkills.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET + # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for + # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent + # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence + # over this value, so it only matters when the project is pinned to an older package. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + TOOLBOX_NAME: ${TOOLBOX_NAME} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted agent that discovers MCP-based skills attached to a Foundry Toolbox and serves them at runtime. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-toolbox-mcp-skills + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file From 9f9aa7e4e103ad5168329389e7469ff9ca636e63 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:46:48 +0100 Subject: [PATCH 4/7] .NET: Migrate Hosted-Workflow-Handoff to source (ZIP) deploy Migrate the triage handoff workflow sample to the zip/code-deploy pattern (azure.yaml with codeConfiguration and Azure OpenAI env passthrough, canonical .agentignore, self-contained csproj using AgentFrameworkVersion for Foundry/Foundry.Hosting/Hosting, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Builds via remote_build; live needs an Azure OpenAI resource (AZURE_OPENAI_ENDPOINT/AZURE_OPENAI_DEPLOYMENT). --- .../Hosted-Workflow-Handoff/.agentignore | 30 +++ .../Hosted-Workflow-Handoff/.env.example | 8 +- .../Hosted-Workflow-Handoff/Dockerfile | 17 -- .../Dockerfile.contributor | 19 -- .../HostedWorkflowHandoff.csproj | 52 ++--- .../Hosted-Workflow-Handoff/Program.cs | 9 +- .../Hosted-Workflow-Handoff/README.md | 186 ++++++------------ .../agent.manifest.yaml | 30 --- .../Hosted-Workflow-Handoff/agent.yaml | 9 - .../Hosted-Workflow-Handoff/azure.yaml | 39 ++++ 10 files changed, 154 insertions(+), 245 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.agentignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/azure.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.agentignore new file mode 100644 index 0000000000..7f9f197c84 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.agentignore @@ -0,0 +1,30 @@ +# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax). +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# .NET build output +bin/ +obj/ +*.user +*.suo +.vs/ + +# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted +# runtime writes its own under the container's home directory, so uploading the local copy +# would ship stale sessions with the agent. +.checkpoints/ + +# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config. +# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent +# Framework from the packages shipped in this upload instead of nuget.org. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example index bfb3c97208..9dbea8cf71 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example @@ -1,5 +1,7 @@ -AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ +# Azure OpenAI resource used by the triage/specialist agents. +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential + +# Local development only. ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +AZURE_TOKEN_CREDENTIALS=dev \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile deleted file mode 100644 index 14b356ad98..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official .NET 10.0 ASP.NET runtime as a parent image -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore -RUN dotnet publish -c Release -o /app/publish - -# Final stage -FROM base AS final -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor deleted file mode 100644 index 4cc047c8bc..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor +++ /dev/null @@ -1,19 +0,0 @@ -# Dockerfile for contributors building from the agent-framework repository source. -# -# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source, -# which means a standard multi-stage Docker build cannot resolve dependencies outside -# this folder. Instead, pre-publish the app targeting the container runtime and copy -# the output into the container: -# -# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -# docker build -f Dockerfile.contributor -t hosted-workflow-handoff . -# docker run --rm -p 8088:8088 -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflow-handoff -# -# For end-users consuming the NuGet package (not ProjectReference), use the standard -# Dockerfile which performs a full dotnet restore + publish inside the container. -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app -COPY out/ . -EXPOSE 8088 -ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj index 2b0a0af826..72bcfd04a3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj @@ -1,46 +1,34 @@ - + + + + false + + + Exe - net10.0 + net10.0 + enable enable HostedWorkflowHandoff HostedWorkflowHandoff - false + 92fa9432-1e9c-4f74-a2c2-7423f2042504 $(NoWarn);NU1605;MAAIW001 + 1.15.0-preview.260722.1 - - - - - - - + + + + + + + - - - - - - - - - - - + - + \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs index e89ad78210..d9370e83f6 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs @@ -19,7 +19,6 @@ using Azure.AI.OpenAI; using Azure.Identity; using DotNetEnv; -using Hosted_Shared_Contributor_Setup; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Hosting; @@ -35,15 +34,13 @@ // --------------------------------------------------------------------------- // 1. Create the shared Azure OpenAI chat client // --------------------------------------------------------------------------- -var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.")); -var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; +var endpoint = new Uri(System.Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.")); +var deployment = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; // 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 azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential( - new DevTemporaryTokenCredential(), - new DefaultAzureCredential())); +var azureClient = new AzureOpenAIClient(endpoint, new DefaultAzureCredential()); IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment); // --------------------------------------------------------------------------- diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md index ba34b3e573..648edfbe53 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md @@ -1,168 +1,96 @@ # Hosted-Workflow-Handoff -A hosted agent server demonstrating two patterns in a single app: +A hosted triage handoff workflow: a triage agent routes each request to a specialist agent (a code expert or a creative writer) and hands control back when done. The sample also wires client-side and server-side MCP tools, and it is backed by an Azure OpenAI resource. It is served over the Responses protocol. -- **`tool-agent`** — an agent with local tools (time, weather) plus remote Microsoft Learn MCP tools -- **`triage-workflow`** — a handoff workflow that routes conversations to specialist agents (code expert or creative writer) using `AgentWorkflowBuilder` +This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image. Source deploy is the default for .NET. -Both agents are served over the Responses protocol. The server also exposes interactive web demos at `/tool-demo` and `/workflow-demo`. +> **Requires an Azure OpenAI resource.** Unlike the other samples (which use the Foundry project's +> model deployment), this one reads `AZURE_OPENAI_ENDPOINT` and `AZURE_OPENAI_DEPLOYMENT` and talks +> to Azure OpenAI directly. Set both to a resource your identity can call before running or +> deploying. -> Unlike the other samples in this folder, this one connects to an **Azure OpenAI** resource directly (not a Foundry project endpoint). +## Files + +| File | Purpose | +|------|---------| +| `Program.cs` | Builds the triage + specialist agents and the handoff workflow, wires MCP tools, hosts it with the Responses protocol and serves demo pages. | +| `Pages.cs` | Static HTML demo pages. | +| `ResponseStreamValidator.cs` | Validates captured SSE streams for the demo. | +| `azure.yaml` | The unified `azd` project file: `codeConfiguration` (source/ZIP deploy) plus the listen port and the Azure OpenAI settings passed through `env`. | +| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). | +| `HostedWorkflowHandoff.csproj` | Self-contained project: single target framework, explicit package versions, opts out of the repo's central package management. | +| `.env.example` | Template for local configuration. | +| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers. | ## Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- An Azure OpenAI resource with a deployed model (e.g., `gpt-4o`) +- An **existing** Foundry project. +- An **Azure OpenAI** resource with a deployed model (for `AZURE_OPENAI_ENDPOINT` / `AZURE_OPENAI_DEPLOYMENT`). - Azure CLI logged in (`az login`) +- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents` ## Configuration -Copy the template and fill in your values: - -```bash -cp .env.example .env -``` - -Edit `.env`: +Copy the template and fill it in: ```env -AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT=gpt-4o -AZURE_BEARER_TOKEN=DefaultAzureCredential ASPNETCORE_URLS=http://+:8088 -ASPNETCORE_ENVIRONMENT=Development +AZURE_TOKEN_CREDENTIALS=dev ``` -`AZURE_BEARER_TOKEN=DefaultAzureCredential` is a sentinel value that tells the app to skip the bearer token and fall through to `DefaultAzureCredential` (requires `az login`). Set it to a real token only when running in Docker. - -> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. +> `.env` is gitignored. Write it as UTF-8 **without** a byte order mark, or `azd ai agent init` fails. +> `AZURE_TOKEN_CREDENTIALS=dev` restricts `DefaultAzureCredential` to developer credentials on a +> machine with no managed identity. Both are local-development only. -## Running directly (contributors) +## Run and test locally -```bash +``` cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff +az login dotnet run ``` -The server starts on `http://localhost:8088`. Open `http://localhost:8088` to see the demo index page. - -### Test it - -Using the Azure Developer CLI (invokes `triage-workflow` — the primary/default agent): - -```bash -azd ai agent invoke --local "Write me a short poem about coding" -``` - -To target a specific agent by name, use curl: - -```bash -# Invoke triage-workflow explicitly -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "Write me a haiku about autumn", "model": "triage-workflow"}' -``` - -```bash -# Invoke tool-agent (local tools + MCP) -curl -X POST http://localhost:8088/responses \ - -H "Content-Type: application/json" \ - -d '{"input": "What time is it in Tokyo?", "model": "tool-agent"}' -``` - -## Running with Docker - -### 1. Publish for the container runtime - -```bash -dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out -``` - -### 2. Build the Docker image - -```bash -docker build -f Dockerfile.contributor -t hosted-workflow-handoff . -``` - -### 3. Run the container - -```bash -export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) - -docker run --rm -p 8088:8088 \ - -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ - --env-file .env \ - hosted-workflow-handoff -``` - -### 4. Test it - -```bash -azd ai agent invoke --local "Explain async/await in C#" -``` - -## How the triage workflow works - -``` -User message - │ - ▼ -┌──────────────┐ -│ Triage Agent │ ──routes──▶ ┌─────────────┐ -│ (router) │ │ Code Expert │ -└──────────────┘ └─────────────┘ - ▲ │ - │◀──────────────────────────────┘ - │ - └──routes──▶ ┌─────────────────┐ - │ Creative Writer │ - └─────────────────┘ -``` - -The triage agent receives every message and hands off to the appropriate specialist. Specialists route back to the triage agent after responding, allowing for multi-turn conversations. +The agent starts on `http://localhost:8088` and also serves demo pages at `/`, `/tool-demo` and `/workflow-demo`. -## Deploying to Foundry (azd spec) +## Deploy to Foundry (source / ZIP) -This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry. +`azd` scaffolds the project into a working folder outside the repository; `-m` points at this sample's `azure.yaml`. -Initialize an `azd` project from this sample's manifest: +```powershell +$work = Join-Path $env:TEMP "hosted-workflow-handoff-work" +mkdir $work; cd $work -```bash -mkdir triage-workflow && cd triage-workflow -azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml -``` +$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/azure.yaml" +azd auth login +azd ai agent init -m $sample -Then deploy: +# Provide the Azure OpenAI settings to the azd environment before deploying: +azd env set AZURE_OPENAI_ENDPOINT https://.openai.azure.com/ +azd env set AZURE_OPENAI_DEPLOYMENT gpt-4o -```bash +cd hosted-workflow-handoff +azd provision azd deploy +azd ai agent invoke "Write a haiku about the sea." ``` -If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying: - -```bash -azd env set AGENT_NAME triage-workflow -azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o -``` - -For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent). - ---- +`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs +`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`). +No Dockerfile, no container registry. Clean up with `azd down`. -## NuGet package users +## Deploy your local framework changes (contributors) -Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative. - -## Troubleshooting - -**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`** - -`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the -agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server, -so every following invoke fails even though the agent itself is healthy. Start a fresh one: +**Skip this unless you are changing the Agent Framework itself.** Run the helper between +`azd ai agent init` and `azd provision` to ship a local framework build inside the upload: +```powershell +cd $work +/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-workflow-handoff ``` -azd ai agent invoke --new-conversation "Hello!" -``` -Add `--new-session` as well if the failure persists. +See the [`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) README for the full explanation. + +For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml deleted file mode 100644 index 54ab18dd91..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml -name: triage-workflow -displayName: "Triage Handoff Workflow Agent" - -description: > - A hosted agent demonstrating two patterns in a single server: a tool-equipped agent - with local tools and remote MCP tools, and a triage workflow that routes conversations - to specialist agents (code expert or creative writer) via handoff orchestration. - -metadata: - tags: - - AI Agent Hosting - - Azure AI AgentServer - - Responses Protocol - - Workflows - - Handoff - - Agent Framework - -template: - name: triage-workflow - kind: hosted - protocols: - - protocol: responses - version: 2.0.0 - resources: - cpu: "0.25" - memory: 0.5Gi -parameters: - properties: [] -resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml deleted file mode 100644 index 6de1c5107f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml -kind: hosted -name: triage-workflow -protocols: - - protocol: responses - version: 2.0.0 -resources: - cpu: "0.25" - memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/azure.yaml new file mode 100644 index 0000000000..9674e03699 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/azure.yaml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: hosted-workflow-handoff +services: + ai-project: + host: azure.ai.project + hosted-workflow-handoff: + project: . + host: azure.ai.agent + language: csharp + uses: + - ai-project + codeConfiguration: + dependencyResolution: remote_build + entryPoint: HostedWorkflowHandoff.dll + runtime: dotnet_10 + # ASPNETCORE_URLS pins the readiness port (8088). AZURE_OPENAI_* point the sample at the + # Azure OpenAI resource it uses for the triage/specialist agents; they are read from the + # active azd environment. + env: + ASPNETCORE_URLS: http://+:8088 + AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} + AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT} + container: + resources: + cpu: "0.5" + memory: 1Gi + description: | + A hosted triage handoff workflow routing to specialist agents, with client-side and server-side MCP tools, backed by Azure OpenAI. + kind: hosted + metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Agent Framework + name: hosted-workflow-handoff + protocols: + - protocol: responses + version: 2.0.0 \ No newline at end of file From d54c8b28bd2c0ad857dbf11d94dff4bce412f86c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:53:09 +0100 Subject: [PATCH 5/7] .NET: Copy Hosted-AgentSkills skills/ into build output The startup provisioning helper reads SKILL.md files from AppContext.BaseDirectory/skills, but the project did not copy the skills/ folder to the build/publish output, so at runtime the source directory did not exist and provisioning was silently skipped. Add a Content include (PreserveNewest), matching the resources/ pattern already used by Hosted-Files. --- .../responses/Hosted-AgentSkills/HostedAgentSkills.csproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj index 11e4d306a8..bd3dd9baf9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj @@ -38,6 +38,14 @@ + + + + PreserveNewest + + + \ No newline at end of file From 88de4cd33c1faf4b440b0b9f47fbce37c5b89325 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:02:13 +0100 Subject: [PATCH 6/7] .NET: Suppress OPENAI001 in Hosted-Workflow-Handoff for standalone ZIP build The repo-wide Directory.Build.props suppresses OPENAI001, but that file does not travel in the code/ZIP deploy package. The standalone dotnet publish the Foundry code deploy runs then fails with error OPENAI001 on the experimental GetResponsesClient().AsIChatClient() call. Add OPENAI001 to the project NoWarn so the sample builds in the code-deploy pipeline, matching SimpleAgent.csproj. --- .../Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj index 72bcfd04a3..4ca7577a21 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj @@ -15,7 +15,7 @@ HostedWorkflowHandoff HostedWorkflowHandoff 92fa9432-1e9c-4f74-a2c2-7423f2042504 - $(NoWarn);NU1605;MAAIW001 + $(NoWarn);NU1605;MAAIW001;OPENAI001 1.15.0-preview.260722.1 From 986d6510fce32346792d7fec6381ae2d6d14ccbe Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:38:44 +0100 Subject: [PATCH 7/7] .NET: Document live-verified idiosyncrasies in Foundry hosted sample READMEs Align every FoundryHostedAgents sample README with the documented azd flow and add the idiosyncrasies found while live-testing each sample on a Foundry project: - All samples: 'azd down' reports success but does not delete the hosted agent; document the explicit REST DELETE needed to remove it. - Hosted-Workflow-Handoff: it builds its own AzureOpenAIClient (data-plane), so the agent identity needs the 'Cognitive Services OpenAI User' role on the Azure OpenAI account. azd only grants 'Foundry User' on the project, so add a step to grant the data-plane role and explain the triage-step failure without it. - Hosted-Toolbox / Toolbox-AuthPaths / ToolboxMcpSkills: the toolbox must already exist and the agent identity must be able to read it; toolboxes with OAuth-gated tools return an oauth_consent_request and response.incomplete on first invoke. --- .../Hosted-Invocations-EchoAgent/README.md | 9 ++++ .../responses/Hosted-AgentSkills/README.md | 9 ++++ .../responses/Hosted-AzureSearchRag/README.md | 9 ++++ .../Hosted-ChatClientAgent/README.md | 9 ++++ .../responses/Hosted-Files/README.md | 9 ++++ .../responses/Hosted-FoundryAgent/README.md | 9 ++++ .../responses/Hosted-LocalCodeAct/README.md | 9 ++++ .../responses/Hosted-LocalTools/README.md | 9 ++++ .../responses/Hosted-McpTools/README.md | 9 ++++ .../responses/Hosted-MemoryAgent/README.md | 9 ++++ .../responses/Hosted-Observability/README.md | 9 ++++ .../responses/Hosted-TextRag/README.md | 9 ++++ .../Hosted-Toolbox-AuthPaths/README.md | 17 +++++++ .../responses/Hosted-Toolbox/README.md | 23 +++++++++ .../Hosted-ToolboxMcpSkills/README.md | 15 ++++++ .../Hosted-Workflow-Handoff/README.md | 48 +++++++++++++++++++ .../Hosted-Workflow-Simple/README.md | 9 ++++ 17 files changed, 220 insertions(+) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md index eafb9fb7ca..570e12a046 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md @@ -94,6 +94,15 @@ curl -X POST "/agents/hosted-invocations-echo-agent/endpoint/p Clean up with `azd down`, then delete the working directory. +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-invocations-echo-agent?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + ## Deploy your local framework changes (contributors) **Skip this section unless you are changing the Agent Framework itself.** The project restores the diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md index 44fe288aae..a3164fa30b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-agent-skills?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md index bd91ad8c77..a8e8830613 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-azure-search-rag?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md index 2c14251a87..fdc82f50c6 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md @@ -219,6 +219,15 @@ dotnet run -- --remote azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-chat-client-agent?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md index 467cecb234..5ca46a838d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-files?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md index cbd2bb59eb..0880e845a8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md @@ -153,6 +153,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-foundry-agent?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md index e708ac71d5..d6351cb0f9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-local-codeact?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md index 6a368a8da5..4b57ed8001 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md @@ -178,6 +178,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-local-tools?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md index c88c13722d..3eaa0f108b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-mcp-tools?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md index 61021b87eb..5bfda16bac 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-memory-agent?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md index 8e33435b8d..330ec4ea8c 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-observability?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md index aad4c1a1af..ce182f05cd 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-text-rag?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md index f17ae69442..06dbce1ffc 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md @@ -151,12 +151,29 @@ azd ai agent invoke "Use a toolbox tool that requires my consent." `dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` in `azure.yaml`). No Dockerfile, no container registry. +> **The toolbox must already exist in the project, and the agent identity must be able to read it.** +> `TOOLBOX_NAME` is resolved at runtime against the project's toolboxes (list them with +> `GET /toolboxes?api-version=v1`). The deployed agent runs under a managed +> identity that `azd` grants `Foundry User` on the project, which is enough to read the toolbox. +> For this sample point `TOOLBOX_NAME` at a toolbox whose tools require per-user OAuth consent, so +> the first invoke streams an `oauth_consent_request` output item and finishes with +> `response.incomplete`; that consent step is the whole point of the sample. + ### Step 4: clean up ``` azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-toolbox-auth-paths?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md index 66b67a5b07..e058435fe4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md @@ -151,12 +151,35 @@ azd ai agent invoke "List the tools you have available." `dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` in `azure.yaml`). No Dockerfile, no container registry. +> **The toolbox must already exist in the project, and the agent identity must be able to read it.** +> `TOOLBOX_NAME` is resolved at runtime against the project's toolboxes (list them with +> `GET /toolboxes?api-version=v1`). The deployed agent runs under a managed +> identity that `azd` grants `Foundry User` on the project, which is enough to read the toolbox. + +> **Toolboxes that contain OAuth-gated tools return a consent request, not an answer.** If the +> toolbox includes MCP tools that require per-user OAuth (for example a GitHub or mail connection), +> the first invoke streams an `oauth_consent_request` output item and finishes with +> `response.incomplete` instead of a final message. That is expected: the caller is meant to +> complete the consent and resume. The sibling [`Hosted-Toolbox-AuthPaths`](../Hosted-Toolbox-AuthPaths/) +> sample and its client show that flow end to end. To see a plain tool answer instead, point +> `TOOLBOX_NAME` at a toolbox whose tools need no consent (for example `web_search` / +> `code_interpreter` only). + ### Step 4: clean up ``` azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-toolbox?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md index 7eaeb76981..f5b449238c 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md @@ -151,12 +151,27 @@ azd ai agent invoke "List the MCP skills available from the toolbox." `dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build` in `azure.yaml`). No Dockerfile, no container registry. +> **The toolbox must already exist in the project, and the agent identity must be able to read it.** +> `TOOLBOX_NAME` is resolved at runtime against the project's toolboxes (list them with +> `GET /toolboxes?api-version=v1`). The deployed agent runs under a managed +> identity that `azd` grants `Foundry User` on the project, which is enough to read the toolbox and +> the MCP skills attached to it. + ### Step 4: clean up ``` azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-toolbox-mcp-skills?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md index 648edfbe53..d2a77f7fd8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md @@ -9,6 +9,15 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the > to Azure OpenAI directly. Set both to a resource your identity can call before running or > deploying. +> **The deployed agent needs its own role on that Azure OpenAI resource.** Because the workflow +> builds its own `AzureOpenAIClient` (a data-plane client) instead of using the Foundry project's +> hosted model, `azd deploy` does **not** grant it access automatically. `azd` only grants the agent +> identity the `Foundry User` role on the project; it does not touch a separate Azure OpenAI account. +> After the first deploy, grant the agent's managed identity the **`Cognitive Services OpenAI User`** +> role on the `AZURE_OPENAI_ENDPOINT` account, or the first model call fails with +> `server_error: An error occurred while executing the workflow.` at the triage step. See +> [Grant the agent access to Azure OpenAI](#grant-the-agent-access-to-azure-openai) below. + ## Files | File | Purpose | @@ -81,6 +90,45 @@ azd ai agent invoke "Write a haiku about the sea." `dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`). No Dockerfile, no container registry. Clean up with `azd down`. +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-workflow-handoff?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + +## Grant the agent access to Azure OpenAI + +The deployed agent runs under a managed identity that Foundry creates. Because this sample calls +Azure OpenAI directly (a data-plane call), that identity needs the `Cognitive Services OpenAI User` +role on the Azure OpenAI account, and `azd` does not grant it. Do this once, after the first +`azd deploy`, and before `azd ai agent invoke`: + +```bash +# 1. Read the agent's managed-identity principal id from the deployed version. +TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +PRINCIPAL=$(curl -s "/agents/hosted-workflow-handoff?api-version=v1" \ + -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" \ + | jq -r '.versions.latest.instance_identity.principal_id') + +# 2. Grant it the data-plane role on the Azure OpenAI account behind AZURE_OPENAI_ENDPOINT. +az role assignment create \ + --assignee-object-id "$PRINCIPAL" --assignee-principal-type ServicePrincipal \ + --role "Cognitive Services OpenAI User" \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/" +``` + +Role assignments take up to a few minutes to propagate. Wait, then +`azd ai agent invoke --new-conversation "Write a haiku about the sea."`. Until the role is in place, +the workflow starts (`HandoffStart` runs) but fails at the triage agent's first model call with +`server_error: An error occurred while executing the workflow.` + +> Creating the role assignment needs `Microsoft.Authorization/roleAssignments/write` on that +> account (for example `Owner` or `User Access Administrator`). If you lack it, ask a resource owner +> to grant the agent's principal id the `Cognitive Services OpenAI User` role. + ## Deploy your local framework changes (contributors) **Skip this unless you are changing the Agent Framework itself.** Run the helper between diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md index 1d4fd09395..095f8a6ba9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md @@ -157,6 +157,15 @@ in `azure.yaml`). No Dockerfile, no container registry. azd down ``` +> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent +> in place. Delete it explicitly with a REST call: +> +> ```bash +> TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) +> curl -X DELETE "/agents/hosted-workflow-simple?api-version=v1&force=true" \ +> -H "Authorization: Bearer $TOKEN" -H "Foundry-Features: HostedAgents=V1Preview" +> ``` + Then delete the working directory. ## Deploy your local framework changes (contributors)