Blog de Amazon Web Services (AWS)

Extienda procesos de negocio integrando WhatsApp y Sistemas SAP utilizando Amazon Lex

Por Maximiliano Alexis Kretowicz Parchuc, Arquitecto de Soluciones AWS

 

En la era de la transformación y digitalización de los procesos, la búsqueda por mejorar la experiencia de los usuarios mediante una estrategia Omnicanal ha sido una meta compartida por pequeñas, medianas y grandes empresas. La generación de nuevas aplicaciones, tanto web como móviles, han mejorado la interacción y velocidad en todo tipo de procesos, ya sea B2C (Business to Customer) como B2B (Business to Business) o B2E (Business to Employee). A pesar de ello, la evolución de la tecnología y su rápida adopción ha provocado que se utilicen aplicaciones de uso cotidiano para la comunicación entre pares, trabajadores y empresas, como por ejemplo Twitter y WhatsApp,  abriendo nuevas posibilidades y canales de comunicación.  Si observamos en nuestro entorno, podremos encontrar que cada vez más personas y grupos de trabajo han reemplazando el uso de herramientas corporativas por aplicaciones como WhatsApp debido a su masificación, obligando a las empresas a adoptarlas en sus procesos internos y estrategia de Omnicanalidad.

Uno de los grandes desafíos en este proceso de adopción es entregar la experiencia que los usuarios, tanto internos como externos, esperan, en tiempo y forma, en la interacción con los diferentes canales existentes. Desafío que Amazon Lex (AWS Chatbot AI) ha ayudado a resolver. El uso de chatbots es una de las principales herramientas para escalar las operaciones de ventas, aumentar el engagement y automatizar tareas simples en los procesos de servicio al cliente y/o asistencia a usuarios. Ejemplos prácticos son la consulta por preguntas frecuentes, consulta de información sobre documentación y/o agendar un servicio. Si profundizamos aún más, podemos transportar el uso de Chatbots a procesos externos e internos de diferentes industrias. Como por ejemplo:

  • En la implementación de proyectos, instalaciones o incluso mantenimiento podemos reportar el avance de una actividad, consultar por ordenes de trabajo/mantenimiento y reportar el consumo de materiales.
  • En los procesos de venta o logísticos, generar ordenes de venta e inclusive obtener el stock existente en un almacén para finalizar con una orden de compra o movimiento de material si el stock existente fuese cero.
  • En procesos de Customer Relationship Management(CRM) o Supplier Relationship Management (SRM), es posible disponibilizar una herramienta que permita consultar el estado de los procesos, documentos, pagos y cobros o incluso, un canal de auto atención para la generación de reclamos o responder consultas sin tiempos de espera. Esto nos permite abrir un nuevo canal de comunicación que incluye el uso de sistemas IVR (Interactive Voice Response) como Amazon Connect.

Como hemos visto, existen interesantes posibilidades con la adopción de estas tecnologías. Ya que permiten facilitar el trabajo y la colaboración entre equipos a través de potenciar herramientas que ya dominan y son parte de las aplicaciones normales que utiliza un usuario de dispositivos móviles. Esto reduce la complejidad de las operaciones de estas compañías y en consecuencia, ayuda a hacerlos más productivos.

En este blog construiremos una demo que validará la integración entre Amazon Lex con sistemas SAP (backend) permitiendo extender procesos de negocio y enriquecer la interacción con diferentes tipos de usuarios y/o clientes.

 

Overview de Solución

A través de la aplicación WhatsApp, el usuario interactúa con Amazon Lex para obtener respuesta a diferentes consultas. En el caso específico de nuestra demo, sobre la existencia de materiales en un almacén o centro; de no encontrarse existencia, puede generar una orden de compra (OC). Para obtener la información sobre cada material y/o crear una OC, Amazon Lex utiliza funciones Lambda (AWS Lambda), AWS Lambda, servicio de computo sin servidor (Serverless), nos permite ejecutar nuestro código en lenguaje Python sin la necesidad de mantener servidores y pagando solo por el computo utilizado en fracciones de milisegundos.  A su vez, las funciones Lambda se conectan al módulo SAP MM (Materials Management) mediante el consumo de BAPIs (Business Application Programming Interface) estándar de nuestro SAP ERP, alojado en instancias de computo AWS EC2.

En la siguiente imagen se aprecia el flujo de interacción entre los usuarios y SAP ERP mediante los servicios de AWS nombrados anteriormente.

 

 

¿Como funciona?

Nuestra demo responde a tres tipos de Intents o Intenciones en español, la intención es el objetivo del usuario en la conversación:

  1. Saludo, el cual detectará el inicio de la conversación o el cierre de la misma cuando el usuario utilice, por ejemplo, “Hola”, “Adiós”.
  2. Stock, que nos permitirá detectar la intención del usuario de conocer la disponibilidad de un material/producto en un almacén específicos y generar la consulta a nuestro backend. Nuestro Bot responderá a frases como “Quiero conocer disponibilidad del material cables”.
  3. PurchaseOrder, intención que iniciará el proceso de obtención de datos (Slots) para generar una Orden de Compra (OC)/ Purchase order (PO) con frases como “comprar” y entregará el resultado de la operación con un ID de Documento.

Los materiales/productos para consulta en esta demo:

  • cables
  • monitor
  • generador
  • celular
  • laptop

Siempre se solicitará el almacén, en este caso “New York”. Para conocer cómo funciona Amazon Lex, puedes revisar la guía completa de desarrollo.

Paso a Paso

En esta sección revisaremos como implementar nuestra demo y los requisitos necesarios para hacerlo.

  1. Creación de Lambda Layer con las librerías para la comunicación de nuestras funciones con el sistema SAP
  2. Creación de Función Lambda “GetStock”.
  3. Creación de Función Lambda “CreatePO”.
  4. Creación de Función Lambda “IntentPO”.
  5. Crear permisos para consumo de funciones Lambda.
  6. Creación de nuestro Bot con Amazon Lex en Español.
  7. Publicación e Integración con Twilio.
  8. Probar nuestra Demo.

Pre Requisitos

  • Una cuenta AWSy sus respectivos accesos como administrador.
  • Conocimientos en el uso de AWS Lambda para desarrollador y uso de Python.
  • Sistema SAP NetWeaver ABAP 4.6C o superior (ECC, S4, BW, …) configurado y accesible dentro del entorno VPC a utilizar.
  • SAP RFC usuario y password con los permisos necesarios para acceso y consulta.
  • SAP S-User para descargar RFC Netweaver Lib.
  • Una cuenta Twilioy sus respectivos accesos como Administrador.

Creación de Lambda Layer

Para lograr la comunicación entre AWS Lambda y AS ABAP crearemos un Lambda Layer que incluya el driver PyRFC provisto por SAP. La guía paso a paso podrás encontrarla en el siguiente link.

Creación de Función Lambda “GetStock”

Para poder obtener información sobre la existencia de diferentes materiales en un almacén, es necesario crear una función Lambda que nos permita invocar la BAPI “BAPI_MATERIAL_STOCK_REQ_LIST”.

  1. En Lambda console, elegimos Create Function, Author from scratch.
  2. Ingresamos el nombre de la función, en este caso “GetStock“.
  3. Para el Runtime, elegimos Python 3.8.
  4. En permisos elegimos Create a new role with basic Lambda permission.
  5. En el apartado Advanced Settings, Networkelegimos como VPC la VPC donde este desplegado nuestro SAP ERP a conectar , de igual manera nuestra Subnet. Como Security Group debemos elegir un security group que nos permita comunicarnos mediante los puertos estándar, o los que tengamos configurados, para el consumo de RFC en nuestro SAP ERP.
  6. Elegimos crear función.
  7. Una vez creada la función, en la sección configuración de nuestra función Lambda realizamos click en Layer.
  8. Agregamos un Layercon Add Layer y seleccionamos el Layer generado en el apartado anterior “Creación Lambda Layer”.
  9. Una vez cargado el Layer nos disponemos a crear nuestro código Python dentro de la función Lambda, puedes usar el siguiente código de ejemplo y luego realizar el deploy del mismo. Es necesario modificar, antes de realizar el deploy, las variables específicas de tu entorno SAP que se encuentran en el código, como por ejemplo el almacén y/o los números de material.
  

import logging
import json
from pyrfc import Connection

#Definición de variables globales.
USERID = '<Indicar SAP User>'
PASSWD = '<Indicar SAP PSW>'
#LANG = 'ES'
SAPHOST= '<Indicar SAP IP o HOSTNAME>'
SAPINSTANCENUM= '<Indicar SAP Instance Number>'
SAPCLIENT= '<Indicar SAP Client>'

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

#Esta funcion genera el objeto de respuesta
def close(session_attributes, fulfillment_state, message):
    response = {
        "sessionAttributes":
session_attributes,
        "dialogAction": {
            "type":
"Close",
            "fulfillmentState":
fulfillment_state,
            "message": message,
        },
    }
    return response

#Crea la respuesta y envía el cierre de la Intención
def dispatch(stock, session_attributes):

    if stock['cantidad']:
        resultado = "El almacen {} aloja la
cantidad de {} unidades del material {}. Indica el nombre de otro material para
revisarlo o comprar si quieres aprovisionar".format(
            stock['almacen'],
            stock['cantidad'],
            stock['material']
        )
    elif not stock['almacen_num']:
        resultado = "El almacen {} no existe.
Indica el nombre de otro material si quieres revisarlo o comprar si quieres
aprovisionar".format(
        
            stock['almacen']
        )
    elif not stock['material_num']:
        resultado = "El material {} no existe.
Indica el nombre de otro material si quieres revisarlo o comprar si quieres
aprovisionar".format(
        
            stock['material']
        )
    elif not stock['cantidad']: #cantidad 0
        resultado = "El almacen {} aloja la
cantidad de {} unidades del material {}. Indica comprar si quieres
aprovisionar".format(
        
            stock['almacen'],
            stock['cantidad'],
            stock['material']
        )

    else: #Si el material no existiese en el almacen. 
        resultado = "El material {} no existe en
el almacen {}. Indica el nombre de otro material si quieres
revisarlo".format(
        
            stock['material'],
            stock['almacen']
        )

    return close (
        session_attributes,
        "Fulfilled",
        {"contentType": "CustomPayload",
"content": resultado},
    )

#Obtiene las transformaciones necesarias de numero de material, es importante
usar las funciones SAP. En este caso usaremos valores estática
def get_material_transform(material_name):
    #quitar acentos
    if not (material_name.upper().find('LAPTOP') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('CELULAR') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('FLOPPY') == -1) or not
(material_name.upper().find('FLOPY') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('IPHONE') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('CABLE') == -1):
        material_number = '102-432'
    if not (material_name.upper().find('MONITOR') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('AD-333-300') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('GENERADOR') == -1):
        material_number = '<Indicar Num Material
interno>' 
 
    return material_number

#Obtiene las transformaciones necesarias de alamacen, es importante usar las
funciones SAP. En este caso usaremos una tabla estatica
def get_almacen_transform(almacen_name):

    almacen_number = ''
    if not (almacen_name.upper().find('NEW YORK') == -1):
        almacen_number = '<Indicar Num Almacén
interno>'

    return almacen_number

#Obtiene la cantidad disponible del material
def get_cantidad(num_material, num_almacen):
    con = Connection(ashost=SAPHOST, sysnr=SAPINSTANCENUM,
client=SAPCLIENT, user=USERID, passwd=PASSWD)
    
    BapiStock = con.call('BAPI_MATERIAL_STOCK_REQ_LIST',
MATERIAL=num_material, PLANT=num_almacen)

    #Validar si hubo error en la información y devolverlo
    #print(BapiStock['RETURN']) #errores
    cantidad_material = BapiStock['MRP_STOCK_DETAIL']['UNRESTRICTED_STCK']#valor
del stock.

    return cantidad_material

# Obtenemos el stock
def get_stock(event):
    request_attributes = request_attributes =
event["requestAttributes"] if "requestAttributes" in event
else None

    if event["currentIntent"]["slotDetails"]["slotMaterial"]["resolutions"]:
        stock_material =
event["currentIntent"]["slotDetails"]["slotMaterial"]["resolutions"][0]["value"]
    else:
        stock_material =
event["currentIntent"]["slotDetails"]["slotMaterial"]["originalValue"]
    stock_material_num = get_material_transform(stock_material)
  
 print(event["currentIntent"]["slotDetails"]["slotPlanta"]["resolutions"])
    if
event["currentIntent"]["slotDetails"]["slotPlanta"]["resolutions"]:
        stock_planta = event["currentIntent"]["slotDetails"]["slotPlanta"]["resolutions"][0]["value"]
    else:
        stock_planta =
event["currentIntent"]["slotDetails"]["slotPlanta"]["originalValue"]
    stock_planta_num = get_almacen_transform(stock_planta)

    stock_info = {
        "material": stock_material,
        "material_num": stock_material_num,
        "almacen": stock_planta,
        "almacen_num": stock_planta_num,
    }
    print(stock_material_num,stock_planta_num)
    if stock_material_num and stock_planta_num:
        stock_info["cantidad"] =
get_cantidad(stock_material_num, stock_planta_num)
    else: 

        stock_info["cantidad"] = 0
    
    return stock_info

# Punto de entrada de la Función, el objeto evento trae los datos del
fulfillment
def lambda_handler(event, context):
    print(event)
    stock_info = get_stock(event)
    respon  = dispatch(stock_info,
event['sessionAttributes'])
    print (respon)
    return respon
 
       

Creación de Función Lambda “CreatePO”.

Para poder crear una Orden de Compra (OC) en nuestro sistema SAP ERP, es necesario crear una función Lambda que nos permita invocar la BAPI “BAPI_PO_CREATE1”.

  1. En Lambda console, elegimos Create Function, Author from scratch.
  2. Ingresamos el nombre de la función, en este caso “CreatePO“.
  3. Para el Runtime, elegimos Python 3.8.
  4. En permisos elegimos Create a new role with basic Lambda permission.
  5. En el apartado Advanced Settings, Networ” elegimos como VP” la VPC donde este desplegado nuestro SAP ERP a conectar , de igual manera nuestra Subnet. Como Security Groupdebemos elegir un security group que nos permita comunicarnos mediante los puertos estándar, o los que tengamos configurados, para el consumo de RFC en nuestro SAP ERP.
  6. Elegimos crear función.
  7. Una vez creada nuestra función, en la sección configuración de nuestra función Lambda realizamos click en Layer.
  8. Agregamos un Layercon Add Layer y seleccionamos el Layer generado en el apartado anterior “Creación Lambda Layer”.
  9. Una vez cargado el Layer nos disponemos a crear nuestro código Python dentro de la función Lambda, puedes usar el siguiente código de ejemplo y luego realizar el deploy del mismo. Es necesario modificar, antes de realizar el deploy, las variables específicas de tu entorno SAP que se encuentran en el código, como por ejemplo el almacén y/o los números de material.
import logging
import json
from pyrfc import Connection
from datetime import date, datetime, timedelta

#Definición de variables globales.
USERID = '<Indicar SAP User>'
PASSWD = '<Indicar SAP PSW>'
#LANG = 'ES'
SAPHOST= '<Indicar SAP IP o HOSTNAME>'
SAPINSTANCENUM= '<Indicar SAP Instance Number>'
SAPCLIENT= '<Indicar SAP Client>'

C_X = 'X'
FECHA = datetime.now().strftime('%Y%m%d')
FECHA_2 =  (lambda x: x.strftime('%d.%m.%Y'))(datetime.now() +
timedelta(days=6))
VENDOR = '<Indicar Vendor>'
ITEM = '1'
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

#Esta funcion genera el objeto de respuesta
def close(session_attributes, fulfillment_state, message):
    response = {
        "sessionAttributes":
session_attributes,
        "dialogAction": {
            "type":
"Close",
            "fulfillmentState":
fulfillment_state,
            "message": message,
        },
    }
    return response

#Crea la respuesta y envía el cierre de la Intención
def dispatch(purchase_order, session_attributes):

    if purchase_order['PO']:
        resultado = "Se ha creado la Orden de
Compra Nº{}, almacen {}, material {} por una cantidad de {}.".format(
            purchase_order['PO'],
            purchase_order['almacen'],
            purchase_order['material'],
            purchase_order['cantidad']
        )
    elif not purchase_order['almacen_num']:
        resultado = "El almacen {} no existe. No
hemos podido crear tu Orden de Compra".format(
            purchase_order['almacen']
        )
    elif not purchase_order['material_num']:
        resultado = "El material {} no existe.
 No hemos podido crear tu Orden de Compra".format(
            purchase_order['material']
        )
    else: #Si el material no existiese en el almacen.
        resultado = "No ha sido posible crear la
Orden de Compra, por favor contactese con el equipo de soporte al número
555-5555"

    return close (
        session_attributes,
        "Fulfilled",
        {"contentType":
"CustomPayload", "content": resultado},
    )

#Obtiene las transformaciones necesarias de numero de material, es importante
usar las funciones SAP. En este caso usaremos valores estática
def get_material_transform(material_name):
    #quitar acentos
    if not (material_name.upper().find('LAPTOP') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('CELULAR') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('FLOPPY') == -1) or not
(material_name.upper().find('FLOPY') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('IPHONE') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('CABLE') == -1):
        material_number = '102-432'
    if not (material_name.upper().find('MONITOR') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('AD-333-300') == -1):
        material_number = '<Indicar Num Material
interno>'
    if not (material_name.upper().find('GENERADOR') == -1):
        material_number = '<Indicar Num Material
interno>' 
 
    return material_number

#Obtiene las transformaciones necesarias de alamacen, es importante usar las
funciones SAP. En este caso usaremos una tabla estatica
def get_almacen_transform(almacen_name):

    almacen_number = ''
    if not (almacen_name.upper().find('NEW YORK') == -1):
        almacen_number = '<Indicar Num Almacén
interno>'

    return almacen_number

#Creamos la orden de compra
def create_po(pohead, poheadx, poitem, poitemx, posched, poschedx):
    con = Connection(ashost=SAPHOST, sysnr=SAPINSTANCENUM,
client=SAPCLIENT, user=USERID, passwd=PASSWD)
    #validar la conexion antes de hacer el request
    BapiPO = con.call('BAPI_PO_CREATE1', POHEADER=pohead,
POHEADERX=poheadx, POITEM=poitem, POITEMX = poitemx, POSCHEDULE = posched,
POSCHEDULEX = poschedx)
    #Validar si hubo error en la información y devolverlo l
    #print(BapiPO['RETURN']) #errores
    pOrder = ''
    if BapiPO:
        messages = BapiPO.get('RETURN')
        if (messages and len(messages)>0):
            #validamos si se creo la OC/PO
caso contrario revisamos los errores
            if (messages[0].get('TYPE','S')
and messages[0].get('ID','06') and messages[0].get('NUMBER','017') and
messages[0].get('MESSAGE_V2') == BapiPO['EXPPURCHASEORDER']):
                print(messages)
                BapiCommit =
con.call('BAPI_TRANSACTION_COMMIT') #validar que no hubo errores
                pOrder =
BapiPO['EXPPURCHASEORDER']# Numero de oc/po.
    print('PO {}'.format(pOrder))
    return pOrder

# Llenamos la OC/PO
def fill_po(event):
    request_attributes = request_attributes =
event["requestAttributes"] if "requestAttributes" in event
else None

    if
event["currentIntent"]["slotDetails"]["slotMaterialPO"]["resolutions"]:
        po_material =
event["currentIntent"]["slotDetails"]["slotMaterialPO"]["resolutions"][0]["value"]
    else:
        po_material =
event["currentIntent"]["slotDetails"]["slotMaterialPO"]["originalValue"]
    po_material_num = get_material_transform(po_material)
    if
event["currentIntent"]["slotDetails"]["slotPlantaPO"]["resolutions"]:
        po_planta =
event["currentIntent"]["slotDetails"]["slotPlantaPO"]["resolutions"][0]["value"]
    else:
        po_planta =
event["currentIntent"]["slotDetails"]["slotPlantaPO"]["originalValue"]
    po_planta_num = get_almacen_transform(po_planta)
    if
event["currentIntent"]["slotDetails"]["slotCantidadPO"]["resolutions"]:
        po_cantidad =
event["currentIntent"]["slotDetails"]["slotCantidadPO"]["resolutions"][0]["value"]
    else:
        po_cantidad = event["currentIntent"]["slotDetails"]["slotCantidadPO"]["originalValue"]

    po_info = {
        "material": po_material,
        "material_num": po_material_num,
        "almacen": po_planta,
        "almacen_num": po_planta_num,
        "cantidad": po_cantidad
    }

    if po_material_num and po_planta_num:
        pohead={
              
 'COMP_CODE':'<indicar el correspondiente a tu entorno>', 
                'DOC_TYPE':'NB',
              
 'CREAT_DATE':FECHA,
              
 'VENDOR':VENDOR.rjust(6 + len(VENDOR), '0'),
              
 'PURCH_ORG':'<indicar el correspondiente a tu entorno>',
              
 'PUR_GROUP':'<indicar el correspondiente a tu entorno>',
                'LANGU':'EN',
                'DOC_DATE':FECHA
        }
    
        poheadx={
                'COMP_CODE':C_X,
                'DOC_TYPE':C_X,
                'CREAT_DATE':C_X,
                'VENDOR':C_X,
                'LANGU':C_X,
                'PURCH_ORG':C_X,
                'PUR_GROUP':C_X,
                'DOC_DATE':C_X
            }
    
        # item level data
        if po_material_num != 'AD-333-300':
            
            poitem=[{
                  
 'PO_ITEM':ITEM,
                  
 'MATERIAL':po_material_num,
                  
 'PLANT':po_planta_num,
                  
 'QUANTITY':po_cantidad,
                  
 'NET_PRICE': '100' 
                }]
        
            poitemx=[{
                  
 'PO_ITEM':ITEM,
                  
 'PO_ITEMX':C_X,
                  
 'MATERIAL':C_X,
                    'PLANT':C_X,
                  
 'STGE_LOC':C_X,
                  
 'QUANTITY':C_X,
                  
 'NET_PRICE':C_X,
                  
 'TAX_CODE':C_X,
                  
 'ITEM_CAT':C_X,
                  
 'ACCTASSCAT':C_X
                }]
        else:
            poitem=[{
                  
 'PO_ITEM':ITEM,
                  
 'MATERIAL':po_material_num,
                  
 'PLANT':po_planta_num,
                  
 'QUANTITY':po_cantidad,
                }]
        
            poitemx=[{
                  
 'PO_ITEM':ITEM,
                  
 'PO_ITEMX':C_X,
                  
 'MATERIAL':C_X,
                  
 'PLANT':C_X,
                  
 'STGE_LOC':C_X,
                  
 'QUANTITY':C_X,
                  
 'TAX_CODE':C_X,
                  
 'ITEM_CAT':C_X,
                  
 'ACCTASSCAT':C_X
                }]
    
        # schedule line level data
        posched=[{
                'PO_ITEM':ITEM,
                'SCHED_LINE':ITEM,
              
 'DEL_DATCAT_EXT':'D',
              
 'DELIVERY_DATE':FECHA_2,
              
 'DELIV_TIME':'000001',
              
 'QUANTITY':po_cantidad
            }]
    
        poschedx=[{
                'PO_ITEM':ITEM,
                'SCHED_LINE':ITEM,
                'PO_ITEMX':C_X,
                'SCHED_LINEX':C_X,
              
 'DEL_DATCAT_EXT':C_X,
              
 'DELIVERY_DATE':C_X,
                'QUANTITY':C_X
            }]
    
        po_info["PO"] = create_po(pohead,
poheadx, poitem, poitemx, posched, poschedx)
    else:
        po_info["PO"] = ''

    return po_info

# Punto de entrada de la Función, el objeto evento trae los datos del
fulfillment
def lambda_handler(event, context):
    print(event)
    po_info = fill_po(event)
    respon  = dispatch(po_info, event['sessionAttributes'])
    print (respon)
    return respon
 
       

Creación de Función Lambda “IntentPO”.

En algunas ocasiones deseamos transferir información entre diferentes Intents o inicializar los Slots que utiliza Amazon Lex para interactuar con nuestros usuarios, para esta demo crearemos una función Lambda con el objetivo de inicializar diferentes Slots con valores iniciales.

  1. En Lambda console, elegimos Create Function, Author from scratch.
  2. Ingresamos el nombre de la función, en este caso “IntentPO“.
  3. Para el Runtime, elegimos Python 3.8.
  4. En permisos elegimos Create a new role with basic Lambda permission.
  5. En el apartado Advanced Settings, Networkelegimos como VPC la VPC donde este desplegado nuestro SAP ERP a conectar.
  6. Elegimos crear función.
  7. Una vez creada nuestra función, nos disponemos a crear nuestro código Python dentro de la función Lambda, puedes usar el siguiente código de ejemplo y luego realizar el deploy del mismo.

 

import logging
import json

#Definición de variables globales.

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

#Esta funcion genera el objeto de respuesta
def close(session_attributes, fulfillment_state, message):
    response = {
        "sessionAttributes":
session_attributes,
        "dialogAction": {
            "type":
"Close",
            "fulfillmentState":
fulfillment_state,
            "message": message,
        },
    }
    return response

def lambda_handler(event, context):
    print(event)
    
    if not event['recentIntentSummaryView']: #no hay acción de
otro intent previa. 
    
        currentIntent = event.get('currentIntent') 
        response = {
              
"sessionAttributes":{
                
 "currentStockQuery":'No Intent Previo'
               },
              
"dialogAction":{
                
 "type":"Delegate",
                  
"slots": {
                    
  'slotMaterialPO': currentIntent.get('slots').get('slotMaterialPO'),
                    
  'slotCantidadPO': currentIntent.get('slots').get('slotCantidadPO'),
                    
  'slotPlantaPO': currentIntent.get('slots').get('slotPlantaPO')
                   }
               }
            }

    else:
        
        recentIntentSummaryView =
event['recentIntentSummaryView'] #obtenemos el valor del Intent Anterior
        slotsValues = event.get('currentIntent')
  
        dialogActionType = "Delegate"
        #validamos si el slot fue contulta de stock y
no se ingresaron valores actualmente cargados
        print(recentIntentSummaryView)
        print(slotsValues)
        if ((recentIntentSummaryView[0]['intentName']
== 'STOCK' and recentIntentSummaryView[0]['slots']['slotPlanta'] != None and
      
 recentIntentSummaryView[0]['slots']['slotMaterial'] != None) and 
        (slotsValues.get('slots').get('slotMaterialPO')
== None and slotsValues.get('slots').get('slotPlantaPO') == None)):
            
            #dialogActionType =
"ConfirmIntent"
          
 slotsValues['slots']['slotPlantaPO'] =
recentIntentSummaryView[0]['slots']['slotPlanta']
            slotsValues['slots']['slotMaterialPO']
= recentIntentSummaryView[0]['slots']['slotMaterial']

    
        response = {
              
"sessionAttributes":{
                
 "currentStockQuery":'Intent Previo'
               },
              
"dialogAction": {
                  
"type": dialogActionType ,
                  
"slots": {
                    
  'slotMaterialPO': slotsValues.get('slots').get('slotMaterialPO'),
                    
  'slotCantidadPO': slotsValues.get('slots').get('slotCantidadPO'),
                    
  'slotPlantaPO': slotsValues.get('slots').get('slotPlantaPO')
                   }
               }
            }

    print(response)
    return response
 
       

Crear permisos para consumo de funciones Lambda

Nuestro Bot debe tener la autorización necesaria para consumir las funciones Lambda anteriormente creadas, para ello generaremos políticas basadas en recursos o “Resources Policies” en cada una de nuestras funciones Lambda.

  1. Accedemos a la consola de Cloud Shell, Cloud Shelles un “Shell” qué puede correr en nuestro browser y esta previamente autenticado con nuestras credenciales.

2. Una vez iniciado Cloud Shell, veremos una terminal en nuestro browser que nos permitirá interactuar con nuestros servicios AWS de forma segura.

3. Para generar nuestra política basada en recursos utilizaremos AWS Command Line Interface (CLI). En este caso con la siguiente secuencia de comandos:

aws lambda add-permission --function-name my-function --action lambda:InvokeFunction --statement-id lex \
--principal lex.amazonaws.com --source-arn my-source-arn --output text

4. Iniciaremos con nuestra función “IntentPO”, utilizaremos el siguiente comando:

aws lambda add-permission --function-name arn:aws:lambda:<region>:<idcuenta>:function:IntentPO --action lambda:InvokeFunction --statement-id lex \
--principal lex.amazonaws.com --source-arn arn:aws:lex:<region>:<idcuenta>:intent:PURCHASEORDER:* --output text

*Reemplazar <region> por la región donde donde estemos trabajando.
**Reemplazar <idcuenta> por el ID de la cuenta AWS donde estemos trabajando. 

5. Para validar que la política se haya creado correctamente debemos ir a la consola de AWS Lambda, seleccionamos la función “IntentPO” y en la sección Permissionsbuscamos Resource-based policy. Debemos ver un JSON similar al siguiente:

 

  1. Continuaremos con la función “GetStock”, utilizaremos el siguiente comando:
    aws lambda add-permission --function-name arn:aws:lambda:<region>:<idcuenta>:function:GetStock --action lambda:InvokeFunction --statement-id lex \
    --principal lex.amazonaws.com --source-arn arn:aws:lex:<region>:<idcuenta>:intent:STOCK:* --output text

    *Reemplazar <region> por la región donde donde estemos trabajando.
    **Reemplazar <idcuenta> por el ID de la cuenta AWS donde estemos trabajando.

  2. Para validar que la política se haya creado correctamente debemos ir a la consola de AWS Lambda, seleccionamos la función “GetStock” y en la sección Permissions buscamos Resource-based policy. Debemos ver un JSON similar al siguiente:

 

  1. Continuaremos con la función “CreatePO”, utilizaremos el siguiente comando:
    aws lambda add-permission --function-name arn:aws:lambda:<region>:<idcuenta>:function:CreatePO --action lambda:InvokeFunction --statement-id lex \
    --principal lex.amazonaws.com --source-arn arn:aws:lex:<region>:<idcuenta>:intent:PURCHASEORDER:* --output text

    *Reemplazar <region> por la región donde donde estemos trabajando.
    **Reemplazar <idcuenta> por el ID de la cuenta AWS donde estemos trabajando.

  2. Para validar que la política se haya creado correctamente debemos ir a la consola de AWS Lambda, seleccionamos la función “CreatePO” y en la sección Permissions buscamos Resource-based policy. Debemos ver un JSON similar al siguiente:

 

Creación de nuestro Bot con Amazon Lex en Español

Para nuestra demo debemos crear nuestro primer Bot con Amazon Lex, que llamaremos “SAP_STOCK_BOT”. Para más información sobre como crear tu primer Bot, revisar la introducción a la consola de Amazon Lex. Para nuestra demo sigue los siguientes pasos:

  1. En la consola de Amazon Lex, seleccionamos Get Startedsi es nuestro primer Bot.
  2. Seleccionamos Create your bot, Custom bot.
  3. Elegimos un nombre para nuestro bot, en este caso “SAP_STOCK_BOT”.
  4. Languageseleccionamos “Spanish(Latam)”
  5. Output Voice= “None”.
  6. Session time Out en 5 min.
  7. COPPA= No.
  8. Mantenemos el resto de opciones por default y presionamos Create.
  9. Nos abrirá automáticamente la edición de nuestro nuevo Bot, no realizaremos modificaciones y volveremos a la consola de Amazon Lex.
  10. En el menú desplegable de la consola de Amazon Lex seleccionaremos “Bots” y presionaremos el botón Actions, Import.
  11. Necesitaremos el archivo ZIP que contiene las configuraciones de nuestro nuevo Bot. Para generar nuestro archivo ZIP debemos seguir los siguientes pasos:

a. Descargamos el siguiente JSON.

b. Reemplazar <region> por la región donde donde estemos trabajando.

c. Reemplazar <idcuenta> por el ID de la cuenta AWS donde estemos trabajando.

d. Guardamos y comprimimos el archivo en un ZIP.

12. Cargamos nuestro archivo ZIP y damos aceptar, nos mostrará una leyenda de advertencia indicando que actualizará y sobre escribirá los recursos existentes. Presionamos Overwrite and Continue.

13. Seleccionamos nuestro Bot de la lista haciendo click en  “SAP_STOCK_BOT”. Esto nos permitirá editar nuestro Bot y apreciar que se han cargado los diferentes intents (Purchase Order, Saludo, Stock) y slots (material, planta).

14. Para probar nuestro Bot, debemos presionar el botón Build. Luego de unos minutos se desplegará el mensaje “SAP_STOCK_BOT build was successful” y la ventana del chat de prueba, también podremos iniciar el chat de prueba en cualquier momento haciendo click en “Test Chatbot” en el extremo derecho del editor.

15. Si escribimos “Hola” en el chat de prueba, nuestro Bot nos contestará “¿En que puedo ayudarte? ¿Deseas conocer la disponibilidad de un material o generar una orden de compra?”. Esto nos permitirá validar su correcto despliegue.

Crear una cuenta Twilio

Luego de publicar nuestro bot crearemos nuestra cuenta de Twilio. Esto nos permitirá integrar nuestro bot con Whatsapp.

  1. Crear una cuenta de Twilio gratuita en para acceder al Sandbox de Whatsapp.
  2. Una vez que la cuenta se encuentra creada, dirigirse al el ícono con los 3 puntos a la izquierda y seleccionar Programmable Messaging.
  3. Ir a la opción Try it Out -> Try WhatsApp
  4. Para poder interactuar con el Sandbox de Twilio, se debe agregar, en los contactos de nuestro teléfono móvil, el número desplegado y enviar un mensaje desde la aplicación WhatsApp siguiendo las instrucciones disponibles en la consola.

 

 

  1. Una vez activado el sandbox desde la consola de Twiliotomar nota de los siguientes campos ACCOUNT SID y AUTH TOKEN :

 

 

Publicación e integración con Twilio

Una vez probado y validado que nuestro Bot funciona correctamente, podemos realizar su publicación para su interacción con la  aplicación de WhatsApp.

  1. Ingresamos en la consola de Amazon Lexy seleccionamos nuestro bot “SAP_STOCK_BOT” para edición.
  2. Seleccionamos el botón Publish.
  3. Cargamos un alias , por ejemplo “sap_bot_test” y presionamos Publish.
  4. Luego de unos minutos, se nos indicará que nuestro Bot ha sido publicado y nos permitirá generar un endpoint para integrarse con Twilio y WhatsApp.
  5. Presionamos en Go to Channelsy seleccionamos Twilio SMS.
  6. En la página de edición de “Twilio SMS” cargamos los diferentes valores como el nombre y descripción. Además, debemos configurar los siguientes campos con información de nuestra cuenta de Twilio:

a. Para KMS key, elegir AWS/Lex.

b. Para Alias, elegir “sap_bot_test”.

c. Para Authentication Token, ingresar el AUTH TOKEN de nuestra cuenta de Twilio.

d. Para Account SID, ingresar ACCOUNT SID de nuestra cuenta de Twilio.

7. Seleccionamos Activatey la consola nos entregará, una vez asociada la información a nuestro Bot, el “Callback URL”. Guardar la URL para su uso.

8. Ingresamos a nuestra cuenta de Twilio, en el panel de navegación elegir la opción WhatsApp.

9. En la sección de configuración del Sandbox, siga los pasos requeridos para la habilitar el mismo y en el campo “When a Message Comes In” pegar la “Callback URL” que obtuvimos al publicar nuestro Bot (paso 7).

10. Guardamos la configuración y ya podemos iniciar nuestras pruebas desde WhatsApp.

Probar nuestra Demo

Ahora que tenemos nuestras funciones Lambda implementadas, nuestro Bot publicado y las configuraciones de integración entre WhatssApp, Twilio y Amazon Lex podemos probar la integración completa. Antes de iniciar es necesario que guardes en tus contactos el número telefónico de tu cuenta Twilio e inicies el Sandbox siguiendo las instrucciones entregadas al configurarlo en el apartado anterior (Publicación e Integración con Twilio). En las siguientes imágenes veremos una demostración de como se comporta nuestro Bot y como se reflejan los resultados en SAP ERP.

  1. Iniciamos la interacción con un “Saludo“.

  1. Preguntamos por el material “cables” en el almacén “New York”.

  1. Validamos en la transacción MMBE en SAP que efectivamente hay 998 unidades en el almacén New York.

  1. Preguntamos por “generadores” en New York.

  1. Validamos en la transacción mmbe la disponibilidad del material y comprobamos su valor en cero.

  1. Indicamos que queremos aprovisionar el material ya que se encuentra en cero, nos damos cuenta de que la cantidad es inexacta y cancelamos la operación.

  1. Solicitamos aprovisionar “generadores” en New York por 100 y confirmamos la operación.

  1. Una vez confirmada la OC/PO vemos como es generada y obtenemos un número de orden.

  1. Validamos en la transacción me23n que la OC/PO este creada correctamente según datos provistos.

  1. Luego de la interacción recibimos una nueva consulta para continuar, si nos negamos a continuar nuestro Bot se despide.

Conclusión

En este artículo hemos podido integrar una aplicación de uso cotidiano, como WhatsApp con Sistemas World Class,  como SAP. Además vimos como Amazon Lex puede potenciar y automatizar la interacción de los usuarios de esta integración en simples pasos, con una arquitectura “sin servidores” y un esquema de “pago por uso”.  La Flexibilidad y Agilidad que nos entregan servicios como Amazon Lex y AWS Lambda nos permiten acelerar nuestra capacidad de Innovación, permitiéndonos transformar y automatizar nuestros procesos incrementando el valor agregado con foco en nuestros usuarios. Que este artículo sea el inicio a la experimentación, si quieres conocer más sobre como AWS puede extender tus procesos de negocio y sistemas SAP visita AWS for SAP.

 

Recursos Relacionados

También puedes profundizar en servicios como Amazon Connect y Amazon Pinpoint que te permitirán potenciar canales de voz, mensajería y correo, entre otros, para acercarte aún más a tus clientes, en los siguientes artículos:

 


Sobre el autor

Maximiliano Alexis Kretowicz Parchuc, Senior Solutions Architect.

 

 

 

 

 

Este post esta inspirado en los artículos  Integrando SAP con Alexa por Felipe Chirinos, Senior Solutions Architect de AWS y Chatbot Amazon Lex + WhatssApp por Enrique Rodriguez, Solutions Architect de AWS.