Add Dockerfiles, README, agent yamls and bearer token support to Hosted-Workflow-Handoff

- Add Dockerfile and Dockerfile.contributor for Docker-based testing
- Add agent.yaml and agent.manifest.yaml with triage-workflow as primary agent
- Add README.md following sibling pattern, noting Azure OpenAI vs Foundry endpoint
- Add DevTemporaryTokenCredential and ChainedTokenCredential for Docker auth
- Register triage-workflow as non-keyed default so azd invoke works without model
- Update .env.example with AZURE_BEARER_TOKEN sentinel
- Add .gitignore to 04-hosting to suppress VS-generated launchSettings.json
- Fix docker run image name in Hosted-Workflow-Simple README
This commit is contained in:
Roger Barreto
2026-04-16 13:05:47 +01:00
parent 7c9ed7f5d6
commit 518e90e114
9 changed files with 246 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
**/Properties/launchSettings.json
@@ -1,4 +1,5 @@
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
@@ -0,0 +1,17 @@
# 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"]
@@ -0,0 +1,19 @@
# 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"]
@@ -17,6 +17,7 @@
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
@@ -37,7 +38,9 @@ var builder = WebApplication.CreateBuilder(args);
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 azureClient = new AzureOpenAIClient(endpoint, new DefaultAzureCredential());
var azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential()));
IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment);
// ---------------------------------------------------------------------------
@@ -109,6 +112,10 @@ Workflow triageWorkflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAg
builder.AddAIAgent("triage-workflow", (_, key) =>
triageWorkflow.AsAIAgent(name: key));
// Register triage-workflow as the non-keyed default so azd invoke (no model) works
builder.Services.AddSingleton(sp =>
sp.GetRequiredKeyedService<AIAgent>("triage-workflow"));
// ---------------------------------------------------------------------------
// 4. Wire up the agent-framework handler and Responses Server SDK
// ---------------------------------------------------------------------------
@@ -150,6 +157,13 @@ app.Run();
// Local tool definitions
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Dev-only credential: reads a pre-fetched bearer token from AZURE_BEARER_TOKEN.
// When the value is missing or set to "DefaultAzureCredential", this credential
// throws CredentialUnavailableException so the ChainedTokenCredential falls
// through to DefaultAzureCredential.
// ---------------------------------------------------------------------------
[Description("Gets the current date and time in the specified timezone.")]
static string GetCurrentTime(
[Description("IANA timezone (e.g. 'America/New_York', 'Europe/London', 'UTC'). Defaults to UTC.")]
@@ -178,3 +192,30 @@ static string GetWeather(
var condition = conditions[rng.Next(conditions.Length)];
return $"Weather in {location}: {temp}C, {condition}. Humidity: {rng.Next(30, 90)}%. Wind: {rng.Next(5, 30)} km/h.";
}
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<AccessToken> 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));
}
}
@@ -0,0 +1,126 @@
# Hosted-Workflow-Handoff
A hosted agent server demonstrating two patterns in a single app:
- **`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`
Both agents are served over the Responses protocol. The server also exposes interactive web demos at `/tool-demo` and `/workflow-demo`.
> Unlike the other samples in this folder, this one connects to an **Azure OpenAI** resource directly (not an Azure AI Foundry project endpoint).
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure OpenAI resource with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your values:
```bash
cp .env.example .env
```
Edit `.env`:
```env
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
```
`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.
## Running directly (contributors)
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff
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.
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,30 @@
# 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: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# 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: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -77,7 +77,7 @@ docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-workflow-simple \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-workflows
hosted-workflow-simple
```
### 4. Test it