From 74e619597c2e9bf6dd059f721ef76463601989d7 Mon Sep 17 00:00:00 2001 From: Dave Taniguchi Date: Thu, 30 May 2019 11:45:47 -0700 Subject: [PATCH 1/3] Add App Insights Sample + Flask Middleware --- .../appinsights_bot_telemetry_client.py | 123 ------- .../application_insights_telemetry_client.py | 321 ++++++++++-------- .../applicationinsights/django/__init__.py | 18 +- .../django/bot_telemetry_middleware.py | 91 ++--- .../integration_post_data.py | 126 +++---- .../botbuilder/dialogs/waterfall_dialog.py | 299 ++++++++-------- .../21.corebot-app-insights/README-LUIS.md | 216 ++++++++++++ .../21.corebot-app-insights/README.md | 56 +++ .../booking_details.py | 8 + .../21.corebot-app-insights/bots/__init__.py | 9 + .../bots/dialog_and_welcome_bot.py | 46 +++ .../bots/dialog_bot.py | 52 +++ .../bots/resources/welcomeCard.json | 46 +++ .../cognitiveModels/FlightBooking.json | 226 ++++++++++++ .../21.corebot-app-insights/config.py | 8 + .../dialogs/__init__.py | 13 + .../dialogs/booking_dialog.py | 106 ++++++ .../dialogs/cancel_and_help_dialog.py | 40 +++ .../dialogs/date_resolver_dialog.py | 62 ++++ .../dialogs/main_dialog.py | 75 ++++ .../helpers/__init__.py | 9 + .../helpers/activity_helper.py | 22 ++ .../helpers/dialog_helper.py | 17 + .../helpers/luis_helper.py | 45 +++ .../21.corebot-app-insights/main.py | 85 +++++ .../21.corebot-app-insights/requirements.txt | 13 + 26 files changed, 1587 insertions(+), 545 deletions(-) delete mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/appinsights_bot_telemetry_client.py create mode 100644 samples/python-flask/21.corebot-app-insights/README-LUIS.md create mode 100644 samples/python-flask/21.corebot-app-insights/README.md create mode 100644 samples/python-flask/21.corebot-app-insights/booking_details.py create mode 100644 samples/python-flask/21.corebot-app-insights/bots/__init__.py create mode 100644 samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py create mode 100644 samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py create mode 100644 samples/python-flask/21.corebot-app-insights/bots/resources/welcomeCard.json create mode 100644 samples/python-flask/21.corebot-app-insights/cognitiveModels/FlightBooking.json create mode 100644 samples/python-flask/21.corebot-app-insights/config.py create mode 100644 samples/python-flask/21.corebot-app-insights/dialogs/__init__.py create mode 100644 samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py create mode 100644 samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py create mode 100644 samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py create mode 100644 samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py create mode 100644 samples/python-flask/21.corebot-app-insights/helpers/__init__.py create mode 100644 samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py create mode 100644 samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py create mode 100644 samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py create mode 100644 samples/python-flask/21.corebot-app-insights/main.py create mode 100644 samples/python-flask/21.corebot-app-insights/requirements.txt diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/appinsights_bot_telemetry_client.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/appinsights_bot_telemetry_client.py deleted file mode 100644 index da097991c..000000000 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/appinsights_bot_telemetry_client.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback -from applicationinsights import TelemetryClient -from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType -from typing import Dict - -class AppinsightsBotTelemetryClient(BotTelemetryClient): - - def __init__(self, instrumentation_key:str): - self._instrumentation_key = instrumentation_key - - self._context = TelemetryContext() - context.instrumentation_key = self._instrumentation_key - # context.user.id = 'BOTID' # telemetry_channel.context.session. - # context.session.id = 'BOTSESSION' - - # set up channel with context - self._channel = TelemetryChannel(context) - # telemetry_channel.context.properties['my_property'] = 'my_value' - - self._client = TelemetryClient(self._instrumentation_key, self._channel) - - - def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None, - measurements: Dict[str, object]=None) -> None: - """ - Send information about the page viewed in the application (a web page for instance). - :param name: the name of the page that was viewed. - :param url: the URL of the page that was viewed. - :param duration: the duration of the page view in milliseconds. (defaults to: 0) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_pageview(name, url, duration, properties, measurements) - - def track_exception(self, type_exception: type = None, value : Exception =None, tb : traceback =None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None: - """ - Send information about a single exception that occurred in the application. - :param type_exception: the type of the exception that was thrown. - :param value: the exception that the client wants to send. - :param tb: the traceback information as returned by :func:`sys.exc_info`. - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_exception(type_exception, value, tb, properties, measurements) - - def track_event(self, name: str, properties: Dict[str, object] = None, - measurements: Dict[str, object] = None) -> None: - """ - Send information about a single event that has occurred in the context of the application. - :param name: the data to associate to this event. - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_event(name, properties, measurements) - - def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None, - count: int =None, min: float=None, max: float=None, std_dev: float=None, - properties: Dict[str, object]=None) -> NotImplemented: - """ - Send information about a single metric data point that was captured for the application. - :param name: The name of the metric that was captured. - :param value: The value of the metric that was captured. - :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) - :param count: the number of metrics that were aggregated into this data point. (defaults to: None) - :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None) - :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None) - :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - """ - self._client.track_metric(name, value, type, count, min, max, std_dev, properties) - - def track_trace(self, name: str, properties: Dict[str, object]=None, severity=None): - """ - Sends a single trace statement. - :param name: the trace statement.\n - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n - :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL - """ - self._client.track_trace(name, properties, severity) - - def track_request(self, name: str, url: str, success: bool, start_time: str=None, - duration: int=None, response_code: str =None, http_method: str=None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None, - request_id: str=None): - """ - Sends a single request that was captured for the application. - :param name: The name for this request. All requests with the same name will be grouped together. - :param url: The actual URL for this request (to show in individual request instances). - :param success: True if the request ended in success, False otherwise. - :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None) - :param duration: the number of milliseconds that this request lasted. (defaults to: None) - :param response_code: the response code that this request returned. (defaults to: None) - :param http_method: the HTTP method that triggered this request. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_request(name, url, success, start_time, duration, response_code, http_method, properties, - measurements, request_id) - - def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None, - success:bool=None, result_code:str=None, properties:Dict[str, object]=None, - measurements:Dict[str, object]=None, dependency_id:str=None): - """ - Sends a single dependency telemetry that was captured for the application. - :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template. - :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters. - :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None) - :param target: the target site of a dependency call. Examples are server name, host address. (default to: None) - :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None) - :param success: true if the dependency call ended in success, false otherwise. (defaults to: None) - :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_dependency(name, data, type, target, duration, success, result_code, properties, - measurements, dependency_id) - diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py index db2b6f89c..a6eb69cb0 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py @@ -1,145 +1,176 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import traceback -from applicationinsights import TelemetryClient -from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType -from typing import Dict -from .integration_post_data import IntegrationPostData - -def bot_telemetry_processor(data, context): - post_data = IntegrationPostData().activity_json - if post_data is None: - return - # Override session and user id - from_prop = post_data['from'] if 'from' in post_data else None - user_id = from_prop['id'] if from_prop != None else None - channel_id = post_data['channelId'] if 'channelId' in post_data else None - conversation = post_data['conversation'] if 'conversation' in post_data else None - conversation_id = conversation['id'] if 'id' in conversation else None - context.user.id = channel_id + user_id - context.session.id = conversation_id - - # Additional bot-specific properties - if 'id' in post_data: - data.properties["activityId"] = post_data['id'] - if 'channelId' in post_data: - data.properties["channelId"] = post_data['channelId'] - if 'type' in post_data: - data.properties["activityType"] = post_data['type'] - - -class ApplicationInsightsTelemetryClient(BotTelemetryClient): - - def __init__(self, instrumentation_key:str, telemetry_client: TelemetryClient = None): - self._instrumentation_key = instrumentation_key - self._client = telemetry_client if telemetry_client != None else TelemetryClient(self._instrumentation_key) - # Telemetry Processor - self._client.add_telemetry_processor(bot_telemetry_processor) - - - def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None, - measurements: Dict[str, object]=None) -> None: - """ - Send information about the page viewed in the application (a web page for instance). - :param name: the name of the page that was viewed. - :param url: the URL of the page that was viewed. - :param duration: the duration of the page view in milliseconds. (defaults to: 0) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_pageview(name, url, duration, properties, measurements) - - def track_exception(self, type_exception: type = None, value : Exception =None, tb : traceback =None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None: - """ - Send information about a single exception that occurred in the application. - :param type_exception: the type of the exception that was thrown. - :param value: the exception that the client wants to send. - :param tb: the traceback information as returned by :func:`sys.exc_info`. - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_exception(type_exception, value, tb, properties, measurements) - - def track_event(self, name: str, properties: Dict[str, object] = None, - measurements: Dict[str, object] = None) -> None: - """ - Send information about a single event that has occurred in the context of the application. - :param name: the data to associate to this event. - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - """ - self._client.track_event(name, properties, measurements) - - def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None, - count: int =None, min: float=None, max: float=None, std_dev: float=None, - properties: Dict[str, object]=None) -> NotImplemented: - """ - Send information about a single metric data point that was captured for the application. - :param name: The name of the metric that was captured. - :param value: The value of the metric that was captured. - :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) - :param count: the number of metrics that were aggregated into this data point. (defaults to: None) - :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None) - :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None) - :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - """ - self._client.track_metric(name, value, type, count, min, max, std_dev, properties) - - def track_trace(self, name: str, properties: Dict[str, object]=None, severity=None): - """ - Sends a single trace statement. - :param name: the trace statement.\n - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n - :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL - """ - self._client.track_trace(name, properties, severity) - - def track_request(self, name: str, url: str, success: bool, start_time: str=None, - duration: int=None, response_code: str =None, http_method: str=None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None, - request_id: str=None): - """ - Sends a single request that was captured for the application. - :param name: The name for this request. All requests with the same name will be grouped together. - :param url: The actual URL for this request (to show in individual request instances). - :param success: True if the request ended in success, False otherwise. - :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None) - :param duration: the number of milliseconds that this request lasted. (defaults to: None) - :param response_code: the response code that this request returned. (defaults to: None) - :param http_method: the HTTP method that triggered this request. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_request(name, url, success, start_time, duration, response_code, http_method, properties, - measurements, request_id) - - def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None, - success:bool=None, result_code:str=None, properties:Dict[str, object]=None, - measurements:Dict[str, object]=None, dependency_id:str=None): - """ - Sends a single dependency telemetry that was captured for the application. - :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template. - :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters. - :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None) - :param target: the target site of a dependency call. Examples are server name, host address. (default to: None) - :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None) - :param success: true if the dependency call ended in success, false otherwise. (defaults to: None) - :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None) - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) - :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) - :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) - """ - self._client.track_dependency(name, data, type, target, duration, success, result_code, properties, - measurements, dependency_id) - - def flush(self): - """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is - being used. - """ - self._client.flush() - - +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import sys +import traceback +from applicationinsights import TelemetryClient +from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType +from typing import Dict +from .integration_post_data import IntegrationPostData + +def bot_telemetry_processor(data, context) -> bool: + """ Application Insights Telemetry Processor for Bot + Traditional Web user and session ID's don't apply for Bots. This processor + replaces the identifiers to be consistent with Bot Framework's notion of + user and session id's. + + Each event that gets logged (with this processor added) will contain additional + properties. + + The following properties are replaced: + - context.user.id - The user ID that Application Insights uses to identify + a unique user. + - context.session.id - The session ID that APplication Insights uses to + identify a unique session. + + In addition, the additional data properties are added: + - activityId - The Bot Framework's Activity ID which represents a unique + message identifier. + - channelId - The Bot Framework "Channel" (ie, slack/facebook/directline/etc) + - activityType - The Bot Framework message classification (ie, message) + + + :param data: Data from Application Insights + :type data: telemetry item + :param context: Context from Application Insights + :type context: context object + :returns: bool -- determines if the event is passed to the server (False = Filtered). + """ + post_data = IntegrationPostData().activity_json + if post_data is None: + # If there is no body (not a BOT request or not configured correctly). + # We *could* filter here, but we're allowing event to go through. + return True + + # Override session and user id + from_prop = post_data['from'] if 'from' in post_data else None + user_id = from_prop['id'] if from_prop != None else None + channel_id = post_data['channelId'] if 'channelId' in post_data else None + conversation = post_data['conversation'] if 'conversation' in post_data else None + conversation_id = conversation['id'] if 'id' in conversation else None + context.user.id = channel_id + user_id + context.session.id = conversation_id + + # Additional bot-specific properties + if 'id' in post_data: + data.properties["activityId"] = post_data['id'] + if 'channelId' in post_data: + data.properties["channelId"] = post_data['channelId'] + if 'type' in post_data: + data.properties["activityType"] = post_data['type'] + return True + + +class ApplicationInsightsTelemetryClient(BotTelemetryClient): + + def __init__(self, instrumentation_key:str, telemetry_client: TelemetryClient = None): + self._instrumentation_key = instrumentation_key + self._client = telemetry_client if telemetry_client != None else TelemetryClient(self._instrumentation_key) + # Telemetry Processor + self._client.add_telemetry_processor(bot_telemetry_processor) + + + def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None, + measurements: Dict[str, object]=None) -> None: + """ + Send information about the page viewed in the application (a web page for instance). + :param name: the name of the page that was viewed. + :param url: the URL of the page that was viewed. + :param duration: the duration of the page view in milliseconds. (defaults to: 0) + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) + """ + self._client.track_pageview(name, url, duration, properties, measurements) + + def track_exception(self, type_exception: type = None, value : Exception =None, tb : traceback =None, + properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None: + """ + Send information about a single exception that occurred in the application. + :param type_exception: the type of the exception that was thrown. + :param value: the exception that the client wants to send. + :param tb: the traceback information as returned by :func:`sys.exc_info`. + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) + """ + self._client.track_exception(type_exception, value, tb, properties, measurements) + + def track_event(self, name: str, properties: Dict[str, object] = None, + measurements: Dict[str, object] = None) -> None: + """ + Send information about a single event that has occurred in the context of the application. + :param name: the data to associate to this event. + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) + """ + self._client.track_event(name, properties = properties, measurements = measurements) + + def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None, + count: int =None, min: float=None, max: float=None, std_dev: float=None, + properties: Dict[str, object]=None) -> NotImplemented: + """ + Send information about a single metric data point that was captured for the application. + :param name: The name of the metric that was captured. + :param value: The value of the metric that was captured. + :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) + :param count: the number of metrics that were aggregated into this data point. (defaults to: None) + :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None) + :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None) + :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + """ + self._client.track_metric(name, value, type, count, min, max, std_dev, properties) + + def track_trace(self, name: str, properties: Dict[str, object]=None, severity=None): + """ + Sends a single trace statement. + :param name: the trace statement.\n + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n + :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL + """ + self._client.track_trace(name, properties, severity) + + def track_request(self, name: str, url: str, success: bool, start_time: str=None, + duration: int=None, response_code: str =None, http_method: str=None, + properties: Dict[str, object]=None, measurements: Dict[str, object]=None, + request_id: str=None): + """ + Sends a single request that was captured for the application. + :param name: The name for this request. All requests with the same name will be grouped together. + :param url: The actual URL for this request (to show in individual request instances). + :param success: True if the request ended in success, False otherwise. + :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None) + :param duration: the number of milliseconds that this request lasted. (defaults to: None) + :param response_code: the response code that this request returned. (defaults to: None) + :param http_method: the HTTP method that triggered this request. (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) + :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) + """ + self._client.track_request(name, url, success, start_time, duration, response_code, http_method, properties, + measurements, request_id) + + def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None, + success:bool=None, result_code:str=None, properties:Dict[str, object]=None, + measurements:Dict[str, object]=None, dependency_id:str=None): + """ + Sends a single dependency telemetry that was captured for the application. + :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template. + :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters. + :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None) + :param target: the target site of a dependency call. Examples are server name, host address. (default to: None) + :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None) + :param success: true if the dependency call ended in success, false otherwise. (defaults to: None) + :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None) + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) + :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) + :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) + """ + self._client.track_dependency(name, data, type, target, duration, success, result_code, properties, + measurements, dependency_id) + + def flush(self): + """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is + being used. + """ + self._client.flush() + + diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py index 71248f5db..27a45e0a3 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py @@ -1,9 +1,9 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body - -__all__ = [ - "BotTelemetryMiddleware", - "retrieve_bot_body" -] +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body + +__all__ = [ + "BotTelemetryMiddleware", + "retrieve_bot_body" +] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py index 59db783b7..043e17f93 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py @@ -1,43 +1,48 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import sys -import json -from threading import current_thread - -# Map of thread id => POST body text -_request_bodies = {} - -def retrieve_bot_body(): - """ retrieve_bot_body - Retrieve the POST body text from temporary cache. - The POST body corresponds with the thread id and should resides in - cache just for lifetime of request. - - TODO: Add cleanup job to kill orphans - """ - result = _request_bodies.pop(current_thread().ident, None) - return result - -class BotTelemetryMiddleware(): - """ - Save off the POST body to later populate bot properties - """ - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - self.process_request(request) - return self.get_response(request) - - def process_request(self, request): - body_unicode = request.body.decode('utf-8') if request.method == "POST" else None - # Sanity check JSON - if body_unicode != None: - try: - body = json.loads(body_unicode) - except: - return - # Integration layer expecting just the json text. - _request_bodies[current_thread().ident] = body_unicode - - +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import sys +import json +from threading import current_thread + +# Map of thread id => POST body text +_request_bodies = {} + +def retrieve_bot_body(): + """ retrieve_bot_body + Retrieve the POST body text from temporary cache. + The POST body corresponds with the thread id and should resides in + cache just for lifetime of request. + + TODO: Add cleanup job to kill orphans + """ + result = _request_bodies.pop(current_thread().ident, None) + return result + +class BotTelemetryMiddleware(): + """ + Save off the POST body to later populate bot-specific properties to + add to Application Insights. + + Example activating MIDDLEWARE in Django settings: + MIDDLEWARE = [ + 'botbuilder.applicationinsights.django.BotTelemetryMiddleware', # Ideally add somewhere near top + ... + ] + """ + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + self.process_request(request) + return self.get_response(request) + + def process_request(self, request): + # Bot Service doesn't handle anything over 256k + # TODO: Add length check + body_unicode = request.body.decode('utf-8') if request.method == "POST" else None + # Sanity check JSON + if body_unicode != None: + # Integration layer expecting just the json text. + _request_bodies[current_thread().ident] = body_unicode + + diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py index bb0891405..f52eb9c30 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py @@ -1,76 +1,50 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import sys -import gc -import imp -import json -from botbuilder.schema import Activity -from botbuilder.applicationinsights.django import retrieve_bot_body - -class IntegrationPostData: - """ - Retrieve the POST body from the underlying framework: - - Flask - - Django - - (soon Tornado?) - - This class: - - Detects framework (currently flask or django) - - Pulls the current request body as a string - - Usage: - botdata = BotTelemetryData() - body = botdata.activity_json # Get current request body as json object - activity_id = body[id] # Get the ID from the POST body - """ - def __init__(self): - pass - - @property - def activity_json(self) -> json: - body_text = self.get_request_body() - #print(f"ACTIVITY_JSON: Body{body_text}", file=sys.stderr) - body = json.loads(body_text) if body_text != None else None - return body - - def get_request_body(self) -> str: - if self.detect_flask(): - flask_app = self.get_flask_app() - - with flask_app.app_context(): - mod = __import__('flask', fromlist=['Flask']) - request = getattr(mod, 'request') - body = self.body_from_WSGI_environ(request.environ) - return body - else: - if self.detect_django(): - # Retrieve from Middleware cache - return retrieve_bot_body() - - def body_from_WSGI_environ(self, environ): - try: - request_body_size = int(environ.get('CONTENT_LENGTH', 0)) - except (ValueError): - request_body_size = 0 - request_body = environ['wsgi.input'].read(request_body_size) - return request_body - - def detect_flask(self) -> bool: - return "flask" in sys.modules - - def detect_django(self) -> bool: - return "django" in sys.modules - - def resolve_flask_type(self) -> 'Flask': - mod = __import__('flask', fromlist=['Flask']) - flask_type = getattr(mod, 'Flask') - return flask_type - - def get_flask_app(self) -> 'Flask': - flask = [o for o in gc.get_objects() if isinstance(o, self.resolve_flask_type())] - flask_instances = len(flask) - if flask_instances <= 0 or flask_instances > 1: - raise Exception(f'Detected {flask_instances} instances of flask. Expecting 1.') - app = flask[0] - return app \ No newline at end of file +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys +import gc +import imp +import json +from botbuilder.schema import Activity +from botbuilder.applicationinsights.django import retrieve_bot_body +from botbuilder.applicationinsights.flask import retrieve_flask_body + +class IntegrationPostData: + """ + Retrieve the POST body from the underlying framework: + - Flask + - Django + - (soon Tornado?) + + This class: + - Detects framework (currently flask or django) + - Pulls the current request body as a string + + Usage: + botdata = BotTelemetryData() + body = botdata.activity_json # Get current request body as json object + activity_id = body[id] # Get the ID from the POST body + """ + def __init__(self): + pass + + @property + def activity_json(self) -> json: + body_text = self.get_request_body() + body = json.loads(body_text) if body_text != None else None + return body + + def get_request_body(self) -> str: + if self.detect_flask(): + return retrieve_flask_body() + else: + if self.detect_django(): + # Retrieve from Middleware cache + return retrieve_bot_body() + + def detect_flask(self) -> bool: + return "flask" in sys.modules + + def detect_django(self) -> bool: + return "django" in sys.modules + diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py index cd8335381..94f84ae62 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py @@ -1,150 +1,151 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import uuid -from typing import ( - Dict, - Coroutine, - List) -from .dialog_reason import DialogReason -from .dialog import Dialog -from .dialog_turn_result import DialogTurnResult -from .dialog_context import DialogContext -from .dialog_instance import DialogInstance -from .waterfall_step_context import WaterfallStepContext -from botbuilder.core import TurnContext -from botbuilder.schema import ActivityTypes -from typing import Coroutine, List - - -class WaterfallDialog(Dialog): - PersistedOptions = "options" - StepIndex = "stepIndex" - PersistedValues = "values" - PersistedInstanceId = "instanceId" - - def __init__(self, dialog_id: str, steps: [Coroutine] = None): - super(WaterfallDialog, self).__init__(dialog_id) - if not steps: - self._steps = [] - else: - if not isinstance(steps, list): - raise TypeError('WaterfallDialog(): steps must be list of steps') - self._steps = steps - - def add_step(self, step): - """ - Adds a new step to the waterfall. - :param step: Step to add - - :return: Waterfall dialog for fluent calls to `add_step()`. - """ - if not step: - raise TypeError('WaterfallDialog.add_step(): step cannot be None.') - - self._steps.append(step) - return self - - async def begin_dialog(self, dc: DialogContext, options: object = None) -> DialogTurnResult: - - if not dc: - raise TypeError('WaterfallDialog.begin_dialog(): dc cannot be None.') - - # Initialize waterfall state - state = dc.active_dialog.state - - instance_id = uuid.uuid1().__str__() - state[self.PersistedOptions] = options - state[self.PersistedValues] = {} - state[self.PersistedInstanceId] = instance_id - - properties = {} - properties['DialogId'] = self.id - properties['InstanceId'] = instance_id - self.telemetry_client.track_event("WaterfallStart", properties) - # Run first stepkinds - return await self.run_step(dc, 0, DialogReason.BeginCalled, None) - - async def continue_dialog(self, dc: DialogContext = None, reason: DialogReason = None, result: object = NotImplementedError) -> DialogTurnResult: - if not dc: - raise TypeError('WaterfallDialog.continue_dialog(): dc cannot be None.') - - if dc.context.activity.type != ActivityTypes.message: - return Dialog.end_of_turn - - return await self.resume_dialog(dc, DialogReason.ContinueCalled, dc.context.activity.text) - - async def resume_dialog(self, dc: DialogContext, reason: DialogReason, result: object): - if dc is None: - raise TypeError('WaterfallDialog.resume_dialog(): dc cannot be None.') - - # Increment step index and run step - state = dc.active_dialog.state - - # Future Me: - # If issues with CosmosDB, see https://github.com/Microsoft/botbuilder-dotnet/issues/871 - # for hints. - return await self.run_step(dc, state[self.StepIndex] + 1, reason, result) - - async def end_dialog(self, turn_context: TurnContext, instance: DialogInstance, reason: DialogReason) -> None: - if reason is DialogReason.CancelCalled: - index = instance.state[self.StepIndex] - step_name = self.get_step_name(index) - instance_id = str(instance.state[self.PersistedInstanceId]) - properties = { - "DialogId": self.id, - "StepName" : step_name, - "InstanceId" : instance_id - } - self.telemetry_client.track_event("WaterfallCancel", properties) - else: - if reason is DialogReason.EndCalled: - - instance_id = str(instance.state[self.PersistedInstanceId]) - properties = { - "DialogId": self.id, - "InstanceId": instance_id - } - self.telemetry_client.track_event("WaterfallComplete", properties) - - return - - async def on_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - step_name = self.get_step_name(step_context.index) - instance_id = str(step_context.active_dialog.state[self.PersistedInstanceId]) - properties = { - "DialogId": self.id, - "StepName": step_name, - "InstanceId": instance_id - } - self.telemetry_client.track_event("WaterfallStep", properties) - return await self._steps[step_context.index](step_context) - - async def run_step(self, dc: DialogContext, index: int, reason: DialogReason, result: object) -> DialogTurnResult: - if not dc: - raise TypeError('WaterfallDialog.run_steps(): dc cannot be None.') - if index < len(self._steps): - # Update persisted step index - state = dc.active_dialog.state - state[self.StepIndex] = index - - # Create step context - options = state[self.PersistedOptions] - values = state[self.PersistedValues] - step_context = WaterfallStepContext(self, dc, options, values, index, reason, result) - return await self.on_step(step_context) - else: - # End of waterfall so just return any result to parent - return await dc.end_dialog(result) - - def get_step_name(self, index: int) -> str: - """ - Give the waterfall step a unique name - """ - step_name = self._steps[index].__qualname__ - - if not step_name or ">" in step_name : - step_name = f"Step{index + 1}of{len(self._steps)}" - +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import uuid +from typing import ( + Dict, + Coroutine, + List) +from .dialog_reason import DialogReason +from .dialog import Dialog +from .dialog_turn_result import DialogTurnResult +from .dialog_context import DialogContext +from .dialog_instance import DialogInstance +from .waterfall_step_context import WaterfallStepContext +from botbuilder.core import TurnContext +from botbuilder.schema import ActivityTypes +from typing import Coroutine, List + + +class WaterfallDialog(Dialog): + PersistedOptions = "options" + StepIndex = "stepIndex" + PersistedValues = "values" + PersistedInstanceId = "instanceId" + + def __init__(self, dialog_id: str, steps: [Coroutine] = None): + super(WaterfallDialog, self).__init__(dialog_id) + if not steps: + self._steps = [] + else: + if not isinstance(steps, list): + raise TypeError('WaterfallDialog(): steps must be list of steps') + self._steps = steps + + def add_step(self, step): + """ + Adds a new step to the waterfall. + :param step: Step to add + + :return: Waterfall dialog for fluent calls to `add_step()`. + """ + if not step: + raise TypeError('WaterfallDialog.add_step(): step cannot be None.') + + self._steps.append(step) + return self + + async def begin_dialog(self, dc: DialogContext, options: object = None) -> DialogTurnResult: + + if not dc: + raise TypeError('WaterfallDialog.begin_dialog(): dc cannot be None.') + + # Initialize waterfall state + state = dc.active_dialog.state + + instance_id = uuid.uuid1().__str__() + state[self.PersistedOptions] = options + state[self.PersistedValues] = {} + state[self.PersistedInstanceId] = instance_id + + properties = {} + properties['DialogId'] = self.id + properties['InstanceId'] = instance_id + self.telemetry_client.track_event("WaterfallStart", properties = properties) + + # Run first stepkinds + return await self.run_step(dc, 0, DialogReason.BeginCalled, None) + + async def continue_dialog(self, dc: DialogContext = None, reason: DialogReason = None, result: object = NotImplementedError) -> DialogTurnResult: + if not dc: + raise TypeError('WaterfallDialog.continue_dialog(): dc cannot be None.') + + if dc.context.activity.type != ActivityTypes.message: + return Dialog.end_of_turn + + return await self.resume_dialog(dc, DialogReason.ContinueCalled, dc.context.activity.text) + + async def resume_dialog(self, dc: DialogContext, reason: DialogReason, result: object): + if dc is None: + raise TypeError('WaterfallDialog.resume_dialog(): dc cannot be None.') + + # Increment step index and run step + state = dc.active_dialog.state + + # Future Me: + # If issues with CosmosDB, see https://github.com/Microsoft/botbuilder-dotnet/issues/871 + # for hints. + return await self.run_step(dc, state[self.StepIndex] + 1, reason, result) + + async def end_dialog(self, turn_context: TurnContext, instance: DialogInstance, reason: DialogReason) -> None: + if reason is DialogReason.CancelCalled: + index = instance.state[self.StepIndex] + step_name = self.get_step_name(index) + instance_id = str(instance.state[self.PersistedInstanceId]) + properties = { + "DialogId": self.id, + "StepName" : step_name, + "InstanceId" : instance_id + } + self.telemetry_client.track_event("WaterfallCancel", properties) + else: + if reason is DialogReason.EndCalled: + + instance_id = str(instance.state[self.PersistedInstanceId]) + properties = { + "DialogId": self.id, + "InstanceId": instance_id + } + self.telemetry_client.track_event("WaterfallComplete", properties) + + return + + async def on_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + step_name = self.get_step_name(step_context.index) + instance_id = str(step_context.active_dialog.state[self.PersistedInstanceId]) + properties = { + "DialogId": self.id, + "StepName": step_name, + "InstanceId": instance_id + } + self.telemetry_client.track_event("WaterfallStep", properties) + return await self._steps[step_context.index](step_context) + + async def run_step(self, dc: DialogContext, index: int, reason: DialogReason, result: object) -> DialogTurnResult: + if not dc: + raise TypeError('WaterfallDialog.run_steps(): dc cannot be None.') + if index < len(self._steps): + # Update persisted step index + state = dc.active_dialog.state + state[self.StepIndex] = index + + # Create step context + options = state[self.PersistedOptions] + values = state[self.PersistedValues] + step_context = WaterfallStepContext(self, dc, options, values, index, reason, result) + return await self.on_step(step_context) + else: + # End of waterfall so just return any result to parent + return await dc.end_dialog(result) + + def get_step_name(self, index: int) -> str: + """ + Give the waterfall step a unique name + """ + step_name = self._steps[index].__qualname__ + + if not step_name or ">" in step_name : + step_name = f"Step{index + 1}of{len(self._steps)}" + return step_name \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/README-LUIS.md b/samples/python-flask/21.corebot-app-insights/README-LUIS.md new file mode 100644 index 000000000..61bde7719 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/README-LUIS.md @@ -0,0 +1,216 @@ +# Setting up LUIS via CLI: + +This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App. + +> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_ +> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_ +> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_ + + [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app + [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app + +## Table of Contents: + +- [Prerequisites](#Prerequisites) +- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application) +- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application) + +___ + +## [Prerequisites](#Table-of-Contents): + +#### Install Azure CLI >=2.0.61: + +Visit the following page to find the correct installer for your OS: +- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest + +#### Install LUIS CLI >=2.4.0: + +Open a CLI of your choice and type the following: + +```bash +npm i -g luis-apis@^2.4.0 +``` + +#### LUIS portal account: + +You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions]. + +After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID. + + [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions] + [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key + +___ + +## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents) + +### 1. Import the local LUIS application to luis.ai + +```bash +luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json" +``` + +Outputs the following JSON: + +```json +{ + "id": "########-####-####-####-############", + "name": "FlightBooking", + "description": "A LUIS model that uses intent and entities.", + "culture": "en-us", + "usageScenario": "", + "domain": "", + "versionsCount": 1, + "createdDateTime": "2019-03-29T18:32:02Z", + "endpoints": {}, + "endpointHitsCount": 0, + "activeVersion": "0.1", + "ownerEmail": "bot@contoso.com", + "tokenizerVersion": "1.0.0" +} +``` + +For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`. + +### 2. Train the LUIS Application + +```bash +luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait +``` + +### 3. Publish the LUIS Application + +```bash +luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion" +``` + +> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast".
+> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai.
+> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth").
+> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions. + + [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78 + +Outputs the following: + +```json + { + "versionId": "0.1", + "isStaging": false, + "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############", + "region": "westus", + "assignedEndpointKey": null, + "endpointRegion": "westus", + "failedRegions": "", + "publishedDateTime": "2019-03-29T18:40:32Z", + "directVersionPublish": false +} +``` + +To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application. + + [README-LUIS]: ./README-LUIS.md + +___ + +## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents) + +### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI + +> _Note:_
+> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_ +> ```bash +> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName" +> ``` +> _To see a list of valid locations, use `az account list-locations`_ + + +```bash +# Use Azure CLI to create the LUIS Key resource on Azure +az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +The command will output a response similar to the JSON below: + +```json +{ + "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0", + "etag": "\"########-####-####-####-############\"", + "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName", + "internalId": "################################", + "kind": "luis", + "location": "westus", + "name": "NewLuisResourceName", + "provisioningState": "Succeeded", + "resourceGroup": "ResourceGroupName", + "sku": { + "name": "S0", + "tier": null + }, + "tags": null, + "type": "Microsoft.CognitiveServices/accounts" +} +``` + + + +Take the output from the previous command and create a JSON file in the following format: + +```json +{ + "azureSubscriptionId": "00000000-0000-0000-0000-000000000000", + "resourceGroup": "ResourceGroupName", + "accountName": "NewLuisResourceName" +} +``` + +### 2. Retrieve ARM access token via Azure CLI + +```bash +az account get-access-token --subscription "AzureSubscriptionGuid" +``` + +This will return an object that looks like this: + +```json +{ + "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken", + "expiresOn": "2200-12-31 23:59:59.999999", + "subscription": "AzureSubscriptionGuid", + "tenant": "tenant-guid", + "tokenType": "Bearer" +} +``` + +The value needed for the next step is the `"accessToken"`. + +### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application + +```bash +luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken" +``` + +If successful, it should yield a response like this: + +```json +{ + "code": "Success", + "message": "Operation Successful" +} +``` + +### 4. See the LUIS Cognitive Services' keys + +```bash +az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName" +``` + +This will return an object that looks like this: + +```json +{ + "key1": "9a69####dc8f####8eb4####399f####", + "key2": "####f99e####4b1a####fb3b####6b9f" +} +``` \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/README.md b/samples/python-flask/21.corebot-app-insights/README.md new file mode 100644 index 000000000..175429c06 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/README.md @@ -0,0 +1,56 @@ +# CoreBot + +Bot Framework v4 core bot sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to: + +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities +- Implement a multi-turn conversation using Dialogs +- Handle user interruptions for such things as `Help` or `Cancel` +- Prompt for and validate requests for information from the user + +## Prerequisites + +This sample **requires** prerequisites in order to run. + +### Overview + +This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding. + +### Install Python 3.6 + + +### Create a LUIS Application to enable language understanding + +LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs). + +If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md). + + +## Testing the bot using Bot Framework Emulator + +[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases) + +### Connect to the bot using Bot Framework Emulator + +- Launch Bot Framework Emulator +- File -> Open Bot +- Enter a Bot URL of `http://localhost:3978/api/messages` + + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) +- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/booking_details.py b/samples/python-flask/21.corebot-app-insights/booking_details.py new file mode 100644 index 000000000..03e066017 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/booking_details.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +class BookingDetails: + def __init__(self, destination: str = None, origin: str = None, travel_date: str = None): + self.destination = destination + self.origin = origin + self.travel_date = travel_date \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/bots/__init__.py b/samples/python-flask/21.corebot-app-insights/bots/__init__.py new file mode 100644 index 000000000..194b46c68 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/bots/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .dialog_bot import DialogBot +from .dialog_and_welcome_bot import DialogAndWelcomeBot + +__all__ = [ + 'DialogBot', + 'DialogAndWelcomeBot'] \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py b/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py new file mode 100644 index 000000000..5fb305735 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import os.path + +from typing import List +from botbuilder.core import CardFactory +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.dialogs import Dialog +from botbuilder.schema import Activity, Attachment, ChannelAccount +from helpers.activity_helper import create_activity_reply +from botbuilder.core import BotTelemetryClient +from .dialog_bot import DialogBot + +class DialogAndWelcomeBot(DialogBot): + + def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog, telemetry_client: BotTelemetryClient): + super(DialogAndWelcomeBot, self).__init__(conversation_state, user_state, dialog, telemetry_client) + self.telemetry_client = telemetry_client + + async def on_members_added_activity(self, members_added: List[ChannelAccount], turn_context: TurnContext): + for member in members_added: + # Greet anyone that was not the target (recipient) of this message. + # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. + if member.id != turn_context.activity.recipient.id: + welcome_card = self.create_adaptive_card_attachment() + response = self.create_response(turn_context.activity, welcome_card) + await turn_context.send_activity(response) + + # Create an attachment message response. + def create_response(self, activity: Activity, attachment: Attachment): + response = create_activity_reply(activity) + response.attachments = [attachment] + return response + + # Load attachment from file. + def create_adaptive_card_attachment(self): + relative_path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.join(relative_path, "resources/welcomeCard.json") + with open(path) as f: + card = json.load(f) + + return Attachment( + content_type= "application/vnd.microsoft.card.adaptive", + content= card) \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py b/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py new file mode 100644 index 000000000..87b49cfa4 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio + +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.dialogs import Dialog +from helpers.dialog_helper import DialogHelper +from botbuilder.core import BotTelemetryClient, NullTelemetryClient + +class DialogBot(ActivityHandler): + + def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog, telemetry_client: BotTelemetryClient): + if conversation_state is None: + raise Exception('[DialogBot]: Missing parameter. conversation_state is required') + if user_state is None: + raise Exception('[DialogBot]: Missing parameter. user_state is required') + if dialog is None: + raise Exception('[DialogBot]: Missing parameter. dialog is required') + + self.conversation_state = conversation_state + self.user_state = user_state + self.dialog = dialog + self.dialogState = self.conversation_state.create_property('DialogState') + self.telemetry_client = telemetry_client + + async def on_turn(self, turn_context: TurnContext): + await super().on_turn(turn_context) + + # Save any state changes that might have occured during the turn. + await self.conversation_state.save_changes(turn_context, False) + await self.user_state.save_changes(turn_context, False) + + async def on_message_activity(self, turn_context: TurnContext): + await DialogHelper.run_dialog(self.dialog, turn_context, self.conversation_state.create_property("DialogState")) + + @property + def telemetry_client(self) -> BotTelemetryClient: + """ + Gets the telemetry client for logging events. + """ + return self._telemetry_client + + @telemetry_client.setter + def telemetry_client(self, value: BotTelemetryClient) -> None: + """ + Sets the telemetry client for logging events. + """ + if value is None: + self._telemetry_client = NullTelemetryClient() + else: + self._telemetry_client = value diff --git a/samples/python-flask/21.corebot-app-insights/bots/resources/welcomeCard.json b/samples/python-flask/21.corebot-app-insights/bots/resources/welcomeCard.json new file mode 100644 index 000000000..169f6328d --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/bots/resources/welcomeCard.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "Image", + "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "size": "stretch" + }, + { + "type": "TextBlock", + "spacing": "medium", + "size": "default", + "weight": "bolder", + "text": "Welcome to Bot Framework!", + "wrap": true, + "maxLines": 0 + }, + { + "type": "TextBlock", + "size": "default", + "isSubtle": "yes", + "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.", + "wrap": true, + "maxLines": 0 + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Get an overview", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Ask a question", + "url": "https://stackoverflow.com/questions/tagged/botframework" + }, + { + "type": "Action.OpenUrl", + "title": "Learn how to deploy", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0" + } + ] +} \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/cognitiveModels/FlightBooking.json b/samples/python-flask/21.corebot-app-insights/cognitiveModels/FlightBooking.json new file mode 100644 index 000000000..5d1c9ec38 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/cognitiveModels/FlightBooking.json @@ -0,0 +1,226 @@ +{ + "luis_schema_version": "3.2.0", + "versionId": "0.1", + "name": "Airline Reservation", + "desc": "A LUIS model that uses intent and entities.", + "culture": "en-us", + "tokenizerVersion": "1.0.0", + "intents": [ + { + "name": "Book flight" + }, + { + "name": "Cancel" + }, + { + "name": "None" + } + ], + "entities": [], + "composites": [ + { + "name": "From", + "children": [ + "Airport" + ], + "roles": [] + }, + { + "name": "To", + "children": [ + "Airport" + ], + "roles": [] + } + ], + "closedLists": [ + { + "name": "Airport", + "subLists": [ + { + "canonicalForm": "Paris", + "list": [ + "paris" + ] + }, + { + "canonicalForm": "London", + "list": [ + "london" + ] + }, + { + "canonicalForm": "Berlin", + "list": [ + "berlin" + ] + }, + { + "canonicalForm": "New York", + "list": [ + "new york" + ] + } + ], + "roles": [] + } + ], + "patternAnyEntities": [], + "regex_entities": [], + "prebuiltEntities": [ + { + "name": "datetimeV2", + "roles": [] + } + ], + "model_features": [], + "regex_features": [], + "patterns": [], + "utterances": [ + { + "text": "book flight from london to paris on feb 14th", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 27, + "endPos": 31 + }, + { + "entity": "From", + "startPos": 17, + "endPos": 22 + } + ] + }, + { + "text": "book flight to berlin on feb 14th", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 15, + "endPos": 20 + } + ] + }, + { + "text": "book me a flight from london to paris", + "intent": "Book flight", + "entities": [ + { + "entity": "From", + "startPos": 22, + "endPos": 27 + }, + { + "entity": "To", + "startPos": 32, + "endPos": 36 + } + ] + }, + { + "text": "bye", + "intent": "Cancel", + "entities": [] + }, + { + "text": "cancel booking", + "intent": "Cancel", + "entities": [] + }, + { + "text": "exit", + "intent": "Cancel", + "entities": [] + }, + { + "text": "flight to paris", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + }, + { + "text": "flight to paris from london on feb 14th", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + }, + { + "entity": "From", + "startPos": 21, + "endPos": 26 + } + ] + }, + { + "text": "fly from berlin to paris on may 5th", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 19, + "endPos": 23 + }, + { + "entity": "From", + "startPos": 9, + "endPos": 14 + } + ] + }, + { + "text": "go to paris", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 6, + "endPos": 10 + } + ] + }, + { + "text": "going from paris to berlin", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 20, + "endPos": 25 + }, + { + "entity": "From", + "startPos": 11, + "endPos": 15 + } + ] + }, + { + "text": "ignore", + "intent": "Cancel", + "entities": [] + }, + { + "text": "travel to paris", + "intent": "Book flight", + "entities": [ + { + "entity": "To", + "startPos": 10, + "endPos": 14 + } + ] + } + ], + "settings": [] +} \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/config.py b/samples/python-flask/21.corebot-app-insights/config.py new file mode 100644 index 000000000..7d0a51c5c --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/config.py @@ -0,0 +1,8 @@ +class DefaultConfig(object): + PORT = 3978 + APP_ID = "" + APP_PASSWORD = "" + LUIS_APP_ID = "" + LUIS_API_KEY = "" + LUIS_API_HOST_NAME = "" + APPINSIGHTS_INSTRUMENTATION_KEY = "" \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py b/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py new file mode 100644 index 000000000..fb59710ca --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .booking_dialog import BookingDialog +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog +from .main_dialog import MainDialog + +__all__ = [ + 'BookingDialog', + 'CancelAndHelpDialog', + 'DateResolverDialog', + 'MainDialog'] \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py new file mode 100644 index 000000000..7dcd0d4f8 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions +from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient +from .cancel_and_help_dialog import CancelAndHelpDialog +from .date_resolver_dialog import DateResolverDialog +from datatypes_date_time.timex import Timex + + +class BookingDialog(CancelAndHelpDialog): + + def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__, telemetry_client) + self.telemetry_client = telemetry_client + text_prompt = TextPrompt(TextPrompt.__name__) + text_prompt.telemetry_client = telemetry_client + + waterfall_dialog = WaterfallDialog(WaterfallDialog.__name__, [ + self.destination_step, + self.origin_step, + self.travel_date_step, + #self.confirm_step, + self.final_step + ]) + waterfall_dialog.telemetry_client = telemetry_client + + self.add_dialog(text_prompt) + #self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) + self.add_dialog(DateResolverDialog(DateResolverDialog.__name__, self.telemetry_client)) + self.add_dialog(waterfall_dialog) + + self.initial_dialog_id = WaterfallDialog.__name__ + + """ + If a destination city has not been provided, prompt for one. + """ + async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + booking_details = step_context.options + + if (booking_details.destination is None): + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt= MessageFactory.text('To what city would you like to travel?'))) + else: + return await step_context.next(booking_details.destination) + + """ + If an origin city has not been provided, prompt for one. + """ + async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + booking_details = step_context.options + + # Capture the response to the previous step's prompt + booking_details.destination = step_context.result + if (booking_details.origin is None): + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt= MessageFactory.text('From what city will you be travelling?'))) + else: + return await step_context.next(booking_details.origin) + + """ + If a travel date has not been provided, prompt for one. + This will use the DATE_RESOLVER_DIALOG. + """ + async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.origin = step_context.result + if (not booking_details.travel_date or self.is_ambiguous(booking_details.travel_date)): + return await step_context.begin_dialog(DateResolverDialog.__name__, booking_details.travel_date) + else: + return await step_context.next(booking_details.travel_date) + + """ + Confirm the information the user has provided. + """ + async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + booking_details = step_context.options + + # Capture the results of the previous step + booking_details.travel_date= step_context.result + msg = f'Please confirm, I have you traveling to: { booking_details.destination } from: { booking_details.origin } on: { booking_details.travel_date}.' + + # Offer a YES/NO prompt. + return await step_context.prompt(ConfirmPrompt.__name__, PromptOptions(prompt= MessageFactory.text(msg))) + + """ + Complete the interaction and end the dialog. + """ + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + + if step_context.result: + booking_details = step_context.options + booking_details.travel_date= step_context.result + + return await step_context.end_dialog(booking_details) + else: + return await step_context.end_dialog() + + def is_ambiguous(self, timex: str) -> bool: + timex_property = Timex(timex) + return 'definite' not in timex_property.types + + + + \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py new file mode 100644 index 000000000..8a59a50ea --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from botbuilder.core import BotTelemetryClient, NullTelemetryClient +from botbuilder.dialogs import ComponentDialog, DialogContext, DialogTurnResult, DialogTurnStatus +from botbuilder.schema import ActivityTypes + + +class CancelAndHelpDialog(ComponentDialog): + + def __init__(self, dialog_id: str, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + super(CancelAndHelpDialog, self).__init__(dialog_id) + self.telemetry_client = telemetry_client + + async def on_begin_dialog(self, inner_dc: DialogContext, options: object) -> DialogTurnResult: + result = await self.interrupt(inner_dc) + if result is not None: + return result + + return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options) + + async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: + result = await self.interrupt(inner_dc) + if result is not None: + return result + + return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) + + async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: + if inner_dc.context.activity.type == ActivityTypes.message: + text = inner_dc.context.activity.text.lower() + + if text == 'help' or text == '?': + await inner_dc.context.send_activity("Show Help...") + return DialogTurnResult(DialogTurnStatus.Waiting) + + if text == 'cancel' or text == 'quit': + await inner_dc.context.send_activity("Cancelling") + return await inner_dc.cancel_all_dialogs() + + return None \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py new file mode 100644 index 000000000..23b1b4e27 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient +from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext +from botbuilder.dialogs.prompts import DateTimePrompt, PromptValidatorContext, PromptOptions, DateTimeResolution +from .cancel_and_help_dialog import CancelAndHelpDialog +from datatypes_date_time.timex import Timex +class DateResolverDialog(CancelAndHelpDialog): + + def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + super(DateResolverDialog, self).__init__(dialog_id or DateResolverDialog.__name__, telemetry_client) + self.telemetry_client = telemetry_client + + date_time_prompt = DateTimePrompt(DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator) + date_time_prompt.telemetry_client = telemetry_client + + waterfall_dialog = WaterfallDialog(WaterfallDialog.__name__ + '2', [ + self.initialStep, + self.finalStep + ]) + waterfall_dialog.telemetry_client = telemetry_client + + self.add_dialog(date_time_prompt) + self.add_dialog(waterfall_dialog) + + self.initial_dialog_id = WaterfallDialog.__name__ + '2' + + async def initialStep(self,step_context: WaterfallStepContext) -> DialogTurnResult: + timex = step_context.options + + prompt_msg = 'On what date would you like to travel?' + reprompt_msg = "I'm sorry, for best results, please enter your travel date including the month, day and year." + + if timex is None: + # We were not given any date at all so prompt the user. + return await step_context.prompt(DateTimePrompt.__name__ , + PromptOptions( + prompt= MessageFactory.text(prompt_msg), + retry_prompt= MessageFactory.text(reprompt_msg) + )) + else: + # We have a Date we just need to check it is unambiguous. + if 'definite' in Timex(timex).types: + # This is essentially a "reprompt" of the data we were given up front. + return await step_context.prompt(DateTimePrompt.__name__, PromptOptions(prompt= reprompt_msg)) + else: + return await step_context.next(DateTimeResolution(timex= timex)) + + async def finalStep(self, step_context: WaterfallStepContext): + timex = step_context.result[0].timex + return await step_context.end_dialog(timex) + + @staticmethod + async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: + if prompt_context.recognized.succeeded: + timex = prompt_context.recognized.value[0].timex.split('T')[0] + + #TODO: Needs TimexProperty + return 'definite' in Timex(timex).types + + return False diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py new file mode 100644 index 000000000..a0d99a2bb --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +from botbuilder.core import BotTelemetryClient, NullTelemetryClient +from botbuilder.dialogs import ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, WaterfallStepContext, DialogTurnResult +from botbuilder.dialogs.prompts import TextPrompt, ConfirmPrompt, PromptOptions +from botbuilder.core import MessageFactory +from .booking_dialog import BookingDialog +from booking_details import BookingDetails +from helpers.luis_helper import LuisHelper +from datatypes_date_time.timex import Timex + +class MainDialog(ComponentDialog): + + def __init__(self, configuration: dict, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) + + self._configuration = configuration + self.telemetry_client = telemetry_client + + text_prompt = TextPrompt(TextPrompt.__name__) + text_prompt.telemetry_client = self.telemetry_client + + booking_dialog = BookingDialog(telemetry_client = self._telemetry_client) + booking_dialog.telemetry_client = self.telemetry_client + + wf_dialog = WaterfallDialog('WFDialog', [ + self.intro_step, + self.act_step, + self.final_step + ]) + wf_dialog.telemetry_client = self.telemetry_client + + self.add_dialog(text_prompt) + self.add_dialog(booking_dialog) + self.add_dialog(wf_dialog) + + self.initial_dialog_id = 'WFDialog' + + async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + if (not self._configuration.get("LUIS_APP_ID", "") or not self._configuration.get("LUIS_API_KEY", "") or not self._configuration.get("LUIS_API_HOST_NAME", "")): + await step_context.context.send_activity( + MessageFactory.text("NOTE: LUIS is not configured. To enable all capabilities, add 'LUIS_APP_ID', 'LUIS_API_KEY' and 'LUIS_API_HOST_NAME' to the config.py file.")) + + return await step_context.next(None) + else: + return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt = MessageFactory.text("What can I help you with today?"))) + + + async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) + booking_details = await LuisHelper.excecute_luis_query(self._configuration, step_context.context) if step_context.result is not None else BookingDetails() + + # In this sample we only have a single Intent we are concerned with. However, typically a scenario + # will have multiple different Intents each corresponding to starting a different child Dialog. + + # Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder. + return await step_context.begin_dialog(BookingDialog.__name__, booking_details) + + async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + # If the child dialog ("BookingDialog") was cancelled or the user failed to confirm, the Result here will be null. + if (step_context.result is not None): + result = step_context.result + + # Now we have all the booking details call the booking service. + + # If the call to the booking service was successful tell the user. + #time_property = Timex(result.travel_date) + #travel_date_msg = time_property.to_natural_language(datetime.now()) + msg = f'I have you booked to {result.destination} from {result.origin} on {result.travel_date}' + await step_context.context.send_activity(MessageFactory.text(msg)) + else: + await step_context.context.send_activity(MessageFactory.text("Thank you.")) + return await step_context.end_dialog() diff --git a/samples/python-flask/21.corebot-app-insights/helpers/__init__.py b/samples/python-flask/21.corebot-app-insights/helpers/__init__.py new file mode 100644 index 000000000..a117d0e80 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/helpers/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import activity_helper, luis_helper, dialog_helper + +__all__ = [ + 'activity_helper', + 'dialog_helper', + 'luis_helper'] \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py new file mode 100644 index 000000000..84b948ac0 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, ConversationAccount + +def create_activity_reply(activity: Activity, text: str = None, locale: str = None): + + return Activity( + type = ActivityTypes.message, + timestamp = datetime.utcnow(), + from_property = ChannelAccount(id= getattr(activity.recipient, 'id', None), name= getattr(activity.recipient, 'name', None)), + recipient = ChannelAccount(id= activity.from_property.id, name= activity.from_property.name), + reply_to_id = activity.id, + service_url = activity.service_url, + channel_id = activity.channel_id, + conversation = ConversationAccount(is_group= activity.conversation.is_group, id= activity.conversation.id, name= activity.conversation.name), + text = text or '', + locale = locale or '', + attachments = [], + entities = [] + ) \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py new file mode 100644 index 000000000..edda1c318 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import StatePropertyAccessor, TurnContext +from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus + +class DialogHelper: + + @staticmethod + async def run_dialog(dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor): + dialog_set = DialogSet(accessor) + dialog_set.add(dialog) + + dialog_context = await dialog_set.create_context(turn_context) + results = await dialog_context.continue_dialog() + if results.status == DialogTurnStatus.Empty: + await dialog_context.begin_dialog(dialog.id) \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py new file mode 100644 index 000000000..0a3195529 --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from botbuilder.ai.luis import LuisRecognizer, LuisApplication +from botbuilder.core import TurnContext + +from booking_details import BookingDetails + +class LuisHelper: + + @staticmethod + async def excecute_luis_query(configuration: dict, turn_context: TurnContext) -> BookingDetails: + booking_details = BookingDetails() + + try: + luis_application = LuisApplication( + configuration['LUIS_APP_ID'], + configuration['LUIS_API_KEY'], + configuration['LUIS_API_HOST_NAME'] + ) + + recognizer = LuisRecognizer(luis_application) + recognizer_result = await recognizer.recognize(turn_context) + + if recognizer_result.intents: + intent = sorted(recognizer_result.intents, key=recognizer_result.intents.get, reverse=True)[:1][0] + if intent == 'Book_flight': + # We need to get the result from the LUIS JSON which at every level returns an array. + to_entities = recognizer_result.entities.get("$instance", {}).get("To", []) + if len(to_entities) > 0: + booking_details.destination = to_entities[0]['text'] + from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) + if len(from_entities) > 0: + booking_details.origin = from_entities[0]['text'] + + # TODO: This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. + date_entities = recognizer_result.entities.get("$instance", {}).get("datetime", []) + if len(date_entities) > 0: + text = date_entities[0]['text'] + booking_details.travel_date = None # TODO: Set when we get a timex format + except Exception as e: + print(e) + + return booking_details + diff --git a/samples/python-flask/21.corebot-app-insights/main.py b/samples/python-flask/21.corebot-app-insights/main.py new file mode 100644 index 000000000..93ea28afc --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/main.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3ex +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +This sample shows how to create a bot that demonstrates the following: +- Use [LUIS](https://www.luis.ai) to implement core AI capabilities. +- Implement a multi-turn conversation using Dialogs. +- Handle user interruptions for such things as `Help` or `Cancel`. +- Prompt for and validate requests for information from the user. +gi +""" +from functools import wraps +import json +import asyncio +import sys +from flask import Flask, jsonify, request, Response +from botbuilder.schema import (Activity, ActivityTypes) +from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext, + ConversationState, MemoryStorage, UserState) + +from dialogs import MainDialog +from bots import DialogAndWelcomeBot +from helpers.dialog_helper import DialogHelper + +from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient +from botbuilder.applicationinsights.flask import BotTelemetryMiddleware + +loop = asyncio.get_event_loop() +app = Flask(__name__, instance_relative_config=True) +app.config.from_object('config.DefaultConfig') +app.wsgi_app = BotTelemetryMiddleware(app.wsgi_app) + +SETTINGS = BotFrameworkAdapterSettings(app.config['APP_ID'], app.config['APP_PASSWORD']) +ADAPTER = BotFrameworkAdapter(SETTINGS) + +# Catch-all for errors. +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f'\n [on_turn_error]: { error }', file=sys.stderr) + # Send a message to the user + await context.send_activity('Oops. Something went wrong!') + # Clear out state + await conversation_state.delete(context) + +ADAPTER.on_turn_error = on_error + +# Create MemoryStorage, UserState and ConversationState +memory = MemoryStorage() + +user_state = UserState(memory) +conversation_state = ConversationState(memory) +instrumentation_key=app.config['APPINSIGHTS_INSTRUMENTATION_KEY'] +telemetry_client = ApplicationInsightsTelemetryClient(instrumentation_key) +dialog = MainDialog(app.config, telemetry_client = telemetry_client) +bot = DialogAndWelcomeBot(conversation_state, user_state, dialog, telemetry_client) + + +@app.route('/api/messages', methods = ['POST']) +def messages(): + if request.headers['Content-Type'] == 'application/json': + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = request.headers['Authorization'] if 'Authorization' in request.headers else '' + + async def aux_func(turn_context): + asyncio.ensure_future(bot.on_turn(turn_context)) + try: + task = loop.create_task(ADAPTER.process_activity(activity, auth_header, aux_func)) + loop.run_until_complete(task) + return Response(status=201) + except Exception as e: + raise e + +if __name__ == "__main__" : + try: + app.run(debug=True, port=app.config["PORT"]) + except Exception as e: + raise e + diff --git a/samples/python-flask/21.corebot-app-insights/requirements.txt b/samples/python-flask/21.corebot-app-insights/requirements.txt new file mode 100644 index 000000000..ffcf72c6b --- /dev/null +++ b/samples/python-flask/21.corebot-app-insights/requirements.txt @@ -0,0 +1,13 @@ +Flask>=1.0.2 +asyncio>=3.4.3 +requests>=2.18.1 +botframework-connector>=4.4.0.b1 +botbuilder-schema>=4.4.0.b1 +botbuilder-core>=4.4.0.b1 +botbuilder-dialogs>=4.4.0.b1 +botbuilder-ai>=4.4.0.b1 +botbuilder-applicationinsights>=4.4.0.b1 +datatypes-date-time>=1.0.0.a1 +azure-cognitiveservices-language-luis>=0.2.0 +msrest>=0.6.6 + From 1403611e80ba9c1786c36c28977deca4dd0eca9e Mon Sep 17 00:00:00 2001 From: Dave Taniguchi Date: Sun, 23 Jun 2019 14:13:29 -0700 Subject: [PATCH 2/3] Fix pylint issues --- .../botbuilder/applicationinsights/about.py | 25 +++--- .../application_insights_telemetry_client.py | 90 +++++++++---------- .../applicationinsights/django/__init__.py | 1 + .../django/bot_telemetry_middleware.py | 25 +++--- .../applicationinsights/flask/__init__.py | 10 +++ .../flask/flask_telemetry_middleware.py | 53 +++++++++++ .../integration_post_data.py | 17 ++-- .../botbuilder/dialogs/dialog_set.py | 9 +- .../booking_details.py | 5 +- .../21.corebot-app-insights/bots/__init__.py | 5 +- .../bots/dialog_and_welcome_bot.py | 33 +++---- .../bots/dialog_bot.py | 23 ++--- .../21.corebot-app-insights/config.py | 9 +- .../dialogs/__init__.py | 10 +-- .../dialogs/booking_dialog.py | 87 +++++++++--------- .../dialogs/cancel_and_help_dialog.py | 14 +-- .../dialogs/date_resolver_dialog.py | 58 +++++++----- .../dialogs/main_dialog.py | 69 +++++++++----- .../helpers/__init__.py | 7 +- .../helpers/activity_helper.py | 32 ++++--- .../helpers/dialog_helper.py | 8 +- .../helpers/luis_helper.py | 44 +++++---- .../21.corebot-app-insights/main.py | 80 ++++++++--------- 23 files changed, 416 insertions(+), 298 deletions(-) create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py create mode 100644 libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py index bb0fd5e9d..f897d2e39 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py @@ -1,12 +1,13 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -__title__ = 'botbuilder-applicationinsights' -__version__ = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1" -__uri__ = 'https://www.github.com/Microsoft/botbuilder-python' -__author__ = 'Microsoft' -__description__ = 'Microsoft Bot Framework Bot Builder' -__summary__ = 'Microsoft Bot Framework Bot Builder SDK for Python.' -__license__ = 'MIT' +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Bot Framework Application Insights integration package info.""" + +import os + +__title__ = 'botbuilder-applicationinsights' +__version__ = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1" +__uri__ = 'https://www.github.com/Microsoft/botbuilder-python' +__author__ = 'Microsoft' +__description__ = 'Microsoft Bot Framework Bot Builder' +__summary__ = 'Microsoft Bot Framework Bot Builder SDK for Python.' +__license__ = 'MIT' diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py index a6eb69cb0..9f3921cff 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import sys +"""Application Insights Telemetry Processor for Bots.""" + import traceback +from typing import Dict from applicationinsights import TelemetryClient from botbuilder.core.bot_telemetry_client import BotTelemetryClient, TelemetryDataPointType -from typing import Dict from .integration_post_data import IntegrationPostData +# pylint: disable=line-too-long def bot_telemetry_processor(data, context) -> bool: """ Application Insights Telemetry Processor for Bot Traditional Web user and session ID's don't apply for Bots. This processor @@ -27,7 +29,6 @@ def bot_telemetry_processor(data, context) -> bool: message identifier. - channelId - The Bot Framework "Channel" (ie, slack/facebook/directline/etc) - activityType - The Bot Framework message classification (ie, message) - :param data: Data from Application Insights :type data: telemetry item @@ -40,10 +41,10 @@ def bot_telemetry_processor(data, context) -> bool: # If there is no body (not a BOT request or not configured correctly). # We *could* filter here, but we're allowing event to go through. return True - + # Override session and user id from_prop = post_data['from'] if 'from' in post_data else None - user_id = from_prop['id'] if from_prop != None else None + user_id = from_prop['id'] if from_prop is not None else None channel_id = post_data['channelId'] if 'channelId' in post_data else None conversation = post_data['conversation'] if 'conversation' in post_data else None conversation_id = conversation['id'] if 'id' in conversation else None @@ -52,25 +53,26 @@ def bot_telemetry_processor(data, context) -> bool: # Additional bot-specific properties if 'id' in post_data: - data.properties["activityId"] = post_data['id'] + data.properties["activityId"] = post_data['id'] if 'channelId' in post_data: - data.properties["channelId"] = post_data['channelId'] + data.properties["channelId"] = post_data['channelId'] if 'type' in post_data: data.properties["activityType"] = post_data['type'] return True class ApplicationInsightsTelemetryClient(BotTelemetryClient): - - def __init__(self, instrumentation_key:str, telemetry_client: TelemetryClient = None): + """Application Insights Telemetry Client.""" + def __init__(self, instrumentation_key: str, telemetry_client: TelemetryClient = None): self._instrumentation_key = instrumentation_key - self._client = telemetry_client if telemetry_client != None else TelemetryClient(self._instrumentation_key) + self._client = telemetry_client if telemetry_client is not None \ + else TelemetryClient(self._instrumentation_key) # Telemetry Processor self._client.add_telemetry_processor(bot_telemetry_processor) - - def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dict[str, object]=None, - measurements: Dict[str, object]=None) -> None: + + def track_pageview(self, name: str, url: str, duration: int = 0, properties: Dict[str, object] = None, + measurements: Dict[str, object] = None) -> None: """ Send information about the page viewed in the application (a web page for instance). :param name: the name of the page that was viewed. @@ -80,58 +82,58 @@ def track_pageview(self, name: str, url:str, duration: int = 0, properties : Dic :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) """ self._client.track_pageview(name, url, duration, properties, measurements) - - def track_exception(self, type_exception: type = None, value : Exception =None, tb : traceback =None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None) -> None: - """ + + def track_exception(self, exception_type: type = None, value: Exception = None, tb: traceback = None, + properties: Dict[str, object] = None, measurements: Dict[str, object] = None) -> None: + """ Send information about a single exception that occurred in the application. - :param type_exception: the type of the exception that was thrown. + :param exception_type: the type of the exception that was thrown. :param value: the exception that the client wants to send. :param tb: the traceback information as returned by :func:`sys.exc_info`. :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) """ - self._client.track_exception(type_exception, value, tb, properties, measurements) + self._client.track_exception(exception_type, value, tb, properties, measurements) - def track_event(self, name: str, properties: Dict[str, object] = None, + def track_event(self, name: str, properties: Dict[str, object] = None, measurements: Dict[str, object] = None) -> None: - """ + """ Send information about a single event that has occurred in the context of the application. :param name: the data to associate to this event. :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) """ - self._client.track_event(name, properties = properties, measurements = measurements) + self._client.track_event(name, properties=properties, measurements=measurements) - def track_metric(self, name: str, value: float, type: TelemetryDataPointType =None, - count: int =None, min: float=None, max: float=None, std_dev: float=None, - properties: Dict[str, object]=None) -> NotImplemented: + def track_metric(self, name: str, value: float, tel_type: TelemetryDataPointType = None, + count: int = None, min_val: float = None, max_val: float = None, std_dev: float = None, + properties: Dict[str, object] = None) -> NotImplemented: """ Send information about a single metric data point that was captured for the application. :param name: The name of the metric that was captured. :param value: The value of the metric that was captured. - :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) + :param tel_type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`) :param count: the number of metrics that were aggregated into this data point. (defaults to: None) - :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None) - :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None) + :param min_val: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None) + :param max_val: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None) :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None) :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) """ - self._client.track_metric(name, value, type, count, min, max, std_dev, properties) + self._client.track_metric(name, value, tel_type, count, min_val, max_val, std_dev, properties) - def track_trace(self, name: str, properties: Dict[str, object]=None, severity=None): + def track_trace(self, name: str, properties: Dict[str, object] = None, severity=None): """ Sends a single trace statement. - :param name: the trace statement.\n - :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n + :param name: the trace statement. + :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None) :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL """ self._client.track_trace(name, properties, severity) - def track_request(self, name: str, url: str, success: bool, start_time: str=None, - duration: int=None, response_code: str =None, http_method: str=None, - properties: Dict[str, object]=None, measurements: Dict[str, object]=None, - request_id: str=None): + def track_request(self, name: str, url: str, success: bool, start_time: str = None, + duration: int = None, response_code: str = None, http_method: str = None, + properties: Dict[str, object] = None, measurements: Dict[str, object] = None, + request_id: str = None): """ Sends a single request that was captured for the application. :param name: The name for this request. All requests with the same name will be grouped together. @@ -146,16 +148,16 @@ def track_request(self, name: str, url: str, success: bool, start_time: str=None :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None) """ self._client.track_request(name, url, success, start_time, duration, response_code, http_method, properties, - measurements, request_id) + measurements, request_id) - def track_dependency(self, name:str, data:str, type:str=None, target:str=None, duration:int=None, - success:bool=None, result_code:str=None, properties:Dict[str, object]=None, - measurements:Dict[str, object]=None, dependency_id:str=None): + def track_dependency(self, name: str, data: str, type_name: str = None, target: str = None, duration: int = None, + success: bool = None, result_code: str = None, properties: Dict[str, object] = None, + measurements: Dict[str, object] = None, dependency_id: str = None): """ Sends a single dependency telemetry that was captured for the application. :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template. :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters. - :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None) + :param type_name: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None) :param target: the target site of a dependency call. Examples are server name, host address. (default to: None) :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None) :param success: true if the dependency call ended in success, false otherwise. (defaults to: None) @@ -164,13 +166,11 @@ def track_dependency(self, name:str, data:str, type:str=None, target:str=None, d :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None) :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None) """ - self._client.track_dependency(name, data, type, target, duration, success, result_code, properties, - measurements, dependency_id) + self._client.track_dependency(name, data, type_name, target, duration, success, result_code, properties, + measurements, dependency_id) def flush(self): """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is being used. """ self._client.flush() - - diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py index 27a45e0a3..dd39a0808 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Djanjo Application Insights package.""" from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py index 043e17f93..c117b59ea 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py @@ -1,21 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import sys -import json +"""Bot Telemetry Middleware.""" + from threading import current_thread # Map of thread id => POST body text -_request_bodies = {} +_REQUEST_BODIES = {} def retrieve_bot_body(): """ retrieve_bot_body Retrieve the POST body text from temporary cache. - The POST body corresponds with the thread id and should resides in + The POST body corresponds with the thread id and should resides in cache just for lifetime of request. TODO: Add cleanup job to kill orphans """ - result = _request_bodies.pop(current_thread().ident, None) + result = _REQUEST_BODIES.pop(current_thread().ident, None) return result class BotTelemetryMiddleware(): @@ -25,7 +25,8 @@ class BotTelemetryMiddleware(): Example activating MIDDLEWARE in Django settings: MIDDLEWARE = [ - 'botbuilder.applicationinsights.django.BotTelemetryMiddleware', # Ideally add somewhere near top + # Ideally add somewhere near top + 'botbuilder.applicationinsights.django.BotTelemetryMiddleware', ... ] """ @@ -36,13 +37,13 @@ def __call__(self, request): self.process_request(request) return self.get_response(request) - def process_request(self, request): + def process_request(self, request) -> bool: + """Process the incoming Django request.""" # Bot Service doesn't handle anything over 256k - # TODO: Add length check + # TODO: Add length check body_unicode = request.body.decode('utf-8') if request.method == "POST" else None # Sanity check JSON - if body_unicode != None: + if body_unicode is not None: # Integration layer expecting just the json text. - _request_bodies[current_thread().ident] = body_unicode - - + _REQUEST_BODIES[current_thread().ident] = body_unicode + return True diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py new file mode 100644 index 000000000..5b35a6c91 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Flask Application Insights package.""" + +from .flask_telemetry_middleware import BotTelemetryMiddleware, retrieve_flask_body + +__all__ = [ + "BotTelemetryMiddleware", + "retrieve_flask_body" +] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py new file mode 100644 index 000000000..90dbe8803 --- /dev/null +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Flask Telemetry Bot Middleware.""" + +from io import BytesIO +from threading import current_thread + +# Map of thread id => POST body text +_REQUEST_BODIES = {} + +def retrieve_flask_body(): + """ retrieve_flask_body + Retrieve the POST body text from temporary cache. + The POST body corresponds with the thread id and should resides in + cache just for lifetime of request. + + TODO: Add cleanup job to kill orphans + """ + result = _REQUEST_BODIES.pop(current_thread().ident, None) + return result + +class BotTelemetryMiddleware(): + """Bot Telemetry Middleware + Save off the POST body to later populate bot-specific properties to + add to Application Insights. + + Example adding telemetry middleware to Flask: + app = Flask(__name__) + app.wsgi_app = BotTelemetryMiddleware(app.wsgi_app) + """ + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + self.process_request(environ) + return self.app(environ, start_response) + + def process_request(self, environ) -> bool: + """Process the incoming Flask request.""" + # Bot Service doesn't handle anything over 256k + length = int(environ.get('CONTENT_LENGTH', '0')) + if length > 256*1024: + print(f'request too long - rejected') + else: + body_bytes = environ['wsgi.input'].read(length) + environ['wsgi.input'] = BytesIO(body_bytes) + body_unicode = body_bytes.decode('utf-8') + + # Sanity check JSON + if body_unicode is not None: + # Integration layer expecting just the json text. + _REQUEST_BODIES[current_thread().ident] = body_unicode + return True diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py index f52eb9c30..74013537f 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/integration_post_data.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Retrieve the POST request body from underlying web framework.""" import sys -import gc -import imp import json -from botbuilder.schema import Activity from botbuilder.applicationinsights.django import retrieve_bot_body from botbuilder.applicationinsights.flask import retrieve_flask_body class IntegrationPostData: - """ + """ Retrieve the POST body from the underlying framework: - Flask - Django @@ -19,7 +17,7 @@ class IntegrationPostData: This class: - Detects framework (currently flask or django) - Pulls the current request body as a string - + Usage: botdata = BotTelemetryData() body = botdata.activity_json # Get current request body as json object @@ -30,11 +28,13 @@ def __init__(self): @property def activity_json(self) -> json: + """Retrieve the request body as json (Activity).""" body_text = self.get_request_body() - body = json.loads(body_text) if body_text != None else None + body = json.loads(body_text) if body_text is not None else None return body - + def get_request_body(self) -> str: + """Retrieve the request body from flask/django middleware component.""" if self.detect_flask(): return retrieve_flask_body() else: @@ -43,8 +43,9 @@ def get_request_body(self) -> str: return retrieve_bot_body() def detect_flask(self) -> bool: + """Detects if running in flask.""" return "flask" in sys.modules def detect_django(self) -> bool: + """Detects if running in django.""" return "django" in sys.modules - diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py index e6d48cca6..bfc82716c 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py @@ -1,17 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import inspect +from typing import Dict + +from botbuilder.core import (TurnContext, BotAssert, StatePropertyAccessor) from .dialog import Dialog from .dialog_state import DialogState from .dialog_turn_result import DialogTurnResult from .dialog_reason import DialogReason from .dialog_context import DialogContext -from botbuilder.core import ( - TurnContext, - BotAssert, - StatePropertyAccessor - ) -from typing import Dict class DialogSet(): diff --git a/samples/python-flask/21.corebot-app-insights/booking_details.py b/samples/python-flask/21.corebot-app-insights/booking_details.py index 03e066017..21960cb53 100644 --- a/samples/python-flask/21.corebot-app-insights/booking_details.py +++ b/samples/python-flask/21.corebot-app-insights/booking_details.py @@ -1,8 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +"""Booking detail.""" class BookingDetails: + """Booking detail implementation""" def __init__(self, destination: str = None, origin: str = None, travel_date: str = None): self.destination = destination self.origin = origin - self.travel_date = travel_date \ No newline at end of file + self.travel_date = travel_date diff --git a/samples/python-flask/21.corebot-app-insights/bots/__init__.py b/samples/python-flask/21.corebot-app-insights/bots/__init__.py index 194b46c68..b721a9329 100644 --- a/samples/python-flask/21.corebot-app-insights/bots/__init__.py +++ b/samples/python-flask/21.corebot-app-insights/bots/__init__.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""bots module.""" from .dialog_bot import DialogBot from .dialog_and_welcome_bot import DialogAndWelcomeBot __all__ = [ - 'DialogBot', - 'DialogAndWelcomeBot'] \ No newline at end of file + 'DialogBot', + 'DialogAndWelcomeBot'] diff --git a/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py b/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py index 5fb305735..eeb7e42b9 100644 --- a/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py +++ b/samples/python-flask/21.corebot-app-insights/bots/dialog_and_welcome_bot.py @@ -1,46 +1,49 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +"""Main dialog to welcome users.""" import json import os.path from typing import List -from botbuilder.core import CardFactory -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext from botbuilder.dialogs import Dialog +from botbuilder.core import TurnContext, ConversationState, UserState, BotTelemetryClient from botbuilder.schema import Activity, Attachment, ChannelAccount from helpers.activity_helper import create_activity_reply -from botbuilder.core import BotTelemetryClient from .dialog_bot import DialogBot class DialogAndWelcomeBot(DialogBot): - - def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog, telemetry_client: BotTelemetryClient): - super(DialogAndWelcomeBot, self).__init__(conversation_state, user_state, dialog, telemetry_client) + """Main dialog to welcome users.""" + def __init__(self, conversation_state: ConversationState, user_state: UserState, + dialog: Dialog, telemetry_client: BotTelemetryClient): + super(DialogAndWelcomeBot, self).__init__(conversation_state, user_state, + dialog, telemetry_client) self.telemetry_client = telemetry_client - async def on_members_added_activity(self, members_added: List[ChannelAccount], turn_context: TurnContext): + async def on_members_added_activity(self, members_added: List[ChannelAccount], + turn_context: TurnContext): for member in members_added: # Greet anyone that was not the target (recipient) of this message. - # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. + # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards + # for more details. if member.id != turn_context.activity.recipient.id: welcome_card = self.create_adaptive_card_attachment() response = self.create_response(turn_context.activity, welcome_card) await turn_context.send_activity(response) - - # Create an attachment message response. + def create_response(self, activity: Activity, attachment: Attachment): + """Create an attachment message response.""" response = create_activity_reply(activity) response.attachments = [attachment] return response # Load attachment from file. def create_adaptive_card_attachment(self): + """Create an adaptive card.""" relative_path = os.path.abspath(os.path.dirname(__file__)) path = os.path.join(relative_path, "resources/welcomeCard.json") - with open(path) as f: - card = json.load(f) + with open(path) as card_file: + card = json.load(card_file) return Attachment( - content_type= "application/vnd.microsoft.card.adaptive", - content= card) \ No newline at end of file + content_type="application/vnd.microsoft.card.adaptive", + content=card) diff --git a/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py b/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py index 87b49cfa4..522f897f0 100644 --- a/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py +++ b/samples/python-flask/21.corebot-app-insights/bots/dialog_bot.py @@ -1,27 +1,27 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Implements bot Activity handler.""" -import asyncio - -from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext, \ + BotTelemetryClient, NullTelemetryClient from botbuilder.dialogs import Dialog from helpers.dialog_helper import DialogHelper -from botbuilder.core import BotTelemetryClient, NullTelemetryClient class DialogBot(ActivityHandler): - - def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog, telemetry_client: BotTelemetryClient): + """Main activity handler for the bot.""" + def __init__(self, conversation_state: ConversationState, user_state: UserState, + dialog: Dialog, telemetry_client: BotTelemetryClient): if conversation_state is None: raise Exception('[DialogBot]: Missing parameter. conversation_state is required') if user_state is None: raise Exception('[DialogBot]: Missing parameter. user_state is required') if dialog is None: raise Exception('[DialogBot]: Missing parameter. dialog is required') - + self.conversation_state = conversation_state self.user_state = user_state self.dialog = dialog - self.dialogState = self.conversation_state.create_property('DialogState') + self.dialogState = self.conversation_state.create_property('DialogState') # pylint:disable=invalid-name self.telemetry_client = telemetry_client async def on_turn(self, turn_context: TurnContext): @@ -30,9 +30,11 @@ async def on_turn(self, turn_context: TurnContext): # Save any state changes that might have occured during the turn. await self.conversation_state.save_changes(turn_context, False) await self.user_state.save_changes(turn_context, False) - + async def on_message_activity(self, turn_context: TurnContext): - await DialogHelper.run_dialog(self.dialog, turn_context, self.conversation_state.create_property("DialogState")) + # pylint:disable=invalid-name + await DialogHelper.run_dialog(self.dialog, turn_context, + self.conversation_state.create_property("DialogState")) @property def telemetry_client(self) -> BotTelemetryClient: @@ -41,6 +43,7 @@ def telemetry_client(self) -> BotTelemetryClient: """ return self._telemetry_client + # pylint:disable=attribute-defined-outside-init @telemetry_client.setter def telemetry_client(self, value: BotTelemetryClient) -> None: """ diff --git a/samples/python-flask/21.corebot-app-insights/config.py b/samples/python-flask/21.corebot-app-insights/config.py index 7d0a51c5c..d1a623709 100644 --- a/samples/python-flask/21.corebot-app-insights/config.py +++ b/samples/python-flask/21.corebot-app-insights/config.py @@ -1,8 +1,15 @@ +#!/usr/bin/env python +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Configuration for the bot.""" class DefaultConfig(object): + """Configuration for the bot.""" PORT = 3978 APP_ID = "" APP_PASSWORD = "" LUIS_APP_ID = "" + # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key LUIS_API_KEY = "" + # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com" LUIS_API_HOST_NAME = "" - APPINSIGHTS_INSTRUMENTATION_KEY = "" \ No newline at end of file + APPINSIGHTS_INSTRUMENTATION_KEY = "" diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py b/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py index fb59710ca..e8c9730b8 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/__init__.py @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +"""Dialogs module""" from .booking_dialog import BookingDialog from .cancel_and_help_dialog import CancelAndHelpDialog from .date_resolver_dialog import DateResolverDialog from .main_dialog import MainDialog __all__ = [ - 'BookingDialog', - 'CancelAndHelpDialog', - 'DateResolverDialog', - 'MainDialog'] \ No newline at end of file + 'BookingDialog', + 'CancelAndHelpDialog', + 'DateResolverDialog', + 'MainDialog'] diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py index 7dcd0d4f8..f8a5a6ed0 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/booking_dialog.py @@ -1,22 +1,23 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Flight booking dialog.""" from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient +from datatypes_date_time.timex import Timex from .cancel_and_help_dialog import CancelAndHelpDialog from .date_resolver_dialog import DateResolverDialog -from datatypes_date_time.timex import Timex - class BookingDialog(CancelAndHelpDialog): - - def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + """Flight booking implementation.""" + def __init__(self, dialog_id: str = None, + telemetry_client: BotTelemetryClient = NullTelemetryClient()): super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__, telemetry_client) self.telemetry_client = telemetry_client text_prompt = TextPrompt(TextPrompt.__name__) text_prompt.telemetry_client = telemetry_client - + waterfall_dialog = WaterfallDialog(WaterfallDialog.__name__, [ self.destination_step, self.origin_step, @@ -32,75 +33,67 @@ def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = self.add_dialog(waterfall_dialog) self.initial_dialog_id = WaterfallDialog.__name__ - - """ - If a destination city has not been provided, prompt for one. - """ - async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + + async def destination_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for destination.""" booking_details = step_context.options - if (booking_details.destination is None): - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt= MessageFactory.text('To what city would you like to travel?'))) - else: + if booking_details.destination is None: + return await step_context.prompt(TextPrompt.__name__, + PromptOptions(prompt=MessageFactory.text( + 'To what city would you like to travel?'))) # pylint: disable=line-too-long,bad-continuation + else: return await step_context.next(booking_details.destination) - """ - If an origin city has not been provided, prompt for one. - """ - async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for origin city.""" booking_details = step_context.options # Capture the response to the previous step's prompt booking_details.destination = step_context.result - if (booking_details.origin is None): - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt= MessageFactory.text('From what city will you be travelling?'))) - else: + if booking_details.origin is None: + return await step_context.prompt(TextPrompt.__name__, + PromptOptions(prompt=MessageFactory.text('From what city will you be travelling?'))) # pylint: disable=line-too-long,bad-continuation + else: return await step_context.next(booking_details.origin) - """ - If a travel date has not been provided, prompt for one. - This will use the DATE_RESOLVER_DIALOG. - """ - async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + async def travel_date_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for travel date. + This will use the DATE_RESOLVER_DIALOG.""" + booking_details = step_context.options # Capture the results of the previous step booking_details.origin = step_context.result - if (not booking_details.travel_date or self.is_ambiguous(booking_details.travel_date)): - return await step_context.begin_dialog(DateResolverDialog.__name__, booking_details.travel_date) - else: + if (not booking_details.travel_date or self.is_ambiguous(booking_details.travel_date)): + return await step_context.begin_dialog(DateResolverDialog.__name__, booking_details.travel_date) # pylint: disable=line-too-long + else: return await step_context.next(booking_details.travel_date) - """ - Confirm the information the user has provided. - """ - async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + async def confirm_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Confirm the information the user has provided.""" booking_details = step_context.options # Capture the results of the previous step - booking_details.travel_date= step_context.result - msg = f'Please confirm, I have you traveling to: { booking_details.destination } from: { booking_details.origin } on: { booking_details.travel_date}.' + booking_details.travel_date = step_context.result + msg = f'Please confirm, I have you traveling to: { booking_details.destination }'\ + f' from: { booking_details.origin } on: { booking_details.travel_date}.' # Offer a YES/NO prompt. - return await step_context.prompt(ConfirmPrompt.__name__, PromptOptions(prompt= MessageFactory.text(msg))) + return await step_context.prompt(ConfirmPrompt.__name__, + PromptOptions(prompt=MessageFactory.text(msg))) - """ - Complete the interaction and end the dialog. - """ async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - - if step_context.result: + """Complete the interaction and end the dialog.""" + if step_context.result: booking_details = step_context.options - booking_details.travel_date= step_context.result + booking_details.travel_date = step_context.result return await step_context.end_dialog(booking_details) - else: + else: return await step_context.end_dialog() - def is_ambiguous(self, timex: str) -> bool: + def is_ambiguous(self, timex: str) -> bool: + """Ensure time is correct.""" timex_property = Timex(timex) return 'definite' not in timex_property.types - - - - \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py index 8a59a50ea..7596a8ca5 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py @@ -1,23 +1,26 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Handle cancel and help intents.""" + from botbuilder.core import BotTelemetryClient, NullTelemetryClient from botbuilder.dialogs import ComponentDialog, DialogContext, DialogTurnResult, DialogTurnStatus from botbuilder.schema import ActivityTypes class CancelAndHelpDialog(ComponentDialog): - - def __init__(self, dialog_id: str, telemetry_client: BotTelemetryClient = NullTelemetryClient()): + """Implementation of handling cancel and help.""" + def __init__(self, dialog_id: str, + telemetry_client: BotTelemetryClient = NullTelemetryClient()): super(CancelAndHelpDialog, self).__init__(dialog_id) self.telemetry_client = telemetry_client - + async def on_begin_dialog(self, inner_dc: DialogContext, options: object) -> DialogTurnResult: result = await self.interrupt(inner_dc) if result is not None: return result return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options) - + async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: result = await self.interrupt(inner_dc) if result is not None: @@ -26,6 +29,7 @@ async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc) async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: + """Detect interruptions.""" if inner_dc.context.activity.type == ActivityTypes.message: text = inner_dc.context.activity.text.lower() @@ -37,4 +41,4 @@ async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult: await inner_dc.context.send_activity("Cancelling") return await inner_dc.cancel_all_dialogs() - return None \ No newline at end of file + return None diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py index 23b1b4e27..601657c6c 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/date_resolver_dialog.py @@ -1,23 +1,28 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +"""Handle date/time resolution for booking dialog.""" from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext -from botbuilder.dialogs.prompts import DateTimePrompt, PromptValidatorContext, PromptOptions, DateTimeResolution -from .cancel_and_help_dialog import CancelAndHelpDialog +from botbuilder.dialogs.prompts import DateTimePrompt, PromptValidatorContext, \ + PromptOptions, DateTimeResolution from datatypes_date_time.timex import Timex -class DateResolverDialog(CancelAndHelpDialog): +from .cancel_and_help_dialog import CancelAndHelpDialog - def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): - super(DateResolverDialog, self).__init__(dialog_id or DateResolverDialog.__name__, telemetry_client) +class DateResolverDialog(CancelAndHelpDialog): + """Resolve the date""" + def __init__(self, dialog_id: str = None, + telemetry_client: BotTelemetryClient = NullTelemetryClient()): + super(DateResolverDialog, self).__init__(dialog_id or DateResolverDialog.__name__, + telemetry_client) self.telemetry_client = telemetry_client - - date_time_prompt = DateTimePrompt(DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator) + + date_time_prompt = DateTimePrompt(DateTimePrompt.__name__, + DateResolverDialog.datetime_prompt_validator) date_time_prompt.telemetry_client = telemetry_client - + waterfall_dialog = WaterfallDialog(WaterfallDialog.__name__ + '2', [ - self.initialStep, - self.finalStep + self.initial_step, + self.final_step ]) waterfall_dialog.telemetry_client = telemetry_client @@ -25,38 +30,43 @@ def __init__(self, dialog_id: str = None, telemetry_client: BotTelemetryClient = self.add_dialog(waterfall_dialog) self.initial_dialog_id = WaterfallDialog.__name__ + '2' - - async def initialStep(self,step_context: WaterfallStepContext) -> DialogTurnResult: + + async def initial_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + """Prompt for the date.""" timex = step_context.options prompt_msg = 'On what date would you like to travel?' - reprompt_msg = "I'm sorry, for best results, please enter your travel date including the month, day and year." + reprompt_msg = "I'm sorry, for best results, please enter your travel "\ + "date including the month, day and year." if timex is None: # We were not given any date at all so prompt the user. - return await step_context.prompt(DateTimePrompt.__name__ , - PromptOptions( - prompt= MessageFactory.text(prompt_msg), - retry_prompt= MessageFactory.text(reprompt_msg) + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions( # pylint: disable=bad-continuation + prompt=MessageFactory.text(prompt_msg), + retry_prompt=MessageFactory.text(reprompt_msg) )) else: # We have a Date we just need to check it is unambiguous. if 'definite' in Timex(timex).types: # This is essentially a "reprompt" of the data we were given up front. - return await step_context.prompt(DateTimePrompt.__name__, PromptOptions(prompt= reprompt_msg)) + return await step_context.prompt(DateTimePrompt.__name__, + PromptOptions(prompt=reprompt_msg)) else: - return await step_context.next(DateTimeResolution(timex= timex)) + return await step_context.next(DateTimeResolution(timex=timex)) - async def finalStep(self, step_context: WaterfallStepContext): + async def final_step(self, step_context: WaterfallStepContext): + """Cleanup - set final return value and end dialog.""" timex = step_context.result[0].timex return await step_context.end_dialog(timex) - + @staticmethod async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool: + """ Validate the date provided is in proper form. """ if prompt_context.recognized.succeeded: timex = prompt_context.recognized.value[0].timex.split('T')[0] - #TODO: Needs TimexProperty + # TODO: Needs TimexProperty return 'definite' in Timex(timex).types - + return False diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py index a0d99a2bb..d0c91a9f9 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py @@ -1,19 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Main dialog. """ -from datetime import datetime from botbuilder.core import BotTelemetryClient, NullTelemetryClient -from botbuilder.dialogs import ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, WaterfallStepContext, DialogTurnResult -from botbuilder.dialogs.prompts import TextPrompt, ConfirmPrompt, PromptOptions +from botbuilder.dialogs import ComponentDialog, WaterfallDialog, WaterfallStepContext,\ + DialogTurnResult +from botbuilder.dialogs.prompts import TextPrompt, PromptOptions from botbuilder.core import MessageFactory -from .booking_dialog import BookingDialog from booking_details import BookingDetails from helpers.luis_helper import LuisHelper -from datatypes_date_time.timex import Timex +from .booking_dialog import BookingDialog -class MainDialog(ComponentDialog): - def __init__(self, configuration: dict, dialog_id: str = None, telemetry_client: BotTelemetryClient = NullTelemetryClient()): +class MainDialog(ComponentDialog): + """Main dialog. """ + def __init__(self, configuration: dict, dialog_id: str = None, + telemetry_client: BotTelemetryClient = NullTelemetryClient()): super(MainDialog, self).__init__(dialog_id or MainDialog.__name__) self._configuration = configuration @@ -21,10 +23,10 @@ def __init__(self, configuration: dict, dialog_id: str = None, telemetry_client: text_prompt = TextPrompt(TextPrompt.__name__) text_prompt.telemetry_client = self.telemetry_client - - booking_dialog = BookingDialog(telemetry_client = self._telemetry_client) - booking_dialog.telemetry_client = self.telemetry_client - + + booking_dialog = BookingDialog(telemetry_client=self._telemetry_client) + booking_dialog.telemetry_client = self.telemetry_client + wf_dialog = WaterfallDialog('WFDialog', [ self.intro_step, self.act_step, @@ -37,38 +39,57 @@ def __init__(self, configuration: dict, dialog_id: str = None, telemetry_client: self.add_dialog(wf_dialog) self.initial_dialog_id = 'WFDialog' - + async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - if (not self._configuration.get("LUIS_APP_ID", "") or not self._configuration.get("LUIS_API_KEY", "") or not self._configuration.get("LUIS_API_HOST_NAME", "")): + """Initial prompt.""" + if (not self._configuration.get("LUIS_APP_ID", "") or + not self._configuration.get("LUIS_API_KEY", "") or + not self._configuration.get("LUIS_API_HOST_NAME", "")): await step_context.context.send_activity( - MessageFactory.text("NOTE: LUIS is not configured. To enable all capabilities, add 'LUIS_APP_ID', 'LUIS_API_KEY' and 'LUIS_API_HOST_NAME' to the config.py file.")) + MessageFactory.text("NOTE: LUIS is not configured. To enable all"\ + " capabilities, add 'LUIS_APP_ID', 'LUIS_API_KEY' and 'LUIS_API_HOST_NAME'"\ + " to the config.py file.")) return await step_context.next(None) else: - return await step_context.prompt(TextPrompt.__name__, PromptOptions(prompt = MessageFactory.text("What can I help you with today?"))) + return await step_context.prompt(TextPrompt.__name__, + PromptOptions(prompt=MessageFactory.text("What can I help you with today?"))) # pylint: disable=bad-continuation async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) - booking_details = await LuisHelper.excecute_luis_query(self._configuration, step_context.context) if step_context.result is not None else BookingDetails() + """Use language understanding to gather details about booking.""" + # Call LUIS and gather any potential booking details. (Note the TurnContext + # has the response to the prompt.) + print(f'LUIS HEKOPE: {self.telemetry_client._instrumentation_key}') + booking_details = await LuisHelper.execute_luis_query(self._configuration,\ + step_context.context, self.telemetry_client) if step_context.result is not None\ + else BookingDetails() # pylint: disable=bad-continuation - # In this sample we only have a single Intent we are concerned with. However, typically a scenario - # will have multiple different Intents each corresponding to starting a different child Dialog. + # In this sample we only have a single Intent we are concerned with. However, + # typically a scenario will have multiple different Intents each corresponding + # to starting a different child Dialog. - # Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder. + + # Run the BookingDialog giving it whatever details we have from the + # model. The dialog will prompt to find out the remaining details. return await step_context.begin_dialog(BookingDialog.__name__, booking_details) async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: - # If the child dialog ("BookingDialog") was cancelled or the user failed to confirm, the Result here will be null. - if (step_context.result is not None): + """Complete dialog. + At this step, with details from the user, display the completed + flight booking to the user. + """ + # If the child dialog ("BookingDialog") was cancelled or the user failed + # to confirm, the Result here will be null. + if step_context.result is not None: result = step_context.result # Now we have all the booking details call the booking service. - # If the call to the booking service was successful tell the user. #time_property = Timex(result.travel_date) #travel_date_msg = time_property.to_natural_language(datetime.now()) - msg = f'I have you booked to {result.destination} from {result.origin} on {result.travel_date}' + msg = f'I have you booked to {result.destination} from'\ + f' {result.origin} on {result.travel_date}.' await step_context.context.send_activity(MessageFactory.text(msg)) else: await step_context.context.send_activity(MessageFactory.text("Thank you.")) diff --git a/samples/python-flask/21.corebot-app-insights/helpers/__init__.py b/samples/python-flask/21.corebot-app-insights/helpers/__init__.py index a117d0e80..6abcfc9cc 100644 --- a/samples/python-flask/21.corebot-app-insights/helpers/__init__.py +++ b/samples/python-flask/21.corebot-app-insights/helpers/__init__.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Helpers module.""" from . import activity_helper, luis_helper, dialog_helper __all__ = [ - 'activity_helper', - 'dialog_helper', - 'luis_helper'] \ No newline at end of file + 'activity_helper', + 'dialog_helper', + 'luis_helper'] \ No newline at end of file diff --git a/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py index 84b948ac0..144f63650 100644 --- a/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py +++ b/samples/python-flask/21.corebot-app-insights/helpers/activity_helper.py @@ -1,22 +1,26 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +"""Helper to create reply object.""" from datetime import datetime from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, ConversationAccount def create_activity_reply(activity: Activity, text: str = None, locale: str = None): - + """Helper to create reply object.""" return Activity( - type = ActivityTypes.message, - timestamp = datetime.utcnow(), - from_property = ChannelAccount(id= getattr(activity.recipient, 'id', None), name= getattr(activity.recipient, 'name', None)), - recipient = ChannelAccount(id= activity.from_property.id, name= activity.from_property.name), - reply_to_id = activity.id, - service_url = activity.service_url, - channel_id = activity.channel_id, - conversation = ConversationAccount(is_group= activity.conversation.is_group, id= activity.conversation.id, name= activity.conversation.name), - text = text or '', - locale = locale or '', - attachments = [], - entities = [] - ) \ No newline at end of file + type=ActivityTypes.message, + timestamp=datetime.utcnow(), + from_property=ChannelAccount(id=getattr(activity.recipient, 'id', None), + name=getattr(activity.recipient, 'name', None)), + recipient=ChannelAccount(id=activity.from_property.id, name=activity.from_property.name), + reply_to_id=activity.id, + service_url=activity.service_url, + channel_id=activity.channel_id, + conversation=ConversationAccount(is_group=activity.conversation.is_group, + id=activity.conversation.id, + name=activity.conversation.name), + text=text or '', + locale=locale or '', + attachments=[], + entities=[] + ) diff --git a/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py index edda1c318..71ae06907 100644 --- a/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py +++ b/samples/python-flask/21.corebot-app-insights/helpers/dialog_helper.py @@ -1,17 +1,19 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +"""Utility to run dialogs.""" from botbuilder.core import StatePropertyAccessor, TurnContext from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus class DialogHelper: + """Dialog Helper implementation.""" @staticmethod - async def run_dialog(dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor): + async def run_dialog(dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor): # pylint: disable=line-too-long + """Run dialog.""" dialog_set = DialogSet(accessor) dialog_set.add(dialog) dialog_context = await dialog_set.create_context(turn_context) results = await dialog_context.continue_dialog() if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog(dialog.id) \ No newline at end of file + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py b/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py index 0a3195529..20da05792 100644 --- a/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py +++ b/samples/python-flask/21.corebot-app-insights/helpers/luis_helper.py @@ -1,45 +1,51 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botbuilder.ai.luis import LuisRecognizer, LuisApplication -from botbuilder.core import TurnContext + +"""Helper to call LUIS service.""" +from botbuilder.ai.luis import LuisRecognizer, LuisApplication, LuisPredictionOptions +from botbuilder.core import TurnContext, BotTelemetryClient, NullTelemetryClient from booking_details import BookingDetails +# pylint: disable=line-too-long class LuisHelper: - + """LUIS helper implementation.""" @staticmethod - async def excecute_luis_query(configuration: dict, turn_context: TurnContext) -> BookingDetails: + async def execute_luis_query(configuration, turn_context: TurnContext, + telemetry_client: BotTelemetryClient = None) -> BookingDetails: + """Invoke LUIS service to perform prediction/evaluation of utterance.""" booking_details = BookingDetails() + # pylint:disable=broad-except try: luis_application = LuisApplication( - configuration['LUIS_APP_ID'], - configuration['LUIS_API_KEY'], - configuration['LUIS_API_HOST_NAME'] + configuration.get('LUIS_APP_ID'), + configuration.get('LUIS_API_KEY'), + configuration.get('LUIS_API_HOST_NAME') ) - - recognizer = LuisRecognizer(luis_application) + options = LuisPredictionOptions() + options.telemetry_client = telemetry_client if telemetry_client is not None else NullTelemetryClient() + recognizer = LuisRecognizer(luis_application, prediction_options=options) recognizer_result = await recognizer.recognize(turn_context) + print(f'Recognize Result: {recognizer_result}') if recognizer_result.intents: intent = sorted(recognizer_result.intents, key=recognizer_result.intents.get, reverse=True)[:1][0] if intent == 'Book_flight': # We need to get the result from the LUIS JSON which at every level returns an array. to_entities = recognizer_result.entities.get("$instance", {}).get("To", []) - if len(to_entities) > 0: + if to_entities: booking_details.destination = to_entities[0]['text'] from_entities = recognizer_result.entities.get("$instance", {}).get("From", []) - if len(from_entities) > 0: + if from_entities: booking_details.origin = from_entities[0]['text'] - # TODO: This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. + # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part. # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year. date_entities = recognizer_result.entities.get("$instance", {}).get("datetime", []) - if len(date_entities) > 0: - text = date_entities[0]['text'] - booking_details.travel_date = None # TODO: Set when we get a timex format - except Exception as e: - print(e) - - return booking_details + if date_entities: + booking_details.travel_date = None # Set when we get a timex format + except Exception as exception: + print(exception) + return booking_details diff --git a/samples/python-flask/21.corebot-app-insights/main.py b/samples/python-flask/21.corebot-app-insights/main.py index 93ea28afc..9c27431c1 100644 --- a/samples/python-flask/21.corebot-app-insights/main.py +++ b/samples/python-flask/21.corebot-app-insights/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3ex +#!/usr/bin/env python # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. @@ -8,58 +8,53 @@ - Implement a multi-turn conversation using Dialogs. - Handle user interruptions for such things as `Help` or `Cancel`. - Prompt for and validate requests for information from the user. -gi + """ -from functools import wraps -import json + import asyncio -import sys -from flask import Flask, jsonify, request, Response -from botbuilder.schema import (Activity, ActivityTypes) -from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext, - ConversationState, MemoryStorage, UserState) +from flask import Flask, request, Response +from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, + ConversationState, MemoryStorage, UserState, TurnContext) +from botbuilder.schema import (Activity) +from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient +from botbuilder.applicationinsights.flask import BotTelemetryMiddleware from dialogs import MainDialog from bots import DialogAndWelcomeBot -from helpers.dialog_helper import DialogHelper -from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient -from botbuilder.applicationinsights.flask import BotTelemetryMiddleware -loop = asyncio.get_event_loop() -app = Flask(__name__, instance_relative_config=True) -app.config.from_object('config.DefaultConfig') -app.wsgi_app = BotTelemetryMiddleware(app.wsgi_app) +LOOP = asyncio.get_event_loop() +APP = Flask(__name__, instance_relative_config=True) +APP.config.from_object('config.DefaultConfig') +APP.wsgi_app = BotTelemetryMiddleware(APP.wsgi_app) -SETTINGS = BotFrameworkAdapterSettings(app.config['APP_ID'], app.config['APP_PASSWORD']) +SETTINGS = BotFrameworkAdapterSettings(APP.config['APP_ID'], APP.config['APP_PASSWORD']) ADAPTER = BotFrameworkAdapter(SETTINGS) -# Catch-all for errors. +# pylint:disable=unused-argument async def on_error(context: TurnContext, error: Exception): - # This check writes out errors to console log - # NOTE: In production environment, you should consider logging this to Azure - # application insights. - print(f'\n [on_turn_error]: { error }', file=sys.stderr) + """ Catch-all for errors.""" # Send a message to the user await context.send_activity('Oops. Something went wrong!') # Clear out state - await conversation_state.delete(context) + await CONVERSATION_STATE.delete(context) ADAPTER.on_turn_error = on_error # Create MemoryStorage, UserState and ConversationState -memory = MemoryStorage() +MEMORY = MemoryStorage() -user_state = UserState(memory) -conversation_state = ConversationState(memory) -instrumentation_key=app.config['APPINSIGHTS_INSTRUMENTATION_KEY'] -telemetry_client = ApplicationInsightsTelemetryClient(instrumentation_key) -dialog = MainDialog(app.config, telemetry_client = telemetry_client) -bot = DialogAndWelcomeBot(conversation_state, user_state, dialog, telemetry_client) +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) +INSTRUMENTATION_KEY = APP.config['APPINSIGHTS_INSTRUMENTATION_KEY'] +TELEMETRY_CLIENT = ApplicationInsightsTelemetryClient(INSTRUMENTATION_KEY) +DIALOG = MainDialog(APP.config, telemetry_client=TELEMETRY_CLIENT) +BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG, TELEMETRY_CLIENT) -@app.route('/api/messages', methods = ['POST']) +@APP.route('/api/messages', methods=['POST']) def messages(): + """Main bot message handler.""" if request.headers['Content-Type'] == 'application/json': body = request.json else: @@ -67,19 +62,22 @@ def messages(): activity = Activity().deserialize(body) auth_header = request.headers['Authorization'] if 'Authorization' in request.headers else '' - + async def aux_func(turn_context): - asyncio.ensure_future(bot.on_turn(turn_context)) + await BOT.on_turn(turn_context) + try: - task = loop.create_task(ADAPTER.process_activity(activity, auth_header, aux_func)) - loop.run_until_complete(task) + future = asyncio.ensure_future(ADAPTER.process_activity(activity, auth_header, aux_func), + loop=LOOP) + LOOP.run_until_complete(future) return Response(status=201) - except Exception as e: - raise e + except Exception as exception: + raise exception + -if __name__ == "__main__" : +if __name__ == "__main__": try: - app.run(debug=True, port=app.config["PORT"]) - except Exception as e: - raise e + APP.run(debug=True, port=APP.config["PORT"]) + except Exception as exception: + raise exception From 26602e0238fea758407f32c99bcc3d2740d032ba Mon Sep 17 00:00:00 2001 From: Dave Taniguchi Date: Tue, 25 Jun 2019 14:39:20 -0700 Subject: [PATCH 3/3] Address CR feedback --- .../botbuilder/applicationinsights/django/__init__.py | 2 +- .../applicationinsights/django/bot_telemetry_middleware.py | 2 -- .../applicationinsights/flask/flask_telemetry_middleware.py | 2 -- .../python-flask/21.corebot-app-insights/dialogs/main_dialog.py | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py index dd39a0808..df9279f17 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -"""Djanjo Application Insights package.""" +"""Django Application Insights package.""" from .bot_telemetry_middleware import BotTelemetryMiddleware, retrieve_bot_body diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py index c117b59ea..ff06549a3 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py @@ -12,8 +12,6 @@ def retrieve_bot_body(): Retrieve the POST body text from temporary cache. The POST body corresponds with the thread id and should resides in cache just for lifetime of request. - - TODO: Add cleanup job to kill orphans """ result = _REQUEST_BODIES.pop(current_thread().ident, None) return result diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py index 90dbe8803..e4cbb84d6 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py @@ -13,8 +13,6 @@ def retrieve_flask_body(): Retrieve the POST body text from temporary cache. The POST body corresponds with the thread id and should resides in cache just for lifetime of request. - - TODO: Add cleanup job to kill orphans """ result = _REQUEST_BODIES.pop(current_thread().ident, None) return result diff --git a/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py index d0c91a9f9..617fb8cb9 100644 --- a/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py +++ b/samples/python-flask/21.corebot-app-insights/dialogs/main_dialog.py @@ -60,7 +60,6 @@ async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult """Use language understanding to gather details about booking.""" # Call LUIS and gather any potential booking details. (Note the TurnContext # has the response to the prompt.) - print(f'LUIS HEKOPE: {self.telemetry_client._instrumentation_key}') booking_details = await LuisHelper.execute_luis_query(self._configuration,\ step_context.context, self.telemetry_client) if step_context.result is not None\ else BookingDetails() # pylint: disable=bad-continuation