Files
Roger Barreto eb709d8fc9 .NET: Update FoundryAgent to address HostedAgents strict URL routing (#5677)
* .NET: Foundry agent-endpoint constructor uses ProjectOpenAIClient directly to fix hosted-agent URL routing

Fixes the experimental FoundryAgent(Uri agentEndpoint, AuthenticationTokenProvider, ...)
constructor so it actually works against Foundry hosted agents.

The previous implementation routed through AzureAIProjectChatClient, which
internally called aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClientForAgent(...).
For an agent-endpoint URL of the canonical shape

  https://<host>/api/projects/<project>/agents/<agentName>/endpoint/protocols/openai

the chain produced

  POST https://<host>/api/projects/<project>/openai/v1/responses

(project-level path, no /agents/ segment). The Foundry service rejects this with
HTTP 400 "Hosted agents can only be called through the agent endpoint:
.../agents/<agentName>/endpoint/protocols/openai/responses".

The constructor also extracted the agent name via
agentEndpoint.Segments[^1].TrimEnd('/'), which returns "openai" (the last segment),
not the agent name.

What changed
- Public ctor signature: clientOptions parameter type changed from
  AIProjectClientOptions? to ProjectOpenAIClientOptions?. The constructor is
  fundamentally building a ProjectOpenAIClient; accepting AIProjectClientOptions
  was a leaky abstraction whose translation silently dropped any pipeline
  policies the caller added via AddPolicy(...). With the direct type, caller
  policies pass through to the per-agent traffic verbatim.
- Per-agent client construction: `new ProjectOpenAIClient(BearerTokenPolicy, ProjectOpenAIClientOptions)`
  with Endpoint and AgentName set, then `GetProjectResponsesClient().AsIChatClient()`.
  The SDK auto-appends ?api-version=v1 when AgentName is set.
- New private static ParseAgentEndpoint helper: single source of truth for both
  agent-name extraction and project-root derivation. Tolerates trailing slash,
  case variants on /agents/ and the suffix segment, strips query/fragment, and
  throws ArgumentException with paramName=nameof(agentEndpoint) for malformed input.
- Project-level client (used by CreateConversationSessionAsync) is built fresh
  from the derived project root with primitive properties copied
  (RetryPolicy/NetworkTimeout/Transport/UserAgentApplicationId) plus MEAI UA.
- New GetService<ProjectOpenAIClient>() entry alongside the existing
  GetService<AIProjectClient>() (the latter returns null in agent-endpoint mode
  since no AIProjectClient is constructed on that path).
- Endpoint and AgentName on caller-supplied ProjectOpenAIClientOptions are
  overridden by values derived from agentEndpoint.

Compatibility
- FoundryAgent is [Experimental(OPENAI001)]. No GA surface touched. The Foundry
  project does not maintain PublicAPI.*.txt baselines so there is no shipped
  baseline to update.
- The Microsoft.Agents.AI.Foundry csproj pins
  Azure.AI.Projects to VersionOverride 2.1.0-beta.1 (matching what the IT and
  hosting projects already use); the central pin in Directory.Packages.props
  stays at 2.0.0.
- WireClientHeaders from PR #5652 is invoked on the agent-endpoint path so
  per-call x-client-* headers behave identically across both ctors.

Tests
- 23 new unit tests in FoundryAgentTests.cs:
  - 12 for the agent-endpoint constructor (URL routing for non-streaming and
    streaming, conversations URL shape, MEAI UA stamping, caller-policy
    passthrough on the per-agent pipeline, Endpoint/AgentName override
    semantics, GetService matrix, ProjectOpenAIClient propagation,
    UserAgentApplicationId propagation, null-arg validation, ID/Name slug)
  - 9 for ParseAgentEndpoint (standard shape, trailing slash, casing,
    sovereign-cloud host without /api/projects/ literal prefix, special chars
    in agent name, query/fragment stripping, three negative cases)
  - 2 null-arg tests for the public ctor
- All 250 Microsoft.Agents.AI.Foundry.UnitTests pass (was 221 baseline plus
  29 from PR #5652 plus 23 new in this PR equals 273; pre-existing tests
  collapsed by the rebase merge keep the total at 250).
- All 225 Microsoft.Agents.AI.Foundry.Hosting.UnitTests pass; no behavioral
  change to the hosting layer.
- dotnet build clean across net8/9/10/netstandard2.0/net472 with
  TreatWarningsAsErrors=true.
- dotnet format --verify-no-changes clean for the touched src and test projects.

* .NET: Bump central Azure.AI.Projects pin to 2.1.0-beta.1 and flip Microsoft.Agents.AI.Foundry to preview

Required to fix the NU1109 downgrade chain that broke CI on the agent-endpoint
constructor rewire (#5677). Microsoft.Agents.AI.Foundry now depends on
ProjectOpenAIClientOptions.AgentName and the (AuthenticationPolicy, options)
constructor that only exist in Azure.AI.Projects 2.1.0-beta.1.

Changes:
* Directory.Packages.props: Azure.AI.Projects 2.0.0 -> 2.1.0-beta.1.
* Microsoft.Agents.AI.Foundry.csproj: drop IsReleased=true so the package ships
  as preview (matches the beta SDK we now depend on). Add a comment noting the
  flip is temporary and should revert once Azure.AI.Projects ships a stable
  2.1.0.
* Drop redundant VersionOverride="2.1.0-beta.1" from the 10 csprojs that had it
  as a workaround; the central pin now suffices.

Verified:
* dotnet build agent-framework-dotnet.slnx --warnaserror clean across all TFMs.
* Microsoft.Agents.AI.Foundry.UnitTests 250/250 pass.
* Microsoft.Agents.AI.Foundry.Hosting.UnitTests 211/211 pass.
* dotnet format --verify-no-changes clean for the touched src and test projects.
eb709d8fc9 · 2026-05-08 14:46:52 +00:00
History
..

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
  • 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:

cp .env.example .env

Edit .env and set your Azure AI Foundry project endpoint:

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.

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:

azd ai agent invoke --local "Find me a hotel in Seattle for Dec 20-25 under $200/night"

Or with curl:

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)

dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out

2. Build the Docker image

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:

# 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:

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.