diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_type_aliases.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_type_aliases.py index a792b195..b0952c5e 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_type_aliases.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_type_aliases.py @@ -1,5 +1,4 @@ from typing import Annotated from pydantic import StringConstraints - NonEmptyString = Annotated[str, StringConstraints(min_length=1)] diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index 78e8ac7d..a573e10a 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -429,7 +429,6 @@ def create_reply(self, text: str = None, locale: str = None): def create_trace( self, name: str, value: object = None, value_type: str = None, label: str = None ): - # robrandao: TODO -> needs to handle Nones like create_reply """ Creates a new trace activity based on this activity. @@ -476,7 +475,6 @@ def create_trace( def create_trace_activity( name: str, value: object = None, value_type: str = None, label: str = None ): - # robrandao: TODO -> SkipNone """ Creates an instance of the :class:`Activity` class as a TraceActivity object. diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index abeb718c..2f0d34d9 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from __future__ import annotations import logging diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py index aea4163a..a16ee180 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_connection_manager.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import re from typing import Dict, List, Optional from microsoft_agents.hosting.core import ( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py index 50c990c8..90d6f0ec 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/__init__.py @@ -11,12 +11,13 @@ from .turn_context import TurnContext # Application Style +from .app._type_defs import RouteHandler, RouteSelector, StateT from .app.agent_application import AgentApplication from .app.app_error import ApplicationError from .app.app_options import ApplicationOptions from .app.input_file import InputFile, InputFileDownloader from .app.query import Query -from .app.route import Route, RouteHandler +from .app._routes import _Route, _RouteList, RouteRank from .app.typing_indicator import TypingIndicator # App Auth diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/__init__.py index c9b319e6..de88c8b4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from ._flow_state import _FlowState, _FlowStateTag, _FlowErrorTag from ._flow_storage_client import _FlowStorageClient from ._oauth_flow import _OAuthFlow, _FlowResponse diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/activity_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/activity_handler.py index 13a641b0..483cb6e9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/activity_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/activity_handler.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from __future__ import annotations from http import HTTPStatus from pydantic import BaseModel diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py index 0cf00fc4..2751bf41 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/__init__.py @@ -10,8 +10,9 @@ from .app_options import ApplicationOptions from .input_file import InputFile, InputFileDownloader from .query import Query -from .route import Route, RouteHandler +from ._routes import _RouteList, _Route, RouteRank from .typing_indicator import TypingIndicator +from ._type_defs import RouteHandler, RouteSelector, StateT # Auth from .oauth import ( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/__init__.py new file mode 100644 index 00000000..1429cdfd --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ._route_list import _RouteList +from ._route import _Route, _agentic_selector +from .route_rank import RouteRank + +__all__ = [ + "_RouteList", + "_Route", + "RouteRank", + "_agentic_selector", +] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route.py new file mode 100644 index 00000000..dfdd9719 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import annotations + +from typing import Generic, Optional, TypeVar + +from ...turn_context import TurnContext +from .._type_defs import RouteHandler, RouteSelector +from ..state.turn_state import TurnState +from .route_rank import RouteRank + + +def _agentic_selector(selector: RouteSelector) -> RouteSelector: + def wrapped_selector(context: TurnContext) -> bool: + return context.activity.is_agentic_request() and selector(context) + + return wrapped_selector + + +StateT = TypeVar("StateT", bound=TurnState) + + +class _Route(Generic[StateT]): + selector: RouteSelector + handler: RouteHandler[StateT] + _is_invoke: bool + _rank: int + auth_handlers: list[str] + _is_agentic: bool + + def __init__( + self, + selector: RouteSelector, + handler: RouteHandler[StateT], + is_invoke: bool = False, + rank: int = RouteRank.DEFAULT, + auth_handlers: Optional[list[str]] = None, + is_agentic: bool = False, + **kwargs, + ) -> None: + + if rank < 0 or rank > RouteRank.LAST: + raise ValueError( + "Route rank must be between 0 and RouteRank.LAST (inclusive)" + ) + + self.selector = selector + self.handler = handler + self._is_invoke = is_invoke + self._rank = int(rank) # conversion from RouteRank IntEnum if necessary + self._is_agentic = is_agentic + self.auth_handlers = auth_handlers or [] + + @property + def is_invoke(self) -> bool: + return self._is_invoke + + @property + def rank(self) -> int: + return self._rank + + @property + def is_agentic(self) -> bool: + return self._is_agentic + + @property + def priority(self) -> list[int]: + """Lower "values" indicate higher priority. + + Priority is determined by: + 1. Whether the route is for an invoke activity (0) or not (1). + 2. Whether the route is agentic (0) or not (1). + 3. The rank of the route (lower numbers indicate higher priority). + + In that order. If both are invokes, the agentic one has higher priority. + If both are agentic and invokes, then the rank determines priority. + + priority is represented as a list of three integers for easy lexicographic comparison. + """ + return [ + 0 if self._is_invoke else 1, + 0 if self._is_agentic else 1, + self._rank, + ] + + def __lt__(self, other: _Route) -> bool: + # built-in list ordering is a lexicographic comparison in Python + return self.priority < other.priority diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route_list.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route_list.py new file mode 100644 index 00000000..8b3ec990 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/_route_list.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import annotations + +import heapq +from typing import Generic, TypeVar + +from ..state.turn_state import TurnState +from ._route import _Route + +StateT = TypeVar("StateT", bound=TurnState) + + +class _RouteList(Generic[StateT]): + _routes: list[_Route[StateT]] + + def __init__( + self, + ) -> None: + # a min-heap where lower "values" indicate higher priority + self._routes = [] + + def add_route(self, route: _Route[StateT]) -> None: + """Adds a route to the list.""" + heapq.heappush(self._routes, route) + + def __iter__(self): + # sorted will return a new list, leaving the heap intact + # returning an iterator over the previous list would expose + # internal details + return iter(sorted(self._routes)) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/route_rank.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/route_rank.py new file mode 100644 index 00000000..48c43880 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_routes/route_rank.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import IntEnum + +_MAX_RANK = 2**16 - 1 # 65,535 + + +class RouteRank(IntEnum): + """Defines the rank of a route. Lower values indicate higher priority.""" + + FIRST = 0 + DEFAULT = _MAX_RANK // 2 + LAST = _MAX_RANK diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_type_defs.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_type_defs.py new file mode 100644 index 00000000..f5ceb61a --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/_type_defs.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Callable, TypeVar, Awaitable, Protocol + +from ..turn_context import TurnContext +from .state import TurnState + +RouteSelector = Callable[[TurnContext], bool] + +StateT = TypeVar("StateT", bound=TurnState) + + +class RouteHandler(Protocol[StateT]): + def __call__(self, context: TurnContext, state: StateT) -> Awaitable[None]: ... 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 d6ad5389..0184965d 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 @@ -13,9 +13,7 @@ Any, Awaitable, Callable, - Dict, Generic, - List, Optional, Pattern, TypeVar, @@ -38,16 +36,17 @@ from .app_error import ApplicationError from .app_options import ApplicationOptions -from .route import Route, RouteHandler from .state import TurnState from ..channel_service_adapter import ChannelServiceAdapter from .oauth import Authorization from .typing_indicator import TypingIndicator +from ._type_defs import RouteHandler, RouteSelector +from ._routes import _RouteList, _Route, RouteRank, _agentic_selector + logger = logging.getLogger(__name__) StateT = TypeVar("StateT", bound=TurnState) -IN_SIGN_IN_KEY = "__InSignInFlow__" class AgentApplication(Agent, Generic[StateT]): @@ -68,25 +67,25 @@ class AgentApplication(Agent, Generic[StateT]): _options: ApplicationOptions _adapter: Optional[ChannelServiceAdapter] = None _auth: Optional[Authorization] = None - _internal_before_turn: List[Callable[[TurnContext, StateT], Awaitable[bool]]] = [] - _internal_after_turn: List[Callable[[TurnContext, StateT], Awaitable[bool]]] = [] - _routes: List[Route[StateT]] = [] + _internal_before_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] = [] + _internal_after_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] = [] + _route_list: _RouteList[StateT] = _RouteList[StateT]() _error: Optional[Callable[[TurnContext, Exception], Awaitable[None]]] = None _turn_state_factory: Optional[Callable[[TurnContext], StateT]] = None def __init__( self, - options: ApplicationOptions = None, + options: Optional[ApplicationOptions] = None, *, - connection_manager: Connections = None, - authorization: Authorization = None, + connection_manager: Optional[Connections] = None, + authorization: Optional[Authorization] = None, **kwargs, ) -> None: """ Creates a new AgentApplication instance. """ self.typing = TypingIndicator() - self._routes = [] + self._route_list = _RouteList[StateT]() configuration = kwargs @@ -204,11 +203,54 @@ def options(self) -> ApplicationOptions: """ return self._options + def add_route( + self, + selector: RouteSelector, + handler: RouteHandler[StateT], + is_invoke: bool = False, + is_agentic: bool = False, + rank: RouteRank = RouteRank.DEFAULT, + auth_handlers: Optional[list[str]] = None, + ) -> None: + """Adds a new route to the application. + + Routes are ordered by: is_agentic, is_invoke, rank (lower is higher priority), in that order. + + :param selector: A function that takes a TurnContext and returns a boolean indicating whether the route should be selected. + :type selector: RouteSelector + :param handler: A function that takes a TurnContext and a TurnState and returns an Awaitable. + :type handler: RouteHandler[StateT] + :param is_invoke: Whether the route is for an invoke activity, defaults to False + :type is_invoke: bool, optional + :param is_agentic: Whether the route is for an agentic request, defaults to False. For agentic requests + the selector will include a new check for `context.activity.is_agentic_request()`. + :type is_agentic: bool, optional + :param rank: The rank of the route, defaults to RouteRank.DEFAULT + :type rank: RouteRank, optional + :param auth_handlers: A list of authentication handler IDs to use for this route, defaults to None + :type auth_handlers: Optional[list[str]], optional + :raises ApplicationError: If the selector or handler are not valid. + """ + if not selector or not handler: + logger.error( + "AgentApplication.add_route(): selector and handler are required." + ) + raise ApplicationError("selector and handler are required.") + + if is_agentic: + selector = _agentic_selector(selector) + + route = _Route[StateT]( + selector, handler, is_invoke, rank, auth_handlers, is_agentic + ) + self._route_list.add_route(route) + def activity( self, - activity_type: Union[str, ActivityTypes, List[Union[str, ActivityTypes]]], + activity_type: Union[str, ActivityTypes, list[Union[str, ActivityTypes]]], *, - auth_handlers: Optional[List[str]] = None, + auth_handlers: Optional[list[str]] = None, + **kwargs, ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: """ Registers a new activity event listener. This method can be used as either @@ -233,18 +275,17 @@ def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( f"Registering activity handler for route handler {func.__name__} with type: {activity_type} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, func, auth_handlers=auth_handlers) - ) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call def message( self, - select: Union[str, Pattern[str], List[Union[str, Pattern[str]]]], + select: Union[str, Pattern[str], list[Union[str, Pattern[str]]]], *, - auth_handlers: Optional[List[str]] = None, + auth_handlers: Optional[list[str]] = None, + **kwargs, ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: """ Registers a new message activity event listener. This method can be used as either @@ -276,9 +317,7 @@ def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( f"Registering message handler for route handler {func.__name__} with select: {select} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, func, auth_handlers=auth_handlers) - ) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call @@ -287,7 +326,8 @@ def conversation_update( self, type: ConversationUpdateTypes, *, - auth_handlers: Optional[List[str]] = None, + auth_handlers: Optional[list[str]] = None, + **kwargs, ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: """ Registers a new message activity event listener. This method can be used as either @@ -311,12 +351,12 @@ def __selector(context: TurnContext): return False if type == "membersAdded": - if isinstance(context.activity.members_added, List): + if isinstance(context.activity.members_added, list): return len(context.activity.members_added) > 0 return False if type == "membersRemoved": - if isinstance(context.activity.members_removed, List): + if isinstance(context.activity.members_removed, list): return len(context.activity.members_removed) > 0 return False @@ -330,15 +370,17 @@ def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( f"Registering conversation update handler for route handler {func.__name__} with type: {type} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, func, auth_handlers=auth_handlers) - ) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call def message_reaction( - self, type: MessageReactionTypes, *, auth_handlers: Optional[List[str]] = None + self, + type: MessageReactionTypes, + *, + auth_handlers: Optional[list[str]] = None, + **kwargs, ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: """ Registers a new message activity event listener. This method can be used as either @@ -361,12 +403,12 @@ def __selector(context: TurnContext): return False if type == "reactionsAdded": - if isinstance(context.activity.reactions_added, List): + if isinstance(context.activity.reactions_added, list): return len(context.activity.reactions_added) > 0 return False if type == "reactionsRemoved": - if isinstance(context.activity.reactions_removed, List): + if isinstance(context.activity.reactions_removed, list): return len(context.activity.reactions_removed) > 0 return False @@ -376,15 +418,17 @@ def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( f"Registering message reaction handler for route handler {func.__name__} with type: {type} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, func, auth_handlers=auth_handlers) - ) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call def message_update( - self, type: MessageUpdateTypes, *, auth_handlers: Optional[List[str]] = None + self, + type: MessageUpdateTypes, + *, + auth_handlers: Optional[list[str]] = None, + **kwargs, ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: """ Registers a new message activity event listener. This method can be used as either @@ -435,14 +479,14 @@ def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( f"Registering message update handler for route handler {func.__name__} with type: {type} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, func, auth_handlers=auth_handlers) - ) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call - def handoff(self, *, auth_handlers: Optional[List[str]] = None) -> Callable[ + def handoff( + self, *, auth_handlers: Optional[list[str]] = None, **kwargs + ) -> Callable[ [Callable[[TurnContext, StateT, str], Awaitable[None]]], Callable[[TurnContext, StateT, str], Awaitable[None]], ]: @@ -483,10 +527,7 @@ async def __handler(context: TurnContext, state: StateT): f"Registering handoff handler for route handler {func.__name__} with auth handlers: {auth_handlers}" ) - self._routes.append( - Route[StateT](__selector, __handler, True, auth_handlers) - ) - self._routes = sorted(self._routes, key=lambda route: not route.is_invoke) + self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func return __call @@ -598,7 +639,6 @@ async def on_turn(self, context: TurnContext): await self._start_long_running_call(context, self._on_turn) async def _on_turn(self, context: TurnContext): - # robrandao: TODO try: if context.activity.type != ActivityTypes.typing: await self._start_typing(context) @@ -663,7 +703,7 @@ def _remove_mentions(self, context: TurnContext): context.activity.text = context.remove_recipient_mention(context.activity) @staticmethod - def parse_env_vars_configuration(vars: Dict[str, Any]) -> dict: + def parse_env_vars_configuration(vars: dict[str, Any]) -> dict: """ Parses environment variables and returns a dictionary with the relevant configuration. """ @@ -738,7 +778,7 @@ async def _run_after_turn_middleware(self, context: TurnContext, state: StateT): return True async def _on_activity(self, context: TurnContext, state: StateT): - for route in self._routes: + for route in self._route_list: if route.selector(context): if not route.auth_handlers: await route.handler(context, state) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py index 7fe3948d..2e12d9cc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/__init__.py @@ -1,3 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + from .authorization import Authorization from .auth_handler import AuthHandler from ._sign_in_state import _SignInState diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/__init__.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/__init__.py index 05cf6dba..6757119c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/__init__.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/__init__.py @@ -1,3 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + from .agentic_user_authorization import AgenticUserAuthorization from ._user_authorization import _UserAuthorization from ._authorization_handler import _AuthorizationHandler 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 eba18b5d..66894d76 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 @@ -1,3 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + from abc import ABC from typing import Optional import logging 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 1083d240..118bad53 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 @@ -1,5 +1,7 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" from __future__ import annotations import logging 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 133d8145..847aedba 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 @@ -1,3 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + import logging from typing import Optional diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_response.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_response.py index 4c2968da..0c756697 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_response.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_response.py @@ -1,3 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + from typing import Optional from microsoft_agents.activity import TokenResponse diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py index 9ade2c80..ddd22d90 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_sign_in_state.py @@ -1,3 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + from __future__ import annotations from typing import Optional diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py index 8e298107..4d11b212 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/auth_handler.py @@ -1,5 +1,7 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" import logging from typing import Optional diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index e105ccf4..f5bd8f56 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -1,3 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + from datetime import datetime import logging from typing import TypeVar, Optional, Callable, Awaitable, Generic, cast diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/route.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/route.py deleted file mode 100644 index df85c5a9..00000000 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/route.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from __future__ import annotations - -from typing import Awaitable, Callable, Generic, List, TypeVar - -from microsoft_agents.hosting.core import TurnContext -from .state import TurnState - -StateT = TypeVar("StateT", bound=TurnState) -RouteHandler = Callable[[TurnContext, StateT], Awaitable[None]] - - -class Route(Generic[StateT]): - selector: Callable[[TurnContext], bool] - handler: RouteHandler[StateT] - is_invoke: bool - - def __init__( - self, - selector: Callable[[TurnContext], bool], - handler: RouteHandler, - is_invoke: bool = False, - auth_handlers: List[str] = None, - ) -> None: - self.selector = selector - self.handler = handler - self.is_invoke = is_invoke - self.auth_handlers = auth_handlers or [] diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py index e69647cd..d319c3fd 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol, Optional from abc import abstractmethod diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index a6fee937..70049a6e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Optional from microsoft_agents.hosting.core.authorization.auth_types import AuthTypes diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py index 6ed36fcf..29c3de8c 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/anonymous_token_provider.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Optional from .access_token_provider_base import AccessTokenProviderBase diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py index 07deff8a..58784ae8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from enum import Enum diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py index b5026022..e11103e2 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connections.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from abc import abstractmethod from typing import Protocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py index 714199b5..399e101f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import logging import jwt diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_api_handler_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_api_handler_protocol.py index 364e679c..82ce7740 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_api_handler_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_api_handler_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from abc import abstractmethod from typing import Protocol, Optional diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py index e325653c..faf46646 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol, Optional from abc import abstractmethod diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/agent_conversation_reference.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/agent_conversation_reference.py index 5fc7c15d..4d135226 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/agent_conversation_reference.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/agent_conversation_reference.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from microsoft_agents.activity import AgentsModel, ConversationReference diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py index 24e920a6..a55001cc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol from microsoft_agents.hosting.core.authorization import AccessTokenProviderBase diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py index ed4940a9..a2990357 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol from .channel_protocol import ChannelProtocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_info_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_info_protocol.py index 85ddc55e..185c24ef 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_info_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_info_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py index 73f3189d..a5855af0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol from microsoft_agents.activity import AgentsModel, Activity, InvokeResponse diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py index e6153852..935611a9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol from .channel_info_protocol import ChannelInfoProtocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/configuration_channel_host.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/configuration_channel_host.py index 478da51a..0c48dbf1 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/configuration_channel_host.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/configuration_channel_host.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from copy import copy from microsoft_agents.hosting.core.authorization import Connections diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_constants.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_constants.py index 162a0d11..1a741726 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_constants.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_constants.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from abc import ABC diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory.py index 7ba34de2..457f96c9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from uuid import uuid4 from functools import partial from typing import Type diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_options.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_options.py index 034977e7..88357a9f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_options.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_options.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from microsoft_agents.activity import Activity from .channel_info_protocol import ChannelInfoProtocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_protocol.py index 845e155a..d4dcaa71 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/conversation_id_factory_protocol.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol from abc import abstractmethod diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel.py index cd4fac10..ecc1cbbe 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from copy import deepcopy, copy from aiohttp import ClientSession diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel_factory.py index d331b0ad..c9187477 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/http_agent_channel_factory.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from microsoft_agents.hosting.core.authorization import AccessTokenProviderBase from .channel_factory_protocol import ChannelFactoryProtocol diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py index 8156ea58..a93eb1a8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py @@ -197,9 +197,6 @@ async def reply_to_activity( ) raise ValueError("conversationId and activityId are required") - print("\n*3") - print(conversation_id) - print("\n*3") conversation_id = self._normalize_conversation_id(conversation_id) url = f"v3/conversations/{conversation_id}/activities/{activity_id}" diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index 10aecc70..9e639480 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Optional import logging diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/error_handling.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/error_handling.py index 40a62d75..9f5cb520 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/error_handling.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/error_handling.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from collections.abc import Callable, Awaitable from typing import TypeVar diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/memory_storage.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/memory_storage.py index 320eed37..5f04c631 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/memory_storage.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/memory_storage.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from threading import Lock from typing import TypeVar diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/storage.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/storage.py index 4a71d939..6fd56037 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/storage.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/storage.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from typing import Protocol, TypeVar, Type, Union from abc import ABC, abstractmethod from asyncio import gather diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/store_item.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/store_item.py index a2f7b13d..7a334905 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/store_item.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/storage/store_item.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from abc import ABC from typing import Protocol, runtime_checkable diff --git a/test_samples/agentic-test/src/agent.py b/test_samples/agentic-test/src/agent.py index efbff47f..9e843719 100644 --- a/test_samples/agentic-test/src/agent.py +++ b/test_samples/agentic-test/src/agent.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -load_dotenv() # robrandao: todo +load_dotenv() agents_sdk_config = load_configuration_from_env(environ) STORAGE = MemoryStorage() @@ -31,7 +31,6 @@ ADAPTER.use(TranscriptLoggerMiddleware(ConsoleTranscriptLogger())) AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config) -# robrandao: downloader? AGENT_APP = AgentApplication[TurnState]( storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config ) diff --git a/test_samples/extensions/README.md b/test_samples/extensions/README.md new file mode 100644 index 00000000..fd85d0ef --- /dev/null +++ b/test_samples/extensions/README.md @@ -0,0 +1,3 @@ +# Doc Samples + +These samples are a bit more specific to development and are meant to highlight usage patterns of specific SDK features. Included are also samples for developing extensions on top of the SDK. \ No newline at end of file diff --git a/test_samples/extensions/extension-starter/README.md b/test_samples/extensions/extension-starter/README.md new file mode 100644 index 00000000..0336837d --- /dev/null +++ b/test_samples/extensions/extension-starter/README.md @@ -0,0 +1,11 @@ +## Extension Starter Sample + +This is a simple example that extends the Agents 365 SDK for Python. This is not meant to be a full outline of how to organize a repo for extension development. + +From this directory, you may run a sample that uses the extension capabilities with: + +```bash +python -m src.sample.main +``` + +To interact with this sample, we recommend the [Microsoft 365 Agents Playground](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/test-with-toolkit-project?tabs=windows). This well let you mock up the invoke activities, the dummy channel data, and so on. \ No newline at end of file diff --git a/test_samples/extensions/extension-starter/env.TEMPLATE b/test_samples/extensions/extension-starter/env.TEMPLATE new file mode 100644 index 00000000..187ec681 --- /dev/null +++ b/test_samples/extensions/extension-starter/env.TEMPLATE @@ -0,0 +1,3 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= \ No newline at end of file diff --git a/test_samples/extensions/extension-starter/requirements.txt b/test_samples/extensions/extension-starter/requirements.txt new file mode 100644 index 00000000..97502ab4 --- /dev/null +++ b/test_samples/extensions/extension-starter/requirements.txt @@ -0,0 +1 @@ +strenum \ No newline at end of file diff --git a/tests/hosting_core/app/oauth/__init__.py b/test_samples/extensions/extension-starter/src/__init__.py similarity index 100% rename from tests/hosting_core/app/oauth/__init__.py rename to test_samples/extensions/extension-starter/src/__init__.py diff --git a/test_samples/extensions/extension-starter/src/extension/__init__.py b/test_samples/extensions/extension-starter/src/extension/__init__.py new file mode 100644 index 00000000..cb21c2dd --- /dev/null +++ b/test_samples/extensions/extension-starter/src/extension/__init__.py @@ -0,0 +1,9 @@ +from .extension import ExtensionAgent +from .models import CustomEventData, CustomEventResult, CustomEventTypes + +__all__ = [ + "ExtensionAgent", + "CustomEventData", + "CustomEventResult", + "CustomEventTypes", +] diff --git a/test_samples/extensions/extension-starter/src/extension/extension.py b/test_samples/extensions/extension-starter/src/extension/extension.py new file mode 100644 index 00000000..98d7c61d --- /dev/null +++ b/test_samples/extensions/extension-starter/src/extension/extension.py @@ -0,0 +1,151 @@ +import logging +from typing import Callable, Generic, TypeVar + +from microsoft_agents.activity import Activity, ActivityTypes, InvokeResponse +from microsoft_agents.hosting.core import ( + AgentApplication, + TurnContext, + TurnState, + RouteHandler, +) + +from .models import ( + CustomEventData, + CustomEventResult, + CustomRouteHandler, +) + +logger = logging.getLogger(__name__) + +MY_CHANNEL = "mychannel" + +StateT = TypeVar("StateT", bound=TurnState) + + +# This extension defines event decorators with custom selecting/handling logic: +class ExtensionAgent(Generic[StateT]): + """An extension agent that provides custom event decorators.""" + + app: AgentApplication[StateT] + + def __init__(self, app: AgentApplication[StateT]): + """Initialize the ExtensionAgent with an AgentApplication. + + :param app: The AgentApplication instance to extend. + """ + self.app = app + + # Allowing event decorators to accept **kwargs and passing + # **kwargs to app.add_route is recommended. + def _on_message_has_hello_event( + self, + **kwargs, + ) -> Callable[[CustomRouteHandler[StateT]], RouteHandler[StateT]]: + """Decorator for message activities that contain the word 'hello'. + + This demonstrates a custom event selector and handler that extracts + additional data from the activity and passes it to the handler. + + :param kwargs: Additional keyword arguments to pass to app.add_route. + :return: A decorator that registers the route handler. + + Usage: + @extension_agent.on_message_has_hello_event() + async def handle_hello_event(context: TurnContext, state: StateT, event_data: CustomEventData): + await context.send_activity(f"Hello! You said: {event_data.text}") + """ + + # the function the AgentApplication uses to determine if this route should be called + def route_selector(context: TurnContext) -> bool: + return ( + context.activity.type == ActivityTypes.message + and "hello" in context.activity.text.lower() + ) + + # the function that wraps the user's handler to extract custom data + def create_handler(handler: CustomRouteHandler[StateT]) -> RouteHandler[StateT]: + async def route_handler(context: TurnContext, state: StateT): + custom_event_data = CustomEventData.from_context(context) + await handler(context, state, custom_event_data) + + return route_handler + + # the decorator that registers the route handler with the app + def __call(func: CustomRouteHandler[StateT]) -> RouteHandler[StateT]: + logger.debug("Registering route for message has hello event") + handler = create_handler(func) + self.app.add_route(route_selector, handler, **kwargs) + return handler + + return __call + + def on_message_has_hello_event( + self, + **kwargs, + ) -> Callable[[CustomRouteHandler[StateT]], RouteHandler[StateT]]: + """Decorator for non-agentic message activities that contain the word 'hello'.""" + return self._on_message_has_hello_event(is_agentic=False, **kwargs) + + def on_agentic_message_has_hello_event( + self, + **kwargs, + ) -> Callable[[CustomRouteHandler[StateT]], RouteHandler[StateT]]: + """Decorator for agentic message activities that contain the word 'hello'.""" + return self._on_message_has_hello_event(is_agentic=True, **kwargs) + + # events that are handled with custom payloads + def on_invoke_custom_event( + self, custom_event_type: str, **kwargs + ) -> Callable[[CustomRouteHandler[StateT]], RouteHandler[StateT]]: + """Decorator for invoke activities with a specific custom event type. + + This demonstrates a custom event selector and handler that extracts + additional data from the activity and passes it to the handler. + + :param custom_event_type: The custom event type to listen for. + :param kwargs: Additional keyword arguments to pass to app.add_route. + :return: A decorator that registers the route handler. + Usage: + @extension_agent.on_invoke_custom_event("my_custom_event") + async def handle_custom_event(context: TurnContext, state: StateT, event_data: CustomEventData): + await context.send_activity(f"Received custom event with data: {event_data.data}") + return CustomEventResult(success=True) + """ + + # the function the AgentApplication uses to determine if this route should be called + def route_selector(context: TurnContext) -> bool: + return ( + context.activity.type == ActivityTypes.invoke + and context.activity.channel_id == MY_CHANNEL + and context.activity.name == f"invoke/{custom_event_type}" + ) + + # the function that wraps the user's handler to extract custom data + # it also sends back an invoke response with the handler's result + def create_handler(handler: CustomRouteHandler) -> RouteHandler[StateT]: + async def route_handler(context: TurnContext, state: StateT): + custom_event_data = CustomEventData.from_context(context) + result = await handler(context, state, custom_event_data) + if not result: + result = CustomEventResult() + + # send an invoke response back to the caller + # invokes must send back an invoke response + response = Activity( + type=ActivityTypes.invoke_response, + value=InvokeResponse( + status=200, body=result.model_dump(mode="json") + ), + ) + await context.send_activity(response) + + return route_handler + + # the decorator that registers the route handler with the app + def __call(func: CustomRouteHandler[StateT]) -> RouteHandler[StateT]: + logger.debug("Registering route for custom event") + handler = create_handler(func) + self.app.add_route(route_selector, handler, is_invoke=True, **kwargs) + return handler + + return __call diff --git a/test_samples/extensions/extension-starter/src/extension/models.py b/test_samples/extensions/extension-starter/src/extension/models.py new file mode 100644 index 00000000..4b6b2a50 --- /dev/null +++ b/test_samples/extensions/extension-starter/src/extension/models.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import Optional, Protocol, TypeVar +from microsoft_agents.activity import AgentsModel +from microsoft_agents.hosting.core import TurnContext, TurnState + +from strenum import StrEnum + +StateT = TypeVar("StateT", bound=TurnState) + + +# this is defined as a class to allow for robust generic typing +class CustomRouteHandler(Protocol[StateT]): + """A protocol for route handlers that accept custom event data.""" + + async def __call__( + self, context: TurnContext, state: StateT, data: CustomEventData + ) -> Optional[CustomEventResult]: ... + + +class CustomEventTypes(StrEnum): + """Custom event types used in the extension.""" + + CUSTOM_EVENT = "customEvent" + OTHER_CUSTOM_EVENT = "otherCustomEvent" + + +# inheriting from AgentsModel allows for easy serialization/deserialization +# using Pydantic features +class CustomEventData(AgentsModel): + """Dummy data extracted from the activity for custom events.""" + + user_id: Optional[str] = None + field: Optional[str] = None + + @staticmethod + def from_context(context) -> CustomEventData: + return CustomEventData( + user_id=context.activity.from_property.id, + field=( + context.activity.channel_data.get("field") + if context.activity.channel_data + else None + ), + ) + + +class CustomEventResult(AgentsModel): + """Dummy result returned by custom event handlers.""" + + user_id: Optional[str] = None + field: Optional[str] = None diff --git a/tests/hosting_core/app/oauth/_handlers/__init__.py b/test_samples/extensions/extension-starter/src/sample/__init__.py similarity index 100% rename from tests/hosting_core/app/oauth/_handlers/__init__.py rename to test_samples/extensions/extension-starter/src/sample/__init__.py diff --git a/test_samples/extensions/extension-starter/src/sample/app.py b/test_samples/extensions/extension-starter/src/sample/app.py new file mode 100644 index 00000000..04539190 --- /dev/null +++ b/test_samples/extensions/extension-starter/src/sample/app.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import re +from dotenv import load_dotenv + +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import ( + Authorization, + MemoryStorage, + AgentApplication, + TurnState, + MemoryStorage, + RouteRank, + TurnContext, +) +from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.authentication.msal import MsalConnectionManager + +# Load configuration from environment +load_dotenv() +agents_sdk_config = load_configuration_from_env(os.environ) + +# Create storage and connection manager +STORAGE = MemoryStorage() +CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) +ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) +AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config) +APP = AgentApplication[TurnState]( + storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config +) + + +@APP.activity("message", rank=RouteRank.LAST) +async def on_message(context: TurnContext, state: TurnState): + await context.send_activity(f"Echo: {context.activity.text}") diff --git a/test_samples/extensions/extension-starter/src/sample/extension_agent.py b/test_samples/extensions/extension-starter/src/sample/extension_agent.py new file mode 100644 index 00000000..f924042e --- /dev/null +++ b/test_samples/extensions/extension-starter/src/sample/extension_agent.py @@ -0,0 +1,52 @@ +from microsoft_agents.hosting.core import TurnContext, TurnState, RouteRank + +from src.extension import ( + ExtensionAgent, + CustomEventData, + CustomEventResult, + CustomEventTypes, +) + +from .app import APP + +EXT = ExtensionAgent[TurnState](APP) + + +@EXT.on_agentic_message_has_hello_event(rank=RouteRank.FIRST) +async def agentic_message_has_hello_event( + context: TurnContext, state: TurnState, data: CustomEventData +): + await context.send_activity( + f"Hello event detected! User ID: {data.user_id}, Field: {data.field}" + ) + + +@EXT.on_message_has_hello_event(rank=RouteRank.FIRST) +async def message_has_hello_event( + context: TurnContext, state: TurnState, data: CustomEventData +): + await context.send_activity( + f"Hello event detected! User ID: {data.user_id}, Field: {data.field}" + ) + + +# specifying the event type via the enum +@EXT.on_invoke_custom_event(CustomEventTypes.CUSTOM_EVENT) +async def invoke_custom_event( + context: TurnContext, state: TurnState, data: CustomEventData +) -> CustomEventResult: + await context.send_activity( + f"Custom event triggered {context.activity.type}/{context.activity.name}" + ) + return CustomEventResult(user_id=data.user_id, field=data.field) + + +# specifying the event type via a string +@EXT.on_invoke_custom_event("otherCustomEvent") +async def invoke_other_custom_event( + context: TurnContext, state: TurnState, data: CustomEventData +) -> CustomEventResult: + await context.send_activity( + f"Custom event triggered {context.activity.type}/{context.activity.name}" + ) + return CustomEventResult(user_id=data.user_id, field=data.field) diff --git a/test_samples/extensions/extension-starter/src/sample/main.py b/test_samples/extensions/extension-starter/src/sample/main.py new file mode 100644 index 00000000..d253252f --- /dev/null +++ b/test_samples/extensions/extension-starter/src/sample/main.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# enable logging for Microsoft Agents library +# for more information, see README.md for Quickstart Agent +import logging + +ms_agents_logger = logging.getLogger("microsoft_agents") +ms_agents_logger.addHandler(logging.StreamHandler()) +ms_agents_logger.setLevel(logging.INFO) + +from .app import CONNECTION_MANAGER +from .extension_agent import APP +from .start_server import start_server + +start_server( + agent_application=APP, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), +) diff --git a/test_samples/extensions/extension-starter/src/sample/start_server.py b/test_samples/extensions/extension-starter/src/sample/start_server.py new file mode 100644 index 00000000..d76b619e --- /dev/null +++ b/test_samples/extensions/extension-starter/src/sample/start_server.py @@ -0,0 +1,32 @@ +from os import environ +from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration +from microsoft_agents.hosting.aiohttp import ( + start_agent_process, + jwt_authorization_middleware, + CloudAdapter, +) +from aiohttp.web import Request, Response, Application, run_app + + +def start_server( + agent_application: AgentApplication, auth_configuration: AgentAuthConfiguration +): + async def entry_point(req: Request) -> Response: + agent: AgentApplication = req.app["agent_app"] + adapter: CloudAdapter = req.app["adapter"] + return await start_agent_process( + req, + agent, + adapter, + ) + + APP = Application(middlewares=[jwt_authorization_middleware]) + APP.router.add_post("/api/messages", entry_point) + APP["agent_configuration"] = auth_configuration + APP["agent_app"] = agent_application + APP["adapter"] = agent_application.adapter + + try: + run_app(APP, host="localhost", port=environ.get("PORT", 3978)) + except Exception as error: + raise error diff --git a/tests/_common/fixtures/roles.py b/tests/_common/fixtures/roles.py new file mode 100644 index 00000000..44cab400 --- /dev/null +++ b/tests/_common/fixtures/roles.py @@ -0,0 +1,12 @@ +import pytest +from microsoft_agents.activity import RoleTypes + + +@pytest.fixture(params=[RoleTypes.user, RoleTypes.skill, RoleTypes.agent]) +def non_agentic_role(request): + return request.param + + +@pytest.fixture(params=[RoleTypes.agentic_user, RoleTypes.agentic_identity]) +def agentic_role(request): + return request.param diff --git a/tests/hosting_core/app/_oauth/__init__.py b/tests/hosting_core/app/_oauth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hosting_core/app/oauth/_common.py b/tests/hosting_core/app/_oauth/_common.py similarity index 100% rename from tests/hosting_core/app/oauth/_common.py rename to tests/hosting_core/app/_oauth/_common.py diff --git a/tests/hosting_core/app/oauth/_env.py b/tests/hosting_core/app/_oauth/_env.py similarity index 100% rename from tests/hosting_core/app/oauth/_env.py rename to tests/hosting_core/app/_oauth/_env.py diff --git a/tests/hosting_core/app/_oauth/_handlers/__init__.py b/tests/hosting_core/app/_oauth/_handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hosting_core/app/oauth/_handlers/test_agentic_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py similarity index 100% rename from tests/hosting_core/app/oauth/_handlers/test_agentic_user_authorization.py rename to tests/hosting_core/app/_oauth/_handlers/test_agentic_user_authorization.py diff --git a/tests/hosting_core/app/oauth/_handlers/test_user_authorization.py b/tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py similarity index 100% rename from tests/hosting_core/app/oauth/_handlers/test_user_authorization.py rename to tests/hosting_core/app/_oauth/_handlers/test_user_authorization.py diff --git a/tests/hosting_core/app/oauth/test_auth_handler.py b/tests/hosting_core/app/_oauth/test_auth_handler.py similarity index 100% rename from tests/hosting_core/app/oauth/test_auth_handler.py rename to tests/hosting_core/app/_oauth/test_auth_handler.py diff --git a/tests/hosting_core/app/oauth/test_authorization.py b/tests/hosting_core/app/_oauth/test_authorization.py similarity index 100% rename from tests/hosting_core/app/oauth/test_authorization.py rename to tests/hosting_core/app/_oauth/test_authorization.py diff --git a/tests/hosting_core/app/oauth/test_sign_in_response.py b/tests/hosting_core/app/_oauth/test_sign_in_response.py similarity index 100% rename from tests/hosting_core/app/oauth/test_sign_in_response.py rename to tests/hosting_core/app/_oauth/test_sign_in_response.py diff --git a/tests/hosting_core/app/_routes/__init__.py b/tests/hosting_core/app/_routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hosting_core/app/_routes/test_agentic_selector.py b/tests/hosting_core/app/_routes/test_agentic_selector.py new file mode 100644 index 00000000..69b651fa --- /dev/null +++ b/tests/hosting_core/app/_routes/test_agentic_selector.py @@ -0,0 +1,97 @@ +import pytest + +from microsoft_agents.activity import Activity, ChannelAccount + +from microsoft_agents.hosting.core import RouteSelector, TurnContext +from microsoft_agents.hosting.core.app._routes import _agentic_selector + +from tests._common.fixtures.roles import ( + agentic_role, + non_agentic_role, +) # required for fixtures + + +def message_selector(context) -> bool: + return context.activity.type == "message" + + +def invoke_selector(context) -> bool: + return context.activity.type == "invoke" + + +def create_text_selector(includes: str) -> RouteSelector: + def text_selector(context) -> bool: + return context.activity.type == "message" and includes in context.activity.text + + return text_selector + + +hello_text_selector = create_text_selector("hello") +bye_text_selector = create_text_selector("bye") + +do_select = { + message_selector: Activity(type="message", text="hello world"), + invoke_selector: Activity(type="invoke"), + hello_text_selector: Activity(type="message", text="hello there"), + bye_text_selector: Activity(type="message", text="goodbye"), +} + +do_not_select = { + message_selector: Activity(type="invoke"), + invoke_selector: Activity(type="message"), + hello_text_selector: Activity(type="message", text="goodbye"), + bye_text_selector: Activity(type="message", text="hello there"), +} + + +@pytest.fixture( + params=[message_selector, invoke_selector, hello_text_selector, bye_text_selector] +) +def selector(request) -> RouteSelector: + return request.param + + +def test_agentic_selector_does_not_select_with_non_agentic_request( + mocker, selector, non_agentic_role +): + channel_account = ChannelAccount(role=non_agentic_role) + + selecting_activity = do_select[selector].model_copy() + non_selecting_activity = do_not_select[selector].model_copy() + + selecting_context = mocker.Mock(spec=TurnContext) + selecting_context.activity = selecting_activity + non_selecting_context = mocker.Mock(spec=TurnContext) + non_selecting_context.activity = non_selecting_activity + + assert selector(selecting_context) + assert not selector(non_selecting_context) + + new_selector = _agentic_selector(selector) + + selecting_activity.recipient = channel_account + + assert not new_selector(selecting_context) + assert not new_selector(non_selecting_context) + + +def test_agentic_selector_selects_with_agentic_request(mocker, selector, agentic_role): + channel_account = ChannelAccount(role=agentic_role) + + selecting_activity = do_select[selector].model_copy() + non_selecting_activity = do_not_select[selector].model_copy() + + selecting_context = mocker.Mock(spec=TurnContext) + selecting_context.activity = selecting_activity + non_selecting_context = mocker.Mock(spec=TurnContext) + non_selecting_context.activity = non_selecting_activity + + assert selector(selecting_context) + assert not selector(non_selecting_context) + + new_selector = _agentic_selector(selector) + + selecting_activity.recipient = channel_account + + assert new_selector(selecting_context) + assert not new_selector(non_selecting_context) diff --git a/tests/hosting_core/app/_routes/test_route.py b/tests/hosting_core/app/_routes/test_route.py new file mode 100644 index 00000000..1473b135 --- /dev/null +++ b/tests/hosting_core/app/_routes/test_route.py @@ -0,0 +1,499 @@ +import pytest + +from microsoft_agents.hosting.core import ( + TurnContext, + _Route, + RouteRank, + TurnState, +) + +from microsoft_agents.hosting.core.app._type_defs import ( + RouteHandler, + RouteSelector, + StateT, +) + + +def selector(context: TurnContext) -> bool: + return True + + +async def handler(context: TurnContext, state: TurnState) -> None: + pass + + +class TestRoute: + + def test_init(self): + + route = _Route( + selector=selector, + handler=handler, + is_invoke=True, + rank=RouteRank.LAST, + auth_handlers=["auth1", "auth2"], + ) + + assert route.selector == selector + assert route.handler == handler + assert route.is_invoke is True + assert route.rank == RouteRank.LAST + assert route.auth_handlers == ["auth1", "auth2"] + + def test_init_defaults(self): + + route = _Route(selector=selector, handler=handler) + + assert route.selector == selector + assert route.handler == handler + assert route.is_invoke is False + assert route.rank == RouteRank.DEFAULT + assert route.auth_handlers == [] + + @pytest.mark.parametrize( + "rank, is_error", + [ + [RouteRank.FIRST, False], + [RouteRank.LAST, False], + [RouteRank.DEFAULT, False], + [1, False], + [-1, True], + [RouteRank.LAST + 1, True], + ], + ) + def test_init_error(self, rank, is_error): + if is_error: + with pytest.raises(ValueError): + _Route(selector=selector, handler=handler, rank=rank) + else: + route = _Route(selector=selector, handler=handler, rank=rank) + assert route.rank == rank + + def test_priority(self): + + route_a = _Route( + selector=selector, + handler=handler, + is_invoke=True, + rank=RouteRank.FIRST, + auth_handlers=["auth1"], + ) + route_b = _Route( + selector=selector, + handler=handler, + is_invoke=False, + rank=RouteRank.LAST, + auth_handlers=["auth2"], + is_agentic=True, + ) + route_c = _Route( + selector=selector, + handler=handler, + is_invoke=False, + rank=RouteRank.DEFAULT, + auth_handlers=["auth2"], + is_agentic=False, + ) + route_d = _Route( + selector=selector, + handler=handler, + is_invoke=False, + rank=23, + auth_handlers=["auth2"], + is_agentic=False, + ) + + assert route_a.priority == [0, 1, RouteRank.FIRST] + assert route_b.priority == [1, 0, RouteRank.LAST] + assert route_c.priority == [1, 1, RouteRank.DEFAULT] + assert route_d.priority == [1, 1, 23] + + @pytest.fixture(params=[None, [], ["authA1", "authA2"], ["github"]]) + def auth_handlers_a(self, request): + return request.param + + @pytest.fixture(params=[None, [], ["authB1", "authB2"], ["github"]]) + def auth_handlers_b(self, request): + return request.param + + @pytest.mark.parametrize( + "is_agentic_a, rank_a, is_invoke_a, is_agentic_b, rank_b, is_invoke_b, expected_result", + [ + # Same agentic status (both False) + [ + False, + RouteRank.DEFAULT, + False, + False, + RouteRank.DEFAULT, + False, + False, + ], # [1,1,DEFAULT] vs [1,1,DEFAULT] + [ + False, + RouteRank.DEFAULT, + False, + False, + RouteRank.LAST, + False, + True, + ], # [1,1,DEFAULT] vs [1,1,LAST] + [ + False, + RouteRank.LAST, + False, + False, + RouteRank.DEFAULT, + False, + False, + ], # [1,1,LAST] vs [1,1,DEFAULT] + [ + False, + RouteRank.DEFAULT, + False, + True, + RouteRank.DEFAULT, + False, + False, + ], # [1,1,DEFAULT] vs [1,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + False, + False, + RouteRank.DEFAULT, + False, + True, + ], # [1,0,DEFAULT] vs [1,1,DEFAULT] + [ + True, + RouteRank.DEFAULT, + False, + True, + RouteRank.DEFAULT, + False, + False, + ], # [1,0,DEFAULT] vs [1,0,DEFAULT] + [ + True, + RouteRank.LAST, + False, + True, + RouteRank.DEFAULT, + False, + False, + ], # [1,0,LAST] vs [1,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + False, + True, + RouteRank.LAST, + False, + True, + ], # [1,0,DEFAULT] vs [1,0,LAST] + [ + False, + RouteRank.FIRST, + False, + True, + RouteRank.DEFAULT, + False, + False, + ], # [1,1,FIRST] vs [1,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + False, + False, + RouteRank.LAST, + False, + True, + ], # [1,0,DEFAULT] vs [1,1,LAST] + [ + False, + RouteRank.LAST, + False, + True, + RouteRank.FIRST, + False, + False, + ], # [1,1,LAST] vs [1,0,FIRST] + [ + True, + RouteRank.FIRST, + False, + False, + RouteRank.LAST, + False, + True, + ], # [1,0,FIRST] vs [1,1,LAST] + [ + False, + RouteRank.FIRST, + False, + False, + RouteRank.LAST, + False, + True, + ], # [1,1,FIRST] vs [1,1,LAST] + [ + True, + RouteRank.FIRST, + False, + True, + RouteRank.LAST, + False, + True, + ], # [1,0,FIRST] vs [1,0,LAST] + # Same agentic status (both True) + [ + False, + RouteRank.DEFAULT, + True, + False, + RouteRank.DEFAULT, + True, + False, + ], # [0,1,DEFAULT] vs [0,1,DEFAULT] + [ + False, + RouteRank.DEFAULT, + True, + False, + RouteRank.LAST, + True, + True, + ], # [0,1,DEFAULT] vs [0,1,LAST] + [ + False, + RouteRank.LAST, + True, + False, + RouteRank.DEFAULT, + True, + False, + ], # [0,1,LAST] vs [0,1,DEFAULT] + [ + False, + RouteRank.DEFAULT, + True, + True, + RouteRank.DEFAULT, + True, + False, + ], # [0,1,DEFAULT] vs [0,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + True, + False, + RouteRank.DEFAULT, + True, + True, + ], # [0,0,DEFAULT] vs [0,1,DEFAULT] + [ + True, + RouteRank.DEFAULT, + True, + True, + RouteRank.DEFAULT, + True, + False, + ], # [0,0,DEFAULT] vs [0,0,DEFAULT] + [ + True, + RouteRank.LAST, + True, + True, + RouteRank.DEFAULT, + True, + False, + ], # [0,0,LAST] vs [0,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + True, + True, + RouteRank.LAST, + True, + True, + ], # [0,0,DEFAULT] vs [0,0,LAST] + [ + False, + RouteRank.FIRST, + True, + True, + RouteRank.DEFAULT, + True, + False, + ], # [0,1,FIRST] vs [0,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + True, + False, + RouteRank.LAST, + True, + True, + ], # [0,0,DEFAULT] vs [0,1,LAST] + [ + False, + RouteRank.LAST, + True, + True, + RouteRank.FIRST, + True, + False, + ], # [0,1,LAST] vs [0,0,FIRST] + [ + True, + RouteRank.FIRST, + True, + False, + RouteRank.LAST, + True, + True, + ], # [0,0,FIRST] vs [0,1,LAST] + [ + False, + RouteRank.FIRST, + True, + False, + RouteRank.LAST, + True, + True, + ], # [0,1,FIRST] vs [0,1,LAST] + [ + True, + RouteRank.FIRST, + True, + True, + RouteRank.LAST, + True, + True, + ], # [0,0,FIRST] vs [0,0,LAST] + # Different agentic status - agentic (True) has higher priority than non-agentic (False) + [ + False, + RouteRank.DEFAULT, + True, + False, + RouteRank.DEFAULT, + False, + True, + ], # [0,1,DEFAULT] vs [1,1,DEFAULT] + [ + False, + RouteRank.DEFAULT, + False, + False, + RouteRank.DEFAULT, + True, + False, + ], # [1,1,DEFAULT] vs [0,1,DEFAULT] + [ + True, + RouteRank.DEFAULT, + True, + True, + RouteRank.DEFAULT, + False, + True, + ], # [0,0,DEFAULT] vs [1,0,DEFAULT] + [ + True, + RouteRank.DEFAULT, + False, + True, + RouteRank.DEFAULT, + True, + False, + ], # [1,0,DEFAULT] vs [0,0,DEFAULT] + [ + False, + RouteRank.LAST, + True, + False, + RouteRank.FIRST, + False, + True, + ], # [0,1,LAST] vs [1,1,FIRST] + [ + False, + RouteRank.FIRST, + False, + False, + RouteRank.LAST, + True, + False, + ], # [1,1,FIRST] vs [0,1,LAST] + [ + True, + RouteRank.LAST, + True, + True, + RouteRank.FIRST, + False, + True, + ], # [0,0,LAST] vs [1,0,FIRST] + [ + True, + RouteRank.FIRST, + False, + True, + RouteRank.LAST, + True, + False, + ], # [1,0,FIRST] vs [0,0,LAST] + [ + False, + RouteRank.LAST, + True, + True, + RouteRank.FIRST, + False, + True, + ], # [0,1,LAST] vs [1,0,FIRST] + [ + True, + RouteRank.LAST, + False, + False, + RouteRank.FIRST, + True, + False, + ], # [1,0,LAST] vs [0,1,FIRST] + ], + ) + def test_lt_with_agentic( + self, + is_invoke_a, + rank_a, + is_agentic_a, + is_invoke_b, + rank_b, + is_agentic_b, + expected_result, + auth_handlers_a, + auth_handlers_b, + ): + + route_a = _Route( + selector, + handler, + is_invoke=is_invoke_a, + rank=rank_a, + is_agentic=is_agentic_a, + auth_handlers=auth_handlers_a, + ) + route_b = _Route( + selector, + handler, + is_invoke=is_invoke_b, + rank=rank_b, + is_agentic=is_agentic_b, + auth_handlers=auth_handlers_b, + ) + + assert (route_a < route_b) == expected_result diff --git a/tests/hosting_core/app/_routes/test_route_list.py b/tests/hosting_core/app/_routes/test_route_list.py new file mode 100644 index 00000000..ae50e793 --- /dev/null +++ b/tests/hosting_core/app/_routes/test_route_list.py @@ -0,0 +1,82 @@ +from microsoft_agents.hosting.core import ( + TurnContext, + TurnState, + _RouteList, + _Route, + RouteRank, +) + + +def selector(context: TurnContext) -> bool: + return True + + +async def handler(context: TurnContext, state: TurnState) -> None: + pass + + +def route_eq(route1: _Route, route2: _Route) -> bool: + return ( + route1.selector == route2.selector + and route1.handler == route2.handler + and route1.is_invoke == route2.is_invoke + and route1.rank == route2.rank + and route1.auth_handlers == route2.auth_handlers + and route1.is_agentic == route2.is_agentic + ) + + +class Test_RouteList: + + def assert_priority_invariant(self, route_list: _RouteList): + + # check priority invariant + routes = list(route_list) + for i in range(1, len(routes)): + assert not routes[i] < routes[i - 1] + + def has_contents(self, route_list: _RouteList, should_contain: list[_Route]): + for route in should_contain: + for existing in list(route_list): + if route_eq(existing, route): + break + else: + return False + return True + + def test_route_list_init(self): + route_list = _RouteList() + assert list(route_list) == [] + + def test_route_list_add_and_order(self): + + route_list = _RouteList() + + all_routes = [ + (selector, handler, False, RouteRank.DEFAULT, ["a"], False), + (selector, handler, True, RouteRank.LAST, ["a"], False), + (selector, handler, False, RouteRank.FIRST, [], True), + (selector, handler, True, RouteRank.DEFAULT, [], True), + (selector, handler, False, RouteRank.DEFAULT, [], False), + (selector, handler, True, RouteRank.DEFAULT, ["slack"], True), + (selector, handler, False, RouteRank.FIRST, ["a", "b"], False), + (selector, handler, True, RouteRank.DEFAULT, ["c"], True), + ] + all_routes = [ + { + "selector": route[0], + "handler": route[1], + "is_invoke": route[2], + "rank": route[3], + "auth_handlers": route[4], + "is_agentic": route[5], + } + for route in all_routes + ] + added_routes = [] + + for i, kwargs in enumerate(all_routes): + added_routes.append(_Route(**kwargs)) + route_list.add_route(_Route(**kwargs)) + self.assert_priority_invariant(route_list) + assert self.has_contents(route_list, added_routes) diff --git a/tests/hosting_core/storage/test_transcript_store_memory.py b/tests/hosting_core/storage/test_transcript_store_memory.py index 2d3fb631..691c3e02 100644 --- a/tests/hosting_core/storage/test_transcript_store_memory.py +++ b/tests/hosting_core/storage/test_transcript_store_memory.py @@ -4,7 +4,8 @@ from datetime import datetime, timezone import pytest from microsoft_agents.hosting.core.storage.transcript_memory_store import ( - TranscriptMemoryStore, PagedResult + TranscriptMemoryStore, + PagedResult, ) from microsoft_agents.activity import Activity, ConversationAccount @@ -12,12 +13,11 @@ @pytest.mark.asyncio async def test_get_transcript_empty(): store = TranscriptMemoryStore() - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert pagedResult.items == [] assert pagedResult.continuation_token is None + @pytest.mark.asyncio async def test_log_activity_add_one_activity(): store = TranscriptMemoryStore() @@ -30,9 +30,7 @@ async def test_log_activity_add_one_activity(): await store.log_activity(activity) # Ask for the activity we just added - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 1 assert pagedResult.items[0].channel_id == activity.channel_id @@ -41,16 +39,12 @@ async def test_log_activity_add_one_activity(): assert pagedResult.continuation_token is None # Ask for a channel that doesn't exist and make sure we get nothing - pagedResult = await store.get_transcript_activities( - "Invalid", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Invalid", "Conversation 1") assert pagedResult.items == [] assert pagedResult.continuation_token is None # Ask for a ConversationID that doesn't exist and make sure we get nothing - pagedResult = await store.get_transcript_activities( - "Channel 1", "INVALID" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "INVALID") assert pagedResult.items == [] assert pagedResult.continuation_token is None @@ -72,9 +66,7 @@ async def test_log_activity_add_two_activity_same_conversation(): await store.log_activity(activity2) # Ask for the activity we just added - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 2 assert pagedResult.items[0].channel_id == activity1.channel_id @@ -87,6 +79,7 @@ async def test_log_activity_add_two_activity_same_conversation(): assert pagedResult.continuation_token is None + @pytest.mark.asyncio async def test_log_activity_add_three_activity_same_conversation(): store = TranscriptMemoryStore() @@ -153,9 +146,7 @@ async def test_log_activity_add_two_activity_two_conversation(): await store.log_activity(activity2) # Ask for the activity we just added - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 1 assert pagedResult.items[0].channel_id == activity1.channel_id @@ -164,9 +155,7 @@ async def test_log_activity_add_two_activity_two_conversation(): assert pagedResult.continuation_token is None # Now grab Conversation 2 - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 2" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 2") assert len(pagedResult.items) == 1 assert pagedResult.items[0].channel_id == activity2.channel_id @@ -174,6 +163,7 @@ async def test_log_activity_add_two_activity_two_conversation(): assert pagedResult.items[0].text == activity2.text assert pagedResult.continuation_token is None + @pytest.mark.asyncio async def test_delete_one_transcript(): store = TranscriptMemoryStore() @@ -186,17 +176,13 @@ async def test_delete_one_transcript(): await store.log_activity(activity) # Ask for the activity we just added - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 1 # Now delete the transcript await store.delete_transcript("Channel 1", "Conversation 1") - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 0 @@ -224,17 +210,14 @@ async def test_delete_one_transcript_of_two(): await store.delete_transcript("Channel 1", "Conversation 1") # Make sure the one we deleted is gone - pagedResult = await store.get_transcript_activities( - "Channel 1", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 1", "Conversation 1") assert len(pagedResult.items) == 0 # Make sure the other one is still there - pagedResult = await store.get_transcript_activities( - "Channel 2", "Conversation 1" - ) + pagedResult = await store.get_transcript_activities("Channel 2", "Conversation 1") assert len(pagedResult.items) == 1 + @pytest.mark.asyncio async def test_list_transcripts(): store = TranscriptMemoryStore()