Did i migrate from the default code to my testing one properly? - python

I want to create develop a chatbot using Lex's interface, as a first step i found a ScheduleAppointment default bot so i decided to do adjustments to it. The default prompts in this bot are date, time and appointmenttype. As a first step i went to using blueprint lex-make-appointment-python https://console.aws.amazon.com/lambda/home?region=us-east-1#/create/new?bp=lex-make-appointment-python and had to change lots of the default stuff there, for example it has an exact appointment time and days etc while the version i want to work on before developing it any further in lambda is one that would take ANY TIME and ANY DAY for example i can't get an error if i ask to schedule an appointment tomorrow but if the bot asks for date and i put something like hfujdhfu or banana i should be asked what is the date? again.
All that being said and done this is my version of that code after adjusting it :
import json
import dateutil.parser
import datetime
import time
import os
import math
import random
import logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
""" --- Helpers to build responses which match the structure of the necessary dialog actions --- """
def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message, response_card):
return {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'ElicitSlot',
'intentName': intent_name,
'slots': slots,
'slotToElicit': slot_to_elicit,
'message': message,
'responseCard': response_card
}
}
def confirm_intent(session_attributes, intent_name, slots, message, response_card):
return {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'ConfirmIntent',
'intentName': intent_name,
'slots': slots,
'message': message,
'responseCard': response_card
}
}
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 build_response_card(title, subtitle, options):
"""
Build a responseCard with a title, subtitle, and an optional set of options which should be displayed as buttons.
"""
buttons = None
if options is not None:
buttons = []
for i in range(min(5, len(options))):
buttons.append(options[i])
return {
'contentType': 'application/vnd.amazonaws.card.generic',
'version': 1,
'genericAttachments': [{
'title': title,
'subTitle': subtitle,
'buttons': buttons
}]
}
""" --- Helper Functions --- """
def parse_int(n):
try:
return int(n)
except ValueError:
return float('nan')
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 isvalid_date(date):
try:
dateutil.parser.parse(date)
return True
except ValueError:
return False
def build_time_output_string(appointment_time):
hour, minute = appointment_time.split(':') # no conversion to int in order to have original string form. for eg) 10:00 instead of 10:0
if int(hour) > 12:
return '{}:{} p.m.'.format((int(hour) - 12), minute)
elif int(hour) == 12:
return '12:{} p.m.'.format(minute)
elif int(hour) == 0:
return '12:{} a.m.'.format(minute)
return '{}:{} a.m.'.format(hour, minute)
def make_appointment(intent_request):
"""
Performs dialog management and fulfillment for booking a dentists appointment.
Beyond fulfillment, the implementation for this intent demonstrates the following:
1) Use of elicitSlot in slot validation and re-prompting
2) Use of confirmIntent to support the confirmation of inferred slot values, when confirmation is required
on the bot model and the inferred slot values fully specify the intent.
"""
appointment_type = intent_request['currentIntent']['slots']['AppointmentType']
date = intent_request['currentIntent']['slots']['Date']
appointment_time = intent_request['currentIntent']['slots']['Time']
source = intent_request['invocationSource']
output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
booking_map = json.loads(try_ex(lambda: output_session_attributes['bookingMap']) or '{}')
if source == 'DialogCodeHook':
# Perform basic validation on the supplied input slots.
slots = intent_request['currentIntent']['slots']
if not appointment_type:
return elicit_slot(
output_session_attributes,
intent_request['currentIntent']['name'],
intent_request['currentIntent']['slots'],
'AppointmentType',
{'contentType': 'PlainText', 'content': 'What type of appointment would you like to schedule?'},
build_response_card(
'Specify Appointment Type', 'What type of appointment would you like to schedule?',
build_options('AppointmentType', appointment_type, date, None)
)
)
if appointment_type and not date:
return elicit_slot(
output_session_attributes,
intent_request['currentIntent']['name'],
intent_request['currentIntent']['slots'],
'Date',
{'contentType': 'PlainText', 'content': 'When would you like to schedule your {}?'.format(appointment_type)},
build_response_card(
'Specify Date',
'When would you like to schedule your {}?'.format(appointment_type),
build_options('Date', appointment_type, date, None)
)
)
)
message_content = 'What time on {} works for you? '.format(date)
if appointment_time:
output_session_attributes['formattedTime'] = build_time_output_string(appointment_time)
)
available_time_string = build_available_time_string(appointment_type_availabilities)
return elicit_slot(
output_session_attributes,
intent_request['currentIntent']['name'],
slots,
'Time',
{'contentType': 'PlainText', 'content': '{}{}'.format(message_content, available_time_string)},
build_response_card(
'Specify Time',
'What time works best for you?',
build_options('Time', appointment_type, date, booking_map)
)
)
return delegate(output_session_attributes, slots)
duration = get_duration(appointment_type)
""" --- 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 == 'MakeAppointment':
return make_appointment(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('event.bot.name={}'.format(event['bot']['name']))
return dispatch(event)

There is ) at line 171 and line 175 without any ( which must be causing syntax error. many code lines are unreachable because you are coding return before them but those won't cause syntax error.
You can watch the logs on Cloudwatch.
The blueprint which you are using is very complex and not beginner friendly. You really should be using this blueprint for starting. That is my suggestion.
Also, since you are using response_card so please be aware that the response cards won't be shown in the Lex Console window. It will work in Facebook and Slack though.

Related

Use 'pytest' to mock a function that can return multiple exceptions

Using pytest, I wish to mock a function that can raise several exceptions. My app will catch the exception and create a response object, and I need to assert that each response contains the correct message, type, and in reality several other properties.
In the first instance, I have created a separate fixture to mock the function and raise each exception, and then I'm passing those fixtures in to a test with a series of events. However, because my fixtures are mocking the same function, the exception raised for every test is the same - in this case, that would be mock_exc_2, the last fixture passed to the test.
One thing I know will work is to separate the test function into multiple functions, but that seems inefficient because any future change would need to be made to multiple functions.
What is the most appropriate / efficient way to do this with with pytest?
Fixtures in 'conftest.py'
#pytest.fixture(scope='function')
def mock_exc_1(mocker):
def mock_response(self, path):
raise MissingOrgIdException()
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
#pytest.fixture(scope='function')
def mock_exc_2(mocker):
def mock_response(self, path):
# Parameter values are not emitted in the error message that is included in the response to the user.
raise InvalidOrgIdException('xxx', 'xxx')
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
# Working fixtures for 'event and 'mock_context' go here.
Broken tests in 'test_events.py'
In this scenario, only the last test is successful because both mock_exc_1 and mock_exc_2 are mocking the same function.
bad_request_args = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_context, mock_exc_1, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
Working tests in 'test_events.py'
Here the tests will pass, because each test is only using the fixture that raises the correct exception for the mocked function.
However, in reality there are more than two exceptions to test, and having to maintain a parameter with the parametrize args and a function to test each exception seems inefficient and prone to error when a change is made.
bad_request_args_1 = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
)
]
)
bad_request_args_2 = ('event, expected',
[
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args_1, indirect=['event'])
def test_400_events_1(event, expected, mock_context, mock_exc_1):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
#pytest.mark.parametrize(*bad_request_args_2, indirect=['event'])
def test_400_events_2(event, expected, mock_context, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
It seems that at the moment there is no "proper" way to do this. However, it is possible to do this by using the request.getfixturevalue('fixture')
bad_request_args = ('event, expected, mock_fixture_name',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
},
'mock_exc_1'
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
},
'mock_exc_2'
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_fixture_name, mock_context, request):
request.getfixturevalue(mock_fixture_name)
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']

Accessing nested data in a supposed dict

Alright, I'm stumped. I have googled everything I can think of from nested Dicts, Dicts inside Lists inside Dicts, to JSON referencing and have no idea how to get to this data.
I have this AWS Lambda handler that is reading Slack events and simply reversing someone's message and then spitting it out back to Slack. However, the bot can respond to itself (creating an infinite loop). I thought I had this solved, however, that was for the legacy stuff. I am Python stupid, so how do reference this data?
Data (slack_body_dict print from below):
{'token': 'NgapUeqidaGeTf4ONWkUQQiP', 'team_id': 'T7BD9RY57', 'api_app_id': 'A01LZHA7R9U', 'event': {'client_msg_id': '383aeac2-a436-4bad-8e19-7fa68facf916', 'type': 'message', 'text': 'rip', 'user': 'U7D1RQ9MM', 'ts': '1612727797.024200', 'team': 'T7BD9RY57', 'blocks': [{'type': 'rich_text', 'block_id': 'gA7K', 'elements': [{'type': 'rich_text_section', 'elements': [{'type': 'text', 'text': 'rip'}]}]}], 'channel': 'D01MK0JSNDP', 'event_ts': '1612727797.024200', 'channel_type': 'im'}, 'type': 'event_callback', 'event_id': 'Ev01MN8LJ117', 'event_time': 1612727797, 'authorizations': [{'enterprise_id': None, 'team_id': 'T7BD9RY57', 'user_id': 'U01MW6UK55W', 'is_bot': True, 'is_enterprise_install': False}], 'is_ext_shared_channel': False, 'event_context': '1-message-T7BD9RY57-D01MK0JSNDP'}
There is an 'is_bot' there under 'authorizations' I want to check. I assume this will let the bot stop responding to itself. However, for the life of me, I cannot reference it. It seems to be nested in there.
I have tried the following:
def lambda_handler(api_event, api_context):
print(f"Received event:\n{api_event}\nWith context:\n{api_context}")
# Grab relevant information form the api_event
slack_body_raw = api_event.get('body')
slack_body_dict = json.loads(slack_body_raw)
request_headers = api_event["headers"]
print(f"!!!!!!!!!!!!!!!!!!!!!!!body_dict:\n{slack_body_dict}")
print(f"#######################is_bot:\n{slack_body_dict('is_bot')}")
print(f"#######################is_bot:\n{slack_body_dict("is_bot")}")
print(f"#######################is_bot:\n{slack_body_dict(['is_bot']}")
print(f"#######################is_bot:\n{slack_body_dict(["is_bot"]}")
print(f"#######################is_bot:\n{slack_body_dict['authorizations']['is_bot']}")
As you can see I have absolutely no clue how to get to that variable to tell if it is true or false. Every 'is_bot' print reference results in an error. Can someone tell me how to reference that variable or give me something to google? Appreciate it. Code is below in case it is relevant.
import json
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
def is_challenge(slack_event_body: dict) -> bool:
"""Is the event a challenge from slack? If yes return the correct response to slack
Args:
slack_event_body (dict): The slack event JSON
Returns:
returns True if it is a slack challenge event returns False otherwise
"""
if "challenge" in slack_event_body:
LOGGER.info(f"Challenge Data: {slack_event_body['challenge']}")
return True
return False
def lambda_handler(api_event, api_context):
# Grab relevant information form the api_event
slack_body_raw = api_event.get('body')
slack_body_dict = json.loads(slack_body_raw)
request_headers = api_event["headers"]
# This is to appease the slack challenge gods
if is_challenge(slack_body_dict):
challenge_response_body = {
"challenge": slack_body_dict["challenge"]
}
return helpers.form_response(200, challenge_response_body)
# This parses the slack body dict to get the event JSON
slack_event_dict = slack_body_dict["event"]
# Build the slack client.
slack_client = WebClient(token=os.environ['BOT_TOKEN'])
# We need to discriminate between events generated by
# the users, which we want to process and handle,
# and those generated by the bot.
if slack_body_dict['is_bot']: #THIS IS GIVING ME THE ERROR. I WANT TO CHECK IF BOT HERE.
logging.warning("Ignore bot event")
else:
# Get the text of the message the user sent to the bot,
# and reverse it.
text = slack_event_dict["text"]
reversed_text = text[::-1]
# Get the ID of the channel where the message was posted.
channel_id = slack_event_dict["channel"]
try:
response = slack_client.chat_postMessage(
channel=channel_id,
text=reversed_text
)
except SlackApiError as e:
# You will get a SlackApiError if "ok" is False
assert e.response["error"] # str like 'invalid_auth', 'channel_not_found'
The structure of the data is:
{
"authorizations": [
{
"is_bot": true
}
]
}
So you would need to first index "authorizations", then to get the first item 0, and lastly "is_bot".
data["authorizations"][0]["is_bot"]
Alternativly, you could iterate over all the authorizations and check if any (or all) of them are marked as a bot like so:
any(auth["is_bot"] for auth in slack_body_dict["authorizations"])

Lambda and Lex integration no String-argument constructor/factory method to deserialize from String value

I'm new to Lex integration with Lambda, have used lambda before with Connect but not sure why this sample code is failing on me and don't understand the error message.
I have two slots setup in Lex (empid and fever). The utterance kicks of the bot and then I get the error message instead of the question "please enter your employee id?".
import json
def build_response(message):
return {
"dialogAction":{
"type":"Close",
"fulfillmentState":"Fulfilled",
"message":{
"contentType":"PlainText",
"content":message
}
}
}
def elicit_slot(intent_name, slots, slot_to_elicit, message):
return {
'dialogAction': {
'type': 'ElicitSlot',
'intentName': intent_name,
'slots': slots,
'slotToElicit': slot_to_elicit,
'message': message
}
}
def delegate(session_attributes, slots):
return {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'Delegate',
'slots': slots
}
}
def perform_action(intent_request):
source = intent_request['invocationSource'] # DialogCodeHook or FulfillmentCodeHook
slots = intent_request['currentIntent']['slots'] # your slots
if source == 'DialogCodeHook':
# Perform basic validation on the supplied input slots.
if slots['empid'] is None: # or any other validation that you want to perform
return elicit_slot(
intent_request['currentIntent']['name'], # current intent name
slots, # current intent slots
'empid', # slot name
'Please enter your employee id' # prompt the user to empid
)
if slots['fever'] is None:
return elicit_slot(
intent_request['currentIntent']['name'], # current intent name
slots, # current intent slots
'fever', # slot name
'Do you have a fever?' # prompt the answer do you have a fever
)
# delegate means all slot validation are done, we can move to Fulfillment
return delegate(output_session_attributes, slots)
if source == 'FulfillmentCodeHook':
#result = your_api_call(slots['city'], slots['country'])
result = "Your employee id is you answered to having a fever."
return build_response(result) # display the response back to user
def dispatch(intent_request):
intent_name = intent_request['currentIntent']['name']
# Dispatch to your bot's intent handlers
if intent_name == 'covidqs':
return perform_action(intent_request)
raise Exception('Intent with name ' + intent_name + ' not supported')
def lambda_handler(event, context):
#logger.debug(event)
return dispatch(event)
Am getting this error:
An error has occurred: Invalid Lambda Response: Received invalid response from Lambda: Can not construct instance of Message: no String-argument constructor/factory method to deserialize from String value ('Please enter your employee id') at [Source: {"dialogAction": {"type": "ElicitSlot", "intentName": "covidqs", "slots": {"empid": null, "fever": null}, "slotToElicit": "empid", "message": "Please enter your employee id"}}; line: 1, column: 143]
I figured this out. The message response has to be in a specific format. So where it is
"message": message
it should have been
"message":{
"contentType":"PlainText",
"content":message
}

Response class in Flask-RESTplus

What is the proper way to handle response classes in Flask-RESTplus?
I am experimenting with a simple GET request seen below:
i_throughput = api.model('Throughput', {
'date': fields.String,
'value': fields.String
})
i_server = api.model('Server', {
'sessionId': fields.String,
'throughput': fields.Nested(i_throughput)
})
#api.route('/servers')
class Server(Resource):
#api.marshal_with(i_server)
def get(self):
servers = mongo.db.servers.find()
data = []
for x in servers:
data.append(x)
return data
I want to return my data in as part of a response object that looks like this:
{
status: // some boolean value
message: // some custom response message
error: // if there is an error store it here
trace: // if there is some stack trace dump throw it in here
data: // what was retrieved from DB
}
I am new to Python in general and new to Flask/Flask-RESTplus. There is a lot of tutorials out there and information. One of my biggest problems is that I'm not sure what to exactly search for to get the information I need. Also how does this work with marshalling? If anyone can post good documentation or examples of excellent API's, it would be greatly appreciated.
https://blog.miguelgrinberg.com/post/customizing-the-flask-response-class
from flask import Flask, Response, jsonify
app = Flask(__name__)
class CustomResponse(Response):
#classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict):
rv = jsonify(rv)
return super(MyResponse, cls).force_type(rv, environ)
app.response_class = CustomResponse
#app.route('/hello', methods=['GET', 'POST'])
def hello():
return {'status': 200, 'message': 'custom_message',
'error': 'error_message', 'trace': 'trace_message',
'data': 'input_data'}
result
import requests
response = requests.get('http://localhost:5000/hello')
print(response.text)
{
"data": "input_data",
"error": "error_message",
"message": "custom_message",
"status": 200,
"trace": "trace_message"
}

how to pass optional parameters into a function in python?

I have a function which I want to pass two optional parameters. I have read through but somehow I am not able to make it work though.
What am I missing here?
I have something like this
def json_response_message(status, message, option_key, option_value):
data = {
'status': status,
'message': message,
}
if option_key:
data[option_key] = option_value
return JsonResponse(data)
the last two parameters I want it to be optional.
I have seen there it can be done by doing
def json_response_message(status, message, option_key='', option_value=''):
but I don't really want to do it this way and saw that there is a way to pass *args and **kwargs but it couldn't get it to work though.
I am just stuck at putting the optional parameters but not sure how to call and use them. I read through some posts and its' easily be done and call by using a for loop but somehow it just didn't work for me
def json_response_message(status, message, *args, **kwargs):
data = {
'status': status,
'message': message,
}
return JsonResponse(data)
I want add extra parameters into my data return such as...
user = {
'facebook': 'fb',
'instagram': 'ig'
}
return json_response_message(True, 'found', 'user', user)
You want something like this, I suppose:
def json_response_message(status, message, options=()):
data = {
'status': status,
'message': message,
}
# assuming options is now just a dictionary or a sequence of key-value pairs
data.update(options)
return data
And you can use it like this:
user = {
'facebook': 'fb',
'instagram': 'ig'
}
print(json_response_message(True, 'found', user))
def json_response_message(status, message, *args):
#input validation
assert len(args) == 0 or len(args) == 2
# add required params to data
data = {
'status': status,
'message': message,
}
# add optional params to data if provided
if args:
option_key = args[0]
option_value = args[1]
data[option_key] = option_value
return data
print(json_response_message(True, 'found', 'user', user))
{'user': 'jim', 'status': True, 'message': 'found'}

Categories

Resources