diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index d4d6ee6ea..edd17d2e2 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -10,6 +10,7 @@ from contextlib import nullcontext from copy import copy from functools import partial +from typing_extensions import deprecated import re from typing import ( @@ -17,7 +18,6 @@ Awaitable, Callable, Generic, - Optional, TypeVar, cast, overload, @@ -74,22 +74,22 @@ class AgentApplication(Agent, Generic[StateT]): typing: TypingIndicator _options: ApplicationOptions - _adapter: Optional[ChannelServiceAdapter] = None + _adapter: ChannelServiceAdapter | None = None _auth: Authorization - _proactive: Optional[Proactive] = None + _proactive: Proactive | None = None _internal_before_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] _internal_after_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] _route_list: _RouteList[StateT] - _error: Optional[Callable[[TurnContext, Exception], Awaitable[None]]] = None - _turn_state_factory: Optional[Callable[[TurnContext], StateT]] = None + _error: Callable[[TurnContext, Exception], Awaitable[None]] | None = None + _turn_state_factory: Callable[[], StateT] | None = None _connection_manager: Connections def __init__( self, - options: Optional[ApplicationOptions] = None, + options: ApplicationOptions | None = None, *, - connection_manager: Optional[Connections] = None, - authorization: Optional[Authorization] = None, + connection_manager: Connections | None = None, + authorization: Authorization | None = None, **kwargs, ) -> None: """ @@ -131,6 +131,7 @@ def __init__( raise ApplicationError(""" The `ApplicationOptions.storage` property is required and was not configured. """) + self._storage = self._options.storage if options.long_running_messages and ( not options.adapter or not options.bot_app_id @@ -150,13 +151,13 @@ def __init__( self._turn_state_factory = ( options.turn_state_factory or kwargs.get("turn_state_factory", None) - or partial(TurnState.with_storage, self._options.storage) + or partial(TurnState.with_storage, self._storage) ) if options.proactive: proactive_opts = copy(options.proactive) if not proactive_opts.storage: - proactive_opts.storage = self._options.storage + proactive_opts.storage = self._storage self._proactive = Proactive(self, proactive_opts) # TODO: decide how to initialize the Authorization (params vs options vs kwargs) @@ -186,7 +187,7 @@ def __init__( if key not in ["storage", "connection_manager", "handlers"] } self._auth = Authorization( - storage=self._options.storage, + storage=self._storage, connection_manager=connection_manager, auth_handlers=options.authorization_handlers, **auth_options, @@ -793,7 +794,7 @@ async def on_error(context: TurnContext, err: Exception): return func - def turn_state_factory(self, func: Callable[[TurnContext], Awaitable[StateT]]): + def set_turn_state_factory(self, func: Callable[[], StateT]): """ Custom Turn State Factory """ @@ -873,6 +874,9 @@ def _remove_mentions(self, context: TurnContext): context.activity.text = context.remove_recipient_mention(context.activity) @staticmethod + @deprecated( + "Use `load_configuration_from_env` from `microsoft_agents.activity` instead." + ) def parse_env_vars_configuration(vars: dict[str, Any]) -> dict: """ Parses environment variables and returns a dictionary with the relevant configuration. @@ -908,12 +912,12 @@ async def _initialize_state(self, context: TurnContext) -> StateT: turn_state = self._turn_state_factory() else: logger.debug("Using default turn state factory") - turn_state = TurnState.with_storage(self._options.storage) + turn_state = TurnState.with_storage(self._storage) turn_state = cast(StateT, turn_state) logger.debug("Loading turn state from storage") - await turn_state.load(context, self._options.storage) + await turn_state.load(context, self._storage) turn_state.temp.input = context.activity.text return turn_state diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index 542b74cb1..99b9760ad 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -59,6 +59,10 @@ def __init__( if auth_handler: self._handler = auth_handler else: + if not auth_handler_settings: + raise ValueError( + "auth_handler_settings must be provided if auth_handler is None." + ) self._handler = AuthHandler._from_settings(auth_handler_settings) self._id = auth_handler_id or self._handler.name 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..f42231e9f 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 @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import Optional +from typing import cast from microsoft_agents.activity import ( Activity, @@ -24,6 +24,8 @@ from microsoft_agents.activity.token_exchange_invoke_response import ( TokenExchangeInvokeResponse, ) + +from microsoft_agents.hosting.core.authorization import ClaimsIdentity 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 @@ -65,9 +67,14 @@ 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 = cast( + UserTokenClient | None, + context.turn_state.get(context.adapter.USER_TOKEN_CLIENT_KEY), ) + if not user_token_client: + raise ValueError( + "UserTokenClient is required in TurnState for OAuth flow handling." + ) if ( not context.activity.channel_id @@ -79,14 +86,21 @@ 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 = cast( + ClaimsIdentity | None, + context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY), + ) + if not identity or "aud" not in identity.claims: + raise ValueError( + "ClaimsIdentity with 'aud' claim is required in TurnState for OAuth flow handling." + ) + + ms_app_id = identity.claims["aud"] # try to load existing state flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage) logger.info("Loading OAuth flow state from storage") - flow_state: _FlowState = await flow_storage_client.read(self._id) + flow_state: _FlowState | None = await flow_storage_client.read(self._id) if not flow_state: logger.info("No existing flow state found, creating new flow state") flow_state = _FlowState( @@ -105,8 +119,8 @@ async def _handle_obo( self, context: TurnContext, input_token_response: TokenResponse, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """ Exchanges a token for another token with different scopes. @@ -244,8 +258,8 @@ async def _handle_flow_response( async def _sign_in( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> _SignInResponse: """Begins or continues an OAuth flow. @@ -296,17 +310,17 @@ async def _sign_in( async def get_refreshed_token( self, context: TurnContext, - exchange_connection: Optional[str] = None, - exchange_scopes: Optional[list[str]] = None, + exchange_connection: str | None = None, + exchange_scopes: list[str] | None = None, ) -> TokenResponse: """Attempts to get a refreshed token for the user with the given scopes :param context: The turn context for the current turn of conversation. :type context: TurnContext :param exchange_connection: Optional name of the connection to use for token exchange. If None, default connection will be used. - :type exchange_connection: Optional[str], Optional + :type exchange_connection: str | None, Optional :param exchange_scopes: Optional list of scopes to request during token exchange. If None, default scopes will be used. - :type exchange_scopes: Optional[list[str]], Optional + :type exchange_scopes: list[str] | None, Optional """ flow, _ = await self._load_flow(context) input_token_response = await flow.get_user_token() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py index 548edaab1..5b1dafd05 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py @@ -75,8 +75,19 @@ async def get_agentic_instance_token(self, context: TurnContext) -> TokenRespons ) agentic_instance_id = context.activity.get_agentic_instance_id() assert agentic_instance_id + + tenant_id = context.activity.get_agentic_tenant_id() + if not tenant_id: + logger.error( + "Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Instance ID: %s", + agentic_instance_id, + ) + raise ValueError( + f"Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Instance ID: {agentic_instance_id}" + ) + instance_token, _ = await connection.get_agentic_instance_token( - context.activity.get_agentic_tenant_id(), agentic_instance_id + tenant_id, agentic_instance_id ) return ( TokenResponse(token=instance_token) if instance_token else TokenResponse() @@ -131,8 +142,18 @@ async def get_agentic_user_token( f"Unable to retrieve agentic user token: missing agentic User Id or agentic instance Id. agentic_user_id: {agentic_user_id}, Agentic Instance ID: {agentic_instance_id}" ) + tenant_id = context.activity.get_agentic_tenant_id() + if not tenant_id: + logger.error( + "Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Instance ID: %s", + agentic_instance_id, + ) + raise ValueError( + f"Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Instance ID: {agentic_instance_id}" + ) + token = await connection.get_agentic_user_token( - context.activity.get_agentic_tenant_id(), + tenant_id, agentic_instance_id, agentic_user_id, scopes, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py index 61f838f77..0e57433a0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py @@ -196,7 +196,7 @@ async def _handle_obo( scopes=scopes, user_assertion=input_token_response.token, ) - return TokenResponse(token=token) if token else None + return TokenResponse(token=token) if token else TokenResponse() def _create_token_response(self, context: TurnContext) -> TokenResponse: """ diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py index 4466b98b1..1e2718da0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py @@ -1,10 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from __future__ import annotations + import uuid import asyncio import logging -from typing import Optional, Callable, Literal, cast +from typing import Optional, Callable, Literal, cast, TYPE_CHECKING from microsoft_agents.activity import ( Activity, @@ -24,6 +26,9 @@ from .citation import Citation from .citation_util import CitationUtil +if TYPE_CHECKING: + from microsoft_agents.hosting.core.turn_context import TurnContext + logger = logging.getLogger(__name__) @@ -39,7 +44,7 @@ class StreamingResponse: Once `end_stream()` is called, the stream is considered ended and no further updates can be sent. """ - def __init__(self, context: "TurnContext"): + def __init__(self, context: TurnContext): """ Creates a new StreamingResponse instance. @@ -267,7 +272,7 @@ async def wait_for_queue(self) -> None: if self._queue_sync: await self._queue_sync - def _set_defaults(self, context: "TurnContext"): + def _set_defaults(self, context: TurnContext): channel = ( context.activity.channel_id.channel if context.activity.channel_id else None 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..6d9c8c989 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 @@ -227,8 +227,8 @@ async def test_get_refreshed_token_handles_obo_failure( context, "some_connection", ["scope1"] ) - # Should return None when OBO fails - assert token_response is None + # Should return an empty TokenResponse when OBO fails + assert token_response == TokenResponse() @pytest.mark.asyncio async def test_sign_out_is_noop(