mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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>
This commit is contained in:
committed by
GitHub
Unverified
parent
e8c22caaeb
commit
e5a6e35843
@@ -15,6 +15,7 @@ its own package (`agent-framework-hosting-responses`,
|
||||
| [`local_responses/`](./local_responses) | The minimal shape: one agent + one `@tool` + `ResponsesChannel` + a single `run_hook` that strips caller-supplied options and forces a `reasoning` preset. | **Local only.** Start here to learn the run-hook seam. |
|
||||
| [`local_responses_workflow/`](./local_responses_workflow) | A 4-step `Workflow` (typed `SloganBrief` intake → writer → legal → formatter) hosted behind **both** the Responses and Invocations channels via a shared `run_hook` that parses inbound text/JSON into the workflow's typed input. The host writes per-conversation checkpoints via `checkpoint_location=…`. Demonstrates workflow targets + structured input adaptation + multi-channel + resume-across-turns. Includes a `call_server.rest` file with REST examples for both endpoints. | **Local only.** |
|
||||
| [`foundry_hosted_agent/`](./foundry_hosted_agent) | One Foundry agent, **Responses + Invocations only** — the minimal shape that is **runtime-compatible with the Foundry Hosted Agents platform**. | Ships with `Dockerfile` + `agent.yaml` + `agent.manifest.yaml` + `azure.yaml` so the same image runs locally **or** as a Foundry Hosted Agent (`azd up`). |
|
||||
| [`foundry_telegram_invocations_weather/`](./foundry_telegram_invocations_weather) | Experimental Telegram weather bot that mounts `TelegramChannel` at `POST /invocations`, registers the Foundry Hosted Agents Invocations URL as the Telegram webhook, and uses `FoundryHostedAgentHistoryProvider` for storage. | Ships with `Dockerfile` + `agent.yaml` + `agent.manifest.yaml` + `azure.yaml`; used to validate whether a non-Responses channel can run under Foundry Invocations. |
|
||||
| [`local_telegram/`](./local_telegram) | Adds Telegram, a `@tool`, `FileHistoryProvider`, run hooks (per-user / per-chat session keying), extra Telegram commands, and `ResponseTarget` multicast. Runs under Hypercorn with multiple workers. | **Local only.** No Dockerfile / Foundry packaging. |
|
||||
| [`local_identity_link/`](./local_identity_link) | Everything in `local_telegram/` plus Teams and the Entra identity-link sidecar (`/auth/start` + `/auth/callback`). Demonstrates linking a Telegram chat to an Entra user so multiple non-Entra channels can share one isolation key. | **Local only.** No Dockerfile / Foundry packaging. |
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 mount roots is
|
||||
**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.
|
||||
@@ -11,7 +11,7 @@ Hosted Agent — no protocol shim, no extra adapter.
|
||||
| Route | Channel | Used by |
|
||||
| ------------------------------ | -------------------- | ------------------------------------------- |
|
||||
| `POST /responses` | `ResponsesChannel` | OpenAI Responses clients (`call_server.py`) |
|
||||
| `POST /invocations/invoke` | `InvocationsChannel` | Host-native JSON envelope (Hosted Agents) |
|
||||
| `POST /invocations` | `InvocationsChannel` | Host-native JSON envelope (Hosted Agents) |
|
||||
|
||||
## Conversation history
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
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
|
||||
Invocations channels under their default endpoints 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.
|
||||
@@ -52,7 +52,7 @@ Run
|
||||
Routes
|
||||
------
|
||||
- ``POST /responses`` — OpenAI Responses-shaped surface.
|
||||
- ``POST /invocations/invoke`` — host-native JSON envelope.
|
||||
- ``POST /invocations`` — host-native JSON envelope.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"""Call the foundry_hosted_agent server three ways.
|
||||
|
||||
The foundry_hosted_agent host exposes ``POST /responses`` (OpenAI Responses-shaped) and
|
||||
``POST /invocations/invoke`` (host-native), and that minimal contract is
|
||||
``POST /invocations`` (host-native), and that minimal contract is
|
||||
**runtime-compatible with the Foundry Hosted Agents platform** — so the same
|
||||
agent code that calls the local server also calls the same image deployed
|
||||
as a Hosted Agent.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# The sample depends on hosting packages from Git refs until they publish to
|
||||
# PyPI, so the remote builder needs git available during `uv sync`.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY app.py ./
|
||||
|
||||
RUN uv sync --no-dev
|
||||
|
||||
ENV PORT=8000
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uv", "run", "python", "app.py"]
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!app.py
|
||||
!pyproject.toml
|
||||
!Dockerfile
|
||||
@@ -0,0 +1,66 @@
|
||||
# foundry_telegram_invocations_weather
|
||||
|
||||
Telegram weather bot sample for validating a non-Responses channel on Foundry
|
||||
Hosted Agents. The sample configures `TelegramChannel(path="/invocations")` so
|
||||
the webhook handler runs at the container endpoint `POST /invocations`; Foundry
|
||||
exposes that route publicly as:
|
||||
|
||||
```text
|
||||
{FOUNDRY_PROJECT_ENDPOINT}/agents/agent-framework-telegram-invocations-weather/endpoint/protocols/invocations?api-version=2025-11-15-preview
|
||||
```
|
||||
|
||||
| Route | Channel | Used by |
|
||||
|---|---|---|
|
||||
| `POST /responses` | `ResponsesChannel` | Quick hosted-agent sanity checks |
|
||||
| `POST /invocations` | `TelegramChannel` | Telegram webhook payloads |
|
||||
|
||||
The agent uses `FoundryHostedAgentHistoryProvider` and a small
|
||||
`lookup_weather` tool so Telegram requests exercise model calls, tool calls,
|
||||
and Foundry-hosted storage.
|
||||
|
||||
## Important platform note
|
||||
|
||||
This is an intentional experiment. Current Foundry Hosted Agents behavior
|
||||
requires Entra bearer auth before a request reaches the container. Telegram
|
||||
cannot attach that bearer token to webhook deliveries, so webhook registration
|
||||
can succeed while live Telegram deliveries fail at the Foundry front door with
|
||||
`401`. Authenticated calls to the Invocations endpoint are still useful for
|
||||
validating the channel and storage behavior inside the container.
|
||||
|
||||
The sample does not configure `TELEGRAM_WEBHOOK_SECRET` because prior probing
|
||||
showed Foundry strips Telegram's `X-Telegram-Bot-Api-Secret-Token` header before
|
||||
the request reaches the container.
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
export FOUNDRY_PROJECT_ENDPOINT=https://<your-project>.services.ai.azure.com
|
||||
export MODEL_DEPLOYMENT_NAME=gpt-5.4-nano
|
||||
export TELEGRAM_BOT_TOKEN=<telegram-bot-token>
|
||||
export TELEGRAM_WEBHOOK_URL=https://<public-local-tunnel>/invocations
|
||||
az login
|
||||
|
||||
uv sync
|
||||
uv run python app.py
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
set -a
|
||||
. ../../../../.env
|
||||
set +a
|
||||
|
||||
azd env set TELEGRAM_BOT_TOKEN "$TELEGRAM_BOT_TOKEN"
|
||||
azd env set MODEL_DEPLOYMENT_NAME "${MODEL_DEPLOYMENT_NAME:-gpt-5.4-nano}"
|
||||
azd env set HOSTING_INVOCATIONS_API_VERSION 2025-11-15-preview
|
||||
azd up
|
||||
```
|
||||
|
||||
If you connect this sample to an existing Foundry project instead of running
|
||||
`azd provision`, make sure the azd environment has `AZURE_AI_PROJECT_ID` and the
|
||||
project's ACR connection values set before running `azd deploy`.
|
||||
|
||||
On startup, `TelegramChannel` calls `setWebhook` using the Foundry public
|
||||
Invocations URL derived from `FOUNDRY_PROJECT_ENDPOINT` and
|
||||
`FOUNDRY_AGENT_NAME`.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
name: agent-framework-telegram-invocations-weather
|
||||
description: >
|
||||
Telegram weather bot sample hosted by Agent Framework. The Telegram webhook
|
||||
handler is mounted at /invocations so the Foundry Hosted Agents Invocations
|
||||
protocol endpoint can be registered as the bot's webhook URL.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Invocations Protocol
|
||||
- Telegram
|
||||
template:
|
||||
name: agent-framework-telegram-invocations-weather
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
- protocol: invocations
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: TELEGRAM_BOT_TOKEN
|
||||
value: "{{TELEGRAM_BOT_TOKEN}}"
|
||||
- name: HOSTING_INVOCATIONS_API_VERSION
|
||||
value: "{{HOSTING_INVOCATIONS_API_VERSION}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-5.4-nano
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
parameters:
|
||||
properties:
|
||||
- name: TELEGRAM_BOT_TOKEN
|
||||
secret: true
|
||||
- name: HOSTING_INVOCATIONS_API_VERSION
|
||||
secret: false
|
||||
@@ -0,0 +1,31 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
|
||||
kind: hosted
|
||||
name: agent-framework-telegram-invocations-weather
|
||||
description: |
|
||||
Telegram weather bot sample hosted by Agent Framework. The Telegram webhook
|
||||
handler is mounted at /invocations so the Foundry Hosted Agents Invocations
|
||||
protocol endpoint can be registered as the bot's webhook URL.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Invocations Protocol
|
||||
- Telegram
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
- protocol: invocations
|
||||
version: 1.0.0
|
||||
resources:
|
||||
cpu: "1"
|
||||
memory: 2Gi
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: ${MODEL_DEPLOYMENT_NAME}
|
||||
- name: TELEGRAM_BOT_TOKEN
|
||||
value: ${TELEGRAM_BOT_TOKEN}
|
||||
- name: HOSTING_INVOCATIONS_API_VERSION
|
||||
value: ${HOSTING_INVOCATIONS_API_VERSION}
|
||||
@@ -0,0 +1,194 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Telegram weather bot hosted behind Foundry Hosted Agents Invocations.
|
||||
|
||||
This sample intentionally mounts the Telegram webhook handler at the container's
|
||||
``/invocations`` route so the Foundry public Invocations protocol URL can be
|
||||
registered as the Telegram webhook URL:
|
||||
|
||||
``{FOUNDRY_PROJECT_ENDPOINT}/agents/{FOUNDRY_AGENT_NAME}/endpoint/protocols/invocations``
|
||||
|
||||
It uses ``FoundryHostedAgentHistoryProvider`` for conversation history and a
|
||||
small weather tool to validate that a normal channel can run under the
|
||||
Hosted Agents runtime. The sample also exposes Responses for a quick platform
|
||||
sanity check.
|
||||
|
||||
Sample output after sending "weather in Amsterdam" to the Telegram bot:
|
||||
Assistant:> Amsterdam is cloudy with a high of 16 C.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import replace
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
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,
|
||||
ChannelCommand,
|
||||
ChannelCommandContext,
|
||||
ChannelRequest,
|
||||
)
|
||||
from agent_framework_hosting_responses import ResponsesChannel
|
||||
from agent_framework_hosting_telegram import TelegramChannel, telegram_isolation_key
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
|
||||
AGENT_NAME = "agent-framework-telegram-invocations-weather"
|
||||
DEFAULT_MODEL_DEPLOYMENT = "gpt-5.4-nano"
|
||||
DEFAULT_INVOCATIONS_API_VERSION = "2025-11-15-preview"
|
||||
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
for _noisy in (
|
||||
"httpx",
|
||||
"httpcore",
|
||||
"azure.core.pipeline.policies.http_logging_policy",
|
||||
"urllib3",
|
||||
):
|
||||
logging.getLogger(_noisy).setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@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."""
|
||||
reports = {
|
||||
"seattle": "Seattle is rainy with a high of 12 C.",
|
||||
"amsterdam": "Amsterdam is cloudy with a high of 16 C.",
|
||||
"tokyo": "Tokyo is clear with a high of 22 C.",
|
||||
"london": "London is misty with a high of 11 C.",
|
||||
}
|
||||
normalized = location.strip().lower()
|
||||
return reports.get(normalized, f"{location} is sunny with a high of 20 C.")
|
||||
|
||||
|
||||
def _foundry_invocations_webhook_url() -> str:
|
||||
"""Build the public Foundry Invocations URL used as Telegram's webhook."""
|
||||
explicit = os.environ.get("TELEGRAM_WEBHOOK_URL")
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
|
||||
agent_name = os.environ.get("FOUNDRY_AGENT_NAME", AGENT_NAME)
|
||||
api_version = os.environ.get("HOSTING_INVOCATIONS_API_VERSION", DEFAULT_INVOCATIONS_API_VERSION)
|
||||
return f"{project_endpoint}/agents/{agent_name}/endpoint/protocols/invocations?api-version={api_version}"
|
||||
|
||||
|
||||
def _configure_observability() -> None:
|
||||
"""Wire Azure Monitor OpenTelemetry when Foundry injects a connection string."""
|
||||
conn_str = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
|
||||
if not conn_str:
|
||||
logger.info("APPLICATIONINSIGHTS_CONNECTION_STRING not set; skipping Azure Monitor export.")
|
||||
return
|
||||
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor # pyright: ignore[reportUnknownVariableType]
|
||||
|
||||
configure_azure_monitor(connection_string=conn_str)
|
||||
logger.info("Azure Monitor OpenTelemetry configured.")
|
||||
|
||||
|
||||
def telegram_hook(request: ChannelRequest, **_: object) -> ChannelRequest:
|
||||
"""Clamp request options for Telegram-originating runs."""
|
||||
options = dict(request.options or {})
|
||||
options.pop("store", None)
|
||||
options["reasoning"] = {"effort": "high", "summary": "auto"}
|
||||
return replace(request, options=options)
|
||||
|
||||
|
||||
def make_commands() -> list[ChannelCommand]:
|
||||
"""Create Telegram slash commands used by the sample."""
|
||||
|
||||
async def handle_start(ctx: ChannelCommandContext) -> None:
|
||||
await ctx.reply("Hi! Ask me for weather in Seattle, Amsterdam, Tokyo, London, or any city.")
|
||||
|
||||
async def handle_help(ctx: ChannelCommandContext) -> None:
|
||||
await ctx.reply(
|
||||
"/weather <city> - call the weather tool directly\n"
|
||||
"/whoami - show your Telegram session key\n"
|
||||
"/help - show this message"
|
||||
)
|
||||
|
||||
async def handle_whoami(ctx: ChannelCommandContext) -> None:
|
||||
await ctx.reply(f"Your session key is {telegram_isolation_key(ctx.request.attributes.get('chat_id'))}.")
|
||||
|
||||
async def handle_weather(ctx: ChannelCommandContext) -> None:
|
||||
command_text = ctx.request.input if isinstance(ctx.request.input, str) else ""
|
||||
_, _, location = command_text.partition(" ")
|
||||
await ctx.reply(lookup_weather(location=(location.strip() or "Seattle")))
|
||||
|
||||
return [
|
||||
ChannelCommand("start", "Introduce the bot", handle_start),
|
||||
ChannelCommand("help", "List available commands", handle_help),
|
||||
ChannelCommand("whoami", "Show the Telegram session key", handle_whoami),
|
||||
ChannelCommand("weather", "Call the weather tool: /weather <city>", handle_weather),
|
||||
]
|
||||
|
||||
|
||||
def build_host() -> AgentFrameworkHost:
|
||||
"""Build the Foundry-hosted Telegram weather agent."""
|
||||
# 1. Create a shared credential for model calls and Foundry storage.
|
||||
credential = DefaultAzureCredential()
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
|
||||
# 2. Create the agent with a simple weather tool and Foundry-backed history.
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=os.environ.get("MODEL_DEPLOYMENT_NAME", DEFAULT_MODEL_DEPLOYMENT),
|
||||
credential=credential,
|
||||
),
|
||||
name="TelegramInvocationsWeatherAgent",
|
||||
instructions=(
|
||||
"You are a concise weather assistant. Use lookup_weather for weather questions "
|
||||
"and answer in one short sentence."
|
||||
),
|
||||
tools=[lookup_weather],
|
||||
context_providers=[
|
||||
FoundryHostedAgentHistoryProvider(
|
||||
credential=credential,
|
||||
endpoint=project_endpoint,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# 3. Register Telegram at /invocations and keep Responses available for sanity checks.
|
||||
return AgentFrameworkHost(
|
||||
target=agent,
|
||||
allow_in_process_runner=True,
|
||||
channels=[
|
||||
ResponsesChannel(response_id_factory=foundry_response_id),
|
||||
TelegramChannel(
|
||||
bot_token=os.environ["TELEGRAM_BOT_TOKEN"],
|
||||
path="/invocations",
|
||||
transport="webhook",
|
||||
webhook_url=_foundry_invocations_webhook_url(),
|
||||
parse_mode="Markdown",
|
||||
commands=make_commands(),
|
||||
run_hook=telegram_hook,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
_configure_observability()
|
||||
enable_instrumentation(enable_sensitive_data=True)
|
||||
app = build_host().app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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]
|
||||
@@ -0,0 +1,18 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
|
||||
|
||||
requiredVersions:
|
||||
extensions:
|
||||
azure.ai.agents: '>=0.1.0-preview'
|
||||
name: ai-foundry-telegram-invocations-weather
|
||||
services:
|
||||
agent-framework-telegram-invocations-weather:
|
||||
project: .
|
||||
host: azure.ai.agent
|
||||
language: docker
|
||||
docker:
|
||||
remoteBuild: true
|
||||
config:
|
||||
container:
|
||||
resources:
|
||||
cpu: "1"
|
||||
memory: 2Gi
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
[project]
|
||||
name = "agent-framework-hosting-foundry-telegram-invocations-weather"
|
||||
version = "0.0.1"
|
||||
description = "Foundry Hosted Agents Telegram weather sample using the Invocations path."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"agent-framework-foundry",
|
||||
"agent-framework-foundry-hosting",
|
||||
"agent-framework-hosting",
|
||||
"agent-framework-hosting-responses",
|
||||
"agent-framework-hosting-telegram",
|
||||
"azure-identity",
|
||||
"aiohttp>=3.13.5",
|
||||
"hypercorn>=0.17",
|
||||
"mcp>=1.24,<2",
|
||||
"azure-monitor-opentelemetry>=1.6",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.uv.sources]
|
||||
agent-framework-foundry-hosting = { git = "https://github.com/microsoft/agent-framework.git", branch = "feature/python-hosting", subdirectory = "python/packages/foundry_hosting" }
|
||||
agent-framework-hosting = { git = "https://github.com/microsoft/agent-framework.git", branch = "feature/python-hosting", subdirectory = "python/packages/hosting" }
|
||||
agent-framework-hosting-responses = { git = "https://github.com/microsoft/agent-framework.git", branch = "feature/python-hosting", subdirectory = "python/packages/hosting-responses" }
|
||||
agent-framework-hosting-telegram = { git = "https://github.com/microsoft/agent-framework.git", branch = "feature/python-hosting", subdirectory = "python/packages/hosting-telegram" }
|
||||
@@ -15,7 +15,7 @@ of the workflow.
|
||||
`Workflow` target and dispatches to `workflow.run(...)` (no
|
||||
`Agent.create_session(...)`).
|
||||
- Two channels are mounted side-by-side (`ResponsesChannel` at
|
||||
`/responses`, `InvocationsChannel` at `/invocations/invoke`). Both
|
||||
`/responses`, `InvocationsChannel` at `/invocations`). Both
|
||||
share the **same `brief_hook`** that **adapts the channel-native
|
||||
input into the workflow start executor's typed input** — Responses
|
||||
delivers a `list[Message]`, Invocations delivers a `str`, but the
|
||||
|
||||
@@ -45,7 +45,7 @@ Content-Type: application/json
|
||||
|
||||
###
|
||||
# 4. Invocations API — structured brief
|
||||
POST {{host}}/invocations/invoke
|
||||
POST {{host}}/invocations
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -55,7 +55,7 @@ Content-Type: application/json
|
||||
|
||||
###
|
||||
# 5. Invocations API — plain topic
|
||||
POST {{host}}/invocations/invoke
|
||||
POST {{host}}/invocations
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -66,7 +66,7 @@ Content-Type: application/json
|
||||
###
|
||||
# 6. Invocations API — resume the same session_id to reuse the
|
||||
# workflow's per-conversation checkpoint store.
|
||||
POST {{host}}/invocations/invoke
|
||||
POST {{host}}/invocations
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -77,7 +77,7 @@ Content-Type: application/json
|
||||
###
|
||||
# 7. Invocations API — streaming (SSE; one `data:` line per chunk,
|
||||
# terminated by `data: [DONE]`).
|
||||
POST {{host}}/invocations/invoke
|
||||
POST {{host}}/invocations
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
|
||||
|
||||
Reference in New Issue
Block a user