diff --git a/python/samples/05-end-to-end/teams-agent/.env.example b/python/samples/05-end-to-end/teams-agent/.env.example new file mode 100644 index 00000000000..a98ff64fa27 --- /dev/null +++ b/python/samples/05-end-to-end/teams-agent/.env.example @@ -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 diff --git a/python/samples/05-end-to-end/teams-agent/README.md b/python/samples/05-end-to-end/teams-agent/README.md new file mode 100644 index 00000000000..6d670e0aaa4 --- /dev/null +++ b/python/samples/05-end-to-end/teams-agent/README.md @@ -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://.services.ai.azure.com/api/projects/" +FOUNDRY_MODEL="" + +# Teams bot credentials +CLIENT_ID="" +CLIENT_SECRET="" +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:///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/) diff --git a/python/samples/05-end-to-end/teams-agent/app.py b/python/samples/05-end-to-end/teams-agent/app.py new file mode 100644 index 00000000000..85e702eaa5e --- /dev/null +++ b/python/samples/05-end-to-end/teams-agent/app.py @@ -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): + 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()