diff --git a/README.md b/README.md
index 7dd43e5c..73ca41cb 100644
--- a/README.md
+++ b/README.md
@@ -59,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/apps/src/microsoft/teams/apps/app.py b/packages/apps/src/microsoft/teams/apps/app.py
index f8b858f0..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, self.log, 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 7dcf5fa3..d4f18625 100644
--- a/packages/apps/src/microsoft/teams/apps/http_plugin.py
+++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py
@@ -9,7 +9,19 @@
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,
+ TypedDict,
+ Union,
+ Unpack,
+ cast,
+)
import uvicorn
from fastapi import FastAPI, Request, Response
@@ -23,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
@@ -47,6 +60,14 @@
version = importlib.metadata.version("microsoft-teams-apps")
+class HttpPluginOptions(TypedDict, total=False):
+ """Options for configuring the HTTP plugin."""
+
+ 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):
"""
@@ -54,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")]
@@ -64,16 +86,9 @@ 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.
logger: Optional logger.
skip_auth: Whether to skip JWT validation.
server_factory: Optional function that takes an ASGI app
@@ -88,8 +103,9 @@ 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._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
@@ -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:
@@ -129,13 +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
- 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
@@ -167,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
@@ -337,38 +361,51 @@ async def _handle_activity_request(self, request: Request) -> Any:
return result
+ def _handle_activity_response(self, response: Response, result: Any) -> Union[Response, Dict[str, object]]:
+ """
+ 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/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):
diff --git a/packages/botbuilder/README.md b/packages/botbuilder/README.md
new file mode 100644
index 00000000..998bb25a
--- /dev/null
+++ 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
new file mode 100644
index 00000000..039c89be
--- /dev/null
+++ b/packages/botbuilder/pyproject.toml
@@ -0,0 +1,36 @@
+[project]
+name = "microsoft-teams-botbuilder"
+version = "2.0.0a5"
+description = "BotBuilder plugin for Microsoft Teams"
+authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }]
+readme = "README.md"
+requires-python = ">=3.12,<3.14"
+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",
+ "microsoft-teams-api",
+ "microsoft-teams-common"
+]
+
+[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"]
+
+[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
new file mode 100644
index 00000000..73a28a2d
--- /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, BotBuilderPluginOptions
+
+__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
new file mode 100644
index 00000000..5045c4ef
--- /dev/null
+++ b/packages/botbuilder/src/microsoft/teams/botbuilder/botbuilder_plugin.py
@@ -0,0 +1,138 @@
+"""
+Copyright (c) Microsoft Corporation. All rights reserved.
+Licensed under the MIT License.
+"""
+
+import importlib.metadata
+from logging import Logger
+from types import SimpleNamespace
+from typing import Annotated, Any, Optional, Unpack, cast
+
+from fastapi import HTTPException, Request, Response
+from microsoft.teams.api import Credentials
+from microsoft.teams.apps import (
+ DependencyMetadata,
+ HttpPlugin,
+ LoggerDependencyOptions,
+ Plugin,
+)
+from microsoft.teams.apps.http_plugin import HttpPluginOptions
+
+from botbuilder.core import (
+ ActivityHandler,
+ TurnContext,
+)
+from botbuilder.integration.aiohttp import (
+ CloudAdapter,
+ ConfigurationBotFrameworkAuthentication,
+)
+from botbuilder.schema import Activity
+
+version = importlib.metadata.version("microsoft-teams-botbuilder")
+
+# Constants for app types
+SINGLE_TENANT = "singletenant"
+MULTI_TENANT = "multitenant"
+
+
+class BotBuilderPluginOptions(HttpPluginOptions, total=False):
+ """Options for configuring the BotBuilder plugin."""
+
+ handler: ActivityHandler
+ adapter: CloudAdapter
+
+
+@Plugin(name="http", version=version, description="BotBuilder plugin for Microsoft Bot Framework integration")
+class BotBuilderPlugin(HttpPlugin):
+ """
+ BotBuilder plugin that provides Microsoft Bot Framework integration.
+ """
+
+ # Dependency injections
+ logger: Annotated[Logger, LoggerDependencyOptions()]
+ credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)]
+
+ def __init__(self, **options: Unpack[BotBuilderPluginOptions]):
+ """
+ Initialize the BotBuilder plugin.
+
+ Args:
+ options: Configuration options for the plugin
+ """
+
+ 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."""
+ 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 = SimpleNamespace(
+ APP_TYPE=SINGLE_TENANT if tenant_id else MULTI_TENANT,
+ APP_ID=client_id,
+ APP_PASSWORD=client_secret,
+ APP_TENANTID=tenant_id,
+ )
+
+ bot_framework_auth = ConfigurationBotFrameworkAuthentication(configuration=config)
+ self.adapter = CloudAdapter(bot_framework_auth)
+
+ self.logger.debug("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")
+
+ try:
+ # Parse activity data
+ body = await request.json()
+ activity_bf = cast(Activity, Activity().deserialize(body))
+
+ # A POST request must contain an Activity
+ if not activity_bf.type:
+ raise HTTPException(status_code=400, detail="Missing activity type")
+
+ async def logic(turn_context: TurnContext):
+ if not turn_context.activity.id:
+ return
+
+ # Handle activity with botframework handler
+ if self.handler:
+ await self.handler.on_turn(turn_context)
+
+ # 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)
+
+ # Call HTTP plugin to handle activity request
+ result = await self._handle_activity_request(request)
+ return self._handle_activity_response(response, result)
+
+ 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)
+ 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/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 1490796f..a9ac9f08 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -16,7 +16,8 @@
"packages/ai/src",
"packages/openai/src",
"packages/mcpplugin/src",
- "packages/a2aprotocol/src"
+ "packages/a2aprotocol/src",
+ "packages/botbuilder/src"
],
"typeCheckingMode": "strict",
"executionEnvironments": [
@@ -24,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/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/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
new file mode 100644
index 00000000..f39b094d
--- /dev/null
+++ b/tests/botbuilder/src/main.py
@@ -0,0 +1,63 @@
+"""
+Copyright (c) Microsoft Corporation. All rights reserved.
+Licensed under the MIT License.
+"""
+
+import asyncio
+import datetime
+import sys
+import traceback
+
+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()
+
+ # 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)
+
+
+adapter.on_turn_error = on_error
+
+# 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
+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 f1c9be73..3fef0db9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -10,6 +10,7 @@ resolution-markers = [
members = [
"a2a",
"ai-test",
+ "botbuilder",
"cards",
"dialogs",
"echo",
@@ -21,6 +22,7 @@ members = [
"microsoft-teams-ai",
"microsoft-teams-api",
"microsoft-teams-apps",
+ "microsoft-teams-botbuilder",
"microsoft-teams-cards",
"microsoft-teams-common",
"microsoft-teams-devtools",
@@ -277,6 +279,94 @@ 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"
+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-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"
+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.1"
@@ -1038,6 +1128,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/87/fc632776344e7aabbab05a95a0075476f418c5d29ab0f2eec672b7a1f0ac/jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba", size = 204225, upload-time = "2025-09-15T09:20:03.102Z" },
]
+[[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"
@@ -1453,6 +1552,27 @@ test = [
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
]
+[[package]]
+name = "microsoft-teams-botbuilder"
+version = "2.0.0a5"
+source = { editable = "packages/botbuilder" }
+dependencies = [
+ { name = "botbuilder-core" },
+ { name = "botbuilder-integration-aiohttp" },
+ { name = "microsoft-teams-api" },
+ { name = "microsoft-teams-apps" },
+ { name = "microsoft-teams-common" },
+]
+
+[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" },
+]
+
[[package]]
name = "microsoft-teams-cards"
version = "2.0.0a5"
@@ -1662,6 +1782,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/c6/6300f0a7759d3546f8a0a69f417f7f9f8def7f43753ae525869abd31f29c/msgraph_sdk-1.46.0-py3-none-any.whl", hash = "sha256:9367551a61edb08dcab96bcadb3e7441401cddf2d2f9d63acca2beeeffb38487", size = 24775723, upload-time = "2025-10-06T21:44:43.188Z" },
]
+[[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.7.0"
@@ -1749,6 +1885,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 = "2.4.0"
@@ -2331,6 +2476,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
+[[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"