Skip to content
Closed
11 changes: 11 additions & 0 deletions python/samples/05-end-to-end/teams-agent/.env.example
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
72 changes: 72 additions & 0 deletions python/samples/05-end-to-end/teams-agent/README.md
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/)
116 changes: 116 additions & 0 deletions python/samples/05-end-to-end/teams-agent/app.py
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())

Copy link
Copy Markdown
Contributor

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.py dispatches each activity concurrently and AgentSession has no internal locking, so both turns run agent.run against 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 (an asyncio.Lock per id) or at least add a NOTE like the approval_mode one? The _sessions dict also grows without bound in a long-running bot, which is a real leak for anyone copying this into production.


text = ctx.activity.text or ""
if not text.strip():
return

async for chunk in agent.run(text, session=session, stream=True):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 stream.close() waits ~30s for a stream id before silently dropping the reply, so the user gets nothing. Could the sample fall back to a plain ctx.send when ctx.activity.conversation isn't personal, or should the README call out that this sample is 1:1 chat only?

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()
Loading