-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Python: Add Teams SDK end-to-end sample #6676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4645d13
bd05d19
ced7501
913a4fc
2de9289
e4c2604
d22cedf
e11594b
506e88a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Microsoft Foundry (model the agent uses). Authenticate with `az login`. | ||
| FOUNDRY_PROJECT_ENDPOINT= | ||
| FOUNDRY_MODEL= | ||
|
|
||
| # Teams bot credentials (from your Azure Bot / app registration) | ||
| CLIENT_ID= | ||
| CLIENT_SECRET= | ||
| TENANT_ID= | ||
|
|
||
| # Local hosting | ||
| PORT=3978 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Microsoft Agent Framework Python Weather Agent sample (Teams SDK) | ||
|
|
||
| This sample demonstrates a simple Weather Forecast Agent built with the Python Microsoft Agent Framework, hosted as a Microsoft Teams bot through the [Teams SDK (teams.py)](https://github.com/microsoft/teams.py). The agent accepts natural language weather requests, streams its reply token-by-token into the chat, and remembers context across turns. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Python 3.11+ | ||
| - [uv](https://github.com/astral-sh/uv) for fast dependency management | ||
| - [devtunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) for local testing | ||
| - A Microsoft Foundry project with a deployed model | ||
| - The [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) (run `az login` to authenticate) | ||
| - A Teams bot registration ([Azure Bot](https://learn.microsoft.com/azure/bot-service/abs-quickstart)) — App ID, password, and tenant | ||
|
|
||
| ## Configuration | ||
|
|
||
| Create a `.env` file in this sample folder (see [.env.example](.env.example)): | ||
|
|
||
| ```bash | ||
| # Microsoft Foundry (model the agent uses). Authenticate with `az login`. | ||
| FOUNDRY_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<project-name>" | ||
| FOUNDRY_MODEL="<deployment-name>" | ||
|
|
||
| # Teams bot credentials | ||
| CLIENT_ID="<app-id>" | ||
| CLIENT_SECRET="<client-secret>" | ||
| TENANT_ID="<tenant-id>" | ||
|
|
||
| # Local hosting | ||
| PORT=3978 | ||
| ``` | ||
|
|
||
| `FOUNDRY_MODEL` is the **deployment name** of your model, not the base model name. | ||
|
|
||
| ## Running the Agent Locally | ||
|
|
||
| Authenticate with the Azure CLI, then start the app: | ||
|
|
||
| ```bash | ||
| az login | ||
| uv run app.py | ||
| ``` | ||
|
|
||
| The bot starts an HTTP listener on `http://localhost:3978`; its messaging endpoint is `POST /api/messages`. | ||
|
|
||
| ## Testing in Teams | ||
|
|
||
| To exchange messages with the bot from Teams, Teams needs to reach your local endpoint: | ||
|
|
||
| 1. Create an Azure Bot (choose Client Secret auth for local tunneling) and copy its App ID, password, and tenant into `.env`. See [Create an Azure Bot resource](https://learn.microsoft.com/azure/bot-service/abs-quickstart) for step-by-step instructions. | ||
| 2. Host a dev tunnel: | ||
|
|
||
| ```bash | ||
| devtunnel host -p 3978 --allow-anonymous | ||
| ``` | ||
|
|
||
| 3. Set the bot's **Messaging endpoint** to `https://<tunnel-host>/api/messages`. | ||
| 4. Run the agent: `uv run app.py`. | ||
| 5. Register the bot as a Teams app and install it into a Teams chat, then message it, e.g. `What's the weather in Seattle?`. See [Register a Teams app in the Developer Portal](https://learn.microsoft.com/microsoftteams/platform/concepts/build-and-test/teams-developer-portal#register-an-app) for the manifest and sideloading steps. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **404 on `/api/messages`**: Ensure you are POSTing and using the correct tunnel URL. | ||
| - **Empty responses**: Check that `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL` are valid and that you have run `az login`. | ||
| - **Auth errors from Teams**: Validate `CLIENT_ID` / `CLIENT_SECRET` / `TENANT_ID` match your Azure Bot registration. | ||
|
|
||
| ## Further Reading | ||
|
|
||
| - [Microsoft Teams SDK for Python (teams.py)](https://github.com/microsoft/teams.py) | ||
| - [Teams SDK for Python — Getting started](https://microsoft.github.io/teams-sdk/python/getting-started/) | ||
| - [Create an Azure Bot resource](https://learn.microsoft.com/azure/bot-service/abs-quickstart) | ||
| - [Register a Teams app in the Developer Portal](https://learn.microsoft.com/microsoftteams/platform/concepts/build-and-test/teams-developer-portal#register-an-app) | ||
| - [Devtunnel docs](https://learn.microsoft.com/azure/developer/dev-tunnels/) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # /// script | ||
| # requires-python = ">=3.11" | ||
| # dependencies = [ | ||
| # "microsoft-teams-apps", | ||
| # "agent-framework-foundry", | ||
| # "azure-identity", | ||
| # ] | ||
| # /// | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
| # Run with any PEP 723 compatible runner, e.g.: | ||
| # uv run samples/05-end-to-end/teams-agent/app.py | ||
|
|
||
| import asyncio | ||
| import logging | ||
| from random import randint | ||
| from typing import Annotated | ||
|
|
||
| from agent_framework import Agent, AgentSession, tool | ||
| from agent_framework.foundry import FoundryChatClient | ||
| from azure.identity import AzureCliCredential | ||
| from dotenv import load_dotenv | ||
| from microsoft_teams.api import CardAction, CardActionType, MessageActivity, MessageActivityInput, SuggestedActions | ||
| from microsoft_teams.apps import ActivityContext, App | ||
| from pydantic import Field | ||
|
|
||
| # Load environment variables from .env file | ||
| load_dotenv() | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| """ | ||
| Demo application using the Microsoft Teams SDK (teams.py). | ||
|
|
||
| This sample demonstrates how to build an AI agent using the Agent Framework, | ||
| hosted as a Microsoft Teams bot through the Teams SDK. | ||
|
|
||
| Key features: | ||
| - Loads Foundry project configuration and Teams bot credentials from environment variables. | ||
| - Demonstrates agent creation and tool registration. | ||
| - Streams the agent response token-by-token into the Teams chat. | ||
| - Maintains per-conversation AgentSession for multi-turn memory. | ||
|
|
||
| To run, set the Teams bot credentials and Foundry project settings (check .env.example) and | ||
| run `az login`, then point your bot's messaging endpoint at this app (e.g. via a dev tunnel). | ||
| """ | ||
|
|
||
|
|
||
| # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in | ||
| # production; see samples/02-agents/tools/function_tool_with_approval.py. | ||
| @tool(approval_mode="never_require") | ||
| def get_weather( | ||
| location: Annotated[str, "The location to get the weather for."], | ||
| ) -> str: | ||
| """Generate a mock weather report for the provided location.""" | ||
| conditions = ["sunny", "cloudy", "rainy", "stormy"] | ||
| return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." | ||
|
|
||
|
|
||
| def build_agent() -> Agent: | ||
| """Create and return the agent instance with the weather tool registered.""" | ||
| client = FoundryChatClient(credential=AzureCliCredential()) | ||
| return Agent( | ||
| client=client, | ||
| name="WeatherAgent", | ||
| instructions="You are a helpful weather agent. Keep your answers brief.", | ||
| tools=get_weather, | ||
| ) | ||
|
|
||
|
|
||
| # Reads CLIENT_ID, CLIENT_SECRET, TENANT_ID, and PORT from the environment. | ||
| app = App() | ||
| agent = build_agent() | ||
|
|
||
| # Per-conversation sessions preserve message history across turns. | ||
| _sessions: dict[str, AgentSession] = {} | ||
|
|
||
|
|
||
| @app.on_message | ||
| async def handle_message(ctx: ActivityContext[MessageActivity]) -> None: | ||
| """Run the agent for each incoming Teams message and stream the reply back.""" | ||
| try: | ||
| conversation_id = ctx.activity.conversation.id | ||
| session = _sessions.setdefault(conversation_id, agent.create_session()) | ||
|
|
||
| text = ctx.activity.text or "" | ||
| if not text.strip(): | ||
| return | ||
|
|
||
| async for chunk in agent.run(text, session=session, stream=True): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have we tested this in a group chat or channel? Teams only supports streamed bot messages in personal (1:1) scope, and the SDK flushes chunks in a fire-and-forget task, so in group scope the send failure never reaches this handler's try/except and |
||
| if chunk.text: | ||
| ctx.stream.emit(chunk.text) | ||
|
|
||
| # Add suggested follow-up questions and AI generated label after streaming completes | ||
| suggested_actions = SuggestedActions( | ||
| to=[ctx.activity.from_.id], | ||
| actions=[ | ||
| CardAction(type=CardActionType.IM_BACK, title="New York weather", value="What's the weather in New York?"), | ||
| CardAction(type=CardActionType.IM_BACK, title="San Francisco weather", value="What's the weather in San Francisco?"), | ||
| ], | ||
| ) | ||
| reply = MessageActivityInput().add_ai_generated().add_feedback() | ||
| reply.with_suggested_actions(suggested_actions) | ||
| ctx.stream.emit(reply) | ||
| except Exception as e: | ||
| logger.exception("Error handling message: %s", e) | ||
| await ctx.send("Sorry, an error occurred while processing your message.") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| """Entry point: start the Teams bot HTTP listener.""" | ||
| asyncio.run(app.start()) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when two messages land in the same conversation at nearly the same time?
teams.pydispatches each activity concurrently andAgentSessionhas no internal locking, so both turns runagent.runagainst the same session and can interleave or duplicate history. Since this sample is the one advertising the per-conversation session pattern, should we serialize turns per conversation (anasyncio.Lockper id) or at least add a NOTE like theapproval_modeone? The_sessionsdict also grows without bound in a long-running bot, which is a real leak for anyone copying this into production.