<filename>lambda-functions/lex_response_handler.py
import json
import logging
import os
import re
import time
from datetime import datetime, date

import boto3 as boto3
import dateutil.parser
import phonenumbers

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

QUEUE_NAME = "SQSQueue"
SQS = boto3.client("sqs")


def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):
    return {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'ElicitSlot',
            'intentName': intent_name,
            'slots': slots,
            'slotToElicit': slot_to_elicit,
            'message': message
        }
    }


def safe_int(n):
    """
    Safely convert n value to int.
    """
    if n is not None:
        return int(n)
    return n


def try_ex(func):
    """
    Call passed in function in try block. If KeyError is encountered return None.
    This function is intended to be used to safely access dictionary.

    Note that this function would have negative impact on performance.
    """

    try:
        return func()
    except KeyError:
        return None


def close(session_attributes, fulfillment_state, message):
    response = {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'Close',
            'fulfillmentState': fulfillment_state,
            'message': message
        }
    }

    return response


def delegate(session_attributes, slots):
    return {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'Delegate',
            'slots': slots
        }
    }


def isvalid_date(date):
    try:
        dateutil.parser.parse(date)
        return True
    except ValueError:
        return False


def isvalid_time(input_time):
    return len(input_time) == 5 and len(input_time.split(":")) == 2 and len(
        input_time.split(":")[0]) == 2


def isvalid_city(city):
    valid_cities = ['new york', 'los angeles', 'chicago', 'houston', 'philadelphia',
                    'phoenix', '<NAME>',
                    '<NAME>', 'dallas', '<NAME>', 'austin', 'jacksonville',
                    '<NAME>ancisco', 'indianapolis',
                    'columbus', 'fort worth', 'charlotte', 'detroit', 'el paso',
                    'seattle', 'denver', 'washington dc',
                    'memphis', 'boston', 'nashville', 'baltimore', 'portland']
    return city.lower() in valid_cities


def isvalid_cuisine(cuisine):
    return True


def build_validation_result(isvalid, violated_slot, message_content):
    return {
        'isValid': isvalid,
        'violatedSlot': violated_slot,
        'message': {'contentType': 'PlainText', 'content': message_content}
    }


def get_day_difference(later_date, earlier_date):
    later_datetime = dateutil.parser.parse(later_date).date()
    earlier_datetime = dateutil.parser.parse(earlier_date).date()
    return abs(later_datetime - earlier_datetime).days


def isvalid_people(people):
    people = safe_int(people)
    if not people or people < 1:
        return False
    return True


def clean_phone(phone):
    phone = re.sub(r'[^0-9]', "", phone)
    if phone[0] == '1':
        phone = phone[1:]
    return phone


def isvalid_phone(phone):
    if len(phone) > 11:
        return False
    if len(phone) == 11 and phone[0] != '1':
        return False
    if len(phone) != 10:
        return False
    if not phonenumbers.PhoneNumberMatcher(phone, "US"):
        return False
    return True


def format_phone(phone):
    us_prefix = "+1"
    return us_prefix + phone


def validate_info(slots):
    location = try_ex(lambda: slots['Location'])
    input_date = try_ex(lambda: slots['Date'])
    reserve_time = try_ex(lambda: slots['Time'])
    cuisine = try_ex(lambda: slots['Cuisine'])
    people = safe_int(try_ex(lambda: slots['People']))
    phone = try_ex(lambda: slots['Phone'])

    if location and not isvalid_city(location):
        return build_validation_result(
            False,
            'Location',
            'We currently do not support {} as a valid destination.  Can you try a different city?'.format(
                location)
        )

    if input_date:
        if not isvalid_date(input_date):
            return build_validation_result(False, 'Date',
                                           'I did not understand what date you want to reserve.  When would you like '
                                           'to reserve a table?')
        if datetime.strptime(input_date, '%Y-%m-%d').date() < date.today():
            return build_validation_result(False, 'Date',
                                           'Reservations must be scheduled for future only.  Can you try a different '
                                           'date?')
    if reserve_time:
        if not isvalid_time(reserve_time):
            return build_validation_result(False, 'Time',
                                           'I did not understand what time you\'re looking for.  When would you like '
                                           'to reserve a table?')
        hour = int(reserve_time.split(":")[0])
        minute = int(reserve_time.split(":")[1])
        input_date = datetime.strptime(input_date, "%Y-%m-%d").replace(hour=hour,
                                                                       minute=minute)
        if input_date < datetime.now():
            return build_validation_result(False, 'Time',
                                           'Reservations must be scheduled for future only.  Can you try a different '
                                           'time?')
    if cuisine:
        if not isvalid_cuisine(cuisine):
            return build_validation_result(False, 'Cuisine',
                                           'I did not understand the cuisine. Can you try a different cuisine?')

    if people:
        if not isvalid_people(people):
            return build_validation_result(False, 'People',
                                           'The number of people should be greater than or equal to one. Please enter '
                                           'a valid number of people')

    if phone:
        phone = clean_phone(phone)
        if not isvalid_phone(phone):
            return build_validation_result(False, 'Phone',
                                           'The phone number you\'ve entered is invalid. Please enter a valid United '
                                           'states number only')

    return {'isValid': True}


def format_and_send_to_sqs(location, input_date, reserve_time, cuisine, people, phone):
    phone = clean_phone(phone)
    phone = format_phone(phone)
    data = {'location': location, 'date': input_date, 'time': reserve_time,
            'cuisine': cuisine, 'people': people,
            'phone': phone}
    json_data = json.dumps(data)
    send_to_sqs(json_data)


def suggest_restaurants(intent_request):
    # Validate any slots which have been specified.  If any are invalid, re-elicit for their value
    slots = intent_request['currentIntent']['slots']
    validation_result = validate_info(slots)
    if not validation_result['isValid']:
        slots = intent_request['currentIntent']['slots']
        slots[validation_result['violatedSlot']] = None

        return elicit_slot(
            {},
            intent_request['currentIntent']['name'],
            slots,
            validation_result['violatedSlot'],
            validation_result['message']
        )
    location = try_ex(lambda: slots['Location'])
    input_date = try_ex(lambda: slots['Date'])
    reserve_time = try_ex(lambda: slots['Time'])
    cuisine = try_ex(lambda: slots['Cuisine'])
    people = safe_int(try_ex(lambda: slots['People']))
    phone = try_ex(lambda: slots['Phone'])
    if location and input_date and reserve_time and cuisine and people and phone:
        format_and_send_to_sqs(location, input_date, reserve_time, cuisine, people,
                               phone)
        return close(
            {},
            'Fulfilled',
            {
                'contentType': 'PlainText',
                'content': 'Ok, I will send you a text containing all information about restaurants based on your '
                           'preferences. '
            }
        )
    else:
        return delegate({}, slots)


def greet(intent_request):
    return close(
        {},
        'Fulfilled',
        {
            'contentType': 'PlainText',
            'content': 'Hi. How can I help you?'
        }
    )


def thank(intent_request):
    return close(
        {},
        'Fulfilled',
        {
            'contentType': 'PlainText',
            'content': 'You\'re welcome'
        }
    )


def dispatch(intent_request):
    """
    Called when the user specifies an intent for this bot.
    """

    logger.debug(
        'dispatch userId={}, intentName={}'.format(intent_request['userId'],
                                                   intent_request['currentIntent'][
                                                       'name']))

    intent_name = intent_request['currentIntent']['name']

    # Dispatch to your bot's intent handlers
    if intent_name == 'DiningSuggestionsIntent':
        return suggest_restaurants(intent_request)
    elif intent_name == 'GreetingIntent':
        return greet(intent_request)
    elif intent_name == 'ThankYouIntent':
        return thank(intent_request)
    raise Exception('Intent with name ' + intent_name + ' not supported')


def lambda_handler(event, context):
    """
    Route the incoming request based on intent.
    The JSON body of the request is provided in the event slot.
    """
    # By default, treat the user request as coming from the America/New_York time zone.
    os.environ['TZ'] = 'America/New_York'
    time.tzset()
    logger.debug('Event received:{}'.format(event))
    # logger.debug('event.bot.name={}'.format(event['bot']['name']))

    return dispatch(event)


def get_queue_url():
    """Retrieve the URL for the configured queue name"""
    q = SQS.get_queue_url(QueueName=QUEUE_NAME).get('QueueUrl')
    logger.debug("Queue URL is %s", q)
    return q


def send_to_sqs(data):
    """The lambda handler"""
    logger.debug("Sending data to SQS %s", data)
    try:
        url = get_queue_url()
        logger.debug("Got queue URL %s", url)
        resp = SQS.send_message(QueueUrl=url, MessageBody=data)
        logger.debug("Sending data to SQS %s", data)
        logger.debug("Send result: %s", resp)
    except Exception as e:
        raise Exception("Could not record link! %s" % e)

<filename>lambda-functions/lex_response_handler.py
import json
import logging
import os
import re
import time
from datetime import datetime, date

import boto3 as boto3
import dateutil.parser
import phonenumbers

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

QUEUE_NAME = "SQSQueue"
SQS = boto3.client("sqs")


def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):
    return {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'ElicitSlot',
            'intentName': intent_name,
            'slots': slots,
            'slotToElicit': slot_to_elicit,
            'message': message
        }
    }


def safe_int(n):
    """
    Safely convert n value to int.
    """
    if n is not None:
        return int(n)
    return n


def try_ex(func):
    """
    Call passed in function in try block. If KeyError is encountered return None.
    This function is intended to be used to safely access dictionary.

    Note that this function would have negative impact on performance.
    """

    try:
        return func()
    except KeyError:
        return None


def close(session_attributes, fulfillment_state, message):
    response = {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'Close',
            'fulfillmentState': fulfillment_state,
            'message': message
        }
    }

    return response


def delegate(session_attributes, slots):
    return {
        'sessionAttributes': session_attributes,
        'dialogAction': {
            'type': 'Delegate',
            'slots': slots
        }
    }


def isvalid_date(date):
    try:
        dateutil.parser.parse(date)
        return True
    except ValueError:
        return False


def isvalid_time(input_time):
    return len(input_time) == 5 and len(input_time.split(":")) == 2 and len(
        input_time.split(":")[0]) == 2


def isvalid_city(city):
    valid_cities = ['new york', 'los angeles', 'chicago', 'houston', 'philadelphia',
                    'phoenix', '<NAME>',
                    '<NAME>', 'dallas', '<NAME>', 'austin', 'jacksonville',
                    '<NAME>ancisco', 'indianapolis',
                    'columbus', 'fort worth', 'charlotte', 'detroit', 'el paso',
                    'seattle', 'denver', 'washington dc',
                    'memphis', 'boston', 'nashville', 'baltimore', 'portland']
    return city.lower() in valid_cities


def isvalid_cuisine(cuisine):
    return True


def build_validation_result(isvalid, violated_slot, message_content):
    return {
        'isValid': isvalid,
        'violatedSlot': violated_slot,
        'message': {'contentType': 'PlainText', 'content': message_content}
    }


def get_day_difference(later_date, earlier_date):
    later_datetime = dateutil.parser.parse(later_date).date()
    earlier_datetime = dateutil.parser.parse(earlier_date).date()
    return abs(later_datetime - earlier_datetime).days


def isvalid_people(people):
    people = safe_int(people)
    if not people or people < 1:
        return False
    return True


def clean_phone(phone):
    phone = re.sub(r'[^0-9]', "", phone)
    if phone[0] == '1':
        phone = phone[1:]
    return phone


def isvalid_phone(phone):
    if len(phone) > 11:
        return False
    if len(phone) == 11 and phone[0] != '1':
        return False
    if len(phone) != 10:
        return False
    if not phonenumbers.PhoneNumberMatcher(phone, "US"):
        return False
    return True


def format_phone(phone):
    us_prefix = "+1"
    return us_prefix + phone


def validate_info(slots):
    location = try_ex(lambda: slots['Location'])
    input_date = try_ex(lambda: slots['Date'])
    reserve_time = try_ex(lambda: slots['Time'])
    cuisine = try_ex(lambda: slots['Cuisine'])
    people = safe_int(try_ex(lambda: slots['People']))
    phone = try_ex(lambda: slots['Phone'])

    if location and not isvalid_city(location):
        return build_validation_result(
            False,
            'Location',
            'We currently do not support {} as a valid destination.  Can you try a different city?'.format(
                location)
        )

    if input_date:
        if not isvalid_date(input_date):
            return build_validation_result(False, 'Date',
                                           'I did not understand what date you want to reserve.  When would you like '
                                           'to reserve a table?')
        if datetime.strptime(input_date, '%Y-%m-%d').date() < date.today():
            return build_validation_result(False, 'Date',
                                           'Reservations must be scheduled for future only.  Can you try a different '
                                           'date?')
    if reserve_time:
        if not isvalid_time(reserve_time):
            return build_validation_result(False, 'Time',
                                           'I did not understand what time you\'re looking for.  When would you like '
                                           'to reserve a table?')
        hour = int(reserve_time.split(":")[0])
        minute = int(reserve_time.split(":")[1])
        input_date = datetime.strptime(input_date, "%Y-%m-%d").replace(hour=hour,
                                                                       minute=minute)
        if input_date < datetime.now():
            return build_validation_result(False, 'Time',
                                           'Reservations must be scheduled for future only.  Can you try a different '
                                           'time?')
    if cuisine:
        if not isvalid_cuisine(cuisine):
            return build_validation_result(False, 'Cuisine',
                                           'I did not understand the cuisine. Can you try a different cuisine?')

    if people:
        if not isvalid_people(people):
            return build_validation_result(False, 'People',
                                           'The number of people should be greater than or equal to one. Please enter '
                                           'a valid number of people')

    if phone:
        phone = clean_phone(phone)
        if not isvalid_phone(phone):
            return build_validation_result(False, 'Phone',
                                           'The phone number you\'ve entered is invalid. Please enter a valid United '
                                           'states number only')

    return {'isValid': True}


def format_and_send_to_sqs(location, input_date, reserve_time, cuisine, people, phone):
    phone = clean_phone(phone)
    phone = format_phone(phone)
    data = {'location': location, 'date': input_date, 'time': reserve_time,
            'cuisine': cuisine, 'people': people,
            'phone': phone}
    json_data = json.dumps(data)
    send_to_sqs(json_data)


def suggest_restaurants(intent_request):
    # Validate any slots which have been specified.  If any are invalid, re-elicit for their value
    slots = intent_request['currentIntent']['slots']
    validation_result = validate_info(slots)
    if not validation_result['isValid']:
        slots = intent_request['currentIntent']['slots']
        slots[validation_result['violatedSlot']] = None

        return elicit_slot(
            {},
            intent_request['currentIntent']['name'],
            slots,
            validation_result['violatedSlot'],
            validation_result['message']
        )
    location = try_ex(lambda: slots['Location'])
    input_date = try_ex(lambda: slots['Date'])
    reserve_time = try_ex(lambda: slots['Time'])
    cuisine = try_ex(lambda: slots['Cuisine'])
    people = safe_int(try_ex(lambda: slots['People']))
    phone = try_ex(lambda: slots['Phone'])
    if location and input_date and reserve_time and cuisine and people and phone:
        format_and_send_to_sqs(location, input_date, reserve_time, cuisine, people,
                               phone)
        return close(
            {},
            'Fulfilled',
            {
                'contentType': 'PlainText',
                'content': 'Ok, I will send you a text containing all information about restaurants based on your '
                           'preferences. '
            }
        )
    else:
        return delegate({}, slots)


def greet(intent_request):
    return close(
        {},
        'Fulfilled',
        {
            'contentType': 'PlainText',
            'content': 'Hi. How can I help you?'
        }
    )


def thank(intent_request):
    return close(
        {},
        'Fulfilled',
        {
            'contentType': 'PlainText',
            'content': 'You\'re welcome'
        }
    )


def dispatch(intent_request):
    """
    Called when the user specifies an intent for this bot.
    """

    logger.debug(
        'dispatch userId={}, intentName={}'.format(intent_request['userId'],
                                                   intent_request['currentIntent'][
                                                       'name']))

    intent_name = intent_request['currentIntent']['name']

    # Dispatch to your bot's intent handlers
    if intent_name == 'DiningSuggestionsIntent':
        return suggest_restaurants(intent_request)
    elif intent_name == 'GreetingIntent':
        return greet(intent_request)
    elif intent_name == 'ThankYouIntent':
        return thank(intent_request)
    raise Exception('Intent with name ' + intent_name + ' not supported')


def lambda_handler(event, context):
    """
    Route the incoming request based on intent.
    The JSON body of the request is provided in the event slot.
    """
    # By default, treat the user request as coming from the America/New_York time zone.
    os.environ['TZ'] = 'America/New_York'
    time.tzset()
    logger.debug('Event received:{}'.format(event))
    # logger.debug('event.bot.name={}'.format(event['bot']['name']))

    return dispatch(event)


def get_queue_url():
    """Retrieve the URL for the configured queue name"""
    q = SQS.get_queue_url(QueueName=QUEUE_NAME).get('QueueUrl')
    logger.debug("Queue URL is %s", q)
    return q


def send_to_sqs(data):
    """The lambda handler"""
    logger.debug("Sending data to SQS %s", data)
    try:
        url = get_queue_url()
        logger.debug("Got queue URL %s", url)
        resp = SQS.send_message(QueueUrl=url, MessageBody=data)
        logger.debug("Sending data to SQS %s", data)
        logger.debug("Send result: %s", resp)
    except Exception as e:
        raise Exception("Could not record link! %s" % e)

Safely convert n value to int. Call passed in function in try block. If KeyError is encountered return None.     This function is intended to be used to safely access dictionary.      Note that this function would have negative impact on performance. # Validate any slots which have been specified.  If any are invalid, re-elicit for their value Called when the user specifies an intent for this bot. # Dispatch to your bot's intent handlers Route the incoming request based on intent.     The JSON body of the request is provided in the event slot. # By default, treat the user request as coming from the America/New_York time zone. # logger.debug('event.bot.name={}'.format(event['bot']['name'])) Retrieve the URL for the configured queue name The lambda handler