From bb78cfb17292edd1744a8581f858ad68751efe4c Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 21 Jul 2026 18:49:42 -0700 Subject: [PATCH 01/21] TurnContext.services refactor --- .../hosting/core/_oauth/_oauth_flow.py | 4 +- .../oauth/_handlers/_user_authorization.py | 14 ++--- .../core/app/proactive/conversation.py | 6 +- .../hosting/core/channel_adapter.py | 5 -- .../hosting/core/channel_service_adapter.py | 59 +++++++------------ .../hosting/core/connector/__init__.py | 3 + .../core/connector/user_token_client_base.py | 15 ++++- .../hosting/core/state/_service_set.py | 53 +++++++++++++++++ .../hosting/core/turn_context.py | 39 ++---------- .../hosting/dialogs/dialog_extensions.py | 25 ++------ .../hosting/dialogs/dialog_manager.py | 9 +-- .../hosting/dialogs/prompts/oauth_prompt.py | 21 ++++--- .../hosting/slack/slack_agent_extension.py | 10 ++-- 13 files changed, 128 insertions(+), 135 deletions(-) create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py index 6100c4c10..400b3286d 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py @@ -17,7 +17,7 @@ SignInResource, ) -from ..connector.client import UserTokenClient +from ..connector import UserTokenClientBase from ._flow_state import _FlowState, _FlowStateTag, _FlowErrorTag logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class _OAuthFlow: """ def __init__( - self, flow_state: _FlowState, user_token_client: UserTokenClient, **kwargs + self, flow_state: _FlowState, user_token_client: UserTokenClientBase, **kwargs ): """ Arguments: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 3dd21bf13..d390b6d0b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -27,7 +27,7 @@ from microsoft_agents.hosting.core._oauth._flow_state import _FlowErrorTag from microsoft_agents.hosting.core.card_factory import CardFactory from microsoft_agents.hosting.core.message_factory import MessageFactory -from microsoft_agents.hosting.core.connector.client import UserTokenClient +from microsoft_agents.hosting.core.connector import UserTokenClientBase from microsoft_agents.hosting.core.turn_context import TurnContext from microsoft_agents.hosting.core._oauth import ( _OAuthFlow, @@ -65,9 +65,11 @@ async def _load_flow( context and the specified auth handler. :rtype: tuple[OAuthFlow, FlowStorageClient] """ - user_token_client: UserTokenClient = context.turn_state.get( - context.adapter.USER_TOKEN_CLIENT_KEY - ) + user_token_client = context.services.get(UserTokenClientBase) + if not user_token_client: + raise ValueError( + "UserTokenClientBase service is not available in the context" + ) if ( not context.activity.channel_id @@ -79,9 +81,7 @@ async def _load_flow( channel_id = context.activity.channel_id user_id = context.activity.from_property.id - ms_app_id = context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY).claims[ - "aud" - ] + ms_app_id = context.identity.claims["aud"] # try to load existing state flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index aadc876a0..f6abbadfb 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -65,11 +65,7 @@ def from_turn_context(cls, context: "TurnContext") -> "Conversation": and conversation reference. :rtype: :class:`microsoft_agents.hosting.core.app.proactive.Conversation` """ - from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter - - identity: Optional[ClaimsIdentity] = context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY - ) + identity: ClaimsIdentity | None = context.turn_state.identity reference = context.activity.get_conversation_reference() return cls(identity or {}, reference) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py index 7d00fc3a1..53f7596d7 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py @@ -19,13 +19,8 @@ class ChannelAdapter(ABC, ChannelAdapterProtocol): - AGENT_IDENTITY_KEY = "AgentIdentity" OAUTH_SCOPE_KEY = "Microsoft.Agents.Builder.ChannelAdapter.OAuthScope" INVOKE_RESPONSE_KEY = "ChannelAdapter.InvokeResponse" - CONNECTOR_FACTORY_KEY = "ConnectorFactory" - USER_TOKEN_CLIENT_KEY = "UserTokenClient" - AGENT_CALLBACK_HANDLER_KEY = "AgentCallbackHandler" - CHANNEL_SERVICE_FACTORY_KEY = "ChannelServiceClientFactory" on_turn_error: Callable[[TurnContext, Exception], Awaitable] | None = None diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 752ae078c..de020a623 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -4,9 +4,8 @@ from __future__ import annotations from abc import ABC -from copy import Error from http import HTTPStatus -from typing import Awaitable, Callable, cast, Optional +from typing import Awaitable, Callable, Optional from uuid import uuid4 from microsoft_agents.activity import ( @@ -27,6 +26,7 @@ from microsoft_agents.hosting.core.connector import ( ConnectorClientBase, ConnectorClient, + UserTokenClientBase, UserTokenClient, ) from microsoft_agents.hosting.core.authorization import ( @@ -40,7 +40,6 @@ class ChannelServiceAdapter(ChannelAdapter, ABC): - _AGENT_CONNECTOR_CLIENT_KEY = "ConnectorClient" def __init__(self, channel_service_client_factory: ChannelServiceClientFactoryBase): """ @@ -91,12 +90,11 @@ async def send_activities( # no-op pass else: - connector_client = cast( - ConnectorClientBase, - context.turn_state.get(self._AGENT_CONNECTOR_CLIENT_KEY), - ) + connector_client = context.services.get(ConnectorClientBase) if not connector_client: - raise Error("Unable to extract ConnectorClient from turn context.") + raise RuntimeError( + "Unable to extract ConnectorClient from turn context." + ) with spans.AdapterSendActivities([activity]): if activity.reply_to_id: @@ -140,12 +138,11 @@ async def update_activity(self, context: TurnContext, activity: Activity): with spans.AdapterUpdateActivity(activity): - connector_client = cast( - ConnectorClientBase, - context.turn_state.get(self._AGENT_CONNECTOR_CLIENT_KEY), - ) + connector_client = context.services.get(ConnectorClientBase) if not connector_client: - raise Error("Unable to extract ConnectorClient from turn context.") + raise RuntimeError( + "Unable to extract ConnectorClient from turn context." + ) return await connector_client.conversations.update_activity( activity.conversation.id, activity.id, activity @@ -171,12 +168,11 @@ async def delete_activity( with spans.AdapterDeleteActivity(context.activity): - connector_client = cast( - ConnectorClientBase, - context.turn_state.get(self._AGENT_CONNECTOR_CLIENT_KEY), - ) + connector_client = context.services.get(ConnectorClientBase) if not connector_client: - raise Error("Unable to extract ConnectorClient from turn context.") + raise RuntimeError( + "Unable to extract ConnectorClient from turn context." + ) await connector_client.conversations.delete_activity( reference.conversation.id, reference.activity_id @@ -294,18 +290,17 @@ async def create_conversation( # pylint: disable=arguments-differ context = self._create_turn_context( claims_identity, None, - callback, create_activity, ) - context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client + context.services.set(ConnectorClientBase, connector_client) # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity ) ) - context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client + context.services.set(UserTokenClientBase, user_token_client) # Run the pipeline await self.run_pipeline(context, callback) @@ -325,7 +320,6 @@ async def process_proactive( context = self._create_turn_context( claims_identity, audience, - callback, activity=continuation_activity, ) @@ -334,7 +328,7 @@ async def process_proactive( context, claims_identity ) ) - context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client + context.services.set(UserTokenClientBase, user_token_client) # Create the connector client to use for outbound requests. connector_client: ConnectorClient = ( @@ -342,7 +336,7 @@ async def process_proactive( context, claims_identity, continuation_activity.service_url, audience ) ) - context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client + context.services.set(ConnectorClientBase, connector_client) # Run the pipeline await self.run_pipeline(context, callback) @@ -410,7 +404,6 @@ async def process_activity( context = self._create_turn_context( claims_identity, outgoing_audience, - callback, activity=activity, ) @@ -420,7 +413,7 @@ async def process_activity( context, claims_identity, use_anonymous_auth_callback ) ) - context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client + context.services.set(UserTokenClientBase, user_token_client) # Create the connector client to use for outbound requests. connector_client: Optional[ConnectorClient] = None @@ -435,7 +428,7 @@ async def process_activity( use_anonymous_auth_callback, ) ) - context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client + context.services.set(ConnectorClientBase, connector_client) await self.run_pipeline(context, callback) @@ -503,19 +496,11 @@ def _create_create_activity( def _create_turn_context( self, claims_identity: ClaimsIdentity, - oauth_scope: str, - callback: Callable[[TurnContext], Awaitable], + oauth_scope: str | None = None, activity: Optional[Activity] = None, ) -> TurnContext: context = TurnContext(self, activity, claims_identity) - - context.turn_state[self.AGENT_IDENTITY_KEY] = claims_identity - context.turn_state[self.AGENT_CALLBACK_HANDLER_KEY] = callback - context.turn_state[self.CHANNEL_SERVICE_FACTORY_KEY] = ( - self._channel_service_client_factory - ) context.turn_state[self.OAUTH_SCOPE_KEY] = oauth_scope - return context def _process_turn_results(self, context: TurnContext) -> Optional[InvokeResponse]: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/__init__.py index 17efa5770..2a910ca71 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/__init__.py @@ -6,6 +6,8 @@ from .client.connector_client import ConnectorClient from .client.user_token_client import UserTokenClient +from .user_token_client_base import UserTokenClientBase + # Teams API from .teams.teams_connector_client import TeamsConnectorClient @@ -20,4 +22,5 @@ "MCSConnectorClient", "ConnectorClientBase", "get_product_info", + "UserTokenClientBase", ] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py index 7c398a58c..c3a9ce3e8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py @@ -9,12 +9,21 @@ class UserTokenClientBase(Protocol): + @property - @abstractmethod def agent_sign_in(self) -> AgentSignInBase: - pass + raise NotImplementedError( + "agent_sign_in property must be implemented by subclasses." + ) @property @abstractmethod def user_token(self) -> UserTokenBase: - pass + raise NotImplementedError( + "user_token property must be implemented by subclasses." + ) + + @abstractmethod + async def close(self) -> None: + """Close the client and release any resources.""" + raise NotImplementedError("close method must be implemented by subclasses.") diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py new file mode 100644 index 000000000..7bfd02283 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +from __future__ import annotations + +from typing import TypeVar, cast, Any + +T = TypeVar("T") + + +class _ServiceSet: + """ + Analog of .NET's TurnContextStateCollection + """ + + def __init__(self, service_set: _ServiceSet | None = None) -> None: + self._state: dict[str, Any] = {} + if service_set is not None: + self._state.update(service_set._state) + + def get(self, key: type[T]) -> T | None: + """ + Gets a value from the state collection. + :param key: + :return: + """ + lookup_key = key.__name__ + + val = self._state.get(lookup_key) + if val is not None: + if not isinstance(val, key): + raise TypeError( + f"Value for key '{lookup_key}' is not of type {key.__name__}" + ) + return cast(T, val) + return None + + def has(self, key: type) -> bool: + """ + Checks if a value exists in the state collection. + :param key: Type of the value to check for. + :return: True if the value exists, False otherwise. + """ + return key.__name__ in self._state + + def set(self, key: type[T], value: T) -> None: + """ + Sets a value in the state collection. + :param key: Type of the value to set. + :param value: The value to set. + """ + self._state[key.__name__] = value diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py index 3bd9a7cd5..0e83405b8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import Optional +from typing import Optional, TYPE_CHECKING from copy import copy, deepcopy from collections.abc import Callable @@ -20,8 +20,11 @@ ) from microsoft_agents.activity.entity.entity_types import EntityTypes from microsoft_agents.hosting.core.authorization.claims_identity import ClaimsIdentity + import microsoft_agents.hosting.core.telemetry.turn_context.spans as spans +from .state._service_set import _ServiceSet + class TurnContext(TurnContextProtocol): # Same constant as in the BF Adapter, duplicating here to avoid circular dependency @@ -47,7 +50,7 @@ def __init__( self.adapter = adapter_or_context self._activity = request # exception thrown if None further down self.responses: list[Activity] = [] - self._services: dict = {} + self._services: _ServiceSet = _ServiceSet() self._on_send_activities: Callable[ ["TurnContext", list[Activity], Callable], list[ResourceResponse] ] = [] @@ -130,7 +133,7 @@ def responded(self, value: bool): self._responded = True @property - def services(self): + def services(self) -> _ServiceSet: """ Map of services and other values cached for the lifetime of the turn. :return: @@ -154,36 +157,6 @@ def streaming_response(self): def identity(self) -> Optional[ClaimsIdentity]: return self._identity - def get(self, key: str) -> object: - if not key or not isinstance(key, str): - raise TypeError('"key" must be a valid string.') - try: - return self._services[key] - except KeyError: - raise KeyError("%s not found in TurnContext._services." % key) - - def has(self, key: str) -> bool: - """ - Returns True is set() has been called for a key. The cached value may be of type 'None'. - :param key: - :return: - """ - if key in self._services: - return True - return False - - def set(self, key: str, value: object) -> None: - """ - Caches a value for the lifetime of the current turn. - :param key: - :param value: - :return: - """ - if not key or not isinstance(key, str): - raise KeyError('"key" must be a valid string.') - - self._services[key] = value - async def send_activity( self, activity_or_text: Activity | str, diff --git a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_extensions.py b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_extensions.py index 3b83fe5c2..5b1028109 100644 --- a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_extensions.py +++ b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_extensions.py @@ -138,26 +138,18 @@ def __is_from_parent_to_skill(turn_context: TurnContext) -> bool: """ Determines if this turn is an incoming request from a parent bot to this skill. """ - claims_identity = turn_context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY, None - ) - return ( - isinstance(claims_identity, ClaimsIdentity) - and claims_identity.is_agent_claim() - ) + claims_identity = turn_context.identity + return claims_identity is not None and claims_identity.is_agent_claim() @staticmethod async def _send_state_snapshot_trace(dialog_context: DialogContext): """ Helper to send a trace activity with a memory snapshot of the active dialog DC. """ - claims_identity = dialog_context.context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY, None - ) + claims_identity = dialog_context.context.identity trace_label = ( "Skill State" - if isinstance(claims_identity, ClaimsIdentity) - and claims_identity.is_agent_claim() + if claims_identity is not None and claims_identity.is_agent_claim() else "Bot State" ) # send trace of memory @@ -178,13 +170,8 @@ def __send_eoc_to_parent(turn_context: TurnContext) -> bool: """ Determines whether to send an EndOfConversation to the parent bot. """ - claims_identity = turn_context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY, None - ) - if ( - isinstance(claims_identity, ClaimsIdentity) - and claims_identity.is_agent_claim() - ): + claims_identity = turn_context.identity + if claims_identity is not None and claims_identity.is_agent_claim(): return True return False diff --git a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_manager.py b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_manager.py index 7c2ec7d87..ef5e9f934 100644 --- a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_manager.py +++ b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/dialog_manager.py @@ -137,13 +137,8 @@ def is_from_parent_to_skill(turn_context: TurnContext) -> bool: Determines if this turn is a request from a parent bot to this skill. """ - claims_identity = turn_context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY, None - ) - return ( - isinstance(claims_identity, ClaimsIdentity) - and claims_identity.is_agent_claim() - ) + claims_identity = turn_context.identity + return claims_identity is not None and claims_identity.is_agent_claim() @staticmethod def should_send_end_of_conversation_to_parent( diff --git a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py index aade7e978..9a8dae712 100644 --- a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py +++ b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py @@ -27,7 +27,7 @@ TurnContext, ChannelAdapter, ClaimsIdentity, - UserTokenClient, + UserTokenClientBase, MemoryStorage, ) from microsoft_agents.hosting.core._oauth import ( @@ -80,8 +80,13 @@ def __init__( self._settings = settings @staticmethod - def _get_user_token_client(context: TurnContext) -> UserTokenClient: - return context.turn_state.get(context.adapter.USER_TOKEN_CLIENT_KEY) + def _get_user_token_client(context: TurnContext) -> UserTokenClientBase: + val = context.services.get(UserTokenClientBase) + if not val: + raise Exception( + "OAuthPrompt._get_user_token_client(): UserTokenClientBase not found in context.services." + ) + return val def _get_app_id(self, context: TurnContext) -> str: if ( @@ -255,10 +260,7 @@ async def sign_out_user(self, context: TurnContext): @staticmethod def __create_caller_info(context: TurnContext) -> CallerInfo | None: - bot_identity = cast( - ClaimsIdentity | None, - context.turn_state.get(ChannelAdapter.AGENT_IDENTITY_KEY), - ) + bot_identity = context.identity if bot_identity and bot_identity.is_agent_claim(): return CallerInfo( caller_service_url=context.activity.service_url, @@ -288,10 +290,7 @@ async def _send_oauth_card( card_action_type = ActionTypes.signin sign_in_resource = flow_response.sign_in_resource link = sign_in_resource.sign_in_link - bot_identity = cast( - ClaimsIdentity | None, - context.turn_state.get(ChannelAdapter.AGENT_IDENTITY_KEY), - ) + bot_identity = context.identity # use the SignInLink when in speech channel or bot is a skill or # an extra OAuthAppCredentials is being passed in diff --git a/libraries/microsoft-agents-hosting-slack/microsoft_agents/hosting/slack/slack_agent_extension.py b/libraries/microsoft-agents-hosting-slack/microsoft_agents/hosting/slack/slack_agent_extension.py index f35963020..a8782f9e6 100644 --- a/libraries/microsoft-agents-hosting-slack/microsoft_agents/hosting/slack/slack_agent_extension.py +++ b/libraries/microsoft-agents-hosting-slack/microsoft_agents/hosting/slack/slack_agent_extension.py @@ -24,8 +24,6 @@ TextSelector = str | Pattern[str] | None -_SLACK_API_SERVICE_KEY = "microsoft_agents.hosting.slack.SlackApi" - def _matches_text(selector: TextSelector, text: Optional[str]) -> bool: if selector is None: @@ -94,8 +92,8 @@ async def call( """Invoke a Slack Web API method, preferring a per-turn :class:`SlackApi` if one has been cached on ``turn_context.services``.""" api = self._slack_api - if turn_context is not None and turn_context.has(_SLACK_API_SERVICE_KEY): - api = turn_context.get(_SLACK_API_SERVICE_KEY) # type: ignore[assignment] + if turn_context is not None and turn_context.services.has(SlackApi): + api = turn_context.services.get(SlackApi) return await api.call(method, options, token) async def create_stream( @@ -111,8 +109,8 @@ async def create_stream( ) resolved_thread_ts = thread_ts or channel_data.envelope.get("event.ts") api = self._slack_api - if turn_context.has(_SLACK_API_SERVICE_KEY): - api = turn_context.get(_SLACK_API_SERVICE_KEY) # type: ignore[assignment] + if turn_context.services.has(SlackApi): + api = turn_context.services.get(SlackApi) stream = SlackStream( api, channel_data.envelope.get("event.channel"), From f732b4077ba1255bdb515c750febd4ffc16a6f77 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 21 Jul 2026 20:41:03 -0700 Subject: [PATCH 02/21] another commit --- .../hosting/core/{state => }/_service_set.py | 0 .../core/app/proactive/conversation.py | 2 +- .../core/connector/connector_client_base.py | 3 ++- .../core/connector/user_token_client_base.py | 3 ++- .../hosting/core/turn_context.py | 2 +- .../hosting/msteams/_teams_api_client.py | 10 +++---- .../adapters/mock_testing_adapter.py | 3 ++- tests/hosting_core/app/_oauth/_common.py | 27 +++++++++---------- .../test_connector_user_authorization.py | 7 +---- .../_handlers/test_user_authorization.py | 14 +++++----- .../app/proactive/test_conversation.py | 5 ++-- .../app/proactive/test_proactive.py | 3 +-- .../test_channel_service_adapter.py | 20 +++----------- tests/hosting_dialogs/helpers.py | 17 +++++------- tests/hosting_msteams/helpers.py | 18 +++++++++++-- tests/hosting_msteams/test_internal.py | 21 ++++++++++----- .../test_teams_agent_extension.py | 21 ++++++++++----- 17 files changed, 91 insertions(+), 85 deletions(-) rename libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/{state => }/_service_set.py (100%) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_service_set.py similarity index 100% rename from libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/state/_service_set.py rename to libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_service_set.py diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index f6abbadfb..60bf67fb1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -65,7 +65,7 @@ def from_turn_context(cls, context: "TurnContext") -> "Conversation": and conversation reference. :rtype: :class:`microsoft_agents.hosting.core.app.proactive.Conversation` """ - identity: ClaimsIdentity | None = context.turn_state.identity + identity: ClaimsIdentity | None = context.identity reference = context.activity.get_conversation_reference() return cls(identity or {}, reference) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py index 545c51772..943b752a2 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py @@ -2,12 +2,13 @@ # Licensed under the MIT License. from abc import abstractmethod -from typing import Protocol +from typing import Protocol, runtime_checkable from .attachments_base import AttachmentsBase from .conversations_base import ConversationsBase +@runtime_checkable class ConnectorClientBase(Protocol): @property @abstractmethod diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py index c3a9ce3e8..020dd1116 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py @@ -2,12 +2,13 @@ # Licensed under the MIT License. from abc import abstractmethod -from typing import Protocol +from typing import Protocol, runtime_checkable from .agent_sign_in_base import AgentSignInBase from .user_token_base import UserTokenBase +@runtime_checkable class UserTokenClientBase(Protocol): @property diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py index 0e83405b8..e714e00a3 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py @@ -23,7 +23,7 @@ import microsoft_agents.hosting.core.telemetry.turn_context.spans as spans -from .state._service_set import _ServiceSet +from ._service_set import _ServiceSet class TurnContext(TurnContextProtocol): diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index a19863230..dbbd0c7c8 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -3,7 +3,7 @@ """Construction and caching of the Teams :class:`ApiClient` for a turn. -The client is cached on the turn state so it is built at most once per turn, and +The client is cached on turn context services so it is built at most once per turn, and is configured with a token factory derived from the turn's identity when one is available. """ @@ -16,8 +16,6 @@ TurnContext, ) -_TEAMS_API_CLIENT_KEY = "TeamsApiClient" - def _get_teams_api_client(context: TurnContext) -> ApiClient: """ @@ -27,7 +25,7 @@ def _get_teams_api_client(context: TurnContext) -> ApiClient: :return: The cached Teams API client. :raises ValueError: If the Teams API client is not found. """ - api_client = context.turn_state.get(_TEAMS_API_CLIENT_KEY) + api_client = context.services.get(ApiClient) if isinstance(api_client, ApiClient): return api_client raise ValueError("Unable to retrieve Teams API client.") @@ -43,7 +41,7 @@ def _set_teams_api_client( :param connection_manager: The connection manager. """ - if _TEAMS_API_CLIENT_KEY in context.turn_state: + if context.services.has(ApiClient): return headers = { @@ -75,4 +73,4 @@ async def token_factory() -> str: options, ) - context.turn_state[_TEAMS_API_CLIENT_KEY] = api_client + context.services.set(ApiClient, api_client) diff --git a/tests/_common/testing_objects/adapters/mock_testing_adapter.py b/tests/_common/testing_objects/adapters/mock_testing_adapter.py index 4cd38b453..eafc4a490 100644 --- a/tests/_common/testing_objects/adapters/mock_testing_adapter.py +++ b/tests/_common/testing_objects/adapters/mock_testing_adapter.py @@ -20,6 +20,7 @@ InvokeResponse, ) from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter +from microsoft_agents.hosting.core.connector import UserTokenClientBase from microsoft_agents.hosting.core.turn_context import TurnContext from ..testing_user_token_client import TestingUserTokenClient @@ -496,7 +497,7 @@ def create_turn_context( """ turn_context = TurnContext(self, activity) - turn_context.services["UserTokenClient"] = self._user_token_client + turn_context.services.set(UserTokenClientBase, self._user_token_client) turn_context._identity = identity or self.claims_identity return turn_context diff --git a/tests/hosting_core/app/_oauth/_common.py b/tests/hosting_core/app/_oauth/_common.py index 4d3fea34e..95151ed2e 100644 --- a/tests/hosting_core/app/_oauth/_common.py +++ b/tests/hosting_core/app/_oauth/_common.py @@ -1,6 +1,6 @@ from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import TurnContext +from microsoft_agents.hosting.core import TurnContext, UserTokenClientBase from tests._common.data import DEFAULT_TEST_VALUES from tests._common.testing_objects import mock_UserTokenClient @@ -36,14 +36,14 @@ def create_testing_TurnContext( ) else: turn_context.activity = activity - turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client" - turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" agent_identity = mocker.Mock() agent_identity.claims = {"aud": DEFAULTS.ms_app_id} - turn_context.turn_state = { - "__user_token_client": user_token_client, - "__agent_identity_key": agent_identity, - } + turn_context.identity = agent_identity + turn_context.services = mocker.Mock() + turn_context.services.get.side_effect = lambda key: ( + user_token_client if key is UserTokenClientBase else None + ) + turn_context.turn_state = {} return turn_context @@ -66,13 +66,12 @@ def create_testing_TurnContext_magic( turn_context.activity.type = ActivityTypes.message else: turn_context.activity = activity - turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client" - turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" agent_identity = mocker.Mock() agent_identity.claims = {"aud": DEFAULTS.ms_app_id} - turn_context.turn_state = mocker.Mock() - turn_context.turn_state = { - "__user_token_client": user_token_client, - "__agent_identity_key": agent_identity, - } + turn_context.identity = agent_identity + turn_context.services = mocker.Mock() + turn_context.services.get.side_effect = lambda key: ( + user_token_client if key is UserTokenClientBase else None + ) + turn_context.turn_state = {} return turn_context diff --git a/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py index 48c753716..8ac0c6afb 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_connector_user_authorization.py @@ -60,7 +60,6 @@ def create_testing_TurnContext( turn_context.activity.channel_id = channel_id turn_context.activity.from_property.id = user_id turn_context.activity.type = ActivityTypes.message - turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" # Create identity with security token identity = mocker.Mock() @@ -68,11 +67,7 @@ def create_testing_TurnContext( identity.security_token = security_token turn_context.identity = identity - agent_identity = mocker.Mock() - agent_identity.claims = {"aud": DEFAULTS.ms_app_id} - turn_context.turn_state = { - "__agent_identity_key": agent_identity, - } + turn_context.turn_state = {} return turn_context diff --git a/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py index e11c612b1..9bc4459d0 100644 --- a/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py +++ b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py @@ -5,7 +5,7 @@ from microsoft_agents.authentication.msal import MsalAuth, MsalConnectionManager -from microsoft_agents.hosting.core import MemoryStorage +from microsoft_agents.hosting.core import MemoryStorage, UserTokenClientBase from microsoft_agents.hosting.core.app.oauth import _UserAuthorization, _SignInResponse from microsoft_agents.hosting.core._oauth import ( _FlowStorageClient, @@ -63,14 +63,14 @@ def create_testing_TurnContext( turn_context.activity.channel_id = channel_id turn_context.activity.from_property.id = user_id turn_context.activity.type = ActivityTypes.message - turn_context.adapter.USER_TOKEN_CLIENT_KEY = "__user_token_client" - turn_context.adapter.AGENT_IDENTITY_KEY = "__agent_identity_key" agent_identity = mocker.Mock() agent_identity.claims = {"aud": DEFAULTS.ms_app_id} - turn_context.turn_state = { - "__user_token_client": user_token_client, - "__agent_identity_key": agent_identity, - } + turn_context.identity = agent_identity + turn_context.services = mocker.Mock() + turn_context.services.get.side_effect = lambda key: ( + user_token_client if key is UserTokenClientBase else None + ) + turn_context.turn_state = {} return turn_context diff --git a/tests/hosting_core/app/proactive/test_conversation.py b/tests/hosting_core/app/proactive/test_conversation.py index 3c65f1681..d6ca5adf9 100644 --- a/tests/hosting_core/app/proactive/test_conversation.py +++ b/tests/hosting_core/app/proactive/test_conversation.py @@ -9,7 +9,6 @@ from microsoft_agents.activity import ConversationAccount, ConversationReference from microsoft_agents.hosting.core.app.proactive import Conversation from microsoft_agents.hosting.core.authorization import ClaimsIdentity -from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter def _make_reference( @@ -75,7 +74,7 @@ def test_from_turn_context_extracts_reference_and_identity(self): ctx = MagicMock() ctx.activity.get_conversation_reference.return_value = ref - ctx.turn_state = {ChannelAdapter.AGENT_IDENTITY_KEY: identity} + ctx.identity = identity conv = Conversation.from_turn_context(ctx) @@ -87,7 +86,7 @@ def test_from_turn_context_handles_missing_identity(self): ref = _make_reference("ctx-conv") ctx = MagicMock() ctx.activity.get_conversation_reference.return_value = ref - ctx.turn_state = {} + ctx.identity = None conv = Conversation.from_turn_context(ctx) diff --git a/tests/hosting_core/app/proactive/test_proactive.py b/tests/hosting_core/app/proactive/test_proactive.py index d7c73f518..e41c10383 100644 --- a/tests/hosting_core/app/proactive/test_proactive.py +++ b/tests/hosting_core/app/proactive/test_proactive.py @@ -24,7 +24,6 @@ ProactiveOptions, ) from microsoft_agents.hosting.core.authorization import ClaimsIdentity -from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter # --------------------------------------------------------------------------- # Helpers @@ -165,7 +164,7 @@ async def test_store_from_turn_context(self, proactive): ctx = MagicMock(spec=TurnContext) ctx.activity = MagicMock() ctx.activity.get_conversation_reference.return_value = ref - ctx.turn_state = {ChannelAdapter.AGENT_IDENTITY_KEY: identity} + ctx.identity = identity await proactive.store_conversation(ctx) result = await proactive.get_conversation("ctx-conv") diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 8a041bd43..aa7267f27 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -157,14 +157,8 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url == service_url - assert ( - context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] - is user_token_client - ) - assert ( - ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY - not in context_arg.turn_state - ) + assert context_arg.services.get(UserTokenClientBase) is user_token_client + assert not context_arg.services.has(ConnectorClientBase) @pytest.mark.asyncio async def test_process_activity_normal_no_service_url( @@ -244,11 +238,5 @@ async def callback(context: TurnContext): assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" assert context_arg.activity.service_url == "service_url" - assert ( - context_arg.turn_state[ChannelServiceAdapter.USER_TOKEN_CLIENT_KEY] - is user_token_client - ) - assert ( - context_arg.turn_state[ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY] - is connector_client - ) + assert context_arg.services.get(UserTokenClientBase) is user_token_client + assert context_arg.services.get(ConnectorClientBase) is connector_client diff --git a/tests/hosting_dialogs/helpers.py b/tests/hosting_dialogs/helpers.py index b8e93bfcb..87fb34a09 100644 --- a/tests/hosting_dialogs/helpers.py +++ b/tests/hosting_dialogs/helpers.py @@ -16,7 +16,7 @@ SignInResource, TokenOrSignInResourceResponse, ) -from microsoft_agents.hosting.core import ChannelAdapter, TurnContext +from microsoft_agents.hosting.core import TurnContext, UserTokenClientBase from microsoft_agents.hosting.core.authorization import ClaimsIdentity from tests._common.testing_objects import MockTestingAdapter @@ -210,7 +210,7 @@ class DialogTestAdapter(MockTestingAdapter): """ A test adapter compatible with the botbuilder TestAdapter API. Provides send() and assert_reply() methods for fluent test flows. - Also provides a proper UserTokenClient in turn_state for OAuthPrompt tests. + Also provides a proper UserTokenClient service for OAuthPrompt tests. """ def __init__(self, callback: AgentCallbackHandler = None, **kwargs): @@ -218,7 +218,7 @@ def __init__(self, callback: AgentCallbackHandler = None, **kwargs): self._callback = callback # Dialog-specific token client that implements the user_token API self._dialog_token_client = DialogUserTokenClient() - # OAuthPrompt reads claims["aud"] from the identity in turn_state + # OAuthPrompt reads claims["aud"] from the turn context identity. self.claims_identity = ClaimsIdentity({"aud": "test-app-id"}, True) def add_user_token( @@ -269,17 +269,12 @@ def create_turn_context( self, activity: Activity, identity: ClaimsIdentity = None ) -> TurnContext: """ - Creates a turn context with the dialog token client in turn_state + Creates a turn context with the dialog token client service so OAuthPrompt can find it via _UserTokenAccess. """ turn_context = super().create_turn_context(activity, identity) - turn_context.turn_state[ChannelAdapter.USER_TOKEN_CLIENT_KEY] = ( - self._dialog_token_client - ) - # OAuthPrompt reads claims["aud"] from this identity - turn_context.turn_state[ChannelAdapter.AGENT_IDENTITY_KEY] = ( - identity or self.claims_identity - ) + turn_context.services.set(UserTokenClientBase, self._dialog_token_client) + turn_context._identity = identity or self.claims_identity return turn_context def make_activity(self, text: str = None) -> Activity: diff --git a/tests/hosting_msteams/helpers.py b/tests/hosting_msteams/helpers.py index 0b5dbbff9..d82cdf62f 100644 --- a/tests/hosting_msteams/helpers.py +++ b/tests/hosting_msteams/helpers.py @@ -17,6 +17,20 @@ from microsoft_agents.hosting.msteams.teams_turn_context import TeamsTurnContext +class _FakeServiceSet: + def __init__(self): + self._state = {} + + def get(self, key): + return self._state.get(key) + + def has(self, key): + return key in self._state + + def set(self, key, value): + self._state[key] = value + + def _make_app() -> Any: app = MagicMock(spec=AgentApplication) app._routes = [] @@ -64,7 +78,7 @@ def _make_context( mock_adapter = MagicMock() context.adapter = mock_adapter context._responded = False - context._services = {} + context._services = _FakeServiceSet() context._on_send_activities = [] context._on_update_activity = [] context._on_delete_activity = [] @@ -74,7 +88,7 @@ def _copy_to(target): target.adapter = mock_adapter target._activity = activity target._responded = False - target._services = {} + target._services = _FakeServiceSet() target._on_send_activities = [] target._on_update_activity = [] target._on_delete_activity = [] diff --git a/tests/hosting_msteams/test_internal.py b/tests/hosting_msteams/test_internal.py index 4b840d993..485597e88 100644 --- a/tests/hosting_msteams/test_internal.py +++ b/tests/hosting_msteams/test_internal.py @@ -16,7 +16,6 @@ from microsoft_teams.api import ApiClient from microsoft_agents.hosting.msteams._teams_api_client import ( - _TEAMS_API_CLIENT_KEY, _get_teams_api_client, ) from microsoft_agents.hosting.msteams.errors.error_resources import ( @@ -24,27 +23,35 @@ ) +class _FakeServices: + def __init__(self, value=None): + self._value = value + + def get(self, key): + return self._value + + class _FakeContext: - """Minimal stand-in exposing only the ``turn_state`` dict the accessor reads.""" + """Minimal stand-in exposing only the ``services`` accessor reads.""" - def __init__(self, turn_state): - self.turn_state = turn_state + def __init__(self, services): + self.services = services class TestGetTeamsApiClient: def test_returns_cached_api_client(self): client = ApiClient("https://smba.trafficmanager.net/teams/") - ctx = _FakeContext({_TEAMS_API_CLIENT_KEY: client}) + ctx = _FakeContext(_FakeServices(client)) assert _get_teams_api_client(ctx) is client def test_raises_when_missing(self): - ctx = _FakeContext({}) + ctx = _FakeContext(_FakeServices()) with pytest.raises(ValueError, match="Teams API client"): _get_teams_api_client(ctx) def test_raises_when_wrong_type(self): - ctx = _FakeContext({_TEAMS_API_CLIENT_KEY: object()}) + ctx = _FakeContext(_FakeServices(object())) with pytest.raises(ValueError, match="Teams API client"): _get_teams_api_client(ctx) diff --git a/tests/hosting_msteams/test_teams_agent_extension.py b/tests/hosting_msteams/test_teams_agent_extension.py index fac8fae1a..c8da5a822 100644 --- a/tests/hosting_msteams/test_teams_agent_extension.py +++ b/tests/hosting_msteams/test_teams_agent_extension.py @@ -14,12 +14,10 @@ if is_supported_version: from microsoft_agents.activity import Activity, Channels + from microsoft_teams.api import ApiClient from microsoft_teams.api.models import ChannelData from microsoft_agents.hosting.msteams import TeamsAgentExtension - from microsoft_agents.hosting.msteams._teams_api_client import ( - _TEAMS_API_CLIENT_KEY, - ) from microsoft_agents.hosting.msteams.channel import Channel from microsoft_agents.hosting.msteams.config import Config from microsoft_agents.hosting.msteams.file_consent import FileConsent @@ -30,13 +28,24 @@ from microsoft_agents.hosting.msteams.team import Team +class _FakeServiceSet: + def __init__(self): + self._state = {} + + def has(self, key): + return key in self._state + + def set(self, key, value): + self._state[key] = value + + class _FakeContext: """Minimal context stand-in for exercising the before_turn hook directly.""" def __init__(self, activity, identity=None): self.activity = activity self.identity = identity - self.turn_state = {} + self.services = _FakeServiceSet() class TestTeamsAgentExtensionProperties: @@ -99,7 +108,7 @@ async def test_non_teams_channel_is_untouched(self): assert result is True # channel_data left as the raw dict; no Teams API client cached assert activity.channel_data == {"channel": {"id": "c"}} - assert _TEAMS_API_CLIENT_KEY not in ctx.turn_state + assert not ctx.services.has(ApiClient) @pytest.mark.asyncio async def test_teams_channel_deserializes_channel_data(self): @@ -116,7 +125,7 @@ async def test_teams_channel_deserializes_channel_data(self): assert result is True assert isinstance(activity.channel_data, ChannelData) assert activity.channel_data.channel.id == "c1" - assert _TEAMS_API_CLIENT_KEY in ctx.turn_state + assert ctx.services.has(ApiClient) @pytest.mark.asyncio async def test_teams_channel_without_channel_data_sets_none(self): From 4664b3b384480a0975412ed2a9734fc43a6516f5 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 21 Jul 2026 21:03:52 -0700 Subject: [PATCH 03/21] another commit --- .../hosting/core/_utils/__init__.py | 6 ++ .../hosting/core/{ => _utils}/_service_set.py | 2 - .../hosting/core/turn_context.py | 2 +- tests/hosting_core/test_service_set.py | 76 +++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py rename libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/{ => _utils}/_service_set.py (99%) create mode 100644 tests/hosting_core/test_service_set.py diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py new file mode 100644 index 000000000..eeba56221 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ._service_set import _ServiceSet + +__all__ = ["_ServiceSet"] \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py similarity index 99% rename from libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_service_set.py rename to libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index 7bfd02283..921be7376 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -1,14 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - from __future__ import annotations from typing import TypeVar, cast, Any T = TypeVar("T") - class _ServiceSet: """ Analog of .NET's TurnContextStateCollection diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py index e714e00a3..02ded9896 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py @@ -23,7 +23,7 @@ import microsoft_agents.hosting.core.telemetry.turn_context.spans as spans -from ._service_set import _ServiceSet +from ._utils._service_set import _ServiceSet class TurnContext(TurnContextProtocol): diff --git a/tests/hosting_core/test_service_set.py b/tests/hosting_core/test_service_set.py new file mode 100644 index 000000000..06cdf9611 --- /dev/null +++ b/tests/hosting_core/test_service_set.py @@ -0,0 +1,76 @@ +import pytest + +from microsoft_agents.hosting.core._utils._service_set import _ServiceSet + + +class Service: + pass + + +class OtherService: + pass + + +def test_get_returns_none_for_missing_service(): + services = _ServiceSet() + + assert services.get(Service) is None + + +def test_has_returns_false_for_missing_service(): + services = _ServiceSet() + + assert not services.has(Service) + + +def test_set_registers_service_by_type(): + service = Service() + services = _ServiceSet() + + services.set(Service, service) + + assert services.has(Service) + assert services.get(Service) is service + assert not services.has(OtherService) + + +def test_set_overwrites_existing_service_for_type(): + first = Service() + second = Service() + services = _ServiceSet() + + services.set(Service, first) + services.set(Service, second) + + assert services.get(Service) is second + + +def test_copy_constructor_copies_registered_services(): + service = Service() + services = _ServiceSet() + services.set(Service, service) + + copy = _ServiceSet(services) + + assert copy.get(Service) is service + + +def test_copy_constructor_does_not_share_state_dictionary(): + service = Service() + replacement = Service() + services = _ServiceSet() + services.set(Service, service) + copy = _ServiceSet(services) + + services.set(Service, replacement) + + assert copy.get(Service) is service + assert services.get(Service) is replacement + + +def test_get_raises_type_error_when_stored_value_does_not_match_key(): + services = _ServiceSet() + services._state[Service.__name__] = OtherService() + + with pytest.raises(TypeError, match="Value for key 'Service' is not of type Service"): + services.get(Service) From ebdc3ea9f83d02f9c969e1e586511640923cdfe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Wed, 22 Jul 2026 09:27:25 -0700 Subject: [PATCH 04/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py index 9a8dae712..2a36b8ce3 100644 --- a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py +++ b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py @@ -82,8 +82,8 @@ def __init__( @staticmethod def _get_user_token_client(context: TurnContext) -> UserTokenClientBase: val = context.services.get(UserTokenClientBase) - if not val: - raise Exception( + if val is None: + raise RuntimeError( "OAuthPrompt._get_user_token_client(): UserTokenClientBase not found in context.services." ) return val From 0cba44e68e7e430bf3aafe8fbfd5c7d2ef7bbce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Wed, 22 Jul 2026 09:29:16 -0700 Subject: [PATCH 05/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../hosting/core/app/oauth/_handlers/_user_authorization.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index d390b6d0b..a4710d881 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -81,7 +81,10 @@ async def _load_flow( channel_id = context.activity.channel_id user_id = context.activity.from_property.id - ms_app_id = context.identity.claims["aud"] + identity = context.identity + if identity is None: + raise ValueError("ClaimsIdentity is required on TurnContext for OAuth flow.") + ms_app_id = identity.claims["aud"] # try to load existing state flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage) From 0846d056af38e44596eda7a98730ee3c33405c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Wed, 22 Jul 2026 09:29:23 -0700 Subject: [PATCH 06/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/hosting/core/turn_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py index 02ded9896..834029700 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from typing import Optional, TYPE_CHECKING +from typing import Optional from copy import copy, deepcopy from collections.abc import Callable From 14d775e078bd43bb744c06022ee5f6f66fdfd731 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 09:32:58 -0700 Subject: [PATCH 07/21] Formatting with black --- .../microsoft_agents/hosting/core/_utils/__init__.py | 2 +- .../microsoft_agents/hosting/core/_utils/_service_set.py | 1 + tests/hosting_core/test_service_set.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py index eeba56221..a9481463d 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/__init__.py @@ -3,4 +3,4 @@ from ._service_set import _ServiceSet -__all__ = ["_ServiceSet"] \ No newline at end of file +__all__ = ["_ServiceSet"] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index 921be7376..f0febcf20 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -7,6 +7,7 @@ T = TypeVar("T") + class _ServiceSet: """ Analog of .NET's TurnContextStateCollection diff --git a/tests/hosting_core/test_service_set.py b/tests/hosting_core/test_service_set.py index 06cdf9611..d65ea5ce9 100644 --- a/tests/hosting_core/test_service_set.py +++ b/tests/hosting_core/test_service_set.py @@ -72,5 +72,7 @@ def test_get_raises_type_error_when_stored_value_does_not_match_key(): services = _ServiceSet() services._state[Service.__name__] = OtherService() - with pytest.raises(TypeError, match="Value for key 'Service' is not of type Service"): + with pytest.raises( + TypeError, match="Value for key 'Service' is not of type Service" + ): services.get(Service) From b09c84769920e0afaf69ee0bee572d067e28379b Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 09:51:51 -0700 Subject: [PATCH 08/21] Formatting with black --- .../hosting/core/app/oauth/_handlers/_user_authorization.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index a4710d881..003fdf02c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -83,7 +83,9 @@ async def _load_flow( identity = context.identity if identity is None: - raise ValueError("ClaimsIdentity is required on TurnContext for OAuth flow.") + raise ValueError( + "ClaimsIdentity is required on TurnContext for OAuth flow." + ) ms_app_id = identity.claims["aud"] # try to load existing state From f1ec07a772f02e15516d08a3d624d07daa72fa79 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 10:54:27 -0700 Subject: [PATCH 09/21] Back compat fixes --- .../hosting/core/channel_adapter.py | 4 +++- .../hosting/core/channel_service_adapter.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py index 53f7596d7..b2df73aaf 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py @@ -19,8 +19,10 @@ class ChannelAdapter(ABC, ChannelAdapterProtocol): - OAUTH_SCOPE_KEY = "Microsoft.Agents.Builder.ChannelAdapter.OAuthScope" + AGENT_IDENTITY_KEY = "AgentIdentity" + USER_TOKEN_CLIENT_KEY = "UserTokenClient" INVOKE_RESPONSE_KEY = "ChannelAdapter.InvokeResponse" + OAUTH_SCOPE_KEY = "Microsoft.Agents.Builder.ChannelAdapter.OAuthScope" on_turn_error: Callable[[TurnContext, Exception], Awaitable] | None = None diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index de020a623..00e744d94 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -40,6 +40,7 @@ class ChannelServiceAdapter(ChannelAdapter, ABC): + _AGENT_CONNECTOR_CLIENT_KEY = "ConnectorClient" def __init__(self, channel_service_client_factory: ChannelServiceClientFactoryBase): """ @@ -293,6 +294,9 @@ async def create_conversation( # pylint: disable=arguments-differ create_activity, ) context.services.set(ConnectorClientBase, connector_client) + context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = ( + connector_client # for back-compat + ) # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) user_token_client = ( @@ -301,6 +305,9 @@ async def create_conversation( # pylint: disable=arguments-differ ) ) context.services.set(UserTokenClientBase, user_token_client) + context.turn_state[self.USER_TOKEN_CLIENT_KEY] = ( + user_token_client # for back-compat + ) # Run the pipeline await self.run_pipeline(context, callback) @@ -329,6 +336,9 @@ async def process_proactive( ) ) context.services.set(UserTokenClientBase, user_token_client) + context.turn_state[self.USER_TOKEN_CLIENT_KEY] = ( + user_token_client # for back-compat + ) # Create the connector client to use for outbound requests. connector_client: ConnectorClient = ( @@ -337,6 +347,9 @@ async def process_proactive( ) ) context.services.set(ConnectorClientBase, connector_client) + context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = ( + connector_client # for back-compat + ) # Run the pipeline await self.run_pipeline(context, callback) @@ -414,6 +427,9 @@ async def process_activity( ) ) context.services.set(UserTokenClientBase, user_token_client) + context.turn_state[self.USER_TOKEN_CLIENT_KEY] = ( + user_token_client # for back-compat + ) # Create the connector client to use for outbound requests. connector_client: Optional[ConnectorClient] = None @@ -429,6 +445,9 @@ async def process_activity( ) ) context.services.set(ConnectorClientBase, connector_client) + context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = ( + connector_client # for back-compat + ) await self.run_pipeline(context, callback) @@ -501,6 +520,7 @@ def _create_turn_context( ) -> TurnContext: context = TurnContext(self, activity, claims_identity) context.turn_state[self.OAUTH_SCOPE_KEY] = oauth_scope + context.turn_state[self.AGENT_IDENTITY_KEY] = claims_identity # for back-compat return context def _process_turn_results(self, context: TurnContext) -> Optional[InvokeResponse]: From 025f19b61c22a53bede0433c340c08d8a8ea2f74 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 10:57:18 -0700 Subject: [PATCH 10/21] Another commit --- .../hosting/core/channel_service_adapter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 00e744d94..ec1cd240f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -269,7 +269,7 @@ async def create_conversation( # pylint: disable=arguments-differ claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url # Create the connector client to use for outbound requests. - connector_client: ConnectorClient = ( + connector_client = ( await self._channel_service_client_factory.create_connector_client( None, claims_identity, service_url, audience ) @@ -330,7 +330,7 @@ async def process_proactive( activity=continuation_activity, ) - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity ) @@ -341,7 +341,7 @@ async def process_proactive( ) # Create the connector client to use for outbound requests. - connector_client: ConnectorClient = ( + connector_client = ( await self._channel_service_client_factory.create_connector_client( context, claims_identity, continuation_activity.service_url, audience ) @@ -421,7 +421,7 @@ async def process_activity( ) # Create a UserTokenClient instance for the OAuth flow. - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity, use_anonymous_auth_callback ) From f29f3b63486150172ae9f759df27b58e3489690f Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 11:08:12 -0700 Subject: [PATCH 11/21] Addressing PR feedback --- .../hosting/core/_utils/_service_set.py | 12 +++++------- tests/hosting_core/test_service_set.py | 4 ++-- tests/hosting_msteams/test_internal.py | 10 +++++----- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index f0febcf20..a34154fc4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -14,7 +14,7 @@ class _ServiceSet: """ def __init__(self, service_set: _ServiceSet | None = None) -> None: - self._state: dict[str, Any] = {} + self._state: dict[type, Any] = {} if service_set is not None: self._state.update(service_set._state) @@ -24,13 +24,11 @@ def get(self, key: type[T]) -> T | None: :param key: :return: """ - lookup_key = key.__name__ - - val = self._state.get(lookup_key) + val = self._state.get(key) if val is not None: if not isinstance(val, key): raise TypeError( - f"Value for key '{lookup_key}' is not of type {key.__name__}" + f"Value for key '{key.__name__}' is not of type {type(val).__name__}" ) return cast(T, val) return None @@ -41,7 +39,7 @@ def has(self, key: type) -> bool: :param key: Type of the value to check for. :return: True if the value exists, False otherwise. """ - return key.__name__ in self._state + return key in self._state def set(self, key: type[T], value: T) -> None: """ @@ -49,4 +47,4 @@ def set(self, key: type[T], value: T) -> None: :param key: Type of the value to set. :param value: The value to set. """ - self._state[key.__name__] = value + self._state[key] = value diff --git a/tests/hosting_core/test_service_set.py b/tests/hosting_core/test_service_set.py index d65ea5ce9..f89fd273b 100644 --- a/tests/hosting_core/test_service_set.py +++ b/tests/hosting_core/test_service_set.py @@ -70,9 +70,9 @@ def test_copy_constructor_does_not_share_state_dictionary(): def test_get_raises_type_error_when_stored_value_does_not_match_key(): services = _ServiceSet() - services._state[Service.__name__] = OtherService() + services._state[Service] = OtherService() with pytest.raises( - TypeError, match="Value for key 'Service' is not of type Service" + TypeError, match="Value for key 'Service' is not of type OtherService" ): services.get(Service) diff --git a/tests/hosting_msteams/test_internal.py b/tests/hosting_msteams/test_internal.py index 485597e88..1ea3a2f97 100644 --- a/tests/hosting_msteams/test_internal.py +++ b/tests/hosting_msteams/test_internal.py @@ -24,11 +24,11 @@ class _FakeServices: - def __init__(self, value=None): - self._value = value + def __init__(self, values=None): + self._values = values or {} def get(self, key): - return self._value + return self._values.get(key) class _FakeContext: @@ -42,7 +42,7 @@ class TestGetTeamsApiClient: def test_returns_cached_api_client(self): client = ApiClient("https://smba.trafficmanager.net/teams/") - ctx = _FakeContext(_FakeServices(client)) + ctx = _FakeContext(_FakeServices({ApiClient: client})) assert _get_teams_api_client(ctx) is client def test_raises_when_missing(self): @@ -51,7 +51,7 @@ def test_raises_when_missing(self): _get_teams_api_client(ctx) def test_raises_when_wrong_type(self): - ctx = _FakeContext(_FakeServices(object())) + ctx = _FakeContext(_FakeServices({ApiClient: object()})) with pytest.raises(ValueError, match="Teams API client"): _get_teams_api_client(ctx) From d5b784506bf45f9a24b378b34bed5d8355fc9d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Wed, 22 Jul 2026 11:24:25 -0700 Subject: [PATCH 12/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../microsoft_agents/hosting/core/_utils/_service_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index a34154fc4..a80fe21cc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -28,7 +28,7 @@ def get(self, key: type[T]) -> T | None: if val is not None: if not isinstance(val, key): raise TypeError( - f"Value for key '{key.__name__}' is not of type {type(val).__name__}" + f"Value for key '{key.__name__}' is not of type {key.__name__} (got {type(val).__name__})" ) return cast(T, val) return None From 10461d5e9572cfcd6d1faea9106b9abf564ca7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Wed, 22 Jul 2026 11:24:36 -0700 Subject: [PATCH 13/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/hosting_core/test_service_set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/hosting_core/test_service_set.py b/tests/hosting_core/test_service_set.py index f89fd273b..dc9620e69 100644 --- a/tests/hosting_core/test_service_set.py +++ b/tests/hosting_core/test_service_set.py @@ -73,6 +73,7 @@ def test_get_raises_type_error_when_stored_value_does_not_match_key(): services._state[Service] = OtherService() with pytest.raises( - TypeError, match="Value for key 'Service' is not of type OtherService" + TypeError, + match="Value for key 'Service' is not of type Service \(got OtherService\)", ): services.get(Service) From f5f4140ccfa08ea8edbe36adbdbd8bc02d272b18 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 11:26:30 -0700 Subject: [PATCH 14/21] Fixing issue in test --- tests/hosting_core/test_service_set.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/hosting_core/test_service_set.py b/tests/hosting_core/test_service_set.py index dc9620e69..6e9278a55 100644 --- a/tests/hosting_core/test_service_set.py +++ b/tests/hosting_core/test_service_set.py @@ -74,6 +74,5 @@ def test_get_raises_type_error_when_stored_value_does_not_match_key(): with pytest.raises( TypeError, - match="Value for key 'Service' is not of type Service \(got OtherService\)", ): services.get(Service) From 9108cd45530d4d7ea5c22ded4f832759a6a67d31 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 11:37:59 -0700 Subject: [PATCH 15/21] Fixing integration test issue --- .../testing/activity_handler_scenario.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py index 8c405c7cb..973454ab5 100644 --- a/dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py +++ b/dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py @@ -16,7 +16,10 @@ from aiohttp.web import Application, Request, Response, middleware from aiohttp.test_utils import TestServer +from dotenv import dotenv_values +from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.authentication.msal import MsalConnectionManager from microsoft_agents.hosting.core import ( ActivityHandler, ConversationState, @@ -44,7 +47,7 @@ class ActivityHandlerEnvironment: storage: In-memory state storage shared by all state objects. conversation_state: Conversation-scoped state accessor. user_state: User-scoped state accessor. - adapter: CloudAdapter instance (anonymous auth, no real credentials). + adapter: CloudAdapter instance configured from the scenario environment. handler: The ActivityHandler instance under test. """ @@ -60,8 +63,9 @@ class ActivityHandlerScenario(Scenario): Use this scenario when your agent extends ``ActivityHandler`` rather than ``AgentApplication``. The scenario creates ``MemoryStorage``, - ``ConversationState``, ``UserState``, and a ``CloudAdapter`` (no auth), then - wires them up and hosts the handler on an ephemeral aiohttp test server. + ``ConversationState``, ``UserState``, and a ``CloudAdapter`` backed by the + configured service connection, then wires them up and hosts the handler on + an ephemeral aiohttp test server. Example:: @@ -104,10 +108,14 @@ def environment(self) -> ActivityHandlerEnvironment: async def _setup(self) -> None: """Create storage, state objects, adapter, and handler.""" + env_vars = dotenv_values(self._config.env_file_path or ".env") + sdk_config = load_configuration_from_env(env_vars) + storage = MemoryStorage() conv_state = ConversationState(storage) user_state = UserState(storage) - adapter = CloudAdapter() + connection_manager = MsalConnectionManager(**sdk_config) + adapter = CloudAdapter(connection_manager=connection_manager) result = self._create_handler(conv_state, user_state, storage) if hasattr(result, "__await__"): From 38187b760bab6f7a3ba240ea263e8e8a103044ec Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 11:42:17 -0700 Subject: [PATCH 16/21] Fixing test mock object --- tests/hosting_dialogs/helpers.py | 3 +++ tests/hosting_slack/test_slack_agent_extension.py | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/hosting_dialogs/helpers.py b/tests/hosting_dialogs/helpers.py index 87fb34a09..87529bf28 100644 --- a/tests/hosting_dialogs/helpers.py +++ b/tests/hosting_dialogs/helpers.py @@ -116,6 +116,9 @@ def __init__(self): ) self.agent_sign_in = _MockAgentSignIn() + async def close(self) -> None: + return None + def add_user_token( self, connection_name: str, diff --git a/tests/hosting_slack/test_slack_agent_extension.py b/tests/hosting_slack/test_slack_agent_extension.py index 20941277e..f979b3d86 100644 --- a/tests/hosting_slack/test_slack_agent_extension.py +++ b/tests/hosting_slack/test_slack_agent_extension.py @@ -13,7 +13,7 @@ from microsoft_agents.activity import Activity, ActivityTypes from microsoft_agents.hosting.core import TurnContext from microsoft_agents.hosting.core.app import AgentApplication, RouteRank -from microsoft_agents.hosting.slack import SlackAgentExtension +from microsoft_agents.hosting.slack import SlackAgentExtension, SlackApi def _make_app() -> MagicMock: @@ -52,7 +52,8 @@ def _make_context( context = MagicMock(spec=TurnContext) context.activity = activity context.send_activity = AsyncMock() - context.has.return_value = False + context.services = MagicMock() + context.services.has.return_value = False return context @@ -157,10 +158,12 @@ async def test_call_prefers_turn_context_service_when_present(self): slack = SlackAgentExtension(app, slack_api=default_api) ctx = _make_context(ActivityTypes.message) - ctx.has.return_value = True - ctx.get.return_value = per_turn_api + ctx.services.has.return_value = True + ctx.services.get.return_value = per_turn_api out = await slack.call(ctx, "auth.test") assert out == "per-turn" + ctx.services.has.assert_called_once_with(SlackApi) + ctx.services.get.assert_called_once_with(SlackApi) per_turn_api.call.assert_awaited_once() default_api.call.assert_not_awaited() From 043d34a40c2708b1b614ebe150d0db7043f39dc2 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 11:44:32 -0700 Subject: [PATCH 17/21] More test fixes --- tests/hosting_slack/test_slack_agent_extension.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/hosting_slack/test_slack_agent_extension.py b/tests/hosting_slack/test_slack_agent_extension.py index f979b3d86..13a048bbb 100644 --- a/tests/hosting_slack/test_slack_agent_extension.py +++ b/tests/hosting_slack/test_slack_agent_extension.py @@ -13,7 +13,8 @@ from microsoft_agents.activity import Activity, ActivityTypes from microsoft_agents.hosting.core import TurnContext from microsoft_agents.hosting.core.app import AgentApplication, RouteRank -from microsoft_agents.hosting.slack import SlackAgentExtension, SlackApi +from microsoft_agents.hosting.slack import SlackAgentExtension +from microsoft_agents.hosting.slack.api import SlackApi def _make_app() -> MagicMock: From 838d058a818354303fefa69227706a531c05bff3 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 14:14:14 -0700 Subject: [PATCH 18/21] Adding missing __init__ doscstring --- .../microsoft_agents/hosting/core/_utils/_service_set.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index a80fe21cc..f99956249 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -14,6 +14,11 @@ class _ServiceSet: """ def __init__(self, service_set: _ServiceSet | None = None) -> None: + """ + Initializes a new instance of the _ServiceSet class. + + :param service_set: An optional _ServiceSet instance to copy the state from. + """ self._state: dict[type, Any] = {} if service_set is not None: self._state.update(service_set._state) From d43c7c166115eee773128cfd38b9f261d316b283 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 14:26:28 -0700 Subject: [PATCH 19/21] Completing unfinished Protocol definition --- .../hosting/core/connector/connector_client_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py index 943b752a2..c324db296 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py @@ -24,3 +24,8 @@ def attachments(self) -> AttachmentsBase: @abstractmethod def conversations(self) -> ConversationsBase: pass + + @abstractmethod + async def close(self) -> None: + """Close the client and release any resources.""" + raise NotImplementedError("close method must be implemented by subclasses.") \ No newline at end of file From 6133106c969d0ff91b81ff41e249c86d196e1120 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 22 Jul 2026 14:26:40 -0700 Subject: [PATCH 20/21] Completing unfinished Protocol definition --- .../hosting/core/connector/connector_client_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py index c324db296..5daf26a07 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py @@ -28,4 +28,4 @@ def conversations(self) -> ConversationsBase: @abstractmethod async def close(self) -> None: """Close the client and release any resources.""" - raise NotImplementedError("close method must be implemented by subclasses.") \ No newline at end of file + raise NotImplementedError("close method must be implemented by subclasses.") From f1ba3d385f4cc78fa2b6e00be84c1f1757151d17 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 24 Jul 2026 14:05:42 -0700 Subject: [PATCH 21/21] Addressing PR feedback --- .../microsoft_agents/hosting/core/_utils/_service_set.py | 2 +- .../hosting/core/channel_service_adapter.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py index f99956249..492114af9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_utils/_service_set.py @@ -10,7 +10,7 @@ class _ServiceSet: """ - Analog of .NET's TurnContextStateCollection + A class that represents a collection of services, allowing for the storage and retrieval of service instances by their type. """ def __init__(self, service_set: _ServiceSet | None = None) -> None: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index e9f0186c5..f8fa4f0b9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -137,9 +137,6 @@ async def update_activity(self, context: TurnContext, activity: Activity): raise RuntimeError( "Unable to extract ConnectorClient from turn context." ) - raise RuntimeError( - "Unable to extract ConnectorClient from turn context." - ) return await connector_client.conversations.update_activity( activity.conversation.id, activity.id, activity @@ -169,9 +166,6 @@ async def delete_activity( raise RuntimeError( "Unable to extract ConnectorClient from turn context." ) - raise RuntimeError( - "Unable to extract ConnectorClient from turn context." - ) await connector_client.conversations.delete_activity( reference.conversation.id, reference.activity_id