Files
agent-framework/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs
T
Roger BarretoandGitHub ad95f2f2fa .NET: Add Hosted-MemoryAgent sample with isolation key plumbing (#5692) (#5702)
* .NET: Add Hosted-MemoryAgent sample with isolation key plumbing (#5692)

Adds HostedSessionContext + HostedSessionIsolationKeyProvider in Microsoft.Agents.AI.Foundry.Hosting so AIContextProviders (notably FoundryMemoryProvider) can scope per user via the platform's x-agent-user-isolation-key / x-agent-chat-isolation-key headers.

- New types: HostedSessionContext (sealed), HostedSessionContextExtensions (public Get, internal Set), abstract HostedSessionIsolationKeyProvider (async), internal PlatformHostedSessionIsolationKeyProvider mapping ResponseContext.Isolation.

- AgentFrameworkResponseHandler now resolves the provider, tags fresh sessions, and validates resumed sessions against the live request (strict 403 'Hosted session identity context mismatch' on any mismatch; 500 on null keys).

- New shared sample project Hosted_Shared_Contributor_Setup hosts DevTemporaryTokenCredential and DevTemporaryLocalSessionIsolationKeyProvider plus AddDevTemporaryLocalContributorSetup. All 9 existing responses samples migrated to consume it so local runs keep working under the strict isolation contract.

- New Hosted-MemoryAgent sample: travel assistant wired through FoundryMemoryProvider with stateInitializer reading session.GetHostedContext().UserId. Includes Dockerfile, smoke.ps1, agent.yaml/manifest.

- New IT scenario 'memory' in Foundry.Hosting.IntegrationTests + MemoryHostedAgentFixture + MemoryHostedAgentTests. Verified end to end against the tao Foundry project.

- ADR 0026 captures the design tree.

* Address PR review feedback

- Dockerfile: add header noting it targets NuGet builds; contributors must use Dockerfile.contributor for ProjectReference source builds.

- PlatformHostedSessionIsolationKeyProvider: doc said 'returns context with empty values'; corrected to 'returns null' which the handler treats as 500.

- FakeHostedSessionIsolationKeyProvider: doc clarifies that null configurations are allowed for testing the handler error path.

- HostedSessionContextExtensions.SetHostedContext: enforce write-once with InvalidOperationException; doc + xml exception updated.

- AgentFrameworkResponseHandler: cache PlatformHostedSessionIsolationKeyProvider as static readonly to avoid per-request allocation.

- MemoryHostedAgentTests: tighten waits from 20s to 5s (FoundryMemoryProvider defaults UpdateDelay=0; ingestion ~3s).

- Sample Program.cs imports reordered to satisfy IDE0005.

* Add HostedFoundryMemoryProviderScopes built-in helpers (#5692)

Addresses review feedback from @lokitoth on Hosted-MemoryAgent/Program.cs:54.

- New HostedFoundryMemoryProviderScopes static class with PerUser, PerChat, PerUserAndChat factories returning Func<AgentSession?, FoundryMemoryProvider.State>.

- All helpers throw InvalidOperationException when GetHostedContext() is null, with a message pointing at writing a custom stateInitializer for non-hosted scenarios.

- New HostedFoundryMemoryScope enum and AddHostedFoundryMemoryProvider DI extension (two overloads: explicit AIProjectClient and DI-resolved). Singleton lifetime. Default scope = PerUser.

- Hosted-MemoryAgent sample and the memory IT scenario container both swap their inline lambdas for HostedFoundryMemoryProviderScopes.PerUser().

- 14 new unit tests (241/241 hosting unit tests pass).

* Replace HostedFoundryMemoryScope enum with Func<...> parameter (#5692)

Address PR review feedback from @westey-m: enums are a breaking-change hazard when extended, and the enum was redundant with the existing HostedFoundryMemoryProviderScopes static class.

- Delete HostedFoundryMemoryScope.cs.

- AddHostedFoundryMemoryProvider DI extensions now take Func<AgentSession?, FoundryMemoryProvider.State>? stateInitializer = null. When null, default to HostedFoundryMemoryProviderScopes.PerUser().

- Callers pick a built-in helper (PerUser/PerChat/PerUserAndChat) or pass a custom delegate. New built-ins are a single static method addition with zero impact on existing callers.

- Tests updated; 244/244 hosting unit tests pass.

* Fix isolation context resume for externally-created conversations (#5692)

Branch on the session's existing hosted-context (not on conversation_id presence) so a conversation provisioned externally (e.g. via conversations.CreateProjectConversationAsync) is treated as fresh on first hosted-agent request and stamped, rather than rejected with 403 hosted_session_identity_mismatch. Strict equality is preserved on real resume of an already-stamped session.

Also tighten dotnet/global.json to version 10.0.204 + rollForward latestPatch so local builds match the CI Docker image SDK and avoid 10.0.300 dotnet format stripping required usings.

* Revert global.json SDK pin to upstream (#5692)

The 10.0.204 + latestPatch pin from the previous commit broke the dotnet-format CI job (hostfxr_resolve_sdk2 could not find a compatible SDK in the mcr.microsoft.com/dotnet/sdk:10.0 image). Restore upstream 10.0.200 + minor; local Release builds with SDK 10.0.300 should set GITHUB_ACTIONS=true to bypass the auto-format-on-build target.
2026-05-15 05:42:12 +00:00

195 lines
8.3 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents
// using the Azure AI Responses Server SDK.
//
// Demos:
// / - Homepage listing all demos
// /tool-demo - Agent with local tools + remote MCP tools
// /workflow-demo - Triage workflow routing to specialist agents
//
// Prerequisites:
// - Azure OpenAI resource with a deployed model
//
// Environment variables:
// - AZURE_OPENAI_ENDPOINT - your Azure OpenAI endpoint
// - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o")
using System.ComponentModel;
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;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var builder = WebApplication.CreateBuilder(args);
// ---------------------------------------------------------------------------
// 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 azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential()));
IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment);
// ---------------------------------------------------------------------------
// 2. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP
// ---------------------------------------------------------------------------
Console.WriteLine("Connecting to Microsoft Learn MCP server...");
McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "Microsoft Learn MCP",
}));
var mcpTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
builder.AddAIAgent(
name: "tool-agent",
instructions: """
You are a helpful assistant hosted as a Foundry Hosted Agent.
You have access to several tools - use them proactively:
- GetCurrentTime: Returns the current date/time in any timezone.
- GetWeather: Returns weather conditions for any location.
- Microsoft Learn MCP tools: Search and fetch Microsoft documentation.
When a user asks a technical question about Microsoft products, use the
documentation search tools to give accurate, up-to-date answers.
""",
chatClient: chatClient)
.WithAITool(AIFunctionFactory.Create(GetCurrentTime))
.WithAITool(AIFunctionFactory.Create(GetWeather))
.WithAITools(mcpTools.Cast<AITool>().ToArray());
// ---------------------------------------------------------------------------
// 3. DEMO 2: Triage Workflow — routes to specialist agents
// ---------------------------------------------------------------------------
ChatClientAgent triageAgent = new(
chatClient,
instructions: """
You are a triage agent that determines which specialist to hand off to.
Based on the user's question, ALWAYS hand off to one of the available agents.
Do NOT answer the question yourself - just route it.
""",
name: "triage_agent",
description: "Routes messages to the appropriate specialist agent");
ChatClientAgent codeExpert = new(
chatClient,
instructions: """
You are a coding and technology expert. You help with programming questions,
explain technical concepts, debug code, and suggest best practices.
Provide clear, well-structured answers with code examples when appropriate.
""",
name: "code_expert",
description: "Specialist agent for programming and technology questions");
ChatClientAgent creativeWriter = new(
chatClient,
instructions: """
You are a creative writing specialist. You help write stories, poems,
marketing copy, emails, and other creative content. You have a flair
for engaging language and vivid descriptions.
""",
name: "creative_writer",
description: "Specialist agent for creative writing and content tasks");
Workflow triageWorkflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
.WithHandoffs(triageAgent, [codeExpert, creativeWriter])
.WithHandoffs([codeExpert, creativeWriter], triageAgent)
.Build();
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
// ---------------------------------------------------------------------------
builder.Services.AddFoundryResponses();
var app = builder.Build();
// Dispose the MCP client on shutdown
app.Lifetime.ApplicationStopping.Register(() =>
mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult());
// ---------------------------------------------------------------------------
// 5. Routes
// ---------------------------------------------------------------------------
app.MapGet("/ready", () => Results.Ok("ready"));
app.MapFoundryResponses();
app.MapGet("/", () => Results.Content(Pages.Home, "text/html"));
app.MapGet("/tool-demo", () => Results.Content(Pages.ToolDemo, "text/html"));
app.MapGet("/workflow-demo", () => Results.Content(Pages.WorkflowDemo, "text/html"));
app.MapGet("/js/sse-validator.js", () => Results.Content(Pages.ValidationScript, "application/javascript"));
// Validation endpoint: accepts captured SSE lines and validates them
app.MapPost("/api/validate", (HostedWorkflowHandoff.CapturedSseStream captured) =>
{
var validator = new HostedWorkflowHandoff.ResponseStreamValidator();
foreach (var evt in captured.Events)
{
validator.ProcessEvent(evt.EventType, evt.Data);
}
validator.Complete();
return Results.Json(validator.GetResult());
});
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.")]
string timezone = "UTC")
{
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz).ToString("F");
}
catch
{
return DateTime.UtcNow.ToString("F") + " (UTC - unknown timezone: " + timezone + ")";
}
}
[Description("Gets the current weather for a location. Returns temperature, conditions, and humidity.")]
static string GetWeather(
[Description("The city or location (e.g. 'Seattle', 'London, UK').")]
string location)
{
// Simulated weather - deterministic per location for demo consistency
var rng = new Random(location.ToUpperInvariant().GetHashCode());
var temp = rng.Next(-5, 35);
string[] conditions = ["sunny", "partly cloudy", "overcast", "rainy", "snowy", "windy", "foggy"];
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.";
}