1

I have an app with the following flow:

  1. Placed a phone call or use the chat UI to invoke a Lex bot via Get Customer Input block in Amazon Connect
  2. The Lex bot asks for some information from the customer such as Name, company you're calling from, who referred you, and the product you're calling aobut.
  3. The Lex bot is based on the order flowers sample and invokes a Lambda function with the salesforce API imported as a layer. The purpose of the SalesForce layer is to check if the caller is an existing contact in SalesForce and to check if the company they are calling from exists or not. If neither is true, then the Lambda function creates new records in SalesForce.
  4. Once the bot understands what the customer is calling about it then places an outbound call to the customer using the phone number in their contact record in SalesForce, unless they don't have one, at which point it asks for their phone number and then places an outbound call to the customer and connects to an agent.

Whenever I use the Chat UI for this flow it works perfectly every time with no issues. However, ever few phone call I place I get something very weird happening. I'm able to get the system to recognize and repeat my name, the company I'm calling from, and the company who referred me every time. But when it breaks, it breaks at the spot where it asks me for the product I'm calling about. I tell it "I'm calling about air filters" and the system says "Hi Hersal Thomas, can you tell me which company you're contacting us from?" and I speak "Facebook" and it understands me and goes to the same point to where it asks me which product I'm inquiring about and when I speak "air filters" it loops back to the previous step where it says "Hi Hersal Thomas, can you tell me which company you're contacting us from?" It does this three times and then eventually connects my call to an agent.

Could there be a weird loop in the Lambda function that is causing this behavior?

Here is the Python 3.6 function:

"""
This sample demonstrates an implementation of the Lex Code Hook Interface
in order to serve a sample bot which manages orders for flowers.
Bot, Intent, and Slot models which are compatible with this sample can be found in the Lex Console
as part of the 'OrderFlowers' template.

For instructions on how to set up and test this bot, as well as additional samples,
visit the Lex Getting Started documentation http://docs.aws.amazon.com/lex/latest/dg/getting-started.html.
"""
import math
import dateutil.parser
import datetime
import calendar
import time
import os
import logging
import json
from salesforce_api import Salesforce

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


""" --- Helpers to build responses which match the structure of the necessary dialog actions --- """


def get_slots(intent_request):
    return intent_request['currentIntent']['slots']


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': {
                "contentType": "PlainText",
                "content": message
            }
        }
    }


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
        }
    }


""" --- Helper Functions --- """


def parse_int(n):
    try:
        return int(n)
    except ValueError:
        return float('nan')


def build_validation_result(is_valid, violated_slot, message_content):
    if message_content is None:
        return {
            "isValid": is_valid,
            "violatedSlot": violated_slot,
        }

    return {
        'isValid': is_valid,
        'violatedSlot': violated_slot,
        'message': {'contentType': 'PlainText', 'content': message_content}
    }


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


def validate_user_informations(full_name, company, refered_from, product):
    flower_types = ['lilies', 'roses', 'tulips']
    if flower_type is not None and flower_type.lower() not in flower_types:
        return build_validation_result(False,
                                       'FlowerType',
                                       'We do not have {}, would you like a different type of flower?  '
                                       'Our most popular flowers are roses'.format(flower_type))

    if date is not None:
        if not isvalid_date(date):
            return build_validation_result(False, 'PickupDate', 'I did not understand that, what date would you like to pick the flowers up?')
        elif datetime.datetime.strptime(date, '%Y-%m-%d').date() <= datetime.date.today():
            return build_validation_result(False, 'PickupDate', 'You can pick up the flowers from tomorrow onwards.  What day would you like to pick them up?')

    if pickup_time is not None:
        if len(pickup_time) != 5:
            # Not a valid time; use a prompt defined on the build-time model.
            return build_validation_result(False, 'PickupTime', None)

        hour, minute = pickup_time.split(':')
        hour = parse_int(hour)
        minute = parse_int(minute)
        if math.isnan(hour) or math.isnan(minute):
            # Not a valid time; use a prompt defined on the build-time model.
            return build_validation_result(False, 'PickupTime', None)

        if hour < 10 or hour > 16:
            # Outside of business hours
            return build_validation_result(False, 'PickupTime', 'Our business hours are from ten a m. to five p m. Can you specify a time during this range?')

    return build_validation_result(True, None, None)

def add_months(sourcedate, months):
    month = sourcedate.month - 1 + months
    year = sourcedate.year + month // 12
    month = month % 12 + 1
    day = min(sourcedate.day, calendar.monthrange(year,month)[1])
    return datetime.date(year, month, day)

def convertNumberToE164(number):
    number = number.replace(' ', '')
    number = number.replace('+', '')
    number = number.replace('(', '')
    number = number.replace(')', '')
    number = number.replace('-', '')
    if len(number) == 10:
        number = '+1{}'.format(number)
    else:
        number = '+{}'.format(number)
    return number

""" --- Functions that control the bot's behavior --- """


def get_customer_information(intent_request):
    """
    Performs dialog management and fulfillment for ordering flowers.
    Beyond fulfillment, the implementation of this intent demonstrates the use of the elicitSlot dialog action
    in slot validation and re-prompting.
    """
    logger.debug("event: ==================")
    logger.debug(json.dumps(intent_request))

    full_name = get_slots(intent_request)["full_name"]
    company = get_slots(intent_request)["company"]
    refered_from = get_slots(intent_request)["refered_from"]
    product = get_slots(intent_request)["product"]
    phone_number = get_slots(intent_request)["phone_number"]
    agree_phone_call = get_slots(intent_request)["agree_phone_call"]

    source = intent_request['invocationSource']

    if source == 'DialogCodeHook':
        # Perform basic validation on the supplied input slots.
        # Use the elicitSlot dialog action to re-prompt for the first violation detected.
        slots = get_slots(intent_request)

        output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
        # validation_result = validate_user_informations(full_name, company, refered_from, product)
        # if not validation_result['isValid']:
        #     slots[validation_result['violatedSlot']] = None
        #     return elicit_slot(intent_request['sessionAttributes'],
        #                       intent_request['currentIntent']['name'],
        #                       slots,
        #                       validation_result['violatedSlot'],
        #                       validation_result['message'])
        if full_name is not None and company is not None and refered_from is not None and product is not None:
            if intent_request['currentIntent']['name'] == 'VoiceOmniIntent' and ('connect_channel' in output_session_attributes and output_session_attributes['connect_channel'] is not None and output_session_attributes['connect_channel'] == "VOICE"):
                #if incoming from Voice
                client = Salesforce(
                        username=os.environ['sales_username'],
                        password=os.environ['sales_password'],
                        security_token=os.environ['sales_token']
                    )
                accounts = client.sobjects.query("SELECT Id, name, phone FROM Account where name = '{}'".format(company))
                account_id = -1
                if len(accounts) > 0:
                    account_id = accounts[0]['Id']
                else:
                    acc_info = client.sobjects.Account.insert({'name': company})
                    account_id = acc_info['id']

                full_name = full_name.title()
                contacts = client.sobjects.query("SELECT Id, name, phone, accountid from Contact where name='{}' and accountid='{}'".format(full_name, account_id))
                if len(contacts) > 0:
                    client.sobjects.Contact.update(contacts[0]['Id'], {'phone': convertNumberToE164(output_session_attributes['phone_number'])})
                    output_session_attributes['contact_id'] = contacts[0]['Id']
                else:
                    name_arr = full_name.split(' ')
                    first_name = name_arr[0] if len(name_arr) > 1 else ''
                    last_name = name_arr[0] if len(name_arr) == 1 else name_arr[1]
                    contact_res = client.sobjects.Contact.insert({'firstname': first_name, 'lastname': last_name, 'phone': convertNumberToE164(output_session_attributes['phone_number']), 'accountid': account_id})
                    output_session_attributes['contact_id'] = contact_res['id']

                today_date = datetime.date.today()
                close_date = add_months(today_date, 1)
                close_date = close_date.strftime('%Y-%m-%d')
                opp_info = client.sobjects.Opportunity.insert({'name': product, 'closedate': close_date, 'stagename': 'Qualification', 'accountid': account_id})
                return delegate(output_session_attributes, get_slots(intent_request))
            else:
                #if incoming from Chat
                if phone_number is not None:
                    #once customer fills his phone_number
                    client = Salesforce(
                        username=os.environ['sales_username'],
                        password=os.environ['sales_password'],
                        security_token=os.environ['sales_token']
                    )
                    accounts = client.sobjects.query("SELECT Id, name, phone FROM Account where name = '{}'".format(company))
                    if len(accounts) > 0:
                        account_id = accounts[0]['Id']
                        full_name = full_name.title()
                        contacts = client.sobjects.query("SELECT Id, name, phone, accountid from Contact where name='{}' and accountid='{}'".format(full_name, account_id))
                        if len(contacts) > 0:
                            client.sobjects.Contact.update(contacts[0]['Id'], {'phone': convertNumberToE164(phone_number)})
                            output_session_attributes['contact_id'] = contacts[0]['Id']
                        else:
                            name_arr = full_name.split(' ')
                            first_name = name_arr[0] if len(name_arr) > 1 else ''
                            last_name = name_arr[0] if len(name_arr) == 1 else name_arr[1]
                            contact_res = client.sobjects.Contact.insert({'firstname': first_name, 'lastname': last_name, 'phone': convertNumberToE164(phone_number), 'accountid': account_id})
                            output_session_attributes['contact_id'] = contact_res['id']
                        output_session_attributes['phone_number'] = convertNumberToE164(phone_number)   
                    return delegate(output_session_attributes, get_slots(intent_request))
                elif agree_phone_call is not None:
                    #once his company and phone_number are exist
                    if agree_phone_call.lower() == 'yes':
                        #if he agreed to phone call to his number
                        phone_number = convertNumberToE164(output_session_attributes['phone_number']) if ('phone_number' in output_session_attributes and output_session_attributes['phone_number'] is not None) else ''
                        return delegate(output_session_attributes, get_slots(intent_request))
                    else:
                        #asking his phone number
                        return elicit_slot(output_session_attributes,
                                intent_request['currentIntent']['name'],
                                slots,
                                "phone_number",
                                "What number would you like us to call you at?")
                else:
                    #once customer fills full_name, company, refered_from, product
                    client = Salesforce(
                        username=os.environ['sales_username'],
                        password=os.environ['sales_password'],
                        security_token=os.environ['sales_token']
                    )
                    accounts = client.sobjects.query("SELECT Id, name, phone FROM Account where name = '{}'".format(company))
                    if len(accounts) > 0:
                        inserted_id = accounts[0]['Id']
                        contacts = client.sobjects.query("SELECT Id, name, phone, accountid from Contact where name='{}' and accountid='{}'".format(full_name, inserted_id))
                        today_date = datetime.date.today()
                        close_date = add_months(today_date, 1)
                        close_date = close_date.strftime('%Y-%m-%d')
                        opp_info = client.sobjects.Opportunity.insert({'name': product, 'closedate': close_date, 'stagename': 'Qualification', 'accountid': inserted_id})
                        if len(contacts) > 0 and contacts[0]['Phone'] is not None:
                            output_session_attributes['phone_number'] = convertNumberToE164(contacts[0]['Phone'])
                            output_session_attributes['contact_id'] = contacts[0]['Id']
                            return elicit_slot(output_session_attributes,
                                    intent_request['currentIntent']['name'],
                                    slots,
                                    "agree_phone_call",
                                    "I see that you’re a customer of ACME Manufacturing. Can we call you at {}? (Yes/No)".format(convertNumberToE164(contacts[0]['Phone'])))
                            logger.debug("{}\'s phone number is {}".format(full_name, convertNumberToE164(contacts[0]['phone'])))
                        else:
                            return elicit_slot(output_session_attributes,
                                    intent_request['currentIntent']['name'],
                                    slots,
                                    "phone_number",
                                    "What number would you like us to call you at?")
                    else:
                        acc_info = client.sobjects.Account.insert({'name': company})
                        inserted_id = acc_info['id']
                        today_date = datetime.date.today()
                        close_date = add_months(today_date, 1)
                        close_date = close_date.strftime('%Y-%m-%d')
                        logger.debug(close_date)
                        opp_info = client.sobjects.Opportunity.insert({'name': product, 'closedate': close_date, 'stagename': 'Qualification', 'accountid': inserted_id})
                        return elicit_slot(output_session_attributes,
                                    intent_request['currentIntent']['name'],
                                    slots,
                                    "phone_number",
                                    "What number would you like us to call you at?")

        # Pass the price of the flowers back through session attributes to be used in various prompts defined
        # on the bot model.

        # if flower_type is not None:
        #     output_session_attributes['Price'] = len(flower_type) * 5  # Elegant pricing model

        return delegate(output_session_attributes, get_slots(intent_request))

    # Order the flowers, and rely on the goodbye message of the bot to define the message to the end user.
    # In a real bot, this would likely involve a call to a backend service.
    output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
    if intent_request['currentIntent']['name'] == 'VoiceOmniIntent' and ('connect_channel' in output_session_attributes and output_session_attributes['connect_channel'] is not None and output_session_attributes['connect_channel'] == "VOICE"):
        output_session_attributes['full_name'] = full_name.title()
        output_session_attributes['company'] = company
        output_session_attributes['refered_from'] = refered_from
        output_session_attributes['product'] = product
        return close(output_session_attributes,
                 'Fulfilled',
                 {'contentType': 'PlainText',
                  'content': 'Thanks {}, we will connect you with our agent now.'.format(full_name)})
    else:
        output_session_attributes['full_name'] = full_name
        output_session_attributes['company'] = company
        output_session_attributes['refered_from'] = refered_from
        output_session_attributes['product'] = product
        output_session_attributes['phone_number'] = convertNumberToE164(phone_number)
        return close(output_session_attributes,
                 'Fulfilled',
                 {'contentType': 'PlainText',
                  'content': 'Okay, our agent will call you shortly. Thanks.'})



""" --- Intents --- """


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 == 'VoiceOmniIntent' or intent_name == 'ChatOmniIntent':
        return get_customer_information(intent_request)

    raise Exception('Intent with name ' + intent_name + ' not supported')


""" --- Main handler --- """


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(json.dumps(event))
    logger.debug('event.bot.name={}'.format(event['bot']['name']))
    # return event
    return dispatch(event)
Tom
  • 25
  • 5
  • Since the bot only seems to break when interacting using voice and not text, this does not seem to be a Lambda function issue. You would need to check if the bot is doing the speech to text conversion correctly to pick up the slot values. Try enabling [conversation logs](https://docs.aws.amazon.com/lex/latest/dg/conversation-logs.html) to confirm this. – Paradigm Jun 10 '20 at 13:05
  • Figured out that this was related to the fact that my sample utterance was causing the problem. Since I was using a slot value of {full_name} with nothing else, the bot later on down the slot progression would get confused when asking which company I was calling from and match on that utterance. Long story short, this has fixed the problem. – Tom Jun 13 '20 at 19:06

0 Answers0