From 0b1ed03cd00c997cd668bb19a68776a5616a0fd2 Mon Sep 17 00:00:00 2001 From: alliscode Date: Tue, 31 Mar 2026 14:57:00 -0700 Subject: [PATCH 01/75] Add Azure AI Foundry Responses hosting adapter Implement Microsoft.Agents.AI.Hosting.AzureAIResponses to host agent-framework AIAgents and workflows within Azure Foundry as hosted agents via the Azure.AI.AgentServer.Responses SDK. - AgentFrameworkResponseHandler: bridges ResponseHandler to AIAgent execution - InputConverter: converts Responses API inputs/history to MEAI ChatMessage - OutputConverter: converts agent response updates to SSE event stream - ServiceCollectionExtensions: DI registration helpers - 336 unit tests across net8.0/net9.0/net10.0 (112 per TFM) - ResponseStreamValidator: SSE protocol validation tool for samples - FoundryResponsesHosting sample app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 4 + dotnet/agent-framework-dotnet.slnx | 5 + dotnet/nuget.config | 4 + .../FoundryResponsesHosting.csproj | 27 + .../FoundryResponsesHosting/Pages.cs | 470 +++++++ .../FoundryResponsesHosting/Program.cs | 183 +++ .../Properties/launchSettings.json | 12 + .../ResponseStreamValidator.cs | 601 +++++++++ .../AgentFrameworkResponseHandler.cs | 186 +++ .../InputConverter.cs | 296 +++++ ....Agents.AI.Hosting.AzureAIResponses.csproj | 40 + .../OutputConverter.cs | 346 ++++++ .../ServiceCollectionExtensions.cs | 78 ++ .../AgentFrameworkResponseHandlerTests.cs | 815 +++++++++++++ .../InputConverterTests.cs | 671 ++++++++++ ....Hosting.AzureAIResponses.UnitTests.csproj | 17 + .../OutputConverterTests.cs | 1080 +++++++++++++++++ .../ServiceCollectionExtensionsTests.cs | 72 ++ .../WorkflowIntegrationTests.cs | 509 ++++++++ 19 files changed, 5416 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 1d3c2608b9..20dbf32bb6 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,13 @@ + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 24b596509e..3d0465763f 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -261,6 +261,9 @@ + + + @@ -491,6 +494,7 @@ + @@ -537,6 +541,7 @@ + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 76d943ce16..202c1fc671 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,8 +3,12 @@ + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj new file mode 100644 index 0000000000..6725ef8d3b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -0,0 +1,27 @@ + + + + Exe + net10.0 + enable + enable + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs new file mode 100644 index 0000000000..bff2c62e99 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. + +/// +/// Static HTML pages served by the sample application. +/// +internal static class Pages +{ + // ═══════════════════════════════════════════════════════════════════════ + // Homepage + // ═══════════════════════════════════════════════════════════════════════ + + internal const string Home = """ + + + + + Foundry Responses Hosting — Demos + + + +
+

🚀 Foundry Responses Hosting

+

+ Agent-framework agents hosted via the Azure AI Responses Server SDK.
+ Each demo registers a different agent and serves it through POST /responses. +

+ +
+ All demos share the same /responses endpoint. + The model field in the request selects which agent handles it. +
+
+ + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // Tool Demo + // ═══════════════════════════════════════════════════════════════════════ + + internal const string ToolDemo = """ + + + + + Tool Demo — Foundry Responses Hosting + + + +
+ ← Back to demos +

🔧 Tool Demo

+

Agent with local tools (time, weather) + Microsoft Learn MCP (docs search)

+
+ + + + +
+
+
+ + +
+
+
+ + + + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // Workflow Demo + // ═══════════════════════════════════════════════════════════════════════ + + internal const string WorkflowDemo = """ + + + + + Workflow Demo — Foundry Responses Hosting + + + +
+ ← Back to demos +

🔀 Workflow Demo — Agent Handoffs

+

A triage agent routes your question to a specialist (Code Expert or Creative Writer)

+
+
👤 User → 🔀 Triage → 💻 Code Expert / ✍️ Creative Writer
+
+
+ + + + +
+
+
+ + +
+
+
+ + + + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // SSE Validator Script (shared by all demo pages) + // ═══════════════════════════════════════════════════════════════════════ + + internal const string ValidationScript = """ +// SseValidator - inline SSE stream validation for Foundry Responses demos +// Captures events during streaming and validates against the API behaviour contract. +(function() { + const style = document.createElement('style'); + style.textContent = ` + .sse-val { margin: .4rem 0 .6rem; padding: .3rem .5rem; font-size: .75rem; color: #aaa; border-top: 1px dashed #e8e8e8; } + .val-ok { color: #7ab88a; } + .val-err { color: #d47272; font-weight: 500; } + .val-issues { margin: .2rem 0; } + .val-issue { color: #c06060; font-size: .72rem; padding: .1rem 0; } + .val-issue b { color: #b04040; } + .val-at { color: #ccc; font-size: .68rem; } + .val-log summary { cursor: pointer; color: #bbb; font-size: .72rem; } + .val-log-items { max-height: 120px; overflow-y: auto; font-size: .7rem; background: #fafafa; + padding: .3rem; border-radius: 3px; margin-top: .15rem; + font-family: 'Cascadia Code', 'Fira Code', monospace; } + .val-i { color: #ccc; display: inline-block; width: 1.8rem; text-align: right; margin-right: .3rem; } + .val-t { color: #8ab4d0; } + `; + document.head.appendChild(style); +})(); + +class SseValidator { + constructor() { this.events = []; } + reset() { this.events = []; } + capture(eventType, data) { this.events.push({ eventType, data }); } + + async validate() { + const resp = await fetch('/api/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ events: this.events }) + }); + return await resp.json(); + } + + renderElement(result) { + const el = document.createElement('div'); + el.className = 'sse-val'; + const n = result.eventCount; + const ok = result.isValid; + const vs = result.violations || []; + const esc = s => String(s).replace(/&/g,'&').replace(//g,'>'); + + let h = ok + ? `${n} events — all rules passed ✅` + : `${n} events — ${vs.length} violation(s)`; + + if (vs.length) { + h += '
'; + vs.forEach(v => { + h += `
[${esc(v.ruleId)}] ${esc(v.message)} #${v.eventIndex}
`; + }); + h += '
'; + } + + h += `
Event log (${this.events.length})
`; + this.events.forEach((e, i) => { + h += `
${i} ${esc(e.eventType)}
`; + }); + h += '
'; + + el.innerHTML = h; + return el; + } +} +"""; +} diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs new file mode 100644 index 0000000000..32f39f641c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -0,0 +1,183 @@ +// 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.AI.AgentServer.Responses; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Agents.AI.Hosting.AzureAIResponses; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; + +var builder = WebApplication.CreateBuilder(args); + +// --------------------------------------------------------------------------- +// 1. Register the Azure AI Responses Server SDK +// --------------------------------------------------------------------------- +builder.Services.AddResponsesServer(); + +// --------------------------------------------------------------------------- +// 2. 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 DefaultAzureCredential()); +IChatClient chatClient = azureClient.GetChatClient(deployment).AsIChatClient(); + +// --------------------------------------------------------------------------- +// 3. 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().ToArray()); + +// --------------------------------------------------------------------------- +// 4. 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)); + +// --------------------------------------------------------------------------- +// 5. Wire up the agent-framework handler as the IResponseHandler +// --------------------------------------------------------------------------- +builder.Services.AddAgentFrameworkHandler(); + +var app = builder.Build(); + +// Dispose the MCP client on shutdown +app.Lifetime.ApplicationStopping.Register(() => + mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult()); + +// --------------------------------------------------------------------------- +// 6. Routes +// --------------------------------------------------------------------------- +app.MapGet("/ready", () => Results.Ok("ready")); +app.MapResponsesServer(); + +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", (FoundryResponsesHosting.CapturedSseStream captured) => +{ + var validator = new FoundryResponsesHosting.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 +// --------------------------------------------------------------------------- + +[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."; +} diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json new file mode 100644 index 0000000000..b56d7a9ff4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "FoundryResponsesHosting": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:54747;http://localhost:54748" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs new file mode 100644 index 0000000000..72da677f45 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs @@ -0,0 +1,601 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FoundryResponsesHosting; + +/// Captured SSE event for validation. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")] +internal sealed record CapturedSseEvent( + [property: JsonPropertyName("eventType")] string EventType, + [property: JsonPropertyName("data")] string Data); + +/// Captured SSE stream sent from the client for server-side validation. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")] +internal sealed record CapturedSseStream( + [property: JsonPropertyName("events")] List Events); + +/// +/// Validates an SSE event stream from the Azure AI Responses Server SDK against +/// the API behaviour contract. Feed events sequentially via +/// and call when the stream ends. +/// +internal sealed class ResponseStreamValidator +{ + private readonly List _violations = []; + private int _eventCount; + private int _expectedSequenceNumber; + private StreamState _state = StreamState.Initial; + private string? _responseId; + private readonly HashSet _addedItemIndices = []; + private readonly HashSet _doneItemIndices = []; + private readonly HashSet _addedContentParts = []; // "outputIdx:partIdx" + private readonly HashSet _doneContentParts = []; + private readonly Dictionary _textAccumulators = []; // "outputIdx:contentIdx" → accumulated text + private bool _hasTerminal; + + /// All violations found so far. + internal IReadOnlyList Violations => _violations; + + /// + /// Processes a single SSE event line pair (event type + JSON data). + /// + /// The SSE event type (e.g. "response.created"). + /// The raw JSON data payload. + internal void ProcessEvent(string eventType, string jsonData) + { + JsonElement data; + try + { + data = JsonDocument.Parse(jsonData).RootElement; + } + catch (JsonException ex) + { + Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); + return; + } + + _eventCount++; + + // ── Sequence number validation ────────────────────────────────── + if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number) + { + int seq = seqProp.GetInt32(); + if (seq != _expectedSequenceNumber) + { + Fail("SEQ-01", $"Expected sequence_number {_expectedSequenceNumber}, got {seq}"); + } + + _expectedSequenceNumber = seq + 1; + } + else if (_state != StreamState.Initial || eventType != "error") + { + // Pre-creation error events may not have sequence_number + Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); + } + + // ── Post-terminal guard ───────────────────────────────────────── + if (_hasTerminal) + { + Fail("TERM-01", $"Event '{eventType}' received after terminal event"); + return; + } + + // ── Dispatch by event type ────────────────────────────────────── + switch (eventType) + { + case "response.created": + ValidateResponseCreated(data); + break; + + case "response.queued": + ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); + ValidateResponseEnvelope(data, eventType); + break; + + case "response.in_progress": + if (_state is StreamState.Created or StreamState.Queued) + { + _state = StreamState.InProgress; + } + else + { + Fail("ORDER-02", $"'response.in_progress' received in state {_state} (expected Created or Queued)"); + } + + ValidateResponseEnvelope(data, eventType); + break; + + case "response.output_item.added": + case "output_item.added": + ValidateInProgress(eventType); + ValidateOutputItemAdded(data); + break; + + case "response.output_item.done": + case "output_item.done": + ValidateInProgress(eventType); + ValidateOutputItemDone(data); + break; + + case "response.content_part.added": + case "content_part.added": + ValidateInProgress(eventType); + ValidateContentPartAdded(data); + break; + + case "response.content_part.done": + case "content_part.done": + ValidateInProgress(eventType); + ValidateContentPartDone(data); + break; + + case "response.output_text.delta": + case "output_text.delta": + ValidateInProgress(eventType); + ValidateTextDelta(data); + break; + + case "response.output_text.done": + case "output_text.done": + ValidateInProgress(eventType); + ValidateTextDone(data); + break; + + case "response.function_call_arguments.delta": + case "function_call_arguments.delta": + ValidateInProgress(eventType); + break; + + case "response.function_call_arguments.done": + case "function_call_arguments.done": + ValidateInProgress(eventType); + break; + + case "response.completed": + ValidateTerminal(data, "completed"); + break; + + case "response.failed": + ValidateTerminal(data, "failed"); + break; + + case "response.incomplete": + ValidateTerminal(data, "incomplete"); + break; + + case "error": + // Pre-creation error — standalone, no response.created precedes it + if (_state != StreamState.Initial) + { + Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); + } + + _hasTerminal = true; + break; + + default: + // Unknown events are not violations — the spec may evolve + break; + } + } + + /// + /// Call after the stream ends. Checks that a terminal event was received. + /// + internal void Complete() + { + if (!_hasTerminal && _state != StreamState.Initial) + { + Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); + } + + if (_state == StreamState.Initial && _eventCount == 0) + { + Fail("EMPTY-01", "No events received in the stream"); + } + + // Check for output items that were added but never completed + foreach (int idx in _addedItemIndices) + { + if (!_doneItemIndices.Contains(idx)) + { + Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); + } + } + + // Check for content parts that were added but never completed + foreach (string key in _addedContentParts) + { + if (!_doneContentParts.Contains(key)) + { + Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); + } + } + } + + /// + /// Returns a summary of all validation results. + /// + internal ValidationResult GetResult() + { + return new ValidationResult( + EventCount: _eventCount, + IsValid: _violations.Count == 0, + Violations: [.. _violations]); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Event-specific validators + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateResponseCreated(JsonElement data) + { + if (_state != StreamState.Initial) + { + Fail("ORDER-01", $"'response.created' received in state {_state} (expected Initial — must be first event)"); + return; + } + + _state = StreamState.Created; + + // Must have a response envelope + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", "'response.created' missing 'response' object"); + return; + } + + // Required response fields + ValidateRequiredResponseFields(resp, "response.created"); + + // Capture response ID for cross-event checks + if (resp.TryGetProperty("id", out var idProp)) + { + _responseId = idProp.GetString(); + } + + // Status must be non-terminal + if (resp.TryGetProperty("status", out var statusProp)) + { + string? status = statusProp.GetString(); + if (status is "completed" or "failed" or "incomplete" or "cancelled") + { + Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); + } + } + } + + private void ValidateTerminal(JsonElement data, string expectedKind) + { + if (_state is StreamState.Initial or StreamState.Created) + { + Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); + } + + _hasTerminal = true; + _state = StreamState.Terminal; + + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); + return; + } + + ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); + + if (resp.TryGetProperty("status", out var statusProp)) + { + string? status = statusProp.GetString(); + + // completed_at validation (B6) + bool hasCompletedAt = resp.TryGetProperty("completed_at", out var catProp) + && catProp.ValueKind != JsonValueKind.Null; + + if (status == "completed" && !hasCompletedAt) + { + Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); + } + + if (status != "completed" && hasCompletedAt) + { + Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); + } + + // error field validation + bool hasError = resp.TryGetProperty("error", out var errProp) + && errProp.ValueKind != JsonValueKind.Null; + + if (status == "failed" && !hasError) + { + Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); + } + + if (status is "completed" or "incomplete" && hasError) + { + Fail("FIELD-05", $"'error' must be null when status is '{status}'"); + } + + // error structure validation + if (hasError) + { + ValidateErrorObject(errProp, $"response.{expectedKind}"); + } + + // cancelled output must be empty (B11) + if (status == "cancelled" && resp.TryGetProperty("output", out var outputProp) + && outputProp.ValueKind == JsonValueKind.Array && outputProp.GetArrayLength() > 0) + { + Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); + } + + // response ID consistency + if (_responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != _responseId) + { + Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + } + } + + // Usage validation (optional, but if present must be structured correctly) + if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) + { + ValidateUsage(usageProp, $"response.{expectedKind}"); + } + } + + private void ValidateOutputItemAdded(JsonElement data) + { + if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) + { + int index = idxProp.GetInt32(); + if (!_addedItemIndices.Add(index)) + { + Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); + } + } + else + { + Fail("FIELD-06", "output_item.added missing 'output_index' field"); + } + + if (!data.TryGetProperty("item", out _)) + { + Fail("FIELD-07", "output_item.added missing 'item' object"); + } + } + + private void ValidateOutputItemDone(JsonElement data) + { + if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) + { + int index = idxProp.GetInt32(); + if (!_addedItemIndices.Contains(index)) + { + Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); + } + + _doneItemIndices.Add(index); + } + else + { + Fail("FIELD-06", "output_item.done missing 'output_index' field"); + } + } + + private void ValidateContentPartAdded(JsonElement data) + { + string key = GetContentPartKey(data); + if (!_addedContentParts.Add(key)) + { + Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); + } + } + + private void ValidateContentPartDone(JsonElement data) + { + string key = GetContentPartKey(data); + if (!_addedContentParts.Contains(key)) + { + Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); + } + + _doneContentParts.Add(key); + } + + private void ValidateTextDelta(JsonElement data) + { + string key = GetTextKey(data); + string delta = data.TryGetProperty("delta", out var deltaProp) + ? deltaProp.GetString() ?? string.Empty + : string.Empty; + + if (!_textAccumulators.TryGetValue(key, out string? existing)) + { + _textAccumulators[key] = delta; + } + else + { + _textAccumulators[key] = existing + delta; + } + } + + private void ValidateTextDone(JsonElement data) + { + string key = GetTextKey(data); + string? finalText = data.TryGetProperty("text", out var textProp) + ? textProp.GetString() + : null; + + if (finalText is null) + { + Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); + return; + } + + if (_textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) + { + Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Shared field validators + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateRequiredResponseFields(JsonElement resp, string context) + { + if (!HasNonNullString(resp, "id")) + { + Fail("FIELD-01", $"{context}: response missing 'id'"); + } + + if (resp.TryGetProperty("object", out var objProp)) + { + if (objProp.GetString() != "response") + { + Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); + } + } + else + { + Fail("FIELD-08", $"{context}: response missing 'object' field"); + } + + if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null) + { + Fail("FIELD-09", $"{context}: response missing 'created_at'"); + } + + if (!resp.TryGetProperty("status", out _)) + { + Fail("FIELD-10", $"{context}: response missing 'status'"); + } + + if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array) + { + Fail("FIELD-11", $"{context}: response missing 'output' array"); + } + } + + private void ValidateErrorObject(JsonElement error, string context) + { + if (!HasNonNullString(error, "code")) + { + Fail("ERR-02", $"{context}: error object missing 'code' field"); + } + + if (!HasNonNullString(error, "message")) + { + Fail("ERR-03", $"{context}: error object missing 'message' field"); + } + } + + private void ValidateUsage(JsonElement usage, string context) + { + if (!usage.TryGetProperty("input_tokens", out _)) + { + Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); + } + + if (!usage.TryGetProperty("output_tokens", out _)) + { + Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); + } + + if (!usage.TryGetProperty("total_tokens", out _)) + { + Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); + } + } + + private void ValidateResponseEnvelope(JsonElement data, string eventType) + { + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", $"'{eventType}' missing 'response' object"); + return; + } + + ValidateRequiredResponseFields(resp, eventType); + + // Response ID consistency + if (_responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != _responseId) + { + Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Helpers + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateInProgress(string eventType) + { + if (_state != StreamState.InProgress) + { + Fail("ORDER-04", $"'{eventType}' received in state {_state} (expected InProgress)"); + } + } + + private void ValidateStateTransition(string eventType, StreamState expected, StreamState next) + { + if (_state != expected) + { + Fail("ORDER-05", $"'{eventType}' received in state {_state} (expected {expected})"); + } + else + { + _state = next; + } + } + + private void Fail(string ruleId, string message) + { + _violations.Add(new ValidationViolation(ruleId, message, _eventCount)); + } + + private static bool HasNonNullString(JsonElement obj, string property) + { + return obj.TryGetProperty(property, out var prop) + && prop.ValueKind == JsonValueKind.String + && !string.IsNullOrEmpty(prop.GetString()); + } + + private static string GetContentPartKey(JsonElement data) + { + int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1; + int partIdx = data.TryGetProperty("content_index", out var pi) ? pi.GetInt32() : -1; + return $"{outputIdx}:{partIdx}"; + } + + private static string GetTextKey(JsonElement data) + { + int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1; + int contentIdx = data.TryGetProperty("content_index", out var ci) ? ci.GetInt32() : -1; + return $"{outputIdx}:{contentIdx}"; + } + + private enum StreamState + { + Initial, + Created, + Queued, + InProgress, + Terminal, + } +} + +/// A single validation violation. +/// The rule identifier (e.g. SEQ-01, FIELD-02). +/// Human-readable description of the violation. +/// 1-based index of the event that triggered this violation. +internal sealed record ValidationViolation(string RuleId, string Message, int EventIndex); + +/// Overall validation result. +/// Total number of events processed. +/// True if no violations were found. +/// List of all violations. +internal sealed record ValidationResult(int EventCount, bool IsValid, IReadOnlyList Violations); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs new file mode 100644 index 0000000000..5f07cd4530 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// A implementation that bridges the Azure AI Responses Server SDK +/// with agent-framework instances, enabling agent-framework agents and workflows +/// to be hosted as Azure Foundry Hosted Agents. +/// +public class AgentFrameworkResponseHandler : ResponseHandler +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// that resolves agents from keyed DI services. + /// + /// The service provider for resolving agents. + /// The logger instance. + public AgentFrameworkResponseHandler( + IServiceProvider serviceProvider, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(logger); + + this._serviceProvider = serviceProvider; + this._logger = logger; + } + + /// + public override async IAsyncEnumerable CreateAsync( + CreateResponse request, + ResponseContext context, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // 1. Resolve agent + var agent = this.ResolveAgent(request); + + // 2. Create the SDK event stream builder + var stream = new ResponseEventStream(context, request); + + // 3. Emit lifecycle events + yield return stream.EmitCreated(); + yield return stream.EmitInProgress(); + + // 4. Convert input: history + current input → ChatMessage[] + var messages = new List(); + + // Load conversation history if available + var history = await context.GetHistoryAsync(cancellationToken).ConfigureAwait(false); + if (history.Count > 0) + { + messages.AddRange(InputConverter.ConvertOutputItemsToMessages(history)); + } + + // Load and convert current input items + var inputItems = await context.GetInputItemsAsync(cancellationToken).ConfigureAwait(false); + if (inputItems.Count > 0) + { + messages.AddRange(InputConverter.ConvertOutputItemsToMessages(inputItems)); + } + else + { + // Fall back to raw request input + messages.AddRange(InputConverter.ConvertInputToMessages(request)); + } + + // 5. Build chat options + var chatOptions = InputConverter.ConvertToChatOptions(request); + chatOptions.Instructions = request.Instructions; + var options = new ChatClientAgentRunOptions(chatOptions); + + // 6. Run the agent and convert output + // NOTE: C# forbids 'yield return' inside a try block that has a catch clause, + // and inside catch blocks. We use a flag to defer the yield to outside the try/catch. + bool emittedTerminal = false; + var enumerator = OutputConverter.ConvertUpdatesToEventsAsync( + agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken), + stream, + cancellationToken).GetAsyncEnumerator(cancellationToken); + try + { + while (true) + { + bool shutdownDetected = false; + ResponseStreamEvent? evt = null; + try + { + if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + break; + } + + evt = enumerator.Current; + } + catch (OperationCanceledException) when (context.IsShutdownRequested && !emittedTerminal) + { + shutdownDetected = true; + } + + if (shutdownDetected) + { + // Server is shutting down — emit incomplete so clients can resume + this._logger.LogInformation("Shutdown detected, emitting incomplete response."); + yield return stream.EmitIncomplete(); + yield break; + } + + // yield is in the outer try (finally-only) — allowed by C# + yield return evt!; + + if (evt is ResponseCompletedEvent or ResponseFailedEvent or ResponseIncompleteEvent) + { + emittedTerminal = true; + } + } + } + finally + { + await enumerator.DisposeAsync().ConfigureAwait(false); + } + } + + /// + /// Resolves an from the request. + /// Tries agent.name first, then falls back to metadata["entity_id"]. + /// If neither is present, attempts to resolve a default (non-keyed) . + /// + private AIAgent ResolveAgent(CreateResponse request) + { + var agentName = GetAgentName(request); + + if (!string.IsNullOrEmpty(agentName)) + { + var agent = this._serviceProvider.GetKeyedService(agentName); + if (agent is not null) + { + return agent; + } + + this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } + + // Try non-keyed default + var defaultAgent = this._serviceProvider.GetService(); + if (defaultAgent is not null) + { + return defaultAgent; + } + + var errorMessage = string.IsNullOrEmpty(agentName) + ? "No agent name specified in the request (via agent.name or metadata[\"entity_id\"]) and no default AIAgent is registered." + : $"Agent '{agentName}' not found. Ensure it is registered via AddAIAgent(\"{agentName}\", ...) or as a default AIAgent."; + + throw new InvalidOperationException(errorMessage); + } + + private static string? GetAgentName(CreateResponse request) + { + // Try agent.name from AgentReference + var agentName = request.AgentReference?.Name; + + // Fall back to "model" field (OpenAI clients send the agent name as the model) + if (string.IsNullOrEmpty(agentName)) + { + agentName = request.Model; + } + + // Fall back to metadata["entity_id"] + if (string.IsNullOrEmpty(agentName) && request.Metadata?.AdditionalProperties is not null) + { + request.Metadata.AdditionalProperties.TryGetValue("entity_id", out agentName); + } + + return agentName; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs new file mode 100644 index 0000000000..a35c8cd5b8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Converts Responses Server SDK input types to agent-framework types. +/// +internal static class InputConverter +{ + /// + /// Converts the SDK request input items into a list of . + /// + /// The create response request from the SDK. + /// A list of chat messages representing the request input. + public static List ConvertInputToMessages(CreateResponse request) + { + var messages = new List(); + + foreach (var item in request.GetInputExpanded()) + { + var message = ConvertInputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + + /// + /// Converts resolved SDK history/input items into instances. + /// + /// The resolved output items from the SDK context. + /// A list of chat messages. + public static List ConvertOutputItemsToMessages(IReadOnlyList items) + { + var messages = new List(); + + foreach (var item in items) + { + var message = ConvertOutputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + + /// + /// Creates from the SDK request properties. + /// + /// The create response request. + /// A configured instance. + public static ChatOptions ConvertToChatOptions(CreateResponse request) + { + return new ChatOptions + { + Temperature = (float?)request.Temperature, + TopP = (float?)request.TopP, + MaxOutputTokens = (int?)request.MaxOutputTokens, + ModelId = request.Model, + }; + } + + private static ChatMessage? ConvertInputItemToMessage(Item item) + { + return item switch + { + ItemMessage msg => ConvertItemMessage(msg), + FunctionCallOutputItemParam funcOutput => ConvertFunctionCallOutput(funcOutput), + ItemFunctionToolCall funcCall => ConvertItemFunctionToolCall(funcCall), + ItemReferenceParam => null, + _ => null + }; + } + + private static ChatMessage ConvertItemMessage(ItemMessage msg) + { + var role = ConvertMessageRole(msg.Role); + var contents = new List(); + + foreach (var content in msg.GetContentExpanded()) + { + switch (content) + { + case MessageContentInputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentInputImageContent imageContent: + if (imageContent.ImageUrl is not null) + { + var url = imageContent.ImageUrl.ToString(); + if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + contents.Add(new DataContent(url, "image/*")); + } + else + { + contents.Add(new UriContent(imageContent.ImageUrl, "image/*")); + } + } + else if (!string.IsNullOrEmpty(imageContent.FileId)) + { + contents.Add(new HostedFileContent(imageContent.FileId)); + } + + break; + case MessageContentInputFileContent fileContent: + if (fileContent.FileUrl is not null) + { + contents.Add(new UriContent(fileContent.FileUrl, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileData)) + { + contents.Add(new DataContent(fileContent.FileData, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileId)) + { + contents.Add(new HostedFileContent(fileContent.FileId)); + } + else if (!string.IsNullOrEmpty(fileContent.Filename)) + { + contents.Add(new MeaiTextContent($"[File: {fileContent.Filename}]")); + } + + break; + } + } + + if (contents.Count == 0) + { + contents.Add(new MeaiTextContent(string.Empty)); + } + + return new ChatMessage(role, contents); + } + + private static ChatMessage ConvertFunctionCallOutput(FunctionCallOutputItemParam funcOutput) + { + var output = funcOutput.Output?.ToString() ?? string.Empty; + return new ChatMessage( + ChatRole.Tool, + [new FunctionResultContent(funcOutput.CallId, output)]); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK input.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK input.")] + private static ChatMessage ConvertItemFunctionToolCall(ItemFunctionToolCall funcCall) + { + IDictionary? arguments = null; + if (funcCall.Arguments is not null) + { + try + { + arguments = JsonSerializer.Deserialize>(funcCall.Arguments); + } + catch (JsonException) + { + arguments = new Dictionary { ["_raw"] = funcCall.Arguments }; + } + } + + return new ChatMessage( + ChatRole.Assistant, + [new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]); + } + + private static ChatMessage? ConvertOutputItemToMessage(OutputItem item) + { + return item switch + { + OutputItemMessage msg => ConvertOutputItemMessageToChat(msg), + OutputItemFunctionToolCall funcCall => ConvertOutputItemFunctionCall(funcCall), + FunctionToolCallOutputResource funcOutput => ConvertFunctionToolCallOutputResource(funcOutput), + OutputItemReasoningItem => null, + _ => null + }; + } + + private static ChatMessage ConvertOutputItemMessageToChat(OutputItemMessage msg) + { + var role = ConvertMessageRole(msg.Role); + var contents = new List(); + + foreach (var content in msg.Content) + { + switch (content) + { + case MessageContentInputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentOutputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentRefusalContent refusal: + contents.Add(new MeaiTextContent($"[Refusal: {refusal.Refusal}]")); + break; + case MessageContentInputImageContent imageContent: + if (imageContent.ImageUrl is not null) + { + var url = imageContent.ImageUrl.ToString(); + if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + contents.Add(new DataContent(url, "image/*")); + } + else + { + contents.Add(new UriContent(imageContent.ImageUrl, "image/*")); + } + } + else if (!string.IsNullOrEmpty(imageContent.FileId)) + { + contents.Add(new HostedFileContent(imageContent.FileId)); + } + + break; + case MessageContentInputFileContent fileContent: + if (fileContent.FileUrl is not null) + { + contents.Add(new UriContent(fileContent.FileUrl, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileData)) + { + contents.Add(new DataContent(fileContent.FileData, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileId)) + { + contents.Add(new HostedFileContent(fileContent.FileId)); + } + else if (!string.IsNullOrEmpty(fileContent.Filename)) + { + contents.Add(new MeaiTextContent($"[File: {fileContent.Filename}]")); + } + + break; + } + } + + if (contents.Count == 0) + { + contents.Add(new MeaiTextContent(string.Empty)); + } + + return new ChatMessage(role, contents); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK output history.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK output history.")] + private static ChatMessage ConvertOutputItemFunctionCall(OutputItemFunctionToolCall funcCall) + { + IDictionary? arguments = null; + if (funcCall.Arguments is not null) + { + try + { + arguments = JsonSerializer.Deserialize>(funcCall.Arguments); + } + catch (JsonException) + { + arguments = new Dictionary { ["_raw"] = funcCall.Arguments }; + } + } + + return new ChatMessage( + ChatRole.Assistant, + [new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]); + } + + private static ChatMessage ConvertFunctionToolCallOutputResource(FunctionToolCallOutputResource funcOutput) + { + return new ChatMessage( + ChatRole.Tool, + [new FunctionResultContent(funcOutput.CallId, funcOutput.Output)]); + } + + private static ChatRole ConvertMessageRole(MessageRole role) + { + return role switch + { + MessageRole.User => ChatRole.User, + MessageRole.Assistant => ChatRole.Assistant, + MessageRole.System => ChatRole.System, + MessageRole.Developer => new ChatRole("developer"), + _ => ChatRole.User + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj new file mode 100644 index 0000000000..b881e287cc --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj @@ -0,0 +1,40 @@ + + + + $(TargetFrameworksCore) + enable + Microsoft.Agents.AI.Hosting.AzureAIResponses + alpha + $(NoWarn);MEAI001;NU1903 + false + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs new file mode 100644 index 0000000000..c620cf6324 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Converts agent-framework streams into +/// Responses Server SDK sequences using the +/// builder pattern. +/// +internal static class OutputConverter +{ + /// + /// Converts a stream of into a stream of + /// using the SDK builder pattern. + /// + /// The agent response updates to convert. + /// The SDK event stream builder. + /// Cancellation token. + /// An async enumerable of SDK response stream events (excluding lifecycle events). + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Serializing function call arguments dictionary.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Serializing function call arguments dictionary.")] + public static async IAsyncEnumerable ConvertUpdatesToEventsAsync( + IAsyncEnumerable updates, + ResponseEventStream stream, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ResponseUsage? accumulatedUsage = null; + OutputItemMessageBuilder? currentMessageBuilder = null; + TextContentBuilder? currentTextBuilder = null; + StringBuilder? accumulatedText = null; + string? previousMessageId = null; + bool hasTerminalEvent = false; + var executorItemIds = new Dictionary(); + + await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Handle workflow events from RawRepresentation + if (update.RawRepresentation is WorkflowEvent workflowEvent) + { + // Close any open message builder before emitting workflow items + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + foreach (var evt in EmitWorkflowEvent(stream, workflowEvent, executorItemIds)) + { + yield return evt; + } + + continue; + } + + foreach (var content in update.Contents) + { + switch (content) + { + case MeaiTextContent textContent: + { + if (!IsSameMessage(update.MessageId, previousMessageId) && currentMessageBuilder is not null) + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + } + + previousMessageId = update.MessageId; + + if (currentMessageBuilder is null) + { + currentMessageBuilder = stream.AddOutputItemMessage(); + yield return currentMessageBuilder.EmitAdded(); + + currentTextBuilder = currentMessageBuilder.AddTextContent(); + yield return currentTextBuilder.EmitAdded(); + + accumulatedText = new StringBuilder(); + } + + if (textContent.Text is { Length: > 0 }) + { + accumulatedText!.Append(textContent.Text); + yield return currentTextBuilder!.EmitDelta(textContent.Text); + } + + break; + } + + case FunctionCallContent funcCall: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + var callId = funcCall.CallId ?? Guid.NewGuid().ToString("N"); + var funcBuilder = stream.AddOutputItemFunctionCall(funcCall.Name, callId); + yield return funcBuilder.EmitAdded(); + + var arguments = funcCall.Arguments is not null + ? JsonSerializer.Serialize(funcCall.Arguments) + : "{}"; + + yield return funcBuilder.EmitArgumentsDelta(arguments); + yield return funcBuilder.EmitArgumentsDone(arguments); + yield return funcBuilder.EmitDone(); + break; + } + + case TextReasoningContent reasoningContent: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + var reasoningBuilder = stream.AddOutputItemReasoningItem(); + yield return reasoningBuilder.EmitAdded(); + + var summaryPart = reasoningBuilder.AddSummaryPart(); + yield return summaryPart.EmitAdded(); + + var text = reasoningContent.Text ?? string.Empty; + yield return summaryPart.EmitTextDelta(text); + yield return summaryPart.EmitTextDone(text); + yield return summaryPart.EmitDone(); + reasoningBuilder.EmitSummaryPartDone(summaryPart); + + yield return reasoningBuilder.EmitDone(); + break; + } + + case UsageContent usageContent when usageContent.Details is not null: + { + accumulatedUsage = ConvertUsage(usageContent.Details, accumulatedUsage); + break; + } + + case ErrorContent errorContent: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + hasTerminalEvent = true; + + yield return stream.EmitFailed( + ResponseErrorCode.ServerError, + errorContent.Message ?? "An error occurred during agent execution.", + accumulatedUsage); + yield break; + } + + case DataContent: + case UriContent: + // Image/audio/file content from agents is not currently supported + // as streaming output items in the Responses Server SDK builder pattern. + // These would need to be serialized as base64 or URL references. + break; + + case FunctionResultContent: + // Function results are internal to the agent's tool-calling loop + // and are not emitted as output items in the response stream. + break; + + default: + break; + } + } + } + + // Close any remaining open message + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + if (!hasTerminalEvent) + { + yield return stream.EmitCompleted(accumulatedUsage); + } + } + + private static IEnumerable CloseCurrentMessage( + OutputItemMessageBuilder? messageBuilder, + TextContentBuilder? textBuilder, + StringBuilder? accumulatedText) + { + if (messageBuilder is null) + { + yield break; + } + + if (textBuilder is not null) + { + var finalText = accumulatedText?.ToString() ?? string.Empty; + yield return textBuilder.EmitDone(finalText); + yield return messageBuilder.EmitContentDone(textBuilder); + } + + yield return messageBuilder.EmitDone(); + } + + private static bool IsSameMessage(string? currentId, string? previousId) => + currentId is not { Length: > 0 } || previousId is not { Length: > 0 } || currentId == previousId; + + private static ResponseUsage ConvertUsage(UsageDetails details, ResponseUsage? existing) + { + var inputTokens = (long)(details.InputTokenCount ?? 0); + var outputTokens = (long)(details.OutputTokenCount ?? 0); + var totalTokens = (long)(details.TotalTokenCount ?? 0); + + if (existing is not null) + { + inputTokens += existing.InputTokens; + outputTokens += existing.OutputTokens; + totalTokens += existing.TotalTokens; + } + + return AzureAIAgentServerResponsesModelFactory.ResponseUsage( + inputTokens: inputTokens, + outputTokens: outputTokens, + totalTokens: totalTokens); + } + + private static IEnumerable EmitWorkflowEvent( + ResponseEventStream stream, + WorkflowEvent workflowEvent, + Dictionary executorItemIds) + { + switch (workflowEvent) + { + case ExecutorInvokedEvent invokedEvent: + { + var itemId = GenerateItemId("wfa"); + executorItemIds[invokedEvent.ExecutorId] = itemId; + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: invokedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.InProgress, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + break; + } + + case ExecutorCompletedEvent completedEvent: + { + var itemId = GenerateItemId("wfa"); + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: completedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.Completed, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + executorItemIds.Remove(completedEvent.ExecutorId); + break; + } + + case ExecutorFailedEvent failedEvent: + { + var itemId = GenerateItemId("wfa"); + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: failedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.Failed, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + executorItemIds.Remove(failedEvent.ExecutorId); + break; + } + + // Informational/lifecycle events — no SDK output needed. + // Note: AgentResponseUpdateEvent and WorkflowErrorEvent are unwrapped by + // WorkflowSession.InvokeStageAsync() into regular AgentResponseUpdate objects + // with populated Contents (TextContent, ErrorContent, etc.), so they flow + // through the normal content processing path above — not through this method. + case SuperStepStartedEvent: + case SuperStepCompletedEvent: + case WorkflowStartedEvent: + case WorkflowWarningEvent: + case RequestInfoEvent: + break; + } + } + + /// + /// Generates a valid item ID matching the SDK's {prefix}_{50chars} format. + /// + private static string GenerateItemId(string prefix) + { + // SDK format: {prefix}_{50 char body} + var bytes = RandomNumberGenerator.GetBytes(25); + var body = Convert.ToHexString(bytes); // 50 hex chars, uppercase + return $"{prefix}_{body}"; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..d596ba3457 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.AgentServer.Responses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Extension methods for to register the agent-framework +/// response handler with the Azure AI Responses Server SDK. +/// +public static class AgentFrameworkResponsesServiceCollectionExtensions +{ + /// + /// Registers as the + /// for the Azure AI Responses Server SDK. Agents are resolved from keyed DI services + /// using the agent.name or metadata["entity_id"] from incoming requests. + /// + /// + /// + /// Call this method after AddResponsesServer() and after registering your + /// instances (e.g., via AddAIAgent()). + /// + /// + /// Example: + /// + /// builder.Services.AddResponsesServer(); + /// builder.AddAIAgent("my-agent", ...); + /// builder.Services.AddAgentFrameworkHandler(); + /// + /// var app = builder.Build(); + /// app.MapResponsesServer(); + /// + /// + /// + /// The service collection. + /// The service collection for chaining. + public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + services.TryAddSingleton(); + return services; + } + + /// + /// Registers a specific as the handler for all incoming requests, + /// regardless of the agent.name in the request. + /// + /// + /// + /// Use this overload when hosting a single agent. The provided agent instance is + /// registered both as a keyed service and as the default . + /// + /// + /// Example: + /// + /// builder.Services.AddResponsesServer(); + /// builder.Services.AddAgentFrameworkHandler(myAgent); + /// + /// var app = builder.Build(); + /// app.MapResponsesServer(); + /// + /// + /// + /// The service collection. + /// The agent instance to register. + /// The service collection for chaining. + public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services, AIAgent agent) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(agent); + + services.TryAddSingleton(agent); + services.TryAddSingleton(); + return services; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs new file mode 100644 index 0000000000..ee32771a67 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -0,0 +1,815 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class AgentFrameworkResponseHandlerTests +{ + private static string ValidResponseId => "resp_" + new string('0', 46); + + [Fact] + public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() + { + // Arrange + var agent = CreateTestAgent("Hello from the agent!"); + var services = new ServiceCollection(); + services.AddSingleton(agent); + services.AddSingleton>(NullLogger.Instance); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); + Assert.IsType(events[0]); + Assert.IsType(events[1]); + } + + [Fact] + public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() + { + // Arrange + var agent = CreateTestAgent("Keyed agent response"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("my-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert - should have produced events from the keyed agent + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + } + + [Fact] + public void Constructor_NullServiceProvider_ThrowsArgumentNullException() + { + Assert.Throws( + () => new AgentFrameworkResponseHandler(null!, NullLogger.Instance)); + } + + [Fact] + public void Constructor_NullLogger_ThrowsArgumentNullException() + { + var sp = new ServiceCollection().BuildServiceProvider(); + Assert.Throws( + () => new AgentFrameworkResponseHandler(sp, null!)); + } + + [Fact] + public async Task CreateAsync_ResolvesAgentByModelField() + { + // Arrange + var agent = CreateTestAgent("model agent"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-agent"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() + { + // Arrange + var agent = CreateTestAgent("entity agent"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("entity-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: ""); + var metadata = new Metadata(); + metadata.AdditionalProperties["entity_id"] = "entity-agent"; + request.Metadata = metadata; + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() + { + // Arrange + var agent = CreateTestAgent("default agent"); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("nonexistent-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("missing-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + var ex = await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + + Assert.Contains("missing-agent", ex.Message); + } + + [Fact] + public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: ""); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + var ex = await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + + Assert.Contains("No agent name specified", ex.Message); + } + + [Fact] + public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvents() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + bool threw = false; + try + { + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + } + catch (InvalidOperationException) + { + threw = true; + } + + // Assert + Assert.True(threw); + Assert.Empty(events); + } + + [Fact] + public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var historyItem = new OutputItemMessage( + id: "hist_1", + role: MessageRole.Assistant, + content: [new MessageContentOutputTextContent( + "Previous response", + Array.Empty(), + Array.Empty())], + status: MessageStatus.Completed); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(new OutputItem[] { historyItem }); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.True(messages.Count >= 2); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Raw input" } } } + }); + + var inputItem = new OutputItemMessage( + id: "input_1", + role: MessageRole.Assistant, + content: [new MessageContentOutputTextContent( + "Resolved input", + Array.Empty(), + Array.Empty())], + status: MessageStatus.Completed); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(new OutputItem[] { inputItem }); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Raw input" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_PassesInstructionsToAgent() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + instructions: "You are a helpful assistant."); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedOptions); + var chatClientOptions = Assert.IsType(agent.CapturedOptions); + Assert.Equal("You are a helpful assistant.", chatClientOptions.ChatOptions?.Instructions); + } + + [Fact] + public async Task CreateAsync_AgentThrows_ExceptionPropagates() + { + // Arrange + var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + } + + [Fact] + public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() + { + // Arrange + var agent1 = CreateTestAgent("Agent 1 response"); + var agent2 = CreateTestAgent("Agent 2 response"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("agent-1", agent1); + services.AddKeyedSingleton("agent-2", agent2); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("agent-2")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCanceledException() + { + // Arrange + var agent = new CancellationCheckingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, cts.Token)) + { + } + }); + } + + private static TestAgent CreateTestAgent(string responseText) + { + return new TestAgent(responseText); + } + + private static async IAsyncEnumerable ToAsyncEnumerableAsync(params AgentResponseUpdate[] items) + { + foreach (var item in items) + { + yield return item; + } + + await Task.CompletedTask; + } + + private sealed class TestAgent(string responseText) : AIAgent + { + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + ToAsyncEnumerableAsync(new AgentResponseUpdate + { + MessageId = "resp_msg_1", + Contents = [new MeaiTextContent(responseText)] + }); + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class ThrowingAgent(Exception exception) : AIAgent + { + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw exception; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class CapturingAgent : AIAgent + { + public IEnumerable? CapturedMessages { get; private set; } + public AgentRunOptions? CapturedOptions { get; private set; } + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) + { + CapturedMessages = messages.ToList(); + CapturedOptions = options; + return ToAsyncEnumerableAsync(new AgentResponseUpdate + { + MessageId = "resp_msg_1", + Contents = [new MeaiTextContent("captured")] + }); + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class CancellationCheckingAgent : AIAgent + { + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new AgentResponseUpdate { Contents = [new MeaiTextContent("test")] }; + await Task.CompletedTask; + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs new file mode 100644 index 0000000000..34555798a7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs @@ -0,0 +1,671 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class InputConverterTests +{ + [Fact] + public void ConvertInputToMessages_EmptyRequest_ReturnsEmptyList() + { + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(Array.Empty()); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Empty(messages); + } + + [Fact] + public void ConvertInputToMessages_UserTextMessage_ReturnsUserMessage() + { + var input = new[] + { + new + { + type = "message", + id = "msg_001", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hello, agent!" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text == "Hello, agent!"); + } + + [Fact] + public void ConvertInputToMessages_FunctionCallOutput_ReturnsToolMessage() + { + var input = new[] + { + new + { + type = "function_call_output", + id = "fc_out_001", + call_id = "call_123", + output = "42" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.Tool, messages[0].Role); + var funcResult = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(funcResult); + Assert.Equal("call_123", funcResult.CallId); + } + + [Fact] + public void ConvertInputToMessages_FunctionToolCall_ReturnsAssistantMessage() + { + var input = new[] + { + new + { + type = "function_call", + id = "fc_001", + call_id = "call_456", + name = "get_weather", + arguments = "{\"location\": \"Seattle\"}" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + var funcCall = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(funcCall); + Assert.Equal("call_456", funcCall.CallId); + Assert.Equal("get_weather", funcCall.Name); + } + + [Fact] + public void ConvertInputToMessages_MultipleItems_ReturnsAllMessages() + { + var input = new object[] + { + new + { + type = "message", + id = "msg_001", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "What's the weather?" } } + }, + new + { + type = "function_call", + id = "fc_001", + call_id = "call_789", + name = "get_weather", + arguments = "{}" + }, + new + { + type = "function_call_output", + id = "fc_out_001", + call_id = "call_789", + output = "Sunny, 72°F" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Equal(3, messages.Count); + Assert.Equal(ChatRole.User, messages[0].Role); + Assert.Equal(ChatRole.Assistant, messages[1].Role); + Assert.Equal(ChatRole.Tool, messages[2].Role); + } + + [Fact] + public void ConvertToChatOptions_SetsTemperatureAndTopP() + { + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + temperature: 0.7, + topP: 0.9, + maxOutputTokens: 1000, + model: "gpt-4o"); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Equal(0.7f, options.Temperature); + Assert.Equal(0.9f, options.TopP); + Assert.Equal(1000, options.MaxOutputTokens); + Assert.Equal("gpt-4o", options.ModelId); + } + + [Fact] + public void ConvertToChatOptions_NullValues_SetsNulls() + { + var request = new CreateResponse(); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Null(options.Temperature); + Assert.Null(options.TopP); + Assert.Null(options.MaxOutputTokens); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessage_ReturnsAssistantMessage() + { + var textContent = new MessageContentOutputTextContent( + "Hello from assistant", + Array.Empty(), + Array.Empty()); + var outputMsg = new OutputItemMessage( + id: "out_001", + role: MessageRole.Assistant, + content: [textContent], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text == "Hello from assistant"); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCall_ReturnsAssistantMessage() + { + var funcCall = new OutputItemFunctionToolCall( + callId: "call_abc", + name: "search", + arguments: "{\"query\": \"test\"}"); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcCall]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + var content = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(content); + Assert.Equal("call_abc", content.CallId); + Assert.Equal("search", content.Name); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCallOutputResource_ReturnsToolMessage() + { + var funcOutput = new FunctionToolCallOutputResource( + callId: "call_def", + output: BinaryData.FromString("result data")); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcOutput]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Tool, messages[0].Role); + var result = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(result); + Assert.Equal("call_def", result.CallId); + } + + [Fact] + public void ConvertOutputItemsToMessages_ReasoningItem_ReturnsNull() + { + var reasoning = AzureAIAgentServerResponsesModelFactory.OutputItemReasoningItem( + id: "reason_001"); + + var messages = InputConverter.ConvertOutputItemsToMessages([reasoning]); + + Assert.Empty(messages); + } + + // ── Image Content Tests (B-03 through B-06) ── + + [Fact] + public void ConvertInputToMessages_ImageContentWithHttpUrl_ReturnsUriContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", image_url = "https://example.com/img.png" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is UriContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentWithDataUri_ReturnsDataContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", image_url = "data:image/png;base64,iVBORw0KGgo=" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is DataContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentWithFileId_ReturnsHostedFileContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", file_id = "file_abc123" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is HostedFileContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentNoUrlOrFileId_ProducesNoContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Single(messages[0].Contents); + } + + // ── File Content Tests (B-07 through B-11) ── + + [Fact] + public void ConvertInputToMessages_FileContentWithUrl_ReturnsUriContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_url = "https://example.com/doc.pdf" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is UriContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithInlineData_ReturnsDataContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_data = "data:application/pdf;base64,iVBORw0KGgo=" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is DataContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithFileId_ReturnsHostedFileContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_id = "file_xyz789" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is HostedFileContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithFilenameOnly_ReturnsFallbackText() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", filename = "report.pdf" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("report.pdf")); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithNothing_ProducesNoContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Single(messages[0].Contents); + } + + // ── Mixed Content / Edge Cases (B-15 through B-18) ── + + [Fact] + public void ConvertInputToMessages_MixedContentInSingleMessage_ReturnsAllContentTypes() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new object[] + { + new { type = "input_text", text = "Look at this:" }, + new { type = "input_image", image_url = "https://example.com/img.png" } + } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(2, messages[0].Contents.Count); + } + + [Fact] + public void ConvertInputToMessages_EmptyMessageContent_ReturnsFallbackTextContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = Array.Empty() + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + var textContent = Assert.IsType(Assert.Single(messages[0].Contents)); + Assert.Equal(string.Empty, textContent.Text); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageRefusal_ReturnsRefusalText() + { + var refusal = new MessageContentRefusalContent("I cannot help with that"); + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [refusal], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("[Refusal:")); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("I cannot help with that")); + } + + [Fact] + public void ConvertInputToMessages_ItemReferenceParam_IsSkipped() + { + var input = new object[] + { + new { type = "item_reference", id = "ref_001" }, + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + } + + // ── Role Mapping Tests (C-01 through C-05) ── + + [Fact] + public void ConvertInputToMessages_UserRole_ReturnsChatRoleUser() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hi" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + } + + [Fact] + public void ConvertOutputItemsToMessages_AssistantRole_ReturnsChatRoleAssistant() + { + // OutputItemMessage always maps to assistant role + var textContent = new MessageContentOutputTextContent( + "Hi", Array.Empty(), Array.Empty()); + var outputMsg = new OutputItemMessage( + id: "msg_1", + role: MessageRole.Assistant, + content: [textContent], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + // ── History Conversion Edge Cases (D-02 through D-12) ── + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageWithRefusal_ReturnsRefusalText() + { + var refusal = new MessageContentRefusalContent("Not allowed"); + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [refusal], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("[Refusal:")); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("Not allowed")); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageWithEmptyContent_ReturnsFallbackText() + { + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + var textContent = Assert.IsType(Assert.Single(messages[0].Contents)); + Assert.Equal(string.Empty, textContent.Text); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCallWithMalformedArgs_UsesRawFallback() + { + var funcCall = new OutputItemFunctionToolCall( + callId: "call_1", + name: "test", + arguments: "not-json{{{"); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcCall]); + + Assert.Single(messages); + var content = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(content); + Assert.NotNull(content.Arguments); + Assert.True(content.Arguments.ContainsKey("_raw")); + } + + [Fact] + public void ConvertOutputItemsToMessages_UnknownOutputItemType_IsSkipped() + { + var messages = InputConverter.ConvertOutputItemsToMessages([]); + + Assert.Empty(messages); + } + + [Fact] + public void ConvertToChatOptions_ModelId_SetFromRequest() + { + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-model"); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Equal("my-model", options.ModelId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj new file mode 100644 index 0000000000..09c3ba24c1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj @@ -0,0 +1,17 @@ + + + + $(TargetFrameworksCore) + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs new file mode 100644 index 0000000000..dde11298c7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs @@ -0,0 +1,1080 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Agents.AI.Workflows; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class OutputConverterTests +{ + private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() + { + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model"); + var stream = new ResponseEventStream(mockContext.Object, request); + return (stream, mockContext); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyStream_EmitsCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = ToAsync(Array.Empty()); + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(updates, stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_SingleTextUpdate_EmitsMessageAndCompleted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + MessageId = "msg_1", + Contents = [new MeaiTextContent("Hello, world!")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Expected: MessageAdded, TextAdded, TextDelta, TextDone, ContentDone, MessageDone, Completed + Assert.True(events.Count >= 5, $"Expected at least 5 events, got {events.Count}"); + Assert.IsType(events[0]); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleTextUpdates_EmitsStreamingDeltas() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Hello, ")] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("world!")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should have two text delta events among the others + Assert.True(events.Count >= 6, $"Expected at least 6 events, got {events.Count}"); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCall_EmitsFunctionCallEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "get_weather", + new Dictionary { ["city"] = "Seattle" })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Should have: FuncAdded, ArgsDelta, ArgsDone, FuncDone, Completed + Assert.IsType(events[0]); + Assert.IsType(events[^1]); + Assert.True(events.Count >= 4, $"Expected at least 4 events for function call, got {events.Count}"); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContent_EmitsFailed() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new ErrorContent("Something went wrong")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContent_DoesNotEmitCompleted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new ErrorContent("Failure")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_UsageContent_IncludesUsageInCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate + { + MessageId = "msg_1", + Contents = [new MeaiTextContent("Hi")] + }, + new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails + { + InputTokenCount = 10, + OutputTokenCount = 5, + TotalTokenCount = 15 + })] + } + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + var completedEvent = events.OfType().SingleOrDefault(); + Assert.NotNull(completedEvent); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningContent_EmitsReasoningEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new TextReasoningContent("Let me think about this...")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Should have: ReasoningAdded, SummaryPartAdded, TextDelta, TextDone, SummaryDone, ReasoningDone, Completed + Assert.True(events.Count >= 5, $"Expected at least 5 events for reasoning, got {events.Count}"); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_CancellationRequested_Throws() + { + var (stream, _) = CreateTestStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var updates = ToAsync(new[] { new AgentResponseUpdate { Contents = [new MeaiTextContent("test")] } }); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(updates, stream, cts.Token)) + { + // Should throw before yielding + } + }); + } + + // F-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseTextDeltaEvent); + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // F-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent((string)null!)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseTextDeltaEvent); + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // F-07 + [Fact] + public async Task ConvertUpdatesToEventsAsync_DifferentMessageIds_CreatesMultipleMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("First")] }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Second")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // F-08 + [Fact] + public async Task ConvertUpdatesToEventsAsync_NullMessageIds_TreatedAsSameMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = null, Contents = [new MeaiTextContent("First")] }, + new AgentResponseUpdate { MessageId = null, Contents = [new MeaiTextContent("Second")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Single(events.OfType()); + } + + // G-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("thinking...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "search", new Dictionary { ["q"] = "test" })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // G-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_EmitsEmptyJson() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "do_something", (IDictionary?)null)] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + } + + // G-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithEmptyCallId_GeneratesCallId() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("", "do_something", new Dictionary { ["x"] = 1 })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + } + + // G-05 + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleFunctionCalls_EmitsSeparateBuilders() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "func_a", new Dictionary { ["a"] = 1 })] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_2", "func_b", new Dictionary { ["b"] = 2 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // H-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningWithNullText_EmitsEmptyString() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new TextReasoningContent(null)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.True(events.Count >= 5, $"Expected at least 5 events, got {events.Count}"); + Assert.IsType(events[^1]); + } + + // H-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial")] }, + new AgentResponseUpdate { Contents = [new TextReasoningContent("thinking")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // I-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContentWithNullMessage_UsesDefaultMessage() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new ErrorContent(null!)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseFailedEvent); + } + + // I-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContentClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial text")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Something broke")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + } + + // I-06 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorAfterPartialText_ClosesMessageThenFails() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial text")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Unexpected error")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + // J-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleUsageUpdates_AccumulatesTokens() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Hi")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 })] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 20, OutputTokenCount = 10, TotalTokenCount = 30 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // J-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_UsageWithZeroTokens_StillCompletes() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 0, OutputTokenCount = 0, TotalTokenCount = 0 })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // K-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_DataContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new DataContent("data:image/png;base64,aWNv", "image/png")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // K-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_UriContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new UriContent("https://example.com/file.txt", "text/plain")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // K-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionResultContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new FunctionResultContent("call_1", "result data")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // L-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItem() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("executor_1", "invoked") }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorCompletedEvent_EmitsCompletedWorkflowAction() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("executor_1", null) }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorFailedEvent_EmitsFailedWorkflowAction() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorFailedEvent("executor_1", new InvalidOperationException("test error")) }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowEventClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("exec_1", "invoked") }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // L-06 + [Fact] + public async Task ConvertUpdatesToEventsAsync_InterleavedWorkflowAndTextEvents() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("exec_1", "invoked") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Agent says hello")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("exec_1", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // M-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_TextThenFunctionCallThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Let me check...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "search", new Dictionary { ["q"] = "weather" })] }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Here are the results")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + } + + // M-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new TextReasoningContent("Thinking about the answer...")] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("The answer is 42")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // M-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_TextThenError_EmitsMessageThenFailed() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting...")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Unexpected error")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + Assert.Single(events.OfType()); + } + + // M-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallThenTextThenFunctionCall_ProducesThreeItems() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "func_a", new Dictionary { ["a"] = 1 })] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Processing...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_2", "func_b", new Dictionary { ["b"] = 2 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + } + + // ===== Workflow content flow tests (W series) ===== + // These simulate the exact update patterns that WorkflowSession.InvokeStageAsync() produces + // when wrapping a Workflow as an AIAgent via AsAIAgent(). + + // W-01: Multi-executor text output — different MessageIds cause separate messages + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultiExecutorTextOutput_CreatesSeparateMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Executor 1 invoked (RawRepresentation) + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + // Executor 1 produces text (unwrapped AgentResponseUpdateEvent) + new AgentResponseUpdate { MessageId = "msg_agent1", Contents = [new MeaiTextContent("Hello from agent 1")] }, + // Executor 1 completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + // Executor 2 invoked + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + // Executor 2 produces text (different MessageId) + new AgentResponseUpdate { MessageId = "msg_agent2", Contents = [new MeaiTextContent("Hello from agent 2")] }, + // Executor 2 completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 2 workflow action items (invoked) + 1 text message + 2 workflow action items (completed) + 1 text message = 6 output items + Assert.Equal(6, events.OfType().Count()); + // 2 text deltas (one per agent) + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-02: Workflow error via ErrorContent (as produced by WorkflowSession for WorkflowErrorEvent) + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowErrorAsContent_EmitsFailed() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting work...")] }, + // WorkflowErrorEvent is converted to ErrorContent by WorkflowSession + new AgentResponseUpdate { Contents = [new ErrorContent("Workflow execution failed")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should close the open message, then emit failed + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + // W-03: Function call from workflow executor (e.g. handoff agent calling transfer_to_agent) + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowFunctionCall_EmitsFunctionCallEvents() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("triage_agent", "start") }, + // Agent produces function call (handoff) + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_handoff", "transfer_to_code_expert", + new Dictionary { ["reason"] = "User asked about code" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("triage_agent", null) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("code_expert", "start") }, + new AgentResponseUpdate { MessageId = "msg_expert", Contents = [new MeaiTextContent("Here's how async/await works...")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("code_expert", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should have: 4 workflow actions + 1 function call + 1 text message = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + Assert.Contains(events, e => e is ResponseTextDeltaEvent); + Assert.IsType(events[^1]); + } + + // W-04: Informational events (superstep, workflow started) are silently skipped + [Fact] + public async Task ConvertUpdatesToEventsAsync_InformationalWorkflowEvents_AreSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("start") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Result")] }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Only one output item (the text message), no workflow action items for informational events + Assert.Single(events.OfType()); + Assert.Contains(events, e => e is ResponseTextDeltaEvent); + Assert.IsType(events[^1]); + } + + // W-05: Warning events are silently skipped + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowWarningEvent_IsSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowWarningEvent("Agent took too long") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Done")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + // W-06: Streaming text from multiple workflow turns (same executor, different message IDs) + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultiTurnSameExecutor_CreatesSeparateMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_turn1", Contents = [new MeaiTextContent("First response")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + // Same executor invoked again (second superstep) + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_turn2", Contents = [new MeaiTextContent("Second response")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow action items + 2 text messages = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + } + + // W-07: Executor failure mid-stream with partial text + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorFailureAfterPartialText_ClosesMessageAndEmitsFailure() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting to process...")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorFailedEvent("agent_1", new InvalidOperationException("Agent crashed")) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Text message should be closed before the failed workflow action item + Assert.True(events.OfType().Any()); + // Workflow action items: invoked + failed = 2, plus text message = 3 + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-08: Full handoff pattern — triage → function call → target agent text + [Fact] + public async Task ConvertUpdatesToEventsAsync_FullHandoffPattern_ProducesCorrectEventSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Workflow lifecycle + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("triage", "start") }, + // Triage agent decides to hand off + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "transfer_to_expert", + new Dictionary { ["reason"] = "technical question" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("triage", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Next superstep + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("expert", "start") }, + // Expert agent responds with text + new AgentResponseUpdate { MessageId = "msg_expert_1", Contents = [new MeaiTextContent("Let me explain...")] }, + new AgentResponseUpdate { MessageId = "msg_expert_1", Contents = [new MeaiTextContent(" Here's how it works.")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("expert", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: invoked triage, completed triage, invoked expert, completed expert = 4 + // Content items: 1 function call, 1 text message = 2 + // Total output items: 6 + Assert.Equal(6, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + // Two text deltas for the two streaming chunks + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-09: SubworkflowErrorEvent treated as informational (error content comes separately) + [Fact] + public async Task ConvertUpdatesToEventsAsync_SubworkflowErrorEvent_IsSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SubworkflowErrorEvent("sub_1", new InvalidOperationException("sub failed")) }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Recovered")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // SubworkflowErrorEvent extends WorkflowErrorEvent which falls through to default skip + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + // W-10: Mixed content types from workflow — reasoning + text + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowReasoningThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("thinking_agent", "start") }, + // Agent produces reasoning content + new AgentResponseUpdate { Contents = [new TextReasoningContent("Analyzing the problem...")] }, + // Then text response + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("The answer is 42")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("thinking_agent", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: 2 (invoked + completed), reasoning: 1, text message: 1 = 4 output items + Assert.Equal(4, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-11: Usage content accumulated across workflow executors + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowUsageAcrossExecutors_AccumulatesCorrectly() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Response 1")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 100, OutputTokenCount = 50, TotalTokenCount = 150 })] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Response 2")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 200, OutputTokenCount = 100, TotalTokenCount = 300 })] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Usage should be accumulated in the completed event + Assert.IsType(events[^1]); + } + + // W-12: Empty workflow — only lifecycle events, no content + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyWorkflowOnlyLifecycle_EmitsOnlyCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("start") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Only the terminal completed event + Assert.Single(events); + Assert.IsType(events[0]); + } + + private static async IAsyncEnumerable ToAsync(IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + + await Task.CompletedTask; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..9cf45d70af --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Azure.AI.AgentServer.Responses; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddAgentFrameworkHandler_RegistersResponseHandler() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddAgentFrameworkHandler(); + + var descriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(ResponseHandler)); + Assert.NotNull(descriptor); + Assert.Equal(typeof(AgentFrameworkResponseHandler), descriptor.ImplementationType); + } + + [Fact] + public void AddAgentFrameworkHandler_CalledTwice_RegistersOnce() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddAgentFrameworkHandler(); + services.AddAgentFrameworkHandler(); + + var count = services.Count(d => d.ServiceType == typeof(ResponseHandler)); + Assert.Equal(1, count); + } + + [Fact] + public void AddAgentFrameworkHandler_NullServices_ThrowsArgumentNullException() + { + Assert.Throws( + () => AgentFrameworkResponsesServiceCollectionExtensions.AddAgentFrameworkHandler(null!)); + } + + [Fact] + public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() + { + var services = new ServiceCollection(); + services.AddLogging(); + var mockAgent = new Mock(); + + services.AddAgentFrameworkHandler(mockAgent.Object); + + var handlerDescriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(ResponseHandler)); + Assert.NotNull(handlerDescriptor); + + var agentDescriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(AIAgent)); + Assert.NotNull(agentDescriptor); + } + + [Fact] + public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() + { + var services = new ServiceCollection(); + Assert.Throws( + () => services.AddAgentFrameworkHandler((AIAgent)null!)); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs new file mode 100644 index 0000000000..e5f9bb0405 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs @@ -0,0 +1,509 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +/// +/// Integration tests that verify workflow execution through the +/// pipeline. +/// These use real workflow builders and the InProcessExecution environment +/// to produce authentic streaming event patterns. +/// +public class WorkflowIntegrationTests +{ + private static string ValidResponseId => "resp_" + new string('0', 46); + + // ===== Sequential Workflow Tests ===== + + [Fact] + public async Task SequentialWorkflow_SingleAgent_ProducesTextOutput() + { + // Arrange: single-agent sequential workflow + var echoAgent = new StreamingTextAgent("echo", "Hello from the workflow!"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential", echoAgent); + var workflowAgent = workflow.AsAIAgent( + id: "workflow-agent", + name: "Test Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + at least one text output + terminal + Assert.IsType(events[0]); + Assert.IsType(events[1]); + Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); + + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + [Fact] + public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBoth() + { + // Arrange: two agents in sequence + var agent1 = new StreamingTextAgent("agent1", "First agent says hello"); + var agent2 = new StreamingTextAgent("agent2", "Second agent says goodbye"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential-2", agent1, agent2); + var workflowAgent = workflow.AsAIAgent( + id: "seq-workflow", + name: "Sequential Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Process this"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have workflow action events for executor lifecycle + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + + // Should have output item events (either text messages or workflow actions) + Assert.True(events.OfType().Any(), + "Expected at least one output item from the workflow"); + } + + // ===== Workflow Error Propagation ===== + + [Fact] + public async Task Workflow_AgentThrowsException_ProducesErrorOutput() + { + // Arrange: workflow with an agent that throws + var throwingAgent = new ThrowingStreamingAgent("thrower", new InvalidOperationException("Agent crashed")); + var workflow = AgentWorkflowBuilder.BuildSequential("test-error", throwingAgent); + var workflowAgent = workflow.AsAIAgent( + id: "error-workflow", + name: "Error Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Trigger error"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + error/failure indicator + Assert.IsType(events[0]); + Assert.IsType(events[1]); + + var lastEvent = events[^1]; + // Workflow errors surface as either Failed or Completed (depending on error handling) + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + // ===== Workflow Action Lifecycle Events ===== + + [Fact] + public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItems() + { + // Arrange + var agent = new StreamingTextAgent("test-agent", "Result"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-actions", agent); + var workflowAgent = workflow.AsAIAgent( + id: "actions-workflow", + name: "Actions Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: workflow should produce OutputItemAdded events for executor lifecycle + var addedEvents = events.OfType().ToList(); + Assert.True(addedEvents.Count >= 1, + $"Expected at least 1 output item added event, got {addedEvents.Count}"); + } + + // ===== Keyed Workflow Registration ===== + + [Fact] + public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() + { + // Arrange: workflow agent registered with a keyed service name + var agent = new StreamingTextAgent("inner", "Keyed workflow response"); + var workflow = AgentWorkflowBuilder.BuildSequential("keyed-wf", agent); + var workflowAgent = workflow.AsAIAgent( + id: "keyed-workflow", + name: "Keyed Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-workflow", workflowAgent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("my-workflow")); + request.Input = CreateUserInput("Test keyed workflow"); + var mockContext = CreateMockContext(); + + // Act + var events = await CollectEventsAsync(handler, request, mockContext.Object); + + // Assert + Assert.IsType(events[0]); + Assert.True(events.Count >= 3, $"Expected at least 3 events, got {events.Count}"); + } + + // ===== OutputConverter Direct Workflow Pattern Tests ===== + // These test the OutputConverter directly with update patterns that mirror real workflows. + + [Fact] + public async Task OutputConverter_SequentialWorkflowPattern_ProducesCorrectEvents() + { + // Simulate what WorkflowSession produces for a 2-agent sequential workflow + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Superstep 1: Agent 1 + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_a1", Contents = [new MeaiTextContent("Agent 1 output")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Superstep 2: Agent 2 + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + new AgentResponseUpdate { MessageId = "msg_a2", Contents = [new MeaiTextContent("Agent 2 output")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow action items + 2 text messages = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_GroupChatPattern_ProducesCorrectEvents() + { + // Simulate round-robin group chat: agent1 → agent2 → agent1 → terminate + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_1", Contents = [new MeaiTextContent("Agent 1 turn 1")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_2", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_2", Contents = [new MeaiTextContent("Agent 2 turn 1")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_2", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(3) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_3", Contents = [new MeaiTextContent("Agent 1 turn 2")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(3) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 6 workflow actions + 3 text messages = 9 output items + Assert.Equal(9, events.OfType().Count()); + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_CodeExecutorPattern_ProducesCorrectEvents() + { + // Simulate a code-based FunctionExecutor: invoked → completed, no text content + // (code executors don't produce AgentResponseUpdateEvent, just executor lifecycle) + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("uppercase_fn", "hello") }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("uppercase_fn", "HELLO") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Second executor uses the output + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("format_agent", "start") }, + new AgentResponseUpdate { MessageId = "msg_fmt", Contents = [new MeaiTextContent("Formatted: HELLO")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("format_agent", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow actions + 1 text message = 5 output items + Assert.Equal(5, events.OfType().Count()); + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_SubworkflowPattern_ProducesCorrectEvents() + { + // Simulate a parent workflow that invokes a sub-workflow executor + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("parent") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + // Sub-workflow executor invoked + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("sub_workflow_host", "start") }, + // Inner agent within sub-workflow produces text (unwrapped by WorkflowSession) + new AgentResponseUpdate { MessageId = "msg_sub_1", Contents = [new MeaiTextContent("Sub-workflow agent output")] }, + // Sub-workflow executor completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("sub_workflow_host", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 2 workflow actions + 1 text message = 3 output items + Assert.Equal(3, events.OfType().Count()); + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_WorkflowWithMultipleContentTypes_HandlesAllCorrectly() + { + // Simulate a workflow producing reasoning, text, function calls, and usage + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("planner", "start") }, + // Reasoning + new AgentResponseUpdate { Contents = [new TextReasoningContent("Let me think about this...")] }, + // Function call (tool use) + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_search", "web_search", + new Dictionary { ["query"] = "latest news" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("planner", null) }, + // Next executor uses tool result + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("writer", "start") }, + new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("Based on my research, ")] }, + new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("here are the findings.")] }, + new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 500, OutputTokenCount = 200, TotalTokenCount = 700 })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("writer", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: 4 (2 invoked + 2 completed) + // Content: 1 reasoning + 1 function call + 1 text message = 3 + // Total: 7 output items + Assert.Equal(7, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // ===== Helpers ===== + + private static (AgentFrameworkResponseHandler handler, CreateResponse request, ResponseContext context) + CreateHandlerWithAgent(AIAgent agent, string userMessage) + { + var services = new ServiceCollection(); + services.AddSingleton(agent); + services.AddSingleton>(NullLogger.Instance); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = CreateUserInput(userMessage); + var mockContext = CreateMockContext(); + + return (handler, request, mockContext.Object); + } + + private static BinaryData CreateUserInput(string text) + { + return BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_in_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text } } + } + }); + } + + private static Mock CreateMockContext() + { + var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mock.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mock.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + return mock; + } + + private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() + { + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model"); + var stream = new ResponseEventStream(mockContext.Object, request); + return (stream, mockContext); + } + + private static async Task> CollectEventsAsync( + AgentFrameworkResponseHandler handler, + CreateResponse request, + ResponseContext context) + { + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, context, CancellationToken.None)) + { + events.Add(evt); + } + + return events; + } + + private static async IAsyncEnumerable ToAsync(IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + + await Task.CompletedTask; + } + + // ===== Test Agent Types ===== + + /// + /// A test agent that streams a single text update. + /// + private sealed class StreamingTextAgent(string id, string responseText) : AIAgent + { + public new string Id => id; + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + yield return new AgentResponseUpdate + { + MessageId = $"msg_{id}", + Contents = [new MeaiTextContent(responseText)] + }; + + await Task.CompletedTask; + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + /// + /// A test agent that always throws an exception during streaming. + /// + private sealed class ThrowingStreamingAgent(string id, Exception exception) : AIAgent + { + public new string Id => id; + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw exception; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } +} From 69597d7d77c549dde20a78c5a873cac2e4cdcb42 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 1 Apr 2026 08:29:42 -0700 Subject: [PATCH 02/75] Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OutputConverter.cs | 8 ++++---- .../ServiceCollectionExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs index c620cf6324..f6b9aed006 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -244,9 +244,9 @@ private static bool IsSameMessage(string? currentId, string? previousId) => private static ResponseUsage ConvertUsage(UsageDetails details, ResponseUsage? existing) { - var inputTokens = (long)(details.InputTokenCount ?? 0); - var outputTokens = (long)(details.OutputTokenCount ?? 0); - var totalTokens = (long)(details.TotalTokenCount ?? 0); + var inputTokens = details.InputTokenCount ?? 0; + var outputTokens = details.OutputTokenCount ?? 0; + var totalTokens = details.TotalTokenCount ?? 0; if (existing is not null) { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs index d596ba3457..c06353a8ec 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; From 52cc4a9f0e9c1666420ebcc3baf7486fb83b366c Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 08:51:05 -0700 Subject: [PATCH 03/75] Clean up tests and sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FoundryResponsesHosting/Pages.cs | 2 +- .../FoundryResponsesHosting/Program.cs | 2 +- .../ResponseStreamValidator.cs | 230 +++++++++--------- .../AgentFrameworkResponseHandlerTests.cs | 24 +- .../InputConverterTests.cs | 4 +- .../OutputConverterTests.cs | 8 +- .../ServiceCollectionExtensionsTests.cs | 4 +- .../WorkflowIntegrationTests.cs | 8 +- 8 files changed, 138 insertions(+), 144 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs index bff2c62e99..916b0fdf17 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. /// /// Static HTML pages served by the sample application. diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 32f39f641c..88af464ac7 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -16,8 +16,8 @@ // - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o") using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.AI.AgentServer.Responses; +using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs index 72da677f45..5dc1b4c791 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using System.Text.Json.Serialization; @@ -36,7 +36,7 @@ internal sealed class ResponseStreamValidator private bool _hasTerminal; /// All violations found so far. - internal IReadOnlyList Violations => _violations; + internal IReadOnlyList Violations => this._violations; /// /// Processes a single SSE event line pair (event type + JSON data). @@ -52,33 +52,33 @@ internal void ProcessEvent(string eventType, string jsonData) } catch (JsonException ex) { - Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); + this.Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); return; } - _eventCount++; + this._eventCount++; // ── Sequence number validation ────────────────────────────────── if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number) { int seq = seqProp.GetInt32(); - if (seq != _expectedSequenceNumber) + if (seq != this._expectedSequenceNumber) { - Fail("SEQ-01", $"Expected sequence_number {_expectedSequenceNumber}, got {seq}"); + this.Fail("SEQ-01", $"Expected sequence_number {this._expectedSequenceNumber}, got {seq}"); } - _expectedSequenceNumber = seq + 1; + this._expectedSequenceNumber = seq + 1; } - else if (_state != StreamState.Initial || eventType != "error") + else if (this._state != StreamState.Initial || eventType != "error") { // Pre-creation error events may not have sequence_number - Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); + this.Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); } // ── Post-terminal guard ───────────────────────────────────────── - if (_hasTerminal) + if (this._hasTerminal) { - Fail("TERM-01", $"Event '{eventType}' received after terminal event"); + this.Fail("TERM-01", $"Event '{eventType}' received after terminal event"); return; } @@ -86,93 +86,93 @@ internal void ProcessEvent(string eventType, string jsonData) switch (eventType) { case "response.created": - ValidateResponseCreated(data); + this.ValidateResponseCreated(data); break; case "response.queued": - ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); - ValidateResponseEnvelope(data, eventType); + this.ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); + this.ValidateResponseEnvelope(data, eventType); break; case "response.in_progress": - if (_state is StreamState.Created or StreamState.Queued) + if (this._state is StreamState.Created or StreamState.Queued) { - _state = StreamState.InProgress; + this._state = StreamState.InProgress; } else { - Fail("ORDER-02", $"'response.in_progress' received in state {_state} (expected Created or Queued)"); + this.Fail("ORDER-02", $"'response.in_progress' received in state {this._state} (expected Created or Queued)"); } - ValidateResponseEnvelope(data, eventType); + this.ValidateResponseEnvelope(data, eventType); break; case "response.output_item.added": case "output_item.added": - ValidateInProgress(eventType); - ValidateOutputItemAdded(data); + this.ValidateInProgress(eventType); + this.ValidateOutputItemAdded(data); break; case "response.output_item.done": case "output_item.done": - ValidateInProgress(eventType); - ValidateOutputItemDone(data); + this.ValidateInProgress(eventType); + this.ValidateOutputItemDone(data); break; case "response.content_part.added": case "content_part.added": - ValidateInProgress(eventType); - ValidateContentPartAdded(data); + this.ValidateInProgress(eventType); + this.ValidateContentPartAdded(data); break; case "response.content_part.done": case "content_part.done": - ValidateInProgress(eventType); - ValidateContentPartDone(data); + this.ValidateInProgress(eventType); + this.ValidateContentPartDone(data); break; case "response.output_text.delta": case "output_text.delta": - ValidateInProgress(eventType); - ValidateTextDelta(data); + this.ValidateInProgress(eventType); + this.ValidateTextDelta(data); break; case "response.output_text.done": case "output_text.done": - ValidateInProgress(eventType); - ValidateTextDone(data); + this.ValidateInProgress(eventType); + this.ValidateTextDone(data); break; case "response.function_call_arguments.delta": case "function_call_arguments.delta": - ValidateInProgress(eventType); + this.ValidateInProgress(eventType); break; case "response.function_call_arguments.done": case "function_call_arguments.done": - ValidateInProgress(eventType); + this.ValidateInProgress(eventType); break; case "response.completed": - ValidateTerminal(data, "completed"); + this.ValidateTerminal(data, "completed"); break; case "response.failed": - ValidateTerminal(data, "failed"); + this.ValidateTerminal(data, "failed"); break; case "response.incomplete": - ValidateTerminal(data, "incomplete"); + this.ValidateTerminal(data, "incomplete"); break; case "error": // Pre-creation error — standalone, no response.created precedes it - if (_state != StreamState.Initial) + if (this._state != StreamState.Initial) { - Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); + this.Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); } - _hasTerminal = true; + this._hasTerminal = true; break; default: @@ -186,31 +186,31 @@ internal void ProcessEvent(string eventType, string jsonData) /// internal void Complete() { - if (!_hasTerminal && _state != StreamState.Initial) + if (!this._hasTerminal && this._state != StreamState.Initial) { - Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); + this.Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); } - if (_state == StreamState.Initial && _eventCount == 0) + if (this._state == StreamState.Initial && this._eventCount == 0) { - Fail("EMPTY-01", "No events received in the stream"); + this.Fail("EMPTY-01", "No events received in the stream"); } // Check for output items that were added but never completed - foreach (int idx in _addedItemIndices) + foreach (int idx in this._addedItemIndices) { - if (!_doneItemIndices.Contains(idx)) + if (!this._doneItemIndices.Contains(idx)) { - Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); + this.Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); } } // Check for content parts that were added but never completed - foreach (string key in _addedContentParts) + foreach (string key in this._addedContentParts) { - if (!_doneContentParts.Contains(key)) + if (!this._doneContentParts.Contains(key)) { - Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); + this.Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); } } } @@ -221,9 +221,9 @@ internal void Complete() internal ValidationResult GetResult() { return new ValidationResult( - EventCount: _eventCount, - IsValid: _violations.Count == 0, - Violations: [.. _violations]); + EventCount: this._eventCount, + IsValid: this._violations.Count == 0, + Violations: [.. this._violations]); } // ═══════════════════════════════════════════════════════════════════════ @@ -232,28 +232,28 @@ internal ValidationResult GetResult() private void ValidateResponseCreated(JsonElement data) { - if (_state != StreamState.Initial) + if (this._state != StreamState.Initial) { - Fail("ORDER-01", $"'response.created' received in state {_state} (expected Initial — must be first event)"); + this.Fail("ORDER-01", $"'response.created' received in state {this._state} (expected Initial — must be first event)"); return; } - _state = StreamState.Created; + this._state = StreamState.Created; // Must have a response envelope if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", "'response.created' missing 'response' object"); + this.Fail("FIELD-01", "'response.created' missing 'response' object"); return; } // Required response fields - ValidateRequiredResponseFields(resp, "response.created"); + this.ValidateRequiredResponseFields(resp, "response.created"); // Capture response ID for cross-event checks if (resp.TryGetProperty("id", out var idProp)) { - _responseId = idProp.GetString(); + this._responseId = idProp.GetString(); } // Status must be non-terminal @@ -262,28 +262,28 @@ private void ValidateResponseCreated(JsonElement data) string? status = statusProp.GetString(); if (status is "completed" or "failed" or "incomplete" or "cancelled") { - Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); + this.Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); } } } private void ValidateTerminal(JsonElement data, string expectedKind) { - if (_state is StreamState.Initial or StreamState.Created) + if (this._state is StreamState.Initial or StreamState.Created) { - Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); + this.Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); } - _hasTerminal = true; - _state = StreamState.Terminal; + this._hasTerminal = true; + this._state = StreamState.Terminal; if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); + this.Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); return; } - ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); + this.ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); if (resp.TryGetProperty("status", out var statusProp)) { @@ -295,12 +295,12 @@ private void ValidateTerminal(JsonElement data, string expectedKind) if (status == "completed" && !hasCompletedAt) { - Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); + this.Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); } if (status != "completed" && hasCompletedAt) { - Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); + this.Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); } // error field validation @@ -309,39 +309,39 @@ private void ValidateTerminal(JsonElement data, string expectedKind) if (status == "failed" && !hasError) { - Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); + this.Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); } if (status is "completed" or "incomplete" && hasError) { - Fail("FIELD-05", $"'error' must be null when status is '{status}'"); + this.Fail("FIELD-05", $"'error' must be null when status is '{status}'"); } // error structure validation if (hasError) { - ValidateErrorObject(errProp, $"response.{expectedKind}"); + this.ValidateErrorObject(errProp, $"response.{expectedKind}"); } // cancelled output must be empty (B11) if (status == "cancelled" && resp.TryGetProperty("output", out var outputProp) && outputProp.ValueKind == JsonValueKind.Array && outputProp.GetArrayLength() > 0) { - Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); + this.Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); } // response ID consistency - if (_responseId is not null && resp.TryGetProperty("id", out var idProp) - && idProp.GetString() != _responseId) + if (this._responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != this._responseId) { - Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'"); } } // Usage validation (optional, but if present must be structured correctly) if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) { - ValidateUsage(usageProp, $"response.{expectedKind}"); + this.ValidateUsage(usageProp, $"response.{expectedKind}"); } } @@ -350,19 +350,19 @@ private void ValidateOutputItemAdded(JsonElement data) if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) { int index = idxProp.GetInt32(); - if (!_addedItemIndices.Add(index)) + if (!this._addedItemIndices.Add(index)) { - Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); + this.Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); } } else { - Fail("FIELD-06", "output_item.added missing 'output_index' field"); + this.Fail("FIELD-06", "output_item.added missing 'output_index' field"); } if (!data.TryGetProperty("item", out _)) { - Fail("FIELD-07", "output_item.added missing 'item' object"); + this.Fail("FIELD-07", "output_item.added missing 'item' object"); } } @@ -371,37 +371,37 @@ private void ValidateOutputItemDone(JsonElement data) if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) { int index = idxProp.GetInt32(); - if (!_addedItemIndices.Contains(index)) + if (!this._addedItemIndices.Contains(index)) { - Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); + this.Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); } - _doneItemIndices.Add(index); + this._doneItemIndices.Add(index); } else { - Fail("FIELD-06", "output_item.done missing 'output_index' field"); + this.Fail("FIELD-06", "output_item.done missing 'output_index' field"); } } private void ValidateContentPartAdded(JsonElement data) { string key = GetContentPartKey(data); - if (!_addedContentParts.Add(key)) + if (!this._addedContentParts.Add(key)) { - Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); + this.Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); } } private void ValidateContentPartDone(JsonElement data) { string key = GetContentPartKey(data); - if (!_addedContentParts.Contains(key)) + if (!this._addedContentParts.Contains(key)) { - Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); + this.Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); } - _doneContentParts.Add(key); + this._doneContentParts.Add(key); } private void ValidateTextDelta(JsonElement data) @@ -411,13 +411,13 @@ private void ValidateTextDelta(JsonElement data) ? deltaProp.GetString() ?? string.Empty : string.Empty; - if (!_textAccumulators.TryGetValue(key, out string? existing)) + if (!this._textAccumulators.TryGetValue(key, out string? existing)) { - _textAccumulators[key] = delta; + this._textAccumulators[key] = delta; } else { - _textAccumulators[key] = existing + delta; + this._textAccumulators[key] = existing + delta; } } @@ -430,13 +430,13 @@ private void ValidateTextDone(JsonElement data) if (finalText is null) { - Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); + this.Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); return; } - if (_textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) + if (this._textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) { - Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); + this.Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); } } @@ -448,34 +448,34 @@ private void ValidateRequiredResponseFields(JsonElement resp, string context) { if (!HasNonNullString(resp, "id")) { - Fail("FIELD-01", $"{context}: response missing 'id'"); + this.Fail("FIELD-01", $"{context}: response missing 'id'"); } if (resp.TryGetProperty("object", out var objProp)) { if (objProp.GetString() != "response") { - Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); + this.Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); } } else { - Fail("FIELD-08", $"{context}: response missing 'object' field"); + this.Fail("FIELD-08", $"{context}: response missing 'object' field"); } if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null) { - Fail("FIELD-09", $"{context}: response missing 'created_at'"); + this.Fail("FIELD-09", $"{context}: response missing 'created_at'"); } if (!resp.TryGetProperty("status", out _)) { - Fail("FIELD-10", $"{context}: response missing 'status'"); + this.Fail("FIELD-10", $"{context}: response missing 'status'"); } if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array) { - Fail("FIELD-11", $"{context}: response missing 'output' array"); + this.Fail("FIELD-11", $"{context}: response missing 'output' array"); } } @@ -483,12 +483,12 @@ private void ValidateErrorObject(JsonElement error, string context) { if (!HasNonNullString(error, "code")) { - Fail("ERR-02", $"{context}: error object missing 'code' field"); + this.Fail("ERR-02", $"{context}: error object missing 'code' field"); } if (!HasNonNullString(error, "message")) { - Fail("ERR-03", $"{context}: error object missing 'message' field"); + this.Fail("ERR-03", $"{context}: error object missing 'message' field"); } } @@ -496,17 +496,17 @@ private void ValidateUsage(JsonElement usage, string context) { if (!usage.TryGetProperty("input_tokens", out _)) { - Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); + this.Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); } if (!usage.TryGetProperty("output_tokens", out _)) { - Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); + this.Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); } if (!usage.TryGetProperty("total_tokens", out _)) { - Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); + this.Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); } } @@ -514,17 +514,17 @@ private void ValidateResponseEnvelope(JsonElement data, string eventType) { if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", $"'{eventType}' missing 'response' object"); + this.Fail("FIELD-01", $"'{eventType}' missing 'response' object"); return; } - ValidateRequiredResponseFields(resp, eventType); + this.ValidateRequiredResponseFields(resp, eventType); // Response ID consistency - if (_responseId is not null && resp.TryGetProperty("id", out var idProp) - && idProp.GetString() != _responseId) + if (this._responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != this._responseId) { - Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'"); } } @@ -534,27 +534,27 @@ private void ValidateResponseEnvelope(JsonElement data, string eventType) private void ValidateInProgress(string eventType) { - if (_state != StreamState.InProgress) + if (this._state != StreamState.InProgress) { - Fail("ORDER-04", $"'{eventType}' received in state {_state} (expected InProgress)"); + this.Fail("ORDER-04", $"'{eventType}' received in state {this._state} (expected InProgress)"); } } private void ValidateStateTransition(string eventType, StreamState expected, StreamState next) { - if (_state != expected) + if (this._state != expected) { - Fail("ORDER-05", $"'{eventType}' received in state {_state} (expected {expected})"); + this.Fail("ORDER-05", $"'{eventType}' received in state {this._state} (expected {expected})"); } else { - _state = next; + this._state = next; } } private void Fail(string ruleId, string message) { - _violations.Add(new ValidationViolation(ruleId, message, _eventCount)); + this._violations.Add(new ValidationViolation(ruleId, message, this._eventCount)); } private static bool HasNonNullString(JsonElement obj, string property) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs index ee32771a67..10d1271d10 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -20,8 +20,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; public class AgentFrameworkResponseHandlerTests { - private static string ValidResponseId => "resp_" + new string('0', 46); - [Fact] public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() { @@ -688,13 +686,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -721,13 +719,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -743,8 +741,8 @@ protected override IAsyncEnumerable RunCoreStreamingAsync( AgentRunOptions? options, CancellationToken cancellationToken = default) { - CapturedMessages = messages.ToList(); - CapturedOptions = options; + this.CapturedMessages = messages.ToList(); + this.CapturedOptions = options; return ToAsyncEnumerableAsync(new AgentResponseUpdate { MessageId = "resp_msg_1", @@ -765,13 +763,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -802,13 +800,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs index 34555798a7..85748214f1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs @@ -1,9 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Linq; -using System.Text.Json; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs index dde11298c7..f139e3bf71 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Extensions.AI; using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; @@ -233,7 +233,7 @@ public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitte public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted() { var (stream, _) = CreateTestStream(); - var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent((string)null!)] }; + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent(null!)] }; var events = new List(); await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) @@ -314,7 +314,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_Emit var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { - Contents = [new FunctionCallContent("call_1", "do_something", (IDictionary?)null)] + Contents = [new FunctionCallContent("call_1", "do_something", null)] }; var events = new List(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs index 9cf45d70af..2be9dc0bc8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; @@ -67,6 +67,6 @@ public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assert.Throws( - () => services.AddAgentFrameworkHandler((AIAgent)null!)); + () => services.AddAgentFrameworkHandler(null!)); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs index e5f9bb0405..e8f035ba85 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -27,8 +27,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; /// public class WorkflowIntegrationTests { - private static string ValidResponseId => "resp_" + new string('0', 46); - // ===== Sequential Workflow Tests ===== [Fact] @@ -156,7 +154,7 @@ public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() executionEnvironment: InProcessExecution.OffThread); var services = new ServiceCollection(); - services.AddKeyedSingleton("my-workflow", workflowAgent); + services.AddKeyedSingleton("my-workflow", workflowAgent); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -357,7 +355,7 @@ private static (AgentFrameworkResponseHandler handler, CreateResponse request, R CreateHandlerWithAgent(AIAgent agent, string userMessage) { var services = new ServiceCollection(); - services.AddSingleton(agent); + services.AddSingleton(agent); services.AddSingleton>(NullLogger.Instance); var sp = services.BuildServiceProvider(); From 59ad42912ff3992947adab61453d29e187830bf2 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 08:54:14 -0700 Subject: [PATCH 04/75] Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 20dbf32bb6..d5af9da0b6 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,9 @@ - - - + + + From bb7082d3c6cad737ffc390ce4995eb94d472f451 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 10:59:50 -0700 Subject: [PATCH 05/75] Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index c64f43949f..3ce985cdd3 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -7,6 +7,8 @@ $(VersionPrefix)-$(VersionSuffix).260402.1 $(VersionPrefix)-preview.260402.1 $(VersionPrefix) + + 0.9.0-hosted.260402.1 1.0.0 Debug;Release;Publish From a1c19b90cd0bbc6f3cd9d51dcf59a77e3b9837d3 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 13:31:58 -0700 Subject: [PATCH 06/75] Move Foundry Responses hosting into Microsoft.Agents.AI.Foundry package Move source and test files from the standalone Hosting.AzureAIResponses project into the Foundry package under a Hosting/ subfolder. This consolidates the Foundry-specific hosting adapter into the main Foundry package. - Source: Microsoft.Agents.AI.Foundry.Hosting namespace - Tests: merged into Foundry.UnitTests/Hosting/ - Conditionally compiled for .NETCoreApp TFMs only (net8.0+) - Deleted standalone Hosting.AzureAIResponses project and test project - Updated sample and solution references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 2 - .../FoundryResponsesHosting.csproj | 2 +- .../FoundryResponsesHosting/Program.cs | 4 +- .../Hosting}/AgentFrameworkResponseHandler.cs | 8 +++- .../Hosting}/InputConverter.cs | 7 +++- .../Hosting}/OutputConverter.cs | 8 +++- .../Hosting}/ServiceCollectionExtensions.cs | 5 ++- .../Microsoft.Agents.AI.Foundry.csproj | 22 +++++++++- ....Agents.AI.Hosting.AzureAIResponses.csproj | 40 ------------------- .../AgentFrameworkResponseHandlerTests.cs | 5 ++- .../Hosting}/InputConverterTests.cs | 5 ++- .../Hosting}/OutputConverterTests.cs | 5 ++- .../ServiceCollectionExtensionsTests.cs | 5 ++- .../Hosting}/WorkflowIntegrationTests.cs | 5 ++- ...crosoft.Agents.AI.Foundry.UnitTests.csproj | 14 +++++++ ....Hosting.AzureAIResponses.UnitTests.csproj | 17 -------- 16 files changed, 73 insertions(+), 81 deletions(-) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/AgentFrameworkResponseHandler.cs (97%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/InputConverter.cs (98%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/OutputConverter.cs (98%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/ServiceCollectionExtensions.cs (96%) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/AgentFrameworkResponseHandlerTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/InputConverterTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/OutputConverterTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/ServiceCollectionExtensionsTests.cs (93%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/WorkflowIntegrationTests.cs (99%) delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 3d0465763f..ab74c2650a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -494,7 +494,6 @@ - @@ -541,7 +540,6 @@ - diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj index 6725ef8d3b..3dfab4ad48 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -18,8 +18,8 @@ + - diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 88af464ac7..10a82d1b87 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents // using the Azure AI Responses Server SDK. @@ -21,7 +21,7 @@ using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; -using Microsoft.Agents.AI.Hosting.AzureAIResponses; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs similarity index 97% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 5f07cd4530..acb8e9c556 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -1,13 +1,17 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// A implementation that bridges the Azure AI Responses Server SDK diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index a35c8cd5b8..49bd216a90 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -1,12 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Converts Responses Server SDK input types to agent-framework types. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index f6b9aed006..1fd6672c83 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -1,17 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Converts agent-framework streams into diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs similarity index 96% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index c06353a8ec..bae8821745 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Extension methods for to register the agent-framework diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj index 670d140043..e3c1773941 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj @@ -3,7 +3,8 @@ true true - $(NoWarn);OPENAI001 + $(NoWarn);OPENAI001;MEAI001;NU1903 + false @@ -20,18 +21,32 @@ true + + + + + + + + + + + + + + Microsoft Agent Framework for Foundry Agents @@ -43,4 +58,9 @@ + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj deleted file mode 100644 index b881e287cc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - $(TargetFrameworksCore) - enable - Microsoft.Agents.AI.Hosting.AzureAIResponses - alpha - $(NoWarn);MEAI001;NU1903 - false - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index 10d1271d10..dc38c42739 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; @@ -16,7 +17,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class AgentFrameworkResponseHandlerTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index 85748214f1..7f36fd10d7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -1,13 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class InputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs index f139e3bf71..330312d2b4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Agents.AI.Workflows; @@ -12,7 +13,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class OutputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs similarity index 93% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs index 2be9dc0bc8..ee61cef71a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; using Moq; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class ServiceCollectionExtensionsTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index e8f035ba85..66924bdcba 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +18,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; /// /// Integration tests that verify workflow execution through the diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 7b85de0384..6473b799bf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -1,10 +1,24 @@ + + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj deleted file mode 100644 index 09c3ba24c1..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - $(TargetFrameworksCore) - false - $(NoWarn);NU1903;NU1605 - - - - - - - - - - - From af9807b4a4540867646df3f4512fab545b50c068 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 15:08:34 -0700 Subject: [PATCH 07/75] Bump package version to 0.9.0-hosted.260402.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 3ce985cdd3..b89b823219 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260402.1 $(VersionPrefix) - 0.9.0-hosted.260402.1 + 0.9.0-hosted.260402.2 1.0.0 Debug;Release;Publish From 5ba8521ce265f60b17ebc131e31b93377928ee40 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 15:14:43 -0700 Subject: [PATCH 08/75] Bump OpenTelemetry packages to fix NU1109 downgrade errors - OpenTelemetry/Api/Exporter.Console/Exporter.InMemory: 1.13.1 -> 1.15.0 - OpenTelemetry.Exporter.OpenTelemetryProtocol: already 1.15.0 - OpenTelemetry.Extensions.Hosting: already 1.14.0 - OpenTelemetry.Instrumentation.AspNetCore/Http: already 1.14.0 - OpenTelemetry.Instrumentation.Runtime: 1.13.0 -> 1.14.0 - Azure.Monitor.OpenTelemetry.Exporter: 1.4.0 -> 1.5.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d5af9da0b6..2a7fc3ee9b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -27,7 +27,7 @@ - + @@ -52,15 +52,15 @@ - - - - - - - - - + + + + + + + + + From 3b03c6dd5579ebf55a2a8afefc5ba381efd60e7d Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 16:39:13 -0700 Subject: [PATCH 09/75] Fix CA1873: guard LogWarning with IsEnabled check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Hosting/AgentFrameworkResponseHandler.cs | 8 +++++--- .../Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs | 3 +-- .../Hosting/OutputConverter.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index acb8e9c556..3423a0b4a3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; @@ -151,7 +150,10 @@ private AIAgent ResolveAgent(CreateResponse request) return agent; } - this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + if (this._logger.IsEnabled(LogLevel.Warning)) + { + this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } } // Try non-keyed default diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index 49bd216a90..e6d607c793 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -1,9 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index 1fd6672c83..79aaf768d9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index bae8821745..d8e8a83f29 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using Azure.AI.AgentServer.Responses; From 21dc5311bea7f8238db6890449e73d8dbdd68e8f Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 19:44:44 -0700 Subject: [PATCH 10/75] Fix model override bug and add client REPL sample - InputConverter: stop propagating request.Model to ChatOptions.ModelId Hosted agents use their own model; client-provided model values like 'hosted-agent' were being passed through and causing server errors. - Add FoundryResponsesRepl sample: interactive CLI client that connects to a Foundry Responses endpoint using ResponsesClient.AsAIAgent() - Bump package version to 0.9.0-hosted.260403.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 6 +- dotnet/nuget/nuget-package.props | 2 +- .../FoundryResponsesRepl.csproj | 21 ++++ .../FoundryResponsesRepl/Program.cs | 98 +++++++++++++++++++ .../Properties/launchSettings.json | 12 +++ .../Hosting/InputConverter.cs | 5 +- .../Hosting/InputConverterTests.cs | 7 +- 7 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index ab74c2650a..5d746ccd97 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -1,4 +1,4 @@ - + @@ -260,7 +260,9 @@ - + + + diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index b89b823219..6d1c657fa8 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260402.1 $(VersionPrefix) - 0.9.0-hosted.260402.2 + 0.9.0-hosted.260403.1 1.0.0 Debug;Release;Publish diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj b/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj new file mode 100644 index 0000000000..15cdf820eb --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs new file mode 100644 index 0000000000..be0e6ccf7b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs @@ -0,0 +1,98 @@ +// Foundry Responses Client REPL +// +// Connects to a Foundry Responses agent running on a given endpoint +// and provides an interactive multi-turn chat REPL. +// +// Usage: +// dotnet run (connects to http://localhost:8088) +// dotnet run -- --endpoint http://localhost:9090 +// dotnet run -- --endpoint https://my-foundry-project.services.ai.azure.com +// +// The endpoint should be running a Foundry Responses server (POST /responses). + +using System.ClientModel; +using Microsoft.Agents.AI; +using OpenAI; +using OpenAI.Responses; + +// ── Parse args ──────────────────────────────────────────────────────────────── + +string endpointUrl = "http://localhost:8088"; +for (int i = 0; i < args.Length - 1; i++) +{ + if (args[i] is "--endpoint" or "-e") + { + endpointUrl = args[i + 1]; + } +} + +// ── Create an agent-framework agent backed by the remote Responses endpoint ── + +// The OpenAI SDK's ResponsesClient can target any OpenAI-compatible endpoint. +// We use a dummy API key since our local server doesn't require auth. +var credential = new ApiKeyCredential( + Environment.GetEnvironmentVariable("RESPONSES_API_KEY") ?? "no-key-needed"); + +var openAiClient = new OpenAIClient( + credential, + new OpenAIClientOptions { Endpoint = new Uri(endpointUrl) }); + +ResponsesClient responsesClient = openAiClient.GetResponsesClient(); + +// Wrap as an agent-framework AIAgent via the OpenAI extensions. +// We pass an empty model since hosted agents use their own model configuration. +AIAgent agent = responsesClient.AsAIAgent( + model: "", + name: "remote-agent"); + +// Create a session so multi-turn context is preserved via previous_response_id +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); +Console.WriteLine("║ Foundry Responses Client REPL ║"); +Console.WriteLine($"║ Connected to: {endpointUrl,-41}║"); +Console.WriteLine("║ Type a message or 'quit' to exit ║"); +Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) continue; + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || + input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + break; + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + // Stream the response token-by-token + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json new file mode 100644 index 0000000000..4a939fc693 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "FoundryResponsesRepl": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61980;http://localhost:61981" + } + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index e6d607c793..10554d4e5c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -69,7 +69,10 @@ public static ChatOptions ConvertToChatOptions(CreateResponse request) Temperature = (float?)request.Temperature, TopP = (float?)request.TopP, MaxOutputTokens = (int?)request.MaxOutputTokens, - ModelId = request.Model, + // Note: We intentionally do NOT set ModelId from request.Model here. + // The hosted agent already has its own model configured, and passing + // the client-provided model would override it (causing failures when + // clients send placeholder values like "hosted-agent"). }; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index 7f36fd10d7..48c1a22ee8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -157,7 +157,7 @@ public void ConvertToChatOptions_SetsTemperatureAndTopP() Assert.Equal(0.7f, options.Temperature); Assert.Equal(0.9f, options.TopP); Assert.Equal(1000, options.MaxOutputTokens); - Assert.Equal("gpt-4o", options.ModelId); + Assert.Null(options.ModelId); } [Fact] @@ -659,12 +659,13 @@ public void ConvertOutputItemsToMessages_UnknownOutputItemType_IsSkipped() } [Fact] - public void ConvertToChatOptions_ModelId_SetFromRequest() + public void ConvertToChatOptions_ModelId_NotSetFromRequest() { var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-model"); var options = InputConverter.ConvertToChatOptions(request); - Assert.Equal("my-model", options.ModelId); + // Model from the request is intentionally NOT propagated — the hosted agent uses its own model. + Assert.Null(options.ModelId); } } From eb673790338d3c977e1744e88280e8289cbb6bf7 Mon Sep 17 00:00:00 2001 From: alliscode Date: Fri, 3 Apr 2026 11:17:21 -0700 Subject: [PATCH 11/75] Catch agent errors and emit response.failed with real error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, unhandled exceptions from agent execution would bubble up to the SDK orchestrator, which emits a generic 'An internal server error occurred.' message — hiding the actual cause (e.g., 401 auth failures, model not found, etc.). Now AgentFrameworkResponseHandler catches non-cancellation exceptions and emits a proper response.failed event containing the real error message, making it visible to clients and in logs. OperationCanceledException still propagates for proper cancellation handling by the SDK. Also bumps package version to 0.9.0-hosted.260403.2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 +- .../FoundryResponsesHosting.csproj | 2 +- .../Hosting/AgentFrameworkResponseHandler.cs | 21 +++++++++++++++++++ .../AgentFrameworkResponseHandlerTests.cs | 19 ++++++++++------- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 6d1c657fa8..2cbd1356bc 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260402.1 $(VersionPrefix) - 0.9.0-hosted.260403.1 + 0.9.0-hosted.260403.2 1.0.0 Debug;Release;Publish diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj index 3dfab4ad48..269754203d 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -6,7 +6,7 @@ enable enable false - $(NoWarn);NU1903;NU1605 + $(NoWarn);NU1903;NU1605;MAAIW001 diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 3423a0b4a3..eaacbc67a3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -95,6 +95,7 @@ public override async IAsyncEnumerable CreateAsync( while (true) { bool shutdownDetected = false; + ResponseStreamEvent? failedEvent = null; ResponseStreamEvent? evt = null; try { @@ -109,6 +110,26 @@ public override async IAsyncEnumerable CreateAsync( { shutdownDetected = true; } + catch (Exception ex) when (ex is not OperationCanceledException && !emittedTerminal) + { + // Catch agent execution errors and emit a proper failed event + // with the real error message instead of letting the SDK emit + // a generic "An internal server error occurred." + if (this._logger.IsEnabled(LogLevel.Error)) + { + this._logger.LogError(ex, "Agent execution failed for response {ResponseId}.", context.ResponseId); + } + + failedEvent = stream.EmitFailed( + ResponseErrorCode.ServerError, + ex.Message); + } + + if (failedEvent is not null) + { + yield return failedEvent; + yield break; + } if (shutdownDetected) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index dc38c42739..b1bdd31916 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -538,7 +538,7 @@ public async Task CreateAsync_PassesInstructionsToAgent() } [Fact] - public async Task CreateAsync_AgentThrows_ExceptionPropagates() + public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() { // Arrange var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); @@ -561,13 +561,18 @@ public async Task CreateAsync_AgentThrows_ExceptionPropagates() mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - // Act & Assert - await Assert.ThrowsAsync(async () => + // Act — collect all events + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) { - await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) - { - } - }); + events.Add(evt); + } + + // Assert — should contain created, in_progress, and failed (with real error message) + Assert.Contains(events, e => e is ResponseCreatedEvent); + Assert.Contains(events, e => e is ResponseInProgressEvent); + var failedEvent = Assert.Single(events.OfType()); + Assert.Contains("Agent crashed", failedEvent.Response.Error.Message); } [Fact] From 23db9569ce9e4d4faceaec85fece61f4cfcae3ef Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Fri, 3 Apr 2026 16:35:44 -0700 Subject: [PATCH 12/75] Renaming and merging hosting extensions. (#5091) * Rename AddAgentFrameworkHandler to AddFoundryResponses and add MapFoundryResponses - Rename extension methods: AddAgentFrameworkHandler -> AddFoundryResponses, MapAgentFrameworkHandler -> MapFoundryResponses - AddFoundryResponses now calls AddResponsesServer() internally - Add MapFoundryResponses() extension on IEndpointRouteBuilder - Update sample and tests to use new API names - Remove redundant AddResponsesServer() and /ready endpoint from sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixing numbering in sample. --------- Co-authored-by: alliscode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FoundryResponsesHosting/Program.cs | 25 ++++----- .../Hosting/ServiceCollectionExtensions.cs | 51 ++++++++++++------- .../ServiceCollectionExtensionsTests.cs | 22 ++++---- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 10a82d1b87..59fa723949 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents // using the Azure AI Responses Server SDK. @@ -16,7 +16,6 @@ // - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o") using System.ComponentModel; -using Azure.AI.AgentServer.Responses; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; @@ -29,22 +28,16 @@ var builder = WebApplication.CreateBuilder(args); // --------------------------------------------------------------------------- -// 1. Register the Azure AI Responses Server SDK +// 1. Create the shared Azure OpenAI chat client // --------------------------------------------------------------------------- -builder.Services.AddResponsesServer(); - -// --------------------------------------------------------------------------- -// 2. 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 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()); IChatClient chatClient = azureClient.GetChatClient(deployment).AsIChatClient(); // --------------------------------------------------------------------------- -// 3. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP +// 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() @@ -72,7 +65,7 @@ You are a helpful assistant hosted as a Foundry Hosted Agent. .WithAITools(mcpTools.Cast().ToArray()); // --------------------------------------------------------------------------- -// 4. DEMO 2: Triage Workflow — routes to specialist agents +// 3. DEMO 2: Triage Workflow — routes to specialist agents // --------------------------------------------------------------------------- ChatClientAgent triageAgent = new( chatClient, @@ -113,9 +106,9 @@ Do NOT answer the question yourself - just route it. triageWorkflow.AsAIAgent(name: key)); // --------------------------------------------------------------------------- -// 5. Wire up the agent-framework handler as the IResponseHandler +// 4. Wire up the agent-framework handler and Responses Server SDK // --------------------------------------------------------------------------- -builder.Services.AddAgentFrameworkHandler(); +builder.Services.AddFoundryResponses(); var app = builder.Build(); @@ -124,10 +117,10 @@ Do NOT answer the question yourself - just route it. mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult()); // --------------------------------------------------------------------------- -// 6. Routes +// 5. Routes // --------------------------------------------------------------------------- app.MapGet("/ready", () => Results.Ok("ready")); -app.MapResponsesServer(); +app.MapFoundryResponses(); app.MapGet("/", () => Results.Content(Pages.Home, "text/html")); app.MapGet("/tool-demo", () => Results.Content(Pages.ToolDemo, "text/html")); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index d8e8a83f29..e3ff61ad0c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -2,78 +2,93 @@ using System; using Azure.AI.AgentServer.Responses; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Agents.AI.Foundry.Hosting; /// -/// Extension methods for to register the agent-framework -/// response handler with the Azure AI Responses Server SDK. +/// Extension methods for registering agent-framework agents as Foundry Hosted Agents +/// using the Azure AI Responses Server SDK. /// -public static class AgentFrameworkResponsesServiceCollectionExtensions +public static class FoundryHostingExtensions { /// - /// Registers as the - /// for the Azure AI Responses Server SDK. Agents are resolved from keyed DI services + /// Registers the Azure AI Responses Server SDK and + /// as the . Agents are resolved from keyed DI services /// using the agent.name or metadata["entity_id"] from incoming requests. /// /// /// - /// Call this method after AddResponsesServer() and after registering your - /// instances (e.g., via AddAIAgent()). + /// This method calls AddResponsesServer() internally, so you do not need to + /// call it separately. Register your instances before calling this. /// /// /// Example: /// - /// builder.Services.AddResponsesServer(); /// builder.AddAIAgent("my-agent", ...); - /// builder.Services.AddAgentFrameworkHandler(); + /// builder.Services.AddFoundryResponses(); /// /// var app = builder.Build(); - /// app.MapResponsesServer(); + /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The service collection for chaining. - public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); + services.AddResponsesServer(); services.TryAddSingleton(); return services; } /// - /// Registers a specific as the handler for all incoming requests, - /// regardless of the agent.name in the request. + /// Registers the Azure AI Responses Server SDK and a specific + /// as the handler for all incoming requests, regardless of the agent.name in the request. /// /// /// /// Use this overload when hosting a single agent. The provided agent instance is - /// registered both as a keyed service and as the default . + /// registered as both a keyed service and the default . + /// This method calls AddResponsesServer() internally. /// /// /// Example: /// - /// builder.Services.AddResponsesServer(); - /// builder.Services.AddAgentFrameworkHandler(myAgent); + /// builder.Services.AddFoundryResponses(myAgent); /// /// var app = builder.Build(); - /// app.MapResponsesServer(); + /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The agent instance to register. /// The service collection for chaining. - public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services, AIAgent agent) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); + services.AddResponsesServer(); services.TryAddSingleton(agent); services.TryAddSingleton(); return services; } + + /// + /// Maps the Responses API routes for the agent-framework handler to the endpoint routing pipeline. + /// + /// The endpoint route builder. + /// Optional route prefix (e.g., "/openai/v1"). Default: empty (routes at /responses). + /// The endpoint route builder for chaining. + public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuilder endpoints, string prefix = "") + { + ArgumentNullException.ThrowIfNull(endpoints); + endpoints.MapResponsesServer(prefix); + return endpoints; + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs index ee61cef71a..d3fffaed5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs @@ -12,12 +12,12 @@ namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class ServiceCollectionExtensionsTests { [Fact] - public void AddAgentFrameworkHandler_RegistersResponseHandler() + public void AddFoundryResponses_RegistersResponseHandler() { var services = new ServiceCollection(); services.AddLogging(); - services.AddAgentFrameworkHandler(); + services.AddFoundryResponses(); var descriptor = services.FirstOrDefault( d => d.ServiceType == typeof(ResponseHandler)); @@ -26,33 +26,33 @@ public void AddAgentFrameworkHandler_RegistersResponseHandler() } [Fact] - public void AddAgentFrameworkHandler_CalledTwice_RegistersOnce() + public void AddFoundryResponses_CalledTwice_RegistersOnce() { var services = new ServiceCollection(); services.AddLogging(); - services.AddAgentFrameworkHandler(); - services.AddAgentFrameworkHandler(); + services.AddFoundryResponses(); + services.AddFoundryResponses(); var count = services.Count(d => d.ServiceType == typeof(ResponseHandler)); Assert.Equal(1, count); } [Fact] - public void AddAgentFrameworkHandler_NullServices_ThrowsArgumentNullException() + public void AddFoundryResponses_NullServices_ThrowsArgumentNullException() { Assert.Throws( - () => AgentFrameworkResponsesServiceCollectionExtensions.AddAgentFrameworkHandler(null!)); + () => FoundryHostingExtensions.AddFoundryResponses(null!)); } [Fact] - public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() + public void AddFoundryResponses_WithAgent_RegistersAgentAndHandler() { var services = new ServiceCollection(); services.AddLogging(); var mockAgent = new Mock(); - services.AddAgentFrameworkHandler(mockAgent.Object); + services.AddFoundryResponses(mockAgent.Object); var handlerDescriptor = services.FirstOrDefault( d => d.ServiceType == typeof(ResponseHandler)); @@ -64,10 +64,10 @@ public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() } [Fact] - public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() + public void AddFoundryResponses_WithNullAgent_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assert.Throws( - () => services.AddAgentFrameworkHandler(null!)); + () => services.AddFoundryResponses(null!)); } } From 53dd99018599fb0e93e1ccc65cf4d9790d1307b3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:30:43 +0100 Subject: [PATCH 13/75] Address breaking changes in 260408 --- dotnet/Directory.Packages.props | 6 +- .../Hosting/AgentFrameworkResponseHandler.cs | 4 +- .../Hosting/InputConverter.cs | 21 +++++ .../AgentFrameworkResponseHandlerTests.cs | 77 +++++++++---------- .../Hosting/WorkflowIntegrationTests.cs | 4 +- 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 2a7fc3ee9b..11628f4e38 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,9 @@ - - - + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index eaacbc67a3..40ad435382 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -66,10 +66,10 @@ public override async IAsyncEnumerable CreateAsync( } // Load and convert current input items - var inputItems = await context.GetInputItemsAsync(cancellationToken).ConfigureAwait(false); + var inputItems = await context.GetInputItemsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); if (inputItems.Count > 0) { - messages.AddRange(InputConverter.ConvertOutputItemsToMessages(inputItems)); + messages.AddRange(InputConverter.ConvertItemsToMessages(inputItems)); } else { diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index 10554d4e5c..1d8be8f590 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -36,6 +36,27 @@ public static List ConvertInputToMessages(CreateResponse request) return messages; } + /// + /// Converts resolved SDK input items into instances. + /// + /// The resolved input items from the SDK context. + /// A list of chat messages. + public static List ConvertItemsToMessages(IReadOnlyList items) + { + var messages = new List(); + + foreach (var item in items) + { + var message = ConvertInputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + /// /// Converts resolved SDK history/input items into instances. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index b1bdd31916..eac5c60263 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -43,8 +43,8 @@ public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -82,8 +82,8 @@ public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -116,8 +116,8 @@ public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationException( var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert await Assert.ThrowsAsync(async () => @@ -164,8 +164,8 @@ public async Task CreateAsync_ResolvesAgentByModelField() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -203,8 +203,8 @@ public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -241,8 +241,8 @@ public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -277,8 +277,8 @@ public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert var ex = await Assert.ThrowsAsync(async () => @@ -310,8 +310,8 @@ public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert var ex = await Assert.ThrowsAsync(async () => @@ -343,8 +343,8 @@ public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvent var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -396,8 +396,8 @@ public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(new OutputItem[] { historyItem }); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -431,20 +431,15 @@ public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() content = new[] { new { type = "input_text", text = "Raw input" } } } }); - var inputItem = new OutputItemMessage( - id: "input_1", - role: MessageRole.Assistant, - content: [new MessageContentOutputTextContent( - "Resolved input", - Array.Empty(), - Array.Empty())], - status: MessageStatus.Completed); + var inputItem = new ItemMessage( + MessageRole.Assistant, + [new MessageContentInputTextContent("Resolved input")]); var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(new OutputItem[] { inputItem }); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new Item[] { inputItem }); // Act var events = new List(); @@ -481,8 +476,8 @@ public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -521,8 +516,8 @@ public async Task CreateAsync_PassesInstructionsToAgent() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -558,8 +553,8 @@ public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act — collect all events var events = new List(); @@ -600,8 +595,8 @@ public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -636,8 +631,8 @@ public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCan var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); using var cts = new CancellationTokenSource(); cts.Cancel(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index 66924bdcba..be78f8046d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -383,8 +383,8 @@ private static Mock CreateMockContext() var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mock.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mock.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mock.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); return mock; } From 176b8f9b3419b08ffd163b190633701541d5c6a0 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:46:10 +0100 Subject: [PATCH 14/75] Bump hosted internal package version --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 2cbd1356bc..27e2bbea9e 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260402.1 $(VersionPrefix) - 0.9.0-hosted.260403.2 + 0.9.0-hosted.260409.1 1.0.0 Debug;Release;Publish From fed3c06817e89b6dc348541dc3dff9a982b6e86f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:40:54 +0100 Subject: [PATCH 15/75] Add UserAgent middleware tests for Foundry hosting --- .../Hosting/ServiceCollectionExtensions.cs | 54 +++++++ .../Hosting/UserAgentMiddlewareTests.cs | 134 ++++++++++++++++++ ...crosoft.Agents.AI.Foundry.UnitTests.csproj | 1 + 3 files changed, 189 insertions(+) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index e3ff61ad0c..7f01e5904c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Reflection; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -89,6 +93,56 @@ public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuild { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapResponsesServer(prefix); + + if (endpoints is IApplicationBuilder app) + { + // Ensure the middleware is added to the pipeline + app.UseMiddleware(); + } + return endpoints; } + + private sealed class AgentFrameworkUserAgentMiddleware(RequestDelegate next) + { + private static readonly string s_userAgentValue = CreateUserAgentValue(); + + public async Task InvokeAsync(HttpContext context) + { + var headers = context.Request.Headers; + var userAgent = headers.UserAgent.ToString(); + + if (string.IsNullOrEmpty(userAgent)) + { + headers.UserAgent = s_userAgentValue; + } + else if (!userAgent.Contains(s_userAgentValue, StringComparison.OrdinalIgnoreCase)) + { + headers.UserAgent = $"{userAgent} {s_userAgentValue}"; + } + + await next(context).ConfigureAwait(false); + } + + private static string CreateUserAgentValue() + { + const string Name = "agent-framework-dotnet"; + + if (typeof(AgentFrameworkUserAgentMiddleware).Assembly.GetCustomAttribute()?.InformationalVersion is string version) + { + int pos = version.IndexOf('+'); + if (pos >= 0) + { + version = version.Substring(0, pos); + } + + if (version.Length > 0) + { + return $"{Name}/{version}"; + } + } + + return Name; + } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs new file mode 100644 index 0000000000..64f7458852 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; + +/// +/// Tests for the AgentFrameworkUserAgentMiddleware registered by +/// . +/// +public sealed partial class UserAgentMiddlewareTests : IAsyncDisposable +{ + private const string VersionedUserAgentPattern = @"agent-framework-dotnet/\d+\.\d+\.\d+"; + + private WebApplication? _app; + private HttpClient? _httpClient; + + public async ValueTask DisposeAsync() + { + this._httpClient?.Dispose(); + if (this._app != null) + { + await this._app.DisposeAsync(); + } + } + + [Fact] + public async Task MapFoundryResponses_NoUserAgentHeader_SetsAgentFrameworkUserAgentAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + [Fact] + public async Task MapFoundryResponses_WithExistingUserAgent_AppendsAgentFrameworkUserAgentAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + request.Headers.TryAddWithoutValidation("User-Agent", "MyApp/1.0"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.StartsWith("MyApp/1.0", userAgent); + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + [Fact] + public async Task MapFoundryResponses_AlreadyContainsUserAgent_DoesNotDuplicateAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + // First request to capture the actual middleware-generated value + using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + var firstResponse = await this._httpClient!.SendAsync(firstRequest); + var middlewareValue = await firstResponse.Content.ReadAsStringAsync(); + + // Act: send a second request that already contains the middleware value + using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + secondRequest.Headers.TryAddWithoutValidation("User-Agent", $"MyApp/2.0 {middlewareValue}"); + var secondResponse = await this._httpClient!.SendAsync(secondRequest); + var userAgent = await secondResponse.Content.ReadAsStringAsync(); + + // Assert: should remain unchanged (no duplication) + Assert.Equal($"MyApp/2.0 {middlewareValue}", userAgent); + Assert.Single(VersionedUserAgentRegex().Matches(userAgent)); + } + + [Fact] + public async Task MapFoundryResponses_UserAgentValue_ContainsVersionAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert: should match "agent-framework-dotnet/x.y.z" pattern + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + private async Task CreateTestServerAsync() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockAgent = new Mock(); + builder.Services.AddFoundryResponses(mockAgent.Object); + + this._app = builder.Build(); + this._app.MapFoundryResponses(); + + // Test endpoint that echoes the User-Agent header after middleware processing + this._app.MapGet("/test-ua", (HttpContext ctx) => + Results.Text(ctx.Request.Headers.UserAgent.ToString())); + + await this._app.StartAsync(); + + var testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + } + + [GeneratedRegex(VersionedUserAgentPattern)] + private static partial Regex VersionedUserAgentRegex(); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 6473b799bf..f006096208 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -12,6 +12,7 @@ + From 7407c0a35ed2e4cb69fdcf9bbe0db5dfb07d7bdc Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:57:16 +0100 Subject: [PATCH 16/75] Hosting Samples update --- dotnet/Directory.Packages.props | 1 + dotnet/agent-framework-dotnet.slnx | 38 +++++++---- .../AgentThreadAndHITL.csproj | 0 .../AgentThreadAndHITL/Dockerfile | 0 .../AgentThreadAndHITL/Program.cs | 0 .../AgentThreadAndHITL/README.md | 0 .../AgentThreadAndHITL/agent.yaml | 0 .../AgentThreadAndHITL/run-requests.http | 0 .../AgentWithHostedMCP.csproj | 0 .../AgentWithHostedMCP/Dockerfile | 0 .../AgentWithHostedMCP/Program.cs | 0 .../AgentWithHostedMCP/README.md | 0 .../AgentWithHostedMCP/agent.yaml | 0 .../AgentWithHostedMCP/run-requests.http | 0 .../AgentWithLocalTools/.dockerignore | 0 .../AgentWithLocalTools.csproj | 0 .../AgentWithLocalTools/Dockerfile | 0 .../AgentWithLocalTools/Program.cs | 0 .../AgentWithLocalTools/README.md | 0 .../AgentWithLocalTools/agent.yaml | 0 .../AgentWithLocalTools/run-requests.http | 0 .../AgentWithTextSearchRag.csproj | 0 .../AgentWithTextSearchRag/Dockerfile | 0 .../AgentWithTextSearchRag/Program.cs | 0 .../AgentWithTextSearchRag/README.md | 0 .../AgentWithTextSearchRag/agent.yaml | 0 .../AgentWithTextSearchRag/run-requests.http | 0 .../AgentsInWorkflows.csproj | 0 .../AgentsInWorkflows/Dockerfile | 0 .../AgentsInWorkflows/Program.cs | 0 .../AgentsInWorkflows/README.md | 0 .../AgentsInWorkflows/agent.yaml | 0 .../AgentsInWorkflows/run-requests.http | 0 .../FoundryMultiAgent/Dockerfile | 0 .../FoundryMultiAgent.csproj | 0 .../FoundryMultiAgent/Program.cs | 0 .../FoundryMultiAgent/README.md | 0 .../FoundryMultiAgent/agent.yaml | 0 .../appsettings.Development.json | 0 .../FoundryMultiAgent/run-requests.http | 0 .../FoundrySingleAgent/Dockerfile | 0 .../FoundrySingleAgent.csproj | 0 .../FoundrySingleAgent/Program.cs | 0 .../FoundrySingleAgent/README.md | 0 .../FoundrySingleAgent/agent.yaml | 0 .../FoundrySingleAgent/run-requests.http | 0 .../HostedAgentsV1}/README.md | 0 .../HostedAgentsV2/consumption/Program.cs | 68 +++++++++++++++++++ .../consumption/SimpleAgent.csproj | 22 ++++++ .../HostedAgentsV2/foundry-hosting/Dockerfile | 17 +++++ .../HostedAgentsV2/foundry-hosting/Program.cs | 30 ++++++++ .../Properties/launchSettings.json | 12 ++++ .../HostedAgentsV2/foundry-hosting/agent.yaml | 20 ++++++ .../simple-agent-foundry.csproj | 25 +++++++ .../instance-hosting/Dockerfile | 17 +++++ .../HostedChatClientAgent.csproj | 25 +++++++ .../instance-hosting/Program.cs | 33 +++++++++ .../Properties/launchSettings.json | 12 ++++ .../instance-hosting/agent.yaml | 20 ++++++ .../AzureAIProjectChatClientExtensions.cs | 8 +-- 60 files changed, 329 insertions(+), 19 deletions(-) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/AgentThreadAndHITL.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/AgentWithHostedMCP.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/.dockerignore (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/AgentWithLocalTools.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/AgentsInWorkflows.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/FoundryMultiAgent.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/appsettings.Development.json (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/FoundrySingleAgent.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/README.md (100%) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 11628f4e38..36e68c958a 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 5d746ccd97..f52c04eb7a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -263,6 +263,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -315,15 +335,6 @@ - - - - - - - - - @@ -485,13 +496,12 @@ - - + @@ -513,11 +523,10 @@ - + - @@ -533,12 +542,11 @@ - - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/.dockerignore rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs new file mode 100644 index 0000000000..f1c3686279 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── +// The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. + +var aiProjectClient = new AIProjectClient(new Uri(agentEndpoint), new AzureCliCredential()); +var agent = aiProjectClient.AsAIAgent(); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); +Console.WriteLine("║ Simple Agent Client ║"); +Console.WriteLine($"║ Connected to: {agentEndpoint,-41}║"); +Console.WriteLine("║ Type a message or 'quit' to exit ║"); +Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || + input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj new file mode 100644 index 0000000000..d2651ef7a7 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + enable + enable + false + SimpleAgentClient + simple-agent-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile new file mode 100644 index 0000000000..2898f31ed0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile @@ -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", "simple-agent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs new file mode 100644 index 0000000000..ad0f2b2ca2 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs @@ -0,0 +1,30 @@ +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); + +// Retrieve the Foundry-managed agent by name (latest version). +ProjectsAgentRecord agentRecord = await aiProjectClient + .AgentAdministrationClient.GetAgentAsync(agentName); + +AIAgent agent = aiProjectClient.AsAIAgent(agentRecord); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); +app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json new file mode 100644 index 0000000000..fc4cf2a105 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "simple-agent-foundry": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59703;http://localhost:59708" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml new file mode 100644 index 0000000000..a0fe89f966 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml @@ -0,0 +1,20 @@ +name: simple-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 + - Simple Agent + - Foundry Agent +template: + name: simple-agent + kind: hosted + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: AGENT_NAME + value: ${AGENT_NAME} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj new file mode 100644 index 0000000000..a9adf5bb21 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + SimpleAgent + simple-agent + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile new file mode 100644 index 0000000000..2898f31ed0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile @@ -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", "simple-agent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj new file mode 100644 index 0000000000..a9adf5bb21 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + SimpleAgent + simple-agent + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs new file mode 100644 index 0000000000..8571f0a1f0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs @@ -0,0 +1,33 @@ +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// Create the agent via the AI project client using the Responses API. +AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()) + .AsAIAgent( + model: deployment, + instructions: """ + You are a helpful AI assistant hosted as a Foundry Hosted Agent. + You can help with a wide range of tasks including answering questions, + providing explanations, brainstorming ideas, and offering guidance. + Be concise, clear, and helpful in your responses. + """, + name: "simple-agent", + description: "A simple general-purpose AI assistant"); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); +app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json new file mode 100644 index 0000000000..5f47fe2db6 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Hosted-ChatClientAgent": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59054;http://localhost:59055" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml new file mode 100644 index 0000000000..dfab24e712 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml @@ -0,0 +1,20 @@ +name: simple-agent +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent. +metadata: + tags: + - AI Agent Hosting + - Simple Agent +template: + name: simple-agent + kind: hosted + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: AGENT_NAME + value: ${AGENT_NAME} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index 4383cfb6d4..4b895e4bc0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -181,7 +182,7 @@ public static ChatClientAgent AsAIAgent( /// Creates a non-versioned backed by the project's Responses API using the specified options. /// /// The to use for Responses API calls. Cannot be . - /// Configuration options that control the agent's behavior. is required. + /// Optional configuration options that control the agent's behavior. /// Provides a way to customize the creation of the underlying used by the agent. /// Optional logger factory for creating loggers used by the agent. /// An optional to use for resolving services required by the instances being invoked. @@ -190,15 +191,14 @@ public static ChatClientAgent AsAIAgent( /// Thrown when does not specify . public static ChatClientAgent AsAIAgent( this AIProjectClient aiProjectClient, - ChatClientAgentOptions options, + ChatClientAgentOptions? options = null, Func? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { Throw.IfNull(aiProjectClient); - Throw.IfNull(options); - return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + return CreateResponsesChatClientAgent(aiProjectClient, options ?? new(), clientFactory, loggerFactory, services); } #region Private From 4ab43734c65d355efdb3deb203fcacb9141ffc55 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:35:53 +0100 Subject: [PATCH 17/75] Hosting Samples update --- .../HostedAgentsV2/instance-hosting/Program.cs | 2 ++ .../instance-hosting/Properties/launchSettings.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs index 8571f0a1f0..07c7103c1a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json index 5f47fe2db6..b7f2c658ac 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:59054;http://localhost:59055" + "applicationUrl": "http://localhost:8088" } } } \ No newline at end of file From 026f71ac32f3790b421d9c8ca3f32421512c1d66 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:36:00 +0100 Subject: [PATCH 18/75] Hosting Samples update --- dotnet/agent-framework-dotnet.slnx | 8 ++++---- .../HostedAgentsV2/consumption/Program.cs | 18 +++++++++--------- ...oundry.csproj => HostedFoundryAgent.csproj} | 0 .../HostedAgentsV2/foundry-hosting/Program.cs | 6 ++++-- .../Properties/launchSettings.json | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/{simple-agent-foundry.csproj => HostedFoundryAgent.csproj} (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index f52c04eb7a..16778e2278 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -274,14 +274,14 @@ - - - - + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs index f1c3686279..f0a1e74069 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs @@ -8,7 +8,7 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:59055"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── // The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. @@ -21,11 +21,13 @@ // ── REPL ────────────────────────────────────────────────────────────────────── Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); -Console.WriteLine("║ Simple Agent Client ║"); -Console.WriteLine($"║ Connected to: {agentEndpoint,-41}║"); -Console.WriteLine("║ Type a message or 'quit' to exit ║"); -Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Simple Agent Sample + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); Console.ResetColor(); Console.WriteLine(); @@ -38,9 +40,7 @@ string? input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || - input.Equals("exit", StringComparison.OrdinalIgnoreCase)) - { break; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } try { diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs index ad0f2b2ca2..0fe1ad9ac3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs @@ -1,8 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using DotNetEnv; -using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Agents.AI.Foundry.Hosting; // Load .env file if present (for local development) @@ -19,7 +21,7 @@ ProjectsAgentRecord agentRecord = await aiProjectClient .AgentAdministrationClient.GetAgentAsync(agentName); -AIAgent agent = aiProjectClient.AsAIAgent(agentRecord); +FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord); // Host the agent as a Foundry Hosted Agent using the Responses API. var builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json index fc4cf2a105..11588cf909 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:59703;http://localhost:59708" + "applicationUrl": "http://localhost:8089" } } } \ No newline at end of file From eb7f8617c1f7bf6b36b004fa209abc11f70b64ab Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:05:44 +0100 Subject: [PATCH 19/75] Hosting Samples update --- dotnet/agent-framework-dotnet.slnx | 6 +++--- .../Dockerfile | 2 +- .../HostedChatClientAgent.csproj | 10 +++------- .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../agent.yaml | 0 .../Dockerfile | 2 +- .../HostedFoundryAgent.csproj | 10 +++------- .../Program.cs | 0 .../Properties/launchSettings.json | 2 +- .../agent.yaml | 0 .../{consumption => UsingHostedAgent}/Program.cs | 2 +- .../SimpleAgent.csproj | 0 13 files changed, 13 insertions(+), 21 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Dockerfile (88%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/HostedChatClientAgent.csproj (59%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Dockerfile (88%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/HostedFoundryAgent.csproj (59%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Properties/launchSettings.json (81%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{consumption => UsingHostedAgent}/Program.cs (98%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{consumption => UsingHostedAgent}/SimpleAgent.csproj (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 16778e2278..969a5c16d8 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -275,13 +275,13 @@ - + - + - + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile similarity index 88% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile index 2898f31ed0..6f1be8ee8e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile @@ -14,4 +14,4 @@ WORKDIR /app COPY --from=build /app/publish . EXPOSE 8088 ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "simple-agent.dll"] +ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj similarity index 59% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj index a9adf5bb21..096681d505 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj @@ -5,16 +5,12 @@ enable enable false - SimpleAgent - simple-agent - $(NoWarn);NU1903;NU1605 + HostedChatClientAgent + HostedChatClientAgent + $(NoWarn); - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile similarity index 88% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile index 2898f31ed0..eda1f7e1e9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile @@ -14,4 +14,4 @@ WORKDIR /app COPY --from=build /app/publish . EXPOSE 8088 ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "simple-agent.dll"] +ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj similarity index 59% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj index a9adf5bb21..2fc0ec43e4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -5,16 +5,12 @@ enable enable false - SimpleAgent - simple-agent - $(NoWarn);NU1903;NU1605 + HostedFoundryAgent + HostedFoundryAgent + $(NoWarn); - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json similarity index 81% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json index 11588cf909..a7047d02a1 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:8089" + "applicationUrl": "http://localhost:8088" } } } \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs similarity index 98% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index f0a1e74069..774cd3781a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -8,7 +8,7 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:59055"; +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── // The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj From a82c1ede09e84dc3f4c80d342cbc58b8d0e030ed Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:03:55 +0100 Subject: [PATCH 20/75] ChatClientAgent working --- .../Hosted-ChatClientAgent/Program.cs | 7 +++ .../Hosted-FoundryAgent/Program.cs | 7 +++ .../UsingHostedAgent/Program.cs | 45 ++++++++++++++++++- .../UsingHostedAgent/SimpleAgent.csproj | 2 + .../AzureAIProjectChatClientExtensions.cs | 4 +- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 07c7103c1a..88a2b1d610 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -32,4 +32,11 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. var app = builder.Build(); app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index 0fe1ad9ac3..c946d60058 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -29,4 +29,11 @@ var app = builder.Build(); app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index 774cd3781a..27e0a404d7 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; @@ -11,9 +12,21 @@ string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── -// The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. -var aiProjectClient = new AIProjectClient(new Uri(agentEndpoint), new AzureCliCredential()); +var endpointUri = new Uri(agentEndpoint); +var options = new AIProjectClientOptions(); + +// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy +// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right +// before the request hits the wire. +Uri clientEndpoint = endpointUri; +if (endpointUri.Scheme == "http") +{ + clientEndpoint = new UriBuilder(endpointUri) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(clientEndpoint, new AzureCliCredential(), options); var agent = aiProjectClient.AsAIAgent(); AgentSession session = await agent.CreateSessionAsync(); @@ -66,3 +79,31 @@ Type a message or 'quit' to exit } Console.WriteLine("Goodbye!"); + +/// +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj index d2651ef7a7..814b5dba2d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj @@ -12,6 +12,8 @@ + + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index 4b895e4bc0..f429221c15 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -13,7 +13,6 @@ using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -230,8 +229,7 @@ private static ChatClientAgent CreateResponsesChatClientAgent( { Throw.IfNull(aiProjectClient); Throw.IfNull(agentOptions); - Throw.IfNull(agentOptions.ChatOptions); - Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); + agentOptions.ChatOptions ??= new(); IChatClient chatClient = aiProjectClient .GetProjectOpenAIClient() From fcc96823a48521f8026a5d9735f172c080c05097 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:45:52 +0100 Subject: [PATCH 21/75] Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting --- .../Hosted-ChatClientAgent/Program.cs | 6 +- .../Properties/launchSettings.json | 1 - .../Properties/launchSettings.json | 3 +- .../UsingHostedAgent/Program.cs | 25 ++++---- .../AzureAIProjectChatClientExtensions.cs | 3 +- .../Hosting/AgentFrameworkResponseHandler.cs | 59 ++++++++++++++++++- .../Hosting/AgentSessionStore.cs | 46 +++++++++++++++ .../Hosting/InMemoryAgentSessionStore.cs | 53 +++++++++++++++++ .../Hosting/ServiceCollectionExtensions.cs | 18 +++++- 9 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 88a2b1d610..448f0f16af 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -11,6 +11,10 @@ var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; // Create the agent via the AI project client using the Responses API. @@ -23,7 +27,7 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. providing explanations, brainstorming ideas, and offering guidance. Be concise, clear, and helpful in your responses. """, - name: "simple-agent", + name: agentName, description: "A simple general-purpose AI assistant"); // Host the agent as a Foundry Hosted Agent using the Responses API. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json index b7f2c658ac..cc21f3dd2e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json @@ -2,7 +2,6 @@ "profiles": { "Hosted-ChatClientAgent": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json index a7047d02a1..b4c4e005d3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json @@ -1,8 +1,7 @@ { "profiles": { - "simple-agent-foundry": { + "HostedFoundryAgent": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index 27e0a404d7..af5b4be275 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -1,33 +1,38 @@ // Copyright (c) Microsoft. All rights reserved. using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); // ── Create an agent-framework agent backed by the remote agent endpoint ────── -var endpointUri = new Uri(agentEndpoint); var options = new AIProjectClientOptions(); -// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy -// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right -// before the request hits the wire. -Uri clientEndpoint = endpointUri; -if (endpointUri.Scheme == "http") +if (agentEndpoint.Scheme == "http") { - clientEndpoint = new UriBuilder(endpointUri) { Scheme = "https" }.Uri; + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); } -var aiProjectClient = new AIProjectClient(clientEndpoint, new AzureCliCredential(), options); -var agent = aiProjectClient.AsAIAgent(); +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); AgentSession session = await agent.CreateSessionAsync(); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index f429221c15..1f0f0a6e5f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -229,7 +229,8 @@ private static ChatClientAgent CreateResponsesChatClientAgent( { Throw.IfNull(aiProjectClient); Throw.IfNull(agentOptions); - agentOptions.ChatOptions ??= new(); + Throw.IfNull(agentOptions.ChatOptions); + Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); IChatClient chatClient = aiProjectClient .GetProjectOpenAIClient() diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 40ad435382..87bfd1fc75 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -47,8 +47,20 @@ public override async IAsyncEnumerable CreateAsync( { // 1. Resolve agent var agent = this.ResolveAgent(request); + var sessionStore = this.ResolveSessionStore(request); - // 2. Create the SDK event stream builder + // 2. Load or create a new session from the interaction + var sessionConversationId = request.GetConversationId() ?? Guid.NewGuid().ToString(); + + var chatClientAgent = agent.GetService(); + + AgentSession? session = !string.IsNullOrEmpty(sessionConversationId) + ? await sessionStore.GetSessionAsync(agent, sessionConversationId, cancellationToken).ConfigureAwait(false) + : chatClientAgent is not null + ? await chatClientAgent.CreateSessionAsync(sessionConversationId, cancellationToken).ConfigureAwait(false) + : await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false); + + // 3. Create the SDK event stream builder var stream = new ResponseEventStream(context, request); // 3. Emit lifecycle events @@ -87,7 +99,7 @@ public override async IAsyncEnumerable CreateAsync( // and inside catch blocks. We use a flag to defer the yield to outside the try/catch. bool emittedTerminal = false; var enumerator = OutputConverter.ConvertUpdatesToEventsAsync( - agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken), + agent.RunStreamingAsync(messages, session, options: options, cancellationToken: cancellationToken), stream, cancellationToken).GetAsyncEnumerator(cancellationToken); try @@ -151,6 +163,12 @@ public override async IAsyncEnumerable CreateAsync( finally { await enumerator.DisposeAsync().ConfigureAwait(false); + + // Persist session after streaming completes (successful or not) + if (session is not null && !string.IsNullOrEmpty(sessionConversationId)) + { + await sessionStore.SaveSessionAsync(agent, sessionConversationId, session, CancellationToken.None).ConfigureAwait(false); + } } } @@ -191,6 +209,43 @@ private AIAgent ResolveAgent(CreateResponse request) throw new InvalidOperationException(errorMessage); } + /// + /// Resolves an from the request. + /// Tries agent.name first, then falls back to metadata["entity_id"]. + /// If neither is present, attempts to resolve a default (non-keyed) . + /// + private AgentSessionStore ResolveSessionStore(CreateResponse request) + { + var agentName = GetAgentName(request); + + if (!string.IsNullOrEmpty(agentName)) + { + var sessionStore = this._serviceProvider.GetKeyedService(agentName); + if (sessionStore is not null) + { + return sessionStore; + } + + if (this._logger.IsEnabled(LogLevel.Warning)) + { + this._logger.LogWarning("SessionStore for agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } + } + + // Try non-keyed default + var defaultSessionStore = this._serviceProvider.GetService(); + if (defaultSessionStore is not null) + { + return defaultSessionStore; + } + + var errorMessage = string.IsNullOrEmpty(agentName) + ? "No agent name specified in the request (via agent.name or metadata[\"entity_id\"]) and no default AgentSessionStore is registered." + : $"Agent '{agentName}' not found. Ensure it is registered via AddAIAgent(\"{agentName}\", ...) or as a default AgentSessionStore."; + + throw new InvalidOperationException(errorMessage); + } + private static string? GetAgentName(CreateResponse request) { // Try agent.name from AgentReference diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs new file mode 100644 index 0000000000..6aef3269b4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting; + +/// +/// Defines the contract for storing and retrieving agent conversation sessions. +/// +/// +/// Implementations of this interface enable persistent storage of conversation sessions, +/// allowing conversations to be resumed across HTTP requests, application restarts, +/// or different service instances in hosted scenarios. +/// +public abstract class AgentSessionStore +{ + /// + /// Saves a serialized agent session to persistent storage. + /// + /// The agent that owns this session. + /// The unique identifier for the conversation/session. + /// The session to save. + /// The to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + public abstract ValueTask SaveSessionAsync( + AIAgent agent, + string conversationId, + AgentSession session, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a serialized agent session from persistent storage. + /// + /// The agent that owns this session. + /// The unique identifier for the conversation/session to retrieve. + /// The to monitor for cancellation requests. + /// + /// A task that represents the asynchronous retrieval operation. + /// The task result contains the session, or a new session if not found. + /// + public abstract ValueTask GetSessionAsync( + AIAgent agent, + string conversationId, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs new file mode 100644 index 0000000000..4ae94ed4fe --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting; + +/// +/// Provides an in-memory implementation of for development and testing scenarios. +/// +/// +/// +/// This implementation stores sessions in memory using a concurrent dictionary and is suitable for: +/// +/// Single-instance development scenarios +/// Testing and prototyping +/// Scenarios where session persistence across restarts is not required +/// +/// +/// +/// Warning: All stored sessions will be lost when the application restarts. +/// For production use with multiple instances or persistence across restarts, use a durable storage implementation +/// such as Redis, SQL Server, or Azure Cosmos DB. +/// +/// +public sealed class InMemoryAgentSessionStore : AgentSessionStore +{ + private readonly ConcurrentDictionary _sessions = new(); + + /// + public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + { + var key = GetKey(conversationId, agent.Id); + this._sessions[key] = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public override async ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + { + var key = GetKey(conversationId, agent.Id); + JsonElement? sessionContent = this._sessions.TryGetValue(key, out var existingSession) ? existingSession : null; + + return sessionContent switch + { + null => await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false), + _ => await agent.DeserializeSessionAsync(sessionContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false), + }; + } + + private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}"; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index 7f01e5904c..bf3b3421f6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser { ArgumentNullException.ThrowIfNull(services); services.AddResponsesServer(); + services.TryAddSingleton(); services.TryAddSingleton(); return services; } @@ -71,14 +72,27 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser /// /// The service collection. /// The agent instance to register. + /// The agent session store to use for managing agent sessions server-side. If null, an in-memory session store will be used. /// The service collection for chaining. - public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent, AgentSessionStore? agentSessionStore = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); services.AddResponsesServer(); - services.TryAddSingleton(agent); + agentSessionStore ??= new InMemoryAgentSessionStore(); + + if (!string.IsNullOrWhiteSpace(agent.Name)) + { + services.TryAddKeyedSingleton(agent.Name, agent); + services.TryAddKeyedSingleton(agent.Name, agentSessionStore); + } + else + { + services.TryAddSingleton(agent); + services.TryAddSingleton(agentSessionStore); + } + services.TryAddSingleton(); return services; } From 4895ad74e13bf22fdb1ed4c36090bb81787ff0a2 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:21:17 +0100 Subject: [PATCH 22/75] Using updates --- .../HostedAgentsV2/UsingHostedAgent/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index af5b4be275..5d9c003dfa 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -86,6 +86,7 @@ Type a message or 'quit' to exit Console.WriteLine("Goodbye!"); /// +/// For Local Development Only /// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient /// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. /// From 9e842e18214c4d5dfa03bfdfbac2e12316f9bb8f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:11:26 +0100 Subject: [PATCH 23/75] Update chat client agent for contributor and devs --- .gitignore | 4 + dotnet/.gitignore | 9 +- dotnet/agent-framework-dotnet.slnx | 4 +- .../Hosted-ChatClientAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedChatClientAgent.csproj | 9 ++ .../Hosted-ChatClientAgent/Program.cs | 49 +++++++- .../Hosted-ChatClientAgent/README.md | 109 ++++++++++++++++++ .../agent.manifest.yaml | 28 +++++ .../Hosted-ChatClientAgent/agent.yaml | 29 ++--- .../Hosted-FoundryAgent/agent.manifest.yaml | 28 +++++ .../Hosted-FoundryAgent/agent.yaml | 29 ++--- .../SimpleAgent}/Program.cs | 0 .../SimpleAgent}/SimpleAgent.csproj | 2 +- .../Hosting/AgentSessionStore.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 10 +- 16 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/SimpleAgent.csproj (85%) diff --git a/.gitignore b/.gitignore index 089abb5395..28d753c87b 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,10 @@ celerybeat.pid .venv env/ venv/ + +# Foundry agent CLI (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log ENV/ env.bak/ venv.bak/ diff --git a/dotnet/.gitignore b/dotnet/.gitignore index ce1409abe9..572680831e 100644 --- a/dotnet/.gitignore +++ b/dotnet/.gitignore @@ -402,4 +402,11 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml + +# Foundry agent CLI config (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log + +# Pre-published output for Docker builds +out/ \ No newline at end of file diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 969a5c16d8..5ffb5692fd 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -280,8 +280,8 @@ - - + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor new file mode 100644 index 0000000000..200f674bdd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor @@ -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-chat-client-agent . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-chat-client-agent --env-file .env hosted-chat-client-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", "HostedChatClientAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj index 096681d505..b1fe8d3e3c 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj @@ -14,8 +14,17 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 448f0f16af..b4d76bbc52 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.Projects; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; @@ -17,8 +18,14 @@ var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; +// 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()); + // Create the agent via the AI project client using the Responses API. -AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()) +AIAgent agent = new AIProjectClient(projectEndpoint, credential) .AsAIAgent( model: deployment, instructions: """ @@ -44,3 +51,43 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. } app.Run(); + +/// +/// A for local Docker debugging only. +/// +/// When debugging and testing a hosted agent in a local Docker container, Azure CLI +/// and other interactive credentials are not available. This credential reads a +/// pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// In production, the Foundry platform injects a managed identity automatically. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return GetAccessToken(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetAccessToken()); + } + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md new file mode 100644 index 0000000000..0c5ce36cfe --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md @@ -0,0 +1,109 @@ +# Hosted-ChatClientAgent + +A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent +dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": "hosted-chat-client-agent"}' +``` + +## 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 +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-chat-client-agent . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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-chat-client-agent \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-chat-client-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. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": "hosted-chat-client-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` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedChatClientAgent.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml new file mode 100644 index 0000000000..58a07d8bb3 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: hosted-chat-client-agent +displayName: "Hosted Chat Client Agent" + +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent + using the Agent Framework instance hosting pattern. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + - Agent Framework + +template: + name: hosted-chat-client-agent + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml index dfab24e712..0a97abc35a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-agent -description: > - A simple general-purpose AI assistant hosted as a Foundry Hosted Agent. -metadata: - tags: - - AI Agent Hosting - - Simple Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: AGENT_NAME - value: ${AGENT_NAME} +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-chat-client-agent +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml new file mode 100644 index 0000000000..9b33646c8a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml index a0fe89f966..74223e72fe 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-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 - - Simple Agent - - Foundry Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AGENT_NAME - value: ${AGENT_NAME} +# 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: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj similarity index 85% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj index 814b5dba2d..05e150880e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj @@ -18,7 +18,7 @@ - + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs index 6aef3269b4..c61584e9e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading; using System.Threading.Tasks; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index bf3b3421f6..fe3b07c023 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -87,11 +87,11 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser services.TryAddKeyedSingleton(agent.Name, agent); services.TryAddKeyedSingleton(agent.Name, agentSessionStore); } - else - { - services.TryAddSingleton(agent); - services.TryAddSingleton(agentSessionStore); - } + + // Also register as the default (non-keyed) agent so requests + // without an agent name can resolve it (e.g., local dev tooling). + services.TryAddSingleton(agent); + services.TryAddSingleton(agentSessionStore); services.TryAddSingleton(); return services; From 340ec82623ffbe047e063c768aab933f19d17586 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:29:36 +0100 Subject: [PATCH 24/75] Foundry Agent Hosting --- .../Hosted-ChatClientAgent/.env.local | 1 + .../Hosted-ChatClientAgent/Program.cs | 2 +- .../Hosted-FoundryAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedFoundryAgent.csproj | 9 ++ .../Hosted-FoundryAgent/Program.cs | 49 ++++++- .../Hosted-FoundryAgent/README.md | 121 ++++++++++++++++++ 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local index 6d7831229d..cbf693b3a9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index b4d76bbc52..21cf34852a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -19,7 +19,7 @@ var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; // 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). +// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry). TokenCredential credential = new ChainedTokenCredential( new DevTemporaryTokenCredential(), new DefaultAzureCredential()); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local new file mode 100644 index 0000000000..1fefe43ebd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor new file mode 100644 index 0000000000..2b6a2dbbc4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj index 2fc0ec43e4..e49a15769f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -14,8 +14,17 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index c946d60058..a8f2f249c8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -2,6 +2,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI.Foundry; @@ -15,7 +16,13 @@ var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? throw new InvalidOperationException("AGENT_NAME is not set."); -var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); +// 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()); + +var aiProjectClient = new AIProjectClient(projectEndpoint, credential); // Retrieve the Foundry-managed agent by name (latest version). ProjectsAgentRecord agentRecord = await aiProjectClient @@ -37,3 +44,43 @@ } app.Run(); + +/// +/// A for local Docker debugging only. +/// +/// When debugging and testing a hosted agent in a local Docker container, Azure CLI +/// and other interactive credentials are not available. This credential reads a +/// pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// In production, the Foundry platform injects a managed identity automatically. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return GetAccessToken(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetAccessToken()); + } + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md new file mode 100644 index 0000000000..b95e7ff808 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md @@ -0,0 +1,121 @@ +# 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. + +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. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a **registered agent** (created via Foundry UI, CLI, or API) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +``` + +> **Note:** `.env` is gitignored. The `.env.local` 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. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent +AGENT_NAME= dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## 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 +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-foundry-agent . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## 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` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative. + +## How it differs from Hosted-ChatClientAgent + +| | 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 | From ad625602a3b0afa65d7c0ce0fb3fb022426be05d Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:20:18 +0100 Subject: [PATCH 25/75] Address text rag sample working --- dotnet/agent-framework-dotnet.slnx | 3 + .../Hosted-ChatClientAgent/Program.cs | 13 +- .../Hosted-FoundryAgent/Program.cs | 13 +- .../HostedAgentsV2/Hosted-TextRag/.env.local | 5 + .../HostedAgentsV2/Hosted-TextRag/Dockerfile | 17 +++ .../Hosted-TextRag/Dockerfile.contributor | 19 +++ .../Hosted-TextRag/HostedTextRag.csproj | 32 +++++ .../HostedAgentsV2/Hosted-TextRag/Program.cs | 130 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-TextRag/README.md | 116 ++++++++++++++++ .../Hosted-TextRag/agent.manifest.yaml | 30 ++++ .../HostedAgentsV2/Hosted-TextRag/agent.yaml | 9 ++ .../AgentThreadAndHITL.csproj | 24 ++++ .../AgentThreadAndHITL/Program.cs | 115 ++++++++++++++++ .../AgentWithLocalTools.csproj | 24 ++++ .../AgentWithLocalTools/Program.cs | 115 ++++++++++++++++ .../AgentWithTextSearchRag.csproj | 24 ++++ .../AgentWithTextSearchRag/Program.cs | 115 ++++++++++++++++ .../AgentsInWorkflows.csproj | 24 ++++ .../AgentsInWorkflows/Program.cs | 115 ++++++++++++++++ 20 files changed, 946 insertions(+), 8 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 5ffb5692fd..8de8933bea 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -280,6 +280,9 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 21cf34852a..e7dcf415f7 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -69,6 +69,12 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. internal sealed class DevTemporaryTokenCredential : TokenCredential { private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; + private readonly string? _token; + + public DevTemporaryTokenCredential() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { @@ -80,14 +86,13 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(GetAccessToken()); } - private static AccessToken GetAccessToken() + private AccessToken GetAccessToken() { - var token = Environment.GetEnvironmentVariable(EnvironmentVariable); - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(_token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index a8f2f249c8..7f509084c0 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -62,6 +62,12 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential { private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; + private readonly string? _token; + + public DevTemporaryTokenCredential() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { @@ -73,14 +79,13 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(GetAccessToken()); } - private static AccessToken GetAccessToken() + private AccessToken GetAccessToken() { - var token = Environment.GetEnvironmentVariable(EnvironmentVariable); - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(_token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local new file mode 100644 index 0000000000..cbf693b3a9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local @@ -0,0 +1,5 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile new file mode 100644 index 0000000000..062d0f4f7e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile @@ -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", "HostedTextRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor new file mode 100644 index 0000000000..9a90c74335 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj new file mode 100644 index 0000000000..9a22108c7b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + false + HostedTextRag + HostedTextRag + $(NoWarn); + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs new file mode 100644 index 0000000000..45d027f740 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. + +// 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. + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +TextSearchProviderOptions textSearchOptions = new() +{ + SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, + RecentMessageMemoryLimit = 6, +}; + +AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) + .AsAIAgent(new ChatClientAgentOptions + { + Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-text-rag", + ChatOptions = new ChatOptions + { + ModelId = deploymentName, + Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", + }, + AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)] + }); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +app.Run(); + +// ── Mock search function ───────────────────────────────────────────────────── +// In production, replace this with a real search provider (e.g., Azure AI Search). + +static Task> MockSearchAsync(string query, CancellationToken cancellationToken) +{ + List results = []; + + if (query.Contains("return", StringComparison.OrdinalIgnoreCase) || query.Contains("refund", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "Contoso Outdoors Return Policy", + SourceLink = "https://contoso.com/policies/returns", + Text = "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." + }); + } + + if (query.Contains("shipping", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "Contoso Outdoors Shipping Guide", + SourceLink = "https://contoso.com/help/shipping", + Text = "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." + }); + } + + if (query.Contains("tent", StringComparison.OrdinalIgnoreCase) || query.Contains("fabric", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "TrailRunner Tent Care Instructions", + SourceLink = "https://contoso.com/manuals/trailrunner-tent", + Text = "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." + }); + } + + return Task.FromResult>(results); +} + +/// +/// A for local Docker debugging only. +/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json new file mode 100644 index 0000000000..932d4e67fc --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedTextRag": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md new file mode 100644 index 0000000000..75c9dba797 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md @@ -0,0 +1,116 @@ +# 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. + +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. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag +AGENT_NAME=hosted-text-rag dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```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?" +``` + +Or with curl: + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "What is your return policy?", "model": "hosted-text-rag"}' +``` + +## Running with Docker + +Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```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-text-rag . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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 +``` + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What is your return policy?" +``` + +## How RAG works in this sample + +The `TextSearchProvider` runs a mock search **before each model invocation**: + +| 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 | + +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. + +## 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 `HostedTextRag.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml new file mode 100644 index 0000000000..1459925136 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml @@ -0,0 +1,30 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml new file mode 100644 index 0000000000..c8d6928e2e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml @@ -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: hosted-text-rag +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj new file mode 100644 index 0000000000..69905ed43b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentThreadAndHITLClient + agent-thread-hitl-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs new file mode 100644 index 0000000000..14ddb29fe8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + HITL Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj new file mode 100644 index 0000000000..0742994449 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentWithLocalToolsClient + agent-with-local-tools-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs new file mode 100644 index 0000000000..0caa06c36b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Hotel Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj new file mode 100644 index 0000000000..028977d0aa --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentWithTextSearchRagClient + agent-with-rag-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs new file mode 100644 index 0000000000..efc6c1d982 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + RAG Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj new file mode 100644 index 0000000000..27e2da3908 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentsInWorkflowsClient + agents-in-workflows-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs new file mode 100644 index 0000000000..33e4765fb9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Translation Workflow Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} From d02cee5353c1b02f0bc118e5b99654c968647017 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:30:13 +0100 Subject: [PATCH 26/75] Version bump --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 27e2bbea9e..2d62fe0b46 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260402.1 $(VersionPrefix) - 0.9.0-hosted.260409.1 + 0.9.0-hosted.260413.1 1.0.0 Debug;Release;Publish From 4b9d848541ec462a0a8cb4b14ff4eeb696e79aed Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:59:18 +0100 Subject: [PATCH 27/75] Adding LocalTools + Workflow samples --- dotnet/agent-framework-dotnet.slnx | 6 + .../Hosted-LocalTools/.env.local | 4 + .../Hosted-LocalTools/Dockerfile | 17 ++ .../Hosted-LocalTools/Dockerfile.contributor | 19 ++ .../Hosted-LocalTools/HostedLocalTools.csproj | 30 ++++ .../Hosted-LocalTools/Program.cs | 164 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../Hosted-LocalTools/README.md | 113 ++++++++++++ .../Hosted-LocalTools/agent.manifest.yaml | 29 ++++ .../Hosted-LocalTools/agent.yaml | 9 + .../Hosted-Workflows/.env.local | 4 + .../Hosted-Workflows/Dockerfile | 17 ++ .../Hosted-Workflows/Dockerfile.contributor | 18 ++ .../Hosted-Workflows/HostedWorkflows.csproj | 34 ++++ .../Hosted-Workflows/Program.cs | 97 +++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-Workflows/README.md | 109 ++++++++++++ .../Hosted-Workflows/agent.manifest.yaml | 29 ++++ .../Hosted-Workflows/agent.yaml | 9 + 19 files changed, 730 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8de8933bea..c065430b54 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -283,6 +283,12 @@ + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile new file mode 100644 index 0000000000..1b72fcd93f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile @@ -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", "HostedLocalTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor new file mode 100644 index 0000000000..65f920824a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj new file mode 100644 index 0000000000..b0d39d8cee --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + HostedLocalTools + HostedLocalTools + $(NoWarn); + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs new file mode 100644 index 0000000000..f1b2f7e3bd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs @@ -0,0 +1,164 @@ +// 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. + +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; +using Microsoft.Extensions.AI; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// ── Hotel data ─────────────────────────────────────────────────────────────── + +Hotel[] seattleHotels = +[ + new("Contoso Suites", 189, 4.5, "Downtown"), + new("Fabrikam Residences", 159, 4.2, "Pike Place Market"), + new("Alpine Ski House", 249, 4.7, "Seattle Center"), + new("Margie's Travel Lodge", 219, 4.4, "Waterfront"), + new("Northwind Inn", 139, 4.0, "Capitol Hill"), + new("Relecloud Hotel", 99, 3.8, "University District"), +]; + +// ── Tool: GetAvailableHotels ───────────────────────────────────────────────── + +[Description("Get available hotels in Seattle for the specified dates.")] +string GetAvailableHotels( + [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, + [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, + [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) +{ + if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn)) + { + return "Error parsing check-in date. Please use YYYY-MM-DD format."; + } + + if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut)) + { + return "Error parsing check-out date. Please use YYYY-MM-DD format."; + } + + if (checkOut <= checkIn) + { + return "Error: Check-out date must be after check-in date."; + } + + int nights = (checkOut - checkIn).Days; + List availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); + + if (availableHotels.Count == 0) + { + return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; + } + + StringBuilder result = new(); + result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); + result.AppendLine(); + + foreach (Hotel hotel in availableHotels) + { + int totalCost = hotel.PricePerNight * nights; + result.AppendLine($"**{hotel.Name}**"); + result.AppendLine($" Location: {hotel.Location}"); + result.AppendLine($" Rating: {hotel.Rating}/5"); + result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); + result.AppendLine(); + } + + return result.ToString(); +} + +// ── Create and host the agent ──────────────────────────────────────────────── + +AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) + .AsAIAgent( + model: deploymentName, + instructions: """ + You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. + + When a user asks about hotels in Seattle: + 1. Ask for their check-in and check-out dates if not provided + 2. Ask about their budget preferences if not mentioned + 3. Use the GetAvailableHotels tool to find available options + 4. Present the results in a friendly, informative way + 5. Offer to help with additional questions about the hotels or Seattle + + Be conversational and helpful. If users ask about things outside of Seattle hotels, + politely let them know you specialize in Seattle hotel recommendations. + """, + name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools", + description: "Seattle hotel search agent with local function tools", + tools: [AIFunctionFactory.Create(GetAvailableHotels)]); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +app.Run(); + +// ── Types ──────────────────────────────────────────────────────────────────── + +internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); + +/// +/// 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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json new file mode 100644 index 0000000000..ae1bb80b7d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedLocalTools": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md new file mode 100644 index 0000000000..3c41803b95 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md @@ -0,0 +1,113 @@ +# 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. + +The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` tool that searches a mock hotel database by dates and budget. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools +AGENT_NAME=hosted-local-tools dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```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"}' +``` + +## Running with Docker + +Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```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-local-tools . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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-local-tools \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-local-tools +``` + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What hotels are available in Seattle for next weekend?" +``` + +## How local tools work + +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: + +| 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) | + +The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing. + +## 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 `HostedLocalTools.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml new file mode 100644 index 0000000000..a056b51649 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml @@ -0,0 +1,29 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml new file mode 100644 index 0000000000..18ecc4a9f7 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml @@ -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: hosted-local-tools +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile new file mode 100644 index 0000000000..e770ec172b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile @@ -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", "HostedWorkflows.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor new file mode 100644 index 0000000000..b8dae44c2b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor @@ -0,0 +1,18 @@ +# 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-workflows . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-workflows -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflows +# +# 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", "HostedWorkflows.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj new file mode 100644 index 0000000000..2f210a18d8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + HostedWorkflows + HostedWorkflows + $(NoWarn); + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs new file mode 100644 index 0000000000..6288e3cf5f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +// 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. + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// Create a chat client from the Foundry project +IChatClient chatClient = new AIProjectClient(new Uri(endpoint), credential) + .GetProjectOpenAIClient() + .GetChatClient(deploymentName) + .AsIChatClient(); + +// Create translation agents +AIAgent frenchAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to French."); +AIAgent spanishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to Spanish."); +AIAgent englishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to English."); + +// Build the sequential workflow: French → Spanish → English +AIAgent agent = new WorkflowBuilder(frenchAgent) + .AddEdge(frenchAgent, spanishAgent) + .AddEdge(spanishAgent, englishAgent) + .Build() + .AsAIAgent( + name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-workflows"); + +// Host the workflow agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json new file mode 100644 index 0000000000..0e2908985a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedWorkflows": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md new file mode 100644 index 0000000000..0bb000aaa1 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md @@ -0,0 +1,109 @@ +# Hosted-Workflows + +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`. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows +AGENT_NAME=hosted-workflows dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "The quick brown fox jumps over the lazy dog" +``` + +Or with curl: + +```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-workflows"}' +``` + +The text will be translated through the chain: English → French → Spanish → English. + +## 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-workflows . +``` + +### 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 AGENT_NAME=hosted-workflows \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-workflows +``` + +### 4. Test it + +```bash +azd ai agent invoke --local "Hello, how are you today?" +``` + +## How the workflow works + +``` +Input text + │ + ▼ +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ French Agent │ → │ Spanish Agent │ → │ English Agent │ +│ (translate) │ │ (translate) │ │ (translate) │ +└─────────────┘ └──────────────┘ └──────────────┘ + │ + ▼ + Final output + (back in English) +``` + +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. + +## NuGet package users + +Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflows.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml new file mode 100644 index 0000000000..e902b6232f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml @@ -0,0 +1,29 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml new file mode 100644 index 0000000000..ab138939b4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml @@ -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: hosted-workflows +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi From 1584879edd63814cfe95d836ac37727cd1e72893 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:50:11 +0100 Subject: [PATCH 28/75] Removing extra using samples --- .../AgentThreadAndHITL.csproj | 24 ---- .../AgentThreadAndHITL/Program.cs | 115 ------------------ .../AgentWithLocalTools.csproj | 24 ---- .../AgentWithLocalTools/Program.cs | 115 ------------------ .../AgentWithTextSearchRag.csproj | 24 ---- .../AgentWithTextSearchRag/Program.cs | 115 ------------------ .../AgentsInWorkflows.csproj | 24 ---- .../AgentsInWorkflows/Program.cs | 115 ------------------ 8 files changed, 556 deletions(-) delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj deleted file mode 100644 index 69905ed43b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentThreadAndHITLClient - agent-thread-hitl-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs deleted file mode 100644 index 14ddb29fe8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - HITL Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj deleted file mode 100644 index 0742994449..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentWithLocalToolsClient - agent-with-local-tools-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs deleted file mode 100644 index 0caa06c36b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - Hotel Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj deleted file mode 100644 index 028977d0aa..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentWithTextSearchRagClient - agent-with-rag-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs deleted file mode 100644 index efc6c1d982..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - RAG Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj deleted file mode 100644 index 27e2da3908..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentsInWorkflowsClient - agents-in-workflows-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs deleted file mode 100644 index 33e4765fb9..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - Translation Workflow Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} From 5b16684930f6ac484b8b1e7741a1ae2f91247b48 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:11:05 +0100 Subject: [PATCH 29/75] Add Hosted-McpTools sample with dual MCP pattern Demonstrates two MCP integration layers in a single hosted agent: - Client-side MCP: McpClient connects to Microsoft Learn, agent handles tool invocations locally (docs_search, code_sample_search, docs_fetch) - Server-side MCP: HostedMcpServerTool delegates tool discovery and invocation to the LLM provider (Responses API), no local connection Includes DevTemporaryTokenCredential for Docker local debugging, Dockerfile.contributor for ProjectReference builds, and the openai/v1 route mapping for AIProjectClient compatibility in Development mode. --- dotnet/agent-framework-dotnet.slnx | 3 + .../HostedAgentsV2/Hosted-McpTools/.env.local | 4 + .../HostedAgentsV2/Hosted-McpTools/Dockerfile | 17 +++ .../Hosted-McpTools/Dockerfile.contributor | 18 +++ .../Hosted-McpTools/HostedMcpTools.csproj | 31 +++++ .../HostedAgentsV2/Hosted-McpTools/Program.cs | 130 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-McpTools/README.md | 86 ++++++++++++ .../Hosted-McpTools/agent.manifest.yaml | 30 ++++ .../HostedAgentsV2/Hosted-McpTools/agent.yaml | 9 ++ 10 files changed, 339 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index c065430b54..12554aa8d7 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -289,6 +289,9 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile new file mode 100644 index 0000000000..fe7fceb685 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile @@ -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", "HostedMcpTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor new file mode 100644 index 0000000000..51c8c347d8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor @@ -0,0 +1,18 @@ +# 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/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj new file mode 100644 index 0000000000..9ce19dd540 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + HostedMcpTools + HostedMcpTools + $(NoWarn); + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs new file mode 100644 index 0000000000..a969b75477 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a hosted agent with two layers of MCP (Model Context Protocol) tools: +// +// 1. CLIENT-SIDE MCP: The agent connects to the Microsoft Learn MCP server directly via +// 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. +// +// 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. + +#pragma warning disable MEAI001 // HostedMcpServerTool is experimental + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// ── Client-side MCP: Microsoft Learn (local resolution) ────────────────────── +// Connect directly to the MCP server. The agent discovers and invokes tools locally. +Console.WriteLine("Connecting to Microsoft Learn MCP server (client-side)..."); + +await using var learnMcp = await McpClient.CreateAsync(new HttpClientTransport(new() +{ + Endpoint = new Uri("https://learn.microsoft.com/api/mcp"), + Name = "Microsoft Learn (client)", +})); + +var clientTools = await learnMcp.ListToolsAsync(); +Console.WriteLine($"Client-side MCP tools: {string.Join(", ", clientTools.Select(t => t.Name))}"); + +// ── Server-side MCP: Microsoft Learn (provider resolution) ─────────────────── +// Declare a HostedMcpServerTool — the LLM provider (Responses API) handles tool +// invocations directly. No local MCP connection needed for this pattern. +AITool serverTool = new HostedMcpServerTool( + serverName: "microsoft_learn_hosted", + serverAddress: "https://learn.microsoft.com/api/mcp") +{ + AllowedTools = ["microsoft_docs_search"], + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire +}; +Console.WriteLine("Server-side MCP tool: microsoft_docs_search (via HostedMcpServerTool)"); + +// ── Combine both tool types into a single agent ────────────────────────────── +// The agent has access to tools from both MCP patterns simultaneously. +List allTools = [.. clientTools.Cast(), serverTool]; + +AIAgent agent = new AIProjectClient(projectEndpoint, credential) + .AsAIAgent( + model: deployment, + instructions: """ + You are a helpful developer assistant with access to Microsoft Learn documentation. + Use the available tools to search and retrieve documentation. + Be concise and provide direct answers with relevant links. + """, + name: "mcp-tools", + 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. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json new file mode 100644 index 0000000000..3042eb4d44 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedMcpTools": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md new file mode 100644 index 0000000000..0990dfc6bd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md @@ -0,0 +1,86 @@ +# Hosted-McpTools + +A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**: + +1. **Client-side MCP (GitHub)** — The agent connects directly to the GitHub 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 (e.g., GitHub PAT) | Provider manages credentials | +| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup | +| **Example** | GitHub (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) | + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) +- A **GitHub Personal Access Token** (create at https://github.com/settings/tokens) + +## Configuration + +Copy the template and fill in your values: + +```bash +cp .env.local .env +``` + +Edit `.env`: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +GITHUB_PAT=ghp_your_token_here +``` + +## Running directly (contributors) + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools +dotnet run +``` + +### Test it + +Using the Azure Developer CLI: + +```bash +# Uses GitHub MCP (client-side) +azd ai agent invoke --local "Search for the agent-framework repository on GitHub" + +# Uses Microsoft Learn MCP (server-side) +azd ai agent invoke --local "How do I create an Azure storage account using az cli?" +``` + +## 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 and run + +```bash +docker build -f Dockerfile.contributor -t hosted-mcp-tools . + +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=mcp-tools \ + -e GITHUB_PAT=$GITHUB_PAT \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-mcp-tools +``` + +## NuGet package users + +Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml new file mode 100644 index 0000000000..d5952940b0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml @@ -0,0 +1,30 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml new file mode 100644 index 0000000000..34beb3e2c9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml @@ -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: mcp-tools +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi From 548dfb10b69ae3938e0c1944f09051dfa3b134a2 Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Wed, 15 Apr 2026 16:34:28 -0700 Subject: [PATCH 30/75] =?UTF-8?q?.NET:=20Bump=20Azure.AI.AgentServer=20pac?= =?UTF-8?q?kages=20to=201.0.0-beta.1/beta.21=20and=20fix=20br=E2=80=A6=20(?= =?UTF-8?q?#5287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix breaking API changes - Azure.AI.AgentServer.Core: 1.0.0-beta.11 -> 1.0.0-beta.21 - Azure.AI.AgentServer.Invocations: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1 - Azure.AI.AgentServer.Responses: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1 - Azure.Identity: 1.20.0 -> 1.21.0 (transitive requirement) - Azure.Core: 1.52.0 -> 1.53.0 (transitive requirement) - Remove azure-sdk-for-net dev feed (packages now on nuget.org) - Fix OutputConverter for new builder API (auto-tracked children, split EmitTextDone/EmitDone) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixing small issues. --------- Co-authored-by: alliscode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 10 +++++----- dotnet/nuget.config | 4 ---- .../samples/04-hosting/FoundryResponsesRepl/Program.cs | 10 +++++++++- .../Hosting/OutputConverter.cs | 5 ++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 36e68c958a..5beaaab40d 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,14 +19,14 @@ - - - + + + - - + + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 202c1fc671..76d943ce16 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,12 +3,8 @@ - - - - diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs index be0e6ccf7b..8b877dc519 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + // Foundry Responses Client REPL // // Connects to a Foundry Responses agent running on a given endpoint @@ -66,10 +68,16 @@ Console.ResetColor(); string? input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) continue; + if (string.IsNullOrWhiteSpace(input)) + { + continue; + } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { break; + } try { diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index 79aaf768d9..58ba989ebf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -161,7 +161,6 @@ public static async IAsyncEnumerable ConvertUpdatesToEvents yield return summaryPart.EmitTextDelta(text); yield return summaryPart.EmitTextDone(text); yield return summaryPart.EmitDone(); - reasoningBuilder.EmitSummaryPartDone(summaryPart); yield return reasoningBuilder.EmitDone(); break; @@ -236,8 +235,8 @@ private static IEnumerable CloseCurrentMessage( if (textBuilder is not null) { var finalText = accumulatedText?.ToString() ?? string.Empty; - yield return textBuilder.EmitDone(finalText); - yield return messageBuilder.EmitContentDone(textBuilder); + yield return textBuilder.EmitTextDone(finalText); + yield return textBuilder.EmitDone(); } yield return messageBuilder.EmitDone(); From 12251b1e69abce25b68d1f25d2b93e425d78dea8 Mon Sep 17 00:00:00 2001 From: alliscode Date: Tue, 31 Mar 2026 14:57:00 -0700 Subject: [PATCH 31/75] Add Azure AI Foundry Responses hosting adapter Implement Microsoft.Agents.AI.Hosting.AzureAIResponses to host agent-framework AIAgents and workflows within Azure Foundry as hosted agents via the Azure.AI.AgentServer.Responses SDK. - AgentFrameworkResponseHandler: bridges ResponseHandler to AIAgent execution - InputConverter: converts Responses API inputs/history to MEAI ChatMessage - OutputConverter: converts agent response updates to SSE event stream - ServiceCollectionExtensions: DI registration helpers - 336 unit tests across net8.0/net9.0/net10.0 (112 per TFM) - ResponseStreamValidator: SSE protocol validation tool for samples - FoundryResponsesHosting sample app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 4 + dotnet/agent-framework-dotnet.slnx | 5 + dotnet/nuget.config | 4 + .../FoundryResponsesHosting.csproj | 27 + .../FoundryResponsesHosting/Pages.cs | 470 +++++++ .../FoundryResponsesHosting/Program.cs | 183 +++ .../Properties/launchSettings.json | 12 + .../ResponseStreamValidator.cs | 601 +++++++++ .../AgentFrameworkResponseHandler.cs | 186 +++ .../InputConverter.cs | 296 +++++ ....Agents.AI.Hosting.AzureAIResponses.csproj | 40 + .../OutputConverter.cs | 346 ++++++ .../ServiceCollectionExtensions.cs | 78 ++ .../AgentFrameworkResponseHandlerTests.cs | 815 +++++++++++++ .../InputConverterTests.cs | 671 ++++++++++ ....Hosting.AzureAIResponses.UnitTests.csproj | 17 + .../OutputConverterTests.cs | 1080 +++++++++++++++++ .../ServiceCollectionExtensionsTests.cs | 72 ++ .../WorkflowIntegrationTests.cs | 509 ++++++++ 19 files changed, 5416 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 0270f0e38b..1f8b894a1b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,13 @@ + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 24b596509e..3d0465763f 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -261,6 +261,9 @@ + + + @@ -491,6 +494,7 @@ + @@ -537,6 +541,7 @@ + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 76d943ce16..202c1fc671 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,8 +3,12 @@ + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj new file mode 100644 index 0000000000..6725ef8d3b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -0,0 +1,27 @@ + + + + Exe + net10.0 + enable + enable + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs new file mode 100644 index 0000000000..bff2c62e99 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. + +/// +/// Static HTML pages served by the sample application. +/// +internal static class Pages +{ + // ═══════════════════════════════════════════════════════════════════════ + // Homepage + // ═══════════════════════════════════════════════════════════════════════ + + internal const string Home = """ + + + + + Foundry Responses Hosting — Demos + + + +
+

🚀 Foundry Responses Hosting

+

+ Agent-framework agents hosted via the Azure AI Responses Server SDK.
+ Each demo registers a different agent and serves it through POST /responses. +

+ +
+ All demos share the same /responses endpoint. + The model field in the request selects which agent handles it. +
+
+ + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // Tool Demo + // ═══════════════════════════════════════════════════════════════════════ + + internal const string ToolDemo = """ + + + + + Tool Demo — Foundry Responses Hosting + + + +
+ ← Back to demos +

🔧 Tool Demo

+

Agent with local tools (time, weather) + Microsoft Learn MCP (docs search)

+
+ + + + +
+
+
+ + +
+
+
+ + + + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // Workflow Demo + // ═══════════════════════════════════════════════════════════════════════ + + internal const string WorkflowDemo = """ + + + + + Workflow Demo — Foundry Responses Hosting + + + +
+ ← Back to demos +

🔀 Workflow Demo — Agent Handoffs

+

A triage agent routes your question to a specialist (Code Expert or Creative Writer)

+
+
👤 User → 🔀 Triage → 💻 Code Expert / ✍️ Creative Writer
+
+
+ + + + +
+
+
+ + +
+
+
+ + + + +"""; + + // ═══════════════════════════════════════════════════════════════════════ + // SSE Validator Script (shared by all demo pages) + // ═══════════════════════════════════════════════════════════════════════ + + internal const string ValidationScript = """ +// SseValidator - inline SSE stream validation for Foundry Responses demos +// Captures events during streaming and validates against the API behaviour contract. +(function() { + const style = document.createElement('style'); + style.textContent = ` + .sse-val { margin: .4rem 0 .6rem; padding: .3rem .5rem; font-size: .75rem; color: #aaa; border-top: 1px dashed #e8e8e8; } + .val-ok { color: #7ab88a; } + .val-err { color: #d47272; font-weight: 500; } + .val-issues { margin: .2rem 0; } + .val-issue { color: #c06060; font-size: .72rem; padding: .1rem 0; } + .val-issue b { color: #b04040; } + .val-at { color: #ccc; font-size: .68rem; } + .val-log summary { cursor: pointer; color: #bbb; font-size: .72rem; } + .val-log-items { max-height: 120px; overflow-y: auto; font-size: .7rem; background: #fafafa; + padding: .3rem; border-radius: 3px; margin-top: .15rem; + font-family: 'Cascadia Code', 'Fira Code', monospace; } + .val-i { color: #ccc; display: inline-block; width: 1.8rem; text-align: right; margin-right: .3rem; } + .val-t { color: #8ab4d0; } + `; + document.head.appendChild(style); +})(); + +class SseValidator { + constructor() { this.events = []; } + reset() { this.events = []; } + capture(eventType, data) { this.events.push({ eventType, data }); } + + async validate() { + const resp = await fetch('/api/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ events: this.events }) + }); + return await resp.json(); + } + + renderElement(result) { + const el = document.createElement('div'); + el.className = 'sse-val'; + const n = result.eventCount; + const ok = result.isValid; + const vs = result.violations || []; + const esc = s => String(s).replace(/&/g,'&').replace(//g,'>'); + + let h = ok + ? `${n} events — all rules passed ✅` + : `${n} events — ${vs.length} violation(s)`; + + if (vs.length) { + h += '
'; + vs.forEach(v => { + h += `
[${esc(v.ruleId)}] ${esc(v.message)} #${v.eventIndex}
`; + }); + h += '
'; + } + + h += `
Event log (${this.events.length})
`; + this.events.forEach((e, i) => { + h += `
${i} ${esc(e.eventType)}
`; + }); + h += '
'; + + el.innerHTML = h; + return el; + } +} +"""; +} diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs new file mode 100644 index 0000000000..32f39f641c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -0,0 +1,183 @@ +// 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.AI.AgentServer.Responses; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Agents.AI.Hosting.AzureAIResponses; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; + +var builder = WebApplication.CreateBuilder(args); + +// --------------------------------------------------------------------------- +// 1. Register the Azure AI Responses Server SDK +// --------------------------------------------------------------------------- +builder.Services.AddResponsesServer(); + +// --------------------------------------------------------------------------- +// 2. 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 DefaultAzureCredential()); +IChatClient chatClient = azureClient.GetChatClient(deployment).AsIChatClient(); + +// --------------------------------------------------------------------------- +// 3. 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().ToArray()); + +// --------------------------------------------------------------------------- +// 4. 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)); + +// --------------------------------------------------------------------------- +// 5. Wire up the agent-framework handler as the IResponseHandler +// --------------------------------------------------------------------------- +builder.Services.AddAgentFrameworkHandler(); + +var app = builder.Build(); + +// Dispose the MCP client on shutdown +app.Lifetime.ApplicationStopping.Register(() => + mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult()); + +// --------------------------------------------------------------------------- +// 6. Routes +// --------------------------------------------------------------------------- +app.MapGet("/ready", () => Results.Ok("ready")); +app.MapResponsesServer(); + +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", (FoundryResponsesHosting.CapturedSseStream captured) => +{ + var validator = new FoundryResponsesHosting.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 +// --------------------------------------------------------------------------- + +[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."; +} diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json new file mode 100644 index 0000000000..b56d7a9ff4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "FoundryResponsesHosting": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:54747;http://localhost:54748" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs new file mode 100644 index 0000000000..72da677f45 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs @@ -0,0 +1,601 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FoundryResponsesHosting; + +/// Captured SSE event for validation. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")] +internal sealed record CapturedSseEvent( + [property: JsonPropertyName("eventType")] string EventType, + [property: JsonPropertyName("data")] string Data); + +/// Captured SSE stream sent from the client for server-side validation. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")] +internal sealed record CapturedSseStream( + [property: JsonPropertyName("events")] List Events); + +/// +/// Validates an SSE event stream from the Azure AI Responses Server SDK against +/// the API behaviour contract. Feed events sequentially via +/// and call when the stream ends. +/// +internal sealed class ResponseStreamValidator +{ + private readonly List _violations = []; + private int _eventCount; + private int _expectedSequenceNumber; + private StreamState _state = StreamState.Initial; + private string? _responseId; + private readonly HashSet _addedItemIndices = []; + private readonly HashSet _doneItemIndices = []; + private readonly HashSet _addedContentParts = []; // "outputIdx:partIdx" + private readonly HashSet _doneContentParts = []; + private readonly Dictionary _textAccumulators = []; // "outputIdx:contentIdx" → accumulated text + private bool _hasTerminal; + + /// All violations found so far. + internal IReadOnlyList Violations => _violations; + + /// + /// Processes a single SSE event line pair (event type + JSON data). + /// + /// The SSE event type (e.g. "response.created"). + /// The raw JSON data payload. + internal void ProcessEvent(string eventType, string jsonData) + { + JsonElement data; + try + { + data = JsonDocument.Parse(jsonData).RootElement; + } + catch (JsonException ex) + { + Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); + return; + } + + _eventCount++; + + // ── Sequence number validation ────────────────────────────────── + if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number) + { + int seq = seqProp.GetInt32(); + if (seq != _expectedSequenceNumber) + { + Fail("SEQ-01", $"Expected sequence_number {_expectedSequenceNumber}, got {seq}"); + } + + _expectedSequenceNumber = seq + 1; + } + else if (_state != StreamState.Initial || eventType != "error") + { + // Pre-creation error events may not have sequence_number + Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); + } + + // ── Post-terminal guard ───────────────────────────────────────── + if (_hasTerminal) + { + Fail("TERM-01", $"Event '{eventType}' received after terminal event"); + return; + } + + // ── Dispatch by event type ────────────────────────────────────── + switch (eventType) + { + case "response.created": + ValidateResponseCreated(data); + break; + + case "response.queued": + ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); + ValidateResponseEnvelope(data, eventType); + break; + + case "response.in_progress": + if (_state is StreamState.Created or StreamState.Queued) + { + _state = StreamState.InProgress; + } + else + { + Fail("ORDER-02", $"'response.in_progress' received in state {_state} (expected Created or Queued)"); + } + + ValidateResponseEnvelope(data, eventType); + break; + + case "response.output_item.added": + case "output_item.added": + ValidateInProgress(eventType); + ValidateOutputItemAdded(data); + break; + + case "response.output_item.done": + case "output_item.done": + ValidateInProgress(eventType); + ValidateOutputItemDone(data); + break; + + case "response.content_part.added": + case "content_part.added": + ValidateInProgress(eventType); + ValidateContentPartAdded(data); + break; + + case "response.content_part.done": + case "content_part.done": + ValidateInProgress(eventType); + ValidateContentPartDone(data); + break; + + case "response.output_text.delta": + case "output_text.delta": + ValidateInProgress(eventType); + ValidateTextDelta(data); + break; + + case "response.output_text.done": + case "output_text.done": + ValidateInProgress(eventType); + ValidateTextDone(data); + break; + + case "response.function_call_arguments.delta": + case "function_call_arguments.delta": + ValidateInProgress(eventType); + break; + + case "response.function_call_arguments.done": + case "function_call_arguments.done": + ValidateInProgress(eventType); + break; + + case "response.completed": + ValidateTerminal(data, "completed"); + break; + + case "response.failed": + ValidateTerminal(data, "failed"); + break; + + case "response.incomplete": + ValidateTerminal(data, "incomplete"); + break; + + case "error": + // Pre-creation error — standalone, no response.created precedes it + if (_state != StreamState.Initial) + { + Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); + } + + _hasTerminal = true; + break; + + default: + // Unknown events are not violations — the spec may evolve + break; + } + } + + /// + /// Call after the stream ends. Checks that a terminal event was received. + /// + internal void Complete() + { + if (!_hasTerminal && _state != StreamState.Initial) + { + Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); + } + + if (_state == StreamState.Initial && _eventCount == 0) + { + Fail("EMPTY-01", "No events received in the stream"); + } + + // Check for output items that were added but never completed + foreach (int idx in _addedItemIndices) + { + if (!_doneItemIndices.Contains(idx)) + { + Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); + } + } + + // Check for content parts that were added but never completed + foreach (string key in _addedContentParts) + { + if (!_doneContentParts.Contains(key)) + { + Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); + } + } + } + + /// + /// Returns a summary of all validation results. + /// + internal ValidationResult GetResult() + { + return new ValidationResult( + EventCount: _eventCount, + IsValid: _violations.Count == 0, + Violations: [.. _violations]); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Event-specific validators + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateResponseCreated(JsonElement data) + { + if (_state != StreamState.Initial) + { + Fail("ORDER-01", $"'response.created' received in state {_state} (expected Initial — must be first event)"); + return; + } + + _state = StreamState.Created; + + // Must have a response envelope + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", "'response.created' missing 'response' object"); + return; + } + + // Required response fields + ValidateRequiredResponseFields(resp, "response.created"); + + // Capture response ID for cross-event checks + if (resp.TryGetProperty("id", out var idProp)) + { + _responseId = idProp.GetString(); + } + + // Status must be non-terminal + if (resp.TryGetProperty("status", out var statusProp)) + { + string? status = statusProp.GetString(); + if (status is "completed" or "failed" or "incomplete" or "cancelled") + { + Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); + } + } + } + + private void ValidateTerminal(JsonElement data, string expectedKind) + { + if (_state is StreamState.Initial or StreamState.Created) + { + Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); + } + + _hasTerminal = true; + _state = StreamState.Terminal; + + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); + return; + } + + ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); + + if (resp.TryGetProperty("status", out var statusProp)) + { + string? status = statusProp.GetString(); + + // completed_at validation (B6) + bool hasCompletedAt = resp.TryGetProperty("completed_at", out var catProp) + && catProp.ValueKind != JsonValueKind.Null; + + if (status == "completed" && !hasCompletedAt) + { + Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); + } + + if (status != "completed" && hasCompletedAt) + { + Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); + } + + // error field validation + bool hasError = resp.TryGetProperty("error", out var errProp) + && errProp.ValueKind != JsonValueKind.Null; + + if (status == "failed" && !hasError) + { + Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); + } + + if (status is "completed" or "incomplete" && hasError) + { + Fail("FIELD-05", $"'error' must be null when status is '{status}'"); + } + + // error structure validation + if (hasError) + { + ValidateErrorObject(errProp, $"response.{expectedKind}"); + } + + // cancelled output must be empty (B11) + if (status == "cancelled" && resp.TryGetProperty("output", out var outputProp) + && outputProp.ValueKind == JsonValueKind.Array && outputProp.GetArrayLength() > 0) + { + Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); + } + + // response ID consistency + if (_responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != _responseId) + { + Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + } + } + + // Usage validation (optional, but if present must be structured correctly) + if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) + { + ValidateUsage(usageProp, $"response.{expectedKind}"); + } + } + + private void ValidateOutputItemAdded(JsonElement data) + { + if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) + { + int index = idxProp.GetInt32(); + if (!_addedItemIndices.Add(index)) + { + Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); + } + } + else + { + Fail("FIELD-06", "output_item.added missing 'output_index' field"); + } + + if (!data.TryGetProperty("item", out _)) + { + Fail("FIELD-07", "output_item.added missing 'item' object"); + } + } + + private void ValidateOutputItemDone(JsonElement data) + { + if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) + { + int index = idxProp.GetInt32(); + if (!_addedItemIndices.Contains(index)) + { + Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); + } + + _doneItemIndices.Add(index); + } + else + { + Fail("FIELD-06", "output_item.done missing 'output_index' field"); + } + } + + private void ValidateContentPartAdded(JsonElement data) + { + string key = GetContentPartKey(data); + if (!_addedContentParts.Add(key)) + { + Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); + } + } + + private void ValidateContentPartDone(JsonElement data) + { + string key = GetContentPartKey(data); + if (!_addedContentParts.Contains(key)) + { + Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); + } + + _doneContentParts.Add(key); + } + + private void ValidateTextDelta(JsonElement data) + { + string key = GetTextKey(data); + string delta = data.TryGetProperty("delta", out var deltaProp) + ? deltaProp.GetString() ?? string.Empty + : string.Empty; + + if (!_textAccumulators.TryGetValue(key, out string? existing)) + { + _textAccumulators[key] = delta; + } + else + { + _textAccumulators[key] = existing + delta; + } + } + + private void ValidateTextDone(JsonElement data) + { + string key = GetTextKey(data); + string? finalText = data.TryGetProperty("text", out var textProp) + ? textProp.GetString() + : null; + + if (finalText is null) + { + Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); + return; + } + + if (_textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) + { + Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Shared field validators + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateRequiredResponseFields(JsonElement resp, string context) + { + if (!HasNonNullString(resp, "id")) + { + Fail("FIELD-01", $"{context}: response missing 'id'"); + } + + if (resp.TryGetProperty("object", out var objProp)) + { + if (objProp.GetString() != "response") + { + Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); + } + } + else + { + Fail("FIELD-08", $"{context}: response missing 'object' field"); + } + + if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null) + { + Fail("FIELD-09", $"{context}: response missing 'created_at'"); + } + + if (!resp.TryGetProperty("status", out _)) + { + Fail("FIELD-10", $"{context}: response missing 'status'"); + } + + if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array) + { + Fail("FIELD-11", $"{context}: response missing 'output' array"); + } + } + + private void ValidateErrorObject(JsonElement error, string context) + { + if (!HasNonNullString(error, "code")) + { + Fail("ERR-02", $"{context}: error object missing 'code' field"); + } + + if (!HasNonNullString(error, "message")) + { + Fail("ERR-03", $"{context}: error object missing 'message' field"); + } + } + + private void ValidateUsage(JsonElement usage, string context) + { + if (!usage.TryGetProperty("input_tokens", out _)) + { + Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); + } + + if (!usage.TryGetProperty("output_tokens", out _)) + { + Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); + } + + if (!usage.TryGetProperty("total_tokens", out _)) + { + Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); + } + } + + private void ValidateResponseEnvelope(JsonElement data, string eventType) + { + if (!data.TryGetProperty("response", out var resp)) + { + Fail("FIELD-01", $"'{eventType}' missing 'response' object"); + return; + } + + ValidateRequiredResponseFields(resp, eventType); + + // Response ID consistency + if (_responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != _responseId) + { + Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Helpers + // ═══════════════════════════════════════════════════════════════════════ + + private void ValidateInProgress(string eventType) + { + if (_state != StreamState.InProgress) + { + Fail("ORDER-04", $"'{eventType}' received in state {_state} (expected InProgress)"); + } + } + + private void ValidateStateTransition(string eventType, StreamState expected, StreamState next) + { + if (_state != expected) + { + Fail("ORDER-05", $"'{eventType}' received in state {_state} (expected {expected})"); + } + else + { + _state = next; + } + } + + private void Fail(string ruleId, string message) + { + _violations.Add(new ValidationViolation(ruleId, message, _eventCount)); + } + + private static bool HasNonNullString(JsonElement obj, string property) + { + return obj.TryGetProperty(property, out var prop) + && prop.ValueKind == JsonValueKind.String + && !string.IsNullOrEmpty(prop.GetString()); + } + + private static string GetContentPartKey(JsonElement data) + { + int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1; + int partIdx = data.TryGetProperty("content_index", out var pi) ? pi.GetInt32() : -1; + return $"{outputIdx}:{partIdx}"; + } + + private static string GetTextKey(JsonElement data) + { + int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1; + int contentIdx = data.TryGetProperty("content_index", out var ci) ? ci.GetInt32() : -1; + return $"{outputIdx}:{contentIdx}"; + } + + private enum StreamState + { + Initial, + Created, + Queued, + InProgress, + Terminal, + } +} + +/// A single validation violation. +/// The rule identifier (e.g. SEQ-01, FIELD-02). +/// Human-readable description of the violation. +/// 1-based index of the event that triggered this violation. +internal sealed record ValidationViolation(string RuleId, string Message, int EventIndex); + +/// Overall validation result. +/// Total number of events processed. +/// True if no violations were found. +/// List of all violations. +internal sealed record ValidationResult(int EventCount, bool IsValid, IReadOnlyList Violations); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs new file mode 100644 index 0000000000..5f07cd4530 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// A implementation that bridges the Azure AI Responses Server SDK +/// with agent-framework instances, enabling agent-framework agents and workflows +/// to be hosted as Azure Foundry Hosted Agents. +/// +public class AgentFrameworkResponseHandler : ResponseHandler +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// that resolves agents from keyed DI services. + /// + /// The service provider for resolving agents. + /// The logger instance. + public AgentFrameworkResponseHandler( + IServiceProvider serviceProvider, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(logger); + + this._serviceProvider = serviceProvider; + this._logger = logger; + } + + /// + public override async IAsyncEnumerable CreateAsync( + CreateResponse request, + ResponseContext context, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // 1. Resolve agent + var agent = this.ResolveAgent(request); + + // 2. Create the SDK event stream builder + var stream = new ResponseEventStream(context, request); + + // 3. Emit lifecycle events + yield return stream.EmitCreated(); + yield return stream.EmitInProgress(); + + // 4. Convert input: history + current input → ChatMessage[] + var messages = new List(); + + // Load conversation history if available + var history = await context.GetHistoryAsync(cancellationToken).ConfigureAwait(false); + if (history.Count > 0) + { + messages.AddRange(InputConverter.ConvertOutputItemsToMessages(history)); + } + + // Load and convert current input items + var inputItems = await context.GetInputItemsAsync(cancellationToken).ConfigureAwait(false); + if (inputItems.Count > 0) + { + messages.AddRange(InputConverter.ConvertOutputItemsToMessages(inputItems)); + } + else + { + // Fall back to raw request input + messages.AddRange(InputConverter.ConvertInputToMessages(request)); + } + + // 5. Build chat options + var chatOptions = InputConverter.ConvertToChatOptions(request); + chatOptions.Instructions = request.Instructions; + var options = new ChatClientAgentRunOptions(chatOptions); + + // 6. Run the agent and convert output + // NOTE: C# forbids 'yield return' inside a try block that has a catch clause, + // and inside catch blocks. We use a flag to defer the yield to outside the try/catch. + bool emittedTerminal = false; + var enumerator = OutputConverter.ConvertUpdatesToEventsAsync( + agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken), + stream, + cancellationToken).GetAsyncEnumerator(cancellationToken); + try + { + while (true) + { + bool shutdownDetected = false; + ResponseStreamEvent? evt = null; + try + { + if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + break; + } + + evt = enumerator.Current; + } + catch (OperationCanceledException) when (context.IsShutdownRequested && !emittedTerminal) + { + shutdownDetected = true; + } + + if (shutdownDetected) + { + // Server is shutting down — emit incomplete so clients can resume + this._logger.LogInformation("Shutdown detected, emitting incomplete response."); + yield return stream.EmitIncomplete(); + yield break; + } + + // yield is in the outer try (finally-only) — allowed by C# + yield return evt!; + + if (evt is ResponseCompletedEvent or ResponseFailedEvent or ResponseIncompleteEvent) + { + emittedTerminal = true; + } + } + } + finally + { + await enumerator.DisposeAsync().ConfigureAwait(false); + } + } + + /// + /// Resolves an from the request. + /// Tries agent.name first, then falls back to metadata["entity_id"]. + /// If neither is present, attempts to resolve a default (non-keyed) . + /// + private AIAgent ResolveAgent(CreateResponse request) + { + var agentName = GetAgentName(request); + + if (!string.IsNullOrEmpty(agentName)) + { + var agent = this._serviceProvider.GetKeyedService(agentName); + if (agent is not null) + { + return agent; + } + + this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } + + // Try non-keyed default + var defaultAgent = this._serviceProvider.GetService(); + if (defaultAgent is not null) + { + return defaultAgent; + } + + var errorMessage = string.IsNullOrEmpty(agentName) + ? "No agent name specified in the request (via agent.name or metadata[\"entity_id\"]) and no default AIAgent is registered." + : $"Agent '{agentName}' not found. Ensure it is registered via AddAIAgent(\"{agentName}\", ...) or as a default AIAgent."; + + throw new InvalidOperationException(errorMessage); + } + + private static string? GetAgentName(CreateResponse request) + { + // Try agent.name from AgentReference + var agentName = request.AgentReference?.Name; + + // Fall back to "model" field (OpenAI clients send the agent name as the model) + if (string.IsNullOrEmpty(agentName)) + { + agentName = request.Model; + } + + // Fall back to metadata["entity_id"] + if (string.IsNullOrEmpty(agentName) && request.Metadata?.AdditionalProperties is not null) + { + request.Metadata.AdditionalProperties.TryGetValue("entity_id", out agentName); + } + + return agentName; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs new file mode 100644 index 0000000000..a35c8cd5b8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Converts Responses Server SDK input types to agent-framework types. +/// +internal static class InputConverter +{ + /// + /// Converts the SDK request input items into a list of . + /// + /// The create response request from the SDK. + /// A list of chat messages representing the request input. + public static List ConvertInputToMessages(CreateResponse request) + { + var messages = new List(); + + foreach (var item in request.GetInputExpanded()) + { + var message = ConvertInputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + + /// + /// Converts resolved SDK history/input items into instances. + /// + /// The resolved output items from the SDK context. + /// A list of chat messages. + public static List ConvertOutputItemsToMessages(IReadOnlyList items) + { + var messages = new List(); + + foreach (var item in items) + { + var message = ConvertOutputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + + /// + /// Creates from the SDK request properties. + /// + /// The create response request. + /// A configured instance. + public static ChatOptions ConvertToChatOptions(CreateResponse request) + { + return new ChatOptions + { + Temperature = (float?)request.Temperature, + TopP = (float?)request.TopP, + MaxOutputTokens = (int?)request.MaxOutputTokens, + ModelId = request.Model, + }; + } + + private static ChatMessage? ConvertInputItemToMessage(Item item) + { + return item switch + { + ItemMessage msg => ConvertItemMessage(msg), + FunctionCallOutputItemParam funcOutput => ConvertFunctionCallOutput(funcOutput), + ItemFunctionToolCall funcCall => ConvertItemFunctionToolCall(funcCall), + ItemReferenceParam => null, + _ => null + }; + } + + private static ChatMessage ConvertItemMessage(ItemMessage msg) + { + var role = ConvertMessageRole(msg.Role); + var contents = new List(); + + foreach (var content in msg.GetContentExpanded()) + { + switch (content) + { + case MessageContentInputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentInputImageContent imageContent: + if (imageContent.ImageUrl is not null) + { + var url = imageContent.ImageUrl.ToString(); + if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + contents.Add(new DataContent(url, "image/*")); + } + else + { + contents.Add(new UriContent(imageContent.ImageUrl, "image/*")); + } + } + else if (!string.IsNullOrEmpty(imageContent.FileId)) + { + contents.Add(new HostedFileContent(imageContent.FileId)); + } + + break; + case MessageContentInputFileContent fileContent: + if (fileContent.FileUrl is not null) + { + contents.Add(new UriContent(fileContent.FileUrl, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileData)) + { + contents.Add(new DataContent(fileContent.FileData, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileId)) + { + contents.Add(new HostedFileContent(fileContent.FileId)); + } + else if (!string.IsNullOrEmpty(fileContent.Filename)) + { + contents.Add(new MeaiTextContent($"[File: {fileContent.Filename}]")); + } + + break; + } + } + + if (contents.Count == 0) + { + contents.Add(new MeaiTextContent(string.Empty)); + } + + return new ChatMessage(role, contents); + } + + private static ChatMessage ConvertFunctionCallOutput(FunctionCallOutputItemParam funcOutput) + { + var output = funcOutput.Output?.ToString() ?? string.Empty; + return new ChatMessage( + ChatRole.Tool, + [new FunctionResultContent(funcOutput.CallId, output)]); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK input.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK input.")] + private static ChatMessage ConvertItemFunctionToolCall(ItemFunctionToolCall funcCall) + { + IDictionary? arguments = null; + if (funcCall.Arguments is not null) + { + try + { + arguments = JsonSerializer.Deserialize>(funcCall.Arguments); + } + catch (JsonException) + { + arguments = new Dictionary { ["_raw"] = funcCall.Arguments }; + } + } + + return new ChatMessage( + ChatRole.Assistant, + [new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]); + } + + private static ChatMessage? ConvertOutputItemToMessage(OutputItem item) + { + return item switch + { + OutputItemMessage msg => ConvertOutputItemMessageToChat(msg), + OutputItemFunctionToolCall funcCall => ConvertOutputItemFunctionCall(funcCall), + FunctionToolCallOutputResource funcOutput => ConvertFunctionToolCallOutputResource(funcOutput), + OutputItemReasoningItem => null, + _ => null + }; + } + + private static ChatMessage ConvertOutputItemMessageToChat(OutputItemMessage msg) + { + var role = ConvertMessageRole(msg.Role); + var contents = new List(); + + foreach (var content in msg.Content) + { + switch (content) + { + case MessageContentInputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentOutputTextContent textContent: + contents.Add(new MeaiTextContent(textContent.Text)); + break; + case MessageContentRefusalContent refusal: + contents.Add(new MeaiTextContent($"[Refusal: {refusal.Refusal}]")); + break; + case MessageContentInputImageContent imageContent: + if (imageContent.ImageUrl is not null) + { + var url = imageContent.ImageUrl.ToString(); + if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + contents.Add(new DataContent(url, "image/*")); + } + else + { + contents.Add(new UriContent(imageContent.ImageUrl, "image/*")); + } + } + else if (!string.IsNullOrEmpty(imageContent.FileId)) + { + contents.Add(new HostedFileContent(imageContent.FileId)); + } + + break; + case MessageContentInputFileContent fileContent: + if (fileContent.FileUrl is not null) + { + contents.Add(new UriContent(fileContent.FileUrl, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileData)) + { + contents.Add(new DataContent(fileContent.FileData, "application/octet-stream")); + } + else if (!string.IsNullOrEmpty(fileContent.FileId)) + { + contents.Add(new HostedFileContent(fileContent.FileId)); + } + else if (!string.IsNullOrEmpty(fileContent.Filename)) + { + contents.Add(new MeaiTextContent($"[File: {fileContent.Filename}]")); + } + + break; + } + } + + if (contents.Count == 0) + { + contents.Add(new MeaiTextContent(string.Empty)); + } + + return new ChatMessage(role, contents); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK output history.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK output history.")] + private static ChatMessage ConvertOutputItemFunctionCall(OutputItemFunctionToolCall funcCall) + { + IDictionary? arguments = null; + if (funcCall.Arguments is not null) + { + try + { + arguments = JsonSerializer.Deserialize>(funcCall.Arguments); + } + catch (JsonException) + { + arguments = new Dictionary { ["_raw"] = funcCall.Arguments }; + } + } + + return new ChatMessage( + ChatRole.Assistant, + [new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]); + } + + private static ChatMessage ConvertFunctionToolCallOutputResource(FunctionToolCallOutputResource funcOutput) + { + return new ChatMessage( + ChatRole.Tool, + [new FunctionResultContent(funcOutput.CallId, funcOutput.Output)]); + } + + private static ChatRole ConvertMessageRole(MessageRole role) + { + return role switch + { + MessageRole.User => ChatRole.User, + MessageRole.Assistant => ChatRole.Assistant, + MessageRole.System => ChatRole.System, + MessageRole.Developer => new ChatRole("developer"), + _ => ChatRole.User + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj new file mode 100644 index 0000000000..b881e287cc --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj @@ -0,0 +1,40 @@ + + + + $(TargetFrameworksCore) + enable + Microsoft.Agents.AI.Hosting.AzureAIResponses + alpha + $(NoWarn);MEAI001;NU1903 + false + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs new file mode 100644 index 0000000000..c620cf6324 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Converts agent-framework streams into +/// Responses Server SDK sequences using the +/// builder pattern. +/// +internal static class OutputConverter +{ + /// + /// Converts a stream of into a stream of + /// using the SDK builder pattern. + /// + /// The agent response updates to convert. + /// The SDK event stream builder. + /// Cancellation token. + /// An async enumerable of SDK response stream events (excluding lifecycle events). + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Serializing function call arguments dictionary.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Serializing function call arguments dictionary.")] + public static async IAsyncEnumerable ConvertUpdatesToEventsAsync( + IAsyncEnumerable updates, + ResponseEventStream stream, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ResponseUsage? accumulatedUsage = null; + OutputItemMessageBuilder? currentMessageBuilder = null; + TextContentBuilder? currentTextBuilder = null; + StringBuilder? accumulatedText = null; + string? previousMessageId = null; + bool hasTerminalEvent = false; + var executorItemIds = new Dictionary(); + + await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Handle workflow events from RawRepresentation + if (update.RawRepresentation is WorkflowEvent workflowEvent) + { + // Close any open message builder before emitting workflow items + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + foreach (var evt in EmitWorkflowEvent(stream, workflowEvent, executorItemIds)) + { + yield return evt; + } + + continue; + } + + foreach (var content in update.Contents) + { + switch (content) + { + case MeaiTextContent textContent: + { + if (!IsSameMessage(update.MessageId, previousMessageId) && currentMessageBuilder is not null) + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + } + + previousMessageId = update.MessageId; + + if (currentMessageBuilder is null) + { + currentMessageBuilder = stream.AddOutputItemMessage(); + yield return currentMessageBuilder.EmitAdded(); + + currentTextBuilder = currentMessageBuilder.AddTextContent(); + yield return currentTextBuilder.EmitAdded(); + + accumulatedText = new StringBuilder(); + } + + if (textContent.Text is { Length: > 0 }) + { + accumulatedText!.Append(textContent.Text); + yield return currentTextBuilder!.EmitDelta(textContent.Text); + } + + break; + } + + case FunctionCallContent funcCall: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + var callId = funcCall.CallId ?? Guid.NewGuid().ToString("N"); + var funcBuilder = stream.AddOutputItemFunctionCall(funcCall.Name, callId); + yield return funcBuilder.EmitAdded(); + + var arguments = funcCall.Arguments is not null + ? JsonSerializer.Serialize(funcCall.Arguments) + : "{}"; + + yield return funcBuilder.EmitArgumentsDelta(arguments); + yield return funcBuilder.EmitArgumentsDone(arguments); + yield return funcBuilder.EmitDone(); + break; + } + + case TextReasoningContent reasoningContent: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + + var reasoningBuilder = stream.AddOutputItemReasoningItem(); + yield return reasoningBuilder.EmitAdded(); + + var summaryPart = reasoningBuilder.AddSummaryPart(); + yield return summaryPart.EmitAdded(); + + var text = reasoningContent.Text ?? string.Empty; + yield return summaryPart.EmitTextDelta(text); + yield return summaryPart.EmitTextDone(text); + yield return summaryPart.EmitDone(); + reasoningBuilder.EmitSummaryPartDone(summaryPart); + + yield return reasoningBuilder.EmitDone(); + break; + } + + case UsageContent usageContent when usageContent.Details is not null: + { + accumulatedUsage = ConvertUsage(usageContent.Details, accumulatedUsage); + break; + } + + case ErrorContent errorContent: + { + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + currentTextBuilder = null; + currentMessageBuilder = null; + accumulatedText = null; + previousMessageId = null; + hasTerminalEvent = true; + + yield return stream.EmitFailed( + ResponseErrorCode.ServerError, + errorContent.Message ?? "An error occurred during agent execution.", + accumulatedUsage); + yield break; + } + + case DataContent: + case UriContent: + // Image/audio/file content from agents is not currently supported + // as streaming output items in the Responses Server SDK builder pattern. + // These would need to be serialized as base64 or URL references. + break; + + case FunctionResultContent: + // Function results are internal to the agent's tool-calling loop + // and are not emitted as output items in the response stream. + break; + + default: + break; + } + } + } + + // Close any remaining open message + foreach (var evt in CloseCurrentMessage(currentMessageBuilder, currentTextBuilder, accumulatedText)) + { + yield return evt; + } + + if (!hasTerminalEvent) + { + yield return stream.EmitCompleted(accumulatedUsage); + } + } + + private static IEnumerable CloseCurrentMessage( + OutputItemMessageBuilder? messageBuilder, + TextContentBuilder? textBuilder, + StringBuilder? accumulatedText) + { + if (messageBuilder is null) + { + yield break; + } + + if (textBuilder is not null) + { + var finalText = accumulatedText?.ToString() ?? string.Empty; + yield return textBuilder.EmitDone(finalText); + yield return messageBuilder.EmitContentDone(textBuilder); + } + + yield return messageBuilder.EmitDone(); + } + + private static bool IsSameMessage(string? currentId, string? previousId) => + currentId is not { Length: > 0 } || previousId is not { Length: > 0 } || currentId == previousId; + + private static ResponseUsage ConvertUsage(UsageDetails details, ResponseUsage? existing) + { + var inputTokens = (long)(details.InputTokenCount ?? 0); + var outputTokens = (long)(details.OutputTokenCount ?? 0); + var totalTokens = (long)(details.TotalTokenCount ?? 0); + + if (existing is not null) + { + inputTokens += existing.InputTokens; + outputTokens += existing.OutputTokens; + totalTokens += existing.TotalTokens; + } + + return AzureAIAgentServerResponsesModelFactory.ResponseUsage( + inputTokens: inputTokens, + outputTokens: outputTokens, + totalTokens: totalTokens); + } + + private static IEnumerable EmitWorkflowEvent( + ResponseEventStream stream, + WorkflowEvent workflowEvent, + Dictionary executorItemIds) + { + switch (workflowEvent) + { + case ExecutorInvokedEvent invokedEvent: + { + var itemId = GenerateItemId("wfa"); + executorItemIds[invokedEvent.ExecutorId] = itemId; + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: invokedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.InProgress, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + break; + } + + case ExecutorCompletedEvent completedEvent: + { + var itemId = GenerateItemId("wfa"); + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: completedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.Completed, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + executorItemIds.Remove(completedEvent.ExecutorId); + break; + } + + case ExecutorFailedEvent failedEvent: + { + var itemId = GenerateItemId("wfa"); + + var item = new WorkflowActionOutputItem( + kind: "InvokeExecutor", + actionId: failedEvent.ExecutorId, + status: WorkflowActionOutputItemStatus.Failed, + id: itemId); + + var builder = stream.AddOutputItem(itemId); + yield return builder.EmitAdded(item); + yield return builder.EmitDone(item); + executorItemIds.Remove(failedEvent.ExecutorId); + break; + } + + // Informational/lifecycle events — no SDK output needed. + // Note: AgentResponseUpdateEvent and WorkflowErrorEvent are unwrapped by + // WorkflowSession.InvokeStageAsync() into regular AgentResponseUpdate objects + // with populated Contents (TextContent, ErrorContent, etc.), so they flow + // through the normal content processing path above — not through this method. + case SuperStepStartedEvent: + case SuperStepCompletedEvent: + case WorkflowStartedEvent: + case WorkflowWarningEvent: + case RequestInfoEvent: + break; + } + } + + /// + /// Generates a valid item ID matching the SDK's {prefix}_{50chars} format. + /// + private static string GenerateItemId(string prefix) + { + // SDK format: {prefix}_{50 char body} + var bytes = RandomNumberGenerator.GetBytes(25); + var body = Convert.ToHexString(bytes); // 50 hex chars, uppercase + return $"{prefix}_{body}"; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..d596ba3457 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.AgentServer.Responses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; + +/// +/// Extension methods for to register the agent-framework +/// response handler with the Azure AI Responses Server SDK. +/// +public static class AgentFrameworkResponsesServiceCollectionExtensions +{ + /// + /// Registers as the + /// for the Azure AI Responses Server SDK. Agents are resolved from keyed DI services + /// using the agent.name or metadata["entity_id"] from incoming requests. + /// + /// + /// + /// Call this method after AddResponsesServer() and after registering your + /// instances (e.g., via AddAIAgent()). + /// + /// + /// Example: + /// + /// builder.Services.AddResponsesServer(); + /// builder.AddAIAgent("my-agent", ...); + /// builder.Services.AddAgentFrameworkHandler(); + /// + /// var app = builder.Build(); + /// app.MapResponsesServer(); + /// + /// + /// + /// The service collection. + /// The service collection for chaining. + public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + services.TryAddSingleton(); + return services; + } + + /// + /// Registers a specific as the handler for all incoming requests, + /// regardless of the agent.name in the request. + /// + /// + /// + /// Use this overload when hosting a single agent. The provided agent instance is + /// registered both as a keyed service and as the default . + /// + /// + /// Example: + /// + /// builder.Services.AddResponsesServer(); + /// builder.Services.AddAgentFrameworkHandler(myAgent); + /// + /// var app = builder.Build(); + /// app.MapResponsesServer(); + /// + /// + /// + /// The service collection. + /// The agent instance to register. + /// The service collection for chaining. + public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services, AIAgent agent) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(agent); + + services.TryAddSingleton(agent); + services.TryAddSingleton(); + return services; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs new file mode 100644 index 0000000000..ee32771a67 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -0,0 +1,815 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class AgentFrameworkResponseHandlerTests +{ + private static string ValidResponseId => "resp_" + new string('0', 46); + + [Fact] + public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() + { + // Arrange + var agent = CreateTestAgent("Hello from the agent!"); + var services = new ServiceCollection(); + services.AddSingleton(agent); + services.AddSingleton>(NullLogger.Instance); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); + Assert.IsType(events[0]); + Assert.IsType(events[1]); + } + + [Fact] + public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() + { + // Arrange + var agent = CreateTestAgent("Keyed agent response"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("my-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert - should have produced events from the keyed agent + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + } + + [Fact] + public void Constructor_NullServiceProvider_ThrowsArgumentNullException() + { + Assert.Throws( + () => new AgentFrameworkResponseHandler(null!, NullLogger.Instance)); + } + + [Fact] + public void Constructor_NullLogger_ThrowsArgumentNullException() + { + var sp = new ServiceCollection().BuildServiceProvider(); + Assert.Throws( + () => new AgentFrameworkResponseHandler(sp, null!)); + } + + [Fact] + public async Task CreateAsync_ResolvesAgentByModelField() + { + // Arrange + var agent = CreateTestAgent("model agent"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-agent"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() + { + // Arrange + var agent = CreateTestAgent("entity agent"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("entity-agent", agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: ""); + var metadata = new Metadata(); + metadata.AdditionalProperties["entity_id"] = "entity-agent"; + request.Metadata = metadata; + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() + { + // Arrange + var agent = CreateTestAgent("default agent"); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("nonexistent-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("missing-agent")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + var ex = await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + + Assert.Contains("missing-agent", ex.Message); + } + + [Fact] + public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: ""); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + var ex = await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + + Assert.Contains("No agent name specified", ex.Message); + } + + [Fact] + public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvents() + { + // Arrange + var services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + bool threw = false; + try + { + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + } + catch (InvalidOperationException) + { + threw = true; + } + + // Assert + Assert.True(threw); + Assert.Empty(events); + } + + [Fact] + public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var historyItem = new OutputItemMessage( + id: "hist_1", + role: MessageRole.Assistant, + content: [new MessageContentOutputTextContent( + "Previous response", + Array.Empty(), + Array.Empty())], + status: MessageStatus.Completed); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(new OutputItem[] { historyItem }); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.True(messages.Count >= 2); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Raw input" } } } + }); + + var inputItem = new OutputItemMessage( + id: "input_1", + role: MessageRole.Assistant, + content: [new MessageContentOutputTextContent( + "Resolved input", + Array.Empty(), + Array.Empty())], + status: MessageStatus.Completed); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(new OutputItem[] { inputItem }); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Raw input" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedMessages); + var messages = agent.CapturedMessages.ToList(); + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + } + + [Fact] + public async Task CreateAsync_PassesInstructionsToAgent() + { + // Arrange + var agent = new CapturingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + instructions: "You are a helpful assistant."); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.NotNull(agent.CapturedOptions); + var chatClientOptions = Assert.IsType(agent.CapturedOptions); + Assert.Equal("You are a helpful assistant.", chatClientOptions.ChatOptions?.Instructions); + } + + [Fact] + public async Task CreateAsync_AgentThrows_ExceptionPropagates() + { + // Arrange + var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + } + }); + } + + [Fact] + public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() + { + // Arrange + var agent1 = CreateTestAgent("Agent 1 response"); + var agent2 = CreateTestAgent("Agent 2 response"); + var services = new ServiceCollection(); + services.AddKeyedSingleton("agent-1", agent1); + services.AddKeyedSingleton("agent-2", agent2); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("agent-2")); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + // Act + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) + { + events.Add(evt); + } + + // Assert + Assert.True(events.Count >= 4); + Assert.IsType(events[0]); + } + + [Fact] + public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCanceledException() + { + // Arrange + var agent = new CancellationCheckingAgent(); + var services = new ServiceCollection(); + services.AddSingleton(agent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } } + }); + + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in handler.CreateAsync(request, mockContext.Object, cts.Token)) + { + } + }); + } + + private static TestAgent CreateTestAgent(string responseText) + { + return new TestAgent(responseText); + } + + private static async IAsyncEnumerable ToAsyncEnumerableAsync(params AgentResponseUpdate[] items) + { + foreach (var item in items) + { + yield return item; + } + + await Task.CompletedTask; + } + + private sealed class TestAgent(string responseText) : AIAgent + { + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + ToAsyncEnumerableAsync(new AgentResponseUpdate + { + MessageId = "resp_msg_1", + Contents = [new MeaiTextContent(responseText)] + }); + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class ThrowingAgent(Exception exception) : AIAgent + { + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw exception; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class CapturingAgent : AIAgent + { + public IEnumerable? CapturedMessages { get; private set; } + public AgentRunOptions? CapturedOptions { get; private set; } + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) + { + CapturedMessages = messages.ToList(); + CapturedOptions = options; + return ToAsyncEnumerableAsync(new AgentResponseUpdate + { + MessageId = "resp_msg_1", + Contents = [new MeaiTextContent("captured")] + }); + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + private sealed class CancellationCheckingAgent : AIAgent + { + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new AgentResponseUpdate { Contents = [new MeaiTextContent("test")] }; + await Task.CompletedTask; + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs new file mode 100644 index 0000000000..34555798a7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs @@ -0,0 +1,671 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class InputConverterTests +{ + [Fact] + public void ConvertInputToMessages_EmptyRequest_ReturnsEmptyList() + { + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(Array.Empty()); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Empty(messages); + } + + [Fact] + public void ConvertInputToMessages_UserTextMessage_ReturnsUserMessage() + { + var input = new[] + { + new + { + type = "message", + id = "msg_001", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hello, agent!" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text == "Hello, agent!"); + } + + [Fact] + public void ConvertInputToMessages_FunctionCallOutput_ReturnsToolMessage() + { + var input = new[] + { + new + { + type = "function_call_output", + id = "fc_out_001", + call_id = "call_123", + output = "42" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.Tool, messages[0].Role); + var funcResult = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(funcResult); + Assert.Equal("call_123", funcResult.CallId); + } + + [Fact] + public void ConvertInputToMessages_FunctionToolCall_ReturnsAssistantMessage() + { + var input = new[] + { + new + { + type = "function_call", + id = "fc_001", + call_id = "call_456", + name = "get_weather", + arguments = "{\"location\": \"Seattle\"}" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + var funcCall = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(funcCall); + Assert.Equal("call_456", funcCall.CallId); + Assert.Equal("get_weather", funcCall.Name); + } + + [Fact] + public void ConvertInputToMessages_MultipleItems_ReturnsAllMessages() + { + var input = new object[] + { + new + { + type = "message", + id = "msg_001", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "What's the weather?" } } + }, + new + { + type = "function_call", + id = "fc_001", + call_id = "call_789", + name = "get_weather", + arguments = "{}" + }, + new + { + type = "function_call_output", + id = "fc_out_001", + call_id = "call_789", + output = "Sunny, 72°F" + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Equal(3, messages.Count); + Assert.Equal(ChatRole.User, messages[0].Role); + Assert.Equal(ChatRole.Assistant, messages[1].Role); + Assert.Equal(ChatRole.Tool, messages[2].Role); + } + + [Fact] + public void ConvertToChatOptions_SetsTemperatureAndTopP() + { + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + temperature: 0.7, + topP: 0.9, + maxOutputTokens: 1000, + model: "gpt-4o"); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Equal(0.7f, options.Temperature); + Assert.Equal(0.9f, options.TopP); + Assert.Equal(1000, options.MaxOutputTokens); + Assert.Equal("gpt-4o", options.ModelId); + } + + [Fact] + public void ConvertToChatOptions_NullValues_SetsNulls() + { + var request = new CreateResponse(); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Null(options.Temperature); + Assert.Null(options.TopP); + Assert.Null(options.MaxOutputTokens); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessage_ReturnsAssistantMessage() + { + var textContent = new MessageContentOutputTextContent( + "Hello from assistant", + Array.Empty(), + Array.Empty()); + var outputMsg = new OutputItemMessage( + id: "out_001", + role: MessageRole.Assistant, + content: [textContent], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text == "Hello from assistant"); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCall_ReturnsAssistantMessage() + { + var funcCall = new OutputItemFunctionToolCall( + callId: "call_abc", + name: "search", + arguments: "{\"query\": \"test\"}"); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcCall]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + var content = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(content); + Assert.Equal("call_abc", content.CallId); + Assert.Equal("search", content.Name); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCallOutputResource_ReturnsToolMessage() + { + var funcOutput = new FunctionToolCallOutputResource( + callId: "call_def", + output: BinaryData.FromString("result data")); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcOutput]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Tool, messages[0].Role); + var result = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(result); + Assert.Equal("call_def", result.CallId); + } + + [Fact] + public void ConvertOutputItemsToMessages_ReasoningItem_ReturnsNull() + { + var reasoning = AzureAIAgentServerResponsesModelFactory.OutputItemReasoningItem( + id: "reason_001"); + + var messages = InputConverter.ConvertOutputItemsToMessages([reasoning]); + + Assert.Empty(messages); + } + + // ── Image Content Tests (B-03 through B-06) ── + + [Fact] + public void ConvertInputToMessages_ImageContentWithHttpUrl_ReturnsUriContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", image_url = "https://example.com/img.png" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is UriContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentWithDataUri_ReturnsDataContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", image_url = "data:image/png;base64,iVBORw0KGgo=" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is DataContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentWithFileId_ReturnsHostedFileContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image", file_id = "file_abc123" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is HostedFileContent); + } + + [Fact] + public void ConvertInputToMessages_ImageContentNoUrlOrFileId_ProducesNoContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_image" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Single(messages[0].Contents); + } + + // ── File Content Tests (B-07 through B-11) ── + + [Fact] + public void ConvertInputToMessages_FileContentWithUrl_ReturnsUriContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_url = "https://example.com/doc.pdf" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is UriContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithInlineData_ReturnsDataContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_data = "data:application/pdf;base64,iVBORw0KGgo=" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is DataContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithFileId_ReturnsHostedFileContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", file_id = "file_xyz789" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is HostedFileContent); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithFilenameOnly_ReturnsFallbackText() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file", filename = "report.pdf" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("report.pdf")); + } + + [Fact] + public void ConvertInputToMessages_FileContentWithNothing_ProducesNoContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_file" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Single(messages[0].Contents); + } + + // ── Mixed Content / Edge Cases (B-15 through B-18) ── + + [Fact] + public void ConvertInputToMessages_MixedContentInSingleMessage_ReturnsAllContentTypes() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new object[] + { + new { type = "input_text", text = "Look at this:" }, + new { type = "input_image", image_url = "https://example.com/img.png" } + } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(2, messages[0].Contents.Count); + } + + [Fact] + public void ConvertInputToMessages_EmptyMessageContent_ReturnsFallbackTextContent() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = Array.Empty() + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + var textContent = Assert.IsType(Assert.Single(messages[0].Contents)); + Assert.Equal(string.Empty, textContent.Text); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageRefusal_ReturnsRefusalText() + { + var refusal = new MessageContentRefusalContent("I cannot help with that"); + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [refusal], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("[Refusal:")); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("I cannot help with that")); + } + + [Fact] + public void ConvertInputToMessages_ItemReferenceParam_IsSkipped() + { + var input = new object[] + { + new { type = "item_reference", id = "ref_001" }, + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hello" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + } + + // ── Role Mapping Tests (C-01 through C-05) ── + + [Fact] + public void ConvertInputToMessages_UserRole_ReturnsChatRoleUser() + { + var input = new[] + { + new + { + type = "message", + id = "msg_1", + status = "completed", + role = "user", + content = new[] { new { type = "input_text", text = "Hi" } } + } + }; + + var request = new CreateResponse(); + request.Input = BinaryData.FromObjectAsJson(input); + + var messages = InputConverter.ConvertInputToMessages(request); + + Assert.Single(messages); + Assert.Equal(ChatRole.User, messages[0].Role); + } + + [Fact] + public void ConvertOutputItemsToMessages_AssistantRole_ReturnsChatRoleAssistant() + { + // OutputItemMessage always maps to assistant role + var textContent = new MessageContentOutputTextContent( + "Hi", Array.Empty(), Array.Empty()); + var outputMsg = new OutputItemMessage( + id: "msg_1", + role: MessageRole.Assistant, + content: [textContent], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + } + + // ── History Conversion Edge Cases (D-02 through D-12) ── + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageWithRefusal_ReturnsRefusalText() + { + var refusal = new MessageContentRefusalContent("Not allowed"); + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [refusal], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + Assert.Equal(ChatRole.Assistant, messages[0].Role); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("[Refusal:")); + Assert.Contains(messages[0].Contents, c => c is MeaiTextContent tc && tc.Text!.Contains("Not allowed")); + } + + [Fact] + public void ConvertOutputItemsToMessages_OutputMessageWithEmptyContent_ReturnsFallbackText() + { + var outputMsg = new OutputItemMessage( + id: "out_1", + role: MessageRole.Assistant, + content: [], + status: MessageStatus.Completed); + + var messages = InputConverter.ConvertOutputItemsToMessages([outputMsg]); + + Assert.Single(messages); + var textContent = Assert.IsType(Assert.Single(messages[0].Contents)); + Assert.Equal(string.Empty, textContent.Text); + } + + [Fact] + public void ConvertOutputItemsToMessages_FunctionToolCallWithMalformedArgs_UsesRawFallback() + { + var funcCall = new OutputItemFunctionToolCall( + callId: "call_1", + name: "test", + arguments: "not-json{{{"); + + var messages = InputConverter.ConvertOutputItemsToMessages([funcCall]); + + Assert.Single(messages); + var content = messages[0].Contents.OfType().FirstOrDefault(); + Assert.NotNull(content); + Assert.NotNull(content.Arguments); + Assert.True(content.Arguments.ContainsKey("_raw")); + } + + [Fact] + public void ConvertOutputItemsToMessages_UnknownOutputItemType_IsSkipped() + { + var messages = InputConverter.ConvertOutputItemsToMessages([]); + + Assert.Empty(messages); + } + + [Fact] + public void ConvertToChatOptions_ModelId_SetFromRequest() + { + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-model"); + + var options = InputConverter.ConvertToChatOptions(request); + + Assert.Equal("my-model", options.ModelId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj new file mode 100644 index 0000000000..09c3ba24c1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj @@ -0,0 +1,17 @@ + + + + $(TargetFrameworksCore) + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs new file mode 100644 index 0000000000..dde11298c7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs @@ -0,0 +1,1080 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Agents.AI.Workflows; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class OutputConverterTests +{ + private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() + { + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model"); + var stream = new ResponseEventStream(mockContext.Object, request); + return (stream, mockContext); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyStream_EmitsCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = ToAsync(Array.Empty()); + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(updates, stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_SingleTextUpdate_EmitsMessageAndCompleted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + MessageId = "msg_1", + Contents = [new MeaiTextContent("Hello, world!")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Expected: MessageAdded, TextAdded, TextDelta, TextDone, ContentDone, MessageDone, Completed + Assert.True(events.Count >= 5, $"Expected at least 5 events, got {events.Count}"); + Assert.IsType(events[0]); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleTextUpdates_EmitsStreamingDeltas() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Hello, ")] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("world!")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should have two text delta events among the others + Assert.True(events.Count >= 6, $"Expected at least 6 events, got {events.Count}"); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCall_EmitsFunctionCallEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "get_weather", + new Dictionary { ["city"] = "Seattle" })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Should have: FuncAdded, ArgsDelta, ArgsDone, FuncDone, Completed + Assert.IsType(events[0]); + Assert.IsType(events[^1]); + Assert.True(events.Count >= 4, $"Expected at least 4 events for function call, got {events.Count}"); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContent_EmitsFailed() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new ErrorContent("Something went wrong")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContent_DoesNotEmitCompleted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new ErrorContent("Failure")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_UsageContent_IncludesUsageInCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate + { + MessageId = "msg_1", + Contents = [new MeaiTextContent("Hi")] + }, + new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails + { + InputTokenCount = 10, + OutputTokenCount = 5, + TotalTokenCount = 15 + })] + } + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + var completedEvent = events.OfType().SingleOrDefault(); + Assert.NotNull(completedEvent); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningContent_EmitsReasoningEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new TextReasoningContent("Let me think about this...")] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + // Should have: ReasoningAdded, SummaryPartAdded, TextDelta, TextDone, SummaryDone, ReasoningDone, Completed + Assert.True(events.Count >= 5, $"Expected at least 5 events for reasoning, got {events.Count}"); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task ConvertUpdatesToEventsAsync_CancellationRequested_Throws() + { + var (stream, _) = CreateTestStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var updates = ToAsync(new[] { new AgentResponseUpdate { Contents = [new MeaiTextContent("test")] } }); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(updates, stream, cts.Token)) + { + // Should throw before yielding + } + }); + } + + // F-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseTextDeltaEvent); + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // F-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent((string)null!)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.DoesNotContain(events, e => e is ResponseTextDeltaEvent); + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // F-07 + [Fact] + public async Task ConvertUpdatesToEventsAsync_DifferentMessageIds_CreatesMultipleMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("First")] }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Second")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // F-08 + [Fact] + public async Task ConvertUpdatesToEventsAsync_NullMessageIds_TreatedAsSameMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = null, Contents = [new MeaiTextContent("First")] }, + new AgentResponseUpdate { MessageId = null, Contents = [new MeaiTextContent("Second")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Single(events.OfType()); + } + + // G-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("thinking...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "search", new Dictionary { ["q"] = "test" })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // G-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_EmitsEmptyJson() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "do_something", (IDictionary?)null)] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + } + + // G-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithEmptyCallId_GeneratesCallId() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new FunctionCallContent("", "do_something", new Dictionary { ["x"] = 1 })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + } + + // G-05 + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleFunctionCalls_EmitsSeparateBuilders() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "func_a", new Dictionary { ["a"] = 1 })] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_2", "func_b", new Dictionary { ["b"] = 2 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // H-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningWithNullText_EmitsEmptyString() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new TextReasoningContent(null)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.True(events.Count >= 5, $"Expected at least 5 events, got {events.Count}"); + Assert.IsType(events[^1]); + } + + // H-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial")] }, + new AgentResponseUpdate { Contents = [new TextReasoningContent("thinking")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // I-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContentWithNullMessage_UsesDefaultMessage() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new ErrorContent(null!)] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseFailedEvent); + } + + // I-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorContentClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial text")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Something broke")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + } + + // I-06 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ErrorAfterPartialText_ClosesMessageThenFails() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial text")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Unexpected error")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + // J-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultipleUsageUpdates_AccumulatesTokens() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Hi")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 })] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 20, OutputTokenCount = 10, TotalTokenCount = 30 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // J-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_UsageWithZeroTokens_StillCompletes() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 0, OutputTokenCount = 0, TotalTokenCount = 0 })] + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseCompletedEvent); + } + + // K-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_DataContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new DataContent("data:image/png;base64,aWNv", "image/png")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // K-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_UriContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new UriContent("https://example.com/file.txt", "text/plain")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // K-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionResultContent_IsSkippedWithNoEvents() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new FunctionResultContent("call_1", "result data")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Single(events); + Assert.IsType(events[0]); + } + + // L-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItem() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("executor_1", "invoked") }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorCompletedEvent_EmitsCompletedWorkflowAction() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("executor_1", null) }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorFailedEvent_EmitsFailedWorkflowAction() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { RawRepresentation = new ExecutorFailedEvent("executor_1", new InvalidOperationException("test error")) }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + Assert.Contains(events, e => e is ResponseOutputItemAddedEvent); + Assert.Contains(events, e => e is ResponseOutputItemDoneEvent); + Assert.IsType(events[^1]); + } + + // L-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowEventClosesOpenMessage() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("partial")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("exec_1", "invoked") }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // L-06 + [Fact] + public async Task ConvertUpdatesToEventsAsync_InterleavedWorkflowAndTextEvents() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("exec_1", "invoked") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Agent says hello")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("exec_1", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // M-01 + [Fact] + public async Task ConvertUpdatesToEventsAsync_TextThenFunctionCallThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Let me check...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "search", new Dictionary { ["q"] = "weather" })] }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Here are the results")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + } + + // M-02 + [Fact] + public async Task ConvertUpdatesToEventsAsync_ReasoningThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new TextReasoningContent("Thinking about the answer...")] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("The answer is 42")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(2, events.OfType().Count()); + } + + // M-03 + [Fact] + public async Task ConvertUpdatesToEventsAsync_TextThenError_EmitsMessageThenFailed() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting...")] }, + new AgentResponseUpdate { Contents = [new ErrorContent("Unexpected error")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + Assert.Single(events.OfType()); + } + + // M-04 + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionCallThenTextThenFunctionCall_ProducesThreeItems() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_1", "func_a", new Dictionary { ["a"] = 1 })] }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Processing...")] }, + new AgentResponseUpdate { Contents = [new FunctionCallContent("call_2", "func_b", new Dictionary { ["b"] = 2 })] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Equal(3, events.OfType().Count()); + } + + // ===== Workflow content flow tests (W series) ===== + // These simulate the exact update patterns that WorkflowSession.InvokeStageAsync() produces + // when wrapping a Workflow as an AIAgent via AsAIAgent(). + + // W-01: Multi-executor text output — different MessageIds cause separate messages + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultiExecutorTextOutput_CreatesSeparateMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Executor 1 invoked (RawRepresentation) + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + // Executor 1 produces text (unwrapped AgentResponseUpdateEvent) + new AgentResponseUpdate { MessageId = "msg_agent1", Contents = [new MeaiTextContent("Hello from agent 1")] }, + // Executor 1 completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + // Executor 2 invoked + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + // Executor 2 produces text (different MessageId) + new AgentResponseUpdate { MessageId = "msg_agent2", Contents = [new MeaiTextContent("Hello from agent 2")] }, + // Executor 2 completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 2 workflow action items (invoked) + 1 text message + 2 workflow action items (completed) + 1 text message = 6 output items + Assert.Equal(6, events.OfType().Count()); + // 2 text deltas (one per agent) + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-02: Workflow error via ErrorContent (as produced by WorkflowSession for WorkflowErrorEvent) + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowErrorAsContent_EmitsFailed() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting work...")] }, + // WorkflowErrorEvent is converted to ErrorContent by WorkflowSession + new AgentResponseUpdate { Contents = [new ErrorContent("Workflow execution failed")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should close the open message, then emit failed + Assert.True(events.OfType().Any()); + Assert.IsType(events[^1]); + Assert.DoesNotContain(events, e => e is ResponseCompletedEvent); + } + + // W-03: Function call from workflow executor (e.g. handoff agent calling transfer_to_agent) + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowFunctionCall_EmitsFunctionCallEvents() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("triage_agent", "start") }, + // Agent produces function call (handoff) + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_handoff", "transfer_to_code_expert", + new Dictionary { ["reason"] = "User asked about code" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("triage_agent", null) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("code_expert", "start") }, + new AgentResponseUpdate { MessageId = "msg_expert", Contents = [new MeaiTextContent("Here's how async/await works...")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("code_expert", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Should have: 4 workflow actions + 1 function call + 1 text message = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + Assert.Contains(events, e => e is ResponseTextDeltaEvent); + Assert.IsType(events[^1]); + } + + // W-04: Informational events (superstep, workflow started) are silently skipped + [Fact] + public async Task ConvertUpdatesToEventsAsync_InformationalWorkflowEvents_AreSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("start") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Result")] }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Only one output item (the text message), no workflow action items for informational events + Assert.Single(events.OfType()); + Assert.Contains(events, e => e is ResponseTextDeltaEvent); + Assert.IsType(events[^1]); + } + + // W-05: Warning events are silently skipped + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowWarningEvent_IsSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowWarningEvent("Agent took too long") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Done")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + // W-06: Streaming text from multiple workflow turns (same executor, different message IDs) + [Fact] + public async Task ConvertUpdatesToEventsAsync_MultiTurnSameExecutor_CreatesSeparateMessages() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_turn1", Contents = [new MeaiTextContent("First response")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + // Same executor invoked again (second superstep) + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_turn2", Contents = [new MeaiTextContent("Second response")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow action items + 2 text messages = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + } + + // W-07: Executor failure mid-stream with partial text + [Fact] + public async Task ConvertUpdatesToEventsAsync_ExecutorFailureAfterPartialText_ClosesMessageAndEmitsFailure() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Starting to process...")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorFailedEvent("agent_1", new InvalidOperationException("Agent crashed")) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Text message should be closed before the failed workflow action item + Assert.True(events.OfType().Any()); + // Workflow action items: invoked + failed = 2, plus text message = 3 + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-08: Full handoff pattern — triage → function call → target agent text + [Fact] + public async Task ConvertUpdatesToEventsAsync_FullHandoffPattern_ProducesCorrectEventSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Workflow lifecycle + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("triage", "start") }, + // Triage agent decides to hand off + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_1", "transfer_to_expert", + new Dictionary { ["reason"] = "technical question" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("triage", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Next superstep + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("expert", "start") }, + // Expert agent responds with text + new AgentResponseUpdate { MessageId = "msg_expert_1", Contents = [new MeaiTextContent("Let me explain...")] }, + new AgentResponseUpdate { MessageId = "msg_expert_1", Contents = [new MeaiTextContent(" Here's how it works.")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("expert", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: invoked triage, completed triage, invoked expert, completed expert = 4 + // Content items: 1 function call, 1 text message = 2 + // Total output items: 6 + Assert.Equal(6, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + // Two text deltas for the two streaming chunks + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-09: SubworkflowErrorEvent treated as informational (error content comes separately) + [Fact] + public async Task ConvertUpdatesToEventsAsync_SubworkflowErrorEvent_IsSkipped() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SubworkflowErrorEvent("sub_1", new InvalidOperationException("sub failed")) }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Recovered")] }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // SubworkflowErrorEvent extends WorkflowErrorEvent which falls through to default skip + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + // W-10: Mixed content types from workflow — reasoning + text + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowReasoningThenText_ProducesCorrectSequence() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("thinking_agent", "start") }, + // Agent produces reasoning content + new AgentResponseUpdate { Contents = [new TextReasoningContent("Analyzing the problem...")] }, + // Then text response + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("The answer is 42")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("thinking_agent", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: 2 (invoked + completed), reasoning: 1, text message: 1 = 4 output items + Assert.Equal(4, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // W-11: Usage content accumulated across workflow executors + [Fact] + public async Task ConvertUpdatesToEventsAsync_WorkflowUsageAcrossExecutors_AccumulatesCorrectly() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("Response 1")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 100, OutputTokenCount = 50, TotalTokenCount = 150 })] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + new AgentResponseUpdate { MessageId = "msg_2", Contents = [new MeaiTextContent("Response 2")] }, + new AgentResponseUpdate { Contents = [new UsageContent(new UsageDetails { InputTokenCount = 200, OutputTokenCount = 100, TotalTokenCount = 300 })] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Usage should be accumulated in the completed event + Assert.IsType(events[^1]); + } + + // W-12: Empty workflow — only lifecycle events, no content + [Fact] + public async Task ConvertUpdatesToEventsAsync_EmptyWorkflowOnlyLifecycle_EmitsOnlyCompleted() + { + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("start") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Only the terminal completed event + Assert.Single(events); + Assert.IsType(events[0]); + } + + private static async IAsyncEnumerable ToAsync(IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + + await Task.CompletedTask; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..9cf45d70af --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Azure.AI.AgentServer.Responses; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddAgentFrameworkHandler_RegistersResponseHandler() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddAgentFrameworkHandler(); + + var descriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(ResponseHandler)); + Assert.NotNull(descriptor); + Assert.Equal(typeof(AgentFrameworkResponseHandler), descriptor.ImplementationType); + } + + [Fact] + public void AddAgentFrameworkHandler_CalledTwice_RegistersOnce() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddAgentFrameworkHandler(); + services.AddAgentFrameworkHandler(); + + var count = services.Count(d => d.ServiceType == typeof(ResponseHandler)); + Assert.Equal(1, count); + } + + [Fact] + public void AddAgentFrameworkHandler_NullServices_ThrowsArgumentNullException() + { + Assert.Throws( + () => AgentFrameworkResponsesServiceCollectionExtensions.AddAgentFrameworkHandler(null!)); + } + + [Fact] + public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() + { + var services = new ServiceCollection(); + services.AddLogging(); + var mockAgent = new Mock(); + + services.AddAgentFrameworkHandler(mockAgent.Object); + + var handlerDescriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(ResponseHandler)); + Assert.NotNull(handlerDescriptor); + + var agentDescriptor = services.FirstOrDefault( + d => d.ServiceType == typeof(AIAgent)); + Assert.NotNull(agentDescriptor); + } + + [Fact] + public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() + { + var services = new ServiceCollection(); + Assert.Throws( + () => services.AddAgentFrameworkHandler((AIAgent)null!)); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs new file mode 100644 index 0000000000..e5f9bb0405 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs @@ -0,0 +1,509 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; + +/// +/// Integration tests that verify workflow execution through the +/// pipeline. +/// These use real workflow builders and the InProcessExecution environment +/// to produce authentic streaming event patterns. +/// +public class WorkflowIntegrationTests +{ + private static string ValidResponseId => "resp_" + new string('0', 46); + + // ===== Sequential Workflow Tests ===== + + [Fact] + public async Task SequentialWorkflow_SingleAgent_ProducesTextOutput() + { + // Arrange: single-agent sequential workflow + var echoAgent = new StreamingTextAgent("echo", "Hello from the workflow!"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential", echoAgent); + var workflowAgent = workflow.AsAIAgent( + id: "workflow-agent", + name: "Test Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + at least one text output + terminal + Assert.IsType(events[0]); + Assert.IsType(events[1]); + Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); + + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + [Fact] + public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBoth() + { + // Arrange: two agents in sequence + var agent1 = new StreamingTextAgent("agent1", "First agent says hello"); + var agent2 = new StreamingTextAgent("agent2", "Second agent says goodbye"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential-2", agent1, agent2); + var workflowAgent = workflow.AsAIAgent( + id: "seq-workflow", + name: "Sequential Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Process this"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have workflow action events for executor lifecycle + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + + // Should have output item events (either text messages or workflow actions) + Assert.True(events.OfType().Any(), + "Expected at least one output item from the workflow"); + } + + // ===== Workflow Error Propagation ===== + + [Fact] + public async Task Workflow_AgentThrowsException_ProducesErrorOutput() + { + // Arrange: workflow with an agent that throws + var throwingAgent = new ThrowingStreamingAgent("thrower", new InvalidOperationException("Agent crashed")); + var workflow = AgentWorkflowBuilder.BuildSequential("test-error", throwingAgent); + var workflowAgent = workflow.AsAIAgent( + id: "error-workflow", + name: "Error Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Trigger error"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + error/failure indicator + Assert.IsType(events[0]); + Assert.IsType(events[1]); + + var lastEvent = events[^1]; + // Workflow errors surface as either Failed or Completed (depending on error handling) + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + // ===== Workflow Action Lifecycle Events ===== + + [Fact] + public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItems() + { + // Arrange + var agent = new StreamingTextAgent("test-agent", "Result"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-actions", agent); + var workflowAgent = workflow.AsAIAgent( + id: "actions-workflow", + name: "Actions Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: workflow should produce OutputItemAdded events for executor lifecycle + var addedEvents = events.OfType().ToList(); + Assert.True(addedEvents.Count >= 1, + $"Expected at least 1 output item added event, got {addedEvents.Count}"); + } + + // ===== Keyed Workflow Registration ===== + + [Fact] + public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() + { + // Arrange: workflow agent registered with a keyed service name + var agent = new StreamingTextAgent("inner", "Keyed workflow response"); + var workflow = AgentWorkflowBuilder.BuildSequential("keyed-wf", agent); + var workflowAgent = workflow.AsAIAgent( + id: "keyed-workflow", + name: "Keyed Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("my-workflow", workflowAgent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse( + model: "test", + agentReference: new AgentReference("my-workflow")); + request.Input = CreateUserInput("Test keyed workflow"); + var mockContext = CreateMockContext(); + + // Act + var events = await CollectEventsAsync(handler, request, mockContext.Object); + + // Assert + Assert.IsType(events[0]); + Assert.True(events.Count >= 3, $"Expected at least 3 events, got {events.Count}"); + } + + // ===== OutputConverter Direct Workflow Pattern Tests ===== + // These test the OutputConverter directly with update patterns that mirror real workflows. + + [Fact] + public async Task OutputConverter_SequentialWorkflowPattern_ProducesCorrectEvents() + { + // Simulate what WorkflowSession produces for a 2-agent sequential workflow + var (stream, _) = CreateTestStream(); + var updates = new[] + { + // Superstep 1: Agent 1 + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") }, + new AgentResponseUpdate { MessageId = "msg_a1", Contents = [new MeaiTextContent("Agent 1 output")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Superstep 2: Agent 2 + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") }, + new AgentResponseUpdate { MessageId = "msg_a2", Contents = [new MeaiTextContent("Agent 2 output")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow action items + 2 text messages = 6 output items + Assert.Equal(6, events.OfType().Count()); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_GroupChatPattern_ProducesCorrectEvents() + { + // Simulate round-robin group chat: agent1 → agent2 → agent1 → terminate + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_1", Contents = [new MeaiTextContent("Agent 1 turn 1")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_2", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_2", Contents = [new MeaiTextContent("Agent 2 turn 1")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_2", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(3) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") }, + new AgentResponseUpdate { MessageId = "msg_gc_3", Contents = [new MeaiTextContent("Agent 1 turn 2")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(3) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 6 workflow actions + 3 text messages = 9 output items + Assert.Equal(9, events.OfType().Count()); + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_CodeExecutorPattern_ProducesCorrectEvents() + { + // Simulate a code-based FunctionExecutor: invoked → completed, no text content + // (code executors don't produce AgentResponseUpdateEvent, just executor lifecycle) + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("uppercase_fn", "hello") }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("uppercase_fn", "HELLO") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + // Second executor uses the output + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) }, + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("format_agent", "start") }, + new AgentResponseUpdate { MessageId = "msg_fmt", Contents = [new MeaiTextContent("Formatted: HELLO")] }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("format_agent", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 4 workflow actions + 1 text message = 5 output items + Assert.Equal(5, events.OfType().Count()); + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_SubworkflowPattern_ProducesCorrectEvents() + { + // Simulate a parent workflow that invokes a sub-workflow executor + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("parent") }, + new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) }, + // Sub-workflow executor invoked + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("sub_workflow_host", "start") }, + // Inner agent within sub-workflow produces text (unwrapped by WorkflowSession) + new AgentResponseUpdate { MessageId = "msg_sub_1", Contents = [new MeaiTextContent("Sub-workflow agent output")] }, + // Sub-workflow executor completed + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("sub_workflow_host", null) }, + new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // 2 workflow actions + 1 text message = 3 output items + Assert.Equal(3, events.OfType().Count()); + Assert.Single(events.OfType()); + Assert.IsType(events[^1]); + } + + [Fact] + public async Task OutputConverter_WorkflowWithMultipleContentTypes_HandlesAllCorrectly() + { + // Simulate a workflow producing reasoning, text, function calls, and usage + var (stream, _) = CreateTestStream(); + var updates = new[] + { + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("planner", "start") }, + // Reasoning + new AgentResponseUpdate { Contents = [new TextReasoningContent("Let me think about this...")] }, + // Function call (tool use) + new AgentResponseUpdate + { + Contents = [new FunctionCallContent("call_search", "web_search", + new Dictionary { ["query"] = "latest news" })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("planner", null) }, + // Next executor uses tool result + new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("writer", "start") }, + new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("Based on my research, ")] }, + new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("here are the findings.")] }, + new AgentResponseUpdate + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 500, OutputTokenCount = 200, TotalTokenCount = 700 })] + }, + new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("writer", null) }, + }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream)) + { + events.Add(evt); + } + + // Workflow actions: 4 (2 invoked + 2 completed) + // Content: 1 reasoning + 1 function call + 1 text message = 3 + // Total: 7 output items + Assert.Equal(7, events.OfType().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + // ===== Helpers ===== + + private static (AgentFrameworkResponseHandler handler, CreateResponse request, ResponseContext context) + CreateHandlerWithAgent(AIAgent agent, string userMessage) + { + var services = new ServiceCollection(); + services.AddSingleton(agent); + services.AddSingleton>(NullLogger.Instance); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test"); + request.Input = CreateUserInput(userMessage); + var mockContext = CreateMockContext(); + + return (handler, request, mockContext.Object); + } + + private static BinaryData CreateUserInput(string text) + { + return BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_in_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text } } + } + }); + } + + private static Mock CreateMockContext() + { + var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mock.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mock.Setup(x => x.GetInputItemsAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + return mock; + } + + private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() + { + var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "test-model"); + var stream = new ResponseEventStream(mockContext.Object, request); + return (stream, mockContext); + } + + private static async Task> CollectEventsAsync( + AgentFrameworkResponseHandler handler, + CreateResponse request, + ResponseContext context) + { + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, context, CancellationToken.None)) + { + events.Add(evt); + } + + return events; + } + + private static async IAsyncEnumerable ToAsync(IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + + await Task.CompletedTask; + } + + // ===== Test Agent Types ===== + + /// + /// A test agent that streams a single text update. + /// + private sealed class StreamingTextAgent(string id, string responseText) : AIAgent + { + public new string Id => id; + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + yield return new AgentResponseUpdate + { + MessageId = $"msg_{id}", + Contents = [new MeaiTextContent(responseText)] + }; + + await Task.CompletedTask; + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } + + /// + /// A test agent that always throws an exception during streaming. + /// + private sealed class ThrowingStreamingAgent(string id, Exception exception) : AIAgent + { + public new string Id => id; + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw exception; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + } +} From e5c1203f4dc234281d7d1183b518ce39e5d85b6e Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 1 Apr 2026 08:29:42 -0700 Subject: [PATCH 32/75] Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../OutputConverter.cs | 8 ++++---- .../ServiceCollectionExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs index c620cf6324..f6b9aed006 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -244,9 +244,9 @@ private static bool IsSameMessage(string? currentId, string? previousId) => private static ResponseUsage ConvertUsage(UsageDetails details, ResponseUsage? existing) { - var inputTokens = (long)(details.InputTokenCount ?? 0); - var outputTokens = (long)(details.OutputTokenCount ?? 0); - var totalTokens = (long)(details.TotalTokenCount ?? 0); + var inputTokens = details.InputTokenCount ?? 0; + var outputTokens = details.OutputTokenCount ?? 0; + var totalTokens = details.TotalTokenCount ?? 0; if (existing is not null) { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs index d596ba3457..c06353a8ec 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; From 15e36efe39686813f59708ada45c4200c0d3f4c9 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 08:51:05 -0700 Subject: [PATCH 33/75] Clean up tests and sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FoundryResponsesHosting/Pages.cs | 2 +- .../FoundryResponsesHosting/Program.cs | 2 +- .../ResponseStreamValidator.cs | 230 +++++++++--------- .../AgentFrameworkResponseHandlerTests.cs | 24 +- .../InputConverterTests.cs | 4 +- .../OutputConverterTests.cs | 8 +- .../ServiceCollectionExtensionsTests.cs | 4 +- .../WorkflowIntegrationTests.cs | 8 +- 8 files changed, 138 insertions(+), 144 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs index bff2c62e99..916b0fdf17 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. /// /// Static HTML pages served by the sample application. diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 32f39f641c..88af464ac7 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -16,8 +16,8 @@ // - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o") using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.AI.AgentServer.Responses; +using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs index 72da677f45..5dc1b4c791 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Text.Json; using System.Text.Json.Serialization; @@ -36,7 +36,7 @@ internal sealed class ResponseStreamValidator private bool _hasTerminal; /// All violations found so far. - internal IReadOnlyList Violations => _violations; + internal IReadOnlyList Violations => this._violations; /// /// Processes a single SSE event line pair (event type + JSON data). @@ -52,33 +52,33 @@ internal void ProcessEvent(string eventType, string jsonData) } catch (JsonException ex) { - Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); + this.Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}"); return; } - _eventCount++; + this._eventCount++; // ── Sequence number validation ────────────────────────────────── if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number) { int seq = seqProp.GetInt32(); - if (seq != _expectedSequenceNumber) + if (seq != this._expectedSequenceNumber) { - Fail("SEQ-01", $"Expected sequence_number {_expectedSequenceNumber}, got {seq}"); + this.Fail("SEQ-01", $"Expected sequence_number {this._expectedSequenceNumber}, got {seq}"); } - _expectedSequenceNumber = seq + 1; + this._expectedSequenceNumber = seq + 1; } - else if (_state != StreamState.Initial || eventType != "error") + else if (this._state != StreamState.Initial || eventType != "error") { // Pre-creation error events may not have sequence_number - Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); + this.Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'"); } // ── Post-terminal guard ───────────────────────────────────────── - if (_hasTerminal) + if (this._hasTerminal) { - Fail("TERM-01", $"Event '{eventType}' received after terminal event"); + this.Fail("TERM-01", $"Event '{eventType}' received after terminal event"); return; } @@ -86,93 +86,93 @@ internal void ProcessEvent(string eventType, string jsonData) switch (eventType) { case "response.created": - ValidateResponseCreated(data); + this.ValidateResponseCreated(data); break; case "response.queued": - ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); - ValidateResponseEnvelope(data, eventType); + this.ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued); + this.ValidateResponseEnvelope(data, eventType); break; case "response.in_progress": - if (_state is StreamState.Created or StreamState.Queued) + if (this._state is StreamState.Created or StreamState.Queued) { - _state = StreamState.InProgress; + this._state = StreamState.InProgress; } else { - Fail("ORDER-02", $"'response.in_progress' received in state {_state} (expected Created or Queued)"); + this.Fail("ORDER-02", $"'response.in_progress' received in state {this._state} (expected Created or Queued)"); } - ValidateResponseEnvelope(data, eventType); + this.ValidateResponseEnvelope(data, eventType); break; case "response.output_item.added": case "output_item.added": - ValidateInProgress(eventType); - ValidateOutputItemAdded(data); + this.ValidateInProgress(eventType); + this.ValidateOutputItemAdded(data); break; case "response.output_item.done": case "output_item.done": - ValidateInProgress(eventType); - ValidateOutputItemDone(data); + this.ValidateInProgress(eventType); + this.ValidateOutputItemDone(data); break; case "response.content_part.added": case "content_part.added": - ValidateInProgress(eventType); - ValidateContentPartAdded(data); + this.ValidateInProgress(eventType); + this.ValidateContentPartAdded(data); break; case "response.content_part.done": case "content_part.done": - ValidateInProgress(eventType); - ValidateContentPartDone(data); + this.ValidateInProgress(eventType); + this.ValidateContentPartDone(data); break; case "response.output_text.delta": case "output_text.delta": - ValidateInProgress(eventType); - ValidateTextDelta(data); + this.ValidateInProgress(eventType); + this.ValidateTextDelta(data); break; case "response.output_text.done": case "output_text.done": - ValidateInProgress(eventType); - ValidateTextDone(data); + this.ValidateInProgress(eventType); + this.ValidateTextDone(data); break; case "response.function_call_arguments.delta": case "function_call_arguments.delta": - ValidateInProgress(eventType); + this.ValidateInProgress(eventType); break; case "response.function_call_arguments.done": case "function_call_arguments.done": - ValidateInProgress(eventType); + this.ValidateInProgress(eventType); break; case "response.completed": - ValidateTerminal(data, "completed"); + this.ValidateTerminal(data, "completed"); break; case "response.failed": - ValidateTerminal(data, "failed"); + this.ValidateTerminal(data, "failed"); break; case "response.incomplete": - ValidateTerminal(data, "incomplete"); + this.ValidateTerminal(data, "incomplete"); break; case "error": // Pre-creation error — standalone, no response.created precedes it - if (_state != StreamState.Initial) + if (this._state != StreamState.Initial) { - Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); + this.Fail("ERR-01", "'error' event received after response.created — should use response.failed instead"); } - _hasTerminal = true; + this._hasTerminal = true; break; default: @@ -186,31 +186,31 @@ internal void ProcessEvent(string eventType, string jsonData) /// internal void Complete() { - if (!_hasTerminal && _state != StreamState.Initial) + if (!this._hasTerminal && this._state != StreamState.Initial) { - Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); + this.Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)"); } - if (_state == StreamState.Initial && _eventCount == 0) + if (this._state == StreamState.Initial && this._eventCount == 0) { - Fail("EMPTY-01", "No events received in the stream"); + this.Fail("EMPTY-01", "No events received in the stream"); } // Check for output items that were added but never completed - foreach (int idx in _addedItemIndices) + foreach (int idx in this._addedItemIndices) { - if (!_doneItemIndices.Contains(idx)) + if (!this._doneItemIndices.Contains(idx)) { - Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); + this.Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done"); } } // Check for content parts that were added but never completed - foreach (string key in _addedContentParts) + foreach (string key in this._addedContentParts) { - if (!_doneContentParts.Contains(key)) + if (!this._doneContentParts.Contains(key)) { - Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); + this.Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done"); } } } @@ -221,9 +221,9 @@ internal void Complete() internal ValidationResult GetResult() { return new ValidationResult( - EventCount: _eventCount, - IsValid: _violations.Count == 0, - Violations: [.. _violations]); + EventCount: this._eventCount, + IsValid: this._violations.Count == 0, + Violations: [.. this._violations]); } // ═══════════════════════════════════════════════════════════════════════ @@ -232,28 +232,28 @@ internal ValidationResult GetResult() private void ValidateResponseCreated(JsonElement data) { - if (_state != StreamState.Initial) + if (this._state != StreamState.Initial) { - Fail("ORDER-01", $"'response.created' received in state {_state} (expected Initial — must be first event)"); + this.Fail("ORDER-01", $"'response.created' received in state {this._state} (expected Initial — must be first event)"); return; } - _state = StreamState.Created; + this._state = StreamState.Created; // Must have a response envelope if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", "'response.created' missing 'response' object"); + this.Fail("FIELD-01", "'response.created' missing 'response' object"); return; } // Required response fields - ValidateRequiredResponseFields(resp, "response.created"); + this.ValidateRequiredResponseFields(resp, "response.created"); // Capture response ID for cross-event checks if (resp.TryGetProperty("id", out var idProp)) { - _responseId = idProp.GetString(); + this._responseId = idProp.GetString(); } // Status must be non-terminal @@ -262,28 +262,28 @@ private void ValidateResponseCreated(JsonElement data) string? status = statusProp.GetString(); if (status is "completed" or "failed" or "incomplete" or "cancelled") { - Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); + this.Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'"); } } } private void ValidateTerminal(JsonElement data, string expectedKind) { - if (_state is StreamState.Initial or StreamState.Created) + if (this._state is StreamState.Initial or StreamState.Created) { - Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); + this.Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'"); } - _hasTerminal = true; - _state = StreamState.Terminal; + this._hasTerminal = true; + this._state = StreamState.Terminal; if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); + this.Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object"); return; } - ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); + this.ValidateRequiredResponseFields(resp, $"response.{expectedKind}"); if (resp.TryGetProperty("status", out var statusProp)) { @@ -295,12 +295,12 @@ private void ValidateTerminal(JsonElement data, string expectedKind) if (status == "completed" && !hasCompletedAt) { - Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); + this.Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'"); } if (status != "completed" && hasCompletedAt) { - Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); + this.Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'"); } // error field validation @@ -309,39 +309,39 @@ private void ValidateTerminal(JsonElement data, string expectedKind) if (status == "failed" && !hasError) { - Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); + this.Fail("FIELD-04", "'error' must be non-null when status is 'failed'"); } if (status is "completed" or "incomplete" && hasError) { - Fail("FIELD-05", $"'error' must be null when status is '{status}'"); + this.Fail("FIELD-05", $"'error' must be null when status is '{status}'"); } // error structure validation if (hasError) { - ValidateErrorObject(errProp, $"response.{expectedKind}"); + this.ValidateErrorObject(errProp, $"response.{expectedKind}"); } // cancelled output must be empty (B11) if (status == "cancelled" && resp.TryGetProperty("output", out var outputProp) && outputProp.ValueKind == JsonValueKind.Array && outputProp.GetArrayLength() > 0) { - Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); + this.Fail("CANCEL-01", "Cancelled response must have empty output array (B11)"); } // response ID consistency - if (_responseId is not null && resp.TryGetProperty("id", out var idProp) - && idProp.GetString() != _responseId) + if (this._responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != this._responseId) { - Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'"); } } // Usage validation (optional, but if present must be structured correctly) if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) { - ValidateUsage(usageProp, $"response.{expectedKind}"); + this.ValidateUsage(usageProp, $"response.{expectedKind}"); } } @@ -350,19 +350,19 @@ private void ValidateOutputItemAdded(JsonElement data) if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) { int index = idxProp.GetInt32(); - if (!_addedItemIndices.Add(index)) + if (!this._addedItemIndices.Add(index)) { - Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); + this.Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}"); } } else { - Fail("FIELD-06", "output_item.added missing 'output_index' field"); + this.Fail("FIELD-06", "output_item.added missing 'output_index' field"); } if (!data.TryGetProperty("item", out _)) { - Fail("FIELD-07", "output_item.added missing 'item' object"); + this.Fail("FIELD-07", "output_item.added missing 'item' object"); } } @@ -371,37 +371,37 @@ private void ValidateOutputItemDone(JsonElement data) if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number) { int index = idxProp.GetInt32(); - if (!_addedItemIndices.Contains(index)) + if (!this._addedItemIndices.Contains(index)) { - Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); + this.Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added"); } - _doneItemIndices.Add(index); + this._doneItemIndices.Add(index); } else { - Fail("FIELD-06", "output_item.done missing 'output_index' field"); + this.Fail("FIELD-06", "output_item.done missing 'output_index' field"); } } private void ValidateContentPartAdded(JsonElement data) { string key = GetContentPartKey(data); - if (!_addedContentParts.Add(key)) + if (!this._addedContentParts.Add(key)) { - Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); + this.Fail("CONTENT-01", $"Duplicate content_part.added for {key}"); } } private void ValidateContentPartDone(JsonElement data) { string key = GetContentPartKey(data); - if (!_addedContentParts.Contains(key)) + if (!this._addedContentParts.Contains(key)) { - Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); + this.Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added"); } - _doneContentParts.Add(key); + this._doneContentParts.Add(key); } private void ValidateTextDelta(JsonElement data) @@ -411,13 +411,13 @@ private void ValidateTextDelta(JsonElement data) ? deltaProp.GetString() ?? string.Empty : string.Empty; - if (!_textAccumulators.TryGetValue(key, out string? existing)) + if (!this._textAccumulators.TryGetValue(key, out string? existing)) { - _textAccumulators[key] = delta; + this._textAccumulators[key] = delta; } else { - _textAccumulators[key] = existing + delta; + this._textAccumulators[key] = existing + delta; } } @@ -430,13 +430,13 @@ private void ValidateTextDone(JsonElement data) if (finalText is null) { - Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); + this.Fail("TEXT-01", $"output_text.done for {key} missing 'text' field"); return; } - if (_textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) + if (this._textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText) { - Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); + this.Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)"); } } @@ -448,34 +448,34 @@ private void ValidateRequiredResponseFields(JsonElement resp, string context) { if (!HasNonNullString(resp, "id")) { - Fail("FIELD-01", $"{context}: response missing 'id'"); + this.Fail("FIELD-01", $"{context}: response missing 'id'"); } if (resp.TryGetProperty("object", out var objProp)) { if (objProp.GetString() != "response") { - Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); + this.Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'"); } } else { - Fail("FIELD-08", $"{context}: response missing 'object' field"); + this.Fail("FIELD-08", $"{context}: response missing 'object' field"); } if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null) { - Fail("FIELD-09", $"{context}: response missing 'created_at'"); + this.Fail("FIELD-09", $"{context}: response missing 'created_at'"); } if (!resp.TryGetProperty("status", out _)) { - Fail("FIELD-10", $"{context}: response missing 'status'"); + this.Fail("FIELD-10", $"{context}: response missing 'status'"); } if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array) { - Fail("FIELD-11", $"{context}: response missing 'output' array"); + this.Fail("FIELD-11", $"{context}: response missing 'output' array"); } } @@ -483,12 +483,12 @@ private void ValidateErrorObject(JsonElement error, string context) { if (!HasNonNullString(error, "code")) { - Fail("ERR-02", $"{context}: error object missing 'code' field"); + this.Fail("ERR-02", $"{context}: error object missing 'code' field"); } if (!HasNonNullString(error, "message")) { - Fail("ERR-03", $"{context}: error object missing 'message' field"); + this.Fail("ERR-03", $"{context}: error object missing 'message' field"); } } @@ -496,17 +496,17 @@ private void ValidateUsage(JsonElement usage, string context) { if (!usage.TryGetProperty("input_tokens", out _)) { - Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); + this.Fail("USAGE-01", $"{context}: usage missing 'input_tokens'"); } if (!usage.TryGetProperty("output_tokens", out _)) { - Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); + this.Fail("USAGE-02", $"{context}: usage missing 'output_tokens'"); } if (!usage.TryGetProperty("total_tokens", out _)) { - Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); + this.Fail("USAGE-03", $"{context}: usage missing 'total_tokens'"); } } @@ -514,17 +514,17 @@ private void ValidateResponseEnvelope(JsonElement data, string eventType) { if (!data.TryGetProperty("response", out var resp)) { - Fail("FIELD-01", $"'{eventType}' missing 'response' object"); + this.Fail("FIELD-01", $"'{eventType}' missing 'response' object"); return; } - ValidateRequiredResponseFields(resp, eventType); + this.ValidateRequiredResponseFields(resp, eventType); // Response ID consistency - if (_responseId is not null && resp.TryGetProperty("id", out var idProp) - && idProp.GetString() != _responseId) + if (this._responseId is not null && resp.TryGetProperty("id", out var idProp) + && idProp.GetString() != this._responseId) { - Fail("ID-01", $"Response ID changed: was '{_responseId}', now '{idProp.GetString()}'"); + this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'"); } } @@ -534,27 +534,27 @@ private void ValidateResponseEnvelope(JsonElement data, string eventType) private void ValidateInProgress(string eventType) { - if (_state != StreamState.InProgress) + if (this._state != StreamState.InProgress) { - Fail("ORDER-04", $"'{eventType}' received in state {_state} (expected InProgress)"); + this.Fail("ORDER-04", $"'{eventType}' received in state {this._state} (expected InProgress)"); } } private void ValidateStateTransition(string eventType, StreamState expected, StreamState next) { - if (_state != expected) + if (this._state != expected) { - Fail("ORDER-05", $"'{eventType}' received in state {_state} (expected {expected})"); + this.Fail("ORDER-05", $"'{eventType}' received in state {this._state} (expected {expected})"); } else { - _state = next; + this._state = next; } } private void Fail(string ruleId, string message) { - _violations.Add(new ValidationViolation(ruleId, message, _eventCount)); + this._violations.Add(new ValidationViolation(ruleId, message, this._eventCount)); } private static bool HasNonNullString(JsonElement obj, string property) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs index ee32771a67..10d1271d10 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -20,8 +20,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; public class AgentFrameworkResponseHandlerTests { - private static string ValidResponseId => "resp_" + new string('0', 46); - [Fact] public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() { @@ -688,13 +686,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -721,13 +719,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -743,8 +741,8 @@ protected override IAsyncEnumerable RunCoreStreamingAsync( AgentRunOptions? options, CancellationToken cancellationToken = default) { - CapturedMessages = messages.ToList(); - CapturedOptions = options; + this.CapturedMessages = messages.ToList(); + this.CapturedOptions = options; return ToAsyncEnumerableAsync(new AgentResponseUpdate { MessageId = "resp_msg_1", @@ -765,13 +763,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } @@ -802,13 +800,13 @@ protected override ValueTask CreateSessionCoreAsync( protected override ValueTask SerializeSessionCoreAsync( AgentSession session, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, - System.Text.Json.JsonSerializerOptions? jsonSerializerOptions, + JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs index 34555798a7..85748214f1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs @@ -1,9 +1,7 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Linq; -using System.Text.Json; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs index dde11298c7..f139e3bf71 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Extensions.AI; using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; @@ -233,7 +233,7 @@ public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitte public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted() { var (stream, _) = CreateTestStream(); - var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent((string)null!)] }; + var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent(null!)] }; var events = new List(); await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) @@ -314,7 +314,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_Emit var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { - Contents = [new FunctionCallContent("call_1", "do_something", (IDictionary?)null)] + Contents = [new FunctionCallContent("call_1", "do_something", null)] }; var events = new List(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs index 9cf45d70af..2be9dc0bc8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; @@ -67,6 +67,6 @@ public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assert.Throws( - () => services.AddAgentFrameworkHandler((AIAgent)null!)); + () => services.AddAgentFrameworkHandler(null!)); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs index e5f9bb0405..e8f035ba85 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -27,8 +27,6 @@ namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; /// public class WorkflowIntegrationTests { - private static string ValidResponseId => "resp_" + new string('0', 46); - // ===== Sequential Workflow Tests ===== [Fact] @@ -156,7 +154,7 @@ public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() executionEnvironment: InProcessExecution.OffThread); var services = new ServiceCollection(); - services.AddKeyedSingleton("my-workflow", workflowAgent); + services.AddKeyedSingleton("my-workflow", workflowAgent); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -357,7 +355,7 @@ private static (AgentFrameworkResponseHandler handler, CreateResponse request, R CreateHandlerWithAgent(AIAgent agent, string userMessage) { var services = new ServiceCollection(); - services.AddSingleton(agent); + services.AddSingleton(agent); services.AddSingleton>(NullLogger.Instance); var sp = services.BuildServiceProvider(); From 5368e530a8cb781a170e4388b8121afccd158b54 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 08:54:14 -0700 Subject: [PATCH 34/75] Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 1f8b894a1b..3613eea80b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,9 @@ - - - + + + From 4dd3a3087505cd742678738581fd6bf5b61a4c03 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 10:59:50 -0700 Subject: [PATCH 35/75] Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 9d91eebf28..e75bfcb9e6 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -7,6 +7,8 @@ $(VersionPrefix)-$(VersionSuffix).260410.1 $(VersionPrefix)-preview.260410.1 $(VersionPrefix) + + 0.9.0-hosted.260402.1 1.1.0 Debug;Release;Publish From 838fd7f4d0e6513490f8d222056ae3e88a571df5 Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 13:31:58 -0700 Subject: [PATCH 36/75] Move Foundry Responses hosting into Microsoft.Agents.AI.Foundry package Move source and test files from the standalone Hosting.AzureAIResponses project into the Foundry package under a Hosting/ subfolder. This consolidates the Foundry-specific hosting adapter into the main Foundry package. - Source: Microsoft.Agents.AI.Foundry.Hosting namespace - Tests: merged into Foundry.UnitTests/Hosting/ - Conditionally compiled for .NETCoreApp TFMs only (net8.0+) - Deleted standalone Hosting.AzureAIResponses project and test project - Updated sample and solution references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 2 - .../FoundryResponsesHosting.csproj | 2 +- .../FoundryResponsesHosting/Program.cs | 4 +- .../Hosting}/AgentFrameworkResponseHandler.cs | 8 +++- .../Hosting}/InputConverter.cs | 7 +++- .../Hosting}/OutputConverter.cs | 8 +++- .../Hosting}/ServiceCollectionExtensions.cs | 5 ++- .../Microsoft.Agents.AI.Foundry.csproj | 22 +++++++++- ....Agents.AI.Hosting.AzureAIResponses.csproj | 40 ------------------- .../AgentFrameworkResponseHandlerTests.cs | 5 ++- .../Hosting}/InputConverterTests.cs | 5 ++- .../Hosting}/OutputConverterTests.cs | 5 ++- .../ServiceCollectionExtensionsTests.cs | 5 ++- .../Hosting}/WorkflowIntegrationTests.cs | 5 ++- ...crosoft.Agents.AI.Foundry.UnitTests.csproj | 14 +++++++ ....Hosting.AzureAIResponses.UnitTests.csproj | 17 -------- 16 files changed, 73 insertions(+), 81 deletions(-) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/AgentFrameworkResponseHandler.cs (97%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/InputConverter.cs (98%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/OutputConverter.cs (98%) rename dotnet/src/{Microsoft.Agents.AI.Hosting.AzureAIResponses => Microsoft.Agents.AI.Foundry/Hosting}/ServiceCollectionExtensions.cs (96%) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/AgentFrameworkResponseHandlerTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/InputConverterTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/OutputConverterTests.cs (99%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/ServiceCollectionExtensionsTests.cs (93%) rename dotnet/tests/{Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests => Microsoft.Agents.AI.Foundry.UnitTests/Hosting}/WorkflowIntegrationTests.cs (99%) delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 3d0465763f..ab74c2650a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -494,7 +494,6 @@ - @@ -541,7 +540,6 @@ - diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj index 6725ef8d3b..3dfab4ad48 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -18,8 +18,8 @@ + - diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 88af464ac7..10a82d1b87 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents // using the Azure AI Responses Server SDK. @@ -21,7 +21,7 @@ using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; -using Microsoft.Agents.AI.Hosting.AzureAIResponses; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs similarity index 97% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 5f07cd4530..acb8e9c556 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -1,13 +1,17 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// A implementation that bridges the Azure AI Responses Server SDK diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index a35c8cd5b8..49bd216a90 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -1,12 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Converts Responses Server SDK input types to agent-framework types. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs similarity index 98% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index f6b9aed006..1fd6672c83 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -1,17 +1,21 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Converts agent-framework streams into diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs similarity index 96% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index c06353a8ec..bae8821745 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses; +namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Extension methods for to register the agent-framework diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj index 670d140043..e3c1773941 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj @@ -3,7 +3,8 @@ true true - $(NoWarn);OPENAI001 + $(NoWarn);OPENAI001;MEAI001;NU1903 + false @@ -20,18 +21,32 @@ true + + + + + + + + + + + + + + Microsoft Agent Framework for Foundry Agents @@ -43,4 +58,9 @@ + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj deleted file mode 100644 index b881e287cc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureAIResponses/Microsoft.Agents.AI.Hosting.AzureAIResponses.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - $(TargetFrameworksCore) - enable - Microsoft.Agents.AI.Hosting.AzureAIResponses - alpha - $(NoWarn);MEAI001;NU1903 - false - - - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index 10d1271d10..dc38c42739 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; @@ -16,7 +17,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class AgentFrameworkResponseHandlerTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index 85748214f1..7f36fd10d7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -1,13 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class InputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs index f139e3bf71..330312d2b4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Agents.AI.Workflows; @@ -12,7 +13,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class OutputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs similarity index 93% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs index 2be9dc0bc8..ee61cef71a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; +using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Microsoft.Extensions.DependencyInjection; using Moq; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class ServiceCollectionExtensionsTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index e8f035ba85..66924bdcba 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +18,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests; +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; /// /// Integration tests that verify workflow execution through the diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 7b85de0384..6473b799bf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -1,10 +1,24 @@ + + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj deleted file mode 100644 index 09c3ba24c1..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests/Microsoft.Agents.AI.Hosting.AzureAIResponses.UnitTests.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - $(TargetFrameworksCore) - false - $(NoWarn);NU1903;NU1605 - - - - - - - - - - - From 4a2e25573f5d020caad04b1cd0552c049a407f8b Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 15:08:34 -0700 Subject: [PATCH 37/75] Bump package version to 0.9.0-hosted.260402.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index e75bfcb9e6..1296a5eb69 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - 0.9.0-hosted.260402.1 + 0.9.0-hosted.260402.2 1.1.0 Debug;Release;Publish From 226abebac287517fe5f75777d020669e98c81cec Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 15:14:43 -0700 Subject: [PATCH 38/75] Bump OpenTelemetry packages to fix NU1109 downgrade errors - OpenTelemetry/Api/Exporter.Console/Exporter.InMemory: 1.13.1 -> 1.15.0 - OpenTelemetry.Exporter.OpenTelemetryProtocol: already 1.15.0 - OpenTelemetry.Extensions.Hosting: already 1.14.0 - OpenTelemetry.Instrumentation.AspNetCore/Http: already 1.14.0 - OpenTelemetry.Instrumentation.Runtime: 1.13.0 -> 1.14.0 - Azure.Monitor.OpenTelemetry.Exporter: 1.4.0 -> 1.5.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 3613eea80b..f3dc77a2dc 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -27,7 +27,7 @@ - + @@ -52,15 +52,15 @@ - - - - - - - - - + + + + + + + + + From eccdcffc6cc60cfc912962d3f23fb5c5dc5ae98e Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 16:39:13 -0700 Subject: [PATCH 39/75] Fix CA1873: guard LogWarning with IsEnabled check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Hosting/AgentFrameworkResponseHandler.cs | 8 +++++--- .../Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs | 3 +-- .../Hosting/OutputConverter.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index acb8e9c556..3423a0b4a3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; @@ -151,7 +150,10 @@ private AIAgent ResolveAgent(CreateResponse request) return agent; } - this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + if (this._logger.IsEnabled(LogLevel.Warning)) + { + this._logger.LogWarning("Agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } } // Try non-keyed default diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index 49bd216a90..e6d607c793 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -1,9 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json; using Azure.AI.AgentServer.Responses.Models; using Microsoft.Extensions.AI; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index 1fd6672c83..79aaf768d9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index bae8821745..d8e8a83f29 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using Azure.AI.AgentServer.Responses; From afce5665e29c74bea29b40353ea94f4b01d0e24c Mon Sep 17 00:00:00 2001 From: alliscode Date: Thu, 2 Apr 2026 19:44:44 -0700 Subject: [PATCH 40/75] Fix model override bug and add client REPL sample - InputConverter: stop propagating request.Model to ChatOptions.ModelId Hosted agents use their own model; client-provided model values like 'hosted-agent' were being passed through and causing server errors. - Add FoundryResponsesRepl sample: interactive CLI client that connects to a Foundry Responses endpoint using ResponsesClient.AsAIAgent() - Bump package version to 0.9.0-hosted.260403.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 6 +- dotnet/nuget/nuget-package.props | 2 +- .../FoundryResponsesRepl.csproj | 21 ++++ .../FoundryResponsesRepl/Program.cs | 98 +++++++++++++++++++ .../Properties/launchSettings.json | 12 +++ .../Hosting/InputConverter.cs | 5 +- .../Hosting/InputConverterTests.cs | 7 +- 7 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index ab74c2650a..5d746ccd97 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -1,4 +1,4 @@ - + @@ -260,7 +260,9 @@ - + + + diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 1296a5eb69..72b2d695fc 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - 0.9.0-hosted.260402.2 + 0.9.0-hosted.260403.1 1.1.0 Debug;Release;Publish diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj b/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj new file mode 100644 index 0000000000..15cdf820eb --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + false + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs new file mode 100644 index 0000000000..be0e6ccf7b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs @@ -0,0 +1,98 @@ +// Foundry Responses Client REPL +// +// Connects to a Foundry Responses agent running on a given endpoint +// and provides an interactive multi-turn chat REPL. +// +// Usage: +// dotnet run (connects to http://localhost:8088) +// dotnet run -- --endpoint http://localhost:9090 +// dotnet run -- --endpoint https://my-foundry-project.services.ai.azure.com +// +// The endpoint should be running a Foundry Responses server (POST /responses). + +using System.ClientModel; +using Microsoft.Agents.AI; +using OpenAI; +using OpenAI.Responses; + +// ── Parse args ──────────────────────────────────────────────────────────────── + +string endpointUrl = "http://localhost:8088"; +for (int i = 0; i < args.Length - 1; i++) +{ + if (args[i] is "--endpoint" or "-e") + { + endpointUrl = args[i + 1]; + } +} + +// ── Create an agent-framework agent backed by the remote Responses endpoint ── + +// The OpenAI SDK's ResponsesClient can target any OpenAI-compatible endpoint. +// We use a dummy API key since our local server doesn't require auth. +var credential = new ApiKeyCredential( + Environment.GetEnvironmentVariable("RESPONSES_API_KEY") ?? "no-key-needed"); + +var openAiClient = new OpenAIClient( + credential, + new OpenAIClientOptions { Endpoint = new Uri(endpointUrl) }); + +ResponsesClient responsesClient = openAiClient.GetResponsesClient(); + +// Wrap as an agent-framework AIAgent via the OpenAI extensions. +// We pass an empty model since hosted agents use their own model configuration. +AIAgent agent = responsesClient.AsAIAgent( + model: "", + name: "remote-agent"); + +// Create a session so multi-turn context is preserved via previous_response_id +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); +Console.WriteLine("║ Foundry Responses Client REPL ║"); +Console.WriteLine($"║ Connected to: {endpointUrl,-41}║"); +Console.WriteLine("║ Type a message or 'quit' to exit ║"); +Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) continue; + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || + input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + break; + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + // Stream the response token-by-token + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json new file mode 100644 index 0000000000..4a939fc693 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "FoundryResponsesRepl": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61980;http://localhost:61981" + } + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index e6d607c793..10554d4e5c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -69,7 +69,10 @@ public static ChatOptions ConvertToChatOptions(CreateResponse request) Temperature = (float?)request.Temperature, TopP = (float?)request.TopP, MaxOutputTokens = (int?)request.MaxOutputTokens, - ModelId = request.Model, + // Note: We intentionally do NOT set ModelId from request.Model here. + // The hosted agent already has its own model configured, and passing + // the client-provided model would override it (causing failures when + // clients send placeholder values like "hosted-agent"). }; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index 7f36fd10d7..48c1a22ee8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -157,7 +157,7 @@ public void ConvertToChatOptions_SetsTemperatureAndTopP() Assert.Equal(0.7f, options.Temperature); Assert.Equal(0.9f, options.TopP); Assert.Equal(1000, options.MaxOutputTokens); - Assert.Equal("gpt-4o", options.ModelId); + Assert.Null(options.ModelId); } [Fact] @@ -659,12 +659,13 @@ public void ConvertOutputItemsToMessages_UnknownOutputItemType_IsSkipped() } [Fact] - public void ConvertToChatOptions_ModelId_SetFromRequest() + public void ConvertToChatOptions_ModelId_NotSetFromRequest() { var request = AzureAIAgentServerResponsesModelFactory.CreateResponse(model: "my-model"); var options = InputConverter.ConvertToChatOptions(request); - Assert.Equal("my-model", options.ModelId); + // Model from the request is intentionally NOT propagated — the hosted agent uses its own model. + Assert.Null(options.ModelId); } } From a25502b7c8f53f91b74417c97aaf2d72a73dc966 Mon Sep 17 00:00:00 2001 From: alliscode Date: Fri, 3 Apr 2026 11:17:21 -0700 Subject: [PATCH 41/75] Catch agent errors and emit response.failed with real error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, unhandled exceptions from agent execution would bubble up to the SDK orchestrator, which emits a generic 'An internal server error occurred.' message — hiding the actual cause (e.g., 401 auth failures, model not found, etc.). Now AgentFrameworkResponseHandler catches non-cancellation exceptions and emits a proper response.failed event containing the real error message, making it visible to clients and in logs. OperationCanceledException still propagates for proper cancellation handling by the SDK. Also bumps package version to 0.9.0-hosted.260403.2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 2 +- .../FoundryResponsesHosting.csproj | 2 +- .../Hosting/AgentFrameworkResponseHandler.cs | 21 +++++++++++++++++++ .../AgentFrameworkResponseHandlerTests.cs | 19 ++++++++++------- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 72b2d695fc..6283982542 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - 0.9.0-hosted.260403.1 + 0.9.0-hosted.260403.2 1.1.0 Debug;Release;Publish diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj index 3dfab4ad48..269754203d 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj @@ -6,7 +6,7 @@ enable enable false - $(NoWarn);NU1903;NU1605 + $(NoWarn);NU1903;NU1605;MAAIW001 diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 3423a0b4a3..eaacbc67a3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -95,6 +95,7 @@ public override async IAsyncEnumerable CreateAsync( while (true) { bool shutdownDetected = false; + ResponseStreamEvent? failedEvent = null; ResponseStreamEvent? evt = null; try { @@ -109,6 +110,26 @@ public override async IAsyncEnumerable CreateAsync( { shutdownDetected = true; } + catch (Exception ex) when (ex is not OperationCanceledException && !emittedTerminal) + { + // Catch agent execution errors and emit a proper failed event + // with the real error message instead of letting the SDK emit + // a generic "An internal server error occurred." + if (this._logger.IsEnabled(LogLevel.Error)) + { + this._logger.LogError(ex, "Agent execution failed for response {ResponseId}.", context.ResponseId); + } + + failedEvent = stream.EmitFailed( + ResponseErrorCode.ServerError, + ex.Message); + } + + if (failedEvent is not null) + { + yield return failedEvent; + yield break; + } if (shutdownDetected) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index dc38c42739..b1bdd31916 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -538,7 +538,7 @@ public async Task CreateAsync_PassesInstructionsToAgent() } [Fact] - public async Task CreateAsync_AgentThrows_ExceptionPropagates() + public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() { // Arrange var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); @@ -561,13 +561,18 @@ public async Task CreateAsync_AgentThrows_ExceptionPropagates() mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - // Act & Assert - await Assert.ThrowsAsync(async () => + // Act — collect all events + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) { - await foreach (var _ in handler.CreateAsync(request, mockContext.Object, CancellationToken.None)) - { - } - }); + events.Add(evt); + } + + // Assert — should contain created, in_progress, and failed (with real error message) + Assert.Contains(events, e => e is ResponseCreatedEvent); + Assert.Contains(events, e => e is ResponseInProgressEvent); + var failedEvent = Assert.Single(events.OfType()); + Assert.Contains("Agent crashed", failedEvent.Response.Error.Message); } [Fact] From 6e834fcac2100c3401fa7389646935e03b2c971f Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Fri, 3 Apr 2026 16:35:44 -0700 Subject: [PATCH 42/75] Renaming and merging hosting extensions. (#5091) * Rename AddAgentFrameworkHandler to AddFoundryResponses and add MapFoundryResponses - Rename extension methods: AddAgentFrameworkHandler -> AddFoundryResponses, MapAgentFrameworkHandler -> MapFoundryResponses - AddFoundryResponses now calls AddResponsesServer() internally - Add MapFoundryResponses() extension on IEndpointRouteBuilder - Update sample and tests to use new API names - Remove redundant AddResponsesServer() and /ready endpoint from sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixing numbering in sample. --------- Co-authored-by: alliscode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FoundryResponsesHosting/Program.cs | 25 ++++----- .../Hosting/ServiceCollectionExtensions.cs | 51 ++++++++++++------- .../ServiceCollectionExtensionsTests.cs | 22 ++++---- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 10a82d1b87..59fa723949 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents // using the Azure AI Responses Server SDK. @@ -16,7 +16,6 @@ // - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o") using System.ComponentModel; -using Azure.AI.AgentServer.Responses; using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; @@ -29,22 +28,16 @@ var builder = WebApplication.CreateBuilder(args); // --------------------------------------------------------------------------- -// 1. Register the Azure AI Responses Server SDK +// 1. Create the shared Azure OpenAI chat client // --------------------------------------------------------------------------- -builder.Services.AddResponsesServer(); - -// --------------------------------------------------------------------------- -// 2. 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 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()); IChatClient chatClient = azureClient.GetChatClient(deployment).AsIChatClient(); // --------------------------------------------------------------------------- -// 3. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP +// 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() @@ -72,7 +65,7 @@ You are a helpful assistant hosted as a Foundry Hosted Agent. .WithAITools(mcpTools.Cast().ToArray()); // --------------------------------------------------------------------------- -// 4. DEMO 2: Triage Workflow — routes to specialist agents +// 3. DEMO 2: Triage Workflow — routes to specialist agents // --------------------------------------------------------------------------- ChatClientAgent triageAgent = new( chatClient, @@ -113,9 +106,9 @@ Do NOT answer the question yourself - just route it. triageWorkflow.AsAIAgent(name: key)); // --------------------------------------------------------------------------- -// 5. Wire up the agent-framework handler as the IResponseHandler +// 4. Wire up the agent-framework handler and Responses Server SDK // --------------------------------------------------------------------------- -builder.Services.AddAgentFrameworkHandler(); +builder.Services.AddFoundryResponses(); var app = builder.Build(); @@ -124,10 +117,10 @@ Do NOT answer the question yourself - just route it. mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult()); // --------------------------------------------------------------------------- -// 6. Routes +// 5. Routes // --------------------------------------------------------------------------- app.MapGet("/ready", () => Results.Ok("ready")); -app.MapResponsesServer(); +app.MapFoundryResponses(); app.MapGet("/", () => Results.Content(Pages.Home, "text/html")); app.MapGet("/tool-demo", () => Results.Content(Pages.ToolDemo, "text/html")); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index d8e8a83f29..e3ff61ad0c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -2,78 +2,93 @@ using System; using Azure.AI.AgentServer.Responses; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Agents.AI.Foundry.Hosting; /// -/// Extension methods for to register the agent-framework -/// response handler with the Azure AI Responses Server SDK. +/// Extension methods for registering agent-framework agents as Foundry Hosted Agents +/// using the Azure AI Responses Server SDK. /// -public static class AgentFrameworkResponsesServiceCollectionExtensions +public static class FoundryHostingExtensions { /// - /// Registers as the - /// for the Azure AI Responses Server SDK. Agents are resolved from keyed DI services + /// Registers the Azure AI Responses Server SDK and + /// as the . Agents are resolved from keyed DI services /// using the agent.name or metadata["entity_id"] from incoming requests. /// /// /// - /// Call this method after AddResponsesServer() and after registering your - /// instances (e.g., via AddAIAgent()). + /// This method calls AddResponsesServer() internally, so you do not need to + /// call it separately. Register your instances before calling this. /// /// /// Example: /// - /// builder.Services.AddResponsesServer(); /// builder.AddAIAgent("my-agent", ...); - /// builder.Services.AddAgentFrameworkHandler(); + /// builder.Services.AddFoundryResponses(); /// /// var app = builder.Build(); - /// app.MapResponsesServer(); + /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The service collection for chaining. - public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); + services.AddResponsesServer(); services.TryAddSingleton(); return services; } /// - /// Registers a specific as the handler for all incoming requests, - /// regardless of the agent.name in the request. + /// Registers the Azure AI Responses Server SDK and a specific + /// as the handler for all incoming requests, regardless of the agent.name in the request. /// /// /// /// Use this overload when hosting a single agent. The provided agent instance is - /// registered both as a keyed service and as the default . + /// registered as both a keyed service and the default . + /// This method calls AddResponsesServer() internally. /// /// /// Example: /// - /// builder.Services.AddResponsesServer(); - /// builder.Services.AddAgentFrameworkHandler(myAgent); + /// builder.Services.AddFoundryResponses(myAgent); /// /// var app = builder.Build(); - /// app.MapResponsesServer(); + /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The agent instance to register. /// The service collection for chaining. - public static IServiceCollection AddAgentFrameworkHandler(this IServiceCollection services, AIAgent agent) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); + services.AddResponsesServer(); services.TryAddSingleton(agent); services.TryAddSingleton(); return services; } + + /// + /// Maps the Responses API routes for the agent-framework handler to the endpoint routing pipeline. + /// + /// The endpoint route builder. + /// Optional route prefix (e.g., "/openai/v1"). Default: empty (routes at /responses). + /// The endpoint route builder for chaining. + public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuilder endpoints, string prefix = "") + { + ArgumentNullException.ThrowIfNull(endpoints); + endpoints.MapResponsesServer(prefix); + return endpoints; + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs index ee61cef71a..d3fffaed5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs @@ -12,12 +12,12 @@ namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class ServiceCollectionExtensionsTests { [Fact] - public void AddAgentFrameworkHandler_RegistersResponseHandler() + public void AddFoundryResponses_RegistersResponseHandler() { var services = new ServiceCollection(); services.AddLogging(); - services.AddAgentFrameworkHandler(); + services.AddFoundryResponses(); var descriptor = services.FirstOrDefault( d => d.ServiceType == typeof(ResponseHandler)); @@ -26,33 +26,33 @@ public void AddAgentFrameworkHandler_RegistersResponseHandler() } [Fact] - public void AddAgentFrameworkHandler_CalledTwice_RegistersOnce() + public void AddFoundryResponses_CalledTwice_RegistersOnce() { var services = new ServiceCollection(); services.AddLogging(); - services.AddAgentFrameworkHandler(); - services.AddAgentFrameworkHandler(); + services.AddFoundryResponses(); + services.AddFoundryResponses(); var count = services.Count(d => d.ServiceType == typeof(ResponseHandler)); Assert.Equal(1, count); } [Fact] - public void AddAgentFrameworkHandler_NullServices_ThrowsArgumentNullException() + public void AddFoundryResponses_NullServices_ThrowsArgumentNullException() { Assert.Throws( - () => AgentFrameworkResponsesServiceCollectionExtensions.AddAgentFrameworkHandler(null!)); + () => FoundryHostingExtensions.AddFoundryResponses(null!)); } [Fact] - public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() + public void AddFoundryResponses_WithAgent_RegistersAgentAndHandler() { var services = new ServiceCollection(); services.AddLogging(); var mockAgent = new Mock(); - services.AddAgentFrameworkHandler(mockAgent.Object); + services.AddFoundryResponses(mockAgent.Object); var handlerDescriptor = services.FirstOrDefault( d => d.ServiceType == typeof(ResponseHandler)); @@ -64,10 +64,10 @@ public void AddAgentFrameworkHandler_WithAgent_RegistersAgentAndHandler() } [Fact] - public void AddAgentFrameworkHandler_WithNullAgent_ThrowsArgumentNullException() + public void AddFoundryResponses_WithNullAgent_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assert.Throws( - () => services.AddAgentFrameworkHandler(null!)); + () => services.AddFoundryResponses(null!)); } } From 34eeea165d4750e90223d496c99f49b995f4465f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:30:43 +0100 Subject: [PATCH 43/75] Address breaking changes in 260408 --- dotnet/Directory.Packages.props | 6 +- .../Hosting/AgentFrameworkResponseHandler.cs | 4 +- .../Hosting/InputConverter.cs | 21 +++++ .../AgentFrameworkResponseHandlerTests.cs | 77 +++++++++---------- .../Hosting/WorkflowIntegrationTests.cs | 4 +- 5 files changed, 64 insertions(+), 48 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index f3dc77a2dc..68d8f47d49 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,9 +19,9 @@ - - - + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index eaacbc67a3..40ad435382 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -66,10 +66,10 @@ public override async IAsyncEnumerable CreateAsync( } // Load and convert current input items - var inputItems = await context.GetInputItemsAsync(cancellationToken).ConfigureAwait(false); + var inputItems = await context.GetInputItemsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); if (inputItems.Count > 0) { - messages.AddRange(InputConverter.ConvertOutputItemsToMessages(inputItems)); + messages.AddRange(InputConverter.ConvertItemsToMessages(inputItems)); } else { diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs index 10554d4e5c..1d8be8f590 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InputConverter.cs @@ -36,6 +36,27 @@ public static List ConvertInputToMessages(CreateResponse request) return messages; } + /// + /// Converts resolved SDK input items into instances. + /// + /// The resolved input items from the SDK context. + /// A list of chat messages. + public static List ConvertItemsToMessages(IReadOnlyList items) + { + var messages = new List(); + + foreach (var item in items) + { + var message = ConvertInputItemToMessage(item); + if (message is not null) + { + messages.Add(message); + } + } + + return messages; + } + /// /// Converts resolved SDK history/input items into instances. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index b1bdd31916..eac5c60263 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -43,8 +43,8 @@ public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -82,8 +82,8 @@ public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -116,8 +116,8 @@ public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationException( var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert await Assert.ThrowsAsync(async () => @@ -164,8 +164,8 @@ public async Task CreateAsync_ResolvesAgentByModelField() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -203,8 +203,8 @@ public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -241,8 +241,8 @@ public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -277,8 +277,8 @@ public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert var ex = await Assert.ThrowsAsync(async () => @@ -310,8 +310,8 @@ public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act & Assert var ex = await Assert.ThrowsAsync(async () => @@ -343,8 +343,8 @@ public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvent var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -396,8 +396,8 @@ public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(new OutputItem[] { historyItem }); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -431,20 +431,15 @@ public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() content = new[] { new { type = "input_text", text = "Raw input" } } } }); - var inputItem = new OutputItemMessage( - id: "input_1", - role: MessageRole.Assistant, - content: [new MessageContentOutputTextContent( - "Resolved input", - Array.Empty(), - Array.Empty())], - status: MessageStatus.Completed); + var inputItem = new ItemMessage( + MessageRole.Assistant, + [new MessageContentInputTextContent("Resolved input")]); var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(new OutputItem[] { inputItem }); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new Item[] { inputItem }); // Act var events = new List(); @@ -481,8 +476,8 @@ public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -521,8 +516,8 @@ public async Task CreateAsync_PassesInstructionsToAgent() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -558,8 +553,8 @@ public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act — collect all events var events = new List(); @@ -600,8 +595,8 @@ public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); // Act var events = new List(); @@ -636,8 +631,8 @@ public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCan var mockContext = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mockContext.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mockContext.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); using var cts = new CancellationTokenSource(); cts.Cancel(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index 66924bdcba..be78f8046d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -383,8 +383,8 @@ private static Mock CreateMockContext() var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; mock.Setup(x => x.GetHistoryAsync(It.IsAny())) .ReturnsAsync(Array.Empty()); - mock.Setup(x => x.GetInputItemsAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); + mock.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); return mock; } From cf4a3f691442f062a2aa2462b97caf4f7375b61f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:46:10 +0100 Subject: [PATCH 44/75] Bump hosted internal package version --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 6283982542..c888abe380 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - 0.9.0-hosted.260403.2 + 0.9.0-hosted.260409.1 1.1.0 Debug;Release;Publish From 3a2617997e11f1e2b877db6fc1ec357ee8d4c139 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:40:54 +0100 Subject: [PATCH 45/75] Add UserAgent middleware tests for Foundry hosting --- .../Hosting/ServiceCollectionExtensions.cs | 54 +++++++ .../Hosting/UserAgentMiddlewareTests.cs | 134 ++++++++++++++++++ ...crosoft.Agents.AI.Foundry.UnitTests.csproj | 1 + 3 files changed, 189 insertions(+) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index e3ff61ad0c..7f01e5904c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Reflection; +using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -89,6 +93,56 @@ public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuild { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapResponsesServer(prefix); + + if (endpoints is IApplicationBuilder app) + { + // Ensure the middleware is added to the pipeline + app.UseMiddleware(); + } + return endpoints; } + + private sealed class AgentFrameworkUserAgentMiddleware(RequestDelegate next) + { + private static readonly string s_userAgentValue = CreateUserAgentValue(); + + public async Task InvokeAsync(HttpContext context) + { + var headers = context.Request.Headers; + var userAgent = headers.UserAgent.ToString(); + + if (string.IsNullOrEmpty(userAgent)) + { + headers.UserAgent = s_userAgentValue; + } + else if (!userAgent.Contains(s_userAgentValue, StringComparison.OrdinalIgnoreCase)) + { + headers.UserAgent = $"{userAgent} {s_userAgentValue}"; + } + + await next(context).ConfigureAwait(false); + } + + private static string CreateUserAgentValue() + { + const string Name = "agent-framework-dotnet"; + + if (typeof(AgentFrameworkUserAgentMiddleware).Assembly.GetCustomAttribute()?.InformationalVersion is string version) + { + int pos = version.IndexOf('+'); + if (pos >= 0) + { + version = version.Substring(0, pos); + } + + if (version.Length > 0) + { + return $"{Name}/{version}"; + } + } + + return Name; + } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs new file mode 100644 index 0000000000..64f7458852 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/UserAgentMiddlewareTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Foundry.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; + +/// +/// Tests for the AgentFrameworkUserAgentMiddleware registered by +/// . +/// +public sealed partial class UserAgentMiddlewareTests : IAsyncDisposable +{ + private const string VersionedUserAgentPattern = @"agent-framework-dotnet/\d+\.\d+\.\d+"; + + private WebApplication? _app; + private HttpClient? _httpClient; + + public async ValueTask DisposeAsync() + { + this._httpClient?.Dispose(); + if (this._app != null) + { + await this._app.DisposeAsync(); + } + } + + [Fact] + public async Task MapFoundryResponses_NoUserAgentHeader_SetsAgentFrameworkUserAgentAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + [Fact] + public async Task MapFoundryResponses_WithExistingUserAgent_AppendsAgentFrameworkUserAgentAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + request.Headers.TryAddWithoutValidation("User-Agent", "MyApp/1.0"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.StartsWith("MyApp/1.0", userAgent); + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + [Fact] + public async Task MapFoundryResponses_AlreadyContainsUserAgent_DoesNotDuplicateAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + // First request to capture the actual middleware-generated value + using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + var firstResponse = await this._httpClient!.SendAsync(firstRequest); + var middlewareValue = await firstResponse.Content.ReadAsStringAsync(); + + // Act: send a second request that already contains the middleware value + using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + secondRequest.Headers.TryAddWithoutValidation("User-Agent", $"MyApp/2.0 {middlewareValue}"); + var secondResponse = await this._httpClient!.SendAsync(secondRequest); + var userAgent = await secondResponse.Content.ReadAsStringAsync(); + + // Assert: should remain unchanged (no duplication) + Assert.Equal($"MyApp/2.0 {middlewareValue}", userAgent); + Assert.Single(VersionedUserAgentRegex().Matches(userAgent)); + } + + [Fact] + public async Task MapFoundryResponses_UserAgentValue_ContainsVersionAsync() + { + // Arrange + await this.CreateTestServerAsync(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua"); + + // Act + var response = await this._httpClient!.SendAsync(request); + var userAgent = await response.Content.ReadAsStringAsync(); + + // Assert: should match "agent-framework-dotnet/x.y.z" pattern + Assert.Matches(VersionedUserAgentPattern, userAgent); + } + + private async Task CreateTestServerAsync() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockAgent = new Mock(); + builder.Services.AddFoundryResponses(mockAgent.Object); + + this._app = builder.Build(); + this._app.MapFoundryResponses(); + + // Test endpoint that echoes the User-Agent header after middleware processing + this._app.MapGet("/test-ua", (HttpContext ctx) => + Results.Text(ctx.Request.Headers.UserAgent.ToString())); + + await this._app.StartAsync(); + + var testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + } + + [GeneratedRegex(VersionedUserAgentPattern)] + private static partial Regex VersionedUserAgentRegex(); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 6473b799bf..f006096208 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -12,6 +12,7 @@ + From d0fc7499b753666bafb40208b1d59d6331f7f300 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 10:57:16 +0100 Subject: [PATCH 46/75] Hosting Samples update --- dotnet/Directory.Packages.props | 1 + dotnet/agent-framework-dotnet.slnx | 38 +++++++---- .../AgentThreadAndHITL.csproj | 0 .../AgentThreadAndHITL/Dockerfile | 0 .../AgentThreadAndHITL/Program.cs | 0 .../AgentThreadAndHITL/README.md | 0 .../AgentThreadAndHITL/agent.yaml | 0 .../AgentThreadAndHITL/run-requests.http | 0 .../AgentWithHostedMCP.csproj | 0 .../AgentWithHostedMCP/Dockerfile | 0 .../AgentWithHostedMCP/Program.cs | 0 .../AgentWithHostedMCP/README.md | 0 .../AgentWithHostedMCP/agent.yaml | 0 .../AgentWithHostedMCP/run-requests.http | 0 .../AgentWithLocalTools/.dockerignore | 0 .../AgentWithLocalTools.csproj | 0 .../AgentWithLocalTools/Dockerfile | 0 .../AgentWithLocalTools/Program.cs | 0 .../AgentWithLocalTools/README.md | 0 .../AgentWithLocalTools/agent.yaml | 0 .../AgentWithLocalTools/run-requests.http | 0 .../AgentWithTextSearchRag.csproj | 0 .../AgentWithTextSearchRag/Dockerfile | 0 .../AgentWithTextSearchRag/Program.cs | 0 .../AgentWithTextSearchRag/README.md | 0 .../AgentWithTextSearchRag/agent.yaml | 0 .../AgentWithTextSearchRag/run-requests.http | 0 .../AgentsInWorkflows.csproj | 0 .../AgentsInWorkflows/Dockerfile | 0 .../AgentsInWorkflows/Program.cs | 0 .../AgentsInWorkflows/README.md | 0 .../AgentsInWorkflows/agent.yaml | 0 .../AgentsInWorkflows/run-requests.http | 0 .../FoundryMultiAgent/Dockerfile | 0 .../FoundryMultiAgent.csproj | 0 .../FoundryMultiAgent/Program.cs | 0 .../FoundryMultiAgent/README.md | 0 .../FoundryMultiAgent/agent.yaml | 0 .../appsettings.Development.json | 0 .../FoundryMultiAgent/run-requests.http | 0 .../FoundrySingleAgent/Dockerfile | 0 .../FoundrySingleAgent.csproj | 0 .../FoundrySingleAgent/Program.cs | 0 .../FoundrySingleAgent/README.md | 0 .../FoundrySingleAgent/agent.yaml | 0 .../FoundrySingleAgent/run-requests.http | 0 .../HostedAgentsV1}/README.md | 0 .../HostedAgentsV2/consumption/Program.cs | 68 +++++++++++++++++++ .../consumption/SimpleAgent.csproj | 22 ++++++ .../HostedAgentsV2/foundry-hosting/Dockerfile | 17 +++++ .../HostedAgentsV2/foundry-hosting/Program.cs | 30 ++++++++ .../Properties/launchSettings.json | 12 ++++ .../HostedAgentsV2/foundry-hosting/agent.yaml | 20 ++++++ .../simple-agent-foundry.csproj | 25 +++++++ .../instance-hosting/Dockerfile | 17 +++++ .../HostedChatClientAgent.csproj | 25 +++++++ .../instance-hosting/Program.cs | 33 +++++++++ .../Properties/launchSettings.json | 12 ++++ .../instance-hosting/agent.yaml | 20 ++++++ .../AzureAIProjectChatClientExtensions.cs | 8 +-- 60 files changed, 329 insertions(+), 19 deletions(-) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/AgentThreadAndHITL.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentThreadAndHITL/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/AgentWithHostedMCP.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithHostedMCP/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/.dockerignore (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/AgentWithLocalTools.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithLocalTools/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentWithTextSearchRag/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/AgentsInWorkflows.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/AgentsInWorkflows/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/FoundryMultiAgent.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/appsettings.Development.json (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundryMultiAgent/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/Dockerfile (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/FoundrySingleAgent.csproj (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/Program.cs (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/README.md (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/agent.yaml (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/FoundrySingleAgent/run-requests.http (100%) rename dotnet/samples/{05-end-to-end/HostedAgents => 04-hosting/FoundryHostedAgents/HostedAgentsV1}/README.md (100%) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 68d8f47d49..d11e95c50c 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -27,6 +27,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 5d746ccd97..f52c04eb7a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -263,6 +263,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -315,15 +335,6 @@ - - - - - - - - - @@ -485,13 +496,12 @@ - - + @@ -513,11 +523,10 @@ - + - @@ -533,12 +542,11 @@ - - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/.dockerignore rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http diff --git a/dotnet/samples/05-end-to-end/HostedAgents/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md similarity index 100% rename from dotnet/samples/05-end-to-end/HostedAgents/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs new file mode 100644 index 0000000000..f1c3686279 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── +// The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. + +var aiProjectClient = new AIProjectClient(new Uri(agentEndpoint), new AzureCliCredential()); +var agent = aiProjectClient.AsAIAgent(); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); +Console.WriteLine("║ Simple Agent Client ║"); +Console.WriteLine($"║ Connected to: {agentEndpoint,-41}║"); +Console.WriteLine("║ Type a message or 'quit' to exit ║"); +Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || + input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj new file mode 100644 index 0000000000..d2651ef7a7 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + enable + enable + false + SimpleAgentClient + simple-agent-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile new file mode 100644 index 0000000000..2898f31ed0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile @@ -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", "simple-agent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs new file mode 100644 index 0000000000..ad0f2b2ca2 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs @@ -0,0 +1,30 @@ +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); + +// Retrieve the Foundry-managed agent by name (latest version). +ProjectsAgentRecord agentRecord = await aiProjectClient + .AgentAdministrationClient.GetAgentAsync(agentName); + +AIAgent agent = aiProjectClient.AsAIAgent(agentRecord); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); +app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json new file mode 100644 index 0000000000..fc4cf2a105 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "simple-agent-foundry": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59703;http://localhost:59708" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml new file mode 100644 index 0000000000..a0fe89f966 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml @@ -0,0 +1,20 @@ +name: simple-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 + - Simple Agent + - Foundry Agent +template: + name: simple-agent + kind: hosted + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: AGENT_NAME + value: ${AGENT_NAME} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj new file mode 100644 index 0000000000..a9adf5bb21 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + SimpleAgent + simple-agent + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile new file mode 100644 index 0000000000..2898f31ed0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile @@ -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", "simple-agent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj new file mode 100644 index 0000000000..a9adf5bb21 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + SimpleAgent + simple-agent + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs new file mode 100644 index 0000000000..8571f0a1f0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs @@ -0,0 +1,33 @@ +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// Create the agent via the AI project client using the Responses API. +AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()) + .AsAIAgent( + model: deployment, + instructions: """ + You are a helpful AI assistant hosted as a Foundry Hosted Agent. + You can help with a wide range of tasks including answering questions, + providing explanations, brainstorming ideas, and offering guidance. + Be concise, clear, and helpful in your responses. + """, + name: "simple-agent", + description: "A simple general-purpose AI assistant"); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); +app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json new file mode 100644 index 0000000000..5f47fe2db6 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Hosted-ChatClientAgent": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59054;http://localhost:59055" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml new file mode 100644 index 0000000000..dfab24e712 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml @@ -0,0 +1,20 @@ +name: simple-agent +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent. +metadata: + tags: + - AI Agent Hosting + - Simple Agent +template: + name: simple-agent + kind: hosted + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: AGENT_NAME + value: ${AGENT_NAME} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index 4383cfb6d4..4b895e4bc0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -181,7 +182,7 @@ public static ChatClientAgent AsAIAgent( /// Creates a non-versioned backed by the project's Responses API using the specified options. /// /// The to use for Responses API calls. Cannot be . - /// Configuration options that control the agent's behavior. is required. + /// Optional configuration options that control the agent's behavior. /// Provides a way to customize the creation of the underlying used by the agent. /// Optional logger factory for creating loggers used by the agent. /// An optional to use for resolving services required by the instances being invoked. @@ -190,15 +191,14 @@ public static ChatClientAgent AsAIAgent( /// Thrown when does not specify . public static ChatClientAgent AsAIAgent( this AIProjectClient aiProjectClient, - ChatClientAgentOptions options, + ChatClientAgentOptions? options = null, Func? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { Throw.IfNull(aiProjectClient); - Throw.IfNull(options); - return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); + return CreateResponsesChatClientAgent(aiProjectClient, options ?? new(), clientFactory, loggerFactory, services); } #region Private From bf779f20292cf29dc91df3514cab7997d60ac2af Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:35:53 +0100 Subject: [PATCH 47/75] Hosting Samples update --- .../HostedAgentsV2/instance-hosting/Program.cs | 2 ++ .../instance-hosting/Properties/launchSettings.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs index 8571f0a1f0..07c7103c1a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json index 5f47fe2db6..b7f2c658ac 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:59054;http://localhost:59055" + "applicationUrl": "http://localhost:8088" } } } \ No newline at end of file From 3984ef4ef14ca086c1f4d8044d26bb775674cfea Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:36:00 +0100 Subject: [PATCH 48/75] Hosting Samples update --- dotnet/agent-framework-dotnet.slnx | 8 ++++---- .../HostedAgentsV2/consumption/Program.cs | 18 +++++++++--------- ...oundry.csproj => HostedFoundryAgent.csproj} | 0 .../HostedAgentsV2/foundry-hosting/Program.cs | 6 ++++-- .../Properties/launchSettings.json | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/{simple-agent-foundry.csproj => HostedFoundryAgent.csproj} (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index f52c04eb7a..16778e2278 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -274,14 +274,14 @@ - - - - + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs index f1c3686279..f0a1e74069 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs @@ -8,7 +8,7 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:59055"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── // The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. @@ -21,11 +21,13 @@ // ── REPL ────────────────────────────────────────────────────────────────────── Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); -Console.WriteLine("║ Simple Agent Client ║"); -Console.WriteLine($"║ Connected to: {agentEndpoint,-41}║"); -Console.WriteLine("║ Type a message or 'quit' to exit ║"); -Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Simple Agent Sample + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); Console.ResetColor(); Console.WriteLine(); @@ -38,9 +40,7 @@ string? input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || - input.Equals("exit", StringComparison.OrdinalIgnoreCase)) - { break; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } try { diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/simple-agent-foundry.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs index ad0f2b2ca2..0fe1ad9ac3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs @@ -1,8 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + using Azure.AI.Projects; using Azure.AI.Projects.Agents; using Azure.Identity; using DotNetEnv; -using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; using Microsoft.Agents.AI.Foundry.Hosting; // Load .env file if present (for local development) @@ -19,7 +21,7 @@ ProjectsAgentRecord agentRecord = await aiProjectClient .AgentAdministrationClient.GetAgentAsync(agentName); -AIAgent agent = aiProjectClient.AsAIAgent(agentRecord); +FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord); // Host the agent as a Foundry Hosted Agent using the Responses API. var builder = WebApplication.CreateBuilder(args); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json index fc4cf2a105..11588cf909 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:59703;http://localhost:59708" + "applicationUrl": "http://localhost:8089" } } } \ No newline at end of file From 3ef5436537f34b5e40c79182f9e7ef80d89fe1bc Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:05:44 +0100 Subject: [PATCH 49/75] Hosting Samples update --- dotnet/agent-framework-dotnet.slnx | 6 +++--- .../Dockerfile | 2 +- .../HostedChatClientAgent.csproj | 10 +++------- .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../agent.yaml | 0 .../Dockerfile | 2 +- .../HostedFoundryAgent.csproj | 10 +++------- .../Program.cs | 0 .../Properties/launchSettings.json | 2 +- .../agent.yaml | 0 .../{consumption => UsingHostedAgent}/Program.cs | 2 +- .../SimpleAgent.csproj | 0 13 files changed, 13 insertions(+), 21 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Dockerfile (88%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/HostedChatClientAgent.csproj (59%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{instance-hosting => Hosted-ChatClientAgent}/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Dockerfile (88%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/HostedFoundryAgent.csproj (59%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/Properties/launchSettings.json (81%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{foundry-hosting => Hosted-FoundryAgent}/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{consumption => UsingHostedAgent}/Program.cs (98%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{consumption => UsingHostedAgent}/SimpleAgent.csproj (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 16778e2278..969a5c16d8 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -275,13 +275,13 @@ - + - + - + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile similarity index 88% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile index 2898f31ed0..6f1be8ee8e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Dockerfile +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile @@ -14,4 +14,4 @@ WORKDIR /app COPY --from=build /app/publish . EXPOSE 8088 ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "simple-agent.dll"] +ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj similarity index 59% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj index a9adf5bb21..096681d505 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/HostedChatClientAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj @@ -5,16 +5,12 @@ enable enable false - SimpleAgent - simple-agent - $(NoWarn);NU1903;NU1605 + HostedChatClientAgent + HostedChatClientAgent + $(NoWarn); - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/instance-hosting/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile similarity index 88% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile index 2898f31ed0..eda1f7e1e9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Dockerfile +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile @@ -14,4 +14,4 @@ WORKDIR /app COPY --from=build /app/publish . EXPOSE 8088 ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "simple-agent.dll"] +ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj similarity index 59% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj index a9adf5bb21..2fc0ec43e4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -5,16 +5,12 @@ enable enable false - SimpleAgent - simple-agent - $(NoWarn);NU1903;NU1605 + HostedFoundryAgent + HostedFoundryAgent + $(NoWarn); - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json similarity index 81% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json index 11588cf909..a7047d02a1 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:8089" + "applicationUrl": "http://localhost:8088" } } } \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/foundry-hosting/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs similarity index 98% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index f0a1e74069..774cd3781a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -8,7 +8,7 @@ // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:59055"; +string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── // The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/consumption/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj From 990330e1985afa5013847d781e96968830bc3691 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:03:55 +0100 Subject: [PATCH 50/75] ChatClientAgent working --- .../Hosted-ChatClientAgent/Program.cs | 7 +++ .../Hosted-FoundryAgent/Program.cs | 7 +++ .../UsingHostedAgent/Program.cs | 45 ++++++++++++++++++- .../UsingHostedAgent/SimpleAgent.csproj | 2 + .../AzureAIProjectChatClientExtensions.cs | 4 +- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 07c7103c1a..88a2b1d610 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -32,4 +32,11 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. var app = builder.Build(); app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index 0fe1ad9ac3..c946d60058 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -29,4 +29,11 @@ var app = builder.Build(); app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index 774cd3781a..27e0a404d7 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; @@ -11,9 +12,21 @@ string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; // ── Create an agent-framework agent backed by the remote agent endpoint ────── -// The Foundry Agent SDK's AIProjectClient can target any OpenAI-compatible endpoint. -var aiProjectClient = new AIProjectClient(new Uri(agentEndpoint), new AzureCliCredential()); +var endpointUri = new Uri(agentEndpoint); +var options = new AIProjectClientOptions(); + +// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy +// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right +// before the request hits the wire. +Uri clientEndpoint = endpointUri; +if (endpointUri.Scheme == "http") +{ + clientEndpoint = new UriBuilder(endpointUri) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(clientEndpoint, new AzureCliCredential(), options); var agent = aiProjectClient.AsAIAgent(); AgentSession session = await agent.CreateSessionAsync(); @@ -66,3 +79,31 @@ Type a message or 'quit' to exit } Console.WriteLine("Goodbye!"); + +/// +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj index d2651ef7a7..814b5dba2d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj @@ -12,6 +12,8 @@ + + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index 4b895e4bc0..f429221c15 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -13,7 +13,6 @@ using Microsoft.Agents.AI.Foundry; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -230,8 +229,7 @@ private static ChatClientAgent CreateResponsesChatClientAgent( { Throw.IfNull(aiProjectClient); Throw.IfNull(agentOptions); - Throw.IfNull(agentOptions.ChatOptions); - Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); + agentOptions.ChatOptions ??= new(); IChatClient chatClient = aiProjectClient .GetProjectOpenAIClient() From ac6a0e3d956c1d220dcd880123afb2792140c943 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:45:52 +0100 Subject: [PATCH 51/75] Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting --- .../Hosted-ChatClientAgent/Program.cs | 6 +- .../Properties/launchSettings.json | 1 - .../Properties/launchSettings.json | 3 +- .../UsingHostedAgent/Program.cs | 25 ++++---- .../AzureAIProjectChatClientExtensions.cs | 3 +- .../Hosting/AgentFrameworkResponseHandler.cs | 59 ++++++++++++++++++- .../Hosting/AgentSessionStore.cs | 46 +++++++++++++++ .../Hosting/InMemoryAgentSessionStore.cs | 53 +++++++++++++++++ .../Hosting/ServiceCollectionExtensions.cs | 18 +++++- 9 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 88a2b1d610..448f0f16af 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -11,6 +11,10 @@ var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; // Create the agent via the AI project client using the Responses API. @@ -23,7 +27,7 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. providing explanations, brainstorming ideas, and offering guidance. Be concise, clear, and helpful in your responses. """, - name: "simple-agent", + name: agentName, description: "A simple general-purpose AI assistant"); // Host the agent as a Foundry Hosted Agent using the Responses API. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json index b7f2c658ac..cc21f3dd2e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json @@ -2,7 +2,6 @@ "profiles": { "Hosted-ChatClientAgent": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json index a7047d02a1..b4c4e005d3 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json @@ -1,8 +1,7 @@ { "profiles": { - "simple-agent-foundry": { + "HostedFoundryAgent": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index 27e0a404d7..af5b4be275 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -1,33 +1,38 @@ // Copyright (c) Microsoft. All rights reserved. using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; // Load .env file if present (for local development) Env.TraversePath().Load(); -string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088"; +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); // ── Create an agent-framework agent backed by the remote agent endpoint ────── -var endpointUri = new Uri(agentEndpoint); var options = new AIProjectClientOptions(); -// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy -// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right -// before the request hits the wire. -Uri clientEndpoint = endpointUri; -if (endpointUri.Scheme == "http") +if (agentEndpoint.Scheme == "http") { - clientEndpoint = new UriBuilder(endpointUri) { Scheme = "https" }.Uri; + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); } -var aiProjectClient = new AIProjectClient(clientEndpoint, new AzureCliCredential(), options); -var agent = aiProjectClient.AsAIAgent(); +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); AgentSession session = await agent.CreateSessionAsync(); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs index f429221c15..1f0f0a6e5f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs @@ -229,7 +229,8 @@ private static ChatClientAgent CreateResponsesChatClientAgent( { Throw.IfNull(aiProjectClient); Throw.IfNull(agentOptions); - agentOptions.ChatOptions ??= new(); + Throw.IfNull(agentOptions.ChatOptions); + Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); IChatClient chatClient = aiProjectClient .GetProjectOpenAIClient() diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs index 40ad435382..87bfd1fc75 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentFrameworkResponseHandler.cs @@ -47,8 +47,20 @@ public override async IAsyncEnumerable CreateAsync( { // 1. Resolve agent var agent = this.ResolveAgent(request); + var sessionStore = this.ResolveSessionStore(request); - // 2. Create the SDK event stream builder + // 2. Load or create a new session from the interaction + var sessionConversationId = request.GetConversationId() ?? Guid.NewGuid().ToString(); + + var chatClientAgent = agent.GetService(); + + AgentSession? session = !string.IsNullOrEmpty(sessionConversationId) + ? await sessionStore.GetSessionAsync(agent, sessionConversationId, cancellationToken).ConfigureAwait(false) + : chatClientAgent is not null + ? await chatClientAgent.CreateSessionAsync(sessionConversationId, cancellationToken).ConfigureAwait(false) + : await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false); + + // 3. Create the SDK event stream builder var stream = new ResponseEventStream(context, request); // 3. Emit lifecycle events @@ -87,7 +99,7 @@ public override async IAsyncEnumerable CreateAsync( // and inside catch blocks. We use a flag to defer the yield to outside the try/catch. bool emittedTerminal = false; var enumerator = OutputConverter.ConvertUpdatesToEventsAsync( - agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken), + agent.RunStreamingAsync(messages, session, options: options, cancellationToken: cancellationToken), stream, cancellationToken).GetAsyncEnumerator(cancellationToken); try @@ -151,6 +163,12 @@ public override async IAsyncEnumerable CreateAsync( finally { await enumerator.DisposeAsync().ConfigureAwait(false); + + // Persist session after streaming completes (successful or not) + if (session is not null && !string.IsNullOrEmpty(sessionConversationId)) + { + await sessionStore.SaveSessionAsync(agent, sessionConversationId, session, CancellationToken.None).ConfigureAwait(false); + } } } @@ -191,6 +209,43 @@ private AIAgent ResolveAgent(CreateResponse request) throw new InvalidOperationException(errorMessage); } + /// + /// Resolves an from the request. + /// Tries agent.name first, then falls back to metadata["entity_id"]. + /// If neither is present, attempts to resolve a default (non-keyed) . + /// + private AgentSessionStore ResolveSessionStore(CreateResponse request) + { + var agentName = GetAgentName(request); + + if (!string.IsNullOrEmpty(agentName)) + { + var sessionStore = this._serviceProvider.GetKeyedService(agentName); + if (sessionStore is not null) + { + return sessionStore; + } + + if (this._logger.IsEnabled(LogLevel.Warning)) + { + this._logger.LogWarning("SessionStore for agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName); + } + } + + // Try non-keyed default + var defaultSessionStore = this._serviceProvider.GetService(); + if (defaultSessionStore is not null) + { + return defaultSessionStore; + } + + var errorMessage = string.IsNullOrEmpty(agentName) + ? "No agent name specified in the request (via agent.name or metadata[\"entity_id\"]) and no default AgentSessionStore is registered." + : $"Agent '{agentName}' not found. Ensure it is registered via AddAIAgent(\"{agentName}\", ...) or as a default AgentSessionStore."; + + throw new InvalidOperationException(errorMessage); + } + private static string? GetAgentName(CreateResponse request) { // Try agent.name from AgentReference diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs new file mode 100644 index 0000000000..6aef3269b4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting; + +/// +/// Defines the contract for storing and retrieving agent conversation sessions. +/// +/// +/// Implementations of this interface enable persistent storage of conversation sessions, +/// allowing conversations to be resumed across HTTP requests, application restarts, +/// or different service instances in hosted scenarios. +/// +public abstract class AgentSessionStore +{ + /// + /// Saves a serialized agent session to persistent storage. + /// + /// The agent that owns this session. + /// The unique identifier for the conversation/session. + /// The session to save. + /// The to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + public abstract ValueTask SaveSessionAsync( + AIAgent agent, + string conversationId, + AgentSession session, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a serialized agent session from persistent storage. + /// + /// The agent that owns this session. + /// The unique identifier for the conversation/session to retrieve. + /// The to monitor for cancellation requests. + /// + /// A task that represents the asynchronous retrieval operation. + /// The task result contains the session, or a new session if not found. + /// + public abstract ValueTask GetSessionAsync( + AIAgent agent, + string conversationId, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs new file mode 100644 index 0000000000..4ae94ed4fe --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/InMemoryAgentSessionStore.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting; + +/// +/// Provides an in-memory implementation of for development and testing scenarios. +/// +/// +/// +/// This implementation stores sessions in memory using a concurrent dictionary and is suitable for: +/// +/// Single-instance development scenarios +/// Testing and prototyping +/// Scenarios where session persistence across restarts is not required +/// +/// +/// +/// Warning: All stored sessions will be lost when the application restarts. +/// For production use with multiple instances or persistence across restarts, use a durable storage implementation +/// such as Redis, SQL Server, or Azure Cosmos DB. +/// +/// +public sealed class InMemoryAgentSessionStore : AgentSessionStore +{ + private readonly ConcurrentDictionary _sessions = new(); + + /// + public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + { + var key = GetKey(conversationId, agent.Id); + this._sessions[key] = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public override async ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + { + var key = GetKey(conversationId, agent.Id); + JsonElement? sessionContent = this._sessions.TryGetValue(key, out var existingSession) ? existingSession : null; + + return sessionContent switch + { + null => await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false), + _ => await agent.DeserializeSessionAsync(sessionContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false), + }; + } + + private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}"; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index 7f01e5904c..bf3b3421f6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser { ArgumentNullException.ThrowIfNull(services); services.AddResponsesServer(); + services.TryAddSingleton(); services.TryAddSingleton(); return services; } @@ -71,14 +72,27 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser /// /// The service collection. /// The agent instance to register. + /// The agent session store to use for managing agent sessions server-side. If null, an in-memory session store will be used. /// The service collection for chaining. - public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent) + public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent, AgentSessionStore? agentSessionStore = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); services.AddResponsesServer(); - services.TryAddSingleton(agent); + agentSessionStore ??= new InMemoryAgentSessionStore(); + + if (!string.IsNullOrWhiteSpace(agent.Name)) + { + services.TryAddKeyedSingleton(agent.Name, agent); + services.TryAddKeyedSingleton(agent.Name, agentSessionStore); + } + else + { + services.TryAddSingleton(agent); + services.TryAddSingleton(agentSessionStore); + } + services.TryAddSingleton(); return services; } From a4519045a49d8e4fae1f0cfdf7ae57a864744024 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:21:17 +0100 Subject: [PATCH 52/75] Using updates --- .../HostedAgentsV2/UsingHostedAgent/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs index af5b4be275..5d9c003dfa 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs @@ -86,6 +86,7 @@ Type a message or 'quit' to exit Console.WriteLine("Goodbye!"); /// +/// For Local Development Only /// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient /// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. /// From 9e820aaad778af199bac935e25cf1d54b98c0026 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:11:26 +0100 Subject: [PATCH 53/75] Update chat client agent for contributor and devs --- .gitignore | 4 + dotnet/.gitignore | 9 +- dotnet/agent-framework-dotnet.slnx | 4 +- .../Hosted-ChatClientAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedChatClientAgent.csproj | 9 ++ .../Hosted-ChatClientAgent/Program.cs | 49 +++++++- .../Hosted-ChatClientAgent/README.md | 109 ++++++++++++++++++ .../agent.manifest.yaml | 28 +++++ .../Hosted-ChatClientAgent/agent.yaml | 29 ++--- .../Hosted-FoundryAgent/agent.manifest.yaml | 28 +++++ .../Hosted-FoundryAgent/agent.yaml | 29 ++--- .../SimpleAgent}/Program.cs | 0 .../SimpleAgent}/SimpleAgent.csproj | 2 +- .../Hosting/AgentSessionStore.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 10 +- 16 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/{UsingHostedAgent => Using-Samples/SimpleAgent}/SimpleAgent.csproj (85%) diff --git a/.gitignore b/.gitignore index 4994e9e2fe..514b0d5e31 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,10 @@ celerybeat.pid .venv env/ venv/ + +# Foundry agent CLI (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log ENV/ env.bak/ venv.bak/ diff --git a/dotnet/.gitignore b/dotnet/.gitignore index ce1409abe9..572680831e 100644 --- a/dotnet/.gitignore +++ b/dotnet/.gitignore @@ -402,4 +402,11 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml + +# Foundry agent CLI config (contains secrets, auto-generated) +.foundry-agent.json +.foundry-agent-build.log + +# Pre-published output for Docker builds +out/ \ No newline at end of file diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 969a5c16d8..5ffb5692fd 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -280,8 +280,8 @@ - - + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor new file mode 100644 index 0000000000..200f674bdd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor @@ -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-chat-client-agent . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-chat-client-agent --env-file .env hosted-chat-client-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", "HostedChatClientAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj index 096681d505..b1fe8d3e3c 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj @@ -14,8 +14,17 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 448f0f16af..b4d76bbc52 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.Projects; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; @@ -17,8 +18,14 @@ var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; +// 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()); + // Create the agent via the AI project client using the Responses API. -AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()) +AIAgent agent = new AIProjectClient(projectEndpoint, credential) .AsAIAgent( model: deployment, instructions: """ @@ -44,3 +51,43 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. } app.Run(); + +/// +/// A for local Docker debugging only. +/// +/// When debugging and testing a hosted agent in a local Docker container, Azure CLI +/// and other interactive credentials are not available. This credential reads a +/// pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// In production, the Foundry platform injects a managed identity automatically. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return GetAccessToken(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetAccessToken()); + } + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md new file mode 100644 index 0000000000..0c5ce36cfe --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md @@ -0,0 +1,109 @@ +# Hosted-ChatClientAgent + +A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent +dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": "hosted-chat-client-agent"}' +``` + +## 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 +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-chat-client-agent . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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-chat-client-agent \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-chat-client-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. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": "hosted-chat-client-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` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedChatClientAgent.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml new file mode 100644 index 0000000000..58a07d8bb3 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: hosted-chat-client-agent +displayName: "Hosted Chat Client Agent" + +description: > + A simple general-purpose AI assistant hosted as a Foundry Hosted Agent + using the Agent Framework instance hosting pattern. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Streaming + - Agent Framework + +template: + name: hosted-chat-client-agent + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml index dfab24e712..0a97abc35a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-agent -description: > - A simple general-purpose AI assistant hosted as a Foundry Hosted Agent. -metadata: - tags: - - AI Agent Hosting - - Simple Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AZURE_AI_MODEL_DEPLOYMENT_NAME - value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: AGENT_NAME - value: ${AGENT_NAME} +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-chat-client-agent +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml new file mode 100644 index 0000000000..9b33646c8a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml @@ -0,0 +1,28 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml index a0fe89f966..74223e72fe 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml @@ -1,20 +1,9 @@ -name: simple-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 - - Simple Agent - - Foundry Agent -template: - name: simple-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: AGENT_NAME - value: ${AGENT_NAME} +# 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: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj similarity index 85% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj index 814b5dba2d..05e150880e 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/UsingHostedAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj @@ -18,7 +18,7 @@ - + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs index 6aef3269b4..c61584e9e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/AgentSessionStore.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading; using System.Threading.Tasks; diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs index bf3b3421f6..fe3b07c023 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/ServiceCollectionExtensions.cs @@ -87,11 +87,11 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser services.TryAddKeyedSingleton(agent.Name, agent); services.TryAddKeyedSingleton(agent.Name, agentSessionStore); } - else - { - services.TryAddSingleton(agent); - services.TryAddSingleton(agentSessionStore); - } + + // Also register as the default (non-keyed) agent so requests + // without an agent name can resolve it (e.g., local dev tooling). + services.TryAddSingleton(agent); + services.TryAddSingleton(agentSessionStore); services.TryAddSingleton(); return services; From ebb3483a2c0a9358a755bfa1f6da771b2b137fcc Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:29:36 +0100 Subject: [PATCH 54/75] Foundry Agent Hosting --- .../Hosted-ChatClientAgent/.env.local | 1 + .../Hosted-ChatClientAgent/Program.cs | 2 +- .../Hosted-FoundryAgent/.env.local | 4 + .../Dockerfile.contributor | 19 +++ .../HostedFoundryAgent.csproj | 9 ++ .../Hosted-FoundryAgent/Program.cs | 49 ++++++- .../Hosted-FoundryAgent/README.md | 121 ++++++++++++++++++ 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local index 6d7831229d..cbf693b3a9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index b4d76bbc52..21cf34852a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -19,7 +19,7 @@ var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; // 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). +// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry). TokenCredential credential = new ChainedTokenCredential( new DevTemporaryTokenCredential(), new DefaultAzureCredential()); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local new file mode 100644 index 0000000000..1fefe43ebd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor new file mode 100644 index 0000000000..2b6a2dbbc4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj index 2fc0ec43e4..e49a15769f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj @@ -14,8 +14,17 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index c946d60058..a8f2f249c8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -2,6 +2,7 @@ using Azure.AI.Projects; using Azure.AI.Projects.Agents; +using Azure.Core; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI.Foundry; @@ -15,7 +16,13 @@ var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? throw new InvalidOperationException("AGENT_NAME is not set."); -var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential()); +// 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()); + +var aiProjectClient = new AIProjectClient(projectEndpoint, credential); // Retrieve the Foundry-managed agent by name (latest version). ProjectsAgentRecord agentRecord = await aiProjectClient @@ -37,3 +44,43 @@ } app.Run(); + +/// +/// A for local Docker debugging only. +/// +/// When debugging and testing a hosted agent in a local Docker container, Azure CLI +/// and other interactive credentials are not available. This credential reads a +/// pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// In production, the Foundry platform injects a managed identity automatically. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return GetAccessToken(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetAccessToken()); + } + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md new file mode 100644 index 0000000000..b95e7ff808 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md @@ -0,0 +1,121 @@ +# 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. + +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. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a **registered agent** (created via Foundry UI, CLI, or API) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +``` + +> **Note:** `.env` is gitignored. The `.env.local` 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. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent +AGENT_NAME= dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## 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 +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-foundry-agent . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "Hello!" +``` + +Or with curl (specifying the agent name explicitly): + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Hello!", "model": ""}' +``` + +## 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` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative. + +## How it differs from Hosted-ChatClientAgent + +| | 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 | From 28f3d178e1aff10e49fd52e8743d976b2be47e07 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:20:18 +0100 Subject: [PATCH 55/75] Address text rag sample working --- dotnet/agent-framework-dotnet.slnx | 3 + .../Hosted-ChatClientAgent/Program.cs | 13 +- .../Hosted-FoundryAgent/Program.cs | 13 +- .../HostedAgentsV2/Hosted-TextRag/.env.local | 5 + .../HostedAgentsV2/Hosted-TextRag/Dockerfile | 17 +++ .../Hosted-TextRag/Dockerfile.contributor | 19 +++ .../Hosted-TextRag/HostedTextRag.csproj | 32 +++++ .../HostedAgentsV2/Hosted-TextRag/Program.cs | 130 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-TextRag/README.md | 116 ++++++++++++++++ .../Hosted-TextRag/agent.manifest.yaml | 30 ++++ .../HostedAgentsV2/Hosted-TextRag/agent.yaml | 9 ++ .../AgentThreadAndHITL.csproj | 24 ++++ .../AgentThreadAndHITL/Program.cs | 115 ++++++++++++++++ .../AgentWithLocalTools.csproj | 24 ++++ .../AgentWithLocalTools/Program.cs | 115 ++++++++++++++++ .../AgentWithTextSearchRag.csproj | 24 ++++ .../AgentWithTextSearchRag/Program.cs | 115 ++++++++++++++++ .../AgentsInWorkflows.csproj | 24 ++++ .../AgentsInWorkflows/Program.cs | 115 ++++++++++++++++ 20 files changed, 946 insertions(+), 8 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 5ffb5692fd..8de8933bea 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -280,6 +280,9 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index 21cf34852a..e7dcf415f7 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -69,6 +69,12 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent. internal sealed class DevTemporaryTokenCredential : TokenCredential { private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; + private readonly string? _token; + + public DevTemporaryTokenCredential() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { @@ -80,14 +86,13 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(GetAccessToken()); } - private static AccessToken GetAccessToken() + private AccessToken GetAccessToken() { - var token = Environment.GetEnvironmentVariable(EnvironmentVariable); - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(_token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index a8f2f249c8..7f509084c0 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -62,6 +62,12 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential { private const string EnvironmentVariable = "AZURE_BEARER_TOKEN"; + private readonly string? _token; + + public DevTemporaryTokenCredential() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { @@ -73,14 +79,13 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(GetAccessToken()); } - private static AccessToken GetAccessToken() + private AccessToken GetAccessToken() { - var token = Environment.GetEnvironmentVariable(EnvironmentVariable); - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(_token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local new file mode 100644 index 0000000000..cbf693b3a9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local @@ -0,0 +1,5 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile new file mode 100644 index 0000000000..062d0f4f7e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile @@ -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", "HostedTextRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor new file mode 100644 index 0000000000..9a90c74335 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj new file mode 100644 index 0000000000..9a22108c7b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + false + HostedTextRag + HostedTextRag + $(NoWarn); + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs new file mode 100644 index 0000000000..45d027f740 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. + +// 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. + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +TextSearchProviderOptions textSearchOptions = new() +{ + SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, + RecentMessageMemoryLimit = 6, +}; + +AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) + .AsAIAgent(new ChatClientAgentOptions + { + Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-text-rag", + ChatOptions = new ChatOptions + { + ModelId = deploymentName, + Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", + }, + AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)] + }); + +// Host the agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +app.Run(); + +// ── Mock search function ───────────────────────────────────────────────────── +// In production, replace this with a real search provider (e.g., Azure AI Search). + +static Task> MockSearchAsync(string query, CancellationToken cancellationToken) +{ + List results = []; + + if (query.Contains("return", StringComparison.OrdinalIgnoreCase) || query.Contains("refund", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "Contoso Outdoors Return Policy", + SourceLink = "https://contoso.com/policies/returns", + Text = "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." + }); + } + + if (query.Contains("shipping", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "Contoso Outdoors Shipping Guide", + SourceLink = "https://contoso.com/help/shipping", + Text = "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." + }); + } + + if (query.Contains("tent", StringComparison.OrdinalIgnoreCase) || query.Contains("fabric", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new() + { + SourceName = "TrailRunner Tent Care Instructions", + SourceLink = "https://contoso.com/manuals/trailrunner-tent", + Text = "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." + }); + } + + return Task.FromResult>(results); +} + +/// +/// A for local Docker debugging only. +/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable. +/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed. +/// +/// 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"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private static AccessToken GetAccessToken() + { + var token = Environment.GetEnvironmentVariable(EnvironmentVariable); + if (string.IsNullOrEmpty(token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json new file mode 100644 index 0000000000..932d4e67fc --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedTextRag": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md new file mode 100644 index 0000000000..75c9dba797 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md @@ -0,0 +1,116 @@ +# 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. + +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. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN= +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag +AGENT_NAME=hosted-text-rag dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```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?" +``` + +Or with curl: + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "What is your return policy?", "model": "hosted-text-rag"}' +``` + +## Running with Docker + +Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```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-text-rag . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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 +``` + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What is your return policy?" +``` + +## How RAG works in this sample + +The `TextSearchProvider` runs a mock search **before each model invocation**: + +| 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 | + +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. + +## 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 `HostedTextRag.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml new file mode 100644 index 0000000000..1459925136 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml @@ -0,0 +1,30 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml new file mode 100644 index 0000000000..c8d6928e2e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml @@ -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: hosted-text-rag +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj new file mode 100644 index 0000000000..69905ed43b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentThreadAndHITLClient + agent-thread-hitl-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs new file mode 100644 index 0000000000..14ddb29fe8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + HITL Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj new file mode 100644 index 0000000000..0742994449 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentWithLocalToolsClient + agent-with-local-tools-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs new file mode 100644 index 0000000000..0caa06c36b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Hotel Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj new file mode 100644 index 0000000000..028977d0aa --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentWithTextSearchRagClient + agent-with-rag-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs new file mode 100644 index 0000000000..efc6c1d982 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + RAG Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj new file mode 100644 index 0000000000..27e2da3908 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + false + AgentsInWorkflowsClient + agents-in-workflows-client + $(NoWarn);NU1903;NU1605;OPENAI001 + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs new file mode 100644 index 0000000000..33e4765fb9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") + ?? throw new InvalidOperationException("AGENT_NAME is not set."); + +// ── Create an agent-framework agent backed by the remote agent endpoint ────── + +var options = new AIProjectClientOptions(); + +if (agentEndpoint.Scheme == "http") +{ + // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy + // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right + // before the request hits the wire. + + agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; + options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); +} + +var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); +FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); + +AgentSession session = await agent.CreateSessionAsync(); + +// ── REPL ────────────────────────────────────────────────────────────────────── + +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Translation Workflow Agent Client + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input, session)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); + +/// +/// For Local Development Only +/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient +/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. +/// +internal sealed class HttpSchemeRewritePolicy : PipelinePolicy +{ + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + RewriteScheme(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void RewriteScheme(PipelineMessage message) + { + var uri = message.Request.Uri!; + if (uri.Scheme == Uri.UriSchemeHttps) + { + message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; + } + } +} From 0949260d16852e56b8e8395557f802f7c9b1ef72 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:30:13 +0100 Subject: [PATCH 56/75] Version bump --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index c888abe380..10e75cf872 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -8,7 +8,7 @@ $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - 0.9.0-hosted.260409.1 + 0.9.0-hosted.260413.1 1.1.0 Debug;Release;Publish From e70f20622a50f91e72383d47443a2bc4785064ee Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:59:18 +0100 Subject: [PATCH 57/75] Adding LocalTools + Workflow samples --- dotnet/agent-framework-dotnet.slnx | 6 + .../Hosted-LocalTools/.env.local | 4 + .../Hosted-LocalTools/Dockerfile | 17 ++ .../Hosted-LocalTools/Dockerfile.contributor | 19 ++ .../Hosted-LocalTools/HostedLocalTools.csproj | 30 ++++ .../Hosted-LocalTools/Program.cs | 164 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../Hosted-LocalTools/README.md | 113 ++++++++++++ .../Hosted-LocalTools/agent.manifest.yaml | 29 ++++ .../Hosted-LocalTools/agent.yaml | 9 + .../Hosted-Workflows/.env.local | 4 + .../Hosted-Workflows/Dockerfile | 17 ++ .../Hosted-Workflows/Dockerfile.contributor | 18 ++ .../Hosted-Workflows/HostedWorkflows.csproj | 34 ++++ .../Hosted-Workflows/Program.cs | 97 +++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-Workflows/README.md | 109 ++++++++++++ .../Hosted-Workflows/agent.manifest.yaml | 29 ++++ .../Hosted-Workflows/agent.yaml | 9 + 19 files changed, 730 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8de8933bea..c065430b54 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -283,6 +283,12 @@ + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile new file mode 100644 index 0000000000..1b72fcd93f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile @@ -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", "HostedLocalTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor new file mode 100644 index 0000000000..65f920824a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor @@ -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-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/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj new file mode 100644 index 0000000000..b0d39d8cee --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + HostedLocalTools + HostedLocalTools + $(NoWarn); + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs new file mode 100644 index 0000000000..f1b2f7e3bd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs @@ -0,0 +1,164 @@ +// 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. + +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; +using Microsoft.Extensions.AI; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// ── Hotel data ─────────────────────────────────────────────────────────────── + +Hotel[] seattleHotels = +[ + new("Contoso Suites", 189, 4.5, "Downtown"), + new("Fabrikam Residences", 159, 4.2, "Pike Place Market"), + new("Alpine Ski House", 249, 4.7, "Seattle Center"), + new("Margie's Travel Lodge", 219, 4.4, "Waterfront"), + new("Northwind Inn", 139, 4.0, "Capitol Hill"), + new("Relecloud Hotel", 99, 3.8, "University District"), +]; + +// ── Tool: GetAvailableHotels ───────────────────────────────────────────────── + +[Description("Get available hotels in Seattle for the specified dates.")] +string GetAvailableHotels( + [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, + [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, + [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) +{ + if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn)) + { + return "Error parsing check-in date. Please use YYYY-MM-DD format."; + } + + if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut)) + { + return "Error parsing check-out date. Please use YYYY-MM-DD format."; + } + + if (checkOut <= checkIn) + { + return "Error: Check-out date must be after check-in date."; + } + + int nights = (checkOut - checkIn).Days; + List availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); + + if (availableHotels.Count == 0) + { + return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; + } + + StringBuilder result = new(); + result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); + result.AppendLine(); + + foreach (Hotel hotel in availableHotels) + { + int totalCost = hotel.PricePerNight * nights; + result.AppendLine($"**{hotel.Name}**"); + result.AppendLine($" Location: {hotel.Location}"); + result.AppendLine($" Rating: {hotel.Rating}/5"); + result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); + result.AppendLine(); + } + + return result.ToString(); +} + +// ── Create and host the agent ──────────────────────────────────────────────── + +AIAgent agent = new AIProjectClient(new Uri(endpoint), credential) + .AsAIAgent( + model: deploymentName, + instructions: """ + You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. + + When a user asks about hotels in Seattle: + 1. Ask for their check-in and check-out dates if not provided + 2. Ask about their budget preferences if not mentioned + 3. Use the GetAvailableHotels tool to find available options + 4. Present the results in a friendly, informative way + 5. Offer to help with additional questions about the hotels or Seattle + + Be conversational and helpful. If users ask about things outside of Seattle hotels, + politely let them know you specialize in Seattle hotel recommendations. + """, + name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools", + description: "Seattle hotel search agent with local function tools", + tools: [AIFunctionFactory.Create(GetAvailableHotels)]); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +app.Run(); + +// ── Types ──────────────────────────────────────────────────────────────────── + +internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); + +/// +/// 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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json new file mode 100644 index 0000000000..ae1bb80b7d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedLocalTools": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md new file mode 100644 index 0000000000..3c41803b95 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md @@ -0,0 +1,113 @@ +# 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. + +The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` tool that searches a mock hotel database by dates and budget. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools +AGENT_NAME=hosted-local-tools dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```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"}' +``` + +## Running with Docker + +Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```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-local-tools . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```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-local-tools \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-local-tools +``` + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What hotels are available in Seattle for next weekend?" +``` + +## How local tools work + +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: + +| 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) | + +The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing. + +## 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 `HostedLocalTools.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml new file mode 100644 index 0000000000..a056b51649 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml @@ -0,0 +1,29 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml new file mode 100644 index 0000000000..18ecc4a9f7 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml @@ -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: hosted-local-tools +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile new file mode 100644 index 0000000000..e770ec172b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile @@ -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", "HostedWorkflows.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor new file mode 100644 index 0000000000..b8dae44c2b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor @@ -0,0 +1,18 @@ +# 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-workflows . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-workflows -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflows +# +# 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", "HostedWorkflows.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj new file mode 100644 index 0000000000..2f210a18d8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + HostedWorkflows + HostedWorkflows + $(NoWarn); + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs new file mode 100644 index 0000000000..6288e3cf5f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +// 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. + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// Create a chat client from the Foundry project +IChatClient chatClient = new AIProjectClient(new Uri(endpoint), credential) + .GetProjectOpenAIClient() + .GetChatClient(deploymentName) + .AsIChatClient(); + +// Create translation agents +AIAgent frenchAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to French."); +AIAgent spanishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to Spanish."); +AIAgent englishAgent = chatClient.AsAIAgent("You are a translation assistant that translates the provided text to English."); + +// Build the sequential workflow: French → Spanish → English +AIAgent agent = new WorkflowBuilder(frenchAgent) + .AddEdge(frenchAgent, spanishAgent) + .AddEdge(spanishAgent, englishAgent) + .Build() + .AsAIAgent( + name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-workflows"); + +// Host the workflow agent as a Foundry Hosted Agent using the Responses API. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json new file mode 100644 index 0000000000..0e2908985a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedWorkflows": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md new file mode 100644 index 0000000000..0bb000aaa1 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md @@ -0,0 +1,109 @@ +# Hosted-Workflows + +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`. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your project endpoint: + +```bash +cp .env.local .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +``` + +> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. + +## Running directly (contributors) + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows +AGENT_NAME=hosted-workflows dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "The quick brown fox jumps over the lazy dog" +``` + +Or with curl: + +```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-workflows"}' +``` + +The text will be translated through the chain: English → French → Spanish → English. + +## 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-workflows . +``` + +### 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 AGENT_NAME=hosted-workflows \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-workflows +``` + +### 4. Test it + +```bash +azd ai agent invoke --local "Hello, how are you today?" +``` + +## How the workflow works + +``` +Input text + │ + ▼ +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ French Agent │ → │ Spanish Agent │ → │ English Agent │ +│ (translate) │ │ (translate) │ │ (translate) │ +└─────────────┘ └──────────────┘ └──────────────┘ + │ + ▼ + Final output + (back in English) +``` + +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. + +## NuGet package users + +Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflows.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml new file mode 100644 index 0000000000..e902b6232f --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml @@ -0,0 +1,29 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml new file mode 100644 index 0000000000..ab138939b4 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml @@ -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: hosted-workflows +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi From 8b49a0a2077b6cac171c826fe9f386df34964c08 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:50:11 +0100 Subject: [PATCH 58/75] Removing extra using samples --- .../AgentThreadAndHITL.csproj | 24 ---- .../AgentThreadAndHITL/Program.cs | 115 ------------------ .../AgentWithLocalTools.csproj | 24 ---- .../AgentWithLocalTools/Program.cs | 115 ------------------ .../AgentWithTextSearchRag.csproj | 24 ---- .../AgentWithTextSearchRag/Program.cs | 115 ------------------ .../AgentsInWorkflows.csproj | 24 ---- .../AgentsInWorkflows/Program.cs | 115 ------------------ 8 files changed, 556 deletions(-) delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj deleted file mode 100644 index 69905ed43b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/AgentThreadAndHITL.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentThreadAndHITLClient - agent-thread-hitl-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs deleted file mode 100644 index 14ddb29fe8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentThreadAndHITL/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - HITL Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj deleted file mode 100644 index 0742994449..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/AgentWithLocalTools.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentWithLocalToolsClient - agent-with-local-tools-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs deleted file mode 100644 index 0caa06c36b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithLocalTools/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - Hotel Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj deleted file mode 100644 index 028977d0aa..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentWithTextSearchRagClient - agent-with-rag-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs deleted file mode 100644 index efc6c1d982..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentWithTextSearchRag/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - RAG Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj deleted file mode 100644 index 27e2da3908..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/AgentsInWorkflows.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - AgentsInWorkflowsClient - agents-in-workflows-client - $(NoWarn);NU1903;NU1605;OPENAI001 - - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs deleted file mode 100644 index 33e4765fb9..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/AgentsInWorkflows/Program.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using DotNetEnv; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Foundry; - -// Load .env file if present (for local development) -Env.TraversePath().Load(); - -Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") - ?? "http://localhost:8088"); - -var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") - ?? throw new InvalidOperationException("AGENT_NAME is not set."); - -// ── Create an agent-framework agent backed by the remote agent endpoint ────── - -var options = new AIProjectClientOptions(); - -if (agentEndpoint.Scheme == "http") -{ - // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy - // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right - // before the request hits the wire. - - agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; - options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); -} - -var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options); -FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName)); - -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine($""" - ══════════════════════════════════════════════════════════ - Translation Workflow Agent Client - Connected to: {agentEndpoint} - Type a message or 'quit' to exit - ══════════════════════════════════════════════════════════ - """); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(input)) { continue; } - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); - -/// -/// For Local Development Only -/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient -/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check. -/// -internal sealed class HttpSchemeRewritePolicy : PipelinePolicy -{ - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - RewriteScheme(message); - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - - private static void RewriteScheme(PipelineMessage message) - { - var uri = message.Request.Uri!; - if (uri.Scheme == Uri.UriSchemeHttps) - { - message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri; - } - } -} From 930d47789a1399355e001fafada3adb9c1ab3766 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:11:05 +0100 Subject: [PATCH 59/75] Add Hosted-McpTools sample with dual MCP pattern Demonstrates two MCP integration layers in a single hosted agent: - Client-side MCP: McpClient connects to Microsoft Learn, agent handles tool invocations locally (docs_search, code_sample_search, docs_fetch) - Server-side MCP: HostedMcpServerTool delegates tool discovery and invocation to the LLM provider (Responses API), no local connection Includes DevTemporaryTokenCredential for Docker local debugging, Dockerfile.contributor for ProjectReference builds, and the openai/v1 route mapping for AIProjectClient compatibility in Development mode. --- dotnet/agent-framework-dotnet.slnx | 3 + .../HostedAgentsV2/Hosted-McpTools/.env.local | 4 + .../HostedAgentsV2/Hosted-McpTools/Dockerfile | 17 +++ .../Hosted-McpTools/Dockerfile.contributor | 18 +++ .../Hosted-McpTools/HostedMcpTools.csproj | 31 +++++ .../HostedAgentsV2/Hosted-McpTools/Program.cs | 130 ++++++++++++++++++ .../Properties/launchSettings.json | 11 ++ .../HostedAgentsV2/Hosted-McpTools/README.md | 86 ++++++++++++ .../Hosted-McpTools/agent.manifest.yaml | 30 ++++ .../HostedAgentsV2/Hosted-McpTools/agent.yaml | 9 ++ 10 files changed, 339 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index c065430b54..12554aa8d7 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -289,6 +289,9 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local new file mode 100644 index 0000000000..6d7831229d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local @@ -0,0 +1,4 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile new file mode 100644 index 0000000000..fe7fceb685 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile @@ -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", "HostedMcpTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor new file mode 100644 index 0000000000..51c8c347d8 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor @@ -0,0 +1,18 @@ +# 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/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj new file mode 100644 index 0000000000..9ce19dd540 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + HostedMcpTools + HostedMcpTools + $(NoWarn); + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs new file mode 100644 index 0000000000..a969b75477 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a hosted agent with two layers of MCP (Model Context Protocol) tools: +// +// 1. CLIENT-SIDE MCP: The agent connects to the Microsoft Learn MCP server directly via +// 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. +// +// 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. + +#pragma warning disable MEAI001 // HostedMcpServerTool is experimental + +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +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) +Env.TraversePath().Load(); + +var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.")); +var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o"; + +// 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()); + +// ── Client-side MCP: Microsoft Learn (local resolution) ────────────────────── +// Connect directly to the MCP server. The agent discovers and invokes tools locally. +Console.WriteLine("Connecting to Microsoft Learn MCP server (client-side)..."); + +await using var learnMcp = await McpClient.CreateAsync(new HttpClientTransport(new() +{ + Endpoint = new Uri("https://learn.microsoft.com/api/mcp"), + Name = "Microsoft Learn (client)", +})); + +var clientTools = await learnMcp.ListToolsAsync(); +Console.WriteLine($"Client-side MCP tools: {string.Join(", ", clientTools.Select(t => t.Name))}"); + +// ── Server-side MCP: Microsoft Learn (provider resolution) ─────────────────── +// Declare a HostedMcpServerTool — the LLM provider (Responses API) handles tool +// invocations directly. No local MCP connection needed for this pattern. +AITool serverTool = new HostedMcpServerTool( + serverName: "microsoft_learn_hosted", + serverAddress: "https://learn.microsoft.com/api/mcp") +{ + AllowedTools = ["microsoft_docs_search"], + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire +}; +Console.WriteLine("Server-side MCP tool: microsoft_docs_search (via HostedMcpServerTool)"); + +// ── Combine both tool types into a single agent ────────────────────────────── +// The agent has access to tools from both MCP patterns simultaneously. +List allTools = [.. clientTools.Cast(), serverTool]; + +AIAgent agent = new AIProjectClient(projectEndpoint, credential) + .AsAIAgent( + model: deployment, + instructions: """ + You are a helpful developer assistant with access to Microsoft Learn documentation. + Use the available tools to search and retrieve documentation. + Be concise and provide direct answers with relevant links. + """, + name: "mcp-tools", + 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. +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); + +var app = builder.Build(); +app.MapFoundryResponses(); + +// In Development, also map the OpenAI-compatible route that AIProjectClient uses. +if (app.Environment.IsDevelopment()) +{ + app.MapFoundryResponses("openai/v1"); +} + +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() + { + _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => GetAccessToken(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => new(GetAccessToken()); + + private AccessToken GetAccessToken() + { + if (string.IsNullOrEmpty(_token)) + { + throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); + } + + return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json new file mode 100644 index 0000000000..3042eb4d44 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "HostedMcpTools": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md new file mode 100644 index 0000000000..0990dfc6bd --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md @@ -0,0 +1,86 @@ +# Hosted-McpTools + +A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**: + +1. **Client-side MCP (GitHub)** — The agent connects directly to the GitHub 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 (e.g., GitHub PAT) | Provider manages credentials | +| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup | +| **Example** | GitHub (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) | + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`) +- Azure CLI logged in (`az login`) +- A **GitHub Personal Access Token** (create at https://github.com/settings/tokens) + +## Configuration + +Copy the template and fill in your values: + +```bash +cp .env.local .env +``` + +Edit `.env`: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +GITHUB_PAT=ghp_your_token_here +``` + +## Running directly (contributors) + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools +dotnet run +``` + +### Test it + +Using the Azure Developer CLI: + +```bash +# Uses GitHub MCP (client-side) +azd ai agent invoke --local "Search for the agent-framework repository on GitHub" + +# Uses Microsoft Learn MCP (server-side) +azd ai agent invoke --local "How do I create an Azure storage account using az cli?" +``` + +## 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 and run + +```bash +docker build -f Dockerfile.contributor -t hosted-mcp-tools . + +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=mcp-tools \ + -e GITHUB_PAT=$GITHUB_PAT \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-mcp-tools +``` + +## NuGet package users + +Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml new file mode 100644 index 0000000000..d5952940b0 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml @@ -0,0 +1,30 @@ +# 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: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi +parameters: + properties: [] +resources: [] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml new file mode 100644 index 0000000000..34beb3e2c9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml @@ -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: mcp-tools +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi From 4739d1ec9cce1979e6d831db76e899f23c7c6f80 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 14:48:34 -0700 Subject: [PATCH 60/75] Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix breaking API changes - Azure.AI.AgentServer.Core: 1.0.0-beta.11 -> 1.0.0-beta.21 - Azure.AI.AgentServer.Invocations: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1 - Azure.AI.AgentServer.Responses: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1 - Azure.Identity: 1.20.0 -> 1.21.0 (transitive requirement) - Azure.Core: 1.52.0 -> 1.53.0 (transitive requirement) - Remove azure-sdk-for-net dev feed (packages now on nuget.org) - Fix OutputConverter for new builder API (auto-tracked children, split EmitTextDone/EmitDone) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 10 +++++----- dotnet/nuget.config | 4 ---- .../Hosting/OutputConverter.cs | 5 ++--- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d11e95c50c..ce1bbffa0a 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,14 +19,14 @@ - - - + + + - - + + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 202c1fc671..76d943ce16 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,12 +3,8 @@ - - - - diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs index 79aaf768d9..58ba989ebf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Hosting/OutputConverter.cs @@ -161,7 +161,6 @@ public static async IAsyncEnumerable ConvertUpdatesToEvents yield return summaryPart.EmitTextDelta(text); yield return summaryPart.EmitTextDone(text); yield return summaryPart.EmitDone(); - reasoningBuilder.EmitSummaryPartDone(summaryPart); yield return reasoningBuilder.EmitDone(); break; @@ -236,8 +235,8 @@ private static IEnumerable CloseCurrentMessage( if (textBuilder is not null) { var finalText = accumulatedText?.ToString() ?? string.Empty; - yield return textBuilder.EmitDone(finalText); - yield return messageBuilder.EmitContentDone(textBuilder); + yield return textBuilder.EmitTextDone(finalText); + yield return textBuilder.EmitDone(); } yield return messageBuilder.EmitDone(); From 5af3d470621c18a954513a6c6041e6fed037c7f7 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 16:33:27 -0700 Subject: [PATCH 61/75] Fixing small issues. --- dotnet/nuget/nuget-package.props | 2 -- .../samples/04-hosting/FoundryResponsesRepl/Program.cs | 10 +++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 10e75cf872..9d91eebf28 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -7,8 +7,6 @@ $(VersionPrefix)-$(VersionSuffix).260410.1 $(VersionPrefix)-preview.260410.1 $(VersionPrefix) - - 0.9.0-hosted.260413.1 1.1.0 Debug;Release;Publish diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs index be0e6ccf7b..8b877dc519 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + // Foundry Responses Client REPL // // Connects to a Foundry Responses agent running on a given endpoint @@ -66,10 +68,16 @@ Console.ResetColor(); string? input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) continue; + if (string.IsNullOrWhiteSpace(input)) + { + continue; + } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { break; + } try { From e751779da464e2f9a9c5e86ab4da13d7282bb342 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 17:05:22 -0700 Subject: [PATCH 62/75] Fix IDE0009: add 'this' qualification in DevTemporaryTokenCredential Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostedAgentsV2/Hosted-ChatClientAgent/Program.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs index e7dcf415f7..5e26322079 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs @@ -73,26 +73,26 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential public DevTemporaryTokenCredential() { - _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { - return GetAccessToken(); + return this.GetAccessToken(); } public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) { - return new ValueTask(GetAccessToken()); + return new ValueTask(this.GetAccessToken()); } private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(_token)) + if (string.IsNullOrEmpty(this._token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); } } From c1ea834e1fd05038f93ea126417d0776460610b5 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 17:14:01 -0700 Subject: [PATCH 63/75] Fix IDE0009: add 'this' qualification in all HostedAgentsV2 samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostedAgentsV2/Hosted-FoundryAgent/Program.cs | 10 +++++----- .../HostedAgentsV2/Hosted-LocalTools/Program.cs | 10 +++++----- .../HostedAgentsV2/Hosted-McpTools/Program.cs | 10 +++++----- .../HostedAgentsV2/Hosted-Workflows/Program.cs | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs index 7f509084c0..11069c6403 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs @@ -66,26 +66,26 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential public DevTemporaryTokenCredential() { - _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { - return GetAccessToken(); + return this.GetAccessToken(); } public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) { - return new ValueTask(GetAccessToken()); + return new ValueTask(this.GetAccessToken()); } private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(_token)) + if (string.IsNullOrEmpty(this._token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs index f1b2f7e3bd..2a296f6ee4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs @@ -143,22 +143,22 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential public DevTemporaryTokenCredential() { - _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => GetAccessToken(); + => this.GetAccessToken(); public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(GetAccessToken()); + => new(this.GetAccessToken()); private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(_token)) + if (string.IsNullOrEmpty(this._token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs index a969b75477..0e97e5f84f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs @@ -109,22 +109,22 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential public DevTemporaryTokenCredential() { - _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => GetAccessToken(); + => this.GetAccessToken(); public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(GetAccessToken()); + => new(this.GetAccessToken()); private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(_token)) + if (string.IsNullOrEmpty(this._token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); } } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs index 6288e3cf5f..955bff62d0 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs @@ -76,22 +76,22 @@ internal sealed class DevTemporaryTokenCredential : TokenCredential public DevTemporaryTokenCredential() { - _token = Environment.GetEnvironmentVariable(EnvironmentVariable); + this._token = Environment.GetEnvironmentVariable(EnvironmentVariable); } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => GetAccessToken(); + => this.GetAccessToken(); public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => new(GetAccessToken()); + => new(this.GetAccessToken()); private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(_token)) + if (string.IsNullOrEmpty(this._token)) { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } - return new AccessToken(_token, DateTimeOffset.UtcNow.AddHours(1)); + return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1)); } } From 8d30cadc837cf7a42eafd1eb6367d966c07f7db6 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 17:20:15 -0700 Subject: [PATCH 64/75] Fix CHARSET: add UTF-8 BOM to Hosted-LocalTools and Hosted-Workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostedAgentsV2/Hosted-LocalTools/Program.cs | 2 +- .../HostedAgentsV2/Hosted-Workflows/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs index 2a296f6ee4..20a9cf079a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// 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, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs index 955bff62d0..279daf5f24 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Translation Chain Workflow Agent — demonstrates how to compose multiple AI agents // into a sequential workflow pipeline. Three translation agents are connected: From bc303d87f4e86093791865ec28380d28ca055186 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 17:45:22 -0700 Subject: [PATCH 65/75] Fix dotnet format: add Async suffix to test methods (IDE1006), fix encoding and style Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FoundryResponsesHosting/Program.cs | 2 +- .../AgentFrameworkResponseHandlerTests.cs | 34 +++--- .../Hosting/InputConverterTests.cs | 4 +- .../Hosting/OutputConverterTests.cs | 100 +++++++++--------- .../ServiceCollectionExtensionsTests.cs | 4 +- .../Hosting/WorkflowIntegrationTests.cs | 22 ++-- 6 files changed, 83 insertions(+), 83 deletions(-) diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs index 59fa723949..55e462e9a7 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs @@ -19,8 +19,8 @@ using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Foundry.Hosting; +using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index eac5c60263..78bccc62a8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -7,9 +7,9 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -22,7 +22,7 @@ namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; public class AgentFrameworkResponseHandlerTests { [Fact] - public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() + public async Task CreateAsync_WithDefaultAgent_ProducesStreamEventsAsync() { // Arrange var agent = CreateTestAgent("Hello from the agent!"); @@ -60,7 +60,7 @@ public async Task CreateAsync_WithDefaultAgent_ProducesStreamEvents() } [Fact] - public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() + public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgentAsync() { // Arrange var agent = CreateTestAgent("Keyed agent response"); @@ -98,7 +98,7 @@ public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgent() } [Fact] - public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationException() + public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationExceptionAsync() { // Arrange var services = new ServiceCollection(); @@ -144,7 +144,7 @@ public void Constructor_NullLogger_ThrowsArgumentNullException() } [Fact] - public async Task CreateAsync_ResolvesAgentByModelField() + public async Task CreateAsync_ResolvesAgentByModelFieldAsync() { // Arrange var agent = CreateTestAgent("model agent"); @@ -180,7 +180,7 @@ public async Task CreateAsync_ResolvesAgentByModelField() } [Fact] - public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() + public async Task CreateAsync_ResolvesAgentByEntityIdMetadataAsync() { // Arrange var agent = CreateTestAgent("entity agent"); @@ -219,7 +219,7 @@ public async Task CreateAsync_ResolvesAgentByEntityIdMetadata() } [Fact] - public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() + public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefaultAsync() { // Arrange var agent = CreateTestAgent("default agent"); @@ -257,7 +257,7 @@ public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefault() } [Fact] - public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() + public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentNameAsync() { // Arrange var services = new ServiceCollection(); @@ -292,7 +292,7 @@ public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentName() } [Fact] - public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() + public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGenericAsync() { // Arrange var services = new ServiceCollection(); @@ -325,7 +325,7 @@ public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGeneric() } [Fact] - public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvents() + public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEventsAsync() { // Arrange var services = new ServiceCollection(); @@ -367,7 +367,7 @@ public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvent } [Fact] - public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() + public async Task CreateAsync_WithHistory_PrependsHistoryToMessagesAsync() { // Arrange var agent = new CapturingAgent(); @@ -414,7 +414,7 @@ public async Task CreateAsync_WithHistory_PrependsHistoryToMessages() } [Fact] - public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() + public async Task CreateAsync_WithInputItems_UsesResolvedInputItemsAsync() { // Arrange var agent = new CapturingAgent(); @@ -456,7 +456,7 @@ public async Task CreateAsync_WithInputItems_UsesResolvedInputItems() } [Fact] - public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() + public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInputAsync() { // Arrange var agent = new CapturingAgent(); @@ -494,7 +494,7 @@ public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInput() } [Fact] - public async Task CreateAsync_PassesInstructionsToAgent() + public async Task CreateAsync_PassesInstructionsToAgentAsync() { // Arrange var agent = new CapturingAgent(); @@ -533,7 +533,7 @@ public async Task CreateAsync_PassesInstructionsToAgent() } [Fact] - public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() + public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessageAsync() { // Arrange var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); @@ -571,7 +571,7 @@ public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessage() } [Fact] - public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() + public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOneAsync() { // Arrange var agent1 = CreateTestAgent("Agent 1 response"); @@ -611,7 +611,7 @@ public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOne() } [Fact] - public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCanceledException() + public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCanceledExceptionAsync() { // Arrange var agent = new CancellationCheckingAgent(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs index 48c1a22ee8..491b1400e5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs @@ -1,10 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; -using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs index 330312d2b4..876339ea2c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using Moq; @@ -26,7 +26,7 @@ private static (ResponseEventStream stream, Mock mockContext) C } [Fact] - public async Task ConvertUpdatesToEventsAsync_EmptyStream_EmitsCompleted() + public async Task ConvertUpdatesToEventsAsync_EmptyStream_EmitsCompletedAsync() { var (stream, _) = CreateTestStream(); var updates = ToAsync(Array.Empty()); @@ -42,7 +42,7 @@ public async Task ConvertUpdatesToEventsAsync_EmptyStream_EmitsCompleted() } [Fact] - public async Task ConvertUpdatesToEventsAsync_SingleTextUpdate_EmitsMessageAndCompleted() + public async Task ConvertUpdatesToEventsAsync_SingleTextUpdate_EmitsMessageAndCompletedAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -64,7 +64,7 @@ public async Task ConvertUpdatesToEventsAsync_SingleTextUpdate_EmitsMessageAndCo } [Fact] - public async Task ConvertUpdatesToEventsAsync_MultipleTextUpdates_EmitsStreamingDeltas() + public async Task ConvertUpdatesToEventsAsync_MultipleTextUpdates_EmitsStreamingDeltasAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -85,7 +85,7 @@ public async Task ConvertUpdatesToEventsAsync_MultipleTextUpdates_EmitsStreaming } [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionCall_EmitsFunctionCallEvents() + public async Task ConvertUpdatesToEventsAsync_FunctionCall_EmitsFunctionCallEventsAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -107,7 +107,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCall_EmitsFunctionCallEven } [Fact] - public async Task ConvertUpdatesToEventsAsync_ErrorContent_EmitsFailed() + public async Task ConvertUpdatesToEventsAsync_ErrorContent_EmitsFailedAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -125,7 +125,7 @@ public async Task ConvertUpdatesToEventsAsync_ErrorContent_EmitsFailed() } [Fact] - public async Task ConvertUpdatesToEventsAsync_ErrorContent_DoesNotEmitCompleted() + public async Task ConvertUpdatesToEventsAsync_ErrorContent_DoesNotEmitCompletedAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -143,7 +143,7 @@ public async Task ConvertUpdatesToEventsAsync_ErrorContent_DoesNotEmitCompleted( } [Fact] - public async Task ConvertUpdatesToEventsAsync_UsageContent_IncludesUsageInCompleted() + public async Task ConvertUpdatesToEventsAsync_UsageContent_IncludesUsageInCompletedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -175,7 +175,7 @@ public async Task ConvertUpdatesToEventsAsync_UsageContent_IncludesUsageInComple } [Fact] - public async Task ConvertUpdatesToEventsAsync_ReasoningContent_EmitsReasoningEvents() + public async Task ConvertUpdatesToEventsAsync_ReasoningContent_EmitsReasoningEventsAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -195,7 +195,7 @@ public async Task ConvertUpdatesToEventsAsync_ReasoningContent_EmitsReasoningEve } [Fact] - public async Task ConvertUpdatesToEventsAsync_CancellationRequested_Throws() + public async Task ConvertUpdatesToEventsAsync_CancellationRequested_ThrowsAsync() { var (stream, _) = CreateTestStream(); using var cts = new CancellationTokenSource(); @@ -214,7 +214,7 @@ await Assert.ThrowsAnyAsync(async () => // F-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitted() + public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmittedAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent("")] }; @@ -231,7 +231,7 @@ public async Task ConvertUpdatesToEventsAsync_EmptyTextContent_NoTextDeltaEmitte // F-04 [Fact] - public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted() + public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmittedAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { MessageId = "msg_1", Contents = [new MeaiTextContent(null!)] }; @@ -248,7 +248,7 @@ public async Task ConvertUpdatesToEventsAsync_NullTextContent_NoTextDeltaEmitted // F-07 [Fact] - public async Task ConvertUpdatesToEventsAsync_DifferentMessageIds_CreatesMultipleMessages() + public async Task ConvertUpdatesToEventsAsync_DifferentMessageIds_CreatesMultipleMessagesAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -268,7 +268,7 @@ public async Task ConvertUpdatesToEventsAsync_DifferentMessageIds_CreatesMultipl // F-08 [Fact] - public async Task ConvertUpdatesToEventsAsync_NullMessageIds_TreatedAsSameMessage() + public async Task ConvertUpdatesToEventsAsync_NullMessageIds_TreatedAsSameMessageAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -288,7 +288,7 @@ public async Task ConvertUpdatesToEventsAsync_NullMessageIds_TreatedAsSameMessag // G-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionCallClosesOpenMessage() + public async Task ConvertUpdatesToEventsAsync_FunctionCallClosesOpenMessageAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -310,7 +310,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallClosesOpenMessage() // G-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_EmitsEmptyJson() + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_EmitsEmptyJsonAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -329,7 +329,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallWithNullArguments_Emit // G-04 [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionCallWithEmptyCallId_GeneratesCallId() + public async Task ConvertUpdatesToEventsAsync_FunctionCallWithEmptyCallId_GeneratesCallIdAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -348,7 +348,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallWithEmptyCallId_Genera // G-05 [Fact] - public async Task ConvertUpdatesToEventsAsync_MultipleFunctionCalls_EmitsSeparateBuilders() + public async Task ConvertUpdatesToEventsAsync_MultipleFunctionCalls_EmitsSeparateBuildersAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -368,7 +368,7 @@ public async Task ConvertUpdatesToEventsAsync_MultipleFunctionCalls_EmitsSeparat // H-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_ReasoningWithNullText_EmitsEmptyString() + public async Task ConvertUpdatesToEventsAsync_ReasoningWithNullText_EmitsEmptyStringAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { Contents = [new TextReasoningContent(null)] }; @@ -385,7 +385,7 @@ public async Task ConvertUpdatesToEventsAsync_ReasoningWithNullText_EmitsEmptySt // H-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_ReasoningClosesOpenMessage() + public async Task ConvertUpdatesToEventsAsync_ReasoningClosesOpenMessageAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -405,7 +405,7 @@ public async Task ConvertUpdatesToEventsAsync_ReasoningClosesOpenMessage() // I-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_ErrorContentWithNullMessage_UsesDefaultMessage() + public async Task ConvertUpdatesToEventsAsync_ErrorContentWithNullMessage_UsesDefaultMessageAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { Contents = [new ErrorContent(null!)] }; @@ -421,7 +421,7 @@ public async Task ConvertUpdatesToEventsAsync_ErrorContentWithNullMessage_UsesDe // I-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_ErrorContentClosesOpenMessage() + public async Task ConvertUpdatesToEventsAsync_ErrorContentClosesOpenMessageAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -442,7 +442,7 @@ public async Task ConvertUpdatesToEventsAsync_ErrorContentClosesOpenMessage() // I-06 [Fact] - public async Task ConvertUpdatesToEventsAsync_ErrorAfterPartialText_ClosesMessageThenFails() + public async Task ConvertUpdatesToEventsAsync_ErrorAfterPartialText_ClosesMessageThenFailsAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -464,7 +464,7 @@ public async Task ConvertUpdatesToEventsAsync_ErrorAfterPartialText_ClosesMessag // J-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_MultipleUsageUpdates_AccumulatesTokens() + public async Task ConvertUpdatesToEventsAsync_MultipleUsageUpdates_AccumulatesTokensAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -485,7 +485,7 @@ public async Task ConvertUpdatesToEventsAsync_MultipleUsageUpdates_AccumulatesTo // J-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_UsageWithZeroTokens_StillCompletes() + public async Task ConvertUpdatesToEventsAsync_UsageWithZeroTokens_StillCompletesAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate @@ -504,7 +504,7 @@ public async Task ConvertUpdatesToEventsAsync_UsageWithZeroTokens_StillCompletes // K-01 [Fact] - public async Task ConvertUpdatesToEventsAsync_DataContent_IsSkippedWithNoEvents() + public async Task ConvertUpdatesToEventsAsync_DataContent_IsSkippedWithNoEventsAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { Contents = [new DataContent("data:image/png;base64,aWNv", "image/png")] }; @@ -521,7 +521,7 @@ public async Task ConvertUpdatesToEventsAsync_DataContent_IsSkippedWithNoEvents( // K-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_UriContent_IsSkippedWithNoEvents() + public async Task ConvertUpdatesToEventsAsync_UriContent_IsSkippedWithNoEventsAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { Contents = [new UriContent("https://example.com/file.txt", "text/plain")] }; @@ -538,7 +538,7 @@ public async Task ConvertUpdatesToEventsAsync_UriContent_IsSkippedWithNoEvents() // K-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionResultContent_IsSkippedWithNoEvents() + public async Task ConvertUpdatesToEventsAsync_FunctionResultContent_IsSkippedWithNoEventsAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { Contents = [new FunctionResultContent("call_1", "result data")] }; @@ -555,7 +555,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionResultContent_IsSkippedWit // L-01 [Fact] - public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItem() + public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItemAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("executor_1", "invoked") }; @@ -573,7 +573,7 @@ public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflow // L-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_ExecutorCompletedEvent_EmitsCompletedWorkflowAction() + public async Task ConvertUpdatesToEventsAsync_ExecutorCompletedEvent_EmitsCompletedWorkflowActionAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("executor_1", null) }; @@ -591,7 +591,7 @@ public async Task ConvertUpdatesToEventsAsync_ExecutorCompletedEvent_EmitsComple // L-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_ExecutorFailedEvent_EmitsFailedWorkflowAction() + public async Task ConvertUpdatesToEventsAsync_ExecutorFailedEvent_EmitsFailedWorkflowActionAsync() { var (stream, _) = CreateTestStream(); var update = new AgentResponseUpdate { RawRepresentation = new ExecutorFailedEvent("executor_1", new InvalidOperationException("test error")) }; @@ -609,7 +609,7 @@ public async Task ConvertUpdatesToEventsAsync_ExecutorFailedEvent_EmitsFailedWor // L-04 [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowEventClosesOpenMessage() + public async Task ConvertUpdatesToEventsAsync_WorkflowEventClosesOpenMessageAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -629,7 +629,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowEventClosesOpenMessage() // L-06 [Fact] - public async Task ConvertUpdatesToEventsAsync_InterleavedWorkflowAndTextEvents() + public async Task ConvertUpdatesToEventsAsync_InterleavedWorkflowAndTextEventsAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -651,7 +651,7 @@ public async Task ConvertUpdatesToEventsAsync_InterleavedWorkflowAndTextEvents() // M-01 [Fact] - public async Task ConvertUpdatesToEventsAsync_TextThenFunctionCallThenText_ProducesCorrectSequence() + public async Task ConvertUpdatesToEventsAsync_TextThenFunctionCallThenText_ProducesCorrectSequenceAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -672,7 +672,7 @@ public async Task ConvertUpdatesToEventsAsync_TextThenFunctionCallThenText_Produ // M-02 [Fact] - public async Task ConvertUpdatesToEventsAsync_ReasoningThenText_ProducesCorrectSequence() + public async Task ConvertUpdatesToEventsAsync_ReasoningThenText_ProducesCorrectSequenceAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -692,7 +692,7 @@ public async Task ConvertUpdatesToEventsAsync_ReasoningThenText_ProducesCorrectS // M-03 [Fact] - public async Task ConvertUpdatesToEventsAsync_TextThenError_EmitsMessageThenFailed() + public async Task ConvertUpdatesToEventsAsync_TextThenError_EmitsMessageThenFailedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -714,7 +714,7 @@ public async Task ConvertUpdatesToEventsAsync_TextThenError_EmitsMessageThenFail // M-04 [Fact] - public async Task ConvertUpdatesToEventsAsync_FunctionCallThenTextThenFunctionCall_ProducesThreeItems() + public async Task ConvertUpdatesToEventsAsync_FunctionCallThenTextThenFunctionCall_ProducesThreeItemsAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -739,7 +739,7 @@ public async Task ConvertUpdatesToEventsAsync_FunctionCallThenTextThenFunctionCa // W-01: Multi-executor text output — different MessageIds cause separate messages [Fact] - public async Task ConvertUpdatesToEventsAsync_MultiExecutorTextOutput_CreatesSeparateMessages() + public async Task ConvertUpdatesToEventsAsync_MultiExecutorTextOutput_CreatesSeparateMessagesAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -773,7 +773,7 @@ public async Task ConvertUpdatesToEventsAsync_MultiExecutorTextOutput_CreatesSep // W-02: Workflow error via ErrorContent (as produced by WorkflowSession for WorkflowErrorEvent) [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowErrorAsContent_EmitsFailed() + public async Task ConvertUpdatesToEventsAsync_WorkflowErrorAsContent_EmitsFailedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -798,7 +798,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowErrorAsContent_EmitsFailed // W-03: Function call from workflow executor (e.g. handoff agent calling transfer_to_agent) [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowFunctionCall_EmitsFunctionCallEvents() + public async Task ConvertUpdatesToEventsAsync_WorkflowFunctionCall_EmitsFunctionCallEventsAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -831,7 +831,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowFunctionCall_EmitsFunction // W-04: Informational events (superstep, workflow started) are silently skipped [Fact] - public async Task ConvertUpdatesToEventsAsync_InformationalWorkflowEvents_AreSkipped() + public async Task ConvertUpdatesToEventsAsync_InformationalWorkflowEvents_AreSkippedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -856,7 +856,7 @@ public async Task ConvertUpdatesToEventsAsync_InformationalWorkflowEvents_AreSki // W-05: Warning events are silently skipped [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowWarningEvent_IsSkipped() + public async Task ConvertUpdatesToEventsAsync_WorkflowWarningEvent_IsSkippedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -877,7 +877,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowWarningEvent_IsSkipped() // W-06: Streaming text from multiple workflow turns (same executor, different message IDs) [Fact] - public async Task ConvertUpdatesToEventsAsync_MultiTurnSameExecutor_CreatesSeparateMessages() + public async Task ConvertUpdatesToEventsAsync_MultiTurnSameExecutor_CreatesSeparateMessagesAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -904,7 +904,7 @@ public async Task ConvertUpdatesToEventsAsync_MultiTurnSameExecutor_CreatesSepar // W-07: Executor failure mid-stream with partial text [Fact] - public async Task ConvertUpdatesToEventsAsync_ExecutorFailureAfterPartialText_ClosesMessageAndEmitsFailure() + public async Task ConvertUpdatesToEventsAsync_ExecutorFailureAfterPartialText_ClosesMessageAndEmitsFailureAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -929,7 +929,7 @@ public async Task ConvertUpdatesToEventsAsync_ExecutorFailureAfterPartialText_Cl // W-08: Full handoff pattern — triage → function call → target agent text [Fact] - public async Task ConvertUpdatesToEventsAsync_FullHandoffPattern_ProducesCorrectEventSequence() + public async Task ConvertUpdatesToEventsAsync_FullHandoffPattern_ProducesCorrectEventSequenceAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -973,7 +973,7 @@ public async Task ConvertUpdatesToEventsAsync_FullHandoffPattern_ProducesCorrect // W-09: SubworkflowErrorEvent treated as informational (error content comes separately) [Fact] - public async Task ConvertUpdatesToEventsAsync_SubworkflowErrorEvent_IsSkipped() + public async Task ConvertUpdatesToEventsAsync_SubworkflowErrorEvent_IsSkippedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -995,7 +995,7 @@ public async Task ConvertUpdatesToEventsAsync_SubworkflowErrorEvent_IsSkipped() // W-10: Mixed content types from workflow — reasoning + text [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowReasoningThenText_ProducesCorrectSequence() + public async Task ConvertUpdatesToEventsAsync_WorkflowReasoningThenText_ProducesCorrectSequenceAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -1021,7 +1021,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowReasoningThenText_Produces // W-11: Usage content accumulated across workflow executors [Fact] - public async Task ConvertUpdatesToEventsAsync_WorkflowUsageAcrossExecutors_AccumulatesCorrectly() + public async Task ConvertUpdatesToEventsAsync_WorkflowUsageAcrossExecutors_AccumulatesCorrectlyAsync() { var (stream, _) = CreateTestStream(); var updates = new[] @@ -1048,7 +1048,7 @@ public async Task ConvertUpdatesToEventsAsync_WorkflowUsageAcrossExecutors_Accum // W-12: Empty workflow — only lifecycle events, no content [Fact] - public async Task ConvertUpdatesToEventsAsync_EmptyWorkflowOnlyLifecycle_EmitsOnlyCompleted() + public async Task ConvertUpdatesToEventsAsync_EmptyWorkflowOnlyLifecycle_EmitsOnlyCompletedAsync() { var (stream, _) = CreateTestStream(); var updates = new[] diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs index d3fffaed5a..1833ea9198 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; -using Microsoft.Agents.AI.Foundry.Hosting; using Azure.AI.AgentServer.Responses; +using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.DependencyInjection; using Moq; diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index be78f8046d..ad6722a549 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -31,7 +31,7 @@ public class WorkflowIntegrationTests // ===== Sequential Workflow Tests ===== [Fact] - public async Task SequentialWorkflow_SingleAgent_ProducesTextOutput() + public async Task SequentialWorkflow_SingleAgent_ProducesTextOutputAsync() { // Arrange: single-agent sequential workflow var echoAgent = new StreamingTextAgent("echo", "Hello from the workflow!"); @@ -59,7 +59,7 @@ public async Task SequentialWorkflow_SingleAgent_ProducesTextOutput() } [Fact] - public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBoth() + public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBothAsync() { // Arrange: two agents in sequence var agent1 = new StreamingTextAgent("agent1", "First agent says hello"); @@ -90,7 +90,7 @@ public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBoth() // ===== Workflow Error Propagation ===== [Fact] - public async Task Workflow_AgentThrowsException_ProducesErrorOutput() + public async Task Workflow_AgentThrowsException_ProducesErrorOutputAsync() { // Arrange: workflow with an agent that throws var throwingAgent = new ThrowingStreamingAgent("thrower", new InvalidOperationException("Agent crashed")); @@ -120,7 +120,7 @@ public async Task Workflow_AgentThrowsException_ProducesErrorOutput() // ===== Workflow Action Lifecycle Events ===== [Fact] - public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItems() + public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItemsAsync() { // Arrange var agent = new StreamingTextAgent("test-agent", "Result"); @@ -144,7 +144,7 @@ public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItems() // ===== Keyed Workflow Registration ===== [Fact] - public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() + public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectlyAsync() { // Arrange: workflow agent registered with a keyed service name var agent = new StreamingTextAgent("inner", "Keyed workflow response"); @@ -177,7 +177,7 @@ public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectly() // These test the OutputConverter directly with update patterns that mirror real workflows. [Fact] - public async Task OutputConverter_SequentialWorkflowPattern_ProducesCorrectEvents() + public async Task OutputConverter_SequentialWorkflowPattern_ProducesCorrectEventsAsync() { // Simulate what WorkflowSession produces for a 2-agent sequential workflow var (stream, _) = CreateTestStream(); @@ -210,7 +210,7 @@ public async Task OutputConverter_SequentialWorkflowPattern_ProducesCorrectEvent } [Fact] - public async Task OutputConverter_GroupChatPattern_ProducesCorrectEvents() + public async Task OutputConverter_GroupChatPattern_ProducesCorrectEventsAsync() { // Simulate round-robin group chat: agent1 → agent2 → agent1 → terminate var (stream, _) = CreateTestStream(); @@ -246,7 +246,7 @@ public async Task OutputConverter_GroupChatPattern_ProducesCorrectEvents() } [Fact] - public async Task OutputConverter_CodeExecutorPattern_ProducesCorrectEvents() + public async Task OutputConverter_CodeExecutorPattern_ProducesCorrectEventsAsync() { // Simulate a code-based FunctionExecutor: invoked → completed, no text content // (code executors don't produce AgentResponseUpdateEvent, just executor lifecycle) @@ -278,7 +278,7 @@ public async Task OutputConverter_CodeExecutorPattern_ProducesCorrectEvents() } [Fact] - public async Task OutputConverter_SubworkflowPattern_ProducesCorrectEvents() + public async Task OutputConverter_SubworkflowPattern_ProducesCorrectEventsAsync() { // Simulate a parent workflow that invokes a sub-workflow executor var (stream, _) = CreateTestStream(); @@ -308,7 +308,7 @@ public async Task OutputConverter_SubworkflowPattern_ProducesCorrectEvents() } [Fact] - public async Task OutputConverter_WorkflowWithMultipleContentTypes_HandlesAllCorrectly() + public async Task OutputConverter_WorkflowWithMultipleContentTypes_HandlesAllCorrectlyAsync() { // Simulate a workflow producing reasoning, text, function calls, and usage var (stream, _) = CreateTestStream(); From 93306f5f5bc8ad17720d21b840746293890434f5 Mon Sep 17 00:00:00 2001 From: alliscode Date: Wed, 15 Apr 2026 18:05:12 -0700 Subject: [PATCH 66/75] Register AgentSessionStore in test DI setups Add InMemoryAgentSessionStore registration to all ServiceCollection setups in AgentFrameworkResponseHandlerTests and WorkflowIntegrationTests. This is needed after the AgentSessionStore infrastructure was introduced in the responses-hosting feature. Tests still have NotImplementedException stubs for CreateSessionCoreAsync which will be fixed when the session infrastructure is fully available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AgentFrameworkResponseHandlerTests.cs | 16 ++++++++++++++++ .../Hosting/WorkflowIntegrationTests.cs | 2 ++ 2 files changed, 18 insertions(+) diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index 78bccc62a8..277156c9bf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -27,6 +27,7 @@ public async Task CreateAsync_WithDefaultAgent_ProducesStreamEventsAsync() // Arrange var agent = CreateTestAgent("Hello from the agent!"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); services.AddSingleton>(NullLogger.Instance); var sp = services.BuildServiceProvider(); @@ -65,6 +66,7 @@ public async Task CreateAsync_WithKeyedAgent_ResolvesCorrectAgentAsync() // Arrange var agent = CreateTestAgent("Keyed agent response"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddKeyedSingleton("my-agent", agent); var sp = services.BuildServiceProvider(); @@ -102,6 +104,7 @@ public async Task CreateAsync_NoAgentRegistered_ThrowsInvalidOperationExceptionA { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -149,6 +152,7 @@ public async Task CreateAsync_ResolvesAgentByModelFieldAsync() // Arrange var agent = CreateTestAgent("model agent"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddKeyedSingleton("my-agent", agent); var sp = services.BuildServiceProvider(); @@ -185,6 +189,7 @@ public async Task CreateAsync_ResolvesAgentByEntityIdMetadataAsync() // Arrange var agent = CreateTestAgent("entity agent"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddKeyedSingleton("entity-agent", agent); var sp = services.BuildServiceProvider(); @@ -224,6 +229,7 @@ public async Task CreateAsync_NamedAgentNotFound_FallsBackToDefaultAsync() // Arrange var agent = CreateTestAgent("default agent"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -261,6 +267,7 @@ public async Task CreateAsync_NoAgentFound_ErrorMessageIncludesAgentNameAsync() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -296,6 +303,7 @@ public async Task CreateAsync_NoAgentNoName_ErrorMessageIsGenericAsync() { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -329,6 +337,7 @@ public async Task CreateAsync_AgentResolvedBeforeEmitCreated_ExceptionHasNoEvent { // Arrange var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); var sp = services.BuildServiceProvider(); var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); @@ -372,6 +381,7 @@ public async Task CreateAsync_WithHistory_PrependsHistoryToMessagesAsync() // Arrange var agent = new CapturingAgent(); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -419,6 +429,7 @@ public async Task CreateAsync_WithInputItems_UsesResolvedInputItemsAsync() // Arrange var agent = new CapturingAgent(); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -461,6 +472,7 @@ public async Task CreateAsync_NoInputItems_FallsBackToRawRequestInputAsync() // Arrange var agent = new CapturingAgent(); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -499,6 +511,7 @@ public async Task CreateAsync_PassesInstructionsToAgentAsync() // Arrange var agent = new CapturingAgent(); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -538,6 +551,7 @@ public async Task CreateAsync_AgentThrows_EmitsFailedEventWithErrorMessageAsync( // Arrange var agent = new ThrowingAgent(new InvalidOperationException("Agent crashed")); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); @@ -577,6 +591,7 @@ public async Task CreateAsync_MultipleKeyedAgents_ResolvesCorrectOneAsync() var agent1 = CreateTestAgent("Agent 1 response"); var agent2 = CreateTestAgent("Agent 2 response"); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddKeyedSingleton("agent-1", agent1); services.AddKeyedSingleton("agent-2", agent2); var sp = services.BuildServiceProvider(); @@ -616,6 +631,7 @@ public async Task CreateAsync_CancellationDuringExecution_PropagatesOperationCan // Arrange var agent = new CancellationCheckingAgent(); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); var sp = services.BuildServiceProvider(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs index ad6722a549..05be11c3d8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs @@ -155,6 +155,7 @@ public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectlyAsync() executionEnvironment: InProcessExecution.OffThread); var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddKeyedSingleton("my-workflow", workflowAgent); var sp = services.BuildServiceProvider(); @@ -356,6 +357,7 @@ private static (AgentFrameworkResponseHandler handler, CreateResponse request, R CreateHandlerWithAgent(AIAgent agent, string userMessage) { var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); services.AddSingleton(agent); services.AddSingleton>(NullLogger.Instance); var sp = services.BuildServiceProvider(); From 53fec2a989d669adfc954cba195ac12ee6dbac50 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:43:13 +0100 Subject: [PATCH 67/75] Add Invocations protocol samples (hosted echo agent + client) (#5278) Add Hosted-Invocations-EchoAgent: a minimal echo agent hosted via the Invocations protocol (POST /invocations) using AddInvocationsServer and MapInvocationsServer, bridged to an Agent Framework AIAgent through a custom InvocationHandler. Add SimpleInvocationsAgent: a console REPL client that wraps HttpClient calls to the /invocations endpoint in a custom InvocationsAIAgent, demonstrating programmatic consumption of the Invocations protocol. Both samples default to port 8088 for consistency with other hosted agent samples. --- .../Hosted-Invocations-EchoAgent/Dockerfile | 17 +++ .../Dockerfile.contributor | 19 +++ .../EchoAIAgent.cs | 85 ++++++++++++ .../EchoInvocationHandler.cs | 33 +++++ .../Hosted-Invocations-EchoAgent.csproj | 29 ++++ .../Hosted-Invocations-EchoAgent/Program.cs | 24 ++++ .../Properties/launchSettings.json | 11 ++ .../Hosted-Invocations-EchoAgent/README.md | 66 +++++++++ .../agent.manifest.yaml | 27 ++++ .../Hosted-Invocations-EchoAgent/agent.yaml | 9 ++ .../InvocationsAIAgent.cs | 129 ++++++++++++++++++ .../SimpleInvocationsAgent/Program.cs | 61 +++++++++ .../SimpleInvocationsAgent.csproj | 22 +++ 13 files changed, 532 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile new file mode 100644 index 0000000000..24585dec12 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile @@ -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", "HostedInvocationsEchoAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor new file mode 100644 index 0000000000..91a403c26c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor @@ -0,0 +1,19 @@ +# 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/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs new file mode 100644 index 0000000000..ccbfe72781 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI; + +/// +/// A minimal that echoes the user's input text back as the response. +/// No LLM or external service is required. +/// +public sealed class EchoAIAgent : AIAgent +{ + /// + public override string Name => "echo-agent"; + + /// + public override string Description => "An agent that echoes back the input message."; + + /// + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + var inputText = GetInputText(messages); + var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, $"Echo: {inputText}")); + return Task.FromResult(response); + } + + /// + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var inputText = GetInputText(messages); + yield return new AgentResponseUpdate + { + Role = ChatRole.Assistant, + Contents = [new TextContent($"Echo: {inputText}")], + }; + + await Task.CompletedTask; + } + + /// + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new EchoAgentSession()); + + /// + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + => new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions)); + + /// + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + => new(new EchoAgentSession()); + + private static string GetInputText(IEnumerable messages) + { + foreach (var message in messages) + { + if (message.Role == ChatRole.User) + { + return message.Text ?? string.Empty; + } + } + + return string.Empty; + } + + /// + /// Minimal session for the echo agent. No state is persisted. + /// + private sealed class EchoAgentSession : AgentSession; +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs new file mode 100644 index 0000000000..9834e7577a --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.AgentServer.Invocations; +using Microsoft.Agents.AI; +using Microsoft.AspNetCore.Http; + +namespace HostedInvocationsEchoAgent; + +/// +/// An that reads the request body as plain text, +/// passes it to the , and writes the response back. +/// +public sealed class EchoInvocationHandler(EchoAIAgent agent) : InvocationHandler +{ + /// + public override async Task HandleAsync( + HttpRequest request, + HttpResponse response, + InvocationContext context, + CancellationToken cancellationToken) + { + // Read the raw text from the request body. + using var reader = new StreamReader(request.Body); + var input = await reader.ReadToEndAsync(cancellationToken); + + // Run the echo agent with the input text. + var agentResponse = await agent.RunAsync(input, cancellationToken: cancellationToken); + + // Write the agent response text back to the HTTP response. + response.ContentType = "text/plain"; + await response.WriteAsync(agentResponse.Text, cancellationToken); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj new file mode 100644 index 0000000000..b84faccf9e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + HostedInvocationsEchoAgent + HostedInvocationsEchoAgent + $(NoWarn); + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs new file mode 100644 index 0000000000..1650253dfc --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.AgentServer.Invocations; +using HostedInvocationsEchoAgent; +using Microsoft.Agents.AI; + +var builder = WebApplication.CreateBuilder(args); + +// Register the echo agent as a singleton (no LLM needed). +builder.Services.AddSingleton(); + +// Register the Invocations SDK services and wire the handler. +builder.Services.AddInvocationsServer(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Map the Invocations protocol endpoints: +// POST /invocations — invoke the agent +// GET /invocations/{id} — get result (not used by this sample) +// POST /invocations/{id}/cancel — cancel (not used by this sample) +app.MapInvocationsServer(); + +app.Run(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json new file mode 100644 index 0000000000..7722815b12 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Hosted-Invocations-EchoAgent": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8088" + } + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md new file mode 100644 index 0000000000..7133c43cf1 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md @@ -0,0 +1,66 @@ +# 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. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent +dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +```bash +curl -X POST http://localhost:8088/invocations \ + -H "Content-Type: text/plain" \ + -d "Hello, world!" +``` + +Expected response: + +``` +Echo: Hello, world! +``` + +## 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 +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-invocations-echo-agent . +``` + +### 3. Run the container + +```bash +docker run --rm -p 8088:8088 hosted-invocations-echo-agent +``` + +### 4. Test it + +```bash +curl -X POST http://localhost:8088/invocations \ + -H "Content-Type: text/plain" \ + -d "Hello from Docker!" +``` + +## 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 `Hosted-Invocations-EchoAgent.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml new file mode 100644 index 0000000000..09e4b0f885 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml @@ -0,0 +1,27 @@ +# 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/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml new file mode 100644 index 0000000000..001a19f0ac --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml @@ -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: 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/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs new file mode 100644 index 0000000000..54f8022d1b --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI; + +/// +/// An that invokes a remote agent hosted with the Invocations protocol +/// by sending plain-text HTTP POST requests to the /invocations endpoint. +/// +public sealed class InvocationsAIAgent : AIAgent +{ + private readonly HttpClient _httpClient; + private readonly Uri _invocationsUri; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The base URI of the hosted agent (e.g., http://localhost:8089). + /// The /invocations path is appended automatically. + /// + /// Optional to use. If , a new instance is created. + /// Optional name for the agent. + /// Optional description for the agent. + public InvocationsAIAgent( + Uri agentEndpoint, + HttpClient? httpClient = null, + string? name = null, + string? description = null) + { + ArgumentNullException.ThrowIfNull(agentEndpoint); + + this._httpClient = httpClient ?? new HttpClient(); + + // Ensure the base URI ends with a slash so that combining works correctly. + var baseUri = agentEndpoint.AbsoluteUri.EndsWith('/') + ? agentEndpoint + : new Uri(agentEndpoint.AbsoluteUri + "/"); + this._invocationsUri = new Uri(baseUri, "invocations"); + + this.Name = name ?? "invocations-agent"; + this.Description = description ?? "An agent that calls a remote Invocations protocol endpoint."; + } + + /// + public override string? Name { get; } + + /// + public override string? Description { get; } + + /// + protected override async Task RunCoreAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + var inputText = GetLastUserText(messages); + var responseText = await SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); + return new AgentResponse(new ChatMessage(ChatRole.Assistant, responseText)); + } + + /// + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // The Invocations protocol returns a complete response (no SSE streaming), + // so we yield a single update with the full text. + var inputText = GetLastUserText(messages); + var responseText = await SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); + + yield return new AgentResponseUpdate + { + Role = ChatRole.Assistant, + Contents = [new TextContent(responseText)], + }; + } + + /// + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new InvocationsAgentSession()); + + /// + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + => new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions)); + + /// + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + => new(new InvocationsAgentSession()); + + private async Task SendInvocationAsync(string input, CancellationToken cancellationToken) + { + using var content = new StringContent(input, System.Text.Encoding.UTF8, "text/plain"); + using var response = await this._httpClient.PostAsync(this._invocationsUri, content, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + private static string GetLastUserText(IEnumerable messages) + { + string? lastUserText = null; + foreach (var message in messages) + { + if (message.Role == ChatRole.User) + { + lastUserText = message.Text; + } + } + + return lastUserText ?? string.Empty; + } + + /// + /// Minimal session for the invocations agent. No state is persisted. + /// + private sealed class InvocationsAgentSession : AgentSession; +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs new file mode 100644 index 0000000000..915e73737d --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using DotNetEnv; +using Microsoft.Agents.AI; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT") + ?? "http://localhost:8088"); + +// Create an agent that calls the remote Invocations endpoint. +InvocationsAIAgent agent = new(agentEndpoint); + +// REPL +Console.ForegroundColor = ConsoleColor.Cyan; +Console.WriteLine($""" + ══════════════════════════════════════════════════════════ + Simple Invocations Agent Sample + Connected to: {agentEndpoint} + Type a message or 'quit' to exit + ══════════════════════════════════════════════════════════ + """); +Console.ResetColor(); +Console.WriteLine(); + +while (true) +{ + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("You> "); + Console.ResetColor(); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { continue; } + if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; } + + try + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Agent> "); + Console.ResetColor(); + + await foreach (var update in agent.RunStreamingAsync(input)) + { + Console.Write(update); + } + + Console.WriteLine(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + } + + Console.WriteLine(); +} + +Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj new file mode 100644 index 0000000000..d509bd2c70 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + enable + enable + false + SimpleInvocationsAgentClient + simple-invocations-agent-client + $(NoWarn);NU1903;NU1605 + + + + + + + + + + + From d49d546bb4286fd1a322fabb62855310c57d90e1 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:04:31 +0100 Subject: [PATCH 68/75] Restructure FoundryHostedAgents samples into invocations/ and responses/ Align dotnet hosted agent samples with the Python side (PR #5281) by reorganizing the directory structure: - Remove HostedAgentsV1 entirely (old API pattern) - Split HostedAgentsV2 into invocations/ and responses/ based on protocol - Move Using-Samples accordingly (SimpleAgent to responses, SimpleInvocationsAgent to invocations) - Update slnx with new project paths and add previously missing invocations projects - Update README cd paths from HostedAgentsV2 to invocations or responses - Rename .env.local to .env.example to match Python naming convention - Fix format violations in newly included invocations projects --- dotnet/agent-framework-dotnet.slnx | 44 +++-- .../AgentThreadAndHITL.csproj | 69 ------- .../AgentThreadAndHITL/Dockerfile | 20 --- .../AgentThreadAndHITL/Program.cs | 41 ----- .../AgentThreadAndHITL/README.md | 46 ----- .../AgentThreadAndHITL/agent.yaml | 28 --- .../AgentThreadAndHITL/run-requests.http | 70 -------- .../AgentWithHostedMCP.csproj | 68 ------- .../AgentWithHostedMCP/Dockerfile | 20 --- .../AgentWithHostedMCP/Program.cs | 40 ----- .../AgentWithHostedMCP/README.md | 45 ----- .../AgentWithHostedMCP/agent.yaml | 31 ---- .../AgentWithHostedMCP/run-requests.http | 32 ---- .../AgentWithLocalTools/.dockerignore | 24 --- .../AgentWithLocalTools.csproj | 70 -------- .../AgentWithLocalTools/Dockerfile | 20 --- .../AgentWithLocalTools/Program.cs | 132 -------------- .../AgentWithLocalTools/README.md | 39 ---- .../AgentWithLocalTools/agent.yaml | 29 --- .../AgentWithLocalTools/run-requests.http | 52 ------ .../AgentWithTextSearchRag.csproj | 68 ------- .../AgentWithTextSearchRag/Dockerfile | 20 --- .../AgentWithTextSearchRag/Program.cs | 79 -------- .../AgentWithTextSearchRag/README.md | 43 ----- .../AgentWithTextSearchRag/agent.yaml | 31 ---- .../AgentWithTextSearchRag/run-requests.http | 30 ---- .../AgentsInWorkflows.csproj | 68 ------- .../AgentsInWorkflows/Dockerfile | 20 --- .../AgentsInWorkflows/Program.cs | 40 ----- .../AgentsInWorkflows/README.md | 28 --- .../AgentsInWorkflows/agent.yaml | 28 --- .../AgentsInWorkflows/run-requests.http | 30 ---- .../FoundryMultiAgent/Dockerfile | 20 --- .../FoundryMultiAgent.csproj | 76 -------- .../FoundryMultiAgent/Program.cs | 51 ------ .../FoundryMultiAgent/README.md | 168 ------------------ .../FoundryMultiAgent/agent.yaml | 31 ---- .../appsettings.Development.json | 4 - .../FoundryMultiAgent/run-requests.http | 34 ---- .../FoundrySingleAgent/Dockerfile | 20 --- .../FoundrySingleAgent.csproj | 67 ------- .../FoundrySingleAgent/Program.cs | 132 -------------- .../FoundrySingleAgent/README.md | 167 ----------------- .../FoundrySingleAgent/agent.yaml | 32 ---- .../FoundrySingleAgent/run-requests.http | 52 ------ .../HostedAgentsV1/README.md | 100 ----------- .../Hosted-Invocations-EchoAgent/Dockerfile | 0 .../Dockerfile.contributor | 0 .../EchoAIAgent.cs | 0 .../EchoInvocationHandler.cs | 1 - .../Hosted-Invocations-EchoAgent.csproj | 0 .../Hosted-Invocations-EchoAgent/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-Invocations-EchoAgent/README.md | 2 +- .../agent.manifest.yaml | 0 .../Hosted-Invocations-EchoAgent/agent.yaml | 0 .../InvocationsAIAgent.cs | 4 +- .../SimpleInvocationsAgent/Program.cs | 0 .../SimpleInvocationsAgent.csproj | 0 .../Hosted-ChatClientAgent/.env.example} | 0 .../Hosted-ChatClientAgent/Dockerfile | 0 .../Dockerfile.contributor | 0 .../HostedChatClientAgent.csproj | 0 .../Hosted-ChatClientAgent/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-ChatClientAgent/README.md | 6 +- .../agent.manifest.yaml | 0 .../Hosted-ChatClientAgent/agent.yaml | 0 .../Hosted-FoundryAgent/.env.example} | 0 .../Hosted-FoundryAgent/Dockerfile | 0 .../Dockerfile.contributor | 0 .../HostedFoundryAgent.csproj | 0 .../Hosted-FoundryAgent/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-FoundryAgent/README.md | 6 +- .../Hosted-FoundryAgent/agent.manifest.yaml | 0 .../Hosted-FoundryAgent/agent.yaml | 0 .../Hosted-LocalTools/.env.example} | 0 .../Hosted-LocalTools/Dockerfile | 0 .../Hosted-LocalTools/Dockerfile.contributor | 0 .../Hosted-LocalTools/HostedLocalTools.csproj | 0 .../Hosted-LocalTools/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-LocalTools/README.md | 6 +- .../Hosted-LocalTools/agent.manifest.yaml | 0 .../Hosted-LocalTools/agent.yaml | 0 .../Hosted-McpTools/.env.example} | 0 .../Hosted-McpTools/Dockerfile | 0 .../Hosted-McpTools/Dockerfile.contributor | 0 .../Hosted-McpTools/HostedMcpTools.csproj | 0 .../Hosted-McpTools/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-McpTools/README.md | 4 +- .../Hosted-McpTools/agent.manifest.yaml | 0 .../Hosted-McpTools/agent.yaml | 0 .../Hosted-TextRag/.env.example} | 0 .../Hosted-TextRag/Dockerfile | 0 .../Hosted-TextRag/Dockerfile.contributor | 0 .../Hosted-TextRag/HostedTextRag.csproj | 0 .../Hosted-TextRag/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-TextRag/README.md | 6 +- .../Hosted-TextRag/agent.manifest.yaml | 0 .../Hosted-TextRag/agent.yaml | 0 .../Hosted-Workflows/.env.example} | 0 .../Hosted-Workflows/Dockerfile | 0 .../Hosted-Workflows/Dockerfile.contributor | 0 .../Hosted-Workflows/HostedWorkflows.csproj | 0 .../Hosted-Workflows/Program.cs | 0 .../Properties/launchSettings.json | 0 .../Hosted-Workflows/README.md | 6 +- .../Hosted-Workflows/agent.manifest.yaml | 0 .../Hosted-Workflows/agent.yaml | 0 .../Using-Samples/SimpleAgent/Program.cs | 0 .../SimpleAgent/SimpleAgent.csproj | 0 115 files changed, 41 insertions(+), 2359 deletions(-) delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/EchoAIAgent.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs (97%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/README.md (95%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Hosted-Invocations-EchoAgent/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs (95%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Using-Samples/SimpleInvocationsAgent/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => invocations}/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-ChatClientAgent/.env.local => responses/Hosted-ChatClientAgent/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/HostedChatClientAgent.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/README.md (94%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-ChatClientAgent/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-FoundryAgent/.env.local => responses/Hosted-FoundryAgent/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/HostedFoundryAgent.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/README.md (95%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-FoundryAgent/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-LocalTools/.env.local => responses/Hosted-LocalTools/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/HostedLocalTools.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/README.md (94%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-LocalTools/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-McpTools/.env.local => responses/Hosted-McpTools/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/HostedMcpTools.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/README.md (96%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-McpTools/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-TextRag/.env.local => responses/Hosted-TextRag/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/HostedTextRag.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/README.md (94%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-TextRag/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2/Hosted-Workflows/.env.local => responses/Hosted-Workflows/.env.example} (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/Dockerfile.contributor (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/HostedWorkflows.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/Properties/launchSettings.json (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/README.md (94%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Hosted-Workflows/agent.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Using-Samples/SimpleAgent/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/{HostedAgentsV2 => responses}/Using-Samples/SimpleAgent/SimpleAgent.csproj (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 12554aa8d7..67f3331b27 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -264,36 +264,34 @@ - - - - - - - - + + + - - - + + - - + + + - - + + - - + + - - + + - - + + - - + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj deleted file mode 100644 index a56157fe9d..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/AgentThreadAndHITL.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - $(NoWarn);MEAI001 - - - false - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile deleted file mode 100644 index 004bd49fa8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentThreadAndHITL.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs deleted file mode 100644 index fee781d660..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates Human-in-the-Loop (HITL) capabilities with thread persistence. -// The agent wraps function tools with ApprovalRequiredAIFunction to require user approval -// before invoking them. Users respond with 'approve' or 'reject' when prompted. - -using System.ComponentModel; -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.AgentServer.AgentFramework.Persistence; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using OpenAI.Chat; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; - -[Description("Get the weather for a given location.")] -static string GetWeather([Description("The location to get the weather for.")] string location) - => $"The weather in {location} is cloudy with a high of 15°C."; - -// Create the chat client and agent. -// Note: ApprovalRequiredAIFunction wraps the tool to require user approval before invocation. -// User should reply with 'approve' or 'reject' when prompted. -// 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. -#pragma warning disable MEAI001 // Type is for evaluation purposes only -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent( - instructions: "You are a helpful assistant", - tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))] - ); -#pragma warning restore MEAI001 - -InMemoryAgentThreadRepository threadRepository = new(agent); -await agent.RunAIAgentAsync(telemetrySourceName: "Agents", threadRepository: threadRepository); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md deleted file mode 100644 index 465dfacbf0..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates Human-in-the-Loop (HITL) capabilities with thread persistence. The agent wraps function tools with `ApprovalRequiredAIFunction` so that every tool invocation requires explicit user approval before execution. Thread state is maintained across requests using `InMemoryAgentThreadRepository`. - -Key features: -- Requiring human approval before executing function calls -- Persisting conversation threads across multiple requests -- Approving or rejecting tool invocations at runtime - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -Before running this sample, ensure you have: - -1. .NET 10 SDK installed -2. An Azure OpenAI endpoint configured -3. A deployment of a chat model (e.g., gpt-5.4-mini) -4. Azure CLI installed and authenticated (`az login`) - -## Environment Variables - -Set the following environment variables: - -```powershell -# Replace with your Azure OpenAI endpoint -$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/" - -# Optional, defaults to gpt-5.4-mini -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -## How It Works - -The sample uses `ApprovalRequiredAIFunction` to wrap standard AI function tools. When the model decides to call a tool, the wrapper intercepts the invocation and returns a HITL approval request to the caller instead of executing the function immediately. - -1. The user sends a message (e.g., "What is the weather in Vancouver?") -2. The model determines a function call is needed and selects the `GetWeather` tool -3. `ApprovalRequiredAIFunction` intercepts the call and returns an approval request containing the function name and arguments -4. The user responds with `approve` or `reject` -5. If approved, the function executes and the model generates a response using the result -6. If rejected, the model generates a response without the function result - -Thread persistence is handled by `InMemoryAgentThreadRepository`, which stores conversation history keyed by `conversation.id`. This means the HITL flow works across multiple HTTP requests as long as each request includes the same `conversation.id`. - -> **Note:** HITL requires a stable `conversation.id` in every request so the agent can correlate the approval response with the original function call. Use the `run-requests.http` file in this directory to test the full approval flow. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml deleted file mode 100644 index c7e67b3d4e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/agent.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: AgentThreadAndHITL -displayName: "Weather Assistant Agent" -description: > - A Weather Assistant Agent that provides weather information and forecasts. It - demonstrates how to use Azure AI AgentServer with Human-in-the-Loop (HITL) - capabilities to get human approval for functional calls. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Human-in-the-Loop -template: - kind: hosted - name: AgentThreadAndHITL - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_OPENAI_ENDPOINT - value: ${AZURE_OPENAI_ENDPOINT} - - name: AZURE_OPENAI_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http deleted file mode 100644 index 196a30a542..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentThreadAndHITL/run-requests.http +++ /dev/null @@ -1,70 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### -# HITL (Human-in-the-Loop) Flow -# -# This sample requires a multi-turn conversation to demonstrate the approval flow: -# 1. Send a request that triggers a tool call (e.g., asking about the weather) -# 2. The agent responds with a function_call named "__hosted_agent_adapter_hitl__" -# containing the call_id and the tool details -# 3. Send a follow-up request with a function_call_output to approve or reject -# -# IMPORTANT: You must use the same conversation.id across all requests in a flow, -# and update the call_id from step 2 into step 3. -### - -### Step 1: Send initial request (triggers HITL approval) -# @name initialRequest -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "What is the weather like in Vancouver?", - "stream": false, - "conversation": { - "id": "conv_test0000000000000000000000000000000000000000000000" - } -} - -### Step 2: Approve the function call -# Copy the call_id from the Step 1 response output and replace below. -# The response will contain: "name": "__hosted_agent_adapter_hitl__" with a "call_id" value. -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "function_call_output", - "call_id": "REPLACE_WITH_CALL_ID_FROM_STEP_1", - "output": "approve" - } - ], - "stream": false, - "conversation": { - "id": "conv_test0000000000000000000000000000000000000000000000" - } -} - -### Step 3 (alternative): Reject the function call -# Use this instead of Step 2 to deny the tool execution. -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "function_call_output", - "call_id": "REPLACE_WITH_CALL_ID_FROM_STEP_1", - "output": "reject" - } - ], - "stream": false, - "conversation": { - "id": "conv_test0000000000000000000000000000000000000000000000" - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj deleted file mode 100644 index 4e46f10c11..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/AgentWithHostedMCP.csproj +++ /dev/null @@ -1,68 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile deleted file mode 100644 index a2590fc112..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentWithHostedMCP.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs deleted file mode 100644 index 4178946604..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/Program.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample shows how to create and use a simple AI agent with OpenAI Responses as the backend, that uses a Hosted MCP Tool. -// In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework. -// The sample demonstrates how to use MCP tools with auto approval by setting ApprovalMode to NeverRequire. - -#pragma warning disable MEAI001 // HostedMcpServerTool, HostedMcpServerToolApprovalMode are experimental -#pragma warning disable OPENAI001 // GetResponsesClient is experimental - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using OpenAI.Responses; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; - -// Create an MCP tool that can be called without approval. -AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAddress: "https://learn.microsoft.com/api/mcp") -{ - AllowedTools = ["microsoft_docs_search"], - ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire -}; - -// Create an agent with the MCP tool using Azure OpenAI Responses. -// 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. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) - .GetResponsesClient(deploymentName) - .AsAIAgent( - instructions: "You answer questions by searching the Microsoft Learn content only.", - name: "MicrosoftLearnAgent", - tools: [mcpTool]); - -await agent.RunAIAgentAsync(); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md deleted file mode 100644 index dc0718dfa7..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates how to use a Hosted Model Context Protocol (MCP) server with an AI agent. -The agent connects to the Microsoft Learn MCP server to search documentation and answer questions using official Microsoft content. - -Key features: -- Configuring MCP tools with automatic approval (no user confirmation required) -- Filtering available tools from an MCP server -- Using Azure OpenAI Responses with MCP tools - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -Before running this sample, ensure you have: - -1. An Azure OpenAI endpoint configured -2. A deployment of a chat model (e.g., gpt-5.4-mini) -3. Azure CLI installed and authenticated - -**Note**: This sample uses `DefaultAzureCredential` for authentication, which probes multiple sources automatically. For local development, make sure you're logged in with `az login` and have access to the Azure OpenAI resource. - -## Environment Variables - -Set the following environment variables: - -```powershell -# Replace with your Azure OpenAI endpoint -$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/" - -# Optional, defaults to gpt-5.4-mini -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -## How It Works - -The sample connects to the Microsoft Learn MCP server and uses its documentation search capabilities: - -1. The agent is configured with a HostedMcpServerTool pointing to `https://learn.microsoft.com/api/mcp` -2. Only the `microsoft_docs_search` tool is enabled from the available MCP tools -3. Approval mode is set to `NeverRequire`, allowing automatic tool execution -4. When you ask questions, Azure OpenAI Responses automatically invokes the MCP tool to search documentation -5. The agent returns answers based on the Microsoft Learn content - -In this configuration, the OpenAI Responses service manages tool invocation directly - the Agent Framework does not handle MCP tool calls. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml deleted file mode 100644 index 7c02acb02a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/agent.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: AgentWithHostedMCP -displayName: "Microsoft Learn Response Agent with MCP" -description: > - An AI agent that uses Azure OpenAI Responses with a Hosted Model Context Protocol (MCP) server. - The agent answers questions by searching Microsoft Learn documentation using MCP tools. - This demonstrates how MCP tools can be integrated with Azure OpenAI Responses where the service - itself handles tool invocation. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Model Context Protocol - - MCP - - Tool Call Approval -template: - kind: hosted - name: AgentWithHostedMCP - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_OPENAI_ENDPOINT - value: ${AZURE_OPENAI_ENDPOINT} - - name: AZURE_OPENAI_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http deleted file mode 100644 index b7c0b35efd..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithHostedMCP/run-requests.http +++ /dev/null @@ -1,32 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple string input - Ask about MCP Tools -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "Please summarize the Azure AI Agent documentation related to MCP Tool calling?" -} - -### Explicit input - Ask about Agent Framework -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "What is the Microsoft Agent Framework?" - } - ] - } - ] -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore deleted file mode 100644 index 2afa2c2601..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/.dockerignore +++ /dev/null @@ -1,24 +0,0 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj deleted file mode 100644 index b7970f8c5f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/AgentWithLocalTools.csproj +++ /dev/null @@ -1,70 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - true - - - false - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile deleted file mode 100644 index c2461965a4..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentWithLocalTools.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs deleted file mode 100644 index 0da7c57b12..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/Program.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle. -// Uses Microsoft Agent Framework with Microsoft Foundry. -// Ready for deployment to Foundry Hosted Agent service. - -using System.ClientModel.Primitives; -using System.ComponentModel; -using System.Globalization; -using System.Text; -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.OpenAI; -using Azure.AI.Projects; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") - ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; -Console.WriteLine($"Project Endpoint: {endpoint}"); -Console.WriteLine($"Model Deployment: {deploymentName}"); - -Hotel[] seattleHotels = -[ - new Hotel("Contoso Suites", 189, 4.5, "Downtown"), - new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"), - new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"), - new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"), - new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"), - new Hotel("Relecloud Hotel", 99, 3.8, "University District"), -]; - -[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")] -string GetAvailableHotels( - [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, - [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, - [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) -{ - try - { - if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn)) - { - return "Error parsing check-in date. Please use YYYY-MM-DD format."; - } - - if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut)) - { - return "Error parsing check-out date. Please use YYYY-MM-DD format."; - } - - if (checkOut <= checkIn) - { - return "Error: Check-out date must be after check-in date."; - } - - int nights = (checkOut - checkIn).Days; - List availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); - - if (availableHotels.Count == 0) - { - return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; - } - - StringBuilder result = new(); - result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); - result.AppendLine(); - - foreach (Hotel hotel in availableHotels) - { - int totalCost = hotel.PricePerNight * nights; - result.AppendLine($"**{hotel.Name}**"); - result.AppendLine($" Location: {hotel.Location}"); - result.AppendLine($" Rating: {hotel.Rating}/5"); - result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); - result.AppendLine(); - } - - return result.ToString(); - } - catch (Exception ex) - { - return $"Error processing request. Details: {ex.Message}"; - } -} - -// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. -// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid -// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -DefaultAzureCredential credential = new(); -AIProjectClient projectClient = new(new Uri(endpoint), credential); - -ClientConnection connection = projectClient.GetConnection(typeof(AzureOpenAIClient).FullName!); - -if (!connection.TryGetLocatorAsUri(out Uri? openAiEndpoint) || openAiEndpoint is null) -{ - throw new InvalidOperationException("Failed to get OpenAI endpoint from project connection."); -} -openAiEndpoint = new Uri($"https://{openAiEndpoint.Host}"); -Console.WriteLine($"OpenAI Endpoint: {openAiEndpoint}"); - -IChatClient chatClient = new AzureOpenAIClient(openAiEndpoint, credential) - .GetChatClient(deploymentName) - .AsIChatClient() - .AsBuilder() - .UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false) - .Build(); - -AIAgent agent = chatClient.AsAIAgent( - name: "SeattleHotelAgent", - instructions: """ - You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. - - When a user asks about hotels in Seattle: - 1. Ask for their check-in and check-out dates if not provided - 2. Ask about their budget preferences if not mentioned - 3. Use the GetAvailableHotels tool to find available options - 4. Present the results in a friendly, informative way - 5. Offer to help with additional questions about the hotels or Seattle - - Be conversational and helpful. If users ask about things outside of Seattle hotels, - politely let them know you specialize in Seattle hotel recommendations. - """, - tools: [AIFunctionFactory.Create(GetAvailableHotels)]) - .AsBuilder() - .UseOpenTelemetry(sourceName: "Agents", configure: cfg => cfg.EnableSensitiveData = false) - .Build(); - -Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088"); -await agent.RunAIAgentAsync(telemetrySourceName: "Agents"); - -internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md deleted file mode 100644 index aba51898ec..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates how to build a hosted agent that uses local C# function tools — a key advantage of code-based hosted agents over prompt agents. The agent acts as a Seattle travel assistant with a `GetAvailableHotels` tool that simulates querying a hotel availability API. - -Key features: -- Defining local C# functions as agent tools using `AIFunctionFactory` -- Using `AIProjectClient` to discover the OpenAI connection from the Microsoft Foundry project -- Building a `ChatClientAgent` with custom instructions and tools -- Deploying to the Foundry Hosted Agent service - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -Before running this sample, ensure you have: - -1. .NET 10 SDK installed -2. A Microsoft Foundry Project with a chat model deployed (e.g., gpt-5.4-mini) -3. Azure CLI installed and authenticated (`az login`) - -## Environment Variables - -Set the following environment variables: - -```powershell -# Replace with your Microsoft Foundry project endpoint -$env:AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project-name" - -# Optional, defaults to gpt-5.4-mini -$env:MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -## How It Works - -1. The agent uses `AIProjectClient` to discover the Azure OpenAI connection from the project endpoint -2. A local C# function `GetAvailableHotels` is registered as a tool using `AIFunctionFactory.Create` -3. When users ask about hotels, the model invokes the local tool to search simulated hotel data -4. The tool filters hotels by price and calculates total costs based on the requested dates -5. Results are returned to the model, which presents them in a conversational format diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml deleted file mode 100644 index 7e75a738ce..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/agent.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: seattle-hotel-agent -description: > - A travel assistant agent that helps users find hotels in Seattle. - Demonstrates local C# tool execution - a key advantage of code-based - hosted agents over prompt agents. -metadata: - authors: - - Microsoft - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Local Tools - - Travel Assistant - - Hotel Search -template: - name: seattle-hotel-agent - kind: hosted - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: MODEL_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - kind: model - id: gpt-5.4-mini - name: chat diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http deleted file mode 100644 index 4f2e87e097..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithLocalTools/run-requests.http +++ /dev/null @@ -1,52 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple hotel search - budget under $200 -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night", - "stream": false -} - -### Hotel search with higher budget -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "Find me hotels in Seattle for March 20-23, 2025 under $250 per night", - "stream": false -} - -### Ask for recommendations without dates (agent should ask for clarification) -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "What hotels do you recommend in Seattle?", - "stream": false -} - -### Explicit input format -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "I'm looking for a hotel in Seattle from 2025-04-01 to 2025-04-05, my budget is $150 per night maximum" - } - ] - } - ], - "stream": false -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj deleted file mode 100644 index 7789abd315..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj +++ /dev/null @@ -1,68 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile deleted file mode 100644 index 3d944c9883..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentWithTextSearchRag.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs deleted file mode 100644 index 518ce5679f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/Program.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG) -// capabilities to an AI agent. The provider runs a search against an external knowledge base -// before each model invocation and injects the results into the model context. - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using OpenAI.Chat; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; - -TextSearchProviderOptions textSearchOptions = new() -{ - // Run the search prior to every model invocation and keep a short rolling window of conversation context. - SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, - RecentMessageMemoryLimit = 6, -}; - -// 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. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent(new ChatClientAgentOptions - { - ChatOptions = new ChatOptions - { - Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", - }, - AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)] - }); - -await agent.RunAIAgentAsync(); - -static Task> MockSearchAsync(string query, CancellationToken cancellationToken) -{ - // The mock search inspects the user's question and returns pre-defined snippets - // that resemble documents stored in an external knowledge source. - List results = []; - - if (query.Contains("return", StringComparison.OrdinalIgnoreCase) || query.Contains("refund", StringComparison.OrdinalIgnoreCase)) - { - results.Add(new() - { - SourceName = "Contoso Outdoors Return Policy", - SourceLink = "https://contoso.com/policies/returns", - Text = "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." - }); - } - - if (query.Contains("shipping", StringComparison.OrdinalIgnoreCase)) - { - results.Add(new() - { - SourceName = "Contoso Outdoors Shipping Guide", - SourceLink = "https://contoso.com/help/shipping", - Text = "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." - }); - } - - if (query.Contains("tent", StringComparison.OrdinalIgnoreCase) || query.Contains("fabric", StringComparison.OrdinalIgnoreCase)) - { - results.Add(new() - { - SourceName = "TrailRunner Tent Care Instructions", - SourceLink = "https://contoso.com/manuals/trailrunner-tent", - Text = "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." - }); - } - - return Task.FromResult>(results); -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md deleted file mode 100644 index b62d9068ce..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates how to use TextSearchProvider to add retrieval augmented generation (RAG) capabilities to an AI agent. The provider runs a search against an external knowledge base before each model invocation and injects the results into the model context. - -Key features: -- Configuring TextSearchProvider with custom search behavior -- Running searches before AI invocations to provide relevant context -- Managing conversation memory with a rolling window approach -- Citing source documents in AI responses - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -Before running this sample, ensure you have: - -1. An Azure OpenAI endpoint configured -2. A deployment of a chat model (e.g., gpt-5.4-mini) -3. Azure CLI installed and authenticated - -## Environment Variables - -Set the following environment variables: - -```powershell -# Replace with your Azure OpenAI endpoint -$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/" - -# Optional, defaults to gpt-5.4-mini -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -## How It Works - -The sample uses a mock search function that demonstrates the RAG pattern: - -1. When the user asks a question, the TextSearchProvider intercepts it -2. The search function looks for relevant documents based on the query -3. Retrieved documents are injected into the model's context -4. The AI responds using both its training and the provided context -5. The agent can cite specific source documents in its answers - -The mock search function returns pre-defined snippets for demonstration purposes. In a production scenario, you would replace this with actual searches against your knowledge base (e.g., Azure AI Search, vector database, etc.). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml deleted file mode 100644 index 6cdad09e9c..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/agent.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: AgentWithTextSearchRag -displayName: "Text Search RAG Agent" -description: > - An AI agent that uses TextSearchProvider for retrieval augmented generation (RAG) capabilities. - The agent runs searches against an external knowledge base before each model invocation and - injects the results into the model context. It can answer questions about Contoso Outdoors - policies and products, including return policies, refunds, shipping options, and product care - instructions such as tent maintenance. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Retrieval-Augmented Generation - - RAG -template: - kind: hosted - name: AgentWithTextSearchRag - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_OPENAI_ENDPOINT - value: ${AZURE_OPENAI_ENDPOINT} - - name: AZURE_OPENAI_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http deleted file mode 100644 index 4bfb02d8f8..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentWithTextSearchRag/run-requests.http +++ /dev/null @@ -1,30 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple string input -POST {{endpoint}} -Content-Type: application/json -{ - "input": "Hi! I need help understanding the return policy." -} - -### Explicit input -POST {{endpoint}} -Content-Type: application/json -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "How long does standard shipping usually take?" - } - ] - } - ] -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj deleted file mode 100644 index 7789abd315..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/AgentsInWorkflows.csproj +++ /dev/null @@ -1,68 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile deleted file mode 100644 index 86b6c156f3..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentsInWorkflows.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs deleted file mode 100644 index 886e205acf..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/Program.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates how to integrate AI agents into a workflow pipeline. -// Three translation agents are connected sequentially to create a translation chain: -// English → French → Spanish → English, showing how agents can be composed as workflow executors. - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Workflows; -using Microsoft.Extensions.AI; - -// Set up the Azure OpenAI client -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; - -// 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. -IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsIChatClient(); - -// Create agents -AIAgent frenchAgent = GetTranslationAgent("French", chatClient); -AIAgent spanishAgent = GetTranslationAgent("Spanish", chatClient); -AIAgent englishAgent = GetTranslationAgent("English", chatClient); - -// Build the workflow and turn it into an agent -AIAgent agent = new WorkflowBuilder(frenchAgent) - .AddEdge(frenchAgent, spanishAgent) - .AddEdge(spanishAgent, englishAgent) - .Build() - .AsAIAgent(); - -await agent.RunAIAgentAsync(); - -static AIAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) => - chatClient.AsAIAgent($"You are a translation assistant that translates the provided text to {targetLanguage}."); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md deleted file mode 100644 index b7a2f9ca53..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates the use of AI agents as executors within a workflow. - -This workflow uses three translation agents: -1. French Agent - translates input text to French -2. Spanish Agent - translates French text to Spanish -3. English Agent - translates Spanish text back to English - -The agents are connected sequentially, creating a translation chain that demonstrates how AI-powered components can be seamlessly integrated into workflow pipelines. - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -Before you begin, ensure you have the following prerequisites: - -- .NET 10 SDK or later -- Azure OpenAI service endpoint and deployment configured -- Azure CLI installed and authenticated (for Azure credential authentication) - -**Note**: This demo uses `DefaultAzureCredential` for authentication, which probes multiple sources automatically. For local development, make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). - -Set the following environment variables: - -```powershell -$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml deleted file mode 100644 index 3c97fa2ac1..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/agent.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: AgentsInWorkflows -displayName: "Translation Chain Workflow Agent" -description: > - A workflow agent that performs sequential translation through multiple languages. - The agent translates text from English to French, then to Spanish, and finally back - to English, leveraging AI-powered translation capabilities in a pipeline workflow. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Workflows -template: - kind: hosted - name: AgentsInWorkflows - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_OPENAI_ENDPOINT - value: ${AZURE_OPENAI_ENDPOINT} - - name: AZURE_OPENAI_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http deleted file mode 100644 index 5c33700a93..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/AgentsInWorkflows/run-requests.http +++ /dev/null @@ -1,30 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple string input -POST {{endpoint}} -Content-Type: application/json -{ - "input": "Hello, how are you today?" -} - -### Explicit input -POST {{endpoint}} -Content-Type: application/json -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "Hello, how are you today?" - } - ] - } - ] -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile deleted file mode 100644 index fc3d3a1a5b..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "FoundryMultiAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj deleted file mode 100644 index e8c7a434b0..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/FoundryMultiAgent.csproj +++ /dev/null @@ -1,76 +0,0 @@ - - - Exe - net10.0 - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - PreserveNewest - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs deleted file mode 100644 index cc1e3314f0..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/Program.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates a multi-agent workflow with Writer and Reviewer agents -// using Microsoft Foundry AIProjectClient and the Agent Framework WorkflowBuilder. - -#pragma warning disable CA2252 // AIProjectClient and Agents API require opting into preview features - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.Projects; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Workflows; - -var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") - ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); -var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; - -Console.WriteLine($"Using Azure AI endpoint: {endpoint}"); -Console.WriteLine($"Using model deployment: {deploymentName}"); - -// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. -// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid -// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential()); - -// Create Foundry agents -AIAgent writerAgent = await aiProjectClient.CreateAIAgentAsync( - name: "Writer", - model: deploymentName, - instructions: "You are an excellent content writer. You create new content and edit contents based on the feedback."); - -AIAgent reviewerAgent = await aiProjectClient.CreateAIAgentAsync( - name: "Reviewer", - model: deploymentName, - instructions: "You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content. Provide the feedback in the most concise manner possible."); - -try -{ - var workflow = new WorkflowBuilder(writerAgent) - .AddEdge(writerAgent, reviewerAgent) - .Build(); - - Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088"); - await workflow.AsAIAgent().RunAIAgentAsync(); -} -finally -{ - // Cleanup server-side agents - await aiProjectClient.Agents.DeleteAgentAsync(writerAgent.Name); - await aiProjectClient.Agents.DeleteAgentAsync(reviewerAgent.Name); -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md deleted file mode 100644 index 390df95e20..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/README.md +++ /dev/null @@ -1,168 +0,0 @@ -**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). - -Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. - -Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. - -Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. - -# What this sample demonstrates - -This sample demonstrates a **key advantage of code-based hosted agents**: - -- **Multi-agent workflows** - Orchestrate multiple agents working together - -Code-based agents can execute **any C# code** you write. This sample includes a Writer-Reviewer workflow where two agents collaborate: a Writer creates content and a Reviewer provides feedback. - -The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/) and can be deployed to Microsoft Foundry. - -## How It Works - -### Multi-Agent Workflow - -In [Program.cs](Program.cs), the sample creates two agents using `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package: - -- **Writer** - An agent that creates and edits content based on feedback -- **Reviewer** - An agent that provides actionable feedback on the content - -The `WorkflowBuilder` from the [Microsoft.Agents.AI.Workflows](https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/) package connects these agents in a sequential flow: - -1. The Writer receives the initial request and generates content -2. The Reviewer evaluates the content and provides feedback -3. Both agent responses are output to the user - -### Agent Hosting - -The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/), -which provisions a REST API endpoint compatible with the OpenAI Responses protocol. - -## Running the Agent Locally - -### Prerequisites - -Before running this sample, ensure you have: - -1. **Microsoft Foundry Project** - - Project created. - - Chat model deployed (e.g., `gpt-5.4-mini`) - - Note your project endpoint URL and model deployment name - > **Note**: You can right-click the project in the Microsoft Foundry VS Code extension and select `Copy Project Endpoint URL` to get the endpoint. - -2. **Azure CLI** - - Installed and authenticated - - Run `az login` and verify with `az account show` - - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) - -3. **.NET 10.0 SDK or later** - - Verify your version: `dotnet --version` - - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) - -### Environment Variables - -Set the following environment variables: - -**PowerShell:** - -```powershell -# Replace with your actual values -$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" -$env:MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -**Bash:** - -```bash -export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" -export MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -### Running the Sample - -To run the agent, execute the following command in your terminal: - -```bash -dotnet restore -dotnet build -dotnet run -``` - -This will start the hosted agent locally on `http://localhost:8088/`. - -### Interacting with the Agent - -**VS Code:** - -1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. -2. Execute the following commands to start the containerized hosted agent. - ```bash - dotnet restore - dotnet build - dotnet run - ``` -3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "Create a slogan for a new electric SUV that is affordable and fun to drive." -4. Review the agent's response in the playground interface. - -> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. - -**PowerShell (Windows):** - -```powershell -$body = @{ - input = "Create a slogan for a new electric SUV that is affordable and fun to drive" - stream = $false -} | ConvertTo-Json - -Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" -``` - -**Bash/curl (Linux/macOS):** - -```bash -curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ - -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive","stream":false}' -``` - -You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. - -The Writer agent will generate content based on your prompt, and the Reviewer agent will provide feedback on the output. - -## Deploying the Agent to Microsoft Foundry - -**Preparation (required)** - -Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. - -To deploy the hosted agent: - -1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. - -2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. - -3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. - -**What the deploy flow does for you:** - -- Creates or obtains an Azure Container Registry for the target project. -- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). -- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). -- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. - -## MSI Configuration in the Azure Portal - -This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. - -To configure the Managed Identity: - -1. In the Azure Portal, open the Foundry Project. -2. Select "Access control (IAM)" from the left-hand menu. -3. Click "Add" and choose "Add role assignment". -4. In the role selection, search for and select "Azure AI User", then click "Next". -5. For "Assign access to", choose "Managed identity". -6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". -7. Click "Review + assign" to complete the assignment. -8. Allow a few minutes for the role assignment to propagate before running the application. - -## Additional Resources - -- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) -- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml deleted file mode 100644 index 79d848fa5a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/agent.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml - -name: FoundryMultiAgent -displayName: "Foundry Multi-Agent Workflow" -description: > - A multi-agent workflow featuring a Writer and Reviewer that collaborate - to create and refine content using Microsoft Foundry PersistentAgentsClient. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Multi-Agent Workflow - - Writer-Reviewer - - Content Creation -template: - kind: hosted - name: FoundryMultiAgent - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: MODEL_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json deleted file mode 100644 index eae0c9ec3f..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/appsettings.Development.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "AZURE_AI_PROJECT_ENDPOINT": "https://.services.ai.azure.com/api/projects/", - "MODEL_DEPLOYMENT_NAME": "gpt-5.4-mini" -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http deleted file mode 100644 index 2fcdb2499e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundryMultiAgent/run-requests.http +++ /dev/null @@ -1,34 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple string input - Content creation request -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "Create a slogan for a new electric SUV that is affordable and fun to drive", - "stream": false -} - -### Explicit input format -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "Write a short product description for a smart water bottle that tracks hydration" - } - ] - } - ], - "stream": false -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile deleted file mode 100644 index 0d1141cc69..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "FoundrySingleAgent.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj deleted file mode 100644 index 70df458d90..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/FoundrySingleAgent.csproj +++ /dev/null @@ -1,67 +0,0 @@ - - - Exe - net10.0 - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs deleted file mode 100644 index c09a0a4a82..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/Program.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle. -// Uses Microsoft Agent Framework with Microsoft Foundry. -// Ready for deployment to Foundry Hosted Agent service. - -#pragma warning disable CA2252 // AIProjectClient and Agents API require opting into preview features - -using System.ComponentModel; -using System.Globalization; -using System.Text; - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.Projects; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -// Get configuration from environment variables -var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") - ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); -var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; -Console.WriteLine($"Project Endpoint: {endpoint}"); -Console.WriteLine($"Model Deployment: {deploymentName}"); -// Simulated hotel data for Seattle -var seattleHotels = new[] -{ - new Hotel("Contoso Suites", 189, 4.5, "Downtown"), - new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"), - new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"), - new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"), - new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"), - new Hotel("Relecloud Hotel", 99, 3.8, "University District"), -}; - -[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")] -string GetAvailableHotels( - [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, - [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, - [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) -{ - try - { - // Parse dates - if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn)) - { - return "Error parsing check-in date. Please use YYYY-MM-DD format."; - } - - if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut)) - { - return "Error parsing check-out date. Please use YYYY-MM-DD format."; - } - - // Validate dates - if (checkOut <= checkIn) - { - return "Error: Check-out date must be after check-in date."; - } - - var nights = (checkOut - checkIn).Days; - - // Filter hotels by price - var availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); - - if (availableHotels.Count == 0) - { - return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; - } - - // Build response - var result = new StringBuilder(); - result - .AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):") - .AppendLine(); - - foreach (var hotel in availableHotels) - { - var totalCost = hotel.PricePerNight * nights; - result - .AppendLine($"**{hotel.Name}**") - .AppendLine($" Location: {hotel.Location}") - .AppendLine($" Rating: {hotel.Rating}/5") - .AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})") - .AppendLine(); - } - - return result.ToString(); - } - catch (Exception ex) - { - return $"Error processing request. Details: {ex.Message}"; - } -} - -// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. -// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid -// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential()); - -// Create Foundry agent with hotel search tool -AIAgent agent = await aiProjectClient.CreateAIAgentAsync( - name: "SeattleHotelAgent", - model: deploymentName, - instructions: """ - You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. - - When a user asks about hotels in Seattle: - 1. Ask for their check-in and check-out dates if not provided - 2. Ask about their budget preferences if not mentioned - 3. Use the GetAvailableHotels tool to find available options - 4. Present the results in a friendly, informative way - 5. Offer to help with additional questions about the hotels or Seattle - - Be conversational and helpful. If users ask about things outside of Seattle hotels, - politely let them know you specialize in Seattle hotel recommendations. - """, - tools: [AIFunctionFactory.Create(GetAvailableHotels)]); - -try -{ - Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088"); - await agent.RunAIAgentAsync(telemetrySourceName: "Agents"); -} -finally -{ - // Cleanup server-side agent - await aiProjectClient.Agents.DeleteAgentAsync(agent.Name); -} - -// Hotel record for simulated data -internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md deleted file mode 100644 index 43c5a6cb69..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/README.md +++ /dev/null @@ -1,167 +0,0 @@ -**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). - -Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. - -Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. - -Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. - -# What this sample demonstrates - -This sample demonstrates a **key advantage of code-based hosted agents**: - -- **Local C# tool execution** - Run custom C# methods as agent tools - -Code-based agents can execute **any C# code** you write. This sample includes a Seattle Hotel Agent with a `GetAvailableHotels` tool that searches for available hotels based on check-in/check-out dates and budget preferences. - -The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme) and can be deployed to Microsoft Foundry. - -## How It Works - -### Local Tools Integration - -In [Program.cs](Program.cs), the agent uses `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package to create a Foundry agent with a local C# method (`GetAvailableHotels`) that simulates a hotel availability API. This demonstrates how code-based agents can execute custom server-side logic that prompt agents cannot access. - -The tool accepts: - -- **checkInDate** - Check-in date in YYYY-MM-DD format -- **checkOutDate** - Check-out date in YYYY-MM-DD format -- **maxPrice** - Maximum price per night in USD (optional, defaults to $500) - -### Agent Hosting - -The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme), -which provisions a REST API endpoint compatible with the OpenAI Responses protocol. - -## Running the Agent Locally - -### Prerequisites - -Before running this sample, ensure you have: - -1. **Microsoft Foundry Project** - - Project created. - - Chat model deployed (e.g., `gpt-5.4-mini`) - - Note your project endpoint URL and model deployment name - -2. **Azure CLI** - - Installed and authenticated - - Run `az login` and verify with `az account show` - - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) - -3. **.NET 10.0 SDK or later** - - Verify your version: `dotnet --version` - - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) - -### Environment Variables - -Set the following environment variables (matching `agent.yaml`): - -- `AZURE_AI_PROJECT_ENDPOINT` - Your Microsoft Foundry project endpoint URL (required) -- `MODEL_DEPLOYMENT_NAME` - The deployment name for your chat model (defaults to `gpt-5.4-mini`) - -**PowerShell:** - -```powershell -# Replace with your actual values -$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" -$env:MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -**Bash:** - -```bash -export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" -export MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" -``` - -### Running the Sample - -To run the agent, execute the following command in your terminal: - -```bash -dotnet restore -dotnet build -dotnet run -``` - -This will start the hosted agent locally on `http://localhost:8088/`. - -### Interacting with the Agent - -**VS Code:** - -1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. -2. Execute the following commands to start the containerized hosted agent. - - ```bash - dotnet restore - dotnet build - dotnet run - ``` - -3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night." -4. The agent will use the GetAvailableHotels tool to search for available hotels matching your criteria. - -> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. - -**PowerShell (Windows):** - -```powershell -$body = @{ - input = "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under `$200 per night" - stream = $false -} | ConvertTo-Json - -Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" -``` - -**Bash/curl (Linux/macOS):** - -```bash -curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ - -d '{"input": "Find me hotels in Seattle for March 20-23, 2025 under $200 per night","stream":false}' -``` - -You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. - -The agent will use the `GetAvailableHotels` tool to search for available hotels matching your criteria. - -## Deploying the Agent to Microsoft Foundry - -**Preparation (required)** - -Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. - -To deploy the hosted agent: - -1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. -2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. -3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. - -**What the deploy flow does for you:** - -- Creates or obtains an Azure Container Registry for the target project. -- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). -- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). -- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. - -## MSI Configuration in the Azure Portal - -This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. - -To configure the Managed Identity: - -1. In the Azure Portal, open the Foundry Project. -2. Select "Access control (IAM)" from the left-hand menu. -3. Click "Add" and choose "Add role assignment". -4. In the role selection, search for and select "Azure AI User", then click "Next". -5. For "Assign access to", choose "Managed identity". -6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". -7. Click "Review + assign" to complete the assignment. -8. Allow a few minutes for the role assignment to propagate before running the application. - -## Additional Resources - -- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) -- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml deleted file mode 100644 index eacb3ec2c0..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/agent.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml - -name: FoundrySingleAgent -displayName: "Foundry Single Agent with Local Tools" -description: > - A travel assistant agent that helps users find hotels in Seattle. - Demonstrates local C# tool execution - a key advantage of code-based - hosted agents over prompt agents. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Local Tools - - Travel Assistant - - Hotel Search -template: - kind: hosted - name: FoundrySingleAgent - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_AI_PROJECT_ENDPOINT - value: ${AZURE_AI_PROJECT_ENDPOINT} - - name: MODEL_DEPLOYMENT_NAME - value: gpt-5.4-mini -resources: - - name: "gpt-5.4-mini" - kind: model - id: gpt-5.4-mini \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http deleted file mode 100644 index 4f2e87e097..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/FoundrySingleAgent/run-requests.http +++ /dev/null @@ -1,52 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple hotel search - budget under $200 -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night", - "stream": false -} - -### Hotel search with higher budget -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "Find me hotels in Seattle for March 20-23, 2025 under $250 per night", - "stream": false -} - -### Ask for recommendations without dates (agent should ask for clarification) -POST {{endpoint}} -Content-Type: application/json - -{ - "input": "What hotels do you recommend in Seattle?", - "stream": false -} - -### Explicit input format -POST {{endpoint}} -Content-Type: application/json - -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "I'm looking for a hotel in Seattle from 2025-04-01 to 2025-04-05, my budget is $150 per night maximum" - } - ] - } - ], - "stream": false -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md deleted file mode 100644 index a2b603cc34..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV1/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Hosted Agent Samples - -These samples demonstrate how to build and host AI agents using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme). Each sample can be run locally and deployed to Microsoft Foundry as a hosted agent. - -## Samples - -| Sample | Description | -|--------|-------------| -| [`AgentWithLocalTools`](./AgentWithLocalTools/) | Local C# function tool execution (Seattle hotel search) | -| [`AgentThreadAndHITL`](./AgentThreadAndHITL/) | Human-in-the-loop with `ApprovalRequiredAIFunction` and thread persistence | -| [`AgentWithHostedMCP`](./AgentWithHostedMCP/) | Hosted MCP server tool (Microsoft Learn search) | -| [`AgentWithTextSearchRag`](./AgentWithTextSearchRag/) | RAG with `TextSearchProvider` (Contoso Outdoors) | -| [`AgentsInWorkflows`](./AgentsInWorkflows/) | Sequential workflow pipeline (translation chain) | -| [`FoundryMultiAgent`](./FoundryMultiAgent/) | Multi-agent Writer-Reviewer workflow using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | -| [`FoundrySingleAgent`](./FoundrySingleAgent/) | Single agent with local C# tool execution (hotel search) using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | - -## Common Prerequisites - -Before running any sample, ensure you have: - -1. **.NET 10 SDK** or later — [Download](https://dotnet.microsoft.com/download/dotnet/10.0) -2. **Azure CLI** installed — [Install guide](https://learn.microsoft.com/cli/azure/install-azure-cli) -3. **Azure OpenAI** or **Microsoft Foundry project** with a chat model deployed (e.g., `gpt-5.4-mini`) - -### Authenticate with Azure CLI - -All samples use `DefaultAzureCredential` for authentication, which automatically probes multiple credential sources (environment variables, managed identity, Azure CLI, etc.). For local development, the simplest approach is to authenticate via Azure CLI: - -```powershell -az login -az account show # Verify the correct subscription -``` - -### Common Environment Variables - -Most samples require one or more of these environment variables: - -| Variable | Used By | Description | -|----------|---------|-------------| -| `AZURE_OPENAI_ENDPOINT` | Most samples | Your Azure OpenAI resource endpoint URL | -| `AZURE_OPENAI_DEPLOYMENT_NAME` | Most samples | Chat model deployment name (defaults to `gpt-5.4-mini`) | -| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Microsoft Foundry project endpoint | -| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Chat model deployment name (defaults to `gpt-5.4-mini`) | - -See each sample's README for the specific variables required. - -## Microsoft Foundry Setup (for samples that use Foundry) - -Some samples (`AgentWithLocalTools`, `FoundrySingleAgent`, `FoundryMultiAgent`) connect to a Microsoft Foundry project. If you're using these samples, you'll need additional setup. - -### Azure AI Developer Role - -Some Foundry operations require the **Azure AI Developer** role on the Cognitive Services resource. Even if you created the project, you may not have this role by default. - -```powershell -az role assignment create ` - --role "Azure AI Developer" ` - --assignee "your-email@microsoft.com" ` - --scope "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.CognitiveServices/accounts/{account-name}" -``` - -> **Note**: You need **Owner** or **User Access Administrator** permissions on the resource to assign roles. If you don't have this, you may need to request JIT (Just-In-Time) elevated access via [Azure PIM](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadmigratedresource). - -For more details on permissions, see [Microsoft Foundry Permissions](https://aka.ms/FoundryPermissions). - -## Running a Sample - -Each sample runs as a standalone hosted agent on `http://localhost:8088/`: - -```powershell -cd -dotnet run -``` - -### Interacting with the Agent - -Each sample includes a `run-requests.http` file for testing with the [VS Code REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension, or you can use PowerShell: - -```powershell -$body = @{ input = "Your question here" } | ConvertTo-Json -Invoke-RestMethod -Uri "http://localhost:8088/responses" -Method Post -Body $body -ContentType "application/json" -``` - -## Deploying to Microsoft Foundry - -Each sample includes a `Dockerfile` and `agent.yaml` for deployment. To deploy your agent to Microsoft Foundry, follow the [hosted agents deployment guide](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents). - -## Troubleshooting - -### `PermissionDenied` — lacks `agents/write` data action - -Assign the **Azure AI Developer** role to your user. See [Azure AI Developer Role](#azure-ai-developer-role) above. - -### Multi-framework error when running `dotnet run` - -If you see "Your project targets multiple frameworks", specify the framework: - -```powershell -dotnet run --framework net10.0 -``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/EchoAIAgent.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoAIAgent.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/EchoAIAgent.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs similarity index 97% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs index 9834e7577a..f0101a57f4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/EchoInvocationHandler.cs @@ -2,7 +2,6 @@ using Azure.AI.AgentServer.Invocations; using Microsoft.Agents.AI; -using Microsoft.AspNetCore.Http; namespace HostedInvocationsEchoAgent; diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.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/HostedAgentsV2/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md similarity index 95% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md index 7133c43cf1..91bb9aac53 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/README.md @@ -11,7 +11,7 @@ A minimal echo agent hosted as a Foundry Hosted Agent using the **Invocations pr This project uses `ProjectReference` to build against the local Agent Framework source. ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent +cd dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Invocations-EchoAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs similarity index 95% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs index 54f8022d1b..db291458c2 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/InvocationsAIAgent.cs @@ -59,7 +59,7 @@ protected override async Task RunCoreAsync( CancellationToken cancellationToken = default) { var inputText = GetLastUserText(messages); - var responseText = await SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); + var responseText = await this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); return new AgentResponse(new ChatMessage(ChatRole.Assistant, responseText)); } @@ -73,7 +73,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA // The Invocations protocol returns a complete response (no SSE streaming), // so we yield a single update with the full text. var inputText = GetLastUserText(messages); - var responseText = await SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); + var responseText = await this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false); yield return new AgentResponseUpdate { diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/HostedChatClientAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md similarity index 94% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md index 0c5ce36cfe..ace8892572 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md @@ -13,7 +13,7 @@ A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Copy the template and fill in your project endpoint: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env` and set your Azure AI Foundry project endpoint: @@ -25,14 +25,14 @@ ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ``` -> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. ## Running directly (contributors) This project uses `ProjectReference` to build against the local Agent Framework source. ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-ChatClientAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/HostedFoundryAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md similarity index 95% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md index b95e7ff808..8265a80632 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md @@ -15,7 +15,7 @@ This is the **Foundry hosting** pattern — the agent's behavior is configured i Copy the template and fill in your project endpoint: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env` and set your Azure AI Foundry project endpoint: @@ -26,7 +26,7 @@ ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development ``` -> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. +> **Note:** `.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. @@ -35,7 +35,7 @@ You also need to set `AGENT_NAME` — the name of the Foundry-managed agent to h This project uses `ProjectReference` to build against the local Agent Framework source. ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent AGENT_NAME= dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-FoundryAgent/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/HostedLocalTools.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md similarity index 94% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md index 3c41803b95..8016ff7ae9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md @@ -15,7 +15,7 @@ The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` Copy the template and fill in your project endpoint: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env` and set your Azure AI Foundry project endpoint: @@ -27,14 +27,14 @@ ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ``` -> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. ## Running directly (contributors) This project uses `ProjectReference` to build against the local Agent Framework source. ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools AGENT_NAME=hosted-local-tools dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-LocalTools/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/HostedMcpTools.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md similarity index 96% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md index 0990dfc6bd..3313633b0f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md @@ -28,7 +28,7 @@ A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool i Copy the template and fill in your values: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env`: @@ -42,7 +42,7 @@ GITHUB_PAT=ghp_your_token_here ## Running directly (contributors) ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-McpTools/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/HostedTextRag.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md similarity index 94% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md index 75c9dba797..5e4e5140c0 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md @@ -15,7 +15,7 @@ This sample demonstrates how to add knowledge grounding to a hosted agent withou Copy the template and fill in your project endpoint: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env` and set your Azure AI Foundry project endpoint: @@ -28,14 +28,14 @@ AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o AZURE_BEARER_TOKEN= ``` -> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. ## Running directly (contributors) This project uses `ProjectReference` to build against the local Agent Framework source. ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag AGENT_NAME=hosted-text-rag dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-TextRag/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/.env.local rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile.contributor similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile.contributor diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/HostedWorkflows.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/HostedWorkflows.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/HostedWorkflows.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/Properties/launchSettings.json rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md similarity index 94% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md index 0bb000aaa1..2a669b7957 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md @@ -13,7 +13,7 @@ A hosted agent that demonstrates **multi-agent workflow orchestration**. Three t Copy the template and fill in your project endpoint: ```bash -cp .env.local .env +cp .env.example .env ``` Edit `.env` and set your Azure AI Foundry project endpoint: @@ -25,12 +25,12 @@ ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ``` -> **Note:** `.env` is gitignored. The `.env.local` template is checked in as a reference. +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. ## Running directly (contributors) ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows AGENT_NAME=hosted-workflows dotnet run ``` diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Hosted-Workflows/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/HostedAgentsV2/Using-Samples/SimpleAgent/SimpleAgent.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj From 98d3a81ed4a59bc5a8f370732f3b5fead2646fd0 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:42:59 +0100 Subject: [PATCH 69/75] Remove launchSettings, use .env for port configuration - Delete all launchSettings.json files (port 8088 now comes from ASPNETCORE_URLS in .env) - Add DotNetEnv to Hosted-Invocations-EchoAgent so it loads .env like the responses samples - Create .env.example for EchoAgent with ASPNETCORE_URLS and ASPNETCORE_ENVIRONMENT - Add AGENT_NAME to ChatClientAgent and FoundryAgent .env.example (required by those samples) - Add AZURE_BEARER_TOKEN=DefaultAzureCredential to all .env.example files - Update DevTemporaryTokenCredential in all 6 samples to treat the sentinel value as unavailable, allowing ChainedTokenCredential to fall through to DefaultAzureCredential - Update EchoAgent README with Configuration section --- .../Hosted-Invocations-EchoAgent/.env.example | 2 ++ .../Hosted-Invocations-EchoAgent.csproj | 1 + .../Hosted-Invocations-EchoAgent/Program.cs | 4 ++++ .../Properties/launchSettings.json | 11 ----------- .../Hosted-Invocations-EchoAgent/README.md | 10 ++++++++++ .../responses/Hosted-ChatClientAgent/.env.example | 3 ++- .../responses/Hosted-ChatClientAgent/Program.cs | 2 +- .../Properties/launchSettings.json | 11 ----------- .../responses/Hosted-FoundryAgent/.env.example | 3 ++- .../responses/Hosted-FoundryAgent/Program.cs | 2 +- .../Properties/launchSettings.json | 11 ----------- .../responses/Hosted-LocalTools/.env.example | 1 + .../responses/Hosted-LocalTools/Program.cs | 2 +- .../Hosted-LocalTools/Properties/launchSettings.json | 11 ----------- .../responses/Hosted-McpTools/.env.example | 1 + .../responses/Hosted-McpTools/Program.cs | 2 +- .../Hosted-McpTools/Properties/launchSettings.json | 11 ----------- .../responses/Hosted-TextRag/.env.example | 2 +- .../responses/Hosted-TextRag/Program.cs | 2 +- .../Hosted-TextRag/Properties/launchSettings.json | 11 ----------- .../responses/Hosted-Workflows/.env.example | 1 + .../responses/Hosted-Workflows/Program.cs | 2 +- .../Hosted-Workflows/Properties/launchSettings.json | 11 ----------- 23 files changed, 31 insertions(+), 86 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json 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 new file mode 100644 index 0000000000..46a6ae748c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/.env.example @@ -0,0 +1,2 @@ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development 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 index b84faccf9e..d925172007 100644 --- 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 @@ -12,6 +12,7 @@ + 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 1650253dfc..d5944560ae 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 @@ -1,9 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using Azure.AI.AgentServer.Invocations; +using DotNetEnv; using HostedInvocationsEchoAgent; using Microsoft.Agents.AI; +// Load .env file if present (for local development) +Env.TraversePath().Load(); + var builder = WebApplication.CreateBuilder(args); // Register the echo agent as a singleton (no LLM needed). diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json deleted file mode 100644 index 7722815b12..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Hosted-Invocations-EchoAgent": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} 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 91bb9aac53..5fcfddab22 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 @@ -6,6 +6,16 @@ A minimal echo agent hosted as a Foundry Hosted Agent using the **Invocations pr - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +## Configuration + +Copy the template: + +```bash +cp .env.example .env +``` + +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. + ## Running directly (contributors) This project uses `ProjectReference` to build against the local Agent Framework source. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example index cbf693b3a9..984e8625cf 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example @@ -2,4 +2,5 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o -AZURE_BEARER_TOKEN= +AGENT_NAME=hosted-chat-client-agent +AZURE_BEARER_TOKEN=DefaultAzureCredential diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs index 5e26322079..ee86bbae1b 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs @@ -88,7 +88,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(this._token)) + if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json deleted file mode 100644 index cc21f3dd2e..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Hosted-ChatClientAgent": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} \ No newline at end of file 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 1fefe43ebd..77f0476f8d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/.env.example @@ -1,4 +1,5 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development -AZURE_BEARER_TOKEN= +AGENT_NAME=hosted-foundry-agent +AZURE_BEARER_TOKEN=DefaultAzureCredential 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 11069c6403..b593b16671 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Program.cs @@ -81,7 +81,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(this._token)) + if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json deleted file mode 100644 index b4c4e005d3..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "HostedFoundryAgent": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} \ No newline at end of file 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 6d7831229d..b8fe9e8e7a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/.env.example @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN=DefaultAzureCredential 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 20a9cf079a..f0e3566fe1 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Program.cs @@ -154,7 +154,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(this._token)) + if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json deleted file mode 100644 index ae1bb80b7d..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "HostedLocalTools": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} 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 6d7831229d..b8fe9e8e7a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/.env.example @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN=DefaultAzureCredential 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 0e97e5f84f..a027047a1d 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Program.cs @@ -120,7 +120,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(this._token)) + if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json deleted file mode 100644 index 3042eb4d44..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "HostedMcpTools": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} 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 cbf693b3a9..b8fe9e8e7a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/.env.example @@ -2,4 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o -AZURE_BEARER_TOKEN= +AZURE_BEARER_TOKEN=DefaultAzureCredential 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 45d027f740..33edb58901 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Program.cs @@ -120,7 +120,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private static AccessToken GetAccessToken() { var token = Environment.GetEnvironmentVariable(EnvironmentVariable); - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(token) || token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json deleted file mode 100644 index 932d4e67fc..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "HostedTextRag": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example index 6d7831229d..b8fe9e8e7a 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example @@ -2,3 +2,4 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o +AZURE_BEARER_TOKEN=DefaultAzureCredential diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs index 279daf5f24..558aef11d4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs @@ -87,7 +87,7 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private AccessToken GetAccessToken() { - if (string.IsNullOrEmpty(this._token)) + if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential") { throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set."); } diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json deleted file mode 100644 index 0e2908985a..0000000000 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "HostedWorkflows": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:8088" - } - } -} From 10077bdb130b7293037de8ed4840b92aea59a811 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:02:51 +0100 Subject: [PATCH 70/75] Use placeholder for AGENT_NAME in Hosted-FoundryAgent .env.example --- .../responses/Hosted-FoundryAgent/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 77f0476f8d..c72380d125 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,5 @@ AZURE_AI_PROJECT_ENDPOINT= ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development -AGENT_NAME=hosted-foundry-agent +AGENT_NAME= AZURE_BEARER_TOKEN=DefaultAzureCredential From e6dfdd46f207d86ea79c1e160934b2e6e5d7deba Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:38:29 +0100 Subject: [PATCH 71/75] Move FoundryResponsesHosting to responses/Hosted-WorkflowHandoff, use GetResponsesClient --- dotnet/agent-framework-dotnet.slnx | 2 +- .../Hosted-WorkflowHandoff/.env.example | 4 ++ .../HostedWorkflowHandoff.csproj | 40 +++++++++++++++++++ .../Hosted-WorkflowHandoff}/Pages.cs | 0 .../Hosted-WorkflowHandoff}/Program.cs | 10 +++-- .../ResponseStreamValidator.cs | 2 +- .../FoundryResponsesHosting.csproj | 27 ------------- 7 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj rename dotnet/samples/04-hosting/{FoundryResponsesHosting => FoundryHostedAgents/responses/Hosted-WorkflowHandoff}/Pages.cs (100%) rename dotnet/samples/04-hosting/{FoundryResponsesHosting => FoundryHostedAgents/responses/Hosted-WorkflowHandoff}/Program.cs (95%) rename dotnet/samples/04-hosting/{FoundryResponsesHosting => FoundryHostedAgents/responses/Hosted-WorkflowHandoff}/ResponseStreamValidator.cs (99%) delete mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 67f3331b27..d356f212b2 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -294,7 +294,7 @@ - + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example new file mode 100644 index 0000000000..0f711561be --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example @@ -0,0 +1,4 @@ +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ +AZURE_OPENAI_DEPLOYMENT=gpt-4o +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj new file mode 100644 index 0000000000..3c0df909c9 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj @@ -0,0 +1,40 @@ + + + + Exe + net10.0 + enable + enable + HostedWorkflowHandoff + HostedWorkflowHandoff + false + $(NoWarn);NU1903;NU1605;MAAIW001 + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Pages.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryResponsesHosting/Pages.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Pages.cs diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Program.cs similarity index 95% rename from dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Program.cs index 55e462e9a7..6b2eafddfd 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Program.cs @@ -18,6 +18,7 @@ using System.ComponentModel; using Azure.AI.OpenAI; using Azure.Identity; +using DotNetEnv; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Hosting; @@ -25,6 +26,9 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Client; +// Load .env file if present (for local development) +Env.TraversePath().Load(); + var builder = WebApplication.CreateBuilder(args); // --------------------------------------------------------------------------- @@ -34,7 +38,7 @@ var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; var azureClient = new AzureOpenAIClient(endpoint, new DefaultAzureCredential()); -IChatClient chatClient = azureClient.GetChatClient(deployment).AsIChatClient(); +IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment); // --------------------------------------------------------------------------- // 2. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP @@ -128,9 +132,9 @@ Do NOT answer the question yourself - just route it. 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", (FoundryResponsesHosting.CapturedSseStream captured) => +app.MapPost("/api/validate", (HostedWorkflowHandoff.CapturedSseStream captured) => { - var validator = new FoundryResponsesHosting.ResponseStreamValidator(); + var validator = new HostedWorkflowHandoff.ResponseStreamValidator(); foreach (var evt in captured.Events) { validator.ProcessEvent(evt.EventType, evt.Data); diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/ResponseStreamValidator.cs similarity index 99% rename from dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/ResponseStreamValidator.cs index 5dc1b4c791..75822608e5 100644 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/ResponseStreamValidator.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/ResponseStreamValidator.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace FoundryResponsesHosting; +namespace HostedWorkflowHandoff; /// Captured SSE event for validation. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")] diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj b/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj deleted file mode 100644 index 269754203d..0000000000 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/FoundryResponsesHosting.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - $(NoWarn);NU1903;NU1605;MAAIW001 - - - - - - - - - - - - - - - - - - From 77529dc3ea6e7359a2ddbbbddde56d6530e175ea Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:43:03 +0100 Subject: [PATCH 72/75] Rename Hosted-Workflows to Hosted-Workflow-Simple, Hosted-WorkflowHandoff to Hosted-Workflow-Handoff --- dotnet/agent-framework-dotnet.slnx | 8 ++++---- .../.env.example | 0 .../HostedWorkflowHandoff.csproj | 0 .../Pages.cs | 0 .../Program.cs | 0 .../ResponseStreamValidator.cs | 0 .../.env.example | 0 .../Dockerfile | 0 .../Dockerfile.contributor | 6 +++--- .../HostedWorkflowSimple.csproj} | 4 ++-- .../Program.cs | 0 .../README.md | 12 ++++++------ .../agent.manifest.yaml | 0 .../agent.yaml | 0 14 files changed, 15 insertions(+), 15 deletions(-) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-WorkflowHandoff => Hosted-Workflow-Handoff}/.env.example (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-WorkflowHandoff => Hosted-Workflow-Handoff}/HostedWorkflowHandoff.csproj (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-WorkflowHandoff => Hosted-Workflow-Handoff}/Pages.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-WorkflowHandoff => Hosted-Workflow-Handoff}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-WorkflowHandoff => Hosted-Workflow-Handoff}/ResponseStreamValidator.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/.env.example (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/Dockerfile (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/Dockerfile.contributor (72%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows/HostedWorkflows.csproj => Hosted-Workflow-Simple/HostedWorkflowSimple.csproj} (92%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/Program.cs (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/README.md (91%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/agent.manifest.yaml (100%) rename dotnet/samples/04-hosting/FoundryHostedAgents/responses/{Hosted-Workflows => Hosted-Workflow-Simple}/agent.yaml (100%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d356f212b2..371dbbab8b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -287,14 +287,14 @@ - - + + - - + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/.env.example rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/HostedWorkflowHandoff.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Pages.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Pages.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Pages.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Pages.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/ResponseStreamValidator.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/ResponseStreamValidator.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-WorkflowHandoff/ResponseStreamValidator.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/ResponseStreamValidator.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/.env.example rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/.env.example diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor similarity index 72% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile.contributor rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor index b8dae44c2b..17a924237f 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Dockerfile.contributor +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Dockerfile.contributor @@ -5,8 +5,8 @@ # 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-workflows . -# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-workflows -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflows +# 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. @@ -15,4 +15,4 @@ WORKDIR /app COPY out/ . EXPOSE 8088 ENV ASPNETCORE_URLS=http://+:8088 -ENTRYPOINT ["dotnet", "HostedWorkflows.dll"] +ENTRYPOINT ["dotnet", "HostedWorkflowSimple.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/HostedWorkflows.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj similarity index 92% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/HostedWorkflows.csproj rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj index 2f210a18d8..a5460564a9 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/HostedWorkflows.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj @@ -5,8 +5,8 @@ enable enable false - HostedWorkflows - HostedWorkflows + HostedWorkflowSimple + HostedWorkflowSimple $(NoWarn); diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/Program.cs rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/Program.cs diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md similarity index 91% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md index 2a669b7957..7312df31de 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md @@ -1,4 +1,4 @@ -# Hosted-Workflows +# 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`. @@ -30,7 +30,7 @@ AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o ## Running directly (contributors) ```bash -cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple AGENT_NAME=hosted-workflows dotnet run ``` @@ -49,7 +49,7 @@ Or with curl: ```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-workflows"}' + -d '{"input": "The quick brown fox jumps over the lazy dog", "model": "hosted-workflow-simple"}' ``` The text will be translated through the chain: English → French → Spanish → English. @@ -65,7 +65,7 @@ dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o o ### 2. Build the Docker image ```bash -docker build -f Dockerfile.contributor -t hosted-workflows . +docker build -f Dockerfile.contributor -t hosted-workflow-simple . ``` ### 3. Run the container @@ -74,7 +74,7 @@ docker build -f Dockerfile.contributor -t hosted-workflows . 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-workflows \ + -e AGENT_NAME=hosted-workflow-simple \ -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ --env-file .env \ hosted-workflows @@ -106,4 +106,4 @@ Each agent in the chain receives the output of the previous agent. The final res ## NuGet package users -Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflows.csproj` for the `PackageReference` alternative. +Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowSimple.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.manifest.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.manifest.yaml diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml similarity index 100% rename from dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflows/agent.yaml rename to dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/agent.yaml From 7c9ed7f5d67488b60e62777e36479289833746fa Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:44:41 +0100 Subject: [PATCH 73/75] Remove FoundryResponsesRepl and empty FoundryResponsesHosting directory --- dotnet/agent-framework-dotnet.slnx | 1 - .../Properties/launchSettings.json | 12 -- .../FoundryResponsesRepl.csproj | 21 ---- .../FoundryResponsesRepl/Program.cs | 106 ------------------ .../Properties/launchSettings.json | 12 -- 5 files changed, 152 deletions(-) delete mode 100644 dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json delete mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj delete mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs delete mode 100644 dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 371dbbab8b..5cc3327b8d 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -261,7 +261,6 @@ - diff --git a/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json deleted file mode 100644 index b56d7a9ff4..0000000000 --- a/dotnet/samples/04-hosting/FoundryResponsesHosting/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "FoundryResponsesHosting": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:54747;http://localhost:54748" - } - } -} \ No newline at end of file diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj b/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj deleted file mode 100644 index 15cdf820eb..0000000000 --- a/dotnet/samples/04-hosting/FoundryResponsesRepl/FoundryResponsesRepl.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net10.0 - enable - enable - false - $(NoWarn);NU1903;NU1605 - - - - - - - - - - - - diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs b/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs deleted file mode 100644 index 8b877dc519..0000000000 --- a/dotnet/samples/04-hosting/FoundryResponsesRepl/Program.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Foundry Responses Client REPL -// -// Connects to a Foundry Responses agent running on a given endpoint -// and provides an interactive multi-turn chat REPL. -// -// Usage: -// dotnet run (connects to http://localhost:8088) -// dotnet run -- --endpoint http://localhost:9090 -// dotnet run -- --endpoint https://my-foundry-project.services.ai.azure.com -// -// The endpoint should be running a Foundry Responses server (POST /responses). - -using System.ClientModel; -using Microsoft.Agents.AI; -using OpenAI; -using OpenAI.Responses; - -// ── Parse args ──────────────────────────────────────────────────────────────── - -string endpointUrl = "http://localhost:8088"; -for (int i = 0; i < args.Length - 1; i++) -{ - if (args[i] is "--endpoint" or "-e") - { - endpointUrl = args[i + 1]; - } -} - -// ── Create an agent-framework agent backed by the remote Responses endpoint ── - -// The OpenAI SDK's ResponsesClient can target any OpenAI-compatible endpoint. -// We use a dummy API key since our local server doesn't require auth. -var credential = new ApiKeyCredential( - Environment.GetEnvironmentVariable("RESPONSES_API_KEY") ?? "no-key-needed"); - -var openAiClient = new OpenAIClient( - credential, - new OpenAIClientOptions { Endpoint = new Uri(endpointUrl) }); - -ResponsesClient responsesClient = openAiClient.GetResponsesClient(); - -// Wrap as an agent-framework AIAgent via the OpenAI extensions. -// We pass an empty model since hosted agents use their own model configuration. -AIAgent agent = responsesClient.AsAIAgent( - model: "", - name: "remote-agent"); - -// Create a session so multi-turn context is preserved via previous_response_id -AgentSession session = await agent.CreateSessionAsync(); - -// ── REPL ────────────────────────────────────────────────────────────────────── - -Console.ForegroundColor = ConsoleColor.Cyan; -Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); -Console.WriteLine("║ Foundry Responses Client REPL ║"); -Console.WriteLine($"║ Connected to: {endpointUrl,-41}║"); -Console.WriteLine("║ Type a message or 'quit' to exit ║"); -Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); -Console.ResetColor(); -Console.WriteLine(); - -while (true) -{ - Console.ForegroundColor = ConsoleColor.Green; - Console.Write("You> "); - Console.ResetColor(); - - string? input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) - { - continue; - } - - if (input.Equals("quit", StringComparison.OrdinalIgnoreCase) || - input.Equals("exit", StringComparison.OrdinalIgnoreCase)) - { - break; - } - - try - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write("Agent> "); - Console.ResetColor(); - - // Stream the response token-by-token - await foreach (var update in agent.RunStreamingAsync(input, session)) - { - Console.Write(update); - } - - Console.WriteLine(); - } - catch (Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error: {ex.Message}"); - Console.ResetColor(); - } - - Console.WriteLine(); -} - -Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json b/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json deleted file mode 100644 index 4a939fc693..0000000000 --- a/dotnet/samples/04-hosting/FoundryResponsesRepl/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "FoundryResponsesRepl": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:61980;http://localhost:61981" - } - } -} \ No newline at end of file From 518e90e1140852eb70b635736b5c029b5eb21fe7 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:05:47 +0100 Subject: [PATCH 74/75] 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 --- dotnet/samples/04-hosting/.gitignore | 1 + .../Hosted-Workflow-Handoff/.env.example | 1 + .../Hosted-Workflow-Handoff/Dockerfile | 17 +++ .../Dockerfile.contributor | 19 +++ .../Hosted-Workflow-Handoff/Program.cs | 43 +++++- .../Hosted-Workflow-Handoff/README.md | 126 ++++++++++++++++++ .../agent.manifest.yaml | 30 +++++ .../Hosted-Workflow-Handoff/agent.yaml | 9 ++ .../Hosted-Workflow-Simple/README.md | 2 +- 9 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 dotnet/samples/04-hosting/.gitignore create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml diff --git a/dotnet/samples/04-hosting/.gitignore b/dotnet/samples/04-hosting/.gitignore new file mode 100644 index 0000000000..324c8dcfb3 --- /dev/null +++ b/dotnet/samples/04-hosting/.gitignore @@ -0,0 +1 @@ +**/Properties/launchSettings.json 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 0f711561be..bfb3c97208 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,4 +1,5 @@ AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT=gpt-4o +AZURE_BEARER_TOKEN=DefaultAzureCredential ASPNETCORE_URLS=http://+:8088 ASPNETCORE_ENVIRONMENT=Development diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile new file mode 100644 index 0000000000..14b356ad98 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile @@ -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"] 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 new file mode 100644 index 0000000000..4cc047c8bc --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Dockerfile.contributor @@ -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"] 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 6b2eafddfd..9783aca8f3 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 @@ -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 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 @@ Do NOT answer the question yourself - just route it. 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("triage-workflow")); + // --------------------------------------------------------------------------- // 4. Wire up the agent-framework handler and Responses Server SDK // --------------------------------------------------------------------------- @@ -150,6 +157,13 @@ Do NOT answer the question yourself - just route it. // 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 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)); + } +} 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 new file mode 100644 index 0000000000..643af74551 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md @@ -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://.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. 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 new file mode 100644 index 0000000000..7909463901 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.manifest.yaml @@ -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: [] 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 new file mode 100644 index 0000000000..6b192c4eb6 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/agent.yaml @@ -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 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 7312df31de..0cd438f5f1 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 @@ -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 From 8f5a6ec4a1896e5432eb800bafdf4ad53d2b6c20 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:35:10 +0100 Subject: [PATCH 75/75] Fix AgentFrameworkResponseHandlerTests: implement session methods in test mock agents --- .../AgentFrameworkResponseHandlerTests.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs index 277156c9bf..169dc75e22 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs @@ -699,19 +699,19 @@ protected override Task RunCoreAsync( protected override ValueTask CreateSessionCoreAsync( CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); protected override ValueTask SerializeSessionCoreAsync( AgentSession session, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(JsonDocument.Parse("{}").RootElement); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); } private sealed class ThrowingAgent(Exception exception) : AIAgent @@ -732,19 +732,19 @@ protected override Task RunCoreAsync( protected override ValueTask CreateSessionCoreAsync( CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); protected override ValueTask SerializeSessionCoreAsync( AgentSession session, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(JsonDocument.Parse("{}").RootElement); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); } private sealed class CapturingAgent : AIAgent @@ -776,19 +776,19 @@ protected override Task RunCoreAsync( protected override ValueTask CreateSessionCoreAsync( CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); protected override ValueTask SerializeSessionCoreAsync( AgentSession session, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(JsonDocument.Parse("{}").RootElement); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); } private sealed class CancellationCheckingAgent : AIAgent @@ -813,18 +813,20 @@ protected override Task RunCoreAsync( protected override ValueTask CreateSessionCoreAsync( CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); protected override ValueTask SerializeSessionCoreAsync( AgentSession session, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(JsonDocument.Parse("{}").RootElement); protected override ValueTask DeserializeSessionCoreAsync( JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + new(new SimpleAgentSession()); } + + private sealed class SimpleAgentSession : AgentSession { } }