Files
agent-framework/python/samples/04-hosting/af-hosting/foundry_hosted_agent
T
Eduard van Valkenburg e5a6e35843 Python: feat(python): cross-channel hosting improvements (endpoint paths, Activity push, Telegram/Teams fixes) (#6307)
* Update hosting channel endpoint paths

Treat channel paths as concrete endpoint paths so built-in channels can be mounted at their defaults or at the app root without sample-specific subclasses. Update docs, tests, and the Foundry Telegram Invocations sample accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add push support to ActivityProtocolChannel

Implement the ChannelPush protocol so the Activity Protocol channel can
receive cross-channel fan-out (ResponseTarget.all_linked) and echo_input
replay as a non-originating destination:

- Add push() that reconstructs a proactive Bot Framework activity (bot/user
  swap) from the stored conversation reference and POSTs it to
  /v3/conversations/{id}/activities.
- Record a ChannelIdentity (service_url, conversation, bot, user, channel_id,
  locale) on ChannelRequest.identity so the host registers the channel under
  its isolation key for fan-out resolution.
- Route the streaming path through deliver_response so Activity-originated
  turns broadcast like Telegram/Discord.
- Add tests for push delivery, service_url validation, ChannelPush instance
  check, and inbound identity recording.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Don't delete Telegram webhook on shutdown by default

The TelegramChannel deleted its webhook on shutdown in webhook mode. During
a rolling redeploy the new revision registers the webhook on startup, then
the old revision's shutdown deletes it, silently breaking inbound delivery
until the next boot. setWebhook is overwriting/idempotent, so startup
re-asserts the webhook every boot and no teardown is needed.

Add a delete_webhook_on_shutdown flag (default False) so teardown is opt-in
for ephemeral deployments, and leave the webhook in place otherwise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Activity channel streaming on non-Teams channels (405 on updateActivity)

The Activity Protocol channel streamed replies the Teams way: POST a
placeholder, then PUT-edit it as tokens arrive. Only Teams supports the
updateActivity REST op; Web Chat, Direct Line and the Emulator return
405 Method Not Allowed on the PUT, so the user saw only the placeholder.

Gate the placeholder+edit flow on edit-capable channels (msteams). Other
channels now buffer the stream and POST a single final message, mirroring
the non-streaming path's fan-out and response-hook semantics. Also add a
defensive 405 fallback inside the Teams edit loop so an unexpected 405
can never strand the user on the placeholder.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(hosting-activity-protocol): don't parse Teams inline attachment content as a URI

Teams message activities include a text/html attachment whose inline
`content` is raw HTML (not a URL). _parse_activity fell back to
`attachment["content"]` and passed it to Content.from_uri, raising
ContentError ("URI must contain a scheme") and failing the whole turn,
so Teams users got no response.

Only treat `contentUrl` as a URI, require an absolute scheme, and skip
unparseable attachments defensively instead of failing the message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(hosting-activity-protocol): native slash-command dispatch for Teams/Activity

Add a commands= parameter to ActivityProtocolChannel that intercepts a
leading /command (after stripping the bot's own @mention) and dispatches
to ChannelCommand handlers, mirroring the Telegram channel. Unknown
commands fall through to the agent. The channel run_hook is applied to
command requests so handlers observe the same resolved isolation key as
ordinary messages, and handler errors are swallowed (200, no Bot Service
retry of non-idempotent commands).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(hosting): silent attributed Telegram echoes + Teams markdown rendering

- hosting-telegram: send cross-channel input echoes with disable_notification
  (silent) and detect echo payloads so they aren't re-broadcast.
- hosting-activity-protocol: render outbound + push activities as textFormat
  'markdown' so Teams shows formatted replies (enables per-channel variants).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(hosting-activity-protocol): address PR #6307 review feedback

Consult the host delivery pipeline even for empty streamed replies so
ResponseTarget.none is honoured and non-originating fan-out is consulted
instead of always emitting an originating "(no response)" message. Applies
to both the progressive-edit (Teams) and buffered (Web Chat/Direct Line)
streaming paths.

Re-validate service_url against the allow-list in push(): the identity is
read from a persisted store and push runs out-of-band, so the captured
service_url must be re-checked before a bearer token is sent.

Adds tests for empty-stream host consultation/suppression on both streaming
paths and for push rejecting a disallowed service_url.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e5a6e35843 · 2026-06-03 16:37:03 +02:00
History
..

foundry_hosted_agent — Responses + Invocations (Foundry Hosted Agents compatible)

Smallest end-to-end hosting sample. One Foundry-backed agent, two channels, no human-chat surface — and that minimal shape is the whole point: a host configured with at least the Responses and Invocations channels under their default endpoints is runtime-compatible with the Foundry Hosted Agents platform. The same container image runs locally, behind any ASGI server, or as a Hosted Agent — no protocol shim, no extra adapter.

Route Channel Used by
POST /responses ResponsesChannel OpenAI Responses clients (call_server.py)
POST /invocations InvocationsChannel Host-native JSON envelope (Hosted Agents)

Conversation history

The agent is wired with FoundryHostedAgentHistoryProvider (from agent-framework-foundry-hosting). When a Responses request supplies previous_response_id, the channel uses it as the session id and the provider fetches the prior turn chain directly from {FOUNDRY_PROJECT_ENDPOINT}/storage/... using the same managed-identity credential as the chat client. Locally (when FOUNDRY_HOSTING_ENVIRONMENT is unset) it transparently falls back to an in-memory store, so the same code runs in dev. Writes are a no-op — Foundry persists Responses turns authoritatively as the runtime executes them.

For richer scenarios (custom tools, history providers, run hooks, multicast, Telegram, Teams, identity linking) see ../local_telegram and ../local_identity_link.

Layout

foundry_hosted_agent/
├── app.py                       # the host (ResponsesChannel + InvocationsChannel)
├── call_server.py               # client: openai SDK / agent framework / FoundryAgent
├── agent.yaml                   # Foundry Hosted Agents minimal definition
├── agent.manifest.yaml          # Foundry Hosted Agents full deployment manifest
├── azure.yaml                   # azd service config (build context = this folder)
├── Dockerfile                   # built from this folder; uv fetches deps from GitHub
├── Dockerfile.dockerignore      # BuildKit allowlist that trims the context
├── pyproject.toml               # depends on the hosting packages via GitHub git refs
└── README.md                    # this file

Run locally

export FOUNDRY_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com
export MODEL_DEPLOYMENT_NAME=gpt-4.1-mini
az login                       # any DefaultAzureCredential source

uv sync
uv run python app.py           # binds 0.0.0.0:8000

The env var names match agent.manifest.yaml so the same shell environment works for both local runs and Hosted Agent deployments.

Call locally

uv sync --group dev

# OpenAI SDK pointed at the local /responses endpoint.
uv run python call_server.py --via openai "hello there"

# The same call via the Agent Framework Agent + OpenAIChatClient stack.
uv run python call_server.py --via af "hello there"

# Once deployed as a Hosted Agent: target the Foundry-managed endpoint.
export FOUNDRY_HOSTED_AGENT_NAME=agent-framework-hosting-sample
uv run python call_server.py --via foundry "hello there"

Docker

The Docker build context is this sample folder. pyproject.toml declares the in-tree agent-framework-hosting* packages via [tool.uv.sources] git refs pointing at the feature/python-hosting branch of microsoft/agent-framework, so uv sync inside the image fetches them directly. No vendoring step is required — the build just needs network access to GitHub. Once the hosting packages publish to PyPI you can drop the [tool.uv.sources] overrides and rely on PyPI resolution.

# From this folder — context = `.` (sample folder).
DOCKER_BUILDKIT=1 docker build -t hosting-sample-hosted-agent .

docker run -p 8000:8000 \
    -e FOUNDRY_PROJECT_ENDPOINT -e MODEL_DEPLOYMENT_NAME \
    -e AZURE_CLIENT_ID -e AZURE_TENANT_ID -e AZURE_CLIENT_SECRET \
    hosting-sample-hosted-agent

Hosted Agent deployment

azure.yaml keeps project: . and uses docker.remoteBuild: true — the remote builder receives only this sample folder and runs uv sync to pull the hosting packages from GitHub.

The two YAMLs follow the same convention as the foundry-hosted-agents/ reference samples — agent.yaml is the minimal kind/protocols/resources card, agent.manifest.yaml is the full template + environment-variable + model-resource binding used during deployment.

azd up        # provisions infra/ + builds + pushes + deploys
azd deploy    # rebuild + redeploy only

Required Foundry RBAC

The container runs as the Hosted Agent's managed identity. That identity needs permission to call the Foundry project's agent/Responses endpoints — without it the call returns 401 PermissionDenied. Grant the Azure AI Project Manager role (or the more granular Microsoft.CognitiveServices/accounts/AIServices/agents/* data actions) on the Foundry project to the Hosted Agent's managed identity. See https://aka.ms/FoundryPermissions for the full role list.

Health probe

The Foundry Hosted Agents runtime probes GET /readiness; AgentFrameworkHost exposes that route automatically (returns 200 ok). No extra wiring needed.

The host code never imports anything Foundry-specific beyond the chat client itself — swapping FoundryChatClient for OpenAIChatClient (or any other client) flips this sample from a Hosted Agent target to a non-Foundry deployment without touching the channels.