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.
- microsoft_teams_m365extensions-2.1.0/.gitignore +57 -0
- microsoft_teams_m365extensions-2.1.0/PKG-INFO +32 -0
- microsoft_teams_m365extensions-2.1.0/README.md +17 -0
- microsoft_teams_m365extensions-2.1.0/pyproject.toml +44 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/__init__.py +17 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/context.py +15 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/credentials.py +88 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/install.py +38 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/middleware.py +118 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/py.typed +1 -0
- microsoft_teams_m365extensions-2.1.0/src/microsoft_teams/m365extensions/token.py +83 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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}"
|