diff --git a/.gitignore b/.gitignore index e80eb444..0723fd02 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,6 @@ cython_debug/ # JetBrains Rider *.sln.iml .idea/ + +# vscode +.vscode/ diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/__init__.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/__init__.py index 275cb6c9..e9ffa8dc 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/__init__.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/__init__.py @@ -1,6 +1,8 @@ # Import necessary modules from .activity_handler import ActivityHandler from .bot import Bot +from .channel_adapter import ChannelAdapter +from .channel_api_handler_protocol import ChannelApiHandlerProtocol from .channel_service_adapter import ChannelServiceAdapter from .channel_service_client_factory_base import ChannelServiceClientFactoryBase from .message_factory import MessageFactory @@ -12,6 +14,8 @@ __all__ = [ "ActivityHandler", "Bot", + "ChannelAdapter", + "ChannelApiHandlerProtocol", "ChannelServiceAdapter", "ChannelServiceClientFactoryBase", "MessageFactory", diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py index 22eedebb..1410329c 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py @@ -4,6 +4,7 @@ from http import HTTPStatus from pydantic import BaseModel +from microsoft.agents.core import TurnContextProtocol from microsoft.agents.core.models import ( Activity, ActivityTypes, @@ -16,7 +17,6 @@ ) from .bot import Bot -from .turn_context import TurnContext class ActivityHandler(Bot): @@ -30,7 +30,7 @@ class ActivityHandler(Bot): """ async def on_turn( - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): # pylint: disable=arguments-differ """ Called by the adapter (for example, :class:`BotFrameworkAdapter`) at runtime @@ -97,7 +97,7 @@ async def on_turn( await self.on_unrecognized_activity_type(turn_context) async def on_message_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this method in a derived class to provide logic specific to activities, @@ -111,7 +111,7 @@ async def on_message_activity( # pylint: disable=unused-argument return async def on_message_update_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this method in a derived class to provide logic specific to activities, @@ -125,7 +125,7 @@ async def on_message_update_activity( # pylint: disable=unused-argument return async def on_message_delete_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this method in a derived class to provide logic specific to activities, @@ -138,7 +138,7 @@ async def on_message_delete_activity( # pylint: disable=unused-argument """ return - async def on_conversation_update_activity(self, turn_context: TurnContext): + async def on_conversation_update_activity(self, turn_context: TurnContextProtocol): """ Invoked when a conversation update activity is received from the channel when the base behavior of :meth:`on_turn()` is used. @@ -176,7 +176,7 @@ async def on_conversation_update_activity(self, turn_context: TurnContext): return async def on_members_added_activity( - self, members_added: list[ChannelAccount], turn_context: TurnContext + self, members_added: list[ChannelAccount], turn_context: TurnContextProtocol ): # pylint: disable=unused-argument """ Override this method in a derived class to provide logic for when members other than the bot join @@ -198,7 +198,7 @@ async def on_members_added_activity( return async def on_members_removed_activity( - self, members_removed: list[ChannelAccount], turn_context: TurnContext + self, members_removed: list[ChannelAccount], turn_context: TurnContextProtocol ): # pylint: disable=unused-argument """ Override this method in a derived class to provide logic for when members other than the bot leave @@ -220,7 +220,7 @@ async def on_members_removed_activity( return - async def on_message_reaction_activity(self, turn_context: TurnContext): + async def on_message_reaction_activity(self, turn_context: TurnContextProtocol): """ Invoked when an event activity is received from the connector when the base behavior of :meth:`on_turn()` is used. @@ -261,7 +261,9 @@ async def on_message_reaction_activity(self, turn_context: TurnContext): ) async def on_reactions_added( # pylint: disable=unused-argument - self, message_reactions: list[MessageReaction], turn_context: TurnContext + self, + message_reactions: list[MessageReaction], + turn_context: TurnContextProtocol, ): """ Override this method in a derived class to provide logic for when reactions to a previous activity @@ -285,7 +287,9 @@ async def on_reactions_added( # pylint: disable=unused-argument return async def on_reactions_removed( # pylint: disable=unused-argument - self, message_reactions: list[MessageReaction], turn_context: TurnContext + self, + message_reactions: list[MessageReaction], + turn_context: TurnContextProtocol, ): """ Override this method in a derived class to provide logic for when reactions to a previous activity @@ -307,7 +311,7 @@ async def on_reactions_removed( # pylint: disable=unused-argument """ return - async def on_event_activity(self, turn_context: TurnContext): + async def on_event_activity(self, turn_context: TurnContextProtocol): """ Invoked when an event activity is received from the connector when the base behavior of :meth:`on_turn()` is used. @@ -336,7 +340,7 @@ async def on_event_activity(self, turn_context: TurnContext): return await self.on_event(turn_context) async def on_token_response_event( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Invoked when a `tokens/response` event is received when the base behavior of @@ -356,7 +360,7 @@ async def on_token_response_event( # pylint: disable=unused-argument return async def on_event( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Invoked when an event other than `tokens/response` is received when the base behavior of @@ -376,7 +380,7 @@ async def on_event( # pylint: disable=unused-argument return async def on_end_of_conversation_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Invoked when a conversation end activity is received from the channel. @@ -388,7 +392,7 @@ async def on_end_of_conversation_activity( # pylint: disable=unused-argument return async def on_typing_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this in a derived class to provide logic specific to @@ -401,7 +405,7 @@ async def on_typing_activity( # pylint: disable=unused-argument return async def on_installation_update( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this in a derived class to provide logic specific to @@ -418,7 +422,7 @@ async def on_installation_update( # pylint: disable=unused-argument return async def on_installation_update_add( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this in a derived class to provide logic specific to @@ -431,7 +435,7 @@ async def on_installation_update_add( # pylint: disable=unused-argument return async def on_installation_update_remove( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Override this in a derived class to provide logic specific to @@ -444,7 +448,7 @@ async def on_installation_update_remove( # pylint: disable=unused-argument return async def on_unrecognized_activity_type( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Invoked when an activity other than a message, conversation update, or event is received when the base @@ -463,7 +467,7 @@ async def on_unrecognized_activity_type( # pylint: disable=unused-argument return async def on_invoke_activity( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ) -> InvokeResponse | None: """ Registers an activity event handler for the _invoke_ event, emitted for every incoming event activity. @@ -496,7 +500,7 @@ async def on_invoke_activity( # pylint: disable=unused-argument return invoke_exception.create_invoke_response() async def on_sign_in_invoke( # pylint: disable=unused-argument - self, turn_context: TurnContext + self, turn_context: TurnContextProtocol ): """ Invoked when a signin/verifyState or signin/tokenExchange event is received when the base behavior of @@ -512,7 +516,7 @@ async def on_sign_in_invoke( # pylint: disable=unused-argument raise _InvokeResponseException(HTTPStatus.NOT_IMPLEMENTED) async def on_adaptive_card_invoke( - self, turn_context: TurnContext, invoke_value: AdaptiveCardInvokeValue + self, turn_context: TurnContextProtocol, invoke_value: AdaptiveCardInvokeValue ) -> AdaptiveCardInvokeResponse: """ Invoked when the bot is sent an Adaptive Card Action Execute. diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_adapter.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_adapter.py index 9305909f..d7a1bfc8 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_adapter.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_adapter.py @@ -3,7 +3,9 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from typing import List, Awaitable, Protocol +from typing import List, Awaitable +from microsoft.agents.authentication import ClaimsIdentity +from microsoft.agents.core import ChannelAdapterProtocol from microsoft.agents.core.models import ( Activity, ConversationReference, @@ -15,7 +17,7 @@ from .middleware_set import MiddlewareSet -class ChannelAdapter(ABC): +class ChannelAdapter(ABC, ChannelAdapterProtocol): BOT_IDENTITY_KEY = "BotIdentity" OAUTH_SCOPE_KEY = "Microsoft.Agents.BotBuilder.ChannelAdapter.OAuthScope" INVOKE_RESPONSE_KEY = "ChannelAdapter.InvokeResponse" @@ -104,6 +106,29 @@ async def continue_conversation( context = TurnContext(self, reference.get_continuation_activity()) return await self.run_pipeline(context, callback) + async def continue_conversation_with_claims( + self, + claims_identity: ClaimsIdentity, + continuation_activity: Activity, + callback: Callable[[TurnContext], Awaitable], + audience: str = None, + ): + """ + Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation. + Most channels require a user to initiate a conversation with a bot before the bot can send activities + to the user. + + :param claims_identity: A :class:`botframework.connector.auth.ClaimsIdentity` for the conversation. + :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity` + :param continuation_activity: The activity to send. + :type continuation_activity: :class:`botbuilder + :param callback: The method to call for the resulting bot turn. + :type callback: :class:`typing.Callable` + :param audience: A value signifying the recipient of the proactive message. + :type audience: str + """ + raise NotImplementedError() + async def create_conversation( self, bot_app_id: str, diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_api_handler_protocol.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_api_handler_protocol.py new file mode 100644 index 00000000..680bb80d --- /dev/null +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_api_handler_protocol.py @@ -0,0 +1,159 @@ +from abc import abstractmethod +from typing import Protocol, Optional + +from microsoft.agents.core.models import ( + Activity, + AttachmentData, + ChannelAccount, + ConversationResourceResponse, + ConversationsResult, + ConversationParameters, + ResourceResponse, + PagedMembersResult, + Transcript, +) + +from microsoft.agents.authentication import ClaimsIdentity + + +class ChannelApiHandlerProtocol(Protocol): + @abstractmethod + async def on_get_conversations( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + continuation_token: Optional[str] = None, + ) -> ConversationsResult: + """ + List the Conversations in which this bot has participated. + """ + raise NotImplementedError() + + @abstractmethod + async def on_create_conversation( + self, claims_identity: ClaimsIdentity, parameters: ConversationParameters + ) -> ConversationResourceResponse: + """ + Create a new Conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_send_to_conversation( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity + ) -> ResourceResponse: + """ + Send an activity to the end of a conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_send_conversation_history( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + transcript: Transcript, + ) -> ResourceResponse: + """ + Upload the historic activities to the conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_update_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + """ + Edit an existing activity. + """ + raise NotImplementedError() + + @abstractmethod + async def on_reply_to_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + """ + Reply to an activity. + """ + raise NotImplementedError() + + @abstractmethod + async def on_delete_activity( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str + ): + """ + Delete an existing activity. + """ + raise NotImplementedError() + + @abstractmethod + async def on_get_conversation_members( + self, claims_identity: ClaimsIdentity, conversation_id: str + ) -> list[ChannelAccount]: + """ + Enumerate the members of a conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_get_conversation_member( + self, + claims_identity: ClaimsIdentity, + user_id: str, + conversation_id: str, + ) -> ChannelAccount: + """ + Enumerate the members of a conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_get_conversation_paged_members( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + """ + Enumerate the members of a conversation one page at a time. + """ + raise NotImplementedError() + + @abstractmethod + async def on_delete_conversation_member( + self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str + ): + """ + Deletes a member from a conversation. + """ + raise NotImplementedError() + + @abstractmethod + async def on_get_activity_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str + ) -> list[ChannelAccount]: + """ + Enumerate the members of an activity. + """ + raise NotImplementedError() + + @abstractmethod + async def on_upload_attachment( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + attachment_upload: AttachmentData, + ) -> ResourceResponse: + """ + Upload an attachment directly into a channel's storage. + """ + raise NotImplementedError() diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_service_adapter.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_service_adapter.py index bf6b018a..e21c4b47 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_service_adapter.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/channel_service_adapter.py @@ -100,7 +100,9 @@ async def send_activities( activity.model_dump(by_alias=True, exclude_unset=True), ) ) - + # TODO: The connector client is not casting the response but returning the JSON, need to fix it appropiatly + if not isinstance(response, ResourceResponse): + response = ResourceResponse.model_validate(response) response = response or ResourceResponse(id=activity.id or "") responses.append(response) @@ -183,11 +185,11 @@ async def continue_conversation_with_claims( self, claims_identity: ClaimsIdentity, continuation_activity: Activity, - logic: Callable[[TurnContext], Awaitable], + callback: Callable[[TurnContext], Awaitable], audience: str = None, ): return await self.process_proactive( - claims_identity, continuation_activity, audience, logic + claims_identity, continuation_activity, audience, callback ) async def create_conversation( # pylint: disable=arguments-differ @@ -215,7 +217,7 @@ async def create_conversation( # pylint: disable=arguments-differ claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url # Create the connector client to use for outbound requests. - connector_client = ( + connector_client: ConnectorClient = ( await self._channel_service_client_factory.create_connector_client( claims_identity, service_url, audience ) @@ -234,7 +236,7 @@ async def create_conversation( # pylint: disable=arguments-differ ) # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) - user_token_client = ( + user_token_client: UserTokenClient = ( await self._channel_service_client_factory.create_user_token_client( claims_identity ) @@ -253,6 +255,9 @@ async def create_conversation( # pylint: disable=arguments-differ # Run the pipeline await self.run_pipeline(context, callback) + await connector_client.close() + await user_token_client.close() + async def process_proactive( self, claims_identity: ClaimsIdentity, @@ -261,14 +266,14 @@ async def process_proactive( callback: Callable[[TurnContext], Awaitable], ): # Create the connector client to use for outbound requests. - connector_client = ( + connector_client: ConnectorClient = ( await self._channel_service_client_factory.create_connector_client( claims_identity, continuation_activity.service_url, audience ) ) # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) - user_token_client = ( + user_token_client: UserTokenClient = ( await self._channel_service_client_factory.create_user_token_client( claims_identity ) @@ -287,6 +292,9 @@ async def process_proactive( # Run the pipeline await self.run_pipeline(context, callback) + await connector_client.close() + await user_token_client.close() + async def process_activity( self, claims_identity: ClaimsIdentity, @@ -300,8 +308,8 @@ async def process_activity( :type auth_header: :class:`typing.Union[typing.str, AuthenticateRequestResult]` :param activity: The incoming activity :type activity: :class:`Activity` - :param logic: The logic to execute at the end of the adapter's middleware pipeline. - :type logic: :class:`typing.Callable` + :param callback: The callback to execute at the end of the adapter's middleware pipeline. + :type callback: :class:`typing.Callable` :return: A task that represents the work queued to execute. @@ -420,14 +428,14 @@ def _create_turn_context( oauth_scope: str, connector_client: ConnectorClientBase, user_token_client: UserTokenClientBase, - logic: Callable[[TurnContext], Awaitable], + callback: Callable[[TurnContext], Awaitable], ) -> TurnContext: context = TurnContext(self, activity) context.turn_state[self.BOT_IDENTITY_KEY] = claims_identity context.turn_state[self._BOT_CONNECTOR_CLIENT_KEY] = connector_client context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client - context.turn_state[self.BOT_CALLBACK_HANDLER_KEY] = logic + context.turn_state[self.BOT_CALLBACK_HANDLER_KEY] = callback context.turn_state[self.CHANNEL_SERVICE_FACTORY_KEY] = ( self._channel_service_client_factory ) diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/turn_context.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/turn_context.py index 39e15c53..eb95c661 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/turn_context.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/turn_context.py @@ -7,6 +7,7 @@ from copy import copy, deepcopy from collections.abc import Callable from datetime import datetime, timezone +from microsoft.agents.core import TurnContextProtocol from microsoft.agents.core.models import ( Activity, ActivityTypes, @@ -18,7 +19,7 @@ ) -class TurnContext: +class TurnContext(TurnContextProtocol): # Same constant as in the BF Adapter, duplicating here to avoid circular dependency _INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse" diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/__init__.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/__init__.py new file mode 100644 index 00000000..1740902b --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/__init__.py @@ -0,0 +1,35 @@ +from .bot_conversation_reference import BotConversationReference +from .channel_factory_protocol import ChannelFactoryProtocol +from .channel_host_protocol import ChannelHostProtocol +from .channel_info_protocol import ChannelInfoProtocol +from .channel_protocol import ChannelProtocol +from .channels_configuration import ( + ChannelsConfiguration, + ChannelHostConfiguration, + ChannelInfo, +) +from .configuration_channel_host import ConfigurationChannelHost +from .conversation_constants import ConversationConstants +from .conversation_id_factory_options import ConversationIdFactoryOptions +from .conversation_id_factory_protocol import ConversationIdFactoryProtocol +from .conversation_id_factory import ConversationIdFactory +from .http_bot_channel_factory import HttpBotChannelFactory +from .http_bot_channel import HttpBotChannel + +__all__ = [ + "BotConversationReference", + "ChannelFactoryProtocol", + "ChannelHostProtocol", + "ChannelInfoProtocol", + "ChannelProtocol", + "ChannelsConfiguration", + "ChannelHostConfiguration", + "ChannelInfo", + "ConfigurationChannelHost", + "ConversationConstants", + "ConversationIdFactoryOptions", + "ConversationIdFactoryProtocol", + "ConversationIdFactory", + "HttpBotChannelFactory", + "HttpBotChannel", +] diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/bot_conversation_reference.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/bot_conversation_reference.py new file mode 100644 index 00000000..d4fe6bef --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/bot_conversation_reference.py @@ -0,0 +1,6 @@ +from microsoft.agents.core.models import AgentsModel, ConversationReference + + +class BotConversationReference(AgentsModel): + conversation_reference: ConversationReference + oauth_scope: str diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_factory_protocol.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_factory_protocol.py new file mode 100644 index 00000000..6ecd17ee --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_factory_protocol.py @@ -0,0 +1,10 @@ +from typing import Protocol + +from microsoft.agents.authentication import AccessTokenProviderBase + +from .channel_protocol import ChannelProtocol + + +class ChannelFactoryProtocol(Protocol): + def create_channel(self, token_access: AccessTokenProviderBase) -> ChannelProtocol: + pass diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_host_protocol.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_host_protocol.py new file mode 100644 index 00000000..ed4940a9 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_host_protocol.py @@ -0,0 +1,24 @@ +from typing import Protocol + +from .channel_protocol import ChannelProtocol +from .channel_info_protocol import ChannelInfoProtocol + + +class ChannelHostProtocol(Protocol): + def __init__( + self, + host_endpoint: str, + host_app_id: str, + channels: dict[str, ChannelInfoProtocol], + ): + self.host_endpoint = host_endpoint + self.host_app_id = host_app_id + self.channels = channels + + def get_channel_from_channel_info( + self, channel_info: ChannelInfoProtocol + ) -> ChannelProtocol: + raise NotImplementedError() + + def get_channel_from_name(self, name: str) -> ChannelProtocol: + raise NotImplementedError() diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_info_protocol.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_info_protocol.py new file mode 100644 index 00000000..85ddc55e --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_info_protocol.py @@ -0,0 +1,10 @@ +from typing import Protocol + + +class ChannelInfoProtocol(Protocol): + id: str + app_id: str + resource_url: str + token_provider: str + channel_factory: str + endpoint: str diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_protocol.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_protocol.py new file mode 100644 index 00000000..4fb4dee9 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channel_protocol.py @@ -0,0 +1,19 @@ +from typing import Protocol + +from microsoft.agents.core.models import AgentsModel, Activity, InvokeResponse + + +class ChannelProtocol(Protocol): + async def post_activity( + self, + to_bot_id: str, + to_bot_resource: str, + endpoint: str, + service_url: str, + conversation_id: str, + activity: Activity, + *, + response_body_type: type[AgentsModel] = None, + **kwargs, + ) -> InvokeResponse: + raise NotImplementedError() diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/channels_configuration.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channels_configuration.py new file mode 100644 index 00000000..e6153852 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/channels_configuration.py @@ -0,0 +1,39 @@ +from typing import Protocol + +from .channel_info_protocol import ChannelInfoProtocol + + +class ChannelInfo(ChannelInfoProtocol): + + def __init__( + self, + id: str = None, + app_id: str = None, + resource_url: str = None, + token_provider: str = None, + channel_factory: str = None, + endpoint: str = None, + **kwargs + ): + self.id = id + self.app_id = app_id + self.resource_url = resource_url + self.token_provider = token_provider + self.channel_factory = channel_factory + self.endpoint = endpoint + + +class ChannelHostConfiguration: + def __init__( + self, CHANNELS: list[ChannelInfoProtocol], HOST_ENDPOINT: str, HOST_APP_ID: str + ): + self.CHANNELS = CHANNELS + self.HOST_ENDPOINT = HOST_ENDPOINT + self.HOST_APP_ID = HOST_APP_ID + + +class ChannelsConfiguration(Protocol): + + @staticmethod + def CHANNEL_HOST_CONFIGURATION() -> ChannelHostConfiguration: + pass diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/configuration_channel_host.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/configuration_channel_host.py new file mode 100644 index 00000000..2316a001 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/configuration_channel_host.py @@ -0,0 +1,59 @@ +from copy import copy + +from microsoft.agents.authentication import Connections + +from .channels_configuration import ChannelsConfiguration +from .channel_factory_protocol import ChannelFactoryProtocol +from .channel_host_protocol import ChannelHostProtocol +from .channel_info_protocol import ChannelInfoProtocol +from .channel_protocol import ChannelProtocol + + +class ConfigurationChannelHost(ChannelHostProtocol): + def __init__( + self, + channel_factory: ChannelFactoryProtocol, + connections: Connections, + configuration: ChannelsConfiguration, + default_channel_name: str, + ): + self._channel_factory = channel_factory + self.connections = connections + self.configuration = configuration + self.channels: dict[str, ChannelInfoProtocol] = {} + self.host_endpoint: str = None + self.host_app_id: str = None + + channel_host_configuration = configuration.CHANNEL_HOST_CONFIGURATION() + + if channel_host_configuration: + if channel_host_configuration.CHANNELS: + for bot_from_config in channel_host_configuration.CHANNELS: + bot = copy(bot_from_config) + if not bot.channel_factory: + bot.channel_factory = default_channel_name + self.channels[bot.id] = bot + + self.host_endpoint = channel_host_configuration.HOST_ENDPOINT + self.host_app_id = channel_host_configuration.HOST_APP_ID + + def get_channel_from_name(self, name: str) -> ChannelProtocol: + if not name in self.channels: + raise ValueError(f"ChannelInfo not found for '{name}'") + return self.get_channel_from_channel_info(self.channels[name]) + + def get_channel_from_channel_info( + self, channel_info: ChannelInfoProtocol + ) -> ChannelProtocol: + if not channel_info: + raise ValueError( + f"ConfigurationChannelHost.get_channel_from_channel_info(): channel_info cannot be None" + ) + + token_provider = self.connections.get_connection(channel_info.token_provider) + if not token_provider: + raise ValueError( + f"ConfigurationChannelHost.get_channel_from_channel_info(): token_provider not found for '{channel_info.token_provider}'" + ) + + return self._channel_factory.create_channel(token_provider) diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_constants.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_constants.py new file mode 100644 index 00000000..162a0d11 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_constants.py @@ -0,0 +1,5 @@ +from abc import ABC + + +class ConversationConstants(ABC): + CONVERSATION_ID_HTTP_HEADER_NAME = "x-ms-conversation-id" diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory.py new file mode 100644 index 00000000..3822a35c --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory.py @@ -0,0 +1,66 @@ +from uuid import uuid4 +from functools import partial +from typing import Type + +from microsoft.agents.core.models import AgentsModel +from microsoft.agents.storage import Storage, StoreItem + +from .bot_conversation_reference import BotConversationReference +from .conversation_id_factory_protocol import ConversationIdFactoryProtocol + + +def _implement_store_item_for_agents_model_cls(model_instance: AgentsModel): + instance_cls = type(model_instance) + if not isinstance(model_instance, StoreItem): + instance_cls = type(model_instance) + setattr( + instance_cls, + "store_item_to_json", + partial(model_instance.model_dump, mode="json", exclude_none=True), + ) + instance_cls.from_json_to_store_item = classmethod(instance_cls.model_validate) + + +class ConversationIdFactory(ConversationIdFactoryProtocol): + def __init__(self, storage: Storage) -> None: + if not storage: + raise ValueError("ConversationIdFactory.__init__(): storage cannot be None") + self._storage = storage + + async def create_conversation_id(self, options) -> str: + if not options: + raise ValueError( + "ConversationIdFactory.create_conversation_id(): options cannot be None" + ) + + conversation_reference = options.activity.get_conversation_reference() + bot_conversation_id = str(uuid4()) + + bot_conversation_reference = BotConversationReference( + conversation_reference=conversation_reference, + oauth_scope=options.from_oauth_scope, + ) + + _implement_store_item_for_agents_model_cls(bot_conversation_reference) + + conversation_info = {bot_conversation_id: bot_conversation_reference} + await self._storage.write(conversation_info) + + return bot_conversation_id + + async def get_bot_conversation_reference( + self, bot_conversation_id + ) -> BotConversationReference: + if not bot_conversation_id: + raise ValueError( + "ConversationIdFactory.get_bot_conversation_reference(): bot_conversation_id cannot be None" + ) + + storage_record = await self._storage.read( + [bot_conversation_id], target_cls=BotConversationReference + ) + + return storage_record[bot_conversation_id] + + async def delete_conversation_reference(self, bot_conversation_id): + await self._storage.delete([bot_conversation_id]) diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_options.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_options.py new file mode 100644 index 00000000..05334903 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_options.py @@ -0,0 +1,18 @@ +from microsoft.agents.core.models import Activity + +from .channel_info_protocol import ChannelInfoProtocol + + +class ConversationIdFactoryOptions: + def __init__( + self, + from_oauth_scope: str, + from_bot_id: str, + activity: Activity, + bot: ChannelInfoProtocol, + ) -> None: + self.from_oauth_scope = from_oauth_scope + self.from_bot_id = from_bot_id + # TODO: implement Activity and types as protocols and replace here + self.activity = activity + self.bot = bot diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_protocol.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_protocol.py new file mode 100644 index 00000000..b8b92c7c --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/conversation_id_factory_protocol.py @@ -0,0 +1,34 @@ +from typing import Protocol +from abc import abstractmethod + +from .bot_conversation_reference import BotConversationReference +from .conversation_id_factory_options import ConversationIdFactoryOptions + + +class ConversationIdFactoryProtocol(Protocol): + @abstractmethod + async def create_conversation_id( + self, options: ConversationIdFactoryOptions + ) -> str: + """ + Creates a conversation ID for a bot conversation. + :param options: A ConversationIdFactoryOptions instance. + :return: A unique conversation ID. + """ + + @abstractmethod + async def get_bot_conversation_reference( + self, bot_conversation_id: str + ) -> BotConversationReference: + """ + Gets the BotConversationReference for a conversation ID. + :param bot_conversation_id: An ID created with create_conversation_id. + :return: BotConversationReference or None if not found. + """ + + @abstractmethod + async def delete_conversation_reference(self, bot_conversation_id: str) -> None: + """ + Deletes a bot conversation reference. + :param bot_conversation_id: A conversation ID created with create_conversation_id. + """ diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel.py new file mode 100644 index 00000000..e62991b7 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel.py @@ -0,0 +1,97 @@ +from copy import deepcopy, copy + +from aiohttp import ClientSession +from microsoft.agents.authentication import AccessTokenProviderBase +from microsoft.agents.core.models import ( + AgentsModel, + Activity, + ConversationReference, + ChannelAccount, + InvokeResponse, + RoleTypes, +) + +from .channel_protocol import ChannelProtocol +from .conversation_constants import ConversationConstants + + +class HttpBotChannel(ChannelProtocol): + def __init__(self, token_access: AccessTokenProviderBase) -> None: + self._token_access = token_access + + async def post_activity( + self, + to_bot_id: str, + to_bot_resource: str, + endpoint: str, + service_url: str, + conversation_id: str, + activity: Activity, + *, + response_body_type: type[AgentsModel] = None, + **kwargs, + ) -> InvokeResponse[AgentsModel]: + if not endpoint: + raise ValueError("HttpBotChannel.post_activity: Endpoint is required") + if not service_url: + raise ValueError("HttpBotChannel.post_activity: Service URL is required") + if not conversation_id: + raise ValueError( + "HttpBotChannel.post_activity: Conversation ID is required" + ) + if not activity: + raise ValueError("HttpBotChannel.post_activity: Activity is required") + + activity_copy = deepcopy(activity) + + # TODO: should conversation should be a deep copy instead of shallow? + activity_copy.relates_to = ConversationReference( + service_url=service_url, + activity_id=activity_copy.id, + channel_id=activity_copy.channel_id, + locale=activity_copy.locale, + conversation=copy(activity_copy.conversation), + ) + + activity_copy.conversation.id = conversation_id + activity_copy.service_url = service_url + activity_copy.recipient = activity_copy.recipient or ChannelAccount() + activity_copy.recipient.role = RoleTypes.skill + + token_result = await self._token_access.get_access_token( + to_bot_resource, [f"{to_bot_id}/.default"] + ) + headers = { + "Authorization": f"Bearer {token_result}", + "Content-Type": "application/json", + ConversationConstants.CONVERSATION_ID_HTTP_HEADER_NAME: conversation_id, + } + + async with ClientSession() as session: + async with session.post( + endpoint, + headers=headers, + json=activity_copy.model_dump( + mode="json", by_alias=True, exclude_unset=True + ), + ) as response: + content = None + if response.ok: + try: + content = await response.json() + except: + pass + if response_body_type: + content = response_body_type.model_validate(content) + + return InvokeResponse(status=response.status, body=content) + + else: + # TODO: Log error + # TODO: Fix generic AgentsModel serialization + if response.content_type == "application/json": + content = await response.json() + elif response.content_type == "text/plain": + content = await response.text() + + return InvokeResponse(status=response.status, body=content) diff --git a/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel_factory.py b/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel_factory.py new file mode 100644 index 00000000..935601e1 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/microsoft/agents/client/http_bot_channel_factory.py @@ -0,0 +1,10 @@ +from microsoft.agents.authentication import AccessTokenProviderBase + +from .channel_factory_protocol import ChannelFactoryProtocol +from .channel_protocol import ChannelProtocol +from .http_bot_channel import HttpBotChannel + + +class HttpBotChannelFactory(ChannelFactoryProtocol): + def create_channel(self, token_access: AccessTokenProviderBase) -> ChannelProtocol: + return HttpBotChannel(token_access) diff --git a/libraries/Client/microsoft-agents-client/pyproject.toml b/libraries/Client/microsoft-agents-client/pyproject.toml new file mode 100644 index 00000000..d7b167c6 --- /dev/null +++ b/libraries/Client/microsoft-agents-client/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "microsoft-agents-client" +version = "0.0.0a1" +description = "A client library for Microsoft Agents" +authors = [{name = "Microsoft Corporation"}] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "microsoft-agents-core", + "microsoft-agents-authentication" +] + +[project.urls] +"Homepage" = "https://github.com/microsoft/microsoft-agents-protocol" diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/__init__.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/__init__.py new file mode 100644 index 00000000..d8a18591 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/__init__.py @@ -0,0 +1,7 @@ +from .channel_adapter_protocol import ChannelAdapterProtocol +from .turn_context_protocol import TurnContextProtocol + +__all__ = [ + "ChannelAdapterProtocol", + "TurnContextProtocol", +] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/channel_adapter_protocol.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/channel_adapter_protocol.py new file mode 100644 index 00000000..c5ac9566 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/channel_adapter_protocol.py @@ -0,0 +1,76 @@ +from abc import abstractmethod +from typing import Protocol, List, Callable, Awaitable, Optional + +from .turn_context_protocol import TurnContextProtocol +from microsoft.agents.core.models import ( + Activity, + ResourceResponse, + ConversationReference, + ConversationParameters, +) + + +class ChannelAdapterProtocol(Protocol): + on_turn_error: Optional[Callable[[TurnContextProtocol, Exception], Awaitable]] + + @abstractmethod + async def send_activities( + self, context: TurnContextProtocol, activities: List[Activity] + ) -> List[ResourceResponse]: + pass + + @abstractmethod + async def update_activity( + self, context: TurnContextProtocol, activity: Activity + ) -> None: + pass + + @abstractmethod + async def delete_activity( + self, context: TurnContextProtocol, reference: ConversationReference + ) -> None: + pass + + @abstractmethod + def use(self, middleware: object) -> "ChannelAdapterProtocol": + pass + + @abstractmethod + async def continue_conversation( + self, + bot_id: str, + reference: ConversationReference, + callback: Callable[[TurnContextProtocol], Awaitable], + ) -> None: + pass + + # TODO: potentially move ClaimsIdentity to core + @abstractmethod + async def continue_conversation_with_claims( + self, + claims_identity: dict, + continuation_activity: Activity, + callback: Callable[[TurnContextProtocol], Awaitable], + audience: str = None, + ): + pass + + @abstractmethod + async def create_conversation( + self, + bot_app_id: str, + channel_id: str, + service_url: str, + audience: str, + conversation_parameters: ConversationParameters, + callback: Callable[[TurnContextProtocol], Awaitable], + ) -> None: + pass + + @abstractmethod + async def run_pipeline( + self, + context: TurnContextProtocol, + callback: Callable[[TurnContextProtocol], Awaitable], + ) -> None: + pass diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/__init__.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/__init__.py index a67c0217..71eda3fa 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/__init__.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/__init__.py @@ -1,3 +1,4 @@ +from .agents_model import AgentsModel from .activity import Activity from .activity_event_names import ActivityEventNames from .activity_types import ActivityTypes @@ -78,6 +79,7 @@ from .caller_id_constants import CallerIdConstants __all__ = [ + "AgentsModel", "Activity", "ActivityEventNames", "AdaptiveCardInvokeAction", diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/activity.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/activity.py index 836b94dd..de22f225 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/activity.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/activity.py @@ -14,10 +14,11 @@ from .conversation_reference import ConversationReference from .text_highlight import TextHighlight from .semantic_action import SemanticAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString +# TODO: B2B Bot 2 is responding with None as id, had to mark it as optional (investigate) class Activity(AgentsModel): """An Activity is the basic communication type for the Bot Framework 3.0 protocol. @@ -120,7 +121,7 @@ class Activity(AgentsModel): """ type: NonEmptyString - id: NonEmptyString = None + id: Optional[NonEmptyString] = None timestamp: datetime = None local_timestamp: datetime = None local_timezone: NonEmptyString = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_action.py index 4a9ffa4e..815d42fe 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_action.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_action.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_response.py index c6c76507..c142fd4a 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_response.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_value.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_value.py index f2d1a859..b6e35110 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_value.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/adaptive_card_invoke_value.py @@ -1,6 +1,6 @@ from .adaptive_card_invoke_action import AdaptiveCardInvokeAction from .token_exchange_invoke_request import TokenExchangeInvokeRequest -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/_agents_model.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py similarity index 100% rename from libraries/Core/microsoft-agents-core/microsoft/agents/core/models/_agents_model.py rename to libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/animation_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/animation_card.py index d945937f..ed6b8115 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/animation_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/animation_card.py @@ -1,7 +1,7 @@ from .thumbnail_url import ThumbnailUrl from .media_url import MediaUrl from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment.py index a36fdb47..8cbf04c6 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_data.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_data.py index a6f91681..f0e38a7a 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_data.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_data.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_info.py index be8ab59f..e1ac76bd 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_info.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_info.py @@ -1,5 +1,5 @@ from .attachment_view import AttachmentView -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_view.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_view.py index 87a129a6..ce0039e9 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_view.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/attachment_view.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/audio_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/audio_card.py index acf2a515..d6cef0e2 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/audio_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/audio_card.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from .thumbnail_url import ThumbnailUrl from .media_url import MediaUrl from .card_action import CardAction diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/basic_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/basic_card.py index c148ea98..76d8e0d8 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/basic_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/basic_card.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from .card_image import CardImage from .card_action import CardAction from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_action.py index 25fe52d7..0c7c7903 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_action.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_action.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_image.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_image.py index 13ec58b0..a47555f9 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_image.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/card_image.py @@ -1,5 +1,5 @@ from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py index 333bb3de..3f601da9 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channels.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channels.py index ce52cd30..ca20b701 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channels.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channels.py @@ -64,6 +64,7 @@ class Channels(str, Enum): webchat = "webchat" """WebChat channel.""" + # TODO: validate the need of Self annotations in the following methods @staticmethod def supports_suggested_actions(channel_id: Self, button_cnt: int = 100) -> bool: """Determine if a number of Suggested Actions are supported by a Channel. diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py index 59221fb0..acd08382 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString @@ -19,7 +19,7 @@ class ConversationAccount(AgentsModel): :param aad_object_id: This account's object ID within Azure Active Directory (AAD) :type aad_object_id: str - :param role: Role of the entity behind the account (Example: User, Bot, Skill + :param role: Role of the entity behind the account (Example: User, Bot, etc.). Possible values include: 'user', 'bot', 'skill' :type role: str or ~microsoft.agents.protocols.models.RoleTypes :param tenant_id: This conversation's tenant ID diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_members.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_members.py index 09a5029b..c61c6e33 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_members.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_members.py @@ -1,5 +1,5 @@ from .channel_account import ChannelAccount -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_parameters.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_parameters.py index 12340225..cada9e04 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_parameters.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_parameters.py @@ -1,6 +1,6 @@ from .channel_account import ChannelAccount from .activity import Activity -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py index 91558845..995196ae 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py @@ -3,7 +3,7 @@ from .channel_account import ChannelAccount from .conversation_account import ConversationAccount -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString from .activity_types import ActivityTypes from .activity_event_names import ActivityEventNames @@ -36,7 +36,7 @@ class ConversationReference(AgentsModel): # optionals here are due to webchat activity_id: Optional[NonEmptyString] = None user: ChannelAccount = None - bot: ChannelAccount + bot: ChannelAccount = None conversation: ConversationAccount channel_id: NonEmptyString locale: Optional[NonEmptyString] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_resource_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_resource_response.py index cd23495a..c2bf7b0b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_resource_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_resource_response.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversations_result.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversations_result.py index a8bb0e97..40a8e710 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversations_result.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversations_result.py @@ -1,5 +1,5 @@ from .conversation_members import ConversationMembers -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py index 7cb9507c..9636980b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error.py index e8ae0041..55f4d54a 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error.py @@ -1,5 +1,5 @@ from .inner_http_error import InnerHttpError -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error_response.py index 6e611bce..f2506cd8 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/error_response.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from .error import Error diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/expected_replies.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/expected_replies.py index 8d9bd8fd..ad5ae2a7 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/expected_replies.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/expected_replies.py @@ -1,5 +1,5 @@ from .activity import Activity -from ._agents_model import AgentsModel +from .agents_model import AgentsModel class ExpectedReplies(AgentsModel): diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/fact.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/fact.py index a53b8116..b66b8073 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/fact.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/fact.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/geo_coordinates.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/geo_coordinates.py index 7b00ff8b..1c457f8e 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/geo_coordinates.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/geo_coordinates.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/hero_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/hero_card.py index d9cd4d92..ff6c99fc 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/hero_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/hero_card.py @@ -1,6 +1,6 @@ from .card_action import CardAction from .card_image import CardImage -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/inner_http_error.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/inner_http_error.py index 8baac4dd..44e37cbb 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/inner_http_error.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/inner_http_error.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel class InnerHttpError(AgentsModel): diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py index d52edd17..b6bcbae6 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py @@ -1,7 +1,11 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel +from typing import Generic, TypeVar, Optional -class InvokeResponse(AgentsModel): +AgentModelT = TypeVar("T", bound=AgentsModel) + + +class InvokeResponse(AgentsModel, Generic[AgentModelT]): """ Tuple class containing an HTTP Status Code and a JSON serializable object. The HTTP Status code is, in the invoke activity scenario, what will @@ -13,7 +17,7 @@ class InvokeResponse(AgentsModel): """ status: int = None - body: object = None + body: Optional[AgentModelT] = None def is_successful_status_code(self) -> bool: """ diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_card.py index 3960b9ea..94c22f7b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_card.py @@ -1,7 +1,7 @@ from .thumbnail_url import ThumbnailUrl from .media_url import MediaUrl from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_event_value.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_event_value.py index 2e0f71f6..478b19c2 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_event_value.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_event_value.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel class MediaEventValue(AgentsModel): diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_url.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_url.py index 7e26b1e1..b7b5e9a9 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_url.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/media_url.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py index e305f62d..c96d8d29 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py @@ -1,5 +1,5 @@ from .channel_account import ChannelAccount -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/message_reaction.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/message_reaction.py index 3911613e..158ea94e 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/message_reaction.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/message_reaction.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/oauth_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/oauth_card.py index acae472b..59e28692 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/oauth_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/oauth_card.py @@ -1,5 +1,5 @@ from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/paged_members_result.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/paged_members_result.py index b552d308..0718263e 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/paged_members_result.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/paged_members_result.py @@ -1,6 +1,6 @@ from .channel_account import ChannelAccount from ._type_aliases import NonEmptyString -from ._agents_model import AgentsModel +from .agents_model import AgentsModel class PagedMembersResult(AgentsModel): diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/place.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/place.py index 91fde9d7..cbf86e90 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/place.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/place.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_card.py index 7f0f3798..17f88214 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_card.py @@ -1,7 +1,7 @@ from .fact import Fact from .receipt_item import ReceiptItem from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_item.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_item.py index 97bca249..0074cc30 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_item.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/receipt_item.py @@ -1,6 +1,6 @@ from .card_image import CardImage from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/resource_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/resource_response.py index 2d482995..8f1802ea 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/resource_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/resource_response.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/semantic_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/semantic_action.py index 7660919f..bfa846aa 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/semantic_action.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/semantic_action.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/signin_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/signin_card.py index 9bd0c9e2..a6396748 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/signin_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/signin_card.py @@ -1,5 +1,5 @@ from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/suggested_actions.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/suggested_actions.py index 51e89ea1..951c82eb 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/suggested_actions.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/suggested_actions.py @@ -1,5 +1,5 @@ from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/text_highlight.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/text_highlight.py index df58cf9f..58115779 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/text_highlight.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/text_highlight.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thing.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thing.py index 8d712f8b..f2bb28c3 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thing.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thing.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_card.py index fea3bc8b..a22bffaa 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_card.py @@ -1,6 +1,6 @@ from .card_image import CardImage from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_url.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_url.py index 69876e10..3b8f3244 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_url.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/thumbnail_url.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_request.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_request.py index 625f9e53..889ef773 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_request.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_request.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_response.py index e25888d0..e09cd999 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_invoke_response.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_state.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_state.py index 88deea03..fa821449 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_state.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_exchange_state.py @@ -1,5 +1,5 @@ from .conversation_reference import ConversationReference -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_request.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_request.py index b6b31ca8..b35a5e08 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_request.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_request.py @@ -1,4 +1,4 @@ -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_response.py index 7e8c05be..682c534b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/token_response.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/transcript.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/transcript.py index 969ae709..bbcaaf84 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/transcript.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/transcript.py @@ -1,5 +1,5 @@ from .activity import Activity -from ._agents_model import AgentsModel +from .agents_model import AgentsModel class Transcript(AgentsModel): diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/video_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/video_card.py index fa2868bd..ce8f7c57 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/video_card.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/video_card.py @@ -1,7 +1,7 @@ from .thumbnail_url import ThumbnailUrl from .media_url import MediaUrl from .card_action import CardAction -from ._agents_model import AgentsModel +from .agents_model import AgentsModel from ._type_aliases import NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/turn_context_protocol.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/turn_context_protocol.py new file mode 100644 index 00000000..d614e91d --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/turn_context_protocol.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Protocol, List, Callable, Awaitable, Optional, Generic, TypeVar +from abc import abstractmethod + +from microsoft.agents.core.models import ( + Activity, + ResourceResponse, + ConversationReference, +) + +# TODO: refactor circular dependency +# from .channel_adapter_protocol import ChannelAdapterProtocol + +T = TypeVar("T", bound=Activity) + + +class TurnContextProtocol(Protocol, Generic[T]): + adapter: "ChannelAdapterProtocol" + activity: Activity | T + responded: bool + turn_state: dict + + @abstractmethod + async def send_activity( + self, + activity_or_text: Activity | str, + speak: Optional[str] = None, + input_hint: Optional[str] = None, + ) -> Optional[ResourceResponse]: + pass + + @abstractmethod + async def send_activities( + self, activities: List[Activity] + ) -> List[ResourceResponse]: + pass + + @abstractmethod + async def update_activity(self, activity: Activity) -> Optional[ResourceResponse]: + pass + + @abstractmethod + async def delete_activity( + self, id_or_reference: str | ConversationReference + ) -> None: + pass + + @abstractmethod + def on_send_activities(self, handler: Callable) -> "TurnContextProtocol": + pass + + @abstractmethod + def on_update_activity(self, handler: Callable) -> "TurnContextProtocol": + pass + + @abstractmethod + def on_delete_activity(self, handler: Callable) -> "TurnContextProtocol": + pass + + @abstractmethod + async def send_trace_activity( + self, name: str, value: object = None, value_type: str = None, label: str = None + ) -> ResourceResponse: + pass diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py index bb0a5753..cf0232ed 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py @@ -1,5 +1,11 @@ from .bot_http_adapter import BotHttpAdapter +from .channel_service_route_table import channel_service_route_table from .cloud_adapter import CloudAdapter from .jwt_authorization_middleware import jwt_authorization_middleware -__all__ = ["BotHttpAdapter", "CloudAdapter", "jwt_authorization_middleware"] +__all__ = [ + "BotHttpAdapter", + "CloudAdapter", + "jwt_authorization_middleware", + "channel_service_route_table", +] diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/channel_service_route_table.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/channel_service_route_table.py new file mode 100644 index 00000000..bc4f9f60 --- /dev/null +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/channel_service_route_table.py @@ -0,0 +1,194 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +from typing import List, Union, Type + +from aiohttp.web import RouteTableDef, Request, Response + +from microsoft.agents.core.models import ( + AgentsModel, + Activity, + AttachmentData, + ConversationParameters, + Transcript, +) +from microsoft.agents.botbuilder import ChannelApiHandlerProtocol + + +async def deserialize_from_body( + request: Request, target_model: Type[AgentsModel] +) -> Activity: + if "application/json" in request.headers["Content-Type"]: + body = await request.json() + else: + return Response(status=415) + + return target_model.model_validate(body) + + +def get_serialized_response( + model_or_list: Union[AgentsModel, List[AgentsModel]], +) -> Response: + if isinstance(model_or_list, AgentsModel): + json_obj = model_or_list.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) + else: + json_obj = [ + model.model_dump(mode="json", exclude_unset=True, by_alias=True) + for model in model_or_list + ] + + return Response(body=json.dumps(json_obj), content_type="application/json") + + +def channel_service_route_table( + handler: ChannelApiHandlerProtocol, base_url: str = "" +) -> RouteTableDef: + # pylint: disable=unused-variable + routes = RouteTableDef() + + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities") + async def send_to_conversation(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.on_send_to_conversation( + request.get("claims_identity"), + request.match_info["conversation_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.post( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def reply_to_activity(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.on_reply_to_activity( + request.get("claims_identity"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.put( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def update_activity(request: Request): + activity = await deserialize_from_body(request, Activity) + result = await handler.on_update_activity( + request.get("claims_identity"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + activity, + ) + + return get_serialized_response(result) + + @routes.delete( + base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + async def delete_activity(request: Request): + await handler.on_delete_activity( + request.get("claims_identity"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + return Response() + + @routes.get( + base_url + + "/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) + async def get_activity_members(request: Request): + result = await handler.on_get_activity_members( + request.get("claims_identity"), + request.match_info["conversation_id"], + request.match_info["activity_id"], + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/") + async def create_conversation(request: Request): + conversation_parameters = deserialize_from_body(request, ConversationParameters) + result = await handler.on_create_conversation( + request.get("claims_identity"), conversation_parameters + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/") + async def get_conversation(request: Request): + # TODO: continuation token? conversation_id? + result = await handler.on_get_conversations( + request.get("claims_identity"), None + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/v3/conversations/{conversation_id}/members") + async def get_conversation_members(request: Request): + result = await handler.on_get_conversation_members( + request.get("claims_identity"), + request.match_info["conversation_id"], + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/v3/conversations/{conversation_id}/members/{member_id}") + async def get_conversation_member(request: Request): + result = await handler.on_get_conversation_member( + request.get("claims_identity"), + request.match_info["member_id"], + request.match_info["conversation_id"], + ) + + return get_serialized_response(result) + + @routes.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers") + async def get_conversation_paged_members(request: Request): + # TODO: continuation token? page size? + result = await handler.on_get_conversation_paged_members( + request.get("claims_identity"), + request.match_info["conversation_id"], + ) + + return get_serialized_response(result) + + @routes.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}") + async def delete_conversation_member(request: Request): + result = await handler.on_delete_conversation_member( + request.get("claims_identity"), + request.match_info["conversation_id"], + request.match_info["member_id"], + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/history") + async def send_conversation_history(request: Request): + transcript = deserialize_from_body(request, Transcript) + result = await handler.on_send_conversation_history( + request.get("claims_identity"), + request.match_info["conversation_id"], + transcript, + ) + + return get_serialized_response(result) + + @routes.post(base_url + "/v3/conversations/{conversation_id}/attachments") + async def upload_attachment(request: Request): + attachment_data = deserialize_from_body(request, AttachmentData) + result = await handler.on_upload_attachment( + request.get("claims_identity"), + request.match_info["conversation_id"], + attachment_data, + ) + + return get_serialized_response(result) + + return routes diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/cloud_adapter.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/cloud_adapter.py index 30e2ee0c..436b0cfc 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/cloud_adapter.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/cloud_adapter.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +from traceback import format_exc from typing import Optional from aiohttp.web import ( @@ -42,6 +42,7 @@ def __init__( async def on_turn_error(context: TurnContext, error: Exception): error_message = f"Exception caught : {error}" + print(format_exc()) await context.send_activity(MessageFactory.text(error_message)) diff --git a/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/__init__.py b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/__init__.py new file mode 100644 index 00000000..be2e6079 --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/__init__.py @@ -0,0 +1,5 @@ +from .store_item import StoreItem +from .storage import Storage +from .memory_storage import MemoryStorage + +__all__ = ["StoreItem", "Storage", "MemoryStorage"] diff --git a/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/_type_aliases.py b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/_type_aliases.py new file mode 100644 index 00000000..f800f57f --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/_type_aliases.py @@ -0,0 +1,3 @@ +from typing import MutableMapping, Any + +JSON = MutableMapping[str, Any] diff --git a/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/memory_storage.py b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/memory_storage.py new file mode 100644 index 00000000..a3876e28 --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/memory_storage.py @@ -0,0 +1,46 @@ +from threading import Lock +from typing import TypeVar + +from ._type_aliases import JSON +from .storage import Storage +from .store_item import StoreItem + + +StoreItemT = TypeVar("StoreItemT", bound=StoreItem) + + +class MemoryStorage(Storage): + def __init__(self, state: dict[str, JSON] = None): + self._memory: dict[str, JSON] = state or {} + self._lock = Lock() + + async def read( + self, keys: list[str], *, target_cls: StoreItemT = None, **kwargs + ) -> dict[str, StoreItemT]: + result: dict[str, StoreItem] = {} + with self._lock: + for key in keys: + if key in self._memory: + try: + result[key] = target_cls.from_json_to_store_item( + self._memory[key] + ) + except TypeError as error: + raise TypeError( + f"MemoryStorage.read(): could not deserialize in-memory item into {target_cls} class. Error: {error}" + ) + return result + + async def write(self, changes: dict[str, StoreItem]): + if not changes: + raise ValueError("MemoryStorage.write(): changes cannot be None") + + with self._lock: + for key in changes: + self._memory[key] = changes[key].store_item_to_json() + + async def delete(self, keys: list[str]): + with self._lock: + for key in keys: + if key in self._memory: + del self._memory[key] diff --git a/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/storage.py b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/storage.py new file mode 100644 index 00000000..2ec1d292 --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/storage.py @@ -0,0 +1,20 @@ +from typing import Protocol, TypeVar, Type + +from ._type_aliases import JSON +from .store_item import StoreItem + + +StoreItemT = TypeVar("StoreItemT", bound=StoreItem) + + +class Storage(Protocol): + async def read( + self, keys: list[str], *, target_cls: Type[StoreItemT] = None, **kwargs + ) -> dict[str, StoreItemT]: + pass + + async def write(self, changes: dict[str, StoreItemT]) -> None: + pass + + async def delete(self, keys: list[str]) -> None: + pass diff --git a/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/store_item.py b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/store_item.py new file mode 100644 index 00000000..e2bda475 --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/microsoft/agents/storage/store_item.py @@ -0,0 +1,13 @@ +from typing import Protocol, runtime_checkable + +from ._type_aliases import JSON + + +@runtime_checkable +class StoreItem(Protocol): + def store_item_to_json(self) -> JSON: + pass + + @staticmethod + def from_json_to_store_item(json_data: JSON) -> "StoreItem": + pass diff --git a/libraries/Storage/microsoft-agents-storage/pyproject.toml b/libraries/Storage/microsoft-agents-storage/pyproject.toml new file mode 100644 index 00000000..6cad46e1 --- /dev/null +++ b/libraries/Storage/microsoft-agents-storage/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "microsoft-agents-storage" +version = "0.0.0a1" +description = "A storage library for Microsoft Agents" +authors = [{name = "Microsoft Corporation"}] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ +] + +[project.urls] +"Homepage" = "https://github.com/microsoft/microsoft-agents-protocol" diff --git a/test_samples/bot_to_bot/bot_1/app.py b/test_samples/bot_to_bot/bot_1/app.py new file mode 100644 index 00000000..525eb5aa --- /dev/null +++ b/test_samples/bot_to_bot/bot_1/app.py @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp.web import Application, Request, Response, run_app + +from microsoft.agents.botbuilder import RestChannelServiceClientFactory +from microsoft.agents.hosting.aiohttp import ( + CloudAdapter, + jwt_authorization_middleware, + channel_service_route_table, +) +from microsoft.agents.authentication import ( + Connections, + AccessTokenProviderBase, + ClaimsIdentity, +) +from microsoft.agents.authorization.msal import MsalAuth +from microsoft.agents.client import ( + ConfigurationChannelHost, + ConversationIdFactory, + HttpBotChannelFactory, +) +from microsoft.agents.storage import MemoryStorage + +from bot1 import Bot1 +from config import DefaultConfig + +AUTH_PROVIDER = MsalAuth(DefaultConfig()) + + +class DefaultConnection(Connections): + def get_default_connection(self) -> AccessTokenProviderBase: + pass + + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: + # This is the provider used for ABS + return AUTH_PROVIDER + + def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + # In this case we are using the same settings for both ABS and Channel + # This is the provider used for Channel + return AUTH_PROVIDER + + +DEFAULT_CONNECTION = DefaultConnection() +CONFIG = DefaultConfig() +CHANNEL_CLIENT_FACTORY = RestChannelServiceClientFactory(CONFIG, DEFAULT_CONNECTION) + +BOT_CHANNEL_FACTORY = HttpBotChannelFactory() +CHANNEL_HOST = ConfigurationChannelHost( + BOT_CHANNEL_FACTORY, DEFAULT_CONNECTION, CONFIG, "HttpBotClient" +) +STORAGE = MemoryStorage() +CONVERSATION_ID_FACTORY = ConversationIdFactory(STORAGE) + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +ADAPTER = CloudAdapter(CHANNEL_CLIENT_FACTORY) + +# Create the Bot +BOT = Bot1( + adapter=ADAPTER, + channel_host=CHANNEL_HOST, + conversation_id_factory=CONVERSATION_ID_FACTORY, +) + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + adapter: CloudAdapter = req.app["adapter"] + return await adapter.process(req, BOT) + + +APP = Application(middlewares=[jwt_authorization_middleware]) +APP.router.add_post("/api/messages", messages) +APP.router.add_routes(channel_service_route_table(BOT, "/api/botresponse")) +APP["bot_configuration"] = CONFIG +APP["adapter"] = ADAPTER + +if __name__ == "__main__": + try: + run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/test_samples/bot_to_bot/bot_1/bot1.py b/test_samples/bot_to_bot/bot_1/bot1.py new file mode 100644 index 00000000..3bca8de5 --- /dev/null +++ b/test_samples/bot_to_bot/bot_1/bot1.py @@ -0,0 +1,298 @@ +from typing import Optional +from uuid import uuid4 + +from aiohttp.web import HTTPException + +from microsoft.agents.core import ChannelAdapterProtocol, TurnContextProtocol +from microsoft.agents.core.models import ( + ActivityTypes, + Activity, + CallerIdConstants, + ChannelAccount, + ResourceResponse, + AttachmentData, + PagedMembersResult, + Transcript, + ConversationParameters, + ConversationResourceResponse, + ConversationsResult, +) +from microsoft.agents.authentication import ClaimsIdentity +from microsoft.agents.client import ( + ChannelHostProtocol, + ChannelInfoProtocol, + ConversationIdFactoryProtocol, + ConversationIdFactoryOptions, +) +from microsoft.agents.botbuilder import ( + ActivityHandler, + ChannelApiHandlerProtocol, + ChannelAdapter, +) + + +class Bot1(ActivityHandler, ChannelApiHandlerProtocol): + _active_bot_client = False + + def __init__( + self, + adapter: ChannelAdapterProtocol, + channel_host: ChannelHostProtocol, + conversation_id_factory: ConversationIdFactoryProtocol, + ): + if not adapter: + raise ValueError("Bot1.__init__(): adapter cannot be None") + if not channel_host: + raise ValueError("Bot1.__init__(): channel_host cannot be None") + if not conversation_id_factory: + raise ValueError("Bot1.__init__(): conversation_id_factory cannot be None") + + self._adapter = adapter + self._channel_host = channel_host + self._conversation_id_factory = conversation_id_factory + + target_b2b_id = "EchoBot" + self._target_b2b = self._channel_host.channels.get(target_b2b_id) + + async def on_turn(self, turn_context: TurnContextProtocol): + # Forward all activities except EndOfConversation to the B2B connection + if turn_context.activity.type != ActivityTypes.end_of_conversation: + # Try to get the active B2B connection + if Bot1._active_bot_client: + await self._send_to_bot(turn_context, self._target_b2b) + return + + await super().on_turn(turn_context) + + # update when doing activity type protocols + async def on_message_activity(self, turn_context: TurnContextProtocol): + if "agent" in turn_context.activity.text.lower(): + # TODO: review activity | str interface for send_activity + await turn_context.send_activity("Got it, connecting you to the agent...") + + Bot1._active_bot_client = True + + # send to bot + await self._send_to_bot(turn_context, self._target_b2b) + return + + await turn_context.send_activity('Say "agent" and I\'ll patch you through') + + async def on_end_of_conversation_activity(self, turn_context: TurnContextProtocol): + # Clear the active B2B connection + Bot1._active_bot_client = False + + # Show status message, text and value returned by the B2B connection + eoc_activity_message = f"Received {turn_context.activity.type}. Code: {turn_context.activity.code}." + if turn_context.activity.text: + eoc_activity_message += f" Text: {turn_context.activity.text}" + + if turn_context.activity.value: + eoc_activity_message += f" Value: {turn_context.activity.value}" + + await turn_context.send_activity(eoc_activity_message) + await turn_context.send_activity( + 'Back in the root bot. Say "agent" and I\'ll patch you through' + ) + + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContextProtocol + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity( + 'Hello and welcome! Say "agent" and I\'ll patch you through' + ) + + """ + ChannelApiHandler protocol + """ + + async def on_get_conversations( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + continuation_token: Optional[str] = None, + ) -> ConversationsResult: + pass + + async def on_create_conversation( + self, claims_identity: ClaimsIdentity, parameters: ConversationParameters + ) -> ConversationResourceResponse: + pass + + async def on_send_to_conversation( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity + ) -> ResourceResponse: + return await self._process_activity( + claims_identity, conversation_id, None, activity + ) + + async def on_send_conversation_history( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + transcript: Transcript, + ) -> ResourceResponse: + pass + + async def on_update_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + pass + + async def on_reply_to_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + activity_id: str, + activity: Activity, + ) -> ResourceResponse: + return await self._process_activity( + claims_identity, conversation_id, activity_id, activity + ) + + async def on_delete_activity( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str + ): + pass + + async def on_get_conversation_members( + self, claims_identity: ClaimsIdentity, conversation_id: str + ) -> list[ChannelAccount]: + pass + + async def on_get_conversation_member( + self, claims_identity: ClaimsIdentity, user_id: str, conversation_id: str + ) -> ChannelAccount: + pass + + async def on_get_conversation_paged_members( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + pass + + async def on_delete_conversation_member( + self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str + ): + pass + + async def on_get_activity_members( + self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str + ) -> list[ChannelAccount]: + pass + + async def on_upload_attachment( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + attachment_upload: AttachmentData, + ) -> ResourceResponse: + pass + + async def _send_to_bot( + self, turn_context: TurnContextProtocol, target_channel: ChannelInfoProtocol + ): + # Create a conversation ID to communicate with the B2B connection + options = ConversationIdFactoryOptions( + from_oauth_scope=turn_context.turn_state.get( + ChannelAdapter.OAUTH_SCOPE_KEY + ), + from_bot_id=self._channel_host.host_app_id, + activity=turn_context.activity, + bot=target_channel, + ) + + conversation_id = await self._conversation_id_factory.create_conversation_id( + options + ) + + # TODO: might need to close connection, tbd + channel = self._channel_host.get_channel_from_channel_info(target_channel) + + # Route activity to the B2B connection + response = await channel.post_activity( + target_channel.app_id, + target_channel.resource_url, + target_channel.endpoint, + self._channel_host.host_endpoint, + conversation_id, + turn_context.activity, + ) + + if response.status < 200 or response.status >= 300: + raise HTTPException( + text=f'Error invoking the id: "{target_channel.id}" at "{target_channel.endpoint}" (status is {response.status}). \r\n {response.body}' + ) + + @staticmethod + def _apply_activity_to_turn_context( + turn_context: TurnContextProtocol, activity: Activity + ): + # TODO: activity.properties? + turn_context.activity.channel_data = activity.channel_data + turn_context.activity.code = activity.code + turn_context.activity.entities = activity.entities + turn_context.activity.locale = activity.locale + turn_context.activity.local_timestamp = activity.local_timestamp + turn_context.activity.name = activity.name + turn_context.activity.relates_to = activity.relates_to + turn_context.activity.reply_to_id = activity.reply_to_id + turn_context.activity.timestamp = activity.timestamp + turn_context.activity.text = activity.text + turn_context.activity.type = activity.type + turn_context.activity.value = activity.value + + async def _process_activity( + self, + claims_identity: ClaimsIdentity, + conversation_id: str, + reply_to_activity_id: Optional[str], + activity: Activity, + ): + bot_conversation_reference = ( + await self._conversation_id_factory.get_bot_conversation_reference( + conversation_id + ) + ) + + resource_response: ResourceResponse = None + + async def bot_callback_handler(turn_context: TurnContextProtocol): + activity.apply_conversation_reference( + bot_conversation_reference.conversation_reference + ) + turn_context.activity.id = reply_to_activity_id + turn_context.activity.caller_id = f"{CallerIdConstants.bot_to_bot_prefix}{claims_identity.get_outgoing_app_id()}" + + if activity.type == ActivityTypes.end_of_conversation: + await self._conversation_id_factory.delete_conversation_reference( + conversation_id + ) + + Bot1._apply_activity_to_turn_context(turn_context, activity) + await self.on_turn(turn_context) + else: + nonlocal resource_response + resource_response = await turn_context.send_activity(activity) + + # TODO: fix overload + continuation_activity = ( + bot_conversation_reference.conversation_reference.get_continuation_activity() + ) + await self._adapter.continue_conversation_with_claims( + claims_identity=claims_identity, + continuation_activity=continuation_activity, + callback=bot_callback_handler, + audience=bot_conversation_reference.oauth_scope, + ) + + return resource_response or ResourceResponse(id=str(uuid4())) diff --git a/test_samples/bot_to_bot/bot_1/config.py b/test_samples/bot_to_bot/bot_1/config.py new file mode 100644 index 00000000..a4daff4b --- /dev/null +++ b/test_samples/bot_to_bot/bot_1/config.py @@ -0,0 +1,35 @@ +from microsoft.agents.authorization.msal import AuthTypes, MsalAuthConfiguration +from microsoft.agents.client import ( + ChannelHostConfiguration, + ChannelsConfiguration, + ChannelInfo, +) + + +class DefaultConfig(MsalAuthConfiguration, ChannelsConfiguration): + """Bot Configuration""" + + AUTH_TYPE = AuthTypes.client_secret + TENANT_ID = "" + CLIENT_ID = "" + CLIENT_SECRET = "" + PORT = 3978 + SCOPES = ["https://api.botframework.com/.default"] + + # ChannelHost configuration + @staticmethod + def CHANNEL_HOST_CONFIGURATION(): + return ChannelHostConfiguration( + CHANNELS=[ + ChannelInfo( + id="EchoBot", + app_id="", # Target bot's app_id + resource_url="http://localhost:3999/api/messages", + token_provider="ChannelConnection", + channel_factory="HttpBotClient", + endpoint="http://localhost:3999/api/messages", + ) + ], + HOST_ENDPOINT="http://localhost:3978/api/botresponse/", + HOST_APP_ID="", # usually the same as CLIENT_ID + ) diff --git a/test_samples/bot_to_bot/bot_2/app.py b/test_samples/bot_to_bot/bot_2/app.py new file mode 100644 index 00000000..041e7dba --- /dev/null +++ b/test_samples/bot_to_bot/bot_2/app.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from aiohttp.web import Application, Request, Response, run_app + +from microsoft.agents.botbuilder import RestChannelServiceClientFactory +from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware +from microsoft.agents.authentication import ( + Connections, + AccessTokenProviderBase, + ClaimsIdentity, +) +from microsoft.agents.authorization.msal import MsalAuth + +from bot2 import Bot2 +from config import DefaultConfig + +AUTH_PROVIDER = MsalAuth(DefaultConfig()) + + +class DefaultConnection(Connections): + def get_default_connection(self) -> AccessTokenProviderBase: + pass + + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: + return AUTH_PROVIDER + + def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + pass + + +CONFIG = DefaultConfig() +CHANNEL_CLIENT_FACTORY = RestChannelServiceClientFactory(CONFIG, DefaultConnection()) + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +ADAPTER = CloudAdapter(CHANNEL_CLIENT_FACTORY) + +# Create the Bot +BOT = Bot2() + + +# Listen for incoming requests on /api/messages +async def messages(req: Request) -> Response: + adapter: CloudAdapter = req.app["adapter"] + return await adapter.process(req, BOT) + + +APP = Application(middlewares=[jwt_authorization_middleware]) +APP.router.add_post("/api/messages", messages) +APP["bot_configuration"] = CONFIG +APP["adapter"] = ADAPTER + +if __name__ == "__main__": + try: + run_app(APP, host="localhost", port=CONFIG.PORT) + except Exception as error: + raise error diff --git a/test_samples/bot_to_bot/bot_2/bot2.py b/test_samples/bot_to_bot/bot_2/bot2.py new file mode 100644 index 00000000..43530c77 --- /dev/null +++ b/test_samples/bot_to_bot/bot_2/bot2.py @@ -0,0 +1,35 @@ +from microsoft.agents.botbuilder import ActivityHandler, MessageFactory, TurnContext +from microsoft.agents.core.models import ( + ChannelAccount, + Activity, + EndOfConversationCodes, +) + + +class Bot2(ActivityHandler): + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hi, This is Bot2") + + async def on_message_activity(self, turn_context: TurnContext): + if any( + stop_message in turn_context.activity.text + for stop_message in ["stop", "end"] + ): + await turn_context.send_activity("(Bot2) Ending conversation from Bot2") + end_of_conversation = Activity.create_end_of_conversation_activity() + end_of_conversation.code = EndOfConversationCodes.completed_successfully + await turn_context.send_activity(end_of_conversation) + else: + await turn_context.send_activity( + f"Echo(Bot2): {turn_context.activity.text}" + ) + await turn_context.send_activity( + 'Echo(Bot2): Say "end" or "stop" and I\'ll end the conversation and return to the parent.' + ) + + async def on_end_of_conversation_activity(self, turn_context): + return diff --git a/test_samples/bot_to_bot/bot_2/config.py b/test_samples/bot_to_bot/bot_2/config.py new file mode 100644 index 00000000..e3ab5f8d --- /dev/null +++ b/test_samples/bot_to_bot/bot_2/config.py @@ -0,0 +1,11 @@ +from microsoft.agents.authorization.msal import AuthTypes, MsalAuthConfiguration + + +class DefaultConfig(MsalAuthConfiguration): + """Bot Configuration""" + + AUTH_TYPE = AuthTypes.client_secret + TENANT_ID = "" + CLIENT_ID = "" + CLIENT_SECRET = "" + PORT = 3999 diff --git a/test_samples/echo_bot/app.py b/test_samples/echo_bot/app.py index 3c7d561b..5a1af1fb 100644 --- a/test_samples/echo_bot/app.py +++ b/test_samples/echo_bot/app.py @@ -5,18 +5,26 @@ from microsoft.agents.botbuilder import RestChannelServiceClientFactory from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_middleware -from microsoft.agents.authentication import Connections, AccessTokenProviderBase, ClaimsIdentity +from microsoft.agents.authentication import ( + Connections, + AccessTokenProviderBase, + ClaimsIdentity, +) from microsoft.agents.authorization.msal import MsalAuth from echo_bot import EchoBot from config import DefaultConfig AUTH_PROVIDER = MsalAuth(DefaultConfig()) + + class DefaultConnection(Connections): def get_default_connection(self) -> AccessTokenProviderBase: pass - def get_token_provider(self, claims_identity: ClaimsIdentity, service_url: str) -> AccessTokenProviderBase: + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: return AUTH_PROVIDER def get_connection(self, connection_name: str) -> AccessTokenProviderBase: @@ -49,4 +57,4 @@ async def messages(req: Request) -> Response: try: run_app(APP, host="localhost", port=CONFIG.PORT) except Exception as error: - raise error \ No newline at end of file + raise error diff --git a/test_samples/echo_bot/config.py b/test_samples/echo_bot/config.py index aa9b31c8..1fe1ea0c 100644 --- a/test_samples/echo_bot/config.py +++ b/test_samples/echo_bot/config.py @@ -1,7 +1,9 @@ from microsoft.agents.authorization.msal import AuthTypes, MsalAuthConfiguration + class DefaultConfig(MsalAuthConfiguration): - """ Bot Configuration """ + """Bot Configuration""" + AUTH_TYPE = AuthTypes.client_secret TENANT_ID = "" CLIENT_ID = "" diff --git a/test_samples/echo_bot/echo_bot.py b/test_samples/echo_bot/echo_bot.py index c3660866..74f722b0 100644 --- a/test_samples/echo_bot/echo_bot.py +++ b/test_samples/echo_bot/echo_bot.py @@ -13,4 +13,4 @@ async def on_members_added_activity( async def on_message_activity(self, turn_context: TurnContext): return await turn_context.send_activity( MessageFactory.text(f"Echo: {turn_context.activity.text}") - ) \ No newline at end of file + )