microsoft-teams-m365extensions 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+
7
+ # Environments
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ .venv
12
+ env/
13
+ venv/
14
+ ENV/
15
+ env.bak/
16
+ venv.bak/
17
+
18
+ # mypy
19
+ .mypy_cache/
20
+ .dmypy.json
21
+ dmypy.json
22
+
23
+ # pytest-cov
24
+ .coverage
25
+ .coverage.*
26
+ htmlcov/
27
+ coverage.xml
28
+
29
+ .copilot-instructions.md
30
+
31
+ # other
32
+ .DS_STORE
33
+ *.bak
34
+ *~
35
+ *.tmp
36
+
37
+ ref/
38
+
39
+ .env.claude/
40
+ .claude/
41
+ tmpclaude-*-cwd
42
+
43
+ examples/**/.vscode/
44
+ examples/**/appPackage/
45
+ examples/**/infra/
46
+ examples/**/teamsapp*
47
+ examples/**/aad.manifest.json
48
+
49
+ # Targeted-messages sample ships a manifest to demonstrate the slash-commands
50
+ # opt-in fields (supportsTargetedMessages, commandLists[].triggers).
51
+ !examples/targeted-messages/appPackage/
52
+ !examples/targeted-messages/appPackage/manifest.json
53
+
54
+ # Node (from tab)
55
+ node_modules
56
+ dist/
57
+ build/
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: microsoft-teams-m365extensions
3
+ Version: 2.1.0
4
+ Summary: Extension package that embeds the Teams SDK inside an M365 Agents Application.
5
+ Project-URL: Homepage, https://github.com/microsoft/teams.py/tree/main/packages/m365extensions/src/microsoft_teams/m365extensions
6
+ Author-email: Microsoft <TeamsAISDKFeedback@microsoft.com>
7
+ License-Expression: MIT
8
+ Keywords: agents,ai,bot,m365,microsoft,teams
9
+ Requires-Python: <4.0,>=3.11
10
+ Requires-Dist: microsoft-agents-activity>=1.3.0
11
+ Requires-Dist: microsoft-agents-hosting-core>=1.3.0
12
+ Requires-Dist: microsoft-teams-api>=2.1.0
13
+ Requires-Dist: microsoft-teams-apps>=2.1.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Microsoft Teams M365 Extensions
17
+
18
+ <p>
19
+ <a href="https://pypi.org/project/microsoft-teams-m365extensions" target="_blank">
20
+ <img src="https://img.shields.io/pypi/v/microsoft-teams-m365extensions" />
21
+ </a>
22
+ <a href="https://pypi.org/project/microsoft-teams-m365extensions" target="_blank">
23
+ <img src="https://img.shields.io/pypi/dw/microsoft-teams-m365extensions" />
24
+ </a>
25
+ </p>
26
+
27
+ This extension package bridges the [Microsoft Teams SDK for Python](https://github.com/microsoft/teams.py) into an [Agents SDK](https://github.com/microsoft/Agents) `AgentApplication`. It installs a middleware that routes Teams turns to a teams.py `App` while letting every other channel — and unmatched Teams turns — fall through to existing Agents SDK handlers.
28
+
29
+ <a href="https://microsoft.github.io/teams-sdk" target="_blank">
30
+ <img src="https://img.shields.io/badge/📖 Getting Started-blue?style=for-the-badge" />
31
+ </a>
32
+
@@ -0,0 +1,17 @@
1
+ # Microsoft Teams M365 Extensions
2
+
3
+ <p>
4
+ <a href="https://pypi.org/project/microsoft-teams-m365extensions" target="_blank">
5
+ <img src="https://img.shields.io/pypi/v/microsoft-teams-m365extensions" />
6
+ </a>
7
+ <a href="https://pypi.org/project/microsoft-teams-m365extensions" target="_blank">
8
+ <img src="https://img.shields.io/pypi/dw/microsoft-teams-m365extensions" />
9
+ </a>
10
+ </p>
11
+
12
+ This extension package bridges the [Microsoft Teams SDK for Python](https://github.com/microsoft/teams.py) into an [Agents SDK](https://github.com/microsoft/Agents) `AgentApplication`. It installs a middleware that routes Teams turns to a teams.py `App` while letting every other channel — and unmatched Teams turns — fall through to existing Agents SDK handlers.
13
+
14
+ <a href="https://microsoft.github.io/teams-sdk" target="_blank">
15
+ <img src="https://img.shields.io/badge/📖 Getting Started-blue?style=for-the-badge" />
16
+ </a>
17
+
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "microsoft-teams-m365extensions"
3
+ dynamic = ["version"]
4
+ description = "Extension package that embeds the Teams SDK inside an M365 Agents Application."
5
+ readme = "README.md"
6
+ repository = "https://github.com/microsoft/teams.py"
7
+ keywords = ["microsoft", "teams", "ai", "bot", "agents", "m365"]
8
+ license = "MIT"
9
+ authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }]
10
+ requires-python = ">=3.11,<4.0"
11
+ dependencies = [
12
+ "microsoft-agents-activity>=1.3.0",
13
+ "microsoft-agents-hosting-core>=1.3.0",
14
+ "microsoft-teams-api",
15
+ "microsoft-teams-apps",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/m365extensions/src/microsoft_teams/m365extensions"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.4.0",
24
+ "pytest-asyncio>=1.0.0",
25
+ ]
26
+
27
+ [build-system]
28
+ requires = ["hatchling", "hatch-teams-build"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.version]
32
+ source = "teams-build"
33
+
34
+ [tool.hatch.metadata.hooks.teams-build]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/microsoft_teams", "src/microsoft"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src"]
41
+
42
+ [tool.uv.sources]
43
+ microsoft-teams-api = { workspace = true }
44
+ microsoft-teams-apps = { workspace = true }
@@ -0,0 +1,17 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .credentials import make_agent_sdk_token_provider
9
+ from .install import use_teams_sdk
10
+ from .middleware import TeamsMiddleware, is_teams_channel
11
+
12
+ __all__ = [
13
+ "TeamsMiddleware",
14
+ "is_teams_channel",
15
+ "make_agent_sdk_token_provider",
16
+ "use_teams_sdk",
17
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from contextvars import ContextVar
9
+
10
+ from microsoft_agents.hosting.core.turn_context import TurnContext
11
+
12
+ # Internal only: the middleware sets this for the duration of a teams.py-handled
13
+ # turn so ``credentials._select_provider`` can pick the connection that matches
14
+ # the inbound identity. It is deliberately not exposed as a public accessor.
15
+ agent_sdk_context: ContextVar[TurnContext] = ContextVar("teams_sdk.agent_sdk_context")
@@ -0,0 +1,88 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import inspect
9
+ import logging
10
+ from typing import Any, Awaitable, Callable, Optional, Union
11
+
12
+ from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter
13
+
14
+ from .context import agent_sdk_context
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Routing key used to select the connection that mints agentic tokens,
19
+ # mirroring the Agents SDK's ``get_token_provider(identity, "agentic")``
20
+ # convention (and the ``CONNECTIONSMAP ... SERVICEURL=agentic`` entry). When no
21
+ # dedicated agentic connection is registered the lookup falls back to the
22
+ # default connection, which is sufficient for the single-connection case.
23
+ _AGENTIC_ROUTING_KEY = "agentic"
24
+
25
+
26
+ def make_agent_sdk_token_provider(
27
+ connection_manager: Any,
28
+ ) -> Callable[..., Awaitable[str]]:
29
+ async def _token(
30
+ scope: Union[str, list[str]],
31
+ tenant_id: Optional[str] = None,
32
+ *,
33
+ agentic_identity: Optional[Any] = None,
34
+ ) -> str:
35
+ scopes = [scope] if isinstance(scope, str) else list(scope)
36
+
37
+ agentic_user_id = getattr(agentic_identity, "agentic_user_id", None)
38
+ if agentic_identity is not None and agentic_user_id:
39
+ provider = _select_provider(connection_manager, _AGENTIC_ROUTING_KEY)
40
+ token = await provider.get_agentic_user_token(
41
+ getattr(agentic_identity, "tenant_id", None) or tenant_id,
42
+ agentic_identity.agentic_app_id,
43
+ agentic_user_id,
44
+ scopes,
45
+ )
46
+ if not token:
47
+ raise RuntimeError(
48
+ "Agents SDK connection returned no agentic user token; the "
49
+ "connection must be an MSAL provider with agentic support."
50
+ )
51
+ return token
52
+
53
+ # Non-agentic path: outbound Bot Framework Service token. Strip
54
+ # ".default" off the first scope to derive the resource_url MSAL wants
55
+ # — '.default' is appended back internally.
56
+ first = scopes[0] if scopes else ""
57
+ resource_url = first[: -len("/.default")] if first.endswith("/.default") else first
58
+
59
+ provider = _select_provider(connection_manager, resource_url)
60
+ result = provider.get_access_token(resource_url, scopes)
61
+ if inspect.isawaitable(result):
62
+ result = await result
63
+ return result
64
+
65
+ return _token
66
+
67
+
68
+ def _select_provider(connection_manager: Any, service_url: str) -> Any:
69
+ context = agent_sdk_context.get(None)
70
+ if context is None:
71
+ return connection_manager.get_default_connection()
72
+
73
+ claims_identity = context.turn_state.get(ChannelAdapter.AGENT_IDENTITY_KEY)
74
+ if claims_identity is None:
75
+ return connection_manager.get_default_connection()
76
+
77
+ target_url = service_url or (context.activity.service_url or "")
78
+ if not target_url:
79
+ return connection_manager.get_default_connection()
80
+
81
+ try:
82
+ return connection_manager.get_token_provider(claims_identity, target_url)
83
+ except Exception as exc: # noqa: BLE001 — degrade gracefully on lookup failure
84
+ logger.debug(
85
+ "connection lookup failed (%s); using default connection",
86
+ exc,
87
+ )
88
+ return connection_manager.get_default_connection()
@@ -0,0 +1,38 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Callable, Optional
9
+
10
+ from microsoft_agents.hosting.core.app.agent_application import AgentApplication
11
+ from microsoft_agents.hosting.core.authorization.connections import Connections
12
+ from microsoft_agents.hosting.core.turn_context import TurnContext
13
+ from microsoft_teams.apps import App
14
+
15
+ from .credentials import make_agent_sdk_token_provider
16
+ from .middleware import TeamsMiddleware
17
+
18
+
19
+ def use_teams_sdk(
20
+ app: AgentApplication[Any],
21
+ connection_manager: Connections,
22
+ should_bypass_teams: Optional[Callable[[TurnContext], bool]] = None,
23
+ **teams_app_kwargs: Any,
24
+ ) -> App:
25
+ reserved = {"client_id", "tenant_id", "token"} & teams_app_kwargs.keys()
26
+ if reserved:
27
+ raise TypeError(f"use_teams_sdk owns {sorted(reserved)}; remove from teams_app_kwargs.")
28
+
29
+ auth = connection_manager.get_default_connection_configuration()
30
+ teams_app = App(
31
+ client_id=auth.CLIENT_ID,
32
+ tenant_id=auth.TENANT_ID,
33
+ token=make_agent_sdk_token_provider(connection_manager),
34
+ **teams_app_kwargs,
35
+ )
36
+
37
+ app.adapter.use(TeamsMiddleware(teams_app, should_bypass_teams))
38
+ return teams_app
@@ -0,0 +1,118 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Awaitable, Callable, Optional, cast
9
+
10
+ from microsoft_agents.activity import Activity, ActivityTypes, InvokeResponse
11
+ from microsoft_agents.hosting.core.middleware_set import Middleware
12
+ from microsoft_agents.hosting.core.turn_context import TurnContext
13
+ from microsoft_teams.api import ActivityTypeAdapter
14
+ from microsoft_teams.apps import App
15
+ from microsoft_teams.apps.events.types import ActivityEvent, CoreActivity
16
+
17
+ from .context import agent_sdk_context
18
+ from .token import TeamsToken
19
+
20
+ TEAMS_CHANNEL_ID = "msteams"
21
+
22
+
23
+ def is_teams_channel(activity: Activity) -> bool:
24
+ """True for Teams turns, including sub-channels like ``msteams:COPILOT``."""
25
+ channel_id: Optional[str] = activity.channel_id
26
+ if not channel_id:
27
+ return False
28
+
29
+ channel = getattr(channel_id, "channel", None)
30
+ if channel is None:
31
+ channel = str(channel_id).split(":", 1)[0]
32
+ return channel == TEAMS_CHANNEL_ID
33
+
34
+
35
+ class TeamsMiddleware(Middleware):
36
+ def __init__(
37
+ self,
38
+ teams_app: App,
39
+ should_bypass_teams: Optional[Callable[[TurnContext], bool]] = None,
40
+ ) -> None:
41
+ self._teams_app = teams_app
42
+ self._should_bypass_teams = should_bypass_teams
43
+
44
+ async def on_turn(
45
+ self,
46
+ context: TurnContext,
47
+ logic: Callable[[TurnContext], Awaitable[None]],
48
+ ) -> None:
49
+ activity: Activity = context.activity
50
+ if not is_teams_channel(activity):
51
+ await logic(context)
52
+ return
53
+
54
+ # Idempotent — App tracks _initialized internally. Run on every Teams
55
+ # turn so AgentApplication handlers can safely call into TEAMS_APP
56
+ # (e.g. ``TEAMS_APP.send`` for proactive sends) even when no teams.py
57
+ # route matched this turn.
58
+ await self._teams_app.initialize()
59
+
60
+ if self._should_bypass_teams is not None and self._should_bypass_teams(context):
61
+ await logic(context)
62
+ return
63
+
64
+ core_activity = self._translate_inbound(activity)
65
+
66
+ if not self._teams_app.router.select_handlers(core_activity):
67
+ # No teams.py route matches; let AgentApplication try its handlers.
68
+ await logic(context)
69
+ return
70
+
71
+ event = ActivityEvent(
72
+ body=cast(CoreActivity, core_activity),
73
+ token=TeamsToken.from_activity(activity),
74
+ )
75
+
76
+ ctx_token = agent_sdk_context.set(context)
77
+ try:
78
+ invoke_response = await self._teams_app.activity_processor.process_activity(
79
+ plugins=self._teams_app.plugins, event=event
80
+ )
81
+ finally:
82
+ agent_sdk_context.reset(ctx_token)
83
+
84
+ if activity.type == ActivityTypes.invoke:
85
+ await self._propagate_invoke_response(context, invoke_response)
86
+
87
+ @staticmethod
88
+ def _translate_inbound(activity: Activity):
89
+ payload: str = activity.model_dump_json(by_alias=True, exclude_none=True)
90
+ return ActivityTypeAdapter.validate_json(payload)
91
+
92
+ @staticmethod
93
+ async def _propagate_invoke_response(context: TurnContext, invoke_response: object) -> None:
94
+ if invoke_response is None:
95
+ return
96
+
97
+ status: Any
98
+ body: Any
99
+ if isinstance(invoke_response, dict):
100
+ response = cast("dict[str, Any]", invoke_response)
101
+ status = response.get("status")
102
+ body = response.get("body")
103
+ else:
104
+ status = getattr(invoke_response, "status", None)
105
+ body = getattr(invoke_response, "body", None)
106
+
107
+ if status is None:
108
+ return
109
+
110
+ if hasattr(body, "model_dump"):
111
+ body = body.model_dump(mode="json", by_alias=True, exclude_none=True)
112
+
113
+ await context.send_activity(
114
+ Activity(
115
+ type=ActivityTypes.invoke_response,
116
+ value=InvokeResponse(status=status, body=body),
117
+ )
118
+ )
@@ -0,0 +1,83 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from typing import Optional
10
+
11
+ from microsoft_agents.activity import Activity
12
+ from microsoft_teams.api.auth.caller import CallerIds, CallerType
13
+ from microsoft_teams.api.auth.token import TokenProtocol
14
+
15
+
16
+ class TeamsToken(TokenProtocol):
17
+ """A minimal TokenProtocol synthesized from an Agents SDK Activity."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ app_id: str,
23
+ tenant_id: Optional[str],
24
+ service_url: str,
25
+ app_display_name: Optional[str] = None,
26
+ ) -> None:
27
+ self._app_id = app_id
28
+ self._tenant_id = tenant_id
29
+ self._service_url = service_url.rstrip("/") if service_url else ""
30
+ self._app_display_name = app_display_name
31
+ # Synthetic expiration ~1h out so is_expired() reports False.
32
+ self._expiration_ms = int(time.time() * 1000) + 60 * 60 * 1000
33
+
34
+ @classmethod
35
+ def from_activity(cls, activity: Activity) -> "TeamsToken":
36
+ recipient = getattr(activity, "recipient", None)
37
+ conversation = getattr(activity, "conversation", None)
38
+ return cls(
39
+ app_id=getattr(recipient, "id", "") or "",
40
+ app_display_name=getattr(recipient, "name", None),
41
+ tenant_id=getattr(conversation, "tenant_id", None),
42
+ service_url=getattr(activity, "service_url", "") or "",
43
+ )
44
+
45
+ # ── TokenProtocol surface ──────────────────────────────────────────
46
+
47
+ @property
48
+ def app_id(self) -> str:
49
+ return self._app_id
50
+
51
+ @property
52
+ def app_display_name(self) -> Optional[str]:
53
+ return self._app_display_name
54
+
55
+ @property
56
+ def tenant_id(self) -> Optional[str]:
57
+ return self._tenant_id
58
+
59
+ @property
60
+ def service_url(self) -> str:
61
+ return self._service_url or "https://smba.trafficmanager.net/teams"
62
+
63
+ @property
64
+ def from_(self) -> CallerType:
65
+ return "bot" if self._app_id else "azure"
66
+
67
+ @property
68
+ def from_id(self) -> str:
69
+ if self.from_ == "bot":
70
+ return f"{CallerIds.BOT}:{self._app_id}"
71
+ return CallerIds.AZURE
72
+
73
+ @property
74
+ def expiration(self) -> Optional[int]:
75
+ return self._expiration_ms
76
+
77
+ def is_expired(self, buffer_ms: int = 5 * 60 * 1000) -> bool:
78
+ return self._expiration_ms < (time.time() * 1000) + buffer_ms
79
+
80
+ def __str__(self) -> str:
81
+ # No real JWT is forwarded; a tagged sentinel is more honest than '' and
82
+ # cannot be mistaken for a signable bearer.
83
+ return f"teams-sdk-synthetic://app/{self._app_id}"