mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
6b822853eb
* samples(hosting): add hosting Channels sample apps under samples/04-hosting/af-hosting Adds five end-to-end sample apps under ``python/samples/04-hosting/af-hosting/`` that exercise the ``agent-framework-hosting`` Channels stack from the simplest single-channel case up to a multi-channel deployment with cross-channel identity linking. Samples (ordered by complexity) ------------------------------- * ``foundry_hosted_agent/`` — minimal Responses + Invocations host with a Foundry-backed agent and ``FoundryHostedAgentHistoryProvider``. ``agd``-deployable; bundles a ``Dockerfile`` and ``scripts/vendor-packages.sh`` that copies workspace packages into ``_vendor/`` for self-contained builds. ``_vendor/`` is gitignored. * ``local_responses/`` — single-channel Responses host with a ``run_hook`` that strips caller-supplied options and forces a reasoning preset. Demonstrates the hook seam over the uniform ``ChannelRequest`` envelope. * ``local_responses_workflow/`` — Responses + Invocations exposing a three-agent workflow with per-conversation checkpoint storage. * ``local_telegram/`` — Responses + Telegram with a ``@tool``, ``FileHistoryProvider``, hooks, and a ``ResponseTarget`` multicast variant (``call_server_multicast.py``) that pushes a single Responses reply to a separate Telegram chat. * ``local_identity_link/`` — full surface: Responses + Invocations + Telegram + Activity Protocol (Teams) + the ``EntraIdentityLinkChannel`` sidecar. Resolves per-channel ids onto a single Entra object id so a user's history follows them across surfaces. Notes ----- * Samples that use Telegram/Teams via Activity Protocol depend on the renamed ``agent-framework-hosting-activity-protocol`` package (see the PR-5 series). * All samples use ``[tool.uv.sources]`` editable workspace deps, except ``foundry_hosted_agent/`` which uses the ``./_vendor/`` self-contained layout for ``azd`` Docker builds. * Each sample includes a ``README.md`` with run instructions and an ``app.py`` ASGI entrypoint plus a ``call_server.py`` client harness. Depends on the prior hosting PRs (foundry-hosted-agent refactor + hosting-core + the per-channel packages). After those merge, this branch can be rebased onto ``main`` cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(hosting): point sample deps at the feature/python-hosting GitHub branch Switches every sample's ``[tool.uv.sources]`` from in-monorepo editable path deps (which only resolve when running inside the agent-framework workspace) to git refs targeting the ``feature/python-hosting`` branch on ``microsoft/agent-framework``. Samples now install standalone outside the monorepo while the ``agent-framework-hosting*`` packages are still pre-PyPI; once they publish, the ``[tool.uv.sources]`` block can be dropped and the declared deps resolve from PyPI. Cleanup ------- * Drops ``foundry_hosted_agent/scripts/vendor-packages.sh``, ``_vendor/`` from ``.gitignore``, the ``hooks.prepackage`` block in ``azure.yaml`` and the ``COPY _vendor/`` step in the Dockerfile — vendoring is no longer needed because git refs make the deps network-resolvable from any context. * Drops obsolete ``workspace.pyproject.toml`` reference and ``scripts/`` / ``workspace.pyproject.toml`` entries from ``Dockerfile.dockerignore``. * Updates the foundry sample's Dockerfile to ``uv sync --no-dev`` (no ``--frozen``) so it locks fresh against the GitHub-hosted deps at build time. * Drops every committed ``uv.lock`` because the resolver needs network access to ``feature/python-hosting`` to lock — they regenerate the first time a user runs ``uv sync`` after the branch lands. * Refreshes the per-sample READMEs to mention the GitHub install path instead of "in-tree workspace packages". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(hosting): address PR #5645 review comments - foundry_hosted_agent/call_server.py: replace hard-coded project_endpoint and service_session_id with FOUNDRY_PROJECT_ENDPOINT, FOUNDRY_HOSTED_AGENT_NAME, and optional FOUNDRY_HOSTED_SESSION_ID environment variables. Session-id is now optional so the sample exercises the new-conversation path by default. - local_identity_link/app.py: * make_telegram_hook: apply the reasoning bump regardless of identity-link state (the previous early-return on linked chats silently dropped the high-effort preset for the very flow the sample exists to demonstrate). * make_responses_hook: add a prominent DEV-ONLY warning that the client-supplied entra_oid shortcut bypasses identity verification and must be replaced by a JWT validator in production. * /link command: early-return when chat_id is missing instead of minting an authorize URL keyed on "telegram:None" (which would poison the link store with a binding any future chat_id-less update would collapse onto). * Switch ENTRA_CERT_PATH / ENTRA_CERT_PASSWORD env vars to the longer ENTRA_CERTIFICATE_PATH / ENTRA_CERTIFICATE_PASSWORD names that the README already documents. * channels: Sequence[Channel] -> list[Channel] (the next line appends, which a Sequence type doesn't expose). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(hosting-samples): apply sample formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-samples): guard command input text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
186 lines
7.7 KiB
Python
186 lines
7.7 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Smallest hosting sample — Responses + Invocations only.
|
|
|
|
This sample is intentionally minimal and is **runtime-compatible with the
|
|
Foundry Hosted Agents platform**: a host that exposes the Responses and
|
|
Invocations channels under their default mount roots can be packaged as a
|
|
container image and deployed to Foundry Hosted Agents without any protocol
|
|
shim. The same image runs locally, behind any ASGI server, or as a Hosted
|
|
Agent.
|
|
|
|
History
|
|
-------
|
|
The agent uses :class:`FoundryHostedAgentHistoryProvider` so that conversation
|
|
history is loaded from the Foundry Hosted Agent storage backend when the
|
|
container runs inside Foundry. When ``previous_response_id`` is supplied on
|
|
an incoming Responses request, the channel routes it through to the
|
|
provider as the ``session_id``, and the provider fetches the prior turn
|
|
chain from ``{FOUNDRY_PROJECT_ENDPOINT}/storage/...``. Locally
|
|
(``FOUNDRY_HOSTING_ENVIRONMENT`` unset) the provider falls back to an
|
|
in-memory store so the same code runs in dev.
|
|
|
|
Setup
|
|
-----
|
|
- ``FOUNDRY_PROJECT_ENDPOINT`` — Foundry project endpoint URL.
|
|
- ``MODEL_DEPLOYMENT_NAME`` — model deployment name (the same env var
|
|
the Foundry Hosted Agents manifest binds via the ``model`` resource —
|
|
see ``agent.manifest.yaml``).
|
|
- ``FOUNDRY_HOSTING_ENVIRONMENT`` — set automatically by the Hosted Agents
|
|
runtime; signals the history provider to talk to the Foundry storage API
|
|
instead of the local in-memory fallback.
|
|
- ``APPLICATIONINSIGHTS_CONNECTION_STRING`` — when present, the sample
|
|
wires Azure Monitor OpenTelemetry export at import time. Foundry Hosted
|
|
Agents inject this when an Application Insights resource is bound to
|
|
the project; locally it's optional.
|
|
|
|
Auth uses ``DefaultAzureCredential`` so any standard Azure auth chain
|
|
works (``az login`` locally, managed identity in Hosted Agents,
|
|
``AZURE_*`` env vars in CI, ...).
|
|
|
|
Run
|
|
---
|
|
- Local: ``python app.py`` (binds ``0.0.0.0:8000``)
|
|
- ASGI: ``hypercorn app:app --bind 0.0.0.0:8000``
|
|
- Docker: ``docker build -t hosting-sample-hosted-agent . && \\
|
|
docker run -p 8000:8000 \\
|
|
-e FOUNDRY_PROJECT_ENDPOINT -e MODEL_DEPLOYMENT_NAME \\
|
|
hosting-sample-hosted-agent``
|
|
- Hosted Agent: build & push the image, then deploy via ``agent.yaml`` /
|
|
``agent.manifest.yaml`` in this folder.
|
|
|
|
Routes
|
|
------
|
|
- ``POST /responses`` — OpenAI Responses-shaped surface.
|
|
- ``POST /invocations/invoke`` — host-native JSON envelope.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
from agent_framework import Agent
|
|
from agent_framework.observability import enable_instrumentation
|
|
from agent_framework_foundry import FoundryChatClient
|
|
from agent_framework_foundry_hosting import (
|
|
FoundryHostedAgentHistoryProvider,
|
|
foundry_response_id,
|
|
)
|
|
from agent_framework_hosting import AgentFrameworkHost
|
|
from agent_framework_hosting_invocations import InvocationsChannel
|
|
from agent_framework_hosting_responses import ResponsesChannel
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
# Configure root logging early so library log records (in particular
|
|
# ``agent_framework_foundry_hosting._history_provider``) are captured by
|
|
# the container's stderr stream and surfaced in the Foundry portal /
|
|
# Azure Monitor. ``LOG_LEVEL`` overrides this for production tightening.
|
|
logging.basicConfig(
|
|
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
)
|
|
# Quiet noisy transports unless explicitly cranked up.
|
|
for _noisy in (
|
|
"httpx",
|
|
"httpcore",
|
|
"azure.core.pipeline.policies.http_logging_policy",
|
|
"urllib3",
|
|
):
|
|
logging.getLogger(_noisy).setLevel(logging.WARNING)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _configure_observability() -> None:
|
|
"""Wire Azure Monitor OpenTelemetry when a connection string is present.
|
|
|
|
Foundry Hosted Agents inject ``APPLICATIONINSIGHTS_CONNECTION_STRING``
|
|
into the container at runtime when an Application Insights resource is
|
|
bound to the project. We honor the same env var locally so the same
|
|
code path lights up in both environments. When the var is absent
|
|
(typical local dev without an AI binding) we silently skip — the host
|
|
still serves traffic, just without OTel export.
|
|
"""
|
|
conn_str = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
|
|
if not conn_str:
|
|
logger.info(
|
|
"APPLICATIONINSIGHTS_CONNECTION_STRING not set — skipping Azure Monitor OpenTelemetry configuration.",
|
|
)
|
|
return
|
|
# Imported lazily so the sample still starts when the optional
|
|
# ``azure-monitor-opentelemetry`` dependency isn't installed (e.g. an
|
|
# ultra-thin local dev image stripped of observability extras).
|
|
from azure.monitor.opentelemetry import configure_azure_monitor
|
|
|
|
configure_azure_monitor(connection_string=conn_str)
|
|
logger.info("Azure Monitor OpenTelemetry configured.")
|
|
|
|
|
|
def build_host() -> AgentFrameworkHost:
|
|
# Single credential is shared by the chat client and the history
|
|
# provider so we only authenticate (and refresh tokens) once.
|
|
credential = DefaultAzureCredential()
|
|
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
|
|
|
agent = Agent(
|
|
client=FoundryChatClient(
|
|
project_endpoint=project_endpoint,
|
|
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
|
credential=credential,
|
|
),
|
|
name="HostedAgentSample",
|
|
instructions="You are called Jarvis, a friendly assistant. Keep answers brief.",
|
|
# Loads history from Foundry storage when running inside a Hosted
|
|
# Agent (FOUNDRY_HOSTING_ENVIRONMENT set); falls back to an in-
|
|
# memory store for local dev.
|
|
context_providers=[
|
|
FoundryHostedAgentHistoryProvider(
|
|
credential=credential,
|
|
endpoint=project_endpoint,
|
|
),
|
|
],
|
|
)
|
|
|
|
return AgentFrameworkHost(
|
|
target=agent,
|
|
channels=[
|
|
# Mint Foundry-storage-compatible response ids
|
|
# (``caresp_{18charPartitionKey}{32charEntropy}``). The
|
|
# Foundry storage backend partitions records by extracting
|
|
# this segment from the id; free-form ``resp_<uuid>`` ids
|
|
# are rejected with an opaque ``HTTP 500 server_error``.
|
|
ResponsesChannel(response_id_factory=foundry_response_id),
|
|
InvocationsChannel(),
|
|
],
|
|
)
|
|
|
|
|
|
# `app` is the canonical ASGI surface — hand it to any ASGI server, or let
|
|
# the Foundry Hosted Agents runtime pick it up via the standard entry point.
|
|
# Observability is configured at import time so trace/log export is wired
|
|
# before the host starts handling requests. Per-request Foundry isolation
|
|
# (the platform-injected ``x-agent-{user,chat}-isolation-key`` headers)
|
|
# is read by the host's installed ASGI middleware off every inbound HTTP
|
|
# request and lifted into a contextvar that
|
|
# :class:`FoundryHostedAgentHistoryProvider` consults on each storage call.
|
|
# Multi-turn persistence works out of the box in both local dev and the
|
|
# Hosted Agents container — no manual middleware wiring needed.
|
|
_configure_observability()
|
|
enable_instrumentation(enable_sensitive_data=True)
|
|
app = build_host().app
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Serve the host's ASGI app directly. The Foundry isolation headers
|
|
# are read by the host's installed ASGI middleware and threaded
|
|
# through the storage provider via a contextvar; nothing extra to wire.
|
|
import asyncio
|
|
|
|
import hypercorn.asyncio
|
|
import hypercorn.config
|
|
|
|
config = hypercorn.config.Config()
|
|
config.bind = [f"0.0.0.0:{int(os.environ.get('PORT', '8000'))}"]
|
|
asyncio.run(hypercorn.asyncio.serve(app, config)) # type: ignore[arg-type]
|