mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
@@ -0,0 +1 @@
|
||||
**/Properties/launchSettings.json
|
||||
+1
@@ -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
|
||||
|
||||
+17
@@ -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"]
|
||||
+19
@@ -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"]
|
||||
+42
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+126
@@ -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.
|
||||
+30
@@ -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: []
|
||||
+9
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user