mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
f2b215a2f6
* 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> * Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clean up tests and sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Bump package version to 0.9.0-hosted.260402.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Fix CA1873: guard LogWarning with IsEnabled check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Catch agent errors and emit response.failed with real error message 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> * 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 <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address breaking changes in 260408 * Bump hosted internal package version * Add UserAgent middleware tests for Foundry hosting * Hosting Samples update * Hosting Samples update * Hosting Samples update * Hosting Samples update * ChatClientAgent working * Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting * Using updates * Update chat client agent for contributor and devs * Foundry Agent Hosting * Address text rag sample working * Version bump * Adding LocalTools + Workflow samples * Removing extra using samples * 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. * .NET: Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix br… (#5287) * 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 <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clean up tests and sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Bump package version to 0.9.0-hosted.260402.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Fix CA1873: guard LogWarning with IsEnabled check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * Catch agent errors and emit response.failed with real error message 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> * 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 <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address breaking changes in 260408 * Bump hosted internal package version * Add UserAgent middleware tests for Foundry hosting * Hosting Samples update * Hosting Samples update * Hosting Samples update * Hosting Samples update * ChatClientAgent working * Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting * Using updates * Update chat client agent for contributor and devs * Foundry Agent Hosting * Address text rag sample working * Version bump * Adding LocalTools + Workflow samples * Removing extra using samples * 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. * 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. * Fix IDE0009: add 'this' qualification in DevTemporaryTokenCredential Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix IDE0009: add 'this' qualification in all HostedAgentsV2 samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CHARSET: add UTF-8 BOM to Hosted-LocalTools and Hosted-Workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix dotnet format: add Async suffix to test methods (IDE1006), fix encoding and style Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * 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. * 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 * 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 * Use placeholder for AGENT_NAME in Hosted-FoundryAgent .env.example * Move FoundryResponsesHosting to responses/Hosted-WorkflowHandoff, use GetResponsesClient * Rename Hosted-Workflows to Hosted-Workflow-Simple, Hosted-WorkflowHandoff to Hosted-Workflow-Handoff * Remove FoundryResponsesRepl and empty FoundryResponsesHosting directory * 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 * Fix AgentFrameworkResponseHandlerTests: implement session methods in test mock agents * .NET: Auto-instrument resolved AIAgents with OpenTelemetry for Foundry Hosted Agents (#5316) * Auto-instrument resolved AIAgents with OpenTelemetry using Core ResponsesSourceName * Add OTel telemetry capture tests for Foundry hosted agent handler * Net: Prepare Foundry Preview Release (#5336) * Prepare Foundry preview release 1.2.0-preview.* Bump VersionPrefix to 1.2.0 and update the preview stamp date. Invert packaging opt-in so only the Foundry preview set produces NuGet packages: - Microsoft.Agents.AI.Abstractions - Microsoft.Agents.AI - Microsoft.Agents.AI.Workflows - Microsoft.Agents.AI.Workflows.Generators - Microsoft.Agents.AI.Foundry Flip IsReleased=false on the preview set so they pick up the -preview.YYMMDD.N suffix. Gate GeneratePackageOnBuild on IsPackable=true. Remove the global IsPackable=true from nuget-package.props so the repo-level default (false) applies to everything else. * Lower preview VersionPrefix to 0.0.1 Retroactive preview publish: bump VersionPrefix and GitTag from 1.2.0 to 0.0.1 so the 5 Foundry preview packages emit as 0.0.1-preview.260417.1. * Net: Publish all packages as 0.0.1-preview.260417.2 (#5341) Revises the Foundry pre-release approach to publish ALL normally packable src projects as preview packages stamped 0.0.1-preview.260417.2, including projects previously flagged IsReleased=true or with a non-default VersionSuffix (rc/alpha). nuget-package.props: - Collapse the four conditional PackageVersion expressions (IsReleaseCandidate, VersionSuffix, default preview, IsReleased stable) into a single unconditional 0.0.1-preview.260417.2. On this preview-only branch every package ships with the same pre-release stamp regardless of per-project flags. - Restore the global IsPackable=true default (offsetting the repo-wide IsPackable=false in Directory.Build.props). Projects that opt out (Mem0, Declarative) already set IsPackable=false AFTER importing this file so they remain non-packable. - Remove the IsReleased-gated EnablePackageValidation line. Package validation does not apply to a 0.0.1 preview. csproj reverts (Abstractions, Agents.AI, Workflows, Workflows.Generators, Foundry): - Revert the IsPackable=true opt-in block introduced in #5336 (now redundant since the props default is true again). - Restore IsReleased=true to its pre-PR value. The setting is now a no-op because the props no longer branches on it. * Bump preview version to 260420.1 and fix AgentServer package deps (#5367) - Bump PackageVersion to 0.0.1-preview.260420.1 - Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by Azure.AI.AgentServer.Responses beta.3) - Replace AgentHostTelemetry.ResponsesSourceName with local constant (type made internal in AgentServer.Core beta.22) Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Hosted agents toolbox support (#5368) * feat: Add Foundry Toolbox (MCP) support to AgentFrameworkResponseHandler Adds support for Foundry Toolsets MCP proxy integration in the hosted agent response handler. Toolsets connect at startup via IHostedService, gating the readiness probe per spec §3.1. MCP tools are injected into every request's ChatOptions and OAuth consent errors (-32006) are intercepted and surfaced as mcp_approval_request + incomplete SSE events. New files: - FoundryToolboxOptions.cs: configuration POCO for toolset names and API version - FoundryToolboxBearerTokenHandler.cs: DelegatingHandler with Azure Bearer token auth, Foundry-Features header injection, and 3x exponential backoff on 429/5xx - McpConsentContext.cs: AsyncLocal-based per-request consent state shared between the tool wrapper and the response handler - ConsentAwareMcpClientTool.cs: AIFunction wrapper that catches -32006 errors and signals consent via shared state and linked CancellationTokenSource - FoundryToolboxService.cs: IHostedService that creates McpClient per toolset at startup and exposes cached tools Modified files: - AgentFrameworkResponseHandler.cs: injects toolbox tools into ChatOptions, sets up linked CTS consent interception, emits mcp_approval_request on -32006 - ServiceCollectionExtensions.cs: adds AddFoundryToolboxes(params string[]) extension - Microsoft.Agents.AI.Foundry.csproj: adds ModelContextProtocol and Azure.Identity dependencies under NETCoreApp condition Sample: - Hosted-Toolbox: minimal hosted agent sample using AddFoundryToolboxes * Rename toolset to toolbox in user-facing API; rename ConsentAwareMcpClientTool to ConsentAwareMcpClientAIFunction * Add HostedMcpToolboxAITool for client-selectable Foundry toolboxes Introduces HostedMcpToolboxAITool, a marker tool subclassing HostedMcpServerTool that rides the OpenAI Responses 'mcp' wire format to let clients request a specific Foundry toolbox per request. - New FoundryAITool.CreateHostedMcpToolbox(name, version?) factory. - FoundryToolboxOptions.StrictMode (default true) rejects unregistered toolboxes; set to false to allow lazy-open on first use. - FoundryToolboxService.GetToolboxToolsAsync(name, version?) resolves cached or lazy-opened MCP tools. - AgentFrameworkResponseHandler parses request.Tools for foundry-toolbox://name[?version=v] markers and injects resolved tools per request, merging with pre-registered ones. - Unit tests for marker parsing and strict-mode resolution. * Bump Azure.AI.Projects to 2.1.0-alpha; add ToolboxRecord/ToolboxVersion factory overloads + tests * Fix PR review issues: retry off-by-one, URI encoding, docs, tests, build - Fix off-by-one in FoundryToolboxBearerTokenHandler retry loop (4 attempts → 3) - URI-encode version parameter in HostedMcpToolboxAITool.BuildAddress - Add XML doc clarifying version pinning is reserved for future use - Add comment clarifying AddHostedService deduplication safety - Fix DevTemporaryTokenCredential expiry to use DateTimeOffset.MaxValue - Fix AgentCard ambiguity in A2AServer sample with using alias - Add 18 new unit tests for retry handler and ReadMcpToolboxMarkers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Hosted agent adapter (#5371) * Bump preview version to 260420.1 and fix AgentServer package deps - Bump PackageVersion to 0.0.1-preview.260420.1 - Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by Azure.AI.AgentServer.Responses beta.3) - Replace AgentHostTelemetry.ResponsesSourceName with local constant (type made internal in AgentServer.Core beta.22) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy the CA1873 analyzer rule which flags potentially expensive argument evaluation when logging is disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Hosted agent adapter (#5374) * Bump preview version to 260420.1 and fix AgentServer package deps - Bump PackageVersion to 0.0.1-preview.260420.1 - Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by Azure.AI.AgentServer.Responses beta.3) - Replace AgentHostTelemetry.ResponsesSourceName with local constant (type made internal in AgentServer.Core beta.22) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy the CA1873 analyzer rule which flags potentially expensive argument evaluation when logging is disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bumping NuGet version --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Hosted agent adapter (#5406) * Bump preview version to 260420.1 and fix AgentServer package deps - Bump PackageVersion to 0.0.1-preview.260420.1 - Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by Azure.AI.AgentServer.Responses beta.3) - Replace AgentHostTelemetry.ResponsesSourceName with local constant (type made internal in AgentServer.Core beta.22) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy the CA1873 analyzer rule which flags potentially expensive argument evaluation when logging is disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bumping NuGet version * Restore conditional versioning, remove dev feed, bump Azure.AI.Projects to beta.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Hosted agent adapter (#5408) * Bump preview version to 260420.1 and fix AgentServer package deps - Bump PackageVersion to 0.0.1-preview.260420.1 - Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by Azure.AI.AgentServer.Responses beta.3) - Replace AgentHostTelemetry.ResponsesSourceName with local constant (type made internal in AgentServer.Core beta.22) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy the CA1873 analyzer rule which flags potentially expensive argument evaluation when logging is disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bumping NuGet version * Restore conditional versioning, remove dev feed, bump Azure.AI.Projects to beta.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #5312 review comments - Add comment explaining NU1903 suppression (Microsoft.Bcl.Memory transitive vuln) - Remove NU1903 from sample/test projects where not needed - Fix Dockerfile ENTRYPOINT mismatch in Hosted-Workflow-Simple - Align agent name to 'hosted-workflow-simple' in agent.yaml and README - Fix Hosted-McpTools README: replace GitHub PAT refs with Microsoft Learn - Fix session persistence: only persist when client provides conversation ID - Upgrade IsNullOrEmpty to IsNullOrWhiteSpace for session ID checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Split Foundry into stable V1 and preview Hosting package Extract hosted agent functionality from Microsoft.Agents.AI.Foundry into a new Microsoft.Agents.AI.Foundry.Hosting preview package. This resolves NU5104 build errors caused by the stable Foundry package depending on prerelease Azure SDK packages (Azure.AI.AgentServer.Responses, Azure.AI.Projects beta). Changes: - Create Microsoft.Agents.AI.Foundry.Hosting with VersionSuffix=preview, targeting .NET Core only (net8.0/9.0/10.0) - Move all Hosting/ source files to the new project - Move ToolboxRecord/ToolboxVersion overloads to FoundryAIToolExtensions - Revert Azure.AI.Projects to 2.0.0 in Directory.Packages.props; Hosting uses VersionOverride for 2.1.0-beta.1 - Clean V1 Foundry csproj: remove beta deps, ASP.NET Core ref, hosting conditionals - Update 8 hosted agent sample projects to reference Foundry.Hosting - Split unit tests: ToolboxRecord/ToolboxVersion tests moved to Hosting/ - Add Foundry.Hosting to solution file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments: experimental attrs, doc fixes, token propagation - Add [Experimental(OPENAI001)] to all 7 public Hosting types per reviewer request - Fix McpConsentContext XML doc: 'Thread-static' -> 'Async-local' (AsyncLocal flows with ExecutionContext, not thread-static) - Expand UserAgentMiddleware test regex to match prerelease versions (e.g. 1.0.0-rc.4) - Propagate CancellationToken in AgentFrameworkResponseHandler session save Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unnecessary MEAI001 suppression from stable Foundry package MEAI001 was a leftover from when Hosting code lived in the same project. The stable V1 Foundry package builds clean without it, and suppressing experimental diagnostics in a released package can hide unintentional exposure of experimental APIs to consumers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Foundry.Hosting to release solution filter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <bentho@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
471 lines
23 KiB
C#
471 lines
23 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
||
|
||
/// <summary>
|
||
/// Static HTML pages served by the sample application.
|
||
/// </summary>
|
||
internal static class Pages
|
||
{
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Homepage
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
internal const string Home = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Foundry Responses Hosting — Demos</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
|
||
main { width: 100%; max-width: 700px; }
|
||
h1 { font-size: 1.5rem; margin-bottom: .5rem; color: #1a1a1a; }
|
||
.subtitle { color: #555; margin-bottom: 2rem; line-height: 1.5; }
|
||
.cards { display: flex; flex-direction: column; gap: 1rem; }
|
||
.card { background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 1.5rem; text-decoration: none; color: inherit; transition: box-shadow .15s, transform .15s; }
|
||
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.1); transform: translateY(-2px); }
|
||
.card h2 { font-size: 1.15rem; color: #0066cc; margin-bottom: .4rem; }
|
||
.card p { color: #555; line-height: 1.5; font-size: .9rem; }
|
||
.card .tags { margin-top: .6rem; display: flex; gap: .4rem; flex-wrap: wrap; }
|
||
.card .tag { background: #e8f0fe; color: #1a73e8; padding: .15rem .5rem; border-radius: 12px; font-size: .75rem; }
|
||
footer { margin-top: 2rem; font-size: .8rem; color: #999; text-align: center; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>🚀 Foundry Responses Hosting</h1>
|
||
<p class="subtitle">
|
||
Agent-framework agents hosted via the Azure AI Responses Server SDK.<br/>
|
||
Each demo registers a different agent and serves it through <code>POST /responses</code>.
|
||
</p>
|
||
<div class="cards">
|
||
<a class="card" href="/tool-demo">
|
||
<h2>🔧 Tool Demo</h2>
|
||
<p>An agent with local function tools (time, weather) and remote MCP tools from
|
||
Microsoft Learn for documentation search.</p>
|
||
<div class="tags">
|
||
<span class="tag">Local Tools</span>
|
||
<span class="tag">MCP</span>
|
||
<span class="tag">Microsoft Learn</span>
|
||
<span class="tag">Streaming</span>
|
||
</div>
|
||
</a>
|
||
<a class="card" href="/workflow-demo">
|
||
<h2>🔀 Workflow Demo</h2>
|
||
<p>A triage workflow that routes questions to specialist agents — a Code Expert
|
||
or a Creative Writer — using agent handoffs.</p>
|
||
<div class="tags">
|
||
<span class="tag">Workflow</span>
|
||
<span class="tag">Handoffs</span>
|
||
<span class="tag">Multi-Agent</span>
|
||
<span class="tag">Triage</span>
|
||
</div>
|
||
</a>
|
||
|
||
</div>
|
||
<footer>
|
||
All demos share the same <code>/responses</code> endpoint.
|
||
The <code>model</code> field in the request selects which agent handles it.
|
||
</footer>
|
||
</main>
|
||
</body>
|
||
</html>
|
||
""";
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Tool Demo
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
internal const string ToolDemo = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Tool Demo — Foundry Responses Hosting</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
|
||
main { width: 100%; max-width: 800px; }
|
||
h1 { font-size: 1.2rem; margin-bottom: .3rem; color: #333; }
|
||
.subtitle { font-size: .85rem; color: #666; margin-bottom: .8rem; }
|
||
a.back { font-size: .85rem; color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 1rem; }
|
||
#chat { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; height: 56vh; overflow-y: auto; margin-bottom: 1rem; }
|
||
.msg { margin-bottom: .75rem; line-height: 1.6; }
|
||
.msg.user { color: #0066cc; }
|
||
.msg.assistant { color: #333; }
|
||
.msg .role { font-weight: 600; margin-right: .25rem; }
|
||
.tool-call { background: #f0f4ff; border-left: 3px solid #4a90d9; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; font-family: 'Cascadia Code', 'Fira Code', monospace; }
|
||
.tool-call .tool-icon { margin-right: .3rem; }
|
||
form { display: flex; gap: .5rem; }
|
||
input { flex: 1; padding: .6rem .8rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
|
||
button { padding: .6rem 1.2rem; background: #0066cc; color: #fff; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; }
|
||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||
#status { font-size: .85rem; color: #888; margin-top: .5rem; }
|
||
.suggestions { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
|
||
.suggestions button { padding: .3rem .7rem; font-size: .8rem; background: #e8f0fe; color: #1a73e8; border: 1px solid #c5d8f8; border-radius: 16px; cursor: pointer; }
|
||
.suggestions button:hover { background: #d2e3fc; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<a class="back" href="/">← Back to demos</a>
|
||
<h1>🔧 Tool Demo</h1>
|
||
<p class="subtitle">Agent with local tools (time, weather) + Microsoft Learn MCP (docs search)</p>
|
||
<div class="suggestions">
|
||
<button onclick="sendText('What time is it in Tokyo?')">🕐 Time in Tokyo</button>
|
||
<button onclick="sendText('What is the weather in Seattle?')">🌤️ Weather in Seattle</button>
|
||
<button onclick="sendText('How do I create an Azure Function using the CLI?')">📚 Azure Functions docs</button>
|
||
<button onclick="sendText('What is Microsoft Agent Framework?')">📚 Agent Framework</button>
|
||
</div>
|
||
<div id="chat"></div>
|
||
<form id="form">
|
||
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Azure AI Foundry'" autocomplete="off" autofocus />
|
||
<button type="submit">Send</button>
|
||
</form>
|
||
<div id="status"></div>
|
||
</main>
|
||
<script src="/js/sse-validator.js"></script>
|
||
<script>
|
||
const AGENT = 'tool-agent';
|
||
const chat = document.getElementById('chat');
|
||
const form = document.getElementById('form');
|
||
const input = document.getElementById('input');
|
||
const status = document.getElementById('status');
|
||
|
||
function escapeHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
function addMsg(role, html) {
|
||
const d = document.createElement('div');
|
||
d.className = 'msg ' + role; d.innerHTML = html;
|
||
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
|
||
}
|
||
|
||
function addToolCall(name) {
|
||
const d = document.createElement('div');
|
||
d.className = 'tool-call';
|
||
d.innerHTML = '<span class="tool-icon">🔧</span> Calling <b>' + escapeHtml(name) + '</b>…';
|
||
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
|
||
}
|
||
|
||
function sendText(t) { input.value = t; form.dispatchEvent(new Event('submit')); }
|
||
|
||
form.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const text = input.value.trim(); if (!text) return;
|
||
input.value = '';
|
||
addMsg('user', '<span class="role">You:</span>' + escapeHtml(text));
|
||
|
||
const btn = form.querySelector('button[type="submit"]');
|
||
btn.disabled = true; status.textContent = 'Streaming…';
|
||
|
||
let fullText = '', assistantDiv = null;
|
||
const toolCalls = {};
|
||
const validator = new SseValidator();
|
||
|
||
try {
|
||
const resp = await fetch('/responses', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model: AGENT, stream: true, input: text })
|
||
});
|
||
if (!resp.ok) { status.textContent = 'Error ' + resp.status; btn.disabled = false; return; }
|
||
|
||
const reader = resp.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buf = '', curEvt = null;
|
||
while (true) {
|
||
const { done, value } = await reader.read(); if (done) break;
|
||
buf += decoder.decode(value, { stream: true });
|
||
const lines = buf.split('\n'); buf = lines.pop();
|
||
for (const line of lines) {
|
||
if (line.startsWith('event: ')) { curEvt = line.slice(7).trim(); continue; }
|
||
if (!line.startsWith('data: ')) continue;
|
||
const d = line.slice(6).trim(); if (d === '[DONE]') continue;
|
||
try {
|
||
const evt = JSON.parse(d);
|
||
validator.capture(curEvt || evt.type || 'unknown', d);
|
||
curEvt = null;
|
||
if (evt.type === 'response.output_item.added' && evt.item?.type === 'function_call') {
|
||
const id = evt.item.id;
|
||
toolCalls[id] = { name: evt.item.name || '?', args: '', el: addToolCall(evt.item.name || '?') };
|
||
status.textContent = 'Calling tool: ' + (evt.item.name || '…');
|
||
}
|
||
if (evt.type === 'response.function_call_arguments.delta' && evt.item_id && toolCalls[evt.item_id])
|
||
toolCalls[evt.item_id].args += (evt.delta || '');
|
||
if (evt.type === 'response.function_call_arguments.done' && evt.item_id && toolCalls[evt.item_id]) {
|
||
const tc = toolCalls[evt.item_id];
|
||
let args = tc.args; try { args = JSON.stringify(JSON.parse(args), null, 0); } catch {}
|
||
tc.el.innerHTML = '<span class="tool-icon">✅</span> Called <b>' + escapeHtml(tc.name) + '</b>(' + escapeHtml(args) + ')';
|
||
}
|
||
if (evt.type === 'response.output_text.delta') {
|
||
if (!assistantDiv) assistantDiv = addMsg('assistant', '<span class="role">Agent:</span>');
|
||
fullText += evt.delta;
|
||
assistantDiv.innerHTML = '<span class="role">Agent:</span>' + escapeHtml(fullText);
|
||
chat.scrollTop = chat.scrollHeight;
|
||
status.textContent = 'Streaming…';
|
||
}
|
||
} catch {}
|
||
}
|
||
}
|
||
if (!fullText && !assistantDiv) addMsg('assistant', '<span class="role">Agent:</span><em>(empty)</em>');
|
||
status.textContent = '';
|
||
} catch (err) { status.textContent = 'Error: ' + err.message; }
|
||
if (validator.events.length > 0) {
|
||
try { const vr = await validator.validate(); chat.appendChild(validator.renderElement(vr)); chat.scrollTop = chat.scrollHeight; } catch {}
|
||
}
|
||
btn.disabled = false; input.focus();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
""";
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// Workflow Demo
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
|
||
internal const string WorkflowDemo = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Workflow Demo — Foundry Responses Hosting</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
|
||
main { width: 100%; max-width: 800px; }
|
||
h1 { font-size: 1.2rem; margin-bottom: .3rem; color: #333; }
|
||
.subtitle { font-size: .85rem; color: #666; margin-bottom: .8rem; }
|
||
a.back { font-size: .85rem; color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 1rem; }
|
||
#chat { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; height: 56vh; overflow-y: auto; margin-bottom: 1rem; }
|
||
.msg { margin-bottom: .75rem; line-height: 1.6; }
|
||
.msg.user { color: #0066cc; }
|
||
.msg.assistant { color: #333; }
|
||
.msg .role { font-weight: 600; margin-right: .25rem; }
|
||
.workflow-evt { background: #f0f9f0; border-left: 3px solid #4caf50; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; }
|
||
.workflow-evt.failed { background: #fef0f0; border-left-color: #e53935; }
|
||
.tool-call { background: #f0f4ff; border-left: 3px solid #4a90d9; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; font-family: 'Cascadia Code', 'Fira Code', monospace; }
|
||
form { display: flex; gap: .5rem; }
|
||
input { flex: 1; padding: .6rem .8rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
|
||
button { padding: .6rem 1.2rem; background: #0066cc; color: #fff; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; }
|
||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||
#status { font-size: .85rem; color: #888; margin-top: .5rem; }
|
||
.suggestions { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
|
||
.suggestions button { padding: .3rem .7rem; font-size: .8rem; background: #e8f0fe; color: #1a73e8; border: 1px solid #c5d8f8; border-radius: 16px; cursor: pointer; }
|
||
.suggestions button:hover { background: #d2e3fc; }
|
||
.agent-diagram { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; font-size: .85rem; text-align: center; color: #555; }
|
||
.agent-diagram .flow { font-size: 1.1rem; letter-spacing: 2px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<a class="back" href="/">← Back to demos</a>
|
||
<h1>🔀 Workflow Demo — Agent Handoffs</h1>
|
||
<p class="subtitle">A triage agent routes your question to a specialist (Code Expert or Creative Writer)</p>
|
||
<div class="agent-diagram">
|
||
<div class="flow">👤 User → 🔀 <b>Triage</b> → 💻 <b>Code Expert</b> / ✍️ <b>Creative Writer</b></div>
|
||
</div>
|
||
<div class="suggestions">
|
||
<button onclick="sendText('Write a Python function to reverse a linked list')">💻 Reverse linked list</button>
|
||
<button onclick="sendText('Write me a haiku about cloud computing')">✍️ Cloud haiku</button>
|
||
<button onclick="sendText('Explain the difference between async and threads in C#')">💻 Async vs threads</button>
|
||
<button onclick="sendText('Write a short story about an AI that learns to paint')">✍️ AI painter story</button>
|
||
</div>
|
||
<div id="chat"></div>
|
||
<form id="form">
|
||
<input id="input" placeholder="Ask a coding question or request creative writing…" autocomplete="off" autofocus />
|
||
<button type="submit">Send</button>
|
||
</form>
|
||
<div id="status"></div>
|
||
</main>
|
||
<script src="/js/sse-validator.js"></script>
|
||
<script>
|
||
const AGENT = 'triage-workflow';
|
||
const chat = document.getElementById('chat');
|
||
const form = document.getElementById('form');
|
||
const input = document.getElementById('input');
|
||
const status = document.getElementById('status');
|
||
|
||
function escapeHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
function addMsg(role, html) {
|
||
const d = document.createElement('div');
|
||
d.className = 'msg ' + role; d.innerHTML = html;
|
||
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
|
||
}
|
||
|
||
function addWorkflowEvent(icon, text, failed) {
|
||
const d = document.createElement('div');
|
||
d.className = 'workflow-evt' + (failed ? ' failed' : '');
|
||
d.innerHTML = icon + ' ' + escapeHtml(text);
|
||
chat.appendChild(d); chat.scrollTop = chat.scrollHeight;
|
||
}
|
||
|
||
function addToolCall(name) {
|
||
const d = document.createElement('div');
|
||
d.className = 'tool-call';
|
||
d.innerHTML = '🔀 Handoff: <b>' + escapeHtml(name) + '</b>';
|
||
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
|
||
}
|
||
|
||
function sendText(t) { input.value = t; form.dispatchEvent(new Event('submit')); }
|
||
|
||
form.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const text = input.value.trim(); if (!text) return;
|
||
input.value = '';
|
||
addMsg('user', '<span class="role">You:</span>' + escapeHtml(text));
|
||
|
||
const btn = form.querySelector('button[type="submit"]');
|
||
btn.disabled = true; status.textContent = 'Running workflow…';
|
||
|
||
let fullText = '', assistantDiv = null;
|
||
const toolCalls = {};
|
||
const validator = new SseValidator();
|
||
|
||
try {
|
||
const resp = await fetch('/responses', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ model: AGENT, stream: true, input: text })
|
||
});
|
||
if (!resp.ok) { status.textContent = 'Error ' + resp.status; btn.disabled = false; return; }
|
||
|
||
const reader = resp.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buf = '', curEvt = null;
|
||
while (true) {
|
||
const { done, value } = await reader.read(); if (done) break;
|
||
buf += decoder.decode(value, { stream: true });
|
||
const lines = buf.split('\n'); buf = lines.pop();
|
||
for (const line of lines) {
|
||
if (line.startsWith('event: ')) { curEvt = line.slice(7).trim(); continue; }
|
||
if (!line.startsWith('data: ')) continue;
|
||
const d = line.slice(6).trim(); if (d === '[DONE]') continue;
|
||
try {
|
||
const evt = JSON.parse(d);
|
||
validator.capture(curEvt || evt.type || 'unknown', d);
|
||
curEvt = null;
|
||
|
||
// Workflow events (executor invoked/completed/failed)
|
||
if (evt.type === 'response.output_item.added' && evt.item?.type === 'workflow_action') {
|
||
const s = evt.item.status;
|
||
const id = evt.item.action_id || evt.item.actionId || '?';
|
||
if (s === 'in_progress' || s === 'InProgress')
|
||
addWorkflowEvent('▶️', 'Agent invoked: ' + id);
|
||
else if (s === 'completed' || s === 'Completed')
|
||
addWorkflowEvent('✅', 'Agent completed: ' + id);
|
||
else if (s === 'failed' || s === 'Failed')
|
||
addWorkflowEvent('❌', 'Agent failed: ' + id, true);
|
||
}
|
||
|
||
// Handoff function calls
|
||
if (evt.type === 'response.output_item.added' && evt.item?.type === 'function_call') {
|
||
const id = evt.item.id;
|
||
toolCalls[id] = { name: evt.item.name || '?', args: '', el: addToolCall(evt.item.name || '?') };
|
||
status.textContent = 'Handoff: ' + (evt.item.name || '…');
|
||
}
|
||
if (evt.type === 'response.function_call_arguments.delta' && evt.item_id && toolCalls[evt.item_id])
|
||
toolCalls[evt.item_id].args += (evt.delta || '');
|
||
if (evt.type === 'response.function_call_arguments.done' && evt.item_id && toolCalls[evt.item_id]) {
|
||
const tc = toolCalls[evt.item_id];
|
||
let args = tc.args; try { args = JSON.stringify(JSON.parse(args), null, 0); } catch {}
|
||
tc.el.innerHTML = '🔀 Handoff: <b>' + escapeHtml(tc.name) + '</b>(' + escapeHtml(args) + ')';
|
||
}
|
||
|
||
// Text streaming from the specialist agent
|
||
if (evt.type === 'response.output_text.delta') {
|
||
if (!assistantDiv) assistantDiv = addMsg('assistant', '<span class="role">Agent:</span>');
|
||
fullText += evt.delta;
|
||
assistantDiv.innerHTML = '<span class="role">Agent:</span>' + escapeHtml(fullText);
|
||
chat.scrollTop = chat.scrollHeight;
|
||
status.textContent = 'Streaming…';
|
||
}
|
||
} catch {}
|
||
}
|
||
}
|
||
if (!fullText && !assistantDiv) addMsg('assistant', '<span class="role">Agent:</span><em>(empty)</em>');
|
||
status.textContent = '';
|
||
} catch (err) { status.textContent = 'Error: ' + err.message; }
|
||
if (validator.events.length > 0) {
|
||
try { const vr = await validator.validate(); chat.appendChild(validator.renderElement(vr)); chat.scrollTop = chat.scrollHeight; } catch {}
|
||
}
|
||
btn.disabled = false; input.focus();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
""";
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════
|
||
// 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,'<').replace(/>/g,'>');
|
||
|
||
let h = ok
|
||
? `<span class="val-ok">${n} events — all rules passed ✅</span>`
|
||
: `<span class="val-err">${n} events — ${vs.length} violation(s)</span>`;
|
||
|
||
if (vs.length) {
|
||
h += '<div class="val-issues">';
|
||
vs.forEach(v => {
|
||
h += `<div class="val-issue"><b>[${esc(v.ruleId)}]</b> ${esc(v.message)} <span class="val-at">#${v.eventIndex}</span></div>`;
|
||
});
|
||
h += '</div>';
|
||
}
|
||
|
||
h += `<details class="val-log"><summary>Event log (${this.events.length})</summary><div class="val-log-items">`;
|
||
this.events.forEach((e, i) => {
|
||
h += `<div><span class="val-i">${i}</span> <span class="val-t">${esc(e.eventType)}</span></div>`;
|
||
});
|
||
h += '</div></details>';
|
||
|
||
el.innerHTML = h;
|
||
return el;
|
||
}
|
||
}
|
||
""";
|
||
}
|