Skip to content
Closed
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,52 @@
# 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:
Comment thread
Copilot marked this conversation as resolved.
"""
Analog of .NET's TurnContextStateCollection
"""

def __init__(self, service_set: _ServiceSet | None = None) -> None:
self._state: dict[str, Any] = {}
Comment thread
rodrigobr-msft marked this conversation as resolved.
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

if (
not context.activity.channel_id
Expand All @@ -79,9 +81,12 @@ 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"
]
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.identity
reference = context.activity.get_conversation_reference()
return cls(identity or {}, reference)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,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"

Comment on lines 21 to 26
on_turn_error: Callable[[TurnContext, Exception], Awaitable] | None = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -27,6 +26,7 @@
from microsoft_agents.hosting.core.connector import (
ConnectorClientBase,
ConnectorClient,
UserTokenClientBase,
UserTokenClient,
)
from microsoft_agents.hosting.core.authorization import (
Expand Down Expand Up @@ -91,12 +91,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."
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

with spans.AdapterSendActivities([activity]):
if activity.reply_to_id:
Expand Down Expand Up @@ -140,12 +139,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."
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

return await connector_client.conversations.update_activity(
activity.conversation.id, activity.id, activity
Expand All @@ -171,12 +169,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."
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

await connector_client.conversations.delete_activity(
reference.conversation.id, reference.activity_id
Expand Down Expand Up @@ -294,18 +291,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 @@ -325,7 +327,6 @@ async def process_proactive(
context = self._create_turn_context(
claims_identity,
audience,
callback,
activity=continuation_activity,
)

Expand All @@ -334,15 +335,21 @@ 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)
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 = (
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 @@ -410,7 +417,6 @@ async def process_activity(
context = self._create_turn_context(
claims_identity,
outgoing_audience,
callback,
activity=activity,
)

Expand All @@ -420,7 +426,10 @@ 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)
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
Expand All @@ -435,7 +444,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 @@ -503,19 +515,12 @@ 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

context.turn_state[self.AGENT_IDENTITY_KEY] = claims_identity # for back-compat
return context
Comment thread
rodrigobr-msft marked this conversation as resolved.

def _process_turn_results(self, context: TurnContext) -> Optional[InvokeResponse]:
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",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@
# 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
@abstractmethod
def agent_sign_in(self) -> AgentSignInBase:
pass
raise NotImplementedError(
"agent_sign_in property must be implemented by subclasses."
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

@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.")
Loading