From 6a0d1291534f82b6563bf570be14402ae2e2587c Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Mon, 20 Oct 2025 12:12:31 -0700 Subject: [PATCH 01/10] init botbuilder --- README.md | 9 +- packages/botbuilder/README.md | 0 packages/botbuilder/pyproject.toml | 31 ++++ .../microsoft/teams/botbuilder/__init__.py | 8 ++ .../teams/botbuilder/botbuilder_plugin.py | 133 ++++++++++++++++++ pyproject.toml | 1 + pyrightconfig.json | 3 +- uv.lock | 120 ++++++++++++++++ 8 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 packages/botbuilder/README.md create mode 100644 packages/botbuilder/pyproject.toml create mode 100644 packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py create mode 100644 packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py diff --git a/README.md b/README.md index fab847a4..130ec285 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ > [!CAUTION] -> This project is in public preview. We’ll do our best to maintain compatibility, but there may be breaking changes in upcoming releases. +> This project is in public preview. We’ll do our best to maintain compatibility, but there may be breaking changes in upcoming releases. # Microsoft Teams AI Library for Python @@ -19,17 +19,20 @@ A comprehensive SDK for building Microsoft Teams applications, bots, and AI agen ### Prerequisites - UV version is >= 0.8.11. Install and upgrade from [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/). -- Python version is >= 3.12. Install or upgrade from [python.org/downloads](https://www.python.org/downloads/). +- Python version is >= 3.12. Install or upgrade from [python.org/downloads](https://www.python.org/downloads/). ### Installation #### 1. Install the dependencies. + ```bash uv sync --all-packages --group dev ``` #### 2. Activate the virtual env + > **Note:** After the initial setup, you need to activate the virtual environment each time you start a new terminal session + ```bash # On Mac `source .venv/bin/activate` @@ -39,6 +42,7 @@ A comprehensive SDK for building Microsoft Teams applications, bots, and AI agen ``` #### 3. Install the pre-commit hooks + ```bash pre-commit install ``` @@ -55,6 +59,7 @@ A comprehensive SDK for building Microsoft Teams applications, bots, and AI agen - [`microsoft-teams-devtools`](./packages/devtools/README.md) - [`microsoft-teams-graph`](./packages/graph/README.md) - [`microsoft-teams-openai`](./packages/openai/README.md) +- [`microsoft-teams-botbuilder`](./packages/botbuilder/README.md) > external packages to integrate with external protocols and microsoft-teams-cards diff --git a/packages/botbuilder/README.md b/packages/botbuilder/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml new file mode 100644 index 00000000..aa2e60bf --- /dev/null +++ b/packages/botbuilder/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "microsoft-teams-botbuilder" +version = "2.0.0a2" +description = "" +authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] +readme = "README.md" +requires-python = ">=3.12" +repository = "https://github.com/microsoft/teams.py" +keywords = ["microsoft", "teams", "ai", "bot", "agents"] +license = "MIT" +dependencies = [ + "botbuilder-core>=4.14.0", + "botbuilder-integration-aiohttp>=4.17.0", + "microsoft-teams-apps>=2.0.0a2", + "microsoft-teams-api>=2.0.0a2", + "microsoft-teams-common>=2.0.0a2", +] + +[project.urls] +Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/botbuilder/src/microsoft/teams/botbuilder" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/microsoft"] + +[tool.hatch.build.targets.sdist] +include = ["src"] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py new file mode 100644 index 00000000..56205f42 --- /dev/null +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .botbuilder_plugin import BotBuilderPlugin + +__all__ = ["BotBuilderPlugin"] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py new file mode 100644 index 00000000..36cd3fd9 --- /dev/null +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -0,0 +1,133 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import importlib.metadata +from dataclasses import dataclass +from logging import Logger +from typing import Annotated, Optional + +from fastapi import Request +from microsoft.teams.api import Credentials +from microsoft.teams.apps import ( + DependencyMetadata, + HttpPlugin, + LoggerDependencyOptions, + Plugin, +) +from pydantic import BaseModel + +from botbuilder.core import ActivityHandler, TurnContext # pyright: ignore[reportMissingTypeStubs] +from botbuilder.integration.aiohttp import ( # pyright: ignore[reportMissingTypeStubs] + CloudAdapter, + ConfigurationBotFrameworkAuthentication, + ConfigurationServiceClientCredentialFactory, +) +from botbuilder.schema import Activity # pyright: ignore[reportMissingTypeStubs] + +version = importlib.metadata.version("microsoft-teams-botbuilder") + + +class BotBuilderPluginOptions(BaseModel): + """Options for configuring the BotBuilder plugin.""" + + skip_auth: bool = False + handler: Optional[ActivityHandler] = None + adapter: Optional[CloudAdapter] = None + + +@dataclass +class BotFrameworkConfig: + APP_TYPE: str + APP_ID: Optional[str] + APP_PASSWORD: Optional[str] + APP_TENANTID: Optional[str] + + +@Plugin(name="botbuilder-plugin", version=version) +class BotBuilderPlugin(HttpPlugin): + """ + BotBuilder plugin that provides Microsoft Bot Framework integration. + + This plugin extends HttpPlugin and provides Bot Framework capabilities + including CloudAdapter integration and activity handling. + """ + + # Dependency injections using type annotations + logger: Annotated[Logger, LoggerDependencyOptions()] + + credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)] + + def __init__(self, options: Optional[BotBuilderPluginOptions] = None): + """ + Initialize the BotBuilder plugin. + + Args: + options: Configuration options for the plugin + """ + self.options = options or BotBuilderPluginOptions() + + # Initialize HttpPlugin + super().__init__( + app_id=self.credentials.client_id if self.credentials else None, + skip_auth=self.options.skip_auth, + ) + + self.handler: Optional[ActivityHandler] = self.options.handler + self.adapter: Optional[CloudAdapter] = self.options.adapter + + async def on_init(self) -> None: + """Initialize the plugin when the app starts.""" + await super().on_init() + + if not self.adapter: + # Extract credentials for Bot Framework authentication + client_id: Optional[str] = None + client_secret: Optional[str] = None + tenant_id: Optional[str] = None + + if self.credentials: + client_id = getattr(self.credentials, "client_id", None) + client_secret = getattr(self.credentials, "client_secret", None) + tenant_id = getattr(self.credentials, "tenant_id", None) + + config = BotFrameworkConfig( + APP_TYPE="SingleTenant" if tenant_id else "MultiTenant", + APP_ID=client_id, + APP_PASSWORD=client_secret, + APP_TENANTID=tenant_id, + ) + + self.adapter = CloudAdapter( + ConfigurationBotFrameworkAuthentication( + ConfigurationServiceClientCredentialFactory(config, logger=self.logger) + ) + ) + + self.logger.info("BotBuilder plugin initialized successfully") + + async def on_request(self, request: Request): + if not self.adapter: + raise RuntimeError("plugin not registered") + + # Parse activity data + body = await request.json() + + activity_type = body.get("type", "unknown") + activity_id = body.get("id", "unknown") + + self.logger.debug(f"Received activity: {activity_type} (ID: {activity_id})") + + activity: Activity = Activity().deserialize(body) # type: ignore + + async def logic(turn_context: TurnContext): + if not turn_context.activity.id: + return + + if self.handler: + await self.handler.on_turn(turn_context) + return + + auth_header = request.headers.get("Authorization", "") + await self.adapter.process_activity(auth_header, activity, logic) # type: ignore diff --git a/pyproject.toml b/pyproject.toml index 74287ac6..46d54ac3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ "microsoft-teams-openai" = { workspace = true } "microsoft-teams-mcpplugin" = { workspace = true } "microsoft-teams-a2a" = { workspace = true } +"microsoft-teams-botbuilder" = { workspace = true } [tool.uv.workspace] members = ["packages/*", "tests/*"] diff --git a/pyrightconfig.json b/pyrightconfig.json index 7702708a..0942b198 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -15,7 +15,8 @@ "packages/ai/src", "packages/openai/src", "packages/mcpplugin/src", - "packages/a2aprotocol/src" + "packages/a2aprotocol/src", + "packages/botbuilder/src" ], "typeCheckingMode": "strict", "executionEnvironments": [ diff --git a/uv.lock b/uv.lock index a08060bc..ee73597e 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ members = [ "microsoft-teams-ai", "microsoft-teams-api", "microsoft-teams-apps", + "microsoft-teams-botbuilder", "microsoft-teams-cards", "microsoft-teams-common", "microsoft-teams-devtools", @@ -34,6 +35,7 @@ members = [ [manifest.dependency-groups] dev = [ { name = "cookiecutter", specifier = ">=2.6.0" }, + { name = "microsoft-teams-botbuilder", editable = "packages/botbuilder" }, { name = "poethepoet", specifier = ">=0.35.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pyright", specifier = ">=1.1.404" }, @@ -309,6 +311,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006, upload-time = "2017-08-03T15:55:31.23Z" }, ] +[[package]] +name = "botbuilder-core" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, + { name = "botframework-streaming" }, + { name = "jsonpickle" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/51/0e4d0ba1fc25d57977e17d7bfd55c7bd06a9d8403d5efb23e6fc512356fa/botbuilder_core-4.17.0-py3-none-any.whl", hash = "sha256:56828b11d9af663a200fba0fae4e0b9ad6f815a679c3e90e9b6425b9cbe16d53", size = 116148, upload-time = "2025-05-29T15:10:00.474Z" }, +] + +[[package]] +name = "botbuilder-schema" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msrest" }, + { name = "urllib3" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/10/db5e16bf91fe60b78c88bf42b46e87af7f75097303cece71478a5ae46ccf/botbuilder_schema-4.17.0-py2.py3-none-any.whl", hash = "sha256:97219f8361a91bfa1529ba1231100b527adbe7c4c249d6b23fe1bffaf012b31b", size = 38197, upload-time = "2025-05-29T15:10:05.111Z" }, +] + +[[package]] +name = "botframework-connector" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "msal" }, + { name = "msrest" }, + { name = "pyjwt" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/df/8daa548a5c73d673e8464cbd156dd8235d6efc35296ad45e89ff9f97c109/botframework_connector-4.17.0-py2.py3-none-any.whl", hash = "sha256:51b8702cc348c63efbdb571f6a4da868d1660efb80ed77d6e04f081720105a87", size = 100886, upload-time = "2025-05-29T15:10:07.271Z" }, +] + +[[package]] +name = "botframework-streaming" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/d2/c55470c918c403ebd76b3c1b54e312021bf7edacaf61e4b1c3b132216490/botframework_streaming-4.17.0-py3-none-any.whl", hash = "sha256:ed64fd2a9f56a3d32dd252bf378d0ce145a9bbc9c9a9a0e770889c7d140df435", size = 41953, upload-time = "2025-05-29T15:10:08.499Z" }, +] + [[package]] name = "cachetools" version = "6.2.0" @@ -1225,6 +1279,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, ] +[[package]] +name = "jsonpickle" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/df/8072fb98c12d78dd29b4a52c50af7ab548f84166b8a3d363c1c754c14af0/jsonpickle-1.4.2.tar.gz", hash = "sha256:c9b99b28a9e6a3043ec993552db79f4389da11afcb1d0246d93c79f4b5e64062", size = 104745, upload-time = "2020-11-30T03:21:56.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d5/1cc282dc23346a43aab461bf2e8c36593aacd34242bee1a13fa750db0cfe/jsonpickle-1.4.2-py2.py3-none-any.whl", hash = "sha256:2ac5863099864c63d7f0c367af5e512c94f3384977dd367f2eae5f2303f7b92c", size = 36529, upload-time = "2020-11-30T03:21:53.857Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -1637,6 +1700,25 @@ test = [ { name = "pytest-asyncio", specifier = ">=0.24.0" }, ] +[[package]] +name = "microsoft-teams-botbuilder" +version = "2.0.0a2" +source = { editable = "packages/botbuilder" } +dependencies = [ + { name = "botbuilder-core" }, + { name = "microsoft-teams-api" }, + { name = "microsoft-teams-apps" }, + { name = "microsoft-teams-common" }, +] + +[package.metadata] +requires-dist = [ + { name = "botbuilder-core", specifier = ">=4.14.0" }, + { name = "microsoft-teams-api", editable = "packages/api" }, + { name = "microsoft-teams-apps", editable = "packages/apps" }, + { name = "microsoft-teams-common", editable = "packages/common" }, +] + [[package]] name = "microsoft-teams-cards" version = "2.0.0a2" @@ -1846,6 +1928,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c4/3c315d2a00de25780a83f3a0392861571c0b00b74c0e66ed66dc32a357c3/msgraph_sdk-1.40.0-py3-none-any.whl", hash = "sha256:1f2e966ccfded5fade55225f2f671b965a7ad58ba16f34be05fccc459596a076", size = 24750562, upload-time = "2025-07-30T16:02:14.48Z" }, ] +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, +] + [[package]] name = "multidict" version = "6.6.4" @@ -1933,6 +2031,15 @@ requires-dist = [ { name = "microsoft-teams-apps", editable = "packages/apps" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "openai" version = "1.102.0" @@ -2517,6 +2624,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "rfc3339-validator" version = "0.1.4" From f767d39a7d425398aef1f4fef8619eeb14e8f66f Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 28 Oct 2025 17:26:18 -0700 Subject: [PATCH 02/10] init botbuilder --- .../src/microsoft/teams/apps/http_plugin.py | 71 +++++----- packages/botbuilder/README.md | 17 +++ packages/botbuilder/pyproject.toml | 22 +++- .../microsoft/teams/botbuilder/__init__.py | 4 +- .../teams/botbuilder/botbuilder_plugin.py | 122 +++++++++--------- .../tests/test_botbuilder_plugin.py | 113 ++++++++++++++++ tests/botbuilder/README.md | 0 tests/botbuilder/pyproject.toml | 16 +++ tests/botbuilder/src/main.py | 38 ++++++ uv.lock | 40 +++++- 10 files changed, 349 insertions(+), 94 deletions(-) create mode 100644 packages/botbuilder/tests/test_botbuilder_plugin.py create mode 100644 tests/botbuilder/README.md create mode 100644 tests/botbuilder/pyproject.toml create mode 100644 tests/botbuilder/src/main.py diff --git a/packages/apps/src/microsoft/teams/apps/http_plugin.py b/packages/apps/src/microsoft/teams/apps/http_plugin.py index ab0c6d40..d53fa27c 100644 --- a/packages/apps/src/microsoft/teams/apps/http_plugin.py +++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py @@ -306,38 +306,51 @@ async def _handle_activity_request(self, request: Request) -> Any: return result + def _handle_activity_response(self, response: Response, result: Any) -> Any: + """ + Handle the activity response formatting. + + Args: + response: The FastAPI response object + result: The result from activity processing + + Returns: + The formatted response + """ + status_code: Optional[int] = None + body: Optional[Dict[str, Any]] = None + resp_dict: Optional[Dict[str, Any]] = None + if isinstance(result, dict): + resp_dict = cast(Dict[str, Any], result) + elif isinstance(result, BaseModel): + resp_dict = result.model_dump(exclude_none=True) + + # if resp_dict has status set it + if resp_dict and "status" in resp_dict: + status_code = resp_dict.get("status") + + if resp_dict and "body" in resp_dict: + body = resp_dict.get("body", None) + + if status_code is not None: + response.status_code = status_code + + if body is not None: + self.logger.debug(f"Returning body {body}") + return body + self.logger.debug("Returning empty body") + return response + + async def on_activity_request(self, request: Request, response: Response) -> Any: + """Handle incoming Teams activity.""" + # Process the activity (token validation handled by middleware) + result = await self._handle_activity_request(request) + return self._handle_activity_response(response, result) + def _setup_routes(self) -> None: """Setup FastAPI routes.""" - async def on_activity_request(request: Request, response: Response) -> Any: - """Handle incoming Teams activity.""" - # Process the activity (token validation handled by middleware) - result = await self._handle_activity_request(request) - status_code: Optional[int] = None - body: Optional[Dict[str, Any]] = None - resp_dict: Optional[Dict[str, Any]] = None - if isinstance(result, dict): - resp_dict = cast(Dict[str, Any], result) - elif isinstance(result, BaseModel): - resp_dict = result.model_dump(exclude_none=True) - - # if resp_dict has status set it - if resp_dict and "status" in resp_dict: - status_code = resp_dict.get("status") - - if resp_dict and "body" in resp_dict: - body = resp_dict.get("body", None) - - if status_code is not None: - response.status_code = status_code - - if body is not None: - self.logger.debug(f"Returning body {body}") - return body - self.logger.debug("Returning empty body") - return response - - self.app.post("/api/messages")(on_activity_request) + self.app.post("/api/messages")(self.on_activity_request) async def health_check() -> Dict[str, Any]: """Basic health check endpoint.""" diff --git a/packages/botbuilder/README.md b/packages/botbuilder/README.md index e69de29b..998bb25a 100644 --- a/packages/botbuilder/README.md +++ b/packages/botbuilder/README.md @@ -0,0 +1,17 @@ +# Microsoft Teams BotBuilder + +

+ + + + + + +

+ +A package used to make the `@microsoft/teams.apps` package backwards compatible with legacy bots built using +`BotBuilder`. + + + + diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml index aa2e60bf..603a1b00 100644 --- a/packages/botbuilder/pyproject.toml +++ b/packages/botbuilder/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "microsoft-teams-botbuilder" version = "2.0.0a2" -description = "" +description = "BotBuilder plugin for Microsoft Teams" authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] readme = "README.md" requires-python = ">=3.12" @@ -11,9 +11,18 @@ license = "MIT" dependencies = [ "botbuilder-core>=4.14.0", "botbuilder-integration-aiohttp>=4.17.0", - "microsoft-teams-apps>=2.0.0a2", - "microsoft-teams-api>=2.0.0a2", - "microsoft-teams-common>=2.0.0a2", + "microsoft-teams-apps", + "microsoft-teams-api", + "microsoft-teams-common", + "coverage>=7.8.0", + "pytest>=8.3.5", + "ruff>=0.11.5", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.0", + "pytest-asyncio>=1.0.0", ] [project.urls] @@ -29,3 +38,8 @@ packages = ["src/microsoft"] [tool.hatch.build.targets.sdist] include = ["src"] + +[tool.uv.sources] +microsoft-teams-apps = { workspace = true } +microsoft-teams-api = { workspace = true } +microsoft-teams-common = { workspace = true } \ No newline at end of file diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py index 56205f42..73a28a2d 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py @@ -3,6 +3,6 @@ Licensed under the MIT License. """ -from .botbuilder_plugin import BotBuilderPlugin +from .botbuilder_plugin import BotBuilderPlugin, BotBuilderPluginOptions -__all__ = ["BotBuilderPlugin"] +__all__ = ["BotBuilderPlugin", "BotBuilderPluginOptions"] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py index 36cd3fd9..36555bbf 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -1,81 +1,81 @@ +# pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false + """ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. """ import importlib.metadata -from dataclasses import dataclass from logging import Logger -from typing import Annotated, Optional +from types import SimpleNamespace +from typing import Annotated, Any, Callable, Optional, TypedDict, Unpack, cast -from fastapi import Request -from microsoft.teams.api import Credentials +from fastapi import HTTPException, Request, Response +from microsoft.teams.api import Credentials, TokenProtocol from microsoft.teams.apps import ( + ActivityEvent, DependencyMetadata, + ErrorEvent, + EventMetadata, HttpPlugin, LoggerDependencyOptions, Plugin, ) -from pydantic import BaseModel +from microsoft.teams.common import Client -from botbuilder.core import ActivityHandler, TurnContext # pyright: ignore[reportMissingTypeStubs] -from botbuilder.integration.aiohttp import ( # pyright: ignore[reportMissingTypeStubs] +from botbuilder.core import ( + ActivityHandler, + TurnContext, +) +from botbuilder.integration.aiohttp import ( CloudAdapter, ConfigurationBotFrameworkAuthentication, - ConfigurationServiceClientCredentialFactory, ) -from botbuilder.schema import Activity # pyright: ignore[reportMissingTypeStubs] +from botbuilder.schema import Activity version = importlib.metadata.version("microsoft-teams-botbuilder") -class BotBuilderPluginOptions(BaseModel): +class BotBuilderPluginOptions(TypedDict, total=False): """Options for configuring the BotBuilder plugin.""" - skip_auth: bool = False - handler: Optional[ActivityHandler] = None - adapter: Optional[CloudAdapter] = None - + skip_auth: bool + handler: ActivityHandler + adapter: CloudAdapter -@dataclass -class BotFrameworkConfig: - APP_TYPE: str - APP_ID: Optional[str] - APP_PASSWORD: Optional[str] - APP_TENANTID: Optional[str] - -@Plugin(name="botbuilder-plugin", version=version) +@Plugin(name="http", version=version, description="BotBuilder plugin for Microsoft Bot Framework integration") class BotBuilderPlugin(HttpPlugin): """ BotBuilder plugin that provides Microsoft Bot Framework integration. - - This plugin extends HttpPlugin and provides Bot Framework capabilities - including CloudAdapter integration and activity handling. """ - # Dependency injections using type annotations + # Dependency injections logger: Annotated[Logger, LoggerDependencyOptions()] - credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)] + client: Annotated[Client, DependencyMetadata()] + + bot_token: Annotated[Optional[Callable[[], TokenProtocol]], DependencyMetadata(optional=True)] + graph_token: Annotated[Optional[Callable[[], TokenProtocol]], DependencyMetadata(optional=True)] + + on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")] + on_activity_event: Annotated[Callable[[ActivityEvent], None], EventMetadata(name="activity")] - def __init__(self, options: Optional[BotBuilderPluginOptions] = None): + def __init__(self, **options: Unpack[BotBuilderPluginOptions]): """ Initialize the BotBuilder plugin. Args: options: Configuration options for the plugin """ - self.options = options or BotBuilderPluginOptions() - - # Initialize HttpPlugin + self.options = options super().__init__( - app_id=self.credentials.client_id if self.credentials else None, - skip_auth=self.options.skip_auth, + app_id=None, + skip_auth=self.options.get("skip_auth", False), ) - self.handler: Optional[ActivityHandler] = self.options.handler - self.adapter: Optional[CloudAdapter] = self.options.adapter + self.handler: Optional[ActivityHandler] = self.options.get("handler") + self.adapter: Optional[CloudAdapter] = self.options.get("adapter") async def on_init(self) -> None: """Initialize the plugin when the app starts.""" @@ -92,42 +92,48 @@ async def on_init(self) -> None: client_secret = getattr(self.credentials, "client_secret", None) tenant_id = getattr(self.credentials, "tenant_id", None) - config = BotFrameworkConfig( - APP_TYPE="SingleTenant" if tenant_id else "MultiTenant", + config = SimpleNamespace( + APP_TYPE="singletenant" if tenant_id else "multitenant", APP_ID=client_id, APP_PASSWORD=client_secret, APP_TENANTID=tenant_id, ) - self.adapter = CloudAdapter( - ConfigurationBotFrameworkAuthentication( - ConfigurationServiceClientCredentialFactory(config, logger=self.logger) - ) - ) + self.adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(configuration=config)) - self.logger.info("BotBuilder plugin initialized successfully") + self.logger.info("BotBuilder plugin initialized successfully") - async def on_request(self, request: Request): + async def on_activity_request(self, request: Request, response: Response) -> Any: if not self.adapter: raise RuntimeError("plugin not registered") - # Parse activity data - body = await request.json() + try: + # Parse activity data + body = await request.json() + activity_bf = cast(Activity, Activity().deserialize(body)) - activity_type = body.get("type", "unknown") - activity_id = body.get("id", "unknown") + # A POST request must contain an Activity + if not activity_bf.type: + raise HTTPException(status_code=400, detail="Missing activity type") - self.logger.debug(f"Received activity: {activity_type} (ID: {activity_id})") + async def logic(turn_context: TurnContext): + if not turn_context.activity.id: + return - activity: Activity = Activity().deserialize(body) # type: ignore + # Handle activity with botframework handler + if self.handler: + await self.handler.on_turn(turn_context) - async def logic(turn_context: TurnContext): - if not turn_context.activity.id: - return + # Grab the auth header from the inbound request + auth_header = request.headers["Authorization"] if "Authorization" in request.headers else "" + await self.adapter.process_activity(auth_header, activity_bf, logic) - if self.handler: - await self.handler.on_turn(turn_context) - return + # Call HTTP plugin to handle activity request + result = await self._handle_activity_request(request) + return self._handle_activity_response(response, result) - auth_header = request.headers.get("Authorization", "") - await self.adapter.process_activity(auth_header, activity, logic) # type: ignore + except HTTPException: + raise + except Exception as err: + self.logger.error(f"Error processing activity: {err}", exc_info=True) + raise HTTPException(status_code=500, detail=str(err)) from err diff --git a/packages/botbuilder/tests/test_botbuilder_plugin.py b/packages/botbuilder/tests/test_botbuilder_plugin.py new file mode 100644 index 00000000..33d117d8 --- /dev/null +++ b/packages/botbuilder/tests/test_botbuilder_plugin.py @@ -0,0 +1,113 @@ +# pyright: reportMissingTypeStubs=false +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from botbuilder.core import ActivityHandler, TurnContext +from botbuilder.integration.aiohttp import CloudAdapter +from botbuilder.schema import Activity +from fastapi import HTTPException, Request, Response +from microsoft.teams.api import Credentials +from microsoft.teams.botbuilder import BotBuilderPlugin + + +class TestBotBuilderPlugin: + """Tests for BotBuilderPlugin.""" + + @pytest.fixture + def mock_logger(self): + return MagicMock() + + @pytest.fixture + def plugin_without_adapter(self): + plugin = BotBuilderPlugin(skip_auth=True) + plugin.credentials = MagicMock(spec=Credentials) + plugin.credentials.client_id = "abc" + plugin.credentials.client_secret = "secret" + plugin.credentials.tenant_id = "tenant-123" + return plugin + + @pytest.fixture + def plugin_with_adapter(self) -> BotBuilderPlugin: + adapter = MagicMock(spec=CloudAdapter) + plugin = BotBuilderPlugin(adapter=adapter, skip_auth=True) + plugin._handle_activity_request = AsyncMock(return_value="fake_result") # pyright: ignore[reportPrivateUsage] + handler = AsyncMock(spec=ActivityHandler) + plugin.handler = handler + return plugin + + @pytest.mark.asyncio + async def test_on_init_creates_adapter_when_missing(self, plugin_without_adapter: BotBuilderPlugin): + assert plugin_without_adapter.adapter is None + + with ( + patch("microsoft.teams.botbuilder.botbuilder_plugin.CloudAdapter") as mock_adapter_class, + patch( + "microsoft.teams.botbuilder.botbuilder_plugin.ConfigurationBotFrameworkAuthentication" + ) as mock_config_class, + ): + mock_adapter_class.return_value = "mock_adapter" + await plugin_without_adapter.on_init() + + mock_config_class.assert_called_once() + mock_adapter_class.assert_called_once() + assert plugin_without_adapter.adapter == "mock_adapter" + + @pytest.mark.asyncio + async def test_on_activity_request_calls_adapter_and_handler(self, plugin_with_adapter: BotBuilderPlugin): + # Mock request and response + activity_data = { + "type": "message", + "id": "activity-id", + "from": {"id": "user1", "name": "Test User"}, + "recipient": {"id": "bot1", "name": "Test Bot"}, + "conversation": {"id": "conv1"}, + "serviceUrl": "https://service.url", + } + request = AsyncMock(spec=Request) + request.json.return_value = activity_data + request.headers = {"Authorization": "Bearer token"} + + response = MagicMock(spec=Response) + + # Mock adapter.process_activity to call logic with a mock TurnContext + async def fake_process_activity(auth_header, activity, logic): # type: ignore + print("Inside fake_process_activity") + await logic(MagicMock(spec=TurnContext)) + + assert plugin_with_adapter.adapter is not None + + plugin_with_adapter.adapter.process_activity = AsyncMock(side_effect=fake_process_activity) + + await plugin_with_adapter.on_activity_request(request, response) + + # Ensure adapter.process_activity called with correct auth and activity + plugin_with_adapter.adapter.process_activity.assert_called_once() + called_auth, called_activity, _ = plugin_with_adapter.adapter.process_activity.call_args[0] + assert called_auth == "Bearer token" + assert isinstance(called_activity, Activity) + + # Ensure handler called via TurnContext + plugin_with_adapter.handler.on_turn.assert_awaited() # type: ignore + + @pytest.mark.asyncio + async def test_on_activity_request_raises_http_exception_on_adapter_error( + self, plugin_with_adapter: BotBuilderPlugin + ): + activity_data = {"type": "message", "id": "activity-id"} + request = AsyncMock(spec=Request) + request.json.return_value = activity_data + request.headers = {} + + response = MagicMock(spec=Response) + assert plugin_with_adapter.adapter is not None + + plugin_with_adapter.adapter.process_activity = AsyncMock(side_effect=Exception("fail")) + + with pytest.raises(HTTPException) as exc: + await plugin_with_adapter.on_activity_request(request, response) + assert exc.value.status_code == 500 diff --git a/tests/botbuilder/README.md b/tests/botbuilder/README.md new file mode 100644 index 00000000..e69de29b diff --git a/tests/botbuilder/pyproject.toml b/tests/botbuilder/pyproject.toml new file mode 100644 index 00000000..f21529ac --- /dev/null +++ b/tests/botbuilder/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "botbuilder" +version = "0.1.0" +description = "Botbuilder sample" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "dotenv>=0.9.9", + "botbuilder-core>=4.14.0", + "microsoft-teams-apps", + "microsoft-teams-devtools", + "microsoft-teams-botbuilder" +] + +[tool.uv.sources] +microsoft-teams-apps = { workspace = true } diff --git a/tests/botbuilder/src/main.py b/tests/botbuilder/src/main.py new file mode 100644 index 00000000..28ce0a45 --- /dev/null +++ b/tests/botbuilder/src/main.py @@ -0,0 +1,38 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio + +from botbuilder.core import ActivityHandler, TurnContext # pyright: ignore[reportMissingTypeStubs] +from microsoft.teams.api import MessageActivity +from microsoft.teams.apps import ActivityContext, App +from microsoft.teams.botbuilder import BotBuilderPlugin +from microsoft.teams.devtools import DevToolsPlugin + + +class MyActivityHandler(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + print("Message activity received.") + await turn_context.send_activity("hi from botbuilder...") + + +handler = MyActivityHandler() + +app = App( + plugins=[ + BotBuilderPlugin(handler=handler), + DevToolsPlugin(), + ] +) + + +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + print("Handling message in app...") + await ctx.send("hi from teams...") + + +if __name__ == "__main__": + asyncio.run(app.start()) diff --git a/uv.lock b/uv.lock index ee73597e..b80c4ffc 100644 --- a/uv.lock +++ b/uv.lock @@ -9,6 +9,7 @@ resolution-markers = [ [manifest] members = [ "ai-test", + "botbuilder", "cards", "dialogs", "echo", @@ -35,7 +36,6 @@ members = [ [manifest.dependency-groups] dev = [ { name = "cookiecutter", specifier = ">=2.6.0" }, - { name = "microsoft-teams-botbuilder", editable = "packages/botbuilder" }, { name = "poethepoet", specifier = ">=0.35.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pyright", specifier = ">=1.1.404" }, @@ -311,6 +311,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006, upload-time = "2017-08-03T15:55:31.23Z" }, ] +[[package]] +name = "botbuilder" +version = "0.1.0" +source = { virtual = "tests/botbuilder" } +dependencies = [ + { name = "botbuilder-core" }, + { name = "dotenv" }, + { name = "microsoft-teams-apps" }, + { name = "microsoft-teams-botbuilder" }, + { name = "microsoft-teams-devtools" }, +] + +[package.metadata] +requires-dist = [ + { name = "botbuilder-core", specifier = ">=4.14.0" }, + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "microsoft-teams-apps", editable = "packages/apps" }, + { name = "microsoft-teams-botbuilder", editable = "packages/botbuilder" }, + { name = "microsoft-teams-devtools", editable = "packages/devtools" }, +] + [[package]] name = "botbuilder-core" version = "4.17.0" @@ -325,6 +346,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/51/0e4d0ba1fc25d57977e17d7bfd55c7bd06a9d8403d5efb23e6fc512356fa/botbuilder_core-4.17.0-py3-none-any.whl", hash = "sha256:56828b11d9af663a200fba0fae4e0b9ad6f815a679c3e90e9b6425b9cbe16d53", size = 116148, upload-time = "2025-05-29T15:10:00.474Z" }, ] +[[package]] +name = "botbuilder-integration-aiohttp" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "botbuilder-core" }, + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, + { name = "yarl" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e2/78d9ed5beeeaf9dcdcfd8b7dd0fe4116c8b3ed55b81eb9b01b28037a2b7c/botbuilder_integration_aiohttp-4.17.0-py3-none-any.whl", hash = "sha256:30cd8de3eeec132463bf1834fbd6cfbc6c4a9a361bf478627b6b2f0e7ee211b6", size = 19030, upload-time = "2025-05-29T15:10:02.884Z" }, +] + [[package]] name = "botbuilder-schema" version = "4.17.0" @@ -1706,6 +1742,7 @@ version = "2.0.0a2" source = { editable = "packages/botbuilder" } dependencies = [ { name = "botbuilder-core" }, + { name = "botbuilder-integration-aiohttp" }, { name = "microsoft-teams-api" }, { name = "microsoft-teams-apps" }, { name = "microsoft-teams-common" }, @@ -1714,6 +1751,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "botbuilder-core", specifier = ">=4.14.0" }, + { name = "botbuilder-integration-aiohttp", specifier = ">=4.17.0" }, { name = "microsoft-teams-api", editable = "packages/api" }, { name = "microsoft-teams-apps", editable = "packages/apps" }, { name = "microsoft-teams-common", editable = "packages/common" }, From 2b5082811e404d12fe392aac3960194d8ec0cee1 Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 28 Oct 2025 17:29:21 -0700 Subject: [PATCH 03/10] vb --- packages/botbuilder/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml index 603a1b00..cf62aff4 100644 --- a/packages/botbuilder/pyproject.toml +++ b/packages/botbuilder/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "microsoft-teams-botbuilder" -version = "2.0.0a2" +version = "2.0.0a3" description = "BotBuilder plugin for Microsoft Teams" authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] readme = "README.md" From 9625f5b705a13c5d1ee64a67b19197f85393fec7 Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 28 Oct 2025 17:44:02 -0700 Subject: [PATCH 04/10] add docs --- packages/botbuilder/pyproject.toml | 5 +---- .../src/microsoft/teams/botbuilder/botbuilder_plugin.py | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml index cf62aff4..cb1656de 100644 --- a/packages/botbuilder/pyproject.toml +++ b/packages/botbuilder/pyproject.toml @@ -13,10 +13,7 @@ dependencies = [ "botbuilder-integration-aiohttp>=4.17.0", "microsoft-teams-apps", "microsoft-teams-api", - "microsoft-teams-common", - "coverage>=7.8.0", - "pytest>=8.3.5", - "ruff>=0.11.5", + "microsoft-teams-common" ] [dependency-groups] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py index 36555bbf..c50e91f5 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -104,6 +104,15 @@ async def on_init(self) -> None: self.logger.info("BotBuilder plugin initialized successfully") async def on_activity_request(self, request: Request, response: Response) -> Any: + """ + Handles an incoming activity. + + Overrides the base HTTP plugin behavior to: + 1. Process the activity using the Bot Framework adapter/handler. + 2. Then pass the request to the Teams plugin pipeline (_handle_activity_request). + + Returns the final HTTP response. + """ if not self.adapter: raise RuntimeError("plugin not registered") From 8a3ebc8e998e92f66f760de58a1c068ceb7f5716 Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 4 Nov 2025 13:09:16 -0800 Subject: [PATCH 05/10] minor --- packages/apps/src/microsoft/teams/apps/http_plugin.py | 4 ++-- .../src/microsoft/teams/botbuilder/botbuilder_plugin.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/apps/src/microsoft/teams/apps/http_plugin.py b/packages/apps/src/microsoft/teams/apps/http_plugin.py index d53fa27c..f64348ba 100644 --- a/packages/apps/src/microsoft/teams/apps/http_plugin.py +++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py @@ -9,7 +9,7 @@ from logging import Logger from pathlib import Path from types import SimpleNamespace -from typing import Annotated, Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, cast +from typing import Annotated, Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, Union, cast import uvicorn from fastapi import FastAPI, Request, Response @@ -306,7 +306,7 @@ async def _handle_activity_request(self, request: Request) -> Any: return result - def _handle_activity_response(self, response: Response, result: Any) -> Any: + def _handle_activity_response(self, response: Response, result: Any) -> Union[Response, Dict[str, object]]: """ Handle the activity response formatting. diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py index c50e91f5..04f98a86 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -101,7 +101,7 @@ async def on_init(self) -> None: self.adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(configuration=config)) - self.logger.info("BotBuilder plugin initialized successfully") + self.logger.debug("BotBuilder plugin initialized successfully") async def on_activity_request(self, request: Request, response: Response) -> Any: """ From 4c4594a77464ef36292a4c155055efc5df16009a Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 4 Nov 2025 15:40:08 -0800 Subject: [PATCH 06/10] add http plugin options --- packages/apps/src/microsoft/teams/apps/app.py | 2 +- .../src/microsoft/teams/apps/http_plugin.py | 37 ++++++++++++---- .../teams/botbuilder/botbuilder_plugin.py | 42 ++++++++----------- tests/botbuilder/src/main.py | 3 +- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/packages/apps/src/microsoft/teams/apps/app.py b/packages/apps/src/microsoft/teams/apps/app.py index 1e74152e..0ecd2582 100644 --- a/packages/apps/src/microsoft/teams/apps/app.py +++ b/packages/apps/src/microsoft/teams/apps/app.py @@ -118,7 +118,7 @@ def __init__(self, **options: Unpack[AppOptions]): if self.credentials and hasattr(self.credentials, "client_id"): app_id = self.credentials.client_id - http_plugin = HttpPlugin(app_id, self.log, self.options.skip_auth) + http_plugin = HttpPlugin(app_id=app_id, logger=self.log, skip_auth=self.options.skip_auth) plugins.insert(0, http_plugin) self.http = cast(HttpPlugin, http_plugin) diff --git a/packages/apps/src/microsoft/teams/apps/http_plugin.py b/packages/apps/src/microsoft/teams/apps/http_plugin.py index de67a35c..b6818adb 100644 --- a/packages/apps/src/microsoft/teams/apps/http_plugin.py +++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py @@ -9,7 +9,20 @@ from logging import Logger from pathlib import Path from types import SimpleNamespace -from typing import Annotated, Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, Union, cast +from typing import ( + Annotated, + Any, + AsyncGenerator, + Awaitable, + Callable, + Dict, + Optional, + Required, + TypedDict, + Union, + Unpack, + cast, +) import uvicorn from fastapi import FastAPI, Request, Response @@ -47,6 +60,15 @@ version = importlib.metadata.version("microsoft-teams-apps") +class HttpPluginOptions(TypedDict, total=False): + """Options for configuring the HTTP plugin.""" + + app_id: Required[Optional[str]] # must be present, but can be None + logger: Logger + skip_auth: bool + server_factory: Callable[[FastAPI], uvicorn.Server] + + @Plugin(name="http", version=version, description="the default plugin for sending/receiving activities") class HttpPlugin(Sender): """ @@ -64,13 +86,7 @@ class HttpPlugin(Sender): lifespans: list[Lifespan[Starlette]] = [] - def __init__( - self, - app_id: Optional[str], - logger: Optional[Logger] = None, - skip_auth: bool = False, - server_factory: Optional[Callable[[FastAPI], uvicorn.Server]] = None, - ): + def __init__(self, **options: Unpack[HttpPluginOptions]): """ Args: app_id: Optional Microsoft App ID. @@ -88,7 +104,7 @@ def custom_server_factory(app: FastAPI) -> uvicorn.Server: ``` """ super().__init__() - self.logger = logger or ConsoleLogger().create_logger("@teams/http-plugin") + self.logger = options.get("logger") or ConsoleLogger().create_logger("@teams/http-plugin") self._port: Optional[int] = None self._server: Optional[uvicorn.Server] = None self._on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None @@ -122,6 +138,7 @@ async def combined_lifespan(app: Starlette): self.app = FastAPI(lifespan=combined_lifespan) # Create uvicorn server if user provides custom factory method + server_factory = options.get("server_factory") if server_factory: self._server = server_factory(self.app) if self._server.config.app is not self.app: @@ -130,6 +147,8 @@ async def combined_lifespan(app: Starlette): ) # Add JWT validation middleware + app_id = options.get("app_id") + skip_auth = options.get("skip_auth", False) if app_id and not skip_auth: jwt_middleware = create_jwt_validation_middleware( app_id=app_id, logger=self.logger, paths=["/api/messages"] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py index 04f98a86..326d8016 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -8,20 +8,17 @@ import importlib.metadata from logging import Logger from types import SimpleNamespace -from typing import Annotated, Any, Callable, Optional, TypedDict, Unpack, cast +from typing import Annotated, Any, Optional, Unpack, cast from fastapi import HTTPException, Request, Response -from microsoft.teams.api import Credentials, TokenProtocol +from microsoft.teams.api import Credentials from microsoft.teams.apps import ( - ActivityEvent, DependencyMetadata, - ErrorEvent, - EventMetadata, HttpPlugin, LoggerDependencyOptions, Plugin, ) -from microsoft.teams.common import Client +from microsoft.teams.apps.http_plugin import HttpPluginOptions from botbuilder.core import ( ActivityHandler, @@ -35,11 +32,14 @@ version = importlib.metadata.version("microsoft-teams-botbuilder") +# Constants for app types +SINGLE_TENANT = "singletenant" +MULTI_TENANT = "multitenant" -class BotBuilderPluginOptions(TypedDict, total=False): + +class BotBuilderPluginOptions(HttpPluginOptions, total=False): """Options for configuring the BotBuilder plugin.""" - skip_auth: bool handler: ActivityHandler adapter: CloudAdapter @@ -53,13 +53,6 @@ class BotBuilderPlugin(HttpPlugin): # Dependency injections logger: Annotated[Logger, LoggerDependencyOptions()] credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)] - client: Annotated[Client, DependencyMetadata()] - - bot_token: Annotated[Optional[Callable[[], TokenProtocol]], DependencyMetadata(optional=True)] - graph_token: Annotated[Optional[Callable[[], TokenProtocol]], DependencyMetadata(optional=True)] - - on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")] - on_activity_event: Annotated[Callable[[ActivityEvent], None], EventMetadata(name="activity")] def __init__(self, **options: Unpack[BotBuilderPluginOptions]): """ @@ -68,14 +61,11 @@ def __init__(self, **options: Unpack[BotBuilderPluginOptions]): Args: options: Configuration options for the plugin """ - self.options = options - super().__init__( - app_id=None, - skip_auth=self.options.get("skip_auth", False), - ) - self.handler: Optional[ActivityHandler] = self.options.get("handler") - self.adapter: Optional[CloudAdapter] = self.options.get("adapter") + self.handler: Optional[ActivityHandler] = options.get("handler") + self.adapter: Optional[CloudAdapter] = options.get("adapter") + + super().__init__(**options) async def on_init(self) -> None: """Initialize the plugin when the app starts.""" @@ -93,13 +83,14 @@ async def on_init(self) -> None: tenant_id = getattr(self.credentials, "tenant_id", None) config = SimpleNamespace( - APP_TYPE="singletenant" if tenant_id else "multitenant", + APP_TYPE=SINGLE_TENANT if tenant_id else MULTI_TENANT, APP_ID=client_id, APP_PASSWORD=client_secret, APP_TENANTID=tenant_id, ) - self.adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(configuration=config)) + bot_framework_auth = ConfigurationBotFrameworkAuthentication(configuration=config) + self.adapter = CloudAdapter(bot_framework_auth) self.logger.debug("BotBuilder plugin initialized successfully") @@ -141,7 +132,8 @@ async def logic(turn_context: TurnContext): result = await self._handle_activity_request(request) return self._handle_activity_response(response, result) - except HTTPException: + except HTTPException as http_err: + self.logger.error(f"HTTP error processing activity: {http_err}", exc_info=True) raise except Exception as err: self.logger.error(f"Error processing activity: {err}", exc_info=True) diff --git a/tests/botbuilder/src/main.py b/tests/botbuilder/src/main.py index 28ce0a45..4e140cc5 100644 --- a/tests/botbuilder/src/main.py +++ b/tests/botbuilder/src/main.py @@ -4,6 +4,7 @@ """ import asyncio +import os from botbuilder.core import ActivityHandler, TurnContext # pyright: ignore[reportMissingTypeStubs] from microsoft.teams.api import MessageActivity @@ -22,7 +23,7 @@ async def on_message_activity(self, turn_context: TurnContext): app = App( plugins=[ - BotBuilderPlugin(handler=handler), + BotBuilderPlugin(app_id=os.getenv("CLIENT_ID"), handler=handler), DevToolsPlugin(), ] ) From f615b8b8b0b243ee0778bbe7687c8ff4e7013c36 Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 4 Nov 2025 15:53:16 -0800 Subject: [PATCH 07/10] fix http tests --- packages/apps/tests/test_http_plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/apps/tests/test_http_plugin.py b/packages/apps/tests/test_http_plugin.py index 8eec2101..f1bf6586 100644 --- a/packages/apps/tests/test_http_plugin.py +++ b/packages/apps/tests/test_http_plugin.py @@ -32,16 +32,16 @@ def mock_logger(self): @pytest.fixture def plugin_with_validator(self, mock_logger): """Create HttpPlugin with token validator.""" - return HttpPlugin("test-app-id", mock_logger) + return HttpPlugin(app_id="test-app-id", logger=mock_logger) @pytest.fixture def plugin_without_validator(self, mock_logger): """Create HttpPlugin without token validator.""" - return HttpPlugin(None, mock_logger) + return HttpPlugin(app_id=None, logger=mock_logger) def test_init_with_app_id(self, mock_logger): """Test HttpPlugin initialization with app ID.""" - plugin = HttpPlugin("test-app-id", mock_logger) + plugin = HttpPlugin(app_id="test-app-id", logger=mock_logger) assert plugin.logger == mock_logger assert plugin.app is not None @@ -49,14 +49,14 @@ def test_init_with_app_id(self, mock_logger): def test_init_without_app_id(self, mock_logger): """Test HttpPlugin initialization without app ID.""" - plugin = HttpPlugin(None, mock_logger) + plugin = HttpPlugin(app_id=None, logger=mock_logger) assert plugin.logger == mock_logger assert plugin.app is not None def test_init_with_default_logger(self): """Test HttpPlugin initialization with default logger.""" - plugin = HttpPlugin("test-app-id", None) + plugin = HttpPlugin(app_id="test-app-id") assert plugin.logger is not None @@ -280,7 +280,7 @@ def test_middleware_setup(self, plugin_with_validator, plugin_without_validator) def test_logger_property(self, mock_logger): """Test logger property assignment.""" - plugin = HttpPlugin("test-app-id", mock_logger) + plugin = HttpPlugin(app_id="test-app-id", logger=mock_logger) assert plugin.logger == mock_logger def test_app_property(self, plugin_with_validator): From 3037dd22d4cab38ccf31e2fe001e843365ab3034 Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Tue, 11 Nov 2025 13:25:44 -0800 Subject: [PATCH 08/10] vb --- packages/botbuilder/pyproject.toml | 10 ++-------- uv.lock | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml index cb1656de..039c89be 100644 --- a/packages/botbuilder/pyproject.toml +++ b/packages/botbuilder/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "microsoft-teams-botbuilder" -version = "2.0.0a3" +version = "2.0.0a5" description = "BotBuilder plugin for Microsoft Teams" authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.12,<3.14" repository = "https://github.com/microsoft/teams.py" keywords = ["microsoft", "teams", "ai", "bot", "agents"] license = "MIT" @@ -16,12 +16,6 @@ dependencies = [ "microsoft-teams-common" ] -[dependency-groups] -dev = [ - "pytest>=8.4.0", - "pytest-asyncio>=1.0.0", -] - [project.urls] Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/botbuilder/src/microsoft/teams/botbuilder" diff --git a/uv.lock b/uv.lock index 75b968f5..3fef0db9 100644 --- a/uv.lock +++ b/uv.lock @@ -1554,7 +1554,7 @@ test = [ [[package]] name = "microsoft-teams-botbuilder" -version = "2.0.0a2" +version = "2.0.0a5" source = { editable = "packages/botbuilder" } dependencies = [ { name = "botbuilder-core" }, From 68e8ac7c6f8c9da8316f6722cdac846c4bbe8a7e Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Wed, 12 Nov 2025 11:24:02 -0800 Subject: [PATCH 09/10] add adapter to sample --- .../teams/botbuilder/botbuilder_plugin.py | 2 - pyrightconfig.json | 10 +++++ tests/botbuilder/src/bots/__init__.py | 11 +++++ tests/botbuilder/src/bots/echo_bot.py | 12 +++++ tests/botbuilder/src/config.py | 24 ++++++++++ tests/botbuilder/src/main.py | 44 ++++++++++++++----- 6 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 tests/botbuilder/src/bots/__init__.py create mode 100644 tests/botbuilder/src/bots/echo_bot.py create mode 100644 tests/botbuilder/src/config.py diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py index 326d8016..5045c4ef 100644 --- a/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py @@ -1,5 +1,3 @@ -# pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false - """ Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. diff --git a/pyrightconfig.json b/pyrightconfig.json index 5f09a7d4..a9ac9f08 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -25,6 +25,16 @@ "root": "packages/api/src", "reportIncompatibleVariableOverride": "none", "reportIncompatibleMethodOverride": "none" + }, + { + "root": "packages/botbuilder/src", + "reportMissingTypeStubs": "none", + "reportUnknownMemberType": "none" + }, + { + "root": "tests/botbuilder/src", + "reportMissingTypeStubs": "none", + "reportUnknownMemberType": "none" } ] } \ No newline at end of file diff --git a/tests/botbuilder/src/bots/__init__.py b/tests/botbuilder/src/bots/__init__.py new file mode 100644 index 00000000..e3a12730 --- /dev/null +++ b/tests/botbuilder/src/bots/__init__.py @@ -0,0 +1,11 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .echo_bot import EchoBot + +__all__ = ["EchoBot"] diff --git a/tests/botbuilder/src/bots/echo_bot.py b/tests/botbuilder/src/bots/echo_bot.py new file mode 100644 index 00000000..6fb03dd7 --- /dev/null +++ b/tests/botbuilder/src/bots/echo_bot.py @@ -0,0 +1,12 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from botbuilder.core import ActivityHandler, MessageFactory, TurnContext + + +class EchoBot(ActivityHandler): + async def on_message_activity(self, turn_context: TurnContext): + print("Message activity received.") + await turn_context.send_activity(MessageFactory.text(f"Echo: {turn_context.activity.text}")) diff --git a/tests/botbuilder/src/config.py b/tests/botbuilder/src/config.py new file mode 100644 index 00000000..abf71a0b --- /dev/null +++ b/tests/botbuilder/src/config.py @@ -0,0 +1,24 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import find_dotenv, load_dotenv + +# Constants for app types +SINGLE_TENANT = "singletenant" +MULTI_TENANT = "multitenant" + + +class DefaultConfig: + """Bot Configuration""" + + def __init__(self): + load_dotenv(find_dotenv(usecwd=True)) + self.PORT = os.getenv("PORT", "") + self.APP_ID = os.getenv("CLIENT_ID", "") + self.APP_PASSWORD = os.getenv("CLIENT_SECRET", "") + self.APP_TENANTID = os.getenv("TENANT_ID", "") + self.APP_TYPE = SINGLE_TENANT if self.APP_TENANTID else MULTI_TENANT diff --git a/tests/botbuilder/src/main.py b/tests/botbuilder/src/main.py index 4e140cc5..48e5cdf5 100644 --- a/tests/botbuilder/src/main.py +++ b/tests/botbuilder/src/main.py @@ -4,28 +4,52 @@ """ import asyncio +import datetime import os +import sys +import traceback -from botbuilder.core import ActivityHandler, TurnContext # pyright: ignore[reportMissingTypeStubs] +from botbuilder.core import TurnContext +from botbuilder.integration.aiohttp import ( + CloudAdapter, + ConfigurationBotFrameworkAuthentication, +) +from botbuilder.schema import Activity, ActivityTypes +from bots.echo_bot import EchoBot +from config import DefaultConfig from microsoft.teams.api import MessageActivity from microsoft.teams.apps import ActivityContext, App from microsoft.teams.botbuilder import BotBuilderPlugin from microsoft.teams.devtools import DevToolsPlugin +config = DefaultConfig() +adapter = CloudAdapter(ConfigurationBotFrameworkAuthentication(config)) + + +# Catch-all for errors. +async def on_error(context: TurnContext, error: Exception): + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() -class MyActivityHandler(ActivityHandler): - async def on_message_activity(self, turn_context: TurnContext): - print("Message activity received.") - await turn_context.send_activity("hi from botbuilder...") + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == "emulator": + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.datetime.now(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error", + ) + await context.send_activity(trace_activity) -handler = MyActivityHandler() +adapter.on_turn_error = on_error app = App( - plugins=[ - BotBuilderPlugin(app_id=os.getenv("CLIENT_ID"), handler=handler), - DevToolsPlugin(), - ] + plugins=[BotBuilderPlugin(app_id=os.getenv("CLIENT_ID"), adapter=adapter, handler=EchoBot()), DevToolsPlugin()] ) From 060dc12ca21a93db77314447a09f93b25d630b4e Mon Sep 17 00:00:00 2001 From: Mehak Bindra Date: Wed, 12 Nov 2025 13:50:58 -0800 Subject: [PATCH 10/10] move creds to DI in http plugin --- packages/apps/src/microsoft/teams/apps/app.py | 6 +--- .../src/microsoft/teams/apps/http_plugin.py | 29 +++++++++++-------- tests/botbuilder/src/main.py | 8 ++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/apps/src/microsoft/teams/apps/app.py b/packages/apps/src/microsoft/teams/apps/app.py index 4e901bd6..5e2ee419 100644 --- a/packages/apps/src/microsoft/teams/apps/app.py +++ b/packages/apps/src/microsoft/teams/apps/app.py @@ -114,11 +114,7 @@ def __init__(self, **options: Unpack[AppOptions]): break if not http_plugin: - app_id = None - if self.credentials and hasattr(self.credentials, "client_id"): - app_id = self.credentials.client_id - - http_plugin = HttpPlugin(app_id=app_id, logger=self.log, skip_auth=self.options.skip_auth) + http_plugin = HttpPlugin(logger=self.log, skip_auth=self.options.skip_auth) plugins.insert(0, http_plugin) self.http = cast(HttpPlugin, http_plugin) diff --git a/packages/apps/src/microsoft/teams/apps/http_plugin.py b/packages/apps/src/microsoft/teams/apps/http_plugin.py index b6818adb..d4f18625 100644 --- a/packages/apps/src/microsoft/teams/apps/http_plugin.py +++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py @@ -17,7 +17,6 @@ Callable, Dict, Optional, - Required, TypedDict, Union, Unpack, @@ -36,6 +35,7 @@ SentActivity, TokenProtocol, ) +from microsoft.teams.api.auth.credentials import Credentials from microsoft.teams.apps.http_stream import HttpStream from microsoft.teams.common.http import Client, ClientOptions, Token from microsoft.teams.common.logging import ConsoleLogger @@ -63,7 +63,6 @@ class HttpPluginOptions(TypedDict, total=False): """Options for configuring the HTTP plugin.""" - app_id: Required[Optional[str]] # must be present, but can be None logger: Logger skip_auth: bool server_factory: Callable[[FastAPI], uvicorn.Server] @@ -76,6 +75,7 @@ class HttpPlugin(Sender): """ logger: Annotated[Logger, LoggerDependencyOptions()] + credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)] on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")] on_activity_event: Annotated[Callable[[ActivityEvent], None], EventMetadata(name="activity")] @@ -89,7 +89,6 @@ class HttpPlugin(Sender): def __init__(self, **options: Unpack[HttpPluginOptions]): """ Args: - app_id: Optional Microsoft App ID. logger: Optional logger. skip_auth: Whether to skip JWT validation. server_factory: Optional function that takes an ASGI app @@ -106,6 +105,7 @@ def custom_server_factory(app: FastAPI) -> uvicorn.Server: super().__init__() self.logger = options.get("logger") or ConsoleLogger().create_logger("@teams/http-plugin") self._port: Optional[int] = None + self._skip_auth: bool = options.get("skip_auth", False) self._server: Optional[uvicorn.Server] = None self._on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None self._on_stopped_callback: Optional[Callable[[], Awaitable[None]]] = None @@ -146,15 +146,6 @@ async def combined_lifespan(app: Starlette): "server_factory must return a uvicorn.Server configured with the provided FastAPI app instance." ) - # Add JWT validation middleware - app_id = options.get("app_id") - skip_auth = options.get("skip_auth", False) - if app_id and not skip_auth: - jwt_middleware = create_jwt_validation_middleware( - app_id=app_id, logger=self.logger, paths=["/api/messages"] - ) - self.app.middleware("http")(jwt_middleware) - # Expose FastAPI routing methods (like TypeScript exposes Express methods) self.get = self.app.get self.post = self.app.post @@ -186,6 +177,20 @@ def on_stopped_callback(self, callback: Optional[Callable[[], Awaitable[None]]]) """Set callback to call when HTTP server is stopped.""" self._on_stopped_callback = callback + async def on_init(self) -> None: + """ + Initialize the HTTP plugin when the app starts. + This adds JWT validation middleware unless `skip_auth` is True. + """ + + # Add JWT validation middleware + app_id = getattr(self.credentials, "client_id", None) + if app_id and not self._skip_auth: + jwt_middleware = create_jwt_validation_middleware( + app_id=app_id, logger=self.logger, paths=["/api/messages"] + ) + self.app.middleware("http")(jwt_middleware) + async def on_start(self, event: PluginStartEvent) -> None: """Start the HTTP server.""" self._port = event.port diff --git a/tests/botbuilder/src/main.py b/tests/botbuilder/src/main.py index 48e5cdf5..f39b094d 100644 --- a/tests/botbuilder/src/main.py +++ b/tests/botbuilder/src/main.py @@ -5,7 +5,6 @@ import asyncio import datetime -import os import sys import traceback @@ -48,9 +47,10 @@ async def on_error(context: TurnContext, error: Exception): adapter.on_turn_error = on_error -app = App( - plugins=[BotBuilderPlugin(app_id=os.getenv("CLIENT_ID"), adapter=adapter, handler=EchoBot()), DevToolsPlugin()] -) +# Provide the Bot Framework's adapter and activity handler to `BotBuilderPlugin` +# BotBuilderPlugin will route incoming Teams messages through the adapter +# and invoke the handler to process and respond to each activity. +app = App(plugins=[BotBuilderPlugin(adapter=adapter, handler=EchoBot()), DevToolsPlugin()]) @app.on_message