Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions packages/apps/src/microsoft/teams/apps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,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.enable_token_validation)
http_plugin = HttpPlugin(self.options.enable_token_validation)

plugins.insert(0, http_plugin)
self.http = cast(HttpPlugin, http_plugin)
Expand Down
69 changes: 34 additions & 35 deletions packages/apps/src/microsoft/teams/apps/http_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
SentActivity,
TokenProtocol,
)
from microsoft.teams.api.auth.credentials import Credentials
from microsoft.teams.apps.http_stream import HttpStream
from microsoft.teams.common.http.client import Client, ClientOptions
from microsoft.teams.common.logging import ConsoleLogger
from pydantic import BaseModel

from .auth import create_jwt_validation_middleware
Expand All @@ -39,7 +39,7 @@
Sender,
StreamerProtocol,
)
from .plugins.metadata import Plugin
from .plugins.metadata import CredentialsDependencyOptions, Plugin

version = importlib.metadata.version("microsoft-teams-apps")

Expand All @@ -54,6 +54,7 @@ class HttpPlugin(Sender):

on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")]
on_activity_event: Annotated[Callable[[ActivityEvent], None], EventMetadata(name="activity")]
credentials: Annotated[Credentials, CredentialsDependencyOptions(optional=True)]

client: Annotated[Client, DependencyMetadata()]

Expand All @@ -62,12 +63,10 @@ class HttpPlugin(Sender):

def __init__(
self,
app_id: Optional[str],
logger: Optional[Logger] = None,
enable_token_validation: bool = True,
):
super().__init__()
self.logger = logger or ConsoleLogger().create_logger("@teams/http-plugin")
self.enable_token_validation = enable_token_validation
self._server: Optional[uvicorn.Server] = None
self._port: Optional[int] = None
self._on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None
Expand All @@ -76,6 +75,7 @@ def __init__(
# Storage for pending HTTP responses by activity ID
self.pending: Dict[str, asyncio.Future[Any]] = {}

async def on_init(self) -> None:
# Setup FastAPI app with lifespan
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
Expand All @@ -92,12 +92,11 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
self.app = FastAPI(lifespan=lifespan)

# Add JWT validation middleware
if app_id and enable_token_validation:
if self.credentials.client_id and self.enable_token_validation:
jwt_middleware = create_jwt_validation_middleware(
app_id=app_id, logger=self.logger, paths=["/api/messages"]
app_id=self.credentials.client_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
Expand Down Expand Up @@ -276,36 +275,36 @@ async def _handle_activity_request(self, request: Request) -> Any:

return result

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)
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:
return body
return cast(Any, 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:
return body
return cast(Any, result)

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."""
Expand Down
11 changes: 8 additions & 3 deletions packages/apps/src/microsoft/teams/apps/plugins/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

from dataclasses import dataclass
from typing import Any, Literal, Optional, Type, Union
from typing import Callable, Literal, Optional, Type, TypeVar, Union

from ..plugins.plugin_base import PluginBase

Expand All @@ -20,10 +20,15 @@ class PluginOptions:
description: Optional[str] = None


def Plugin(name: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None) -> Any:
T = TypeVar("T")


def Plugin(
name: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None
) -> Callable[[Type[T]], Type[T]]:
"""Turns any class into a plugin using the decorator pattern."""

def decorator(cls: Type[Any]) -> Any:
def decorator(cls: Type[T]) -> Type[T]:
plugin_name = name or cls.__name__
plugin_version = version or "0.0.0"
plugin_description = description or ""
Expand Down
Empty file added packages/botbuilder/README.md
Empty file.
23 changes: 23 additions & 0 deletions packages/botbuilder/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[project]
name = "microsoft-teams-botbuilder"
version = "0.0.1-alpha.4"
description = "A plugin to use TeamsAI libraries with existing botbuilder applications"
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-integration-aiohttp>=4.17.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/microsoft"]

[tool.hatch.build.targets.sdist]
include = ["src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""


def hello() -> str:
return "Hello from botbuilder!"
55 changes: 55 additions & 0 deletions packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import importlib.metadata
from typing import Annotated

from fastapi import Request
from microsoft.teams.api.auth.credentials import ClientCredentials, Credentials
from microsoft.teams.apps import Plugin, Sender
from microsoft.teams.apps.http_plugin import HttpPlugin
from microsoft.teams.apps.plugins.metadata import DependencyMetadata
from microsoft.teams.common.http.client import Client

from botbuilder.core import ActivityHandler, Bot
from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationServiceClientCredentialFactory

version = importlib.metadata.version("microsoft-teams-botbuilder")


@Plugin(name="http", version=version, description="A plugin to use teams ai library with bot builder")
class BotbuilderPlugin(HttpPlugin, Sender):
client: Annotated[Client, DependencyMetadata()]
credentials: Annotated[Credentials | None, DependencyMetadata()]

adapter: CloudAdapter

def __init__(self, bot: Bot, adapter: CloudAdapter | None = None, handler: ActivityHandler | None = None):
self.adapter = adapter
self.bot = bot
self.botbuilder_handler = handler

async def on_init(self) -> None:
await super().on_init()
if self.adapter is None:
client_id = self.credentials.client_id if self.credentials else None
secret = (
self.credentials.client_secret
if self.credentials and isinstance(self.credentials, ClientCredentials)
else None
)
tenant_id = (
self.credentials.tenant_id
if self.credentials and isinstance(self.credentials, ClientCredentials)
else None
)
self.adapter = CloudAdapter(
ConfigurationServiceClientCredentialFactory(
{
"APP_TYPE": "SingleTenant" if tenant_id is not None else "MultiTenant",
"APP_ID": client_id,
"APP_PASSWORD": secret,
}
)
)

async def on_activity_request(self, request: Request, response: Response) -> Any:

Check failure on line 53 in packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py

View workflow job for this annotation

GitHub Actions / Build, Lint & Test (3.13)

Ruff (F821)

packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py:53:82: F821 Undefined name `Any`

Check failure on line 53 in packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py

View workflow job for this annotation

GitHub Actions / Build, Lint & Test (3.13)

Ruff (F821)

packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py:53:69: F821 Undefined name `Response`

Check failure on line 53 in packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py

View workflow job for this annotation

GitHub Actions / Build, Lint & Test (3.12)

Ruff (F821)

packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py:53:82: F821 Undefined name `Any`

Check failure on line 53 in packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py

View workflow job for this annotation

GitHub Actions / Build, Lint & Test (3.12)

Ruff (F821)

packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py:53:69: F821 Undefined name `Response`
await self.adapter.process(request, self.bot)
await super().on_activity_request(request, response)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"microsoft-teams-graph" = { workspace = true }
"microsoft-teams-ai" = { workspace = true }
"microsoft-teams-openai" = { workspace = true }
"microsoft-teams-botbuilder" = { workspace = true }

[tool.uv.workspace]
members = ["packages/*", "tests/*"]
Expand Down
3 changes: 2 additions & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"packages/graph/src",
"packages/devtools/src",
"packages/ai/src",
"packages/openai/src"
"packages/openai/src",
"packages/botbuilder/src"
],
"typeCheckingMode": "strict",
"executionEnvironments": [
Expand Down
Empty file added tests/botbuilder-test/README.md
Empty file.
13 changes: 13 additions & 0 deletions tests/botbuilder-test/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "botbuilder-test"
version = "0.1.0"
description = "test to make sure botbuilder plugin seemlessly integrates with botbuilder and teamsai"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
]

[tool.uv.sources]
microsoft-teams-apps = { workspace = true }
27 changes: 27 additions & 0 deletions tests/botbuilder-test/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

import asyncio

from microsoft.teams.api import MessageActivity
from microsoft.teams.apps import ActivityContext, App

app = App()


@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
"""Handle message activities using the new generated handler system."""
print(f"[GENERATED onMessage] Message received: {ctx.activity.text}")
print(f"[GENERATED onMessage] From: {ctx.activity.from_}")

if "reply" in ctx.activity.text.lower():
await ctx.reply("Hello! How can I assist you today?")
else:
await ctx.send(f"You said '{ctx.activity.text}'")


if __name__ == "__main__":
asyncio.run(app.start())
Loading
Loading