diff --git a/changelog.md b/changelog.md index 461d61ec..2c6e3c50 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# Microsoft 365 Agents SDK for Python - Release Notes v1.4.0 (Unreleased) + +## New Models & APIs +- **Regionalized UserTokenClient Support**: Added optional argument to `CloudAdapter` to configure Token Service endpoint used by `RestChannelServiceClientFactory` when creating `UserTokenClient` instances. + # Microsoft 365 Agents SDK for Python - Release Notes v1.3.0 **Release Date:** 2026-07-30 diff --git a/dev/integration/tests/adapter/__init__.py b/dev/integration/tests/adapter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py b/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py new file mode 100644 index 00000000..154b6529 --- /dev/null +++ b/dev/integration/tests/adapter/test_aiohttp_cloud_adapter.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import cast + +import pytest + +from microsoft_agents.activity import Activity, ConversationAccount +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import TurnContext +from microsoft_agents.hosting.core.authorization import ClaimsIdentity, Connections +from microsoft_agents.hosting.core.connector.client import UserTokenClient + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "token_service_endpoint", + [ + "https://europe.api.botframework.com", + "https://unitedstates.api.botframework.com", + "https://india.api.botframework.com", + ], +) +async def test_cloud_adapter_configures_user_token_client_endpoint( + token_service_endpoint: str, +): + adapter = CloudAdapter( + connection_manager=cast(Connections, object()), + channel_service_client_factory_options={ + "token_service_endpoint": token_service_endpoint + }, + ) + identity = ClaimsIdentity({"aud": "test-app-id"}, True) + context = TurnContext( + adapter, + Activity( + type="message", + conversation=ConversationAccount(id="conversation-id"), + ), + identity, + ) + + client = await adapter._channel_service_client_factory.create_user_token_client( + context, + identity, + use_anonymous=True, + ) + assert isinstance(client, UserTokenClient) + try: + assert str(client.client._base_url) == f"{token_service_endpoint}/" + finally: + await client.close() diff --git a/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py b/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py new file mode 100644 index 00000000..4c79c1a7 --- /dev/null +++ b/dev/integration/tests/adapter/test_fastapi_cloud_adapter.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import cast + +import pytest + +from microsoft_agents.activity import Activity, ConversationAccount +from microsoft_agents.hosting.core import TurnContext +from microsoft_agents.hosting.core.authorization import ClaimsIdentity, Connections +from microsoft_agents.hosting.core.connector.client import UserTokenClient +from microsoft_agents.hosting.fastapi import CloudAdapter + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "token_service_endpoint", + [ + "https://europe.api.botframework.com", + "https://unitedstates.api.botframework.com", + "https://india.api.botframework.com", + ], +) +async def test_cloud_adapter_configures_user_token_client_endpoint( + token_service_endpoint: str, +): + adapter = CloudAdapter( + connection_manager=cast(Connections, object()), + channel_service_client_factory_options={ + "token_service_endpoint": token_service_endpoint + }, + ) + identity = ClaimsIdentity({"aud": "test-app-id"}, True) + context = TurnContext( + adapter, + Activity( + type="message", + conversation=ConversationAccount(id="conversation-id"), + ), + identity, + ) + + client = await adapter._channel_service_client_factory.create_user_token_client( + context, + identity, + use_anonymous=True, + ) + assert isinstance(client, UserTokenClient) + try: + assert str(client.client._base_url) == f"{token_service_endpoint}/" + finally: + await client.close() diff --git a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py index 234cd679..9ca08b5a 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py +++ b/libraries/microsoft-agents-hosting-aiohttp/microsoft_agents/hosting/aiohttp/cloud_adapter.py @@ -24,16 +24,20 @@ def __init__( *, connection_manager: Connections | None = None, channel_service_client_factory: ChannelServiceClientFactoryBase | None = None, + channel_service_client_factory_options: dict | None = None, ): """ Initializes a new instance of the CloudAdapter class. :param connection_manager: Optional connection manager for OAuth. - :param channel_service_client_factory: The factory to use to create the channel service client. + :param channel_service_client_factory: Factory for creating channel service clients. + :param channel_service_client_factory_options: Optional dictionary of options to pass to the channel service client factory + This is only used if channel_service_client_factory is not provided and connection_manager is provided. """ super().__init__( connection_manager=connection_manager, channel_service_client_factory=channel_service_client_factory, + channel_service_client_factory_options=channel_service_client_factory_options, ) async def process(self, request: Request, agent: Agent) -> Optional[Response]: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py index e99b80b5..8add7c0c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_http_adapter_base.py @@ -34,12 +34,14 @@ def __init__( *, connection_manager: Connections | None = None, channel_service_client_factory: ChannelServiceClientFactoryBase | None = None, + channel_service_client_factory_options: dict | None = None, ): """Initialize the HTTP adapter. - Args: - connection_manager: Optional connection manager for OAuth. - channel_service_client_factory: Factory for creating channel service clients. + :param connection_manager: Optional connection manager for OAuth. + :param channel_service_client_factory: Factory for creating channel service clients. + :param channel_service_client_factory_options: Optional dictionary of options to pass to the channel service client factory + This is only used if channel_service_client_factory is not provided and connection_manager is provided. """ async def on_turn_error(context: TurnContext, error: Exception): @@ -67,7 +69,10 @@ async def on_turn_error(context: TurnContext, error: Exception): raise ValueError( "HttpAdapterBase.__init__: Either channel_service_client_factory or connection_manager must be provided." ) - factory = RestChannelServiceClientFactory(connection_manager) + factory = RestChannelServiceClientFactory( + connection_manager, + **(channel_service_client_factory_options or {}), + ) super().__init__(factory) diff --git a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py index 15f477fa..731ccb9c 100644 --- a/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py +++ b/libraries/microsoft-agents-hosting-fastapi/microsoft_agents/hosting/fastapi/cloud_adapter.py @@ -25,16 +25,20 @@ def __init__( *, connection_manager: Connections | None = None, channel_service_client_factory: ChannelServiceClientFactoryBase | None = None, + channel_service_client_factory_options: dict | None = None, ): """ Initializes a new instance of the CloudAdapter class. :param connection_manager: Optional connection manager for OAuth. - :param channel_service_client_factory: The factory to use to create the channel service client. + :param channel_service_client_factory: Factory for creating channel service clients. + :param channel_service_client_factory_options: Optional dictionary of options to pass to the channel service client factory + This is only used if channel_service_client_factory is not provided and connection_manager is provided. """ super().__init__( connection_manager=connection_manager, channel_service_client_factory=channel_service_client_factory, + channel_service_client_factory_options=channel_service_client_factory_options, ) async def process(self, request: Request, agent: Agent) -> Optional[Response]: