mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2e143192 | ||
|
|
bbbb78b8df | ||
|
|
d450865583 | ||
|
|
3c48ca17c8 | ||
|
|
acf8fa47c1 | ||
|
|
215a0ad473 | ||
|
|
1f4bcd6c88 | ||
|
|
9baf76ea77 | ||
|
|
ecc34ff902 | ||
|
|
31acaccc50 | ||
|
|
e737ac6a44 | ||
|
|
8f5a6ec4a1 | ||
|
|
518e90e114 | ||
|
|
7c9ed7f5d6 | ||
|
|
77529dc3ea | ||
|
|
e6dfdd46f2 | ||
|
|
10077bdb13 | ||
|
|
98d3a81ed4 | ||
|
|
d49d546bb4 | ||
|
|
030ef3a115 | ||
|
|
53fec2a989 | ||
|
|
93306f5f5b | ||
|
|
bc303d87f4 | ||
|
|
8d30cadc83 | ||
|
|
c1ea834e1f | ||
|
|
e751779da4 | ||
|
|
5af3d47062 | ||
|
|
4739d1ec9c | ||
|
|
930d47789a | ||
|
|
8b49a0a207 | ||
|
|
e70f20622a | ||
|
|
0949260d16 | ||
|
|
28f3d178e1 | ||
|
|
ebb3483a2c | ||
|
|
9e820aaad7 | ||
|
|
a4519045a4 | ||
|
|
ac6a0e3d95 | ||
|
|
990330e198 | ||
|
|
3ef5436537 | ||
|
|
3984ef4ef1 | ||
|
|
bf779f2029 | ||
|
|
d0fc7499b7 | ||
|
|
3a2617997e | ||
|
|
cf4a3f6914 | ||
|
|
34eeea165d | ||
|
|
6e834fcac2 | ||
|
|
a25502b7c8 | ||
|
|
afce5665e2 | ||
|
|
eccdcffc6c | ||
|
|
226abebac2 | ||
|
|
4a2e25573f | ||
|
|
838fd7f4d0 | ||
|
|
4dd3a30875 | ||
|
|
5368e530a8 | ||
|
|
15e36efe39 | ||
|
|
e5c1203f4d | ||
|
|
12251b1e69 | ||
|
|
548dfb10b6 | ||
|
|
5b16684930 | ||
|
|
1584879edd | ||
|
|
4b9d848541 | ||
|
|
d02cee5353 | ||
|
|
ad625602a3 | ||
|
|
340ec82623 | ||
|
|
9e842e1821 | ||
|
|
4895ad74e1 | ||
|
|
fcc96823a4 | ||
|
|
a82c1ede09 | ||
|
|
eb7f8617c1 | ||
|
|
026f71ac32 | ||
|
|
4ab43734c6 | ||
|
|
7407c0a35e | ||
|
|
fed3c06817 | ||
|
|
176b8f9b34 | ||
|
|
53dd990185 | ||
|
|
23db9569ce | ||
|
|
eb67379033 | ||
|
|
21dc5311be | ||
|
|
3b03c6dd55 | ||
|
|
5ba8521ce2 | ||
|
|
af9807b4a4 | ||
|
|
a1c19b90cd | ||
|
|
bb7082d3c6 | ||
|
|
59ad42912f | ||
|
|
52cc4a9f0e | ||
|
|
69597d7d77 | ||
|
|
0b1ed03cd0 |
@@ -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/
|
||||
|
||||
+8
-1
@@ -402,4 +402,11 @@ FodyWeavers.xsd
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
*.sln.iml
|
||||
|
||||
# Foundry agent CLI config (contains secrets, auto-generated)
|
||||
.foundry-agent.json
|
||||
.foundry-agent-build.log
|
||||
|
||||
# Pre-published output for Docker builds
|
||||
out/
|
||||
@@ -19,11 +19,16 @@
|
||||
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.22" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.1" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-alpha.20260417.1" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.20.0" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
|
||||
<PackageVersion Include="Azure.Core" Version="1.53.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
|
||||
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
|
||||
<!-- Google Gemini -->
|
||||
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
|
||||
@@ -48,15 +53,15 @@
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
|
||||
<!-- Microsoft.AspNetCore.* -->
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
|
||||
@@ -188,4 +193,4 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Solution>
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<BuildType Name="Debug" />
|
||||
<BuildType Name="Publish" />
|
||||
@@ -260,7 +260,44 @@
|
||||
<Project Path="samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/06_MixedWorkflowAgentsAndExecutors.csproj" />
|
||||
<Project Path="samples/03-workflows/_StartHere/07_WriterCriticWorkflow/07_WriterCriticWorkflow.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/" />
|
||||
<Folder Name="/Samples/04-hosting/">
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/" />
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
|
||||
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/" />
|
||||
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
|
||||
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
|
||||
@@ -310,15 +347,6 @@
|
||||
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
|
||||
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/HostedAgents/">
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj" />
|
||||
<Project Path="samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
|
||||
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
|
||||
@@ -480,13 +508,12 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
|
||||
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
@@ -508,11 +535,10 @@
|
||||
<Folder Name="/Tests/IntegrationTests/">
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
@@ -528,12 +554,11 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
|
||||
|
||||
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="azure-sdk-dev" value="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="azure-sdk-dev">
|
||||
<package pattern="Azure.AI.Projects*" />
|
||||
<package pattern="Azure.AI.AgentServer*" />
|
||||
<package pattern="Azure.AI.Extensions.OpenAI*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -1,21 +1,19 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.1.0</VersionPrefix>
|
||||
<VersionPrefix>0.0.1</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260410.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260410.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.1.0</GitTag>
|
||||
<!-- Preview-only branch: all publishable packages ship as 0.0.1-preview.260417.2 regardless of IsReleaseCandidate/VersionSuffix. -->
|
||||
<PackageVersion>$(VersionPrefix)-preview.260420.1</PackageVersion>
|
||||
<GitTag>0.0.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<!-- Override the repo-wide IsPackable=false default from Directory.Build.props. Projects that want to stay non-packable (e.g. Mem0) set IsPackable=false AFTER importing this file. -->
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
|
||||
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
|
||||
<!-- Enable validation for GA packages -->
|
||||
<EnablePackageValidation Condition="'$(IsReleased)' == 'true'">true</EnablePackageValidation>
|
||||
<!-- Preview-only branch: package validation disabled entirely. -->
|
||||
<!-- Validate assembly attributes only for Publish builds -->
|
||||
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
|
||||
<!-- Do not validate reference assemblies -->
|
||||
@@ -70,7 +68,7 @@
|
||||
<None Include="$(RepoRoot)/dotnet/nuget/NUGET.md" Link="NUGET.md" Pack="true" PackagePath="." />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' AND '$(IsPackable)' == 'true' ">
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
**/Properties/launchSettings.json
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.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"]
|
||||
+85
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal <see cref="AIAgent"/> that echoes the user's input text back as the response.
|
||||
/// No LLM or external service is required.
|
||||
/// </summary>
|
||||
public sealed class EchoAIAgent : AIAgent
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "echo-agent";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Description => "An agent that echoes back the input message.";
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
|
||||
=> new(new EchoAgentSession());
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(
|
||||
AgentSession session,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions));
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
|
||||
JsonElement serializedState,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(new EchoAgentSession());
|
||||
|
||||
private static string GetInputText(IEnumerable<ChatMessage> messages)
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Role == ChatRole.User)
|
||||
{
|
||||
return message.Text ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal session for the echo agent. No state is persisted.
|
||||
/// </summary>
|
||||
private sealed class EchoAgentSession : AgentSession;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.AgentServer.Invocations;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace HostedInvocationsEchoAgent;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="InvocationHandler"/> that reads the request body as plain text,
|
||||
/// passes it to the <see cref="EchoAIAgent"/>, and writes the response back.
|
||||
/// </summary>
|
||||
public sealed class EchoInvocationHandler(EchoAIAgent agent) : InvocationHandler
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedInvocationsEchoAgent</RootNamespace>
|
||||
<AssemblyName>HostedInvocationsEchoAgent</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0" />
|
||||
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// 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).
|
||||
builder.Services.AddSingleton<EchoAIAgent>();
|
||||
|
||||
// Register the Invocations SDK services and wire the handler.
|
||||
builder.Services.AddInvocationsServer();
|
||||
builder.Services.AddScoped<InvocationHandler, EchoInvocationHandler>();
|
||||
|
||||
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();
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# 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)
|
||||
|
||||
## 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.
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/04-hosting/FoundryHostedAgents/invocations/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.
|
||||
+27
@@ -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: []
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: hosted-invocations-echo-agent
|
||||
protocols:
|
||||
- protocol: invocations
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
+129
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AIAgent"/> that invokes a remote agent hosted with the Invocations protocol
|
||||
/// by sending plain-text HTTP POST requests to the <c>/invocations</c> endpoint.
|
||||
/// </summary>
|
||||
public sealed class InvocationsAIAgent : AIAgent
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Uri _invocationsUri;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvocationsAIAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agentEndpoint">
|
||||
/// The base URI of the hosted agent (e.g., <c>http://localhost:8089</c>).
|
||||
/// The <c>/invocations</c> path is appended automatically.
|
||||
/// </param>
|
||||
/// <param name="httpClient">Optional <see cref="HttpClient"/> to use. If <see langword="null"/>, a new instance is created.</param>
|
||||
/// <param name="name">Optional name for the agent.</param>
|
||||
/// <param name="description">Optional description for the agent.</param>
|
||||
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.";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Name { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Description { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputText = GetLastUserText(messages);
|
||||
var responseText = await this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false);
|
||||
return new AgentResponse(new ChatMessage(ChatRole.Assistant, responseText));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> 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 this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent(responseText)],
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
|
||||
=> new(new InvocationsAgentSession());
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(
|
||||
AgentSession session,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions));
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
|
||||
JsonElement serializedState,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(new InvocationsAgentSession());
|
||||
|
||||
private async Task<string> 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<ChatMessage> messages)
|
||||
{
|
||||
string? lastUserText = null;
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Role == ChatRole.User)
|
||||
{
|
||||
lastUserText = message.Text;
|
||||
}
|
||||
}
|
||||
|
||||
return lastUserText ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal session for the invocations agent. No state is persisted.
|
||||
/// </summary>
|
||||
private sealed class InvocationsAgentSession : AgentSession;
|
||||
}
|
||||
+61
@@ -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!");
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>SimpleInvocationsAgentClient</RootNamespace>
|
||||
<AssemblyName>simple-invocations-agent-client</AssemblyName>
|
||||
<NoWarn>$(NoWarn);NU1903;NU1605</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AGENT_NAME=hosted-chat-client-agent
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-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"]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedChatClientAgent</RootNamespace>
|
||||
<AssemblyName>HostedChatClientAgent</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Core;
|
||||
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 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 running in foundry).
|
||||
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, credential)
|
||||
.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: agentName,
|
||||
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();
|
||||
|
||||
// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapFoundryResponses("openai/v1");
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> 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 <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
return this.GetAccessToken();
|
||||
}
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<AccessToken>(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));
|
||||
}
|
||||
}
|
||||
+109
@@ -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.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set your Azure AI Foundry project endpoint:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
```
|
||||
|
||||
> **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/responses/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.
|
||||
+28
@@ -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: []
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: hosted-chat-client-agent
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AGENT_NAME=<your-foundry-agent-name>
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-foundry-agent .
|
||||
# docker run --rm -p 8088:8088 -e AGENT_NAME=<your-agent> -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"]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedFoundryAgent</RootNamespace>
|
||||
<AssemblyName>HostedFoundryAgent</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using DotNetEnv;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
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.");
|
||||
|
||||
// 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
|
||||
.AgentAdministrationClient.GetAgentAsync(agentName);
|
||||
|
||||
FoundryAgent 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();
|
||||
|
||||
// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapFoundryResponses("openai/v1");
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> 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 <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
return this.GetAccessToken();
|
||||
}
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<AccessToken>(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));
|
||||
}
|
||||
}
|
||||
+121
@@ -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.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set your Azure AI Foundry project endpoint:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
## Running directly (contributors)
|
||||
|
||||
This project uses `ProjectReference` to build against the local Agent Framework source.
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent
|
||||
AGENT_NAME=<your-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": "<your-agent-name>"}'
|
||||
```
|
||||
|
||||
## 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=<your-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": "<your-agent-name>"}'
|
||||
```
|
||||
|
||||
## 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 |
|
||||
+28
@@ -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: []
|
||||
@@ -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-foundry-agent
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
@@ -0,0 +1,5 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
@@ -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"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-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"]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedLocalTools</RootNamespace>
|
||||
<AssemblyName>HostedLocalTools</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
@@ -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<Hotel> 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);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> for local Docker debugging only.
|
||||
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
private readonly string? _token;
|
||||
|
||||
public DevTemporaryTokenCredential()
|
||||
{
|
||||
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
}
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> this.GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(this.GetAccessToken());
|
||||
|
||||
private AccessToken GetAccessToken()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set your Azure AI Foundry project endpoint:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
```
|
||||
|
||||
> **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/responses/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.
|
||||
+29
@@ -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: []
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
@@ -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"]
|
||||
+18
@@ -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"]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedMcpTools</RootNamespace>
|
||||
<AssemblyName>HostedMcpTools</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="ModelContextProtocol" VersionOverride="1.2.0" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
@@ -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<AITool> allTools = [.. clientTools.Cast<AITool>(), 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();
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> for local Docker debugging only.
|
||||
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
private readonly string? _token;
|
||||
|
||||
public DevTemporaryTokenCredential()
|
||||
{
|
||||
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
}
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> this.GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(this.GetAccessToken());
|
||||
|
||||
private AccessToken GetAccessToken()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
GITHUB_PAT=ghp_your_token_here
|
||||
```
|
||||
|
||||
## Running directly (contributors)
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/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.
|
||||
+30
@@ -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: []
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
@@ -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"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-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"]
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedTextRag</RootNamespace>
|
||||
<AssemblyName>HostedTextRag</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
@@ -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<IEnumerable<TextSearchProvider.TextSearchResult>> MockSearchAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
List<TextSearchProvider.TextSearchResult> 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<IEnumerable<TextSearchProvider.TextSearchResult>>(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> for local Docker debugging only.
|
||||
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(GetAccessToken());
|
||||
|
||||
private static AccessToken GetAccessToken()
|
||||
{
|
||||
var token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
if (string.IsNullOrEmpty(token) || token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
}
|
||||
@@ -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.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set your Azure AI Foundry project endpoint:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AZURE_BEARER_TOKEN=
|
||||
```
|
||||
|
||||
> **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/responses/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.
|
||||
+30
@@ -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: []
|
||||
@@ -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
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedToolbox</RootNamespace>
|
||||
<AssemblyName>HostedToolbox</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Foundry Toolbox Agent - A hosted agent that uses Foundry Toolset MCP tools.
|
||||
//
|
||||
// Demonstrates how to register one or more Foundry toolsets so the agent can
|
||||
// call tools provided by the Foundry platform's managed MCP proxy.
|
||||
//
|
||||
// Required environment variables:
|
||||
// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
|
||||
// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
|
||||
// FOUNDRY_AGENT_TOOLSET_ENDPOINT - Foundry Toolsets proxy base URL
|
||||
// (injected automatically by Foundry platform at runtime)
|
||||
//
|
||||
// Optional:
|
||||
// FOUNDRY_TOOLBOX_NAME - Name of the toolset to load (default: my-toolset)
|
||||
// FOUNDRY_AGENT_NAME - Client name reported to MCP server
|
||||
// FOUNDRY_AGENT_VERSION - Client version reported to MCP server
|
||||
// FOUNDRY_AGENT_TOOLSET_FEATURES - Feature flags sent to Foundry proxy via header
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Core;
|
||||
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();
|
||||
|
||||
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";
|
||||
string toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME") ?? "my-toolset";
|
||||
|
||||
// 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 agent ─────────────────────────────────────────────────────────────
|
||||
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
|
||||
.AsAIAgent(
|
||||
model: deploymentName,
|
||||
instructions: """
|
||||
You are a helpful assistant with access to tools provided by the Foundry Toolset.
|
||||
Use the available tools to answer user questions.
|
||||
If a tool is not available for a request, let the user know clearly.
|
||||
""",
|
||||
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent",
|
||||
description: "Hosted agent backed by Foundry Toolset MCP tools");
|
||||
|
||||
// ── Build the host ────────────────────────────────────────────────────────────
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register the agent and response handler
|
||||
builder.Services.AddFoundryResponses(agent);
|
||||
|
||||
// Register Foundry Toolbox: connects to the MCP proxy at startup and makes tools available.
|
||||
// The toolset name must match a toolset registered in your Foundry project.
|
||||
// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent (e.g., in local development without Foundry
|
||||
// infrastructure), startup succeeds without error and no toolbox tools are loaded.
|
||||
builder.Services.AddFoundryToolboxes(toolboxName);
|
||||
|
||||
var app = builder.Build();
|
||||
app.MapFoundryResponses();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapFoundryResponses("openai/v1");
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
// ── DevTemporaryTokenCredential ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> for local Docker debugging only.
|
||||
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
private readonly string? _token;
|
||||
|
||||
public DevTemporaryTokenCredential()
|
||||
{
|
||||
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
}
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> this.GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(this.GetAccessToken());
|
||||
|
||||
private AccessToken GetAccessToken()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(this._token, DateTimeOffset.MaxValue);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for contributors building from the agent-framework repository source.
|
||||
#
|
||||
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
|
||||
# which means a standard multi-stage Docker build cannot resolve dependencies outside
|
||||
# this folder. Instead, pre-publish the app targeting the container runtime and copy
|
||||
# the output into the container:
|
||||
#
|
||||
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
# docker build -f Dockerfile.contributor -t hosted-workflow-handoff .
|
||||
# docker run --rm -p 8088:8088 -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflow-handoff
|
||||
#
|
||||
# For end-users consuming the NuGet package (not ProjectReference), use the standard
|
||||
# Dockerfile which performs a full dotnet restore + publish inside the container.
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
|
||||
WORKDIR /app
|
||||
COPY out/ .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"]
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>HostedWorkflowHandoff</RootNamespace>
|
||||
<AssemblyName>HostedWorkflowHandoff</AssemblyName>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<NoWarn>$(NoWarn);NU1903;NU1605;MAAIW001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Core" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// 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.Core;
|
||||
using Azure.Identity;
|
||||
using DotNetEnv;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Client;
|
||||
|
||||
// Load .env file if present (for local development)
|
||||
Env.TraversePath().Load();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Create the shared Azure OpenAI chat client
|
||||
// ---------------------------------------------------------------------------
|
||||
var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."));
|
||||
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";
|
||||
|
||||
var azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential(
|
||||
new DevTemporaryTokenCredential(),
|
||||
new DefaultAzureCredential()));
|
||||
IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP
|
||||
// ---------------------------------------------------------------------------
|
||||
Console.WriteLine("Connecting to Microsoft Learn MCP server...");
|
||||
McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
|
||||
Name = "Microsoft Learn MCP",
|
||||
}));
|
||||
var mcpTools = await mcpClient.ListToolsAsync();
|
||||
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
|
||||
|
||||
builder.AddAIAgent(
|
||||
name: "tool-agent",
|
||||
instructions: """
|
||||
You are a helpful assistant hosted as a Foundry Hosted Agent.
|
||||
You have access to several tools - use them proactively:
|
||||
- GetCurrentTime: Returns the current date/time in any timezone.
|
||||
- GetWeather: Returns weather conditions for any location.
|
||||
- Microsoft Learn MCP tools: Search and fetch Microsoft documentation.
|
||||
When a user asks a technical question about Microsoft products, use the
|
||||
documentation search tools to give accurate, up-to-date answers.
|
||||
""",
|
||||
chatClient: chatClient)
|
||||
.WithAITool(AIFunctionFactory.Create(GetCurrentTime))
|
||||
.WithAITool(AIFunctionFactory.Create(GetWeather))
|
||||
.WithAITools(mcpTools.Cast<AITool>().ToArray());
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. DEMO 2: Triage Workflow — routes to specialist agents
|
||||
// ---------------------------------------------------------------------------
|
||||
ChatClientAgent triageAgent = new(
|
||||
chatClient,
|
||||
instructions: """
|
||||
You are a triage agent that determines which specialist to hand off to.
|
||||
Based on the user's question, ALWAYS hand off to one of the available agents.
|
||||
Do NOT answer the question yourself - just route it.
|
||||
""",
|
||||
name: "triage_agent",
|
||||
description: "Routes messages to the appropriate specialist agent");
|
||||
|
||||
ChatClientAgent codeExpert = new(
|
||||
chatClient,
|
||||
instructions: """
|
||||
You are a coding and technology expert. You help with programming questions,
|
||||
explain technical concepts, debug code, and suggest best practices.
|
||||
Provide clear, well-structured answers with code examples when appropriate.
|
||||
""",
|
||||
name: "code_expert",
|
||||
description: "Specialist agent for programming and technology questions");
|
||||
|
||||
ChatClientAgent creativeWriter = new(
|
||||
chatClient,
|
||||
instructions: """
|
||||
You are a creative writing specialist. You help write stories, poems,
|
||||
marketing copy, emails, and other creative content. You have a flair
|
||||
for engaging language and vivid descriptions.
|
||||
""",
|
||||
name: "creative_writer",
|
||||
description: "Specialist agent for creative writing and content tasks");
|
||||
|
||||
Workflow triageWorkflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
|
||||
.WithHandoffs(triageAgent, [codeExpert, creativeWriter])
|
||||
.WithHandoffs([codeExpert, creativeWriter], triageAgent)
|
||||
.Build();
|
||||
|
||||
builder.AddAIAgent("triage-workflow", (_, key) =>
|
||||
triageWorkflow.AsAIAgent(name: key));
|
||||
|
||||
// Register triage-workflow as the non-keyed default so azd invoke (no model) works
|
||||
builder.Services.AddSingleton(sp =>
|
||||
sp.GetRequiredKeyedService<AIAgent>("triage-workflow"));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Wire up the agent-framework handler and Responses Server SDK
|
||||
// ---------------------------------------------------------------------------
|
||||
builder.Services.AddFoundryResponses();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Dispose the MCP client on shutdown
|
||||
app.Lifetime.ApplicationStopping.Register(() =>
|
||||
mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult());
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
app.MapGet("/ready", () => Results.Ok("ready"));
|
||||
app.MapFoundryResponses();
|
||||
|
||||
app.MapGet("/", () => Results.Content(Pages.Home, "text/html"));
|
||||
app.MapGet("/tool-demo", () => Results.Content(Pages.ToolDemo, "text/html"));
|
||||
app.MapGet("/workflow-demo", () => Results.Content(Pages.WorkflowDemo, "text/html"));
|
||||
app.MapGet("/js/sse-validator.js", () => Results.Content(Pages.ValidationScript, "application/javascript"));
|
||||
|
||||
// Validation endpoint: accepts captured SSE lines and validates them
|
||||
app.MapPost("/api/validate", (HostedWorkflowHandoff.CapturedSseStream captured) =>
|
||||
{
|
||||
var validator = new HostedWorkflowHandoff.ResponseStreamValidator();
|
||||
foreach (var evt in captured.Events)
|
||||
{
|
||||
validator.ProcessEvent(evt.EventType, evt.Data);
|
||||
}
|
||||
|
||||
validator.Complete();
|
||||
return Results.Json(validator.GetResult());
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local tool definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dev-only credential: reads a pre-fetched bearer token from AZURE_BEARER_TOKEN.
|
||||
// When the value is missing or set to "DefaultAzureCredential", this credential
|
||||
// throws CredentialUnavailableException so the ChainedTokenCredential falls
|
||||
// through to DefaultAzureCredential.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
[Description("Gets the current date and time in the specified timezone.")]
|
||||
static string GetCurrentTime(
|
||||
[Description("IANA timezone (e.g. 'America/New_York', 'Europe/London', 'UTC'). Defaults to UTC.")]
|
||||
string timezone = "UTC")
|
||||
{
|
||||
try
|
||||
{
|
||||
var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
|
||||
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz).ToString("F");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.UtcNow.ToString("F") + " (UTC - unknown timezone: " + timezone + ")";
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Gets the current weather for a location. Returns temperature, conditions, and humidity.")]
|
||||
static string GetWeather(
|
||||
[Description("The city or location (e.g. 'Seattle', 'London, UK').")]
|
||||
string location)
|
||||
{
|
||||
// Simulated weather - deterministic per location for demo consistency
|
||||
var rng = new Random(location.ToUpperInvariant().GetHashCode());
|
||||
var temp = rng.Next(-5, 35);
|
||||
string[] conditions = ["sunny", "partly cloudy", "overcast", "rainy", "snowy", "windy", "foggy"];
|
||||
var condition = conditions[rng.Next(conditions.Length)];
|
||||
return $"Weather in {location}: {temp}C, {condition}. Humidity: {rng.Next(30, 90)}%. Wind: {rng.Next(5, 30)} km/h.";
|
||||
}
|
||||
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
private readonly string? _token;
|
||||
|
||||
public DevTemporaryTokenCredential()
|
||||
{
|
||||
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
}
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> this.GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(this.GetAccessToken());
|
||||
|
||||
private AccessToken GetAccessToken()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Hosted-Workflow-Handoff
|
||||
|
||||
A hosted agent server demonstrating two patterns in a single app:
|
||||
|
||||
- **`tool-agent`** — an agent with local tools (time, weather) plus remote Microsoft Learn MCP tools
|
||||
- **`triage-workflow`** — a handoff workflow that routes conversations to specialist agents (code expert or creative writer) using `AgentWorkflowBuilder`
|
||||
|
||||
Both agents are served over the Responses protocol. The server also exposes interactive web demos at `/tool-demo` and `/workflow-demo`.
|
||||
|
||||
> Unlike the other samples in this folder, this one connects to an **Azure OpenAI** resource directly (not an Azure AI Foundry project endpoint).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- An Azure OpenAI resource with a deployed model (e.g., `gpt-4o`)
|
||||
- Azure CLI logged in (`az login`)
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy the template and fill in your values:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```env
|
||||
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
```
|
||||
|
||||
`AZURE_BEARER_TOKEN=DefaultAzureCredential` is a sentinel value that tells the app to skip the bearer token and fall through to `DefaultAzureCredential` (requires `az login`). Set it to a real token only when running in Docker.
|
||||
|
||||
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
|
||||
|
||||
## Running directly (contributors)
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:8088`. Open `http://localhost:8088` to see the demo index page.
|
||||
|
||||
### Test it
|
||||
|
||||
Using the Azure Developer CLI (invokes `triage-workflow` — the primary/default agent):
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Write me a short poem about coding"
|
||||
```
|
||||
|
||||
To target a specific agent by name, use curl:
|
||||
|
||||
```bash
|
||||
# Invoke triage-workflow explicitly
|
||||
curl -X POST http://localhost:8088/responses \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "Write me a haiku about autumn", "model": "triage-workflow"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Invoke tool-agent (local tools + MCP)
|
||||
curl -X POST http://localhost:8088/responses \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "What time is it in Tokyo?", "model": "tool-agent"}'
|
||||
```
|
||||
|
||||
## Running with Docker
|
||||
|
||||
### 1. Publish for the container runtime
|
||||
|
||||
```bash
|
||||
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
|
||||
```
|
||||
|
||||
### 2. Build the Docker image
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.contributor -t hosted-workflow-handoff .
|
||||
```
|
||||
|
||||
### 3. Run the container
|
||||
|
||||
```bash
|
||||
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
|
||||
|
||||
docker run --rm -p 8088:8088 \
|
||||
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
|
||||
--env-file .env \
|
||||
hosted-workflow-handoff
|
||||
```
|
||||
|
||||
### 4. Test it
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Explain async/await in C#"
|
||||
```
|
||||
|
||||
## How the triage workflow works
|
||||
|
||||
```
|
||||
User message
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Triage Agent │ ──routes──▶ ┌─────────────┐
|
||||
│ (router) │ │ Code Expert │
|
||||
└──────────────┘ └─────────────┘
|
||||
▲ │
|
||||
│◀──────────────────────────────┘
|
||||
│
|
||||
└──routes──▶ ┌─────────────────┐
|
||||
│ Creative Writer │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
The triage agent receives every message and hands off to the appropriate specialist. Specialists route back to the triage agent after responding, allowing for multi-turn conversations.
|
||||
|
||||
## NuGet package users
|
||||
|
||||
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative.
|
||||
+601
@@ -0,0 +1,601 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HostedWorkflowHandoff;
|
||||
|
||||
/// <summary>Captured SSE event for validation.</summary>
|
||||
[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);
|
||||
|
||||
/// <summary>Captured SSE stream sent from the client for server-side validation.</summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")]
|
||||
internal sealed record CapturedSseStream(
|
||||
[property: JsonPropertyName("events")] List<CapturedSseEvent> Events);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an SSE event stream from the Azure AI Responses Server SDK against
|
||||
/// the API behaviour contract. Feed events sequentially via <see cref="ProcessEvent"/>
|
||||
/// and call <see cref="Complete"/> when the stream ends.
|
||||
/// </summary>
|
||||
internal sealed class ResponseStreamValidator
|
||||
{
|
||||
private readonly List<ValidationViolation> _violations = [];
|
||||
private int _eventCount;
|
||||
private int _expectedSequenceNumber;
|
||||
private StreamState _state = StreamState.Initial;
|
||||
private string? _responseId;
|
||||
private readonly HashSet<int> _addedItemIndices = [];
|
||||
private readonly HashSet<int> _doneItemIndices = [];
|
||||
private readonly HashSet<string> _addedContentParts = []; // "outputIdx:partIdx"
|
||||
private readonly HashSet<string> _doneContentParts = [];
|
||||
private readonly Dictionary<string, string> _textAccumulators = []; // "outputIdx:contentIdx" → accumulated text
|
||||
private bool _hasTerminal;
|
||||
|
||||
/// <summary>All violations found so far.</summary>
|
||||
internal IReadOnlyList<ValidationViolation> Violations => this._violations;
|
||||
|
||||
/// <summary>
|
||||
/// Processes a single SSE event line pair (event type + JSON data).
|
||||
/// </summary>
|
||||
/// <param name="eventType">The SSE event type (e.g. "response.created").</param>
|
||||
/// <param name="jsonData">The raw JSON data payload.</param>
|
||||
internal void ProcessEvent(string eventType, string jsonData)
|
||||
{
|
||||
JsonElement data;
|
||||
try
|
||||
{
|
||||
data = JsonDocument.Parse(jsonData).RootElement;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
this.Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
this._eventCount++;
|
||||
|
||||
// ── Sequence number validation ──────────────────────────────────
|
||||
if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
int seq = seqProp.GetInt32();
|
||||
if (seq != this._expectedSequenceNumber)
|
||||
{
|
||||
this.Fail("SEQ-01", $"Expected sequence_number {this._expectedSequenceNumber}, got {seq}");
|
||||
}
|
||||
|
||||
this._expectedSequenceNumber = seq + 1;
|
||||
}
|
||||
else if (this._state != StreamState.Initial || eventType != "error")
|
||||
{
|
||||
// Pre-creation error events may not have sequence_number
|
||||
this.Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'");
|
||||
}
|
||||
|
||||
// ── Post-terminal guard ─────────────────────────────────────────
|
||||
if (this._hasTerminal)
|
||||
{
|
||||
this.Fail("TERM-01", $"Event '{eventType}' received after terminal event");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Dispatch by event type ──────────────────────────────────────
|
||||
switch (eventType)
|
||||
{
|
||||
case "response.created":
|
||||
this.ValidateResponseCreated(data);
|
||||
break;
|
||||
|
||||
case "response.queued":
|
||||
this.ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued);
|
||||
this.ValidateResponseEnvelope(data, eventType);
|
||||
break;
|
||||
|
||||
case "response.in_progress":
|
||||
if (this._state is StreamState.Created or StreamState.Queued)
|
||||
{
|
||||
this._state = StreamState.InProgress;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Fail("ORDER-02", $"'response.in_progress' received in state {this._state} (expected Created or Queued)");
|
||||
}
|
||||
|
||||
this.ValidateResponseEnvelope(data, eventType);
|
||||
break;
|
||||
|
||||
case "response.output_item.added":
|
||||
case "output_item.added":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateOutputItemAdded(data);
|
||||
break;
|
||||
|
||||
case "response.output_item.done":
|
||||
case "output_item.done":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateOutputItemDone(data);
|
||||
break;
|
||||
|
||||
case "response.content_part.added":
|
||||
case "content_part.added":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateContentPartAdded(data);
|
||||
break;
|
||||
|
||||
case "response.content_part.done":
|
||||
case "content_part.done":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateContentPartDone(data);
|
||||
break;
|
||||
|
||||
case "response.output_text.delta":
|
||||
case "output_text.delta":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateTextDelta(data);
|
||||
break;
|
||||
|
||||
case "response.output_text.done":
|
||||
case "output_text.done":
|
||||
this.ValidateInProgress(eventType);
|
||||
this.ValidateTextDone(data);
|
||||
break;
|
||||
|
||||
case "response.function_call_arguments.delta":
|
||||
case "function_call_arguments.delta":
|
||||
this.ValidateInProgress(eventType);
|
||||
break;
|
||||
|
||||
case "response.function_call_arguments.done":
|
||||
case "function_call_arguments.done":
|
||||
this.ValidateInProgress(eventType);
|
||||
break;
|
||||
|
||||
case "response.completed":
|
||||
this.ValidateTerminal(data, "completed");
|
||||
break;
|
||||
|
||||
case "response.failed":
|
||||
this.ValidateTerminal(data, "failed");
|
||||
break;
|
||||
|
||||
case "response.incomplete":
|
||||
this.ValidateTerminal(data, "incomplete");
|
||||
break;
|
||||
|
||||
case "error":
|
||||
// Pre-creation error — standalone, no response.created precedes it
|
||||
if (this._state != StreamState.Initial)
|
||||
{
|
||||
this.Fail("ERR-01", "'error' event received after response.created — should use response.failed instead");
|
||||
}
|
||||
|
||||
this._hasTerminal = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown events are not violations — the spec may evolve
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call after the stream ends. Checks that a terminal event was received.
|
||||
/// </summary>
|
||||
internal void Complete()
|
||||
{
|
||||
if (!this._hasTerminal && this._state != StreamState.Initial)
|
||||
{
|
||||
this.Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)");
|
||||
}
|
||||
|
||||
if (this._state == StreamState.Initial && this._eventCount == 0)
|
||||
{
|
||||
this.Fail("EMPTY-01", "No events received in the stream");
|
||||
}
|
||||
|
||||
// Check for output items that were added but never completed
|
||||
foreach (int idx in this._addedItemIndices)
|
||||
{
|
||||
if (!this._doneItemIndices.Contains(idx))
|
||||
{
|
||||
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 this._addedContentParts)
|
||||
{
|
||||
if (!this._doneContentParts.Contains(key))
|
||||
{
|
||||
this.Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a summary of all validation results.
|
||||
/// </summary>
|
||||
internal ValidationResult GetResult()
|
||||
{
|
||||
return new ValidationResult(
|
||||
EventCount: this._eventCount,
|
||||
IsValid: this._violations.Count == 0,
|
||||
Violations: [.. this._violations]);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Event-specific validators
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private void ValidateResponseCreated(JsonElement data)
|
||||
{
|
||||
if (this._state != StreamState.Initial)
|
||||
{
|
||||
this.Fail("ORDER-01", $"'response.created' received in state {this._state} (expected Initial — must be first event)");
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = StreamState.Created;
|
||||
|
||||
// Must have a response envelope
|
||||
if (!data.TryGetProperty("response", out var resp))
|
||||
{
|
||||
this.Fail("FIELD-01", "'response.created' missing 'response' object");
|
||||
return;
|
||||
}
|
||||
|
||||
// Required response fields
|
||||
this.ValidateRequiredResponseFields(resp, "response.created");
|
||||
|
||||
// Capture response ID for cross-event checks
|
||||
if (resp.TryGetProperty("id", out var idProp))
|
||||
{
|
||||
this._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")
|
||||
{
|
||||
this.Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateTerminal(JsonElement data, string expectedKind)
|
||||
{
|
||||
if (this._state is StreamState.Initial or StreamState.Created)
|
||||
{
|
||||
this.Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'");
|
||||
}
|
||||
|
||||
this._hasTerminal = true;
|
||||
this._state = StreamState.Terminal;
|
||||
|
||||
if (!data.TryGetProperty("response", out var resp))
|
||||
{
|
||||
this.Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object");
|
||||
return;
|
||||
}
|
||||
|
||||
this.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)
|
||||
{
|
||||
this.Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'");
|
||||
}
|
||||
|
||||
if (status != "completed" && hasCompletedAt)
|
||||
{
|
||||
this.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)
|
||||
{
|
||||
this.Fail("FIELD-04", "'error' must be non-null when status is 'failed'");
|
||||
}
|
||||
|
||||
if (status is "completed" or "incomplete" && hasError)
|
||||
{
|
||||
this.Fail("FIELD-05", $"'error' must be null when status is '{status}'");
|
||||
}
|
||||
|
||||
// error structure validation
|
||||
if (hasError)
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.Fail("CANCEL-01", "Cancelled response must have empty output array (B11)");
|
||||
}
|
||||
|
||||
// response ID consistency
|
||||
if (this._responseId is not null && resp.TryGetProperty("id", out var idProp)
|
||||
&& idProp.GetString() != this._responseId)
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.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 (!this._addedItemIndices.Add(index))
|
||||
{
|
||||
this.Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Fail("FIELD-06", "output_item.added missing 'output_index' field");
|
||||
}
|
||||
|
||||
if (!data.TryGetProperty("item", out _))
|
||||
{
|
||||
this.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 (!this._addedItemIndices.Contains(index))
|
||||
{
|
||||
this.Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added");
|
||||
}
|
||||
|
||||
this._doneItemIndices.Add(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Fail("FIELD-06", "output_item.done missing 'output_index' field");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateContentPartAdded(JsonElement data)
|
||||
{
|
||||
string key = GetContentPartKey(data);
|
||||
if (!this._addedContentParts.Add(key))
|
||||
{
|
||||
this.Fail("CONTENT-01", $"Duplicate content_part.added for {key}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateContentPartDone(JsonElement data)
|
||||
{
|
||||
string key = GetContentPartKey(data);
|
||||
if (!this._addedContentParts.Contains(key))
|
||||
{
|
||||
this.Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added");
|
||||
}
|
||||
|
||||
this._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 (!this._textAccumulators.TryGetValue(key, out string? existing))
|
||||
{
|
||||
this._textAccumulators[key] = delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._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)
|
||||
{
|
||||
this.Fail("TEXT-01", $"output_text.done for {key} missing 'text' field");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText)
|
||||
{
|
||||
this.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"))
|
||||
{
|
||||
this.Fail("FIELD-01", $"{context}: response missing 'id'");
|
||||
}
|
||||
|
||||
if (resp.TryGetProperty("object", out var objProp))
|
||||
{
|
||||
if (objProp.GetString() != "response")
|
||||
{
|
||||
this.Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Fail("FIELD-08", $"{context}: response missing 'object' field");
|
||||
}
|
||||
|
||||
if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
this.Fail("FIELD-09", $"{context}: response missing 'created_at'");
|
||||
}
|
||||
|
||||
if (!resp.TryGetProperty("status", out _))
|
||||
{
|
||||
this.Fail("FIELD-10", $"{context}: response missing 'status'");
|
||||
}
|
||||
|
||||
if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
this.Fail("FIELD-11", $"{context}: response missing 'output' array");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateErrorObject(JsonElement error, string context)
|
||||
{
|
||||
if (!HasNonNullString(error, "code"))
|
||||
{
|
||||
this.Fail("ERR-02", $"{context}: error object missing 'code' field");
|
||||
}
|
||||
|
||||
if (!HasNonNullString(error, "message"))
|
||||
{
|
||||
this.Fail("ERR-03", $"{context}: error object missing 'message' field");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateUsage(JsonElement usage, string context)
|
||||
{
|
||||
if (!usage.TryGetProperty("input_tokens", out _))
|
||||
{
|
||||
this.Fail("USAGE-01", $"{context}: usage missing 'input_tokens'");
|
||||
}
|
||||
|
||||
if (!usage.TryGetProperty("output_tokens", out _))
|
||||
{
|
||||
this.Fail("USAGE-02", $"{context}: usage missing 'output_tokens'");
|
||||
}
|
||||
|
||||
if (!usage.TryGetProperty("total_tokens", out _))
|
||||
{
|
||||
this.Fail("USAGE-03", $"{context}: usage missing 'total_tokens'");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateResponseEnvelope(JsonElement data, string eventType)
|
||||
{
|
||||
if (!data.TryGetProperty("response", out var resp))
|
||||
{
|
||||
this.Fail("FIELD-01", $"'{eventType}' missing 'response' object");
|
||||
return;
|
||||
}
|
||||
|
||||
this.ValidateRequiredResponseFields(resp, eventType);
|
||||
|
||||
// Response ID consistency
|
||||
if (this._responseId is not null && resp.TryGetProperty("id", out var idProp)
|
||||
&& idProp.GetString() != this._responseId)
|
||||
{
|
||||
this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Helpers
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private void ValidateInProgress(string eventType)
|
||||
{
|
||||
if (this._state != StreamState.InProgress)
|
||||
{
|
||||
this.Fail("ORDER-04", $"'{eventType}' received in state {this._state} (expected InProgress)");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateStateTransition(string eventType, StreamState expected, StreamState next)
|
||||
{
|
||||
if (this._state != expected)
|
||||
{
|
||||
this.Fail("ORDER-05", $"'{eventType}' received in state {this._state} (expected {expected})");
|
||||
}
|
||||
else
|
||||
{
|
||||
this._state = next;
|
||||
}
|
||||
}
|
||||
|
||||
private void Fail(string ruleId, string message)
|
||||
{
|
||||
this._violations.Add(new ValidationViolation(ruleId, message, this._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,
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A single validation violation.</summary>
|
||||
/// <param name="RuleId">The rule identifier (e.g. SEQ-01, FIELD-02).</param>
|
||||
/// <param name="Message">Human-readable description of the violation.</param>
|
||||
/// <param name="EventIndex">1-based index of the event that triggered this violation.</param>
|
||||
internal sealed record ValidationViolation(string RuleId, string Message, int EventIndex);
|
||||
|
||||
/// <summary>Overall validation result.</summary>
|
||||
/// <param name="EventCount">Total number of events processed.</param>
|
||||
/// <param name="IsValid">True if no violations were found.</param>
|
||||
/// <param name="Violations">List of all violations.</param>
|
||||
internal sealed record ValidationResult(int EventCount, bool IsValid, IReadOnlyList<ValidationViolation> Violations);
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
|
||||
name: triage-workflow
|
||||
displayName: "Triage Handoff Workflow Agent"
|
||||
|
||||
description: >
|
||||
A hosted agent demonstrating two patterns in a single server: a tool-equipped agent
|
||||
with local tools and remote MCP tools, and a triage workflow that routes conversations
|
||||
to specialist agents (code expert or creative writer) via handoff orchestration.
|
||||
|
||||
metadata:
|
||||
tags:
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Workflows
|
||||
- Handoff
|
||||
- Agent Framework
|
||||
|
||||
template:
|
||||
name: triage-workflow
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
parameters:
|
||||
properties: []
|
||||
resources: []
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: triage-workflow
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
AZURE_BEARER_TOKEN=DefaultAzureCredential
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Use the official .NET 10.0 ASP.NET runtime as a parent image
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Final stage
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedWorkflows.dll"]
|
||||
+18
@@ -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-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.
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
|
||||
WORKDIR /app
|
||||
COPY out/ .
|
||||
EXPOSE 8088
|
||||
ENV ASPNETCORE_URLS=http://+:8088
|
||||
ENTRYPOINT ["dotnet", "HostedWorkflowSimple.dll"]
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>HostedWorkflowSimple</RootNamespace>
|
||||
<AssemblyName>HostedWorkflowSimple</AssemblyName>
|
||||
<NoWarn>$(NoWarn);</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For contributors: uses ProjectReference to build against local source -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
+97
@@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TokenCredential"/> for local Docker debugging only.
|
||||
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> 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 ...
|
||||
/// </summary>
|
||||
internal sealed class DevTemporaryTokenCredential : TokenCredential
|
||||
{
|
||||
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
|
||||
private readonly string? _token;
|
||||
|
||||
public DevTemporaryTokenCredential()
|
||||
{
|
||||
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
}
|
||||
|
||||
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> this.GetAccessToken();
|
||||
|
||||
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
|
||||
=> new(this.GetAccessToken());
|
||||
|
||||
private AccessToken GetAccessToken()
|
||||
{
|
||||
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
|
||||
{
|
||||
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
|
||||
}
|
||||
|
||||
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# 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`.
|
||||
|
||||
## 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.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set your Azure AI Foundry project endpoint:
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
|
||||
ASPNETCORE_URLS=http://+:8088
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
|
||||
```
|
||||
|
||||
> **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-Simple
|
||||
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-workflow-simple"}'
|
||||
```
|
||||
|
||||
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-workflow-simple .
|
||||
```
|
||||
|
||||
### 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-workflow-simple \
|
||||
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
|
||||
--env-file .env \
|
||||
hosted-workflow-simple
|
||||
```
|
||||
|
||||
### 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 `HostedWorkflowSimple.csproj` for the `PackageReference` alternative.
|
||||
+29
@@ -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: []
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: hosted-workflows
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
+115
@@ -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($"""
|
||||
══════════════════════════════════════════════════════════
|
||||
Simple 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, 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!");
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
|
||||
{
|
||||
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
|
||||
{
|
||||
RewriteScheme(message);
|
||||
ProcessNext(message, pipeline, currentIndex);
|
||||
}
|
||||
|
||||
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
<RootNamespace>SimpleAgentClient</RootNamespace>
|
||||
<AssemblyName>simple-agent-client</AssemblyName>
|
||||
<NoWarn>$(NoWarn);NU1903;NU1605;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using AgentCard = A2A.AgentCard;
|
||||
|
||||
namespace A2AServer;
|
||||
|
||||
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.11" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc4" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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"]
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.11" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc4" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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"]
|
||||
@@ -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();
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnablePreviewFeatures>true</EnablePreviewFeatures>
|
||||
|
||||
<!--
|
||||
Disable central package management for this project.
|
||||
This project requires explicit package references with versions specified inline rather than
|
||||
inheriting them from Directory.Packages.props. This is necessary because a Docker image will
|
||||
be created from this project, and the Docker build process only has access to this folder
|
||||
and cannot access parent folders where Directory.Packages.props resides.
|
||||
-->
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Remove analyzer PackageReference items inherited from Directory.Packages.props.
|
||||
Note: ManagePackageVersionsCentrally only controls PackageVersion items, not PackageReference items.
|
||||
Directory.Packages.props contains both PackageVersion and PackageReference entries for analyzers,
|
||||
and the PackageReference items are always inherited through MSBuild imports regardless of the
|
||||
ManagePackageVersionsCentrally setting. We must explicitly remove them before adding our own versions.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
|
||||
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
|
||||
<PackageReference Remove="xunit.analyzers" />
|
||||
<PackageReference Remove="Moq.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.11" />
|
||||
<PackageReference Include="Azure.AI.Projects" Version="2.0.0-beta.1" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc4" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Add analyzers with compatible versions -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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"]
|
||||
@@ -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<Hotel> 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);
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user