Skip to content
Closed
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
35 changes: 11 additions & 24 deletions examples/agent365/README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,18 @@
# agent365

Acquire an Agent 365 agent user token using an agent identity blueprint, an agent identity app ID, and an agent user.
Demonstrates passing `AgenticIdentity` directly to Teams API surfaces.

## Run
## Proactive API Send

Set these environment variables or add them to `.env`:
`src/main.py` sends through the normal API client, but supplies `agentic_identity` to the activity create operation. The API layer uses the agentic token provider to put the right Agent ID token in the request header.

```bash
AGENT365_TENANT_ID=<tenant-id>
AGENT365_BLUEPRINT_CLIENT_ID=<agent-identity-blueprint-app-id>
AGENT365_BLUEPRINT_CLIENT_SECRET=<agent-identity-blueprint-secret>
AGENT365_AGENT_IDENTITY_APP_ID=<agent-identity-app-id>
AGENT365_AGENT_USER_ID=<agent-user-object-id>
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 \
<conversation-id> \
<agentic-app-id> \
<agentic-user-id>
```

Then run:

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

By default this requests a Teams bot API token for `https://botapi.skype.com/.default`.

To request another resource, set `AGENT365_SCOPE`, for example:

```bash
AGENT365_SCOPE=https://graph.microsoft.com/.default
```

You can use `AGENT365_AGENT_USER_UPN` instead of `AGENT365_AGENT_USER_ID`.
56 changes: 21 additions & 35 deletions examples/agent365/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,36 @@
Licensed under the MIT License.
"""

import argparse
import asyncio
import os
import logging

from dotenv import load_dotenv
from microsoft_teams.api import ClientCredentials
from microsoft_teams.apps.token_manager import AGENT_BOT_API_SCOPE, TokenManager
from microsoft_teams.api import MessageActivityInput
from microsoft_teams.apps import App

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

def get_required_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise ValueError(f"{name} must be set")

return value
async def main():
parser = argparse.ArgumentParser(description="Send a proactive message using API-level 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()

async def main():
load_dotenv()

tenant_id = get_required_env("AGENT365_TENANT_ID")
blueprint_client_id = get_required_env("AGENT365_BLUEPRINT_CLIENT_ID")
blueprint_client_secret = get_required_env("AGENT365_BLUEPRINT_CLIENT_SECRET")
agent_identity_app_id = get_required_env("AGENT365_AGENT_IDENTITY_APP_ID")
agent_user_id = os.getenv("AGENT365_AGENT_USER_ID")
agent_user_upn = os.getenv("AGENT365_AGENT_USER_UPN")
scope = os.getenv("AGENT365_SCOPE", AGENT_BOT_API_SCOPE)

credentials = ClientCredentials(
client_id=blueprint_client_id,
client_secret=blueprint_client_secret,
tenant_id=tenant_id,
)
token_manager = TokenManager(credentials=credentials)

token = await token_manager.get_agent_user_token(
tenant_id,
agent_identity_app_id,
scope,
agent_user_id=agent_user_id,
agent_user_upn=agent_user_upn,
agentic_identity = app.get_agentic_identity(args.agentic_app_id, args.agentic_user_id)
activity = MessageActivityInput(text="Hello from an API-level AgenticIdentity.")

result = await app.api.conversations.activities(args.conversation_id).create(
activity,
agentic_identity=agentic_identity,
)

print(f"Acquired agent user token for {scope}")
print(f"Token preview: {str(token)[:20]}...")
logger.info("Sent activity as agentic identity. Activity ID: %s", result.id)


if __name__ == "__main__":
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,7 @@ class CloudEnvironment:
login_endpoint="https://login.microsoftonline.us",
login_tenant="MicrosoftServices.onmicrosoft.us",
bot_scope="https://api.botframework.us/.default",
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 +61,7 @@ class CloudEnvironment:
login_endpoint="https://login.microsoftonline.us",
login_tenant="MicrosoftServices.onmicrosoft.us",
bot_scope="https://api.botframework.us/.default",
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 +73,7 @@ class CloudEnvironment:
login_endpoint="https://login.partner.microsoftonline.cn",
login_tenant="microsoftservices.partner.onmschina.cn",
bot_scope="https://api.botframework.azure.cn/.default",
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
9 changes: 6 additions & 3 deletions packages/api/src/microsoft_teams/api/auth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from typing import Awaitable, Callable, Literal, Optional, Union

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


class ClientCredentials(CustomBaseModel):
Expand Down Expand Up @@ -36,8 +36,11 @@ 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: Callable[
[Union[str, list[str]], Optional[str], Optional[AgenticIdentity]],
Union[str, Awaitable[str]],
]
"""
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 @@ -6,6 +6,7 @@
from . import bot, conversation, meeting, reaction, team, user
from .api_client import ApiClient
from .api_client_settings import ApiClientSettings, merge_api_client_settings
from .base_client import AuthProvider
from .bot import * # noqa: F403
from .conversation import * # noqa: F403
from .meeting 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
14 changes: 11 additions & 3 deletions packages/api/src/microsoft_teams/api/clients/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from microsoft_teams.common import ClientOptions

from .api_client_settings import ApiClientSettings
from .base_client import BaseClient
from .base_client import AuthProvider, BaseClient
from .bot import BotClient
from .conversation import ConversationClient
from .meeting import MeetingClient
Expand All @@ -32,6 +32,8 @@ def __init__(
options: Optional[Union[HttpClient, ClientOptions]] = None,
api_client_settings: Optional[ApiClientSettings] = None,
cloud: Optional[CloudEnvironment] = None,
*,
auth_provider: Optional[AuthProvider] = None,
) -> None:
"""Initialize the unified Teams API client.

Expand All @@ -41,13 +43,19 @@ def __init__(
api_client_settings: Optional API client settings.
cloud: Optional cloud environment for sovereign cloud support.
"""
super().__init__(options, api_client_settings)
super().__init__(options, api_client_settings, auth_provider=auth_provider, cloud=cloud)
self.service_url = service_url.rstrip("/")

# Initialize all client types
self.bots = BotClient(self._http, self._api_client_settings, cloud=cloud)
self.users = UserClient(self._http, self._api_client_settings)
self.conversations = ConversationClient(self.service_url, self._http, self._api_client_settings)
self.conversations = ConversationClient(
self.service_url,
self._http,
self._api_client_settings,
auth_provider=auth_provider,
cloud=cloud,
)
self.teams = TeamClient(self.service_url, self._http, self._api_client_settings)
self.meetings = MeetingClient(self.service_url, self._http, self._api_client_settings)
self._reactions: Optional[ReactionClient] = None
Expand Down
25 changes: 24 additions & 1 deletion packages/api/src/microsoft_teams/api/clients/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@
Licensed under the MIT License.
"""

from typing import Optional, Union
from typing import Awaitable, Optional, Protocol, Union

from microsoft_teams.api.auth.cloud_environment import PUBLIC, CloudEnvironment
from microsoft_teams.common.http import Client, ClientOptions
from microsoft_teams.common.http.client_token import StringLike

from ..models.agentic_identity import AgenticIdentity
from .api_client_settings import ApiClientSettings, merge_api_client_settings


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


class BaseClient:
"""Base client"""

def __init__(
self,
options: Optional[Union[Client, ClientOptions]] = None,
api_client_settings: Optional[ApiClientSettings] = None,
*,
auth_provider: Optional[AuthProvider] = None,
cloud: Optional[CloudEnvironment] = None,
) -> None:
"""Initialize the BaseClient.

Expand All @@ -32,6 +44,8 @@ def __init__(
self._http = Client(options)

self._api_client_settings = merge_api_client_settings(api_client_settings)
self._auth_provider = auth_provider
self._cloud = cloud or PUBLIC

@property
def http(self) -> Client:
Expand All @@ -42,3 +56,12 @@ def http(self) -> Client:
def http(self, value: Client) -> None:
"""Set the HTTP client instance."""
self._http = value

def _get_agentic_token(self, identity: AgenticIdentity | None):
if identity is None:
return None
if self._auth_provider is None:
raise ValueError("agentic_identity requires a Teams auth provider")
auth_provider = self._auth_provider

return lambda: auth_provider.token(self._cloud.agentic_bot_scope, identity.tenant_id, identity)
Loading
Loading