Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bb78cfb
TurnContext.services refactor
rodrigobr-msft Jul 22, 2026
f732b40
another commit
rodrigobr-msft Jul 22, 2026
4664b3b
another commit
rodrigobr-msft Jul 22, 2026
ebdc3ea
Potential fix for pull request finding
rodrigobr-msft Jul 22, 2026
0cba44e
Potential fix for pull request finding
rodrigobr-msft Jul 22, 2026
0846d05
Potential fix for pull request finding
rodrigobr-msft Jul 22, 2026
14d775e
Formatting with black
rodrigobr-msft Jul 22, 2026
5451499
Merge branch 'users/robrandao/tc-services' of https://github.com/micr…
rodrigobr-msft Jul 22, 2026
b09c847
Formatting with black
rodrigobr-msft Jul 22, 2026
f1ec07a
Back compat fixes
rodrigobr-msft Jul 22, 2026
025f19b
Another commit
rodrigobr-msft Jul 22, 2026
74c899d
Resolving merge conflicts
rodrigobr-msft Jul 22, 2026
f29f3b6
Addressing PR feedback
rodrigobr-msft Jul 22, 2026
d5b7845
Potential fix for pull request finding
rodrigobr-msft Jul 22, 2026
10461d5
Potential fix for pull request finding
rodrigobr-msft Jul 22, 2026
f5f4140
Fixing issue in test
rodrigobr-msft Jul 22, 2026
9108cd4
Fixing integration test issue
rodrigobr-msft Jul 22, 2026
38187b7
Fixing test mock object
rodrigobr-msft Jul 22, 2026
043d34a
More test fixes
rodrigobr-msft Jul 22, 2026
838d058
Adding missing __init__ doscstring
rodrigobr-msft Jul 22, 2026
d85dd34
Addressing merge conflicts
rodrigobr-msft Jul 22, 2026
d43c7c1
Completing unfinished Protocol definition
rodrigobr-msft Jul 22, 2026
6133106
Completing unfinished Protocol definition
rodrigobr-msft Jul 22, 2026
f1ba3d3
Addressing PR feedback
rodrigobr-msft Jul 24, 2026
aaf61ad
Fixing merge conflict
rodrigobr-msft Jul 24, 2026
18b0391
Merge branch 'main' into users/robrandao/tc-services
rodrigobr-msft Jul 24, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
rodrigobr-msft marked this conversation as resolved.
from microsoft_agents.hosting.core import (
ActivityHandler,
ConversationState,
Expand Down Expand Up @@ -47,7 +50,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.
"""

Expand All @@ -63,8 +66,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::

Expand Down Expand Up @@ -107,6 +111,9 @@ 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)

Comment on lines +114 to +116
storage = MemoryStorage()
Comment thread
rodrigobr-msft marked this conversation as resolved.
conv_state = ConversationState(storage)
user_state = UserState(storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from ._service_set import _ServiceSet

__all__ = ["_ServiceSet"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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:
"""
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:
"""
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)

def get(self, key: type[T]) -> T | None:
"""
Gets a value from the state collection.
:param key:
:return:
"""
val = self._state.get(key)
if val is not None:
if not isinstance(val, key):
raise TypeError(
f"Value for key '{key.__name__}' is not of type {key.__name__} (got {type(val).__name__})"
)
return cast(T, val)
Comment thread
rodrigobr-msft marked this conversation as resolved.
return None
Comment thread
Copilot marked this conversation as resolved.
Comment thread
rodrigobr-msft marked this conversation as resolved.

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 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] = value
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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,
Expand Down Expand Up @@ -67,13 +67,10 @@ async def _load_flow(
context and the specified auth handler.
:rtype: tuple[OAuthFlow, FlowStorageClient]
"""
user_token_client = cast(
UserTokenClient | None,
context.turn_state.get(context.adapter.USER_TOKEN_CLIENT_KEY),
)
user_token_client = context.services.get(UserTokenClientBase)
if not user_token_client:
raise ValueError(
"UserTokenClient is required in TurnState for OAuth flow handling."
"UserTokenClientBase service is not available in the context"
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

if (
Expand All @@ -86,15 +83,11 @@ async def _load_flow(
channel_id = context.activity.channel_id
user_id = context.activity.from_property.id

identity = cast(
ClaimsIdentity | None,
context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY),
)
if not identity or "aud" not in identity.claims:
identity = context.identity
if identity is None:
raise ValueError(
"ClaimsIdentity with 'aud' claim is required in TurnState for OAuth flow handling."
"ClaimsIdentity is required on TurnContext for OAuth flow."
)

ms_app_id = identity.claims["aud"]

Comment thread
rodrigobr-msft marked this conversation as resolved.
# try to load existing state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@

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"
INVOKE_RESPONSE_KEY = "ChannelAdapter.InvokeResponse"
OAUTH_SCOPE_KEY = "Microsoft.Agents.Builder.ChannelAdapter.OAuthScope"

on_turn_error: Callable[[TurnContext, Exception], Awaitable] | None = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from abc import ABC
from http import HTTPStatus
from typing import Awaitable, Callable, cast
from typing import Awaitable, Callable, Optional
from uuid import uuid4

from microsoft_agents.activity import (
Expand All @@ -27,6 +27,7 @@
from microsoft_agents.hosting.core.connector import (
ConnectorClientBase,
ConnectorClient,
UserTokenClientBase,
UserTokenClient,
)
from microsoft_agents.hosting.core.authorization import (
Expand Down Expand Up @@ -85,10 +86,7 @@ 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 RuntimeError(
"Unable to extract ConnectorClient from turn context."
Expand Down Expand Up @@ -134,10 +132,7 @@ 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 RuntimeError(
"Unable to extract ConnectorClient from turn context."
Expand Down Expand Up @@ -166,10 +161,7 @@ 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 RuntimeError(
"Unable to extract ConnectorClient from turn context."
Expand Down Expand Up @@ -263,7 +255,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
)
Expand All @@ -285,18 +277,23 @@ 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)
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: 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)
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = (
user_token_client # for back-compat
)

# Run the pipeline
await self.run_pipeline(context, callback)
Expand All @@ -316,24 +313,29 @@ async def process_proactive(
context = self._create_turn_context(
claims_identity,
audience,
callback,
activity=continuation_activity,
)

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)
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 = (
connector_client = (
await self._channel_service_client_factory.create_connector_client(
context, claims_identity, continuation_activity.service_url, audience
)
)
context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client
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)
Expand Down Expand Up @@ -401,20 +403,22 @@ async def process_activity(
context = self._create_turn_context(
claims_identity,
outgoing_audience,
callback,
activity=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
)
)
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
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 | None = None
connector_client: ConnectorClientBase | None = None
if self._resolve_if_connector_client_is_needed(activity):
connector_client = (
await self._channel_service_client_factory.create_connector_client(
Expand All @@ -426,7 +430,10 @@ async def process_activity(
use_anonymous_auth_callback,
)
)
context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client
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)

Expand Down Expand Up @@ -492,19 +499,12 @@ def _create_create_activity(
def _create_turn_context(
self,
claims_identity: ClaimsIdentity,
oauth_scope: str,
callback: Callable[[TurnContext], Awaitable],
activity: Activity | None = None,
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

context.turn_state[self.AGENT_IDENTITY_KEY] = claims_identity # for back-compat
return context

def _process_turn_results(self, context: TurnContext) -> InvokeResponse | None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -20,4 +22,5 @@
"MCSConnectorClient",
"ConnectorClientBase",
"get_product_info",
"UserTokenClientBase",
]
Loading
Loading