diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index 40c06d91e..8d7b7cccc 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -8,6 +8,26 @@ class ActivityHandler: async def on_turn(self, turn_context: TurnContext): + """ + Called by the adapter (for example, :class:`BotFrameworkAdapter`) at runtime + in order to process an inbound :class:`botbuilder.schema.Activity`. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. remarks:: + It calls other methods in this class based on the type of the activity to + process, which allows a derived class to provide type-specific logic in a controlled way. + In a derived class, override this method to add logic that applies to all activity types. + + .. note:: + - Add logic to apply before the type-specific logic and before the call to the + :meth:`ActivityHandler.on_turn()` method. + - Add logic to apply after the type-specific logic after the call to the + :meth:`ActivityHandler.on_turn()` method. + """ if turn_context is None: raise TypeError("ActivityHandler.on_turn(): turn_context cannot be None.") @@ -40,9 +60,38 @@ async def on_turn(self, turn_context: TurnContext): async def on_message_activity( # pylint: disable=unused-argument self, turn_context: TurnContext ): + """ + Override this method in a derived class to provide logic specific to activities, + such as the conversational logic. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + """ return async def on_conversation_update_activity(self, turn_context: TurnContext): + """ + Invoked when a conversation update activity is received from the channel when the base behavior of + :meth:`ActivityHandler.on_turn()` is used. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_turn()` method receives a conversation update activity, it calls this + method. + If the conversation update activity indicates that members other than the bot joined the conversation, + it calls the :meth:`ActivityHandler.on_members_added_activity()` method. + If the conversation update activity indicates that members other than the bot left the conversation, + it calls the :meth:`ActivityHandler.on_members_removed_activity()` method. + In a derived class, override this method to add logic that applies to all conversation update activities. + Add logic to apply before the member added or removed logic before the call to this base class method. + """ if ( turn_context.activity.members_added is not None and turn_context.activity.members_added @@ -62,14 +111,75 @@ async def on_conversation_update_activity(self, turn_context: TurnContext): async def on_members_added_activity( self, members_added: List[ChannelAccount], turn_context: TurnContext ): # pylint: disable=unused-argument + """ + Override this method in a derived class to provide logic for when members other than the bot join + the conversation. You can add your bot's welcome logic. + + :param members_added: A list of all the members added to the conversation, as described by the + conversation update activity + :type members_added: :class:`typing.List` + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_conversation_update_activity()` method receives a conversation + update activity that indicates + one or more users other than the bot are joining the conversation, it calls this method. + """ return async def on_members_removed_activity( self, members_removed: List[ChannelAccount], turn_context: TurnContext ): # pylint: disable=unused-argument + """ + Override this method in a derived class to provide logic for when members other than the bot leave + the conversation. You can add your bot's good-bye logic. + + :param members_added: A list of all the members removed from the conversation, as described by the + conversation update activity + :type members_added: :class:`typing.List` + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_conversation_update_activity()` method receives a conversation + update activity that indicates one or more users other than the bot are leaving the conversation, + it calls this method. + """ + return async def on_message_reaction_activity(self, turn_context: TurnContext): + """ + Invoked when an event activity is received from the connector when the base behavior of + :meth:'ActivityHandler.on_turn()` is used. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji) to a previously + sent activity. + Message reactions are only supported by a few channels. The activity that the message reaction corresponds + to is indicated in the reply to Id property. The value of this property is the activity id of a previously + sent activity given back to the bot as the response from a send call. + When the :meth:'ActivityHandler.on_turn()` method receives a message reaction activity, it calls this + method. + If the message reaction indicates that reactions were added to a message, it calls + :meth:'ActivityHandler.on_reaction_added(). + If the message reaction indicates that reactions were removed from a message, it calls + :meth:'ActivityHandler.on_reaction_removed(). + In a derived class, override this method to add logic that applies to all message reaction activities. + Add logic to apply before the reactions added or removed logic before the call to the this base class + method. + Add logic to apply after the reactions added or removed logic after the call to the this base class method. + """ if turn_context.activity.reactions_added is not None: await self.on_reactions_added( turn_context.activity.reactions_added, turn_context @@ -83,14 +193,72 @@ 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 ): + """ + Override this method in a derived class to provide logic for when reactions to a previous activity + are added to the conversation. + + :param message_reactions: The list of reactions added + :type message_reactions: :class:`typing.List` + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji) + to a previously sent message on the conversation. Message reactions are supported by only a few channels. + The activity that the message is in reaction to is identified by the activity's reply to Id property. + The value of this property is the activity ID of a previously sent activity. When the bot sends an activity, + the channel assigns an ID to it, which is available in the resource response Id of the result. + """ return async def on_reactions_removed( # pylint: disable=unused-argument self, message_reactions: List[MessageReaction], turn_context: TurnContext ): + """ + Override this method in a derived class to provide logic for when reactions to a previous activity + are removed from the conversation. + + :param message_reactions: The list of reactions removed + :type message_reactions: :class:`typing.List` + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji) + to a previously sent message on the conversation. Message reactions are supported by only a few channels. + The activity that the message is in reaction to is identified by the activity's reply to Id property. + The value of this property is the activity ID of a previously sent activity. When the bot sends an activity, + the channel assigns an ID to it, which is available in the resource response Id of the result. + """ return async def on_event_activity(self, turn_context: TurnContext): + """ + Invoked when an event activity is received from the connector when the base behavior of + :meth:'ActivityHandler.on_turn()` is used. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_turn()` method receives an event activity, it calls this method. + If the activity name is `tokens/response`, it calls :meth:'ActivityHandler.on_token_response_event()`; + otherwise, it calls :meth:'ActivityHandler.on_event()`. + + In a derived class, override this method to add logic that applies to all event activities. + Add logic to apply before the specific event-handling logic before the call to this base class method. + Add logic to apply after the specific event-handling logic after the call to this base class method. + + Event activities communicate programmatic information from a client or channel to a bot. + The meaning of an event activity is defined by the event activity name property, which is meaningful within + the scope of a channel. + """ if turn_context.activity.name == "tokens/response": return await self.on_token_response_event(turn_context) @@ -99,19 +267,70 @@ async def on_event_activity(self, turn_context: TurnContext): async def on_token_response_event( # pylint: disable=unused-argument self, turn_context: TurnContext ): + """ + Invoked when a `tokens/response` event is received when the base behavior of + :meth:'ActivityHandler.on_event_activity()` is used. + If using an `oauth_prompt`, override this method to forward this activity to the current dialog. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_event()` method receives an event with an activity name of + `tokens/response`, it calls this method. If your bot uses an `oauth_prompt`, forward the incoming + activity to the current dialog. + """ return async def on_event( # pylint: disable=unused-argument self, turn_context: TurnContext ): + """ + Invoked when an event other than `tokens/response` is received when the base behavior of + :meth:'ActivityHandler.on_event_activity()` is used. + + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:'ActivityHandler.on_event_activity()` is used method receives an event with an + activity name other than `tokens/response`, it calls this method. + This method could optionally be overridden if the bot is meant to handle miscellaneous events. + """ return async def on_end_of_conversation_activity( # pylint: disable=unused-argument self, turn_context: TurnContext ): + """ + Invoked when a conversation end activity is received from the channel. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :returns: A task that represents the work queued to execute + """ return async def on_unrecognized_activity_type( # pylint: disable=unused-argument self, turn_context: TurnContext ): + """ + Invoked when an activity other than a message, conversation update, or event is received when the base + behavior of :meth:`ActivityHandler.on_turn()` is used. + If overridden, this method could potentially respond to any of the other activity types. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + + :returns: A task that represents the work queued to execute + + .. note:: + When the :meth:`ActivityHandler.on_turn()` method receives an activity that is not a message, + conversation update, message reaction, or event activity, it calls this method. + """ return diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 7309bdbef..6f4681437 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -7,15 +7,7 @@ import os from typing import List, Callable, Awaitable, Union, Dict from msrest.serialization import Model -from botbuilder.schema import ( - Activity, - ActivityTypes, - ConversationAccount, - ConversationParameters, - ConversationReference, - TokenResponse, - ResourceResponse, -) + from botframework.connector import Channels, EmulatorApiClient from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ( @@ -33,6 +25,15 @@ ) from botframework.connector.token_api import TokenApiClient from botframework.connector.token_api.models import TokenStatus +from botbuilder.schema import ( + Activity, + ActivityTypes, + ConversationAccount, + ConversationParameters, + ConversationReference, + TokenResponse, + ResourceResponse, +) from . import __version__ from .bot_adapter import BotAdapter @@ -83,6 +84,29 @@ def __init__( channel_provider: ChannelProvider = None, auth_configuration: AuthenticationConfiguration = None, ): + """ + Contains the settings used to initialize a :class:`BotFrameworkAdapter` instance. + + :param app_id: The bot application ID. This is the appId returned by the Azure portal registration, and is + the value of the `MicrosoftAppId` parameter in the `config.py` file. + :type app_id: str + :param app_password: The bot application password. This is the password returned by the Azure portal + registration, and is + the value os the `MicrosoftAppPassword` parameter in the `config.py` file. + :type app_password: str + :param channel_auth_tenant: The channel tenant to use in conversation + :type channel_auth_tenant: str + :param oauth_endpoint: + :type oauth_endpoint: str + :param open_id_metadata: + :type open_id_metadata: str + :param channel_service: + :type channel_service: str + :param channel_provider: The channel provider + :type channel_provider: :class:`botframework.connector.auth.ChannelProvider` + :param auth_configuration: + :type auth_configuration: :class:`botframework.connector.auth.AuthenticationConfiguration` + """ self.app_id = app_id self.app_password = app_password self.channel_auth_tenant = channel_auth_tenant @@ -94,9 +118,29 @@ def __init__( class BotFrameworkAdapter(BotAdapter, UserTokenProvider): + """ + Defines an adapter to connect a bot to a service endpoint. + + .. remarks:: + The bot adapter encapsulates authentication processes and sends activities to and + receives activities from the Bot Connector Service. When your bot receives an activity, + the adapter creates a context object, passes it to your bot's application logic, and + sends responses back to the user's channel. + The adapter processes and directs incoming activities in through the bot middleware + pipeline to your bot’s logic and then back out again. + As each activity flows in and out of the bot, each piece of middleware can inspect or act + upon the activity, both before and after the bot logic runs. + """ + _INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse" def __init__(self, settings: BotFrameworkAdapterSettings): + """ + Initializes a new instance of the :class:`BotFrameworkAdapter` class. + + :param settings: The settings to initialize the adapter + :type settings: :class:`BotFrameworkAdapterSettings` + """ super(BotFrameworkAdapter, self).__init__() self.settings = settings or BotFrameworkAdapterSettings("", "") self.settings.channel_service = self.settings.channel_service or os.environ.get( @@ -141,17 +185,27 @@ async def continue_conversation( claims_identity: ClaimsIdentity = None, # pylint: disable=unused-argument ): """ - Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging" - flow as its lets the bot proactively send messages to a conversation or user that its already - communicated with. Scenarios like sending notifications or coupons to a user are enabled by this - method. - :param bot_id: - :param reference: - :param callback: - :param claims_identity: - :return: + Continues a conversation with a user. + + :param reference: A reference to the conversation to continue + :type reference: :class:`botbuilder.schema.ConversationReference + :param callback: The method to call for the resulting bot turn + :type callback: :class:`typing.Callable` + :param bot_id: The application Id of the bot. This is the appId returned by the Azure portal registration, + and is generally found in the `MicrosoftAppId` parameter in `config.py`. + :type bot_id: :class:`typing.str` + :param claims_identity: The bot claims identity + :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity` + + :raises: It raises an argument null exception. + + :return: A task that represents the work queued to execute. + + .. note:: + This is often referred to as the bots *proactive messaging* flow as it lets the bot proactively + send messages to a conversation or user that are already in a communication. + Scenarios such as sending notifications or coupons to a user are enabled by this function. """ - # TODO: proactive messages if not claims_identity: if not bot_id: @@ -177,11 +231,28 @@ async def create_conversation( conversation_parameters: ConversationParameters = None, ): """ - Starts a new conversation with a user. This is typically used to Direct Message (DM) a member - of a group. - :param reference: - :param logic: - :return: + Starts a new conversation with a user. Used to direct message to a member of a group. + + :param reference: The conversation reference that contains the tenant + :type reference: :class:`botbuilder.schema.ConversationReference` + :param logic: The logic to use for the creation of the conversation + :type logic: :class:`typing.Callable` + :param conversation_parameters: The information to use to create the conversation + :type conversation_parameters: + + :raises: It raises a generic exception error. + + :return: A task representing the work queued to execute. + + .. note:: + To start a conversation, your bot must know its account information and the user's + account information on that channel. + Most channels only support initiating a direct message (non-group) conversation. + The adapter attempts to create a new conversation on the channel, and + then sends a conversation update activity through its middleware pipeline + to the the callback method. + If the conversation is established with the specified users, the ID of the activity + will contain the ID of the new conversation. """ try: if reference.service_url is None: @@ -232,13 +303,26 @@ async def create_conversation( async def process_activity(self, req, auth_header: str, logic: Callable): """ + Creates a turn context and runs the middleware pipeline for an incoming activity, Processes an activity received by the bots web server. This includes any messages sent from a - user and is the method that drives what's often referred to as the bots "Reactive Messaging" - flow. - :param req: - :param auth_header: - :param logic: - :return: + user and is the method that drives what's often referred to as the bots *reactive messaging* flow. + + :param req: The incoming activity + :type req: :class:`typing.str` + :param auth_header: The HTTP authentication header of the request + :type auth_header: :class:`typing.str` + :param logic: The logic to execute at the end of the adapter's middleware pipeline. + :type logic: :class:`typing.Callable` + + :return: A task that represents the work queued to execute. If the activity type + was `Invoke` and the corresponding key (`channelId` + `activityId`) was found then + an :class:`InvokeResponse` is returned; otherwise, `null` is returned. + + .. note:: + Call this method to reactively send a message to a conversation. + If the task completes successfully, then an :class:`InvokeResponse` is returned; + otherwise. `null` is returned. + """ activity = await self.parse_request(req) auth_header = auth_header or "" @@ -281,9 +365,15 @@ async def authenticate_request( ) -> ClaimsIdentity: """ Allows for the overriding of authentication in unit tests. - :param request: - :param auth_header: - :return: + + :param request: The request to authenticate + :type request: :class:`botbuilder.schema.Activity` + :param auth_header: The authentication header + + :raises: A permission exception error. + + :return: The request claims identity + :rtype: :class:`botframework.connector.auth.ClaimsIdentity` """ claims = await JwtTokenValidation.authenticate_request( request, @@ -354,9 +444,22 @@ async def update_activity(self, context: TurnContext, activity: Activity): """ Replaces an activity that was previously sent to a channel. It should be noted that not all channels support this feature. - :param context: - :param activity: - :return: + + :param context: The context object for the turn + :type context: :class:`TurnContext' + :param activity: New replacement activity + :type activity: :class:`botbuilder.schema.Activity` + + :raises: A generic exception error + + :return: A task that represents the work queued to execute + + .. note:: + If the activity is successfully sent, the task result contains + a :class:`botbuilder.schema.ResourceResponse` object containing the ID that + the receiving channel assigned to the activity. + Before calling this function, set the ID of the replacement activity to the ID + of the activity to replace. """ try: identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) @@ -373,9 +476,18 @@ async def delete_activity( """ Deletes an activity that was previously sent to a channel. It should be noted that not all channels support this feature. - :param context: - :param reference: - :return: + + :param context: The context object for the turn + :type context: :class:`TurnContext' + :param reference: Conversation reference for the activity to delete + :type reference: :class:`botbuilder.schema.ConversationReference` + + :raises: A exception error + + :return: A task that represents the work queued to execute + + .. note:: + The activity_id of the :class:`botbuilder.schema.ConversationReference` identifies the activity to delete. """ try: identity: ClaimsIdentity = context.turn_state.get(BOT_IDENTITY_KEY) @@ -448,9 +560,15 @@ async def delete_conversation_member( ) -> None: """ Deletes a member from the current conversation. - :param context: - :param member_id: - :return: + + :param context: The context object for the turn + :type context: :class:`TurnContext` + :param member_id: The ID of the member to remove from the conversation + :type member_id: str + + :raises: A exception error + + :return: A task that represents the work queued to execute. TokenResponse: + + """ + Attempts to retrieve the token for a user that's in a login flow. + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + :param connection_name: Name of the auth connection to use + :type connection_name: str + :param magic_code" (Optional) user entered code to validate + :str magic_code" str + + :raises: An exception error + + :returns: Token Response + :rtype: :class:'botbuilder.schema.TokenResponse` + + """ + if ( context.activity.from_property is None or not context.activity.from_property.id @@ -585,6 +745,18 @@ async def get_user_token( async def sign_out_user( self, context: TurnContext, connection_name: str = None, user_id: str = None ) -> str: + """ + Signs the user out with the token server. + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + :param connection_name: Name of the auth connection to use + :type connection_name: str + :param user_id: User id of user to sign out + :type user_id: str + + :returns: A task that represents the work queued to execute + """ if not context.activity.from_property or not context.activity.from_property.id: raise Exception( "BotFrameworkAdapter.sign_out_user(): missing from_property or from_property.id" @@ -602,6 +774,19 @@ async def sign_out_user( async def get_oauth_sign_in_link( self, context: TurnContext, connection_name: str ) -> str: + """ + Gets the raw sign-in link to be sent to the user for sign-in for a connection name. + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + :param connection_name: Name of the auth connection to use + :type connection_name: str + + :returns: A task that represents the work queued to execute + + .. note:: + If the task completes successfully, the result contains the raw sign-in link + """ self.check_emulating_oauth_cards(context) conversation = TurnContext.get_conversation_reference(context.activity) url = self.oauth_api_url(context) @@ -621,6 +806,21 @@ async def get_oauth_sign_in_link( async def get_token_status( self, context: TurnContext, user_id: str = None, include_filter: str = None ) -> List[TokenStatus]: + + """ + Retrieves the token status for each configured connection for the given user. + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + :param user_id: The user Id for which token status is retrieved + :type user_id: str + :param include_filter: (Optional) Comma separated list of connection's to include. + Blank will return token status for all configured connections. + :type include_filter: str + + :returns: Array of :class:`botframework.connector.token_api.modelsTokenStatus` + """ + if not user_id and ( not context.activity.from_property or not context.activity.from_property.id ): @@ -641,6 +841,21 @@ async def get_token_status( async def get_aad_tokens( self, context: TurnContext, connection_name: str, resource_urls: List[str] ) -> Dict[str, TokenResponse]: + """ + Retrieves Azure Active Directory tokens for particular resources on a configured connection. + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + + :param connection_name: The name of the Azure Active Directory connection configured with this bot + :type connection_name: str + + :param resource_urls: The list of resource URLs to retrieve tokens for + :type resource_urls: :class:`typing.List` + + :returns: Dictionary of resource Urls to the corresponding :class:'botbuilder.schema.TokenResponse` + :rtype: :class:`typing.Dict` + """ if not context.activity.from_property or not context.activity.from_property.id: raise Exception( "BotFrameworkAdapter.get_aad_tokens(): missing from_property or from_property.id" @@ -659,11 +874,11 @@ async def get_aad_tokens( async def create_connector_client( self, service_url: str, identity: ClaimsIdentity = None ) -> ConnectorClient: - """ - Allows for mocking of the connector client in unit tests. - :param service_url: - :param identity: - :return: + """Allows for mocking of the connector client in unit tests + :param service_url: The service URL + :param identity: The claims identity + + :return: An instance of the :class:`ConnectorClient` class """ if identity: bot_app_id_claim = identity.claims.get( diff --git a/libraries/botbuilder-core/botbuilder/core/bot_state.py b/libraries/botbuilder-core/botbuilder/core/bot_state.py index 4e615dda0..7f2c26984 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_state.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_state.py @@ -13,10 +13,11 @@ class CachedBotState: """ - Internal cached bot state. + Internal cached bot state. """ def __init__(self, state: Dict[str, object] = None): + self.state = state if state is not None else {} self.hash = self.compute_hash(state) @@ -29,17 +30,45 @@ def compute_hash(self, obj: object) -> str: class BotState(PropertyManager): + """ + Defines a state management object and automates the reading and writing of + associated state properties to a storage layer. + + .. remarks:: + Each state management object defines a scope for a storage layer. + State properties are created within a state management scope, and the Bot Framework + defines these scopes: :class:`ConversationState`, :class:`UserState`, and :class:`PrivateConversationState`. + You can define additional scopes for your bot. + """ + def __init__(self, storage: Storage, context_service_key: str): + """ + Initializes a new instance of the :class:`BotState` class. + + :param storage: The storage layer this state management object will use to store and retrieve state + :type storage: :class:`bptbuilder.core.Storage` + :param context_service_key: The key for the state cache for this :class:`BotState` + :type context_service_key: str + + .. note:: + This constructor creates a state management object and associated scope. The object uses + the :param storage: to persist state property values and the :param context_service_key: to cache state + within the context for each turn. + + :raises: It raises an argument null exception. + """ self.state_key = "state" self._storage = storage self._context_service_key = context_service_key def create_property(self, name: str) -> StatePropertyAccessor: """ - Create a property definition and register it with this BotState. - :param name: The name of the property. - :param force: - :return: If successful, the state property accessor created. + Create a property definition and register it with this :class:`BotState`. + + :param name: The name of the property + :type name: str + :return: If successful, the state property accessor created + :rtype: :class:`StatePropertyAccessor` """ if not name: raise TypeError("BotState.create_property(): name cannot be None or empty.") @@ -52,9 +81,12 @@ def get(self, turn_context: TurnContext) -> Dict[str, object]: async def load(self, turn_context: TurnContext, force: bool = False) -> None: """ - Reads in the current state object and caches it in the context object for this turm. - :param turn_context: The context object for this turn. - :param force: Optional. True to bypass the cache. + Reads the current state object and caches it in the context object for this turn. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :param force: Optional, true to bypass the cache + :type force: bool """ if turn_context is None: raise TypeError("BotState.load(): turn_context cannot be None.") @@ -71,10 +103,13 @@ async def save_changes( self, turn_context: TurnContext, force: bool = False ) -> None: """ - If it has changed, writes to storage the state object that is cached in the current context object - for this turn. - :param turn_context: The context object for this turn. - :param force: Optional. True to save state to storage whether or not there are changes. + Saves the state cached in the current context for this turn. + If the state has changed, it saves the state cached in the current context for this turn. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :param force: Optional, true to save state to storage whether or not there are changes + :type force: bool """ if turn_context is None: raise TypeError("BotState.save_changes(): turn_context cannot be None.") @@ -90,9 +125,14 @@ async def save_changes( async def clear_state(self, turn_context: TurnContext): """ Clears any state currently stored in this state scope. - NOTE: that save_changes must be called in order for the cleared state to be persisted to the underlying store. - :param turn_context: The context object for this turn. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :return: None + + .. note:: + This function must be called in order for the cleared state to be persisted to the underlying store. """ if turn_context is None: raise TypeError("BotState.clear_state(): turn_context cannot be None.") @@ -104,8 +144,11 @@ async def clear_state(self, turn_context: TurnContext): async def delete(self, turn_context: TurnContext) -> None: """ - Delete any state currently stored in this state scope. - :param turn_context: The context object for this turn. + Deletes any state currently stored in this state scope. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :return: None """ if turn_context is None: @@ -121,6 +164,16 @@ def get_storage_key(self, turn_context: TurnContext) -> str: raise NotImplementedError() async def get_property_value(self, turn_context: TurnContext, property_name: str): + """ + Gets the value of the specified property in the turn context. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :param property_name: The property name + :type property_name: str + + :return: The value of the property + """ if turn_context is None: raise TypeError( "BotState.get_property_value(): turn_context cannot be None." @@ -140,11 +193,14 @@ async def delete_property_value( ) -> None: """ Deletes a property from the state cache in the turn context. - :param turn_context: The context object for this turn. - :param property_name: The name of the property to delete. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :param property_name: The name of the property to delete + :type property_name: str + :return: None """ - if turn_context is None: raise TypeError("BotState.delete_property(): turn_context cannot be None.") if not property_name: @@ -156,12 +212,17 @@ async def set_property_value( self, turn_context: TurnContext, property_name: str, value: object ) -> None: """ - Deletes a property from the state cache in the turn context. - :param turn_context: The context object for this turn. - :param property_name: The value to set on the property. + Sets a property to the specified value in the turn context. + + :param turn_context: The context object for this turn + :type turn_context: :class:`TurnContext` + :param property_name: The property name + :type property_name: str + :param value: The value to assign to the property + :type value: Object + :return: None """ - if turn_context is None: raise TypeError("BotState.delete_property(): turn_context cannot be None.") if not property_name: diff --git a/libraries/botbuilder-core/botbuilder/core/conversation_state.py b/libraries/botbuilder-core/botbuilder/core/conversation_state.py index 94bc8fedb..445b8949d 100644 --- a/libraries/botbuilder-core/botbuilder/core/conversation_state.py +++ b/libraries/botbuilder-core/botbuilder/core/conversation_state.py @@ -7,16 +7,44 @@ class ConversationState(BotState): - """Conversation State - Reads and writes conversation state for your bot to storage. + """ + Defines a state management object for conversation state. + Extends :class:`BootState` base class. + + .. remarks:: + Conversation state is available in any turn in a specific conversation, regardless of the user, such as + in a group conversation. """ no_key_error_message = "ConversationState: channelId and/or conversation missing from context.activity." def __init__(self, storage: Storage): + """ + Creates a :class:`ConversationState` instance. + + Creates a new instance of the :class:`ConversationState` class. + :param storage: The storage containing the conversation state. + :type storage: :class:`Storage` + """ super(ConversationState, self).__init__(storage, "ConversationState") def get_storage_key(self, turn_context: TurnContext) -> object: + """ + Gets the key to use when reading and writing state to and from storage. + + :param turn_context: The context object for this turn. + :type turn_context: :class:`TurnContext` + + :raise: :class:`TypeError` if the :meth:`TurnContext.activity` for the current turn is missing + :class:`botbuilder.schema.Activity` channelId or conversation information or the conversation's + account id is missing. + + :return: The storage key. + :rtype: str + + .. remarks:: + Conversation state includes the channel ID and conversation ID as part of its storage key. + """ channel_id = turn_context.activity.channel_id or self.__raise_type_error( "invalid activity-missing channel_id" ) @@ -31,4 +59,7 @@ def get_storage_key(self, turn_context: TurnContext) -> object: return storage_key def __raise_type_error(self, err: str = "NoneType found while expecting value"): + """ Raise type error exception + :raises: :class:`TypeError` + """ raise TypeError(err) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py index b4c531b23..6857ad5b4 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py @@ -14,9 +14,25 @@ class ComponentDialog(Dialog): + """ + A :class:`Dialog` that is composed of other dialogs + + A component dialog has an inner :class:`DialogSet` :class:`DialogContext`, + which provides an inner dialog stack that is hidden from the parent dialog. + + :var persisted_dialog state: + :vartype persisted_dialog_state: str + """ + persisted_dialog_state = "dialogs" def __init__(self, dialog_id: str): + """ + Initializes a new instance of the :class:`ComponentDialog` + + :param dialog_id: The ID to assign to the new dialog within the parent dialog set. + :type dialog_id: str + """ super(ComponentDialog, self).__init__(dialog_id) if dialog_id is None: @@ -30,6 +46,19 @@ def __init__(self, dialog_id: str): async def begin_dialog( self, dialog_context: DialogContext, options: object = None ) -> DialogTurnResult: + """ + Called when the dialog is started and pushed onto the parent's dialog stack. + + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. + + :param dialog_context: The :class:`DialogContext` for the current turn of the conversation. + :type dialog_context: :class:`DialogContext` + :param options: Optional, initial information to pass to the dialog. + :type options: object + :return: Signals the end of the turn + :rtype: :class:`Dialog.end_of_turn` + """ if dialog_context is None: raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.") @@ -49,6 +78,27 @@ async def begin_dialog( return Dialog.end_of_turn async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult: + """ + Called when the dialog is continued, where it is the active dialog and the + user replies with a new activity. + + .. note:: + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. The result may also + contain a return value. + + If this method is *not* overriden the component dialog calls the + :meth:`DialogContext.continue_dialog` method on it's inner dialog + context. If the inner dialog stack is empty, the component dialog ends, + and if a :class:`DialogTurnResult.result` is available, the component dialog + uses that as it's return value. + + + :param dialog_context: The parent :class:`DialogContext` for the current turn of the conversation. + :type dialog_context: :class:`DialogContext` + :return: Signals the end of the turn + :rtype: :class:`Dialog.end_of_turn` + """ if dialog_context is None: raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.") # Continue execution of inner dialog. @@ -65,17 +115,52 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu async def resume_dialog( self, dialog_context: DialogContext, reason: DialogReason, result: object = None ) -> DialogTurnResult: - # Containers are typically leaf nodes on the stack but the dev is free to push other dialogs - # on top of the stack which will result in the container receiving an unexpected call to - # resume_dialog() when the pushed on dialog ends. - # To avoid the container prematurely ending we need to implement this method and simply - # ask our inner dialog stack to re-prompt. + """ + Called when a child dialog on the parent's dialog stack completed this turn, returning + control to this dialog component. + + .. note:: + Containers are typically leaf nodes on the stack but the dev is free to push other dialogs + on top of the stack which will result in the container receiving an unexpected call to + :meth:resume_dialog() when the pushed on dialog ends. + To avoid the container prematurely ending we need to implement this method and simply + ask our inner dialog stack to re-prompt. + + If the task is successful, the result indicates whether this dialog is still + active after this dialog turn has been processed. + + Generally, the child dialog was started with a call to :meth:`def async begin_dialog()` + in the parent's context. However, if the :meth:`DialogContext.replace_dialog()` method is + is called, the logical child dialog may be different than the original. + + If this method is *not* overridden, the dialog automatically calls its + :meth:`asyn def reprompt_dialog()` when the user replies. + + :param dialog_context: The :class:`DialogContext` for the current turn of the conversation. + :type dialog_context: :class:`DialogContext` + :param reason: Reason why the dialog resumed. + :type reason: :class:`DialogReason` + :param result: Optional, value returned from the dialog that was called. The type of the + value returned is dependent on the child dialog. + :type result: object + :return: Signals the end of the turn + :rtype: :class:`Dialog.end_of_turn` + """ + await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog) return Dialog.end_of_turn async def reprompt_dialog( self, context: TurnContext, instance: DialogInstance ) -> None: + """ + Called when the dialog should re-prompt the user for input. + + :param context: The context object for this turn. + :type context: :class:`TurnContext` + :param instance: State information for this dialog. + :type instance: :class:`DialogInstance` + """ # Delegate to inner dialog. dialog_state = instance.state[self.persisted_dialog_state] inner_dc = DialogContext(self._dialogs, context, dialog_state) @@ -87,7 +172,18 @@ async def reprompt_dialog( async def end_dialog( self, context: TurnContext, instance: DialogInstance, reason: DialogReason ) -> None: - # Forward cancel to inner dialogs + """ + Called when the dialog is ending. + + :param context: The context object for this turn. + :type context: :class:`TurnContext` + :param instance: State information associated with the instance of this component dialog + on its parent's dialog stack. + :type instance: :class:`DialogInstance` + :param reason: Reason why the dialog ended. + :type reason: :class:`DialogReason` + """ + # Forward cancel to inner dialog if reason == DialogReason.CancelCalled: dialog_state = instance.state[self.persisted_dialog_state] inner_dc = DialogContext(self._dialogs, context, dialog_state) @@ -96,10 +192,12 @@ async def end_dialog( def add_dialog(self, dialog: Dialog) -> object: """ - Adds a dialog to the component dialog. - Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog. + Adds a :class:`Dialog` to the component dialog and returns the updated component. + Adding a new dialog will inherit the :class:`BotTelemetryClient` of the :class:`ComponentDialog`. + :param dialog: The dialog to add. - :return: The updated ComponentDialog + :return: The updated :class:`ComponentDialog` + :rtype: :class:`ComponentDialog` """ self._dialogs.add(dialog) if not self.initial_dialog_id: @@ -109,15 +207,33 @@ def add_dialog(self, dialog: Dialog) -> object: def find_dialog(self, dialog_id: str) -> Dialog: """ Finds a dialog by ID. - Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog. + :param dialog_id: The dialog to add. :return: The dialog; or None if there is not a match for the ID. + :rtype: :class:Dialog """ return self._dialogs.find(dialog_id) async def on_begin_dialog( self, inner_dc: DialogContext, options: object ) -> DialogTurnResult: + """ + Called when the dialog is started and pushed onto the parent's dialog stack. + + .. note:: + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. + + By default, this calls the :meth:`Dialog.begin_dialog()` method of the component + dialog's initial dialog. + + Override this method in a derived class to implement interrupt logic. + + :param inner_dc: The inner :class:`DialogContext` for the current turn of conversation. + :type inner_dc: :class:`DialogContext` + :param options: Optional, initial information to pass to the dialog. + :type options: object + """ return await inner_dc.begin_dialog(self.initial_dialog_id, options) async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: @@ -126,14 +242,55 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: async def on_end_dialog( # pylint: disable=unused-argument self, context: TurnContext, instance: DialogInstance, reason: DialogReason ) -> None: + """ + Ends the component dialog in its parent's context. + + :param turn_context: The :class:`TurnContext` for the current turn of the conversation. + :type turn_context: :class:`TurnContext` + :param instance: State information associated with the instance of this component dialog on + its parent's dialog stack. + :type instance: :class:`DialogInstance` + :param reason: Reason why the dialog ended. + :type reason: :class:`DialogReason` + """ return async def on_reprompt_dialog( # pylint: disable=unused-argument self, turn_context: TurnContext, instance: DialogInstance ) -> None: + """ + :param turn_context: The :class:`TurnContext` for the current turn of the conversation. + :type turn_context: :class:`DialogInstance` + :param instance: State information associated with the instance of this component dialog + on its parent's dialog stack. + :type instance: :class:`DialogInstance` + """ return async def end_component( self, outer_dc: DialogContext, result: object # pylint: disable=unused-argument ) -> DialogTurnResult: + """ + Ends the component dialog in its parent's context. + + .. note:: + If the task is successful, the result indicates that the dialog ended after the + turn was processed by the dialog. + + In general, the parent context is the dialog or bot turn handler that started the dialog. + If the parent is a dialog, the stack calls the parent's :meth:`Dialog.resume_dialog()` method + to return a result to the parent dialog. If the parent dialog does not implement + :meth:`Dialog.resume_dialog()`, then the parent will end, too, and the result is passed to the next + parent context, if one exists. + + The returned :class:`DialogTurnResult`contains the return value in its + :class:`DialogTurnResult.result` property. + + :param outer_dc: The parent class:`DialogContext` for the current turn of conversation. + :type outer_dc: class:`DialogContext` + :param result: Optional, value to return from the dialog component to the parent context. + :type result: object + :return: Value to return. + :rtype: :class:`DialogTurnResult.result` + """ return await outer_dc.end_dialog(result) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py index 3b5b4423f..add9e2dc6 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py @@ -10,10 +10,25 @@ class DialogInstance: """ def __init__(self): + """ + Gets or sets the ID of the dialog and gets or sets the instance's persisted state. + + :var self.id: The ID of the dialog + :vartype self.id: str + :var self.state: The instance's persisted state. + :vartype self.state: :class:`typing.Dict[str, object]` + """ self.id: str = None # pylint: disable=invalid-name + self.state: Dict[str, object] = {} def __str__(self): + """ + Gets or sets a stack index. + + :return: Returns stack index. + :rtype: str + """ result = "\ndialog_instance_id: %s\n" % self.id if self.state is not None: for key, value in self.state.items(): diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py index c20f2e3b2..7c8eb9bef 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py @@ -4,15 +4,32 @@ class DialogReason(Enum): - # A dialog is being started through a call to `DialogContext.begin()`. + """ + Indicates in which a dialog-related method is being called. + + :var BeginCalled: A dialog is being started through a call to `DialogContext.begin()`. + :vartype BeginCalled: int + :var ContinuCalled: A dialog is being continued through a call to `DialogContext.continue_dialog()`. + :vartype ContinueCalled: int + :var EndCalled: A dialog ended normally through a call to `DialogContext.end_dialog() + :vartype EndCalled: int + :var ReplaceCalled: A dialog is ending because it's being replaced through a call to + `DialogContext.replace_dialog()`. + :vartype ReplacedCalled: int + :var CancelCalled: A dialog was cancelled as part of a call to `DialogContext.cancel_all_dialogs()`. + :vartype CancelCalled: int + :var NextCalled: A preceding step was skipped through a call to `WaterfallStepContext.next()`. + :vartype NextCalled: int + """ + BeginCalled = 1 - # A dialog is being continued through a call to `DialogContext.continue_dialog()`. + ContinueCalled = 2 - # A dialog ended normally through a call to `DialogContext.end_dialog()`. + EndCalled = 3 - # A dialog is ending because it's being replaced through a call to `DialogContext.replace_dialog()`. + ReplaceCalled = 4 - # A dialog was cancelled as part of a call to `DialogContext.cancel_all_dialogs()`. + CancelCalled = 5 - # A step was advanced through a call to `WaterfallStepContext.next()`. + NextCalled = 6 diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py index 278e6b14d..940ee73ff 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py @@ -6,7 +6,18 @@ class DialogState: + """ + Contains state information for the dialog stack. + """ + def __init__(self, stack: List[DialogInstance] = None): + """ + Initializes a new instance of the :class:`DialogState` class. + The new instance is created with an empty dialog stack. + + :param stack: The state information to initialize the stack with. + :type stack: :class:`typing.List[:class:`DialogInstance`]` + """ if stack is None: self._dialog_stack = [] else: @@ -14,6 +25,12 @@ def __init__(self, stack: List[DialogInstance] = None): @property def dialog_stack(self): + """ + Initializes a new instance of the :class:`DialogState` class. + + :return: The state information to initialize the stack with. + :rtype: list + """ return self._dialog_stack def __str__(self): diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py index e36504f8b..7fd1b5632 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py @@ -5,14 +5,44 @@ class DialogTurnResult: + """ + Result returned to the caller of one of the various stack manipulation methods. + + Use :meth:`DialogContext.end_dialogAsync()` to end a :class:`Dialog` and + return a result to the calling context. + """ + def __init__(self, status: DialogTurnStatus, result: object = None): + """ + :param status: The current status of the stack. + :type status: :class:`DialogTurnStatus` + :param result: The result returned by a dialog that was just ended. + :type result: object + """ self._status = status self._result = result @property def status(self): + """ + Gets or sets the current status of the stack. + + :return self._status: + :rtype self._status: :class:`DialogTurnStatus` + """ return self._status @property def result(self): + """ + Final result returned by a dialog that just completed. + + .. note:: + This will only be populated in certain cases: + * The bot calls :meth:`DialogContext.begin_dialog()` to start a new dialog and the dialog ends immediately. + * The bot calls :meth:`DialogContext.continue_dialog()` and a dialog that was active ends. + + :return self._result: Final result returned by a dialog that just completed. + :rtype self._result: object + """ return self._result diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py index e734405a8..b88cd359b 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py @@ -4,14 +4,23 @@ class DialogTurnStatus(Enum): - # Indicates that there is currently nothing on the dialog stack. + """ + Codes indicating the state of the dialog stack after a call to `DialogContext.continueDialog()` + + :var Empty: Indicates that there is currently nothing on the dialog stack. + :vartype Empty: int + :var Waiting: Indicates that the dialog on top is waiting for a response from the user. + :vartype Waiting: int + :var Complete: Indicates that the dialog completed successfully, the result is available, and the stack is empty. + :vartype Complete: int + :var Cancelled: Indicates that the dialog was cancelled and the stack is empty. + :vartype Cancelled: int + """ + Empty = 1 - # Indicates that the dialog on top is waiting for a response from the user. Waiting = 2 - # Indicates that the dialog completed successfully, the result is available, and the stack is empty. Complete = 3 - # Indicates that the dialog was cancelled and the stack is empty. Cancelled = 4 diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py index 5930441e1..db0e87e01 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py @@ -24,9 +24,15 @@ class ActivityPrompt(Dialog, ABC): """ Waits for an activity to be received. - This prompt requires a validator be passed in and is useful when waiting for non-message - activities like an event to be received. The validator can ignore received events until the - expected activity is received. + .. remarks: + This prompt requires a validator be passed in and is useful when waiting for non-message + activities like an event to be received. The validator can ignore received events until the + expected activity is received. + + :var persisted_options: + :typevar persisted_options: str + :var persisted_state: + :vartype persisted_state: str """ persisted_options = "options" @@ -36,13 +42,12 @@ def __init__( self, dialog_id: str, validator: Callable[[PromptValidatorContext], bool] ): """ - Initializes a new instance of the ActivityPrompt class. + Initializes a new instance of the :class:`ActivityPrompt` class. - Parameters: - ---------- - dialog_id (str): Unique ID of the dialog within its parent DialogSet or ComponentDialog. - - validator: Validator that will be called each time a new activity is received. + :param dialog_id: Unique ID of the dialog within its parent :class:`DialogSet` or :class:`ComponentDialog`. + :type dialog_id: str + :param validator: Validator that will be called each time a new activity is received. + :type validator: :class:`typing.Callable[[:class:`PromptValidatorContext`], bool]` """ Dialog.__init__(self, dialog_id) @@ -53,6 +58,16 @@ def __init__( async def begin_dialog( self, dialog_context: DialogContext, options: PromptOptions = None ) -> DialogTurnResult: + """ + Called when a prompt dialog is pushed onto the dialog stack and is being activated. + + :param dialog_context: The dialog context for the current turn of the conversation. + :type dialog_context: :class:`DialogContext` + :param options: Optional, additional information to pass to the prompt being started. + :type options: :class:`PromptOptions` + :return Dialog.end_of_turn: + :rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult` + """ if not dialog_context: raise TypeError("ActivityPrompt.begin_dialog(): dc cannot be None.") if not isinstance(options, PromptOptions): @@ -83,6 +98,14 @@ async def begin_dialog( return Dialog.end_of_turn async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult: + """ + Called when a prompt dialog is the active dialog and the user replied with a new activity. + + :param dialog_context: The dialog context for the current turn of the conversation. + :type dialog_context: :class:`DialogContext` + :return Dialog.end_of_turn: + :rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult` + """ if not dialog_context: raise TypeError( "ActivityPrompt.continue_dialog(): DialogContext cannot be None." @@ -130,11 +153,22 @@ async def resume_dialog( # pylint: disable=unused-argument self, dialog_context: DialogContext, reason: DialogReason, result: object = None ): """ - Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs - on top of the stack which will result in the prompt receiving an unexpected call to - resume_dialog() when the pushed on dialog ends. - To avoid the prompt prematurely ending, we need to implement this method and - simply re-prompt the user + Called when a prompt dialog resumes being the active dialog on the dialog stack, such + as when the previous active dialog on the stack completes. + + .. note: + Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs + on top of the stack which will result in the prompt receiving an unexpected call to + :meth:resume_dialog() when the pushed on dialog ends. + To avoid the prompt prematurely ending, we need to implement this method and + simply re-prompt the user. + + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :param reason: An enum indicating why the dialog resumed. + :type reason: :class:`DialogReason` + :param result: Optional, value returned from the previous dialog on the stack. + :type result: object """ await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog) @@ -155,15 +189,14 @@ async def on_prompt( """ Called anytime the derived class should send the user a prompt. - Parameters: - ---------- - context: Context for the current turn of conversation with the user. - - state: Additional state being persisted for the prompt. - - options: Options that the prompt started with in the call to `DialogContext.prompt()`. - - isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent. + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :param state: Additional state being persisted for the prompt. + :type state: :class:`typing.Dict[str, dict]` + :param options: Options that the prompt started with in the call to :meth:`DialogContext.prompt()`. + :type options: :class:`PromptOptions` + :param isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent. + :type isRetry: bool """ if is_retry and options.retry_prompt: options.retry_prompt.input_hint = InputHints.expecting_input @@ -175,7 +208,18 @@ async def on_prompt( async def on_recognize( # pylint: disable=unused-argument self, context: TurnContext, state: Dict[str, object], options: PromptOptions ) -> PromptRecognizerResult: - + """ + When overridden in a derived class, attempts to recognize the incoming activity. + + :param context: Context for the current turn of conversation with the user. + :type context: :class:`TurnContext` + :param state: Contains state for the current instance of the prompt on the dialog stack. + :type state: :class:`typing.Dict[str, dict]` + :param options: A prompt options object + :type options: :class:`PromptOptions` + :return result: constructed from the options initially provided in the call to :meth:`async def on_prompt()` + :rtype result: :class:`PromptRecognizerResult` + """ result = PromptRecognizerResult() result.succeeded = (True,) result.value = context.activity diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index 49ab9624d..736637d30 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta from typing import Union, Awaitable, Callable +from botframework.connector import Channels +from botframework.connector.auth import ClaimsIdentity, SkillValidation from botbuilder.core import ( CardFactory, MessageFactory, @@ -23,8 +25,6 @@ OAuthCard, TokenResponse, ) -from botframework.connector import Channels -from botframework.connector.auth import ClaimsIdentity, SkillValidation from .prompt_options import PromptOptions from .oauth_prompt_settings import OAuthPromptSettings from .prompt_validator_context import PromptValidatorContext @@ -33,30 +33,35 @@ class OAuthPrompt(Dialog): """ - Creates a new prompt that asks the user to sign in using the Bot Framework Single Sign On (SSO) service. - The prompt will attempt to retrieve the users current token and if the user isn't signed in, it - will send them an `OAuthCard` containing a button they can press to sign in. Depending on the channel, - the user will be sent through one of two possible sign-in flows: - - The automatic sign-in flow where once the user signs in, the SSO service will forward - the bot the users access token using either an `event` or `invoke` activity. - - The "magic code" flow where once the user signs in, they will be prompted by the SSO service - to send the bot a six digit code confirming their identity. This code will be sent as a - standard `message` activity. - Both flows are automatically supported by the `OAuthPrompt` and they only thing you need to be careful of - is that you don't block the `event` and `invoke` activities that the prompt might be waiting on. - Note: - You should avaoid persisting the access token with your bots other state. The Bot Frameworks SSO service - will securely store the token on your behalf. If you store it in your bots state, - it could expire or be revoked in between turns. - When calling the prompt from within a waterfall step, you should use the token within the step - following the prompt and then let the token go out of scope at the end of your function - Prompt Usage - When used with your bots `DialogSet`, you can simply add a new instance of the prompt as a named dialog using - `DialogSet.add()`. - You can then start the prompt from a waterfall step using either - `DialogContext.begin()` or `DialogContext.prompt()`. - The user will be prompted to sign in as needed and their access token will be passed as an argument to the callers - next waterfall step. + Creates a new prompt that asks the user to sign in, using the Bot Framework Single Sign On (SSO) service. + + .. remarks:: + The prompt will attempt to retrieve the users current token and if the user isn't signed in, it + will send them an `OAuthCard` containing a button they can press to sign in. Depending on the channel, + the user will be sent through one of two possible sign-in flows: + - The automatic sign-in flow where once the user signs in, the SSO service will forward + the bot the users access token using either an `event` or `invoke` activity. + - The "magic code" flow where once the user signs in, they will be prompted by the SSO service + to send the bot a six digit code confirming their identity. This code will be sent as a + standard `message` activity. + Both flows are automatically supported by the `OAuthPrompt` and they only thing you need to be careful of + is that you don't block the `event` and `invoke` activities that the prompt might be waiting on. + + .. note:: + You should avoid persisting the access token with your bots other state. The Bot Frameworks SSO service + will securely store the token on your behalf. If you store it in your bots state, + it could expire or be revoked in between turns. + When calling the prompt from within a waterfall step, you should use the token within the step + following the prompt and then let the token go out of scope at the end of your function. + + **Prompt Usage** + When used with your bots :class:`DialogSet`, you can simply add a new instance of the prompt as a named + dialog using + :meth`DialogSet.add()`. + You can then start the prompt from a waterfall step using either :meth:`DialogContext.begin()` or + :meth:`DialogContext.prompt()`. + The user will be prompted to sign in as needed and their access token will be passed as an argument to + the callers next waterfall step. """ def __init__( @@ -65,6 +70,21 @@ def __init__( settings: OAuthPromptSettings, validator: Callable[[PromptValidatorContext], Awaitable[bool]] = None, ): + """ + Creates a new instance of the :class:`OAuthPrompt` class. + + :param dialogId: The Id to assign to this prompt. + :type dialogId: str + :param settings: Additional authentication settings to use with this instance of the prompt + :type settings: :class:`OAuthPromptSettings` + :param validator: Optional, a :class:`PromptValidator` that contains additional, custom validation + for this prompt + :type validator: :class:`PromptValidatorContext` + + .. note:: + The value of :param dialogId: must be unique within the :class:`DialogSet`or :class:`ComponentDialog` + to which the prompt is added. + """ super().__init__(dialog_id) self._validator = validator @@ -79,6 +99,21 @@ def __init__( async def begin_dialog( self, dialog_context: DialogContext, options: PromptOptions = None ) -> DialogTurnResult: + """ + Starts an authentication prompt dialog. Called when an authentication prompt dialog is pushed onto the + dialog stack and is being activated. + + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :param options: Optional, additional information to pass to the prompt being started + :type options: :class:PromptOptions + :return: Dialog turn result + :rtype: :class:DialogTurnResult + + .. note:: + If the task is successful, the result indicates whether the prompt is still active after the turn + has been processed by the prompt. + """ if dialog_context is None: raise TypeError( f"OAuthPrompt.begin_dialog: Expected DialogContext but got NoneType instead" @@ -120,6 +155,20 @@ async def begin_dialog( return Dialog.end_of_turn async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult: + """ + Continues a dialog. Called when a prompt dialog is the active dialog and the user replied with a new activity. + + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :return: Dialog turn result + :rtype: :class:DialogTurnResult + + .. note:: + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. + The prompt generally continues to receive the user's replies until it accepts the + user's reply as valid input for the prompt. + """ # Recognize token recognized = await self._recognize_token(dialog_context.context) @@ -167,6 +216,18 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu async def get_user_token( self, context: TurnContext, code: str = None ) -> TokenResponse: + """ + Gets the user's tokeN. + + :param context: Context for the current turn of conversation with the user + :type context: :class:TurnContext + :return: A response that includes the user's token + :rtype: :class:TokenResponse + + .. note:: + If the task is successful and the user already has a token or the user successfully signs in, + the result contains the user's token. + """ adapter = context.adapter # Validate adapter type @@ -180,6 +241,18 @@ async def get_user_token( ) async def sign_out_user(self, context: TurnContext): + """ + Signs out the user + + :param context: Context for the current turn of conversation with the user + :type context: :class:`TurnContext` + :return: A :class:`Task` representing the work queued to execute + :rtype: :class:`Task` + + .. note:: + If the task is successful and the user already has a token or the user successfully signs in, + the result contains the user's token. + """ adapter = context.adapter # Validate adapter type diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py index 0ab60ba17..8c9c0edc5 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py @@ -23,22 +23,33 @@ class Prompt(Dialog): - """ Base class for all prompts.""" + """ + Defines the core behavior of prompt dialogs. Extends the :class:`Dialog` base class. + + .. remarks:: + When the prompt ends, it returns an object that represents the value it was prompted for. + Use :method:`DialogSet.add()` or :method:`ComponentDialog.add_dialog()` to add a prompt to a dialog set or + component dialog, respectively. + Use :method:`DialogContext.prompt()` or :meth:`DialogContext.begin_dialog()` to start the prompt. + .. note:: + If you start a prompt from a :class:`WaterfallStep` in a :class:`WaterfallDialog`, then the prompt result + will be available in the next step of the waterfall. + """ ATTEMPT_COUNT_KEY = "AttemptCount" persisted_options = "options" persisted_state = "state" def __init__(self, dialog_id: str, validator: object = None): - """Creates a new Prompt instance. - Parameters - ---------- - dialog_id - Unique ID of the prompt within its parent `DialogSet` or - `ComponentDialog`. - validator - (Optional) custom validator used to provide additional validation and - re-prompting logic for the prompt. + """ + Creates a new :class:`Prompt` instance. + + :param dialog_id: Unique Id of the prompt within its parent :class:`DialogSet` or + :class:`ComponentDialog`. + :type dialog_id: str + :param validator: Optional custom validator used to provide additional validation and re-prompting + logic for the prompt. + :type validator: Object """ super(Prompt, self).__init__(dialog_id) @@ -47,6 +58,20 @@ def __init__(self, dialog_id: str, validator: object = None): async def begin_dialog( self, dialog_context: DialogContext, options: object = None ) -> DialogTurnResult: + """ + Starts a prompt dialog. Called when a prompt dialog is pushed onto the dialog stack and is being activated. + + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :param options: Optional, additional information to pass to the prompt being started + :type options: Object + :return: The dialog turn result + :rtype: :class:`DialogTurnResult` + + .. note:: + If the task is successful, the result indicates whether the prompt is still active + after the turn has been processed by the prompt. + """ if not dialog_context: raise TypeError("Prompt(): dc cannot be None.") if not isinstance(options, PromptOptions): @@ -74,6 +99,20 @@ async def begin_dialog( return Dialog.end_of_turn async def continue_dialog(self, dialog_context: DialogContext): + """ + Continues a dialog. Called when a prompt dialog is the active dialog and the user replied with a new activity. + + :param dialog_context: The dialog context for the current turn of the conversation + :type dialog_context: :class:`DialogContext` + :return: The dialog turn result + :rtype: :class:`DialogTurnResult` + + .. note:: + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. + The prompt generally continues to receive the user's replies until it accepts the + user's reply as valid input for the prompt. + """ if not dialog_context: raise TypeError("Prompt(): dc cannot be None.") @@ -111,15 +150,42 @@ async def continue_dialog(self, dialog_context: DialogContext): async def resume_dialog( self, dialog_context: DialogContext, reason: DialogReason, result: object ) -> DialogTurnResult: - # Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs - # on top of the stack which will result in the prompt receiving an unexpected call to - # dialog_resume() when the pushed on dialog ends. - # To avoid the prompt prematurely ending we need to implement this method and - # simply re-prompt the user. + """ + Resumes a dialog. Called when a prompt dialog resumes being the active dialog + on the dialog stack, such as when the previous active dialog on the stack completes. + + :param dialog_context: The dialog context for the current turn of the conversation. + :type dialog_context: :class:DialogContext + :param reason: An enum indicating why the dialog resumed. + :type reason: :class:DialogReason + :param result: Optional, value returned from the previous dialog on the stack. + The type of the value returned is dependent on the previous dialog. + :type result: object + :return: The dialog turn result + :rtype: :class:`DialogTurnResult` + + .. note:: + If the task is successful, the result indicates whether the dialog is still + active after the turn has been processed by the dialog. + Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs + on top of the stack which will result in the prompt receiving an unexpected call to + :meth:resume_dialog() when the pushed on dialog ends. + To avoid the prompt prematurely ending we need to simply re-prompt the user. + """ await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog) return Dialog.end_of_turn async def reprompt_dialog(self, context: TurnContext, instance: DialogInstance): + """ + Reprompts user for input. Called when a prompt dialog has been requested to re-prompt the user for input. + + :param context: Context for the current turn of conversation with the user + :type context: :class:TurnContext + :param instance: The instance of the dialog on the stack + :type instance: :class:DialogInstance + :return: A :class:Task representing the asynchronous operation + :rtype: :class:Task + """ state = instance.state[self.persisted_state] options = instance.state[self.persisted_options] await self.on_prompt(context, state, options, False) @@ -132,7 +198,23 @@ async def on_prompt( options: PromptOptions, is_retry: bool, ): - pass + """ + Prompts user for input. When overridden in a derived class, prompts the user for input. + + :param turn_context: Context for the current turn of conversation with the user + :type turn_context: :class:`TurnContext` + :param state: Contains state for the current instance of the prompt on the dialog stack + :type state: :class:Dict + :param options: A prompt options object constructed from the options initially provided + in the call :meth:`DialogContext.prompt(self, dialog_id: str, options)` + :type options: :class:`PromptOptions` + :param is_retry: true if this is the first time this prompt dialog instance on the stack is prompting + the user for input; otherwise, false + :type is_retry: bool + + :return: A :class:Task representing the asynchronous operation. + :rtype: :class:Task + """ @abstractmethod async def on_recognize( @@ -141,7 +223,20 @@ async def on_recognize( state: Dict[str, object], options: PromptOptions, ): - pass + """ + Recognizes the user's input. When overridden in a derived class, attempts to recognize the user's input. + + :param turn_context: Context for the current turn of conversation with the user + :type turn_context: :class:`TurnContext` + :param state: Contains state for the current instance of the prompt on the dialog stack + :type state: :class:Dict + :param options: A prompt options object constructed from the options initially provided + in the call :meth:`DialogContext.prompt(self, dialog_id: str, options)` + :type options: :class:PromptOptions + + :return: A :class:Task representing the asynchronous operation. + :rtype: :class:Task + """ def append_choices( self, @@ -152,19 +247,25 @@ def append_choices( options: ChoiceFactoryOptions = None, ) -> Activity: """ + Composes an output activity containing a set of choices. + When overridden in a derived class, appends choices to the activity when the user is prompted for input. Helper function to compose an output activity containing a set of choices. - Parameters: - ----------- - prompt: The prompt to append the user's choice to. - - channel_id: ID of the channel the prompt is being sent to. - - choices: List of choices to append. - - style: Configured style for the list of choices. - - options: (Optional) options to configure the underlying `ChoiceFactory` call. + :param prompt: The prompt to append the user's choice to + :type prompt: + :param channel_id: Id of the channel the prompt is being sent to + :type channel_id: str + :param: choices: List of choices to append + :type choices: :class:`List` + :param: style: Configured style for the list of choices + :type style: :class:`ListStyle` + :param: options: Optional formatting options to use when presenting the choices + :type style: :class:`ChoiceFactoryOptions` + :return: A :class:Task representing the asynchronous operation + :rtype: :class:Task + + .. note:: + If the task is successful, the result contains the updated activity. """ # Get base prompt text (if any) text = prompt.text if prompt is not None and prompt.text else "" diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py index 8d9801424..c341a4b52 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py @@ -8,6 +8,10 @@ class PromptOptions: + """ + Contains settings to pass to a :class:`Prompt` object when the prompt is started. + """ + def __init__( self, prompt: Activity = None, @@ -17,6 +21,23 @@ def __init__( validations: object = None, number_of_attempts: int = 0, ): + """ + Sets the initial prompt to send to the user as an :class:`botbuilder.schema.Activity`. + + :param prompt: The initial prompt to send to the user + :type prompt: :class:`botbuilder.schema.Activity` + :param retry_prompt: The retry prompt to send to the user + :type retry_prompt: :class:`botbuilder.schema.Activity` + :param choices: The choices to send to the user + :type choices: :class:`List` + :param style: The style of the list of choices to send to the user + :type style: :class:`ListStyle` + :param validations: The prompt validations + :type validations: :class:`Object` + :param number_of_attempts: The number of attempts allowed + :type number_of_attempts: :class:`int` + + """ self.prompt = prompt self.retry_prompt = retry_prompt self.choices = choices diff --git a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py index 4ead12a3e..2ca60fa07 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py +++ b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from typing import List, Union from botbuilder.core import (