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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

A comprehensive SDK for building Microsoft Teams applications, bots, and AI agents using Python. This SDK provides a high-level framework with built-in Microsoft Graph integration, OAuth handling, and extensible plugin architecture.

## Agent 365 Support

Agent 365 support is being developed on this integration branch.
Comment on lines +8 to +10

<a href="https://microsoft.github.io/teams-sdk" target="_blank">
<img src="https://img.shields.io/badge/📖 Getting Started-blue?style=for-the-badge" />
</a>
Expand Down
30 changes: 30 additions & 0 deletions examples/agent365/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# agent365

Demonstrates passing `AgenticIdentity` directly to Teams API surfaces.

## Reactive Echo

`src/main.py` mimics the echo example. Incoming messages are handled normally; the inbound service URL and agentic identity are carried by the context/API layer.

```bash
export CLIENT_ID=<agent-identity-blueprint-app-id>
export CLIENT_SECRET=<agent-identity-blueprint-secret>
export TENANT_ID=<tenant-id>

uv run --project examples/agent365 python src/main.py
```

## Proactive API Send

`src/proactive.py` shows both `app.send(..., agentic_identity=...)` and the lower-level conversation activity API. In both cases the API layer asks the auth provider for the right Agent ID token and uses it in the request header.

```bash
export CLIENT_ID=<agent-identity-blueprint-app-id>
export CLIENT_SECRET=<agent-identity-blueprint-secret>
export TENANT_ID=<tenant-id>

uv run --project examples/agent365 python src/proactive.py \
<conversation-id> \
<agentic-app-id> \
<agentic-user-id>
```
13 changes: 13 additions & 0 deletions examples/agent365/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "agent365"
version = "0.1.0"
description = "Agent 365 token example"
readme = "README.md"
requires-python = ">=3.11,<4.0"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
]

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

import asyncio
import logging
import re

from microsoft_teams.api import MessageActivity
from microsoft_teams.api.activities.typing import TypingActivityInput
from microsoft_teams.apps import ActivityContext, App

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = App()


@app.on_message_pattern(re.compile(r"hello|hi|greetings"))
async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None:
"""Handle greeting messages using the inbound AgenticIdentity when present."""
await ctx.reply("Hello! How can I assist you today?")


@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
"""Echo incoming messages using the inbound AgenticIdentity when present."""
logger.info("[Agent365 reactive] Message received: %s", ctx.activity.text)
logger.info("[Agent365 reactive] From: %s", ctx.activity.from_)
logger.info("[Agent365 reactive] Agentic identity: %s", ctx.activity.recipient.agentic_identity)

await ctx.reply(TypingActivityInput())

if "react" in ctx.activity.text.lower():
await ctx.api.reactions.add(
conversation_id=ctx.activity.conversation.id,
activity_id=ctx.activity.id,
reaction_type="like",
)
await ctx.reply("Added a like reaction to your message.")
return

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())
43 changes: 43 additions & 0 deletions examples/agent365/src/proactive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

import argparse
import asyncio
import logging

from microsoft_teams.api import MessageActivityInput
from microsoft_teams.apps import App

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


async def main():
parser = argparse.ArgumentParser(description="Send proactive messages using AgenticIdentity")
parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to")
parser.add_argument("agentic_app_id", help="The concrete agent identity app/client ID")
parser.add_argument("agentic_user_id", help="The agent user object ID")
args = parser.parse_args()

app = App()
await app.initialize()

agentic_identity = app.get_agentic_identity(args.agentic_app_id, args.agentic_user_id)
sent = await app.send(
args.conversation_id,
"Hello from app.send with an AgenticIdentity.",
agentic_identity=agentic_identity,
)
logger.info("Sent activity through app.send. Activity ID: %s", sent.id)

api_sent = await app.api.conversations.activities(args.conversation_id).create(
MessageActivityInput(text="Hello from the conversation activity API with an AgenticIdentity."),
agentic_identity=agentic_identity,
)
logger.info("Sent activity through app.api. Activity ID: %s", api_sent.id)


if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions packages/api/src/microsoft_teams/api/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
)
from .cloud_environment import from_name as config_from_cloud_name
from .credentials import (
BasicTokenProvider,
ClientCredentials,
Credentials,
FederatedIdentityCredentials,
ManagedIdentityCredentials,
TokenCredentials,
TokenProvider,
)
from .json_web_token import JsonWebToken, JsonWebTokenPayload
from .token import TokenProtocol
Expand All @@ -23,12 +25,14 @@
"CallerIds",
"CallerType",
"CloudEnvironment",
"BasicTokenProvider",
"ClientCredentials",
"config_from_cloud_name",
"Credentials",
"FederatedIdentityCredentials",
"ManagedIdentityCredentials",
"TokenCredentials",
"TokenProvider",
"TokenProtocol",
"JsonWebToken",
"JsonWebTokenPayload",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class CloudEnvironment:
"""The default multi-tenant login tenant (e.g. "botframework.com")."""
bot_scope: str
"""The Bot Framework OAuth scope (e.g. "https://api.botframework.com/.default")."""
agentic_bot_scope: str
"""The Teams Bot API scope for Agent ID user-token calls."""
token_service_url: str
"""The Bot Framework token service base URL (e.g. "https://token.botframework.com")."""
openid_metadata_url: str
Expand All @@ -35,6 +37,7 @@ class CloudEnvironment:
login_endpoint="https://login.microsoftonline.com",
login_tenant="botframework.com",
bot_scope="https://api.botframework.com/.default",
agentic_bot_scope="https://botapi.skype.com/.default",
token_service_url="https://token.botframework.com",
openid_metadata_url="https://login.botframework.com/v1/.well-known/openidconfiguration",
token_issuer="https://api.botframework.com",
Expand All @@ -46,6 +49,8 @@ class CloudEnvironment:
login_endpoint="https://login.microsoftonline.us",
login_tenant="MicrosoftServices.onmicrosoft.us",
bot_scope="https://api.botframework.us/.default",
# TODO: confirm Agent ID Bot API scope for this cloud before enabling sovereign Agent ID scenarios.
agentic_bot_scope="https://botapi.skype.com/.default",
token_service_url="https://tokengcch.botframework.azure.us",
openid_metadata_url="https://login.botframework.azure.us/v1/.well-known/openidconfiguration",
token_issuer="https://api.botframework.us",
Expand All @@ -57,6 +62,8 @@ class CloudEnvironment:
login_endpoint="https://login.microsoftonline.us",
login_tenant="MicrosoftServices.onmicrosoft.us",
bot_scope="https://api.botframework.us/.default",
# TODO: confirm Agent ID Bot API scope for this cloud before enabling sovereign Agent ID scenarios.
agentic_bot_scope="https://botapi.skype.com/.default",
token_service_url="https://apiDoD.botframework.azure.us",
openid_metadata_url="https://login.botframework.azure.us/v1/.well-known/openidconfiguration",
token_issuer="https://api.botframework.us",
Expand All @@ -68,6 +75,8 @@ class CloudEnvironment:
login_endpoint="https://login.partner.microsoftonline.cn",
login_tenant="microsoftservices.partner.onmschina.cn",
bot_scope="https://api.botframework.azure.cn/.default",
# TODO: confirm Agent ID Bot API scope for this cloud before enabling China cloud Agent ID scenarios.
agentic_bot_scope="https://botapi.skype.com/.default",
token_service_url="https://token.botframework.azure.cn",
openid_metadata_url="https://login.botframework.azure.cn/v1/.well-known/openidconfiguration",
token_issuer="https://api.botframework.azure.cn",
Expand Down
33 changes: 29 additions & 4 deletions packages/api/src/microsoft_teams/api/auth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,34 @@
Licensed under the MIT License.
"""

from typing import Awaitable, Callable, Literal, Optional, Union
from typing import Awaitable, Callable, Literal, Optional, Protocol, TypeAlias, Union, runtime_checkable

from ..models import CustomBaseModel
from ..models import AgenticIdentity, CustomBaseModel

TokenScope: TypeAlias = Union[str, list[str]]
TokenResult: TypeAlias = Union[str, Awaitable[str]]
BasicTokenProvider: TypeAlias = Callable[[TokenScope, Optional[str]], TokenResult]
_PositionalAgenticTokenProvider: TypeAlias = Callable[
[TokenScope, Optional[str], Optional[AgenticIdentity]], TokenResult
]


@runtime_checkable
class _KeywordAgenticTokenProvider(Protocol):
def __call__(
self,
scope: TokenScope,
tenant_id: Optional[str],
*,
agentic_identity: Optional[AgenticIdentity] = None,
) -> TokenResult: ...


TokenProvider: TypeAlias = Union[
BasicTokenProvider,
_PositionalAgenticTokenProvider,
_KeywordAgenticTokenProvider,
]


class ClientCredentials(CustomBaseModel):
Expand Down Expand Up @@ -36,8 +61,8 @@ class TokenCredentials(CustomBaseModel):
"""
The tenant ID.
"""
# (scope: string | string[], tenantId?: string) => string | Promise<string>
token: Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]
# (scope: string | string[], tenantId?: string, agenticIdentity?: AgenticIdentity) => string | Promise<string>
token: TokenProvider
"""
The token function.
"""
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/microsoft_teams/api/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from . import bot, conversation, meeting, reaction, team, user
from ._auth_provider_interceptor import AuthProvider
from .api_client import ApiClient
from .api_client_settings import ApiClientSettings, merge_api_client_settings
from .bot import * # noqa: F403
Expand All @@ -17,6 +18,7 @@
__all__: list[str] = [
"ApiClient",
"ApiClientSettings",
"AuthProvider",
"merge_api_client_settings",
]
__all__.extend(bot.__all__)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
"""

from __future__ import annotations

import logging
from typing import Awaitable, Protocol, cast

from microsoft_teams.common import InterceptorRequestContext, resolve_token
from microsoft_teams.common.http.client_token import StringLike

from ..models.agentic_identity import AgenticIdentity


class AuthProvider(Protocol):
def token(
self, *, scope: str | None = None, agentic_identity: AgenticIdentity | None = None
) -> str | StringLike | None | Awaitable[str | StringLike | None]: ...


AGENTIC_IDENTITY_EXTENSION = "microsoft_teams.agentic_identity"
logger = logging.getLogger(__name__)


class AuthProviderInterceptor:
"""Adds an auth-provider token when a request has no Authorization header."""

def __init__(
self,
auth_provider: AuthProvider,
*,
default_agentic_identity: AgenticIdentity | None = None,
) -> None:
self._auth_provider = auth_provider
self._default_agentic_identity = default_agentic_identity

async def request(self, ctx: InterceptorRequestContext) -> None:
if "Authorization" in ctx.request.headers:
return

request_agentic_identity = cast(AgenticIdentity | None, ctx.request.extensions.get(AGENTIC_IDENTITY_EXTENSION))
agentic_identity = request_agentic_identity or self._default_agentic_identity
token = await resolve_token(lambda: self._auth_provider.token(agentic_identity=agentic_identity))
if token is None:
return

token = token.strip()
if not token:
logger.warning("Auth provider returned an empty token; skipping Authorization header.")
return

ctx.request.headers["Authorization"] = f"Bearer {token}"
Loading
Loading