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>
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Minimal Responses-only hosting sample.
|
|
|
|
Single agent with one ``@tool`` (``lookup_weather``), single channel
|
|
(``ResponsesChannel``), one ``run_hook`` that demonstrates the
|
|
settings-mutation seam over caller-supplied options.
|
|
|
|
What the hook does
|
|
------------------
|
|
On every Responses request the hook receives the ``ChannelRequest`` that
|
|
the channel built from the inbound HTTP body. It:
|
|
|
|
- strips ``store`` (this agent owns persistence) and ``temperature``
|
|
(the configured model may not honor it),
|
|
- forces a ``reasoning`` effort + summary preset so the deployed surface
|
|
is consistent regardless of what the caller sent.
|
|
|
|
The hook is the documented escape hatch over the uniform
|
|
``ChannelRequest`` envelope.
|
|
|
|
Run
|
|
---
|
|
``app`` is a module-level Starlette ASGI app. Recommended local launch::
|
|
|
|
uv sync
|
|
az login
|
|
export FOUNDRY_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com
|
|
export FOUNDRY_MODEL=gpt-5.4-nano
|
|
uv run hypercorn app:app --bind 0.0.0.0:8000
|
|
|
|
Or use the ``__main__`` block (single-process Hypercorn) for quick
|
|
iteration::
|
|
|
|
uv run python app.py
|
|
|
|
Then call it::
|
|
|
|
uv run python call_server.py "What is the weather in Tokyo?"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from random import randint
|
|
from typing import Annotated
|
|
|
|
from agent_framework import Agent, FileHistoryProvider, tool
|
|
from agent_framework_foundry import FoundryChatClient
|
|
from agent_framework_hosting import AgentFrameworkHost, ChannelRequest
|
|
from agent_framework_hosting_responses import ResponsesChannel
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
|
|
SESSIONS_DIR = Path(__file__).resolve().parent / "storage" / "sessions"
|
|
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
@tool(approval_mode="never_require")
|
|
def lookup_weather(
|
|
location: Annotated[str, "The city to look up weather for."],
|
|
) -> str:
|
|
"""Return a deterministic weather report for a city."""
|
|
high_temp = randint(5, 25)
|
|
reports = {
|
|
"Seattle": f"Seattle is rainy with a high of {high_temp}°C.",
|
|
"Amsterdam": f"Amsterdam is cloudy with a high of {high_temp}°C.",
|
|
"Tokyo": f"Tokyo is clear with a high of {high_temp}°C.",
|
|
}
|
|
return reports.get(location, f"{location} is sunny with a high of {high_temp}°C.")
|
|
|
|
|
|
def responses_hook(request: ChannelRequest, **_: object) -> ChannelRequest:
|
|
"""Strip caller-supplied options the host should own and force a
|
|
reasoning preset."""
|
|
options = dict(request.options or {})
|
|
|
|
# The agent's default_options own ``store``; the model may not honor
|
|
# ``temperature``. Strip both so the caller can't override.
|
|
options.pop("temperature", None)
|
|
options.pop("store", None)
|
|
|
|
# Force a consistent reasoning preset on every turn.
|
|
options["reasoning"] = {"effort": "medium", "summary": "auto"}
|
|
|
|
return replace(request, options=options or None)
|
|
|
|
|
|
def build_host() -> AgentFrameworkHost:
|
|
agent = Agent(
|
|
client=FoundryChatClient(credential=DefaultAzureCredential()),
|
|
name="WeatherAgent",
|
|
instructions=(
|
|
"You are a friendly weather assistant. Use the lookup_weather tool "
|
|
"for any weather question and answer in one short sentence."
|
|
),
|
|
tools=[lookup_weather],
|
|
context_providers=[FileHistoryProvider(SESSIONS_DIR)],
|
|
default_options={"store": False},
|
|
)
|
|
return AgentFrameworkHost(
|
|
target=agent,
|
|
channels=[ResponsesChannel(run_hook=responses_hook)],
|
|
debug=True,
|
|
)
|
|
|
|
|
|
app = build_host().app
|
|
|
|
|
|
if __name__ == "__main__":
|
|
build_host().serve(host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))
|