# System overview ![](https://i.imgur.com/AA54hAC.jpg) Our system contains several components: Interaction with users, Data persistence and exchange, Data processing for eRFA report generation. For interaction with users, we develop the Alexa Skill and deploy it to Alexa devices to ask questions and receive answers from users of the health questionnaires. For data persistence and exchange, we utilize several AWS products to fulfill our needs: AWS DynamoDB as the database to store user response, AWS Lambda and AWS API Gateway to implement and deploy the data polling API. For the last component in the system, we use Python with ReportLab toolkit to process user responses and generate eRFA pdf reports. # AWS ## ASK The Alexa Skills Kit (ASK) is a software development framework that enables developers to create content, called skills. Skills are applications within Amazon Alexa. With an interactive voice interface, Alexa gives users a hands-free way to interact with your skill. Users can use their voice to perform everyday tasks like checking the news, listening to music, or playing a game. Users can also use their voice to control cloud-connected devices. For example, users can ask Alexa to turn on lights or change the thermostat. Skills are available on Alexa-enabled devices, such as Amazon Echo and Amazon Fire TV, and on Alexa-enabled devices built by other manufacturers. We continue the previous Alexa Skill: Health questionnaire to develop new features for the data pipeline. Since the skill already has the basic function to ask questions and store the user responses in that interaction session, we leverage it to store the user response to the AWS Dynamo database. We designed a JSON format dictionary to persist the user response. At the beginning of the interaction session, we create an answer dictionary to store users' answers. We will keep updating this dictionary when the user answers different questions and saves this answer dictionary to the database simultaneously. With this design, we do not need to wait until the user finishes all questions to save their answers to the database. ## AWS DynamoDB Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale. It's a fully managed, multi-region, multi-active, durable database with built-in security, backup and restore, and in-memory caching for internet-scale applications. DynamoDB can handle more than 10 trillion requests per day and can support peaks of more than 20 million requests per second. ## AWS Lambda AWS Lambda is a serverless compute service that lets developers run code without provisioning or managing servers, creating workload-aware cluster scaling logic, maintaining event integrations, or managing runtimes. With Lambda, developers can run code for virtually any type of application or backend service - all with zero administration. ## AWS API GATEWAY The second step we did was implementing a REST API to help us polling data from the database. A REST API (also known as RESTful API) is an application programming interface (API or web API) that conforms to the constraints of REST architectural style and allows for interaction with RESTful web services. We simplified the functionality of our API in the system, only implementing the data polling feature without other complicated operations to the database. In general, it makes sense for a big system to build an API with a 24/7 non-stop server to keep the stability and with powerful hardware to make sure the speed of calculation. However, it takes extra cost to maintain a virtual server and needs much more effort to set the environment for the API. Instead, we choose to use the solution with AWS Lambda and Amazon API Gateway. As we mentioned before, Lambda provides a serverless environment meaning that we can focus on the feature development of our service without not taking care of the runtime environment. The Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from the backend services. API Gateway supports containerized and serverless workloads, as well as web applications. We utilize this combination to create our data polling API with stability and sustainability. # Step by step Tutorial ### ASK Set up details > [TODO] - [ ] add detailed content steps - [ ] add screenshots It is advised that each developer sets up their own developer console so that they can freely test their changes. - To create your own dev console, make sure that you set up your own Amazon account - Create a new skill in the developer console using the custom template: https://developer.amazon.com/alexa/console/ask - Import the JSON Editor file (this is located in the "build" tab): eRFA_Care_Questionnaire > JsonEditor.json - Click Build Model - Replace the index.js file with the file at (this is located in the "code" tab): eRFA_Care_Questionnaire > lambda > index.js - Within the lambda folder, create a folder called "languages" and in it create a new file called "en-US.json", then copy the content of the en-US.json in this repo into it: eRFA_Care_Questionnaire > lambda > languages > en-US.json - Click Save and then Deploy - Click the Test tab, and enable testing by selecting Development on the dropdown menu at the top - Test your code by typing "open my care questionnaire" in the Alexa Simulator ### AWS permission Set up details > [TODO] - [ ] add detailed content steps - [ ] add screenshots #### Set up permissions To enable your Alexa-hosted skill to use resources in your personal AWS account, create an AWS Identity and Access Management (IAM) role to allow access to the resource from your Alexa-hosted skill. For details about creating AWS IAM roles, see the AWS Identity and Access Management User Guide. #### To get your Alexa skill ARN 1. Open the Alexa developer console and log in. 1. In the console, open your Alexa-hosted skill. 1. In the code editor, click the icon for Link your personal AWS resources. 1. Copy the ARN. ![](https://i.imgur.com/54CLsD6.jpg) #### To create an AWS IAM role 1. Open the AWS management console and log in. 1. In the console, open the Identity and Access Management (IAM) dashboard. 1. In the IAM dashboard, click Roles. 1. On the Roles page, click Create role. 1. On the Create role page, under Select type of trusted entity, select AWS service. 1. In the Choose a use case section, under Common use cases, select one of the common use cases. -Or- Under Or select a service to view its use cases, select one of the services from the list, and then under select your use case, select a use case. For example, select DynamoDB from the list of services, and then select Amazon DynamoDB Accelerator (DAX) - DynamoDB access from the list of use cases. 1. Click Next: Permissions. 1. Choose one or more policies to attach to your new role, and then click Next: Tags. For example, select the AmazonDynamoDBFullAccess policy for full access to your DynamoDB table. 3. Click Next: Review, and enter the name and description of your role. 4. Click Create role. #### To add your Alexa skill ARN to the role 1. In the IAM dashboard, click Roles. 1. On the Roles page, click the name of the role you just created, and then click the Trust relationships tab. 1. Click Edit trust relationship. 1. Add an entry for the AWS Lambda Role Execution ARN from your Alexa-hosted skill to the Statement property and include the sts:AssumeRole action as shown in the following example. Don't overwrite other existing entries in the Statement property. ```json= { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": "<Replace with AWS Lambda Execution Role ARN from Alexa-hosted skill>"}, "Action": "sts:AssumeRole" } ] } ``` 5. Click Update Trust Policy. https://youtu.be/yZDzxB4IT90 {%youtube yZDzxB4IT90 %} ### Use personal AWS resources with Node.js In the code for your Alexa-hosted skill, assume the role by using the AWS Security Token Service (STS) API. For example, the following code requests temporary credentials of a role with AWS DynamoDB access, and scans the DynamoDB table. ```javascript= const AWS = require("aws-sdk"); const ShowUserMessageHandler = { //... Your canHandle function for intent ... async handle(handlerInput) { // 1. Assume the AWS resource role using STS AssumeRole Action const STS = new AWS.STS({ apiVersion: '2011-06-15' }); const credentials = await STS.assumeRole({ RoleArn: '<Your AWS resource role ARN>', RoleSessionName: 'ExampleSkillRoleSession' // You can rename with any name }, (err, res) => { if (err) { console.log('AssumeRole FAILED: ', err); throw new Error('Error while assuming role'); } return res; }).promise(); // 2. Make a new DynamoDB instance with the assumed role credentials // and scan the DynamoDB table const dynamoDB = new AWS.DynamoDB({ apiVersion: '2012-08-10', accessKeyId: credentials.Credentials.AccessKeyId, secretAccessKey: credentials.Credentials.SecretAccessKey, sessionToken: credentials.Credentials.SessionToken }); const tableData = await dynamoDB.scan({ TableName: 'TestTable' }, (err, data) => { if (err) { console.log('Scan FAILED', err); throw new Error('Error while scanning table'); } return data; }).promise(); // ... Use table data as required ... } }; ``` *https://developer.amazon.com/en-US/docs/alexa/hosted-skills/alexa-hosted-skills-personal-aws.html* ### AWS DynamoDB Set up details > [TODO] - [ ] add detailed content steps - [ ] add screenshots *https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStartedDynamoDB.html *https://developer.amazon.com/en-US/docs/alexa/hosted-skills/alexa-hosted-skills-session-persistence.html* ### AWS Lambda / API Gateway Set up details > [TODO] - [ ] add detailed content steps - [ ] add screenshots *https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html* # Python Script The last step of the data pipeline system is to process the user data and generate eRFA report. We use Python with an open-sourced document creation engine ReportLab to help to generate the file in PDF format. We follow the format of the eRFA report provided by Dr. Shahrokni, which gives us the report creation and data processing standard. For the data processing, we would get the data from the database and calculate the score of each question. Different scores will apply to different answers to the question. For example, for the Activities of Daily Living questions (ADL), we will give 2 points to the answer "not limited 1," point to "limited a little," and 0 to "limited a lot." After calculating all the answers, we can create an "Impairment Summary" table based on these scores as shown in Table1. This summary table presents the assessment result of the health questionnaire. In the report, we also provide detailed answers from the user of each question. ## Prerequisite ```bash= pip install requests pip install reportlab ``` ## Usage ```bash= python generater.py ``` ## Code Please update the data inside the {} to your own api url/parameters/user IDs ```python import requests import json import reportlab # get user data from api url = "{your api url}" para = {'operation': 'read'} res = requests.post(url, data=json.dumps(para), headers={'Content-Type': 'application/json'}) raw_data = res.json() users = raw_data['Items'] # specify userid userID = 'your user ID' userRecord = None for user in users: if user['userId']['S'] == userID: userRecord = user["response"]['M'] userdate = user["timestamp"] break ADL = userRecord['ADL']['M'] iADL = userRecord['iADL']['M'] # process user data by table # ADL = None # IADL = None SOCIAL = None SOCIAL_ACTIVITY = None FALL = None WHEN_FALL = None WHERE_FALL = None VISION = None def calcuate_adl_score(records): score = 0 limited_a_lot = [] limited_a_little = [] not_limited = [] for i in records: activity = i activity_dict = records[i]['M'] answer = activity_dict['answer']['S'] time = activity_dict['time'] if answer == "limited a lot": score += 0 limited_a_lot.append(activity) elif answer == "limited a little": score += 1 limited_a_little.append(activity) elif answer == "not limited": score += 2 not_limited.append(activity) return score,{'limited a lot':limited_a_lot,'limited a little':limited_a_little,'not limited':not_limited} def calculate_iadl_score(records): score = 0 unable_to_do = [] with_some_help = [] without_help = [] for i in records: activity = i activity_dict = records[i]['M'] answer = activity_dict['answer']['S'] time = activity_dict['time'] if answer == "unable to do": score += 0 unable_to_do.append(activity) elif answer == "with some help": score += 1 with_some_help.append(activity) elif answer == "without help": score += 2 without_help.append(activity) return score, {"unable to do":unable_to_do, "with some help":with_some_help,"without help":without_help } def calculate_social_score(records): score = 0 return score def calculate_social_activity_score(records): score = 0 return score ADL_score, ADL_activity = calcuate_adl_score(ADL) iADL_score, iADL_activity = calculate_iadl_score(iADL) # generate pdf from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import SimpleDocTemplate, Paragraph, TableStyle, Table report = [] # styles = getSampleStyleSheet() styleNormalCustom = ParagraphStyle( 'styleNormalCustom', parent=styles["Heading1"], fontSize=18, alignment=TA_CENTER, ) styleNormalCustomSmall = ParagraphStyle( 'styleNormalCustom', parent=styles["Heading2"], fontSize=11, alignment=TA_CENTER, ) # pdfmetrics.registerFont(TTFont('kaiu', "font/kaiu.ttf")) fileName = "report.pdf" pdfTemplate = SimpleDocTemplate(fileName) sample_style_sheet = getSampleStyleSheet() # p1 PATIENT paragraph_1 = Paragraph("PATIENT", sample_style_sheet['Heading1']) report.append(paragraph_1) # p1-1 name = userID age = "Not answered" date = userdate['S'] paragraph_1_1 = Paragraph("<b>{} <br/> Age:{} {} Survey Date:{}</b>".format(name, age, "&nbsp;" * 10, date)) report.append(paragraph_1_1) # p2 PATIENT UNANSWERED QUESTIONS paragraph_2 = Paragraph("<br/>PATIENT UNANSWERED QUESTIONS", sample_style_sheet['Heading1']) paragraph_2_1 = Paragraph("<br/>") report.append(paragraph_2) report.append(paragraph_2_1) # p3 ERFA SUMMARY paragraph_3 = Paragraph("ERFA SUMMARY", sample_style_sheet['Heading1']) paragraph_3_1 = Paragraph("OVERVIEW", styleNormalCustom) #TODO: calculate abnormal num_impair = 7 abnormals = ["ADL", "iADL", "TUG Test", "Social Support", "Limited Social Activity", "Distress Level", "Medications", ] over_str = "Patient has {} impairments with abnormal scores in: ".format(num_impair) for idx, ab in enumerate(abnormals): over_str += ab if idx < len(abnormals) - 1: over_str += "," else: over_str += "." paragraph_3_2 = Paragraph(over_str) paragraph_3_3 = Paragraph("<br/> Scoring for MiniCog couldn't be calculated due to unanswered questions. Consider " "addressing these questions.") paragraph_3_4 = Paragraph("<br/>") paragraph_3_5 = Paragraph("IMPAIRMENT SUMMARY", styleNormalCustom) paragraph_3_6 = Paragraph("<b>Global recommendation</b>", styleNormalCustomSmall) paragraph_3_7 = Paragraph("If 3 or more impairments are listed in the summary " "below, consider referral to geriatrics.<br/>*”% Below Threshold” refers to the proportion " "of patients who score below the threshold value.") paragraph_3_8 = Paragraph("<br/><br/>") tableStyle = TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('GRID', (0, 0), (-1, -1), 0.5, colors.white), ('FONTNAME', (0, 0), (1, -1), 'Helvetica-Bold'), ('FONTNAME', (0, 0), (3, 0), 'Helvetica-Bold') ]) dataList = [ ["", "Patient Score", "Impairment Threshold", "% Below Threshold"], ["ADL", "{}".format(ADL_score), "< 14", "52%"], ["iADL", "{}".format(iADL_score), "< 16", "45%"], ["Timed Up and Go (TUG) Test", "", ">= 10 seconds", "36%"], ["Social Support", "", "<= 16", "43%"], ["Limited Social Activity", "", ">= 8", "50%"], ["Distress Level", "", ">= 4", "55%"], ["Number of Medications", "", "", ""], ] table = Table(dataList, style=tableStyle) report.append(paragraph_3) report.append(paragraph_3_1) report.append(paragraph_3_2) report.append(paragraph_3_3) report.append(paragraph_3_4) report.append(paragraph_3_5) report.append(paragraph_3_6) report.append(paragraph_3_7) report.append(paragraph_3_8) report.append(table) # p4 COGNITIVE AND MOBILITY SCREENING paragraph_4 = Paragraph("<br/>COGNITIVE AND MOBILITY SCREENING", sample_style_sheet['Heading1']) com_by = "Not answered" clock_draw = "Not answered" recall_word = "Not answered" getupandgo = "Not answered" paragraph_4_des = Paragraph( "<b>Questionnaire completed by:</b> {} <br/><b>ClockDrawing:</b>{} <br/><b>Recalled Words:</b> {} <br/><b> Get up and Go Test Results:</b> {}".format( com_by, clock_draw, recall_word, getupandgo)) # p4-1 SOCIO-DEMOGRAPHIC paragraph_4_1 = Paragraph("SOCIO-DEMOGRAPHIC", sample_style_sheet['Heading2']) martial_status = "Not answered" education = "Not answered" living = "Not answered" smoke = "Not answered" alcohol = "Not answered" paragraph_4_1_1 = Paragraph( "<b>Marital status: </b>{}<br/><b>Highest level of education: </b>{}<br/><b>Living situation: </b>{}<br/><b>Smoker status (100 cig+):</b> {}<br/><b>Alcohol or drug use:</b> {}".format( martial_status, education, living, smoke, alcohol)) # p4-2 PERFORMANCE/FUNCTIONAL STATUS paragraph_4_2 = Paragraph("PERFORMANCE/FUNCTIONAL STATUS", sample_style_sheet['Heading2']) function_status = "Not answered" kps = "Not answered" home_care = "Not answered" indent_1 = "&nbsp;" * 10 not_limited = "Not answered" for s in ADL_activity: if s == "not limited": act = ADL_activity[s] act_s = "&nbsp;" * 10 for i,a in enumerate(act): if i < len(act)-1: act_s += a act_s += ";" not_limited = act_s indent_2 = "&nbsp;" * 10 limited_a_little = "Not answered" for s in ADL_activity: if s == "limited a little": act = ADL_activity[s] act_s = "&nbsp;" * 10 for i,a in enumerate(act): if i < len(act)-1: act_s += a act_s += ";" limited_a_little = act_s indent_3 = "&nbsp;" * 10 if ADL_score: adl_score = ADL_score else: adl_score = "Not answered" indent_4 = "&nbsp;" * 10 without_help = "Not answered" for s in iADL_activity: if s == "without help": act = iADL_activity[s] act_s = "&nbsp;" * 10 for i,a in enumerate(act): if i < len(act)-1: act_s += a act_s += ";" without_help = act_s indent_5 = "&nbsp;" * 10 with_some_help = "Not answered" for s in iADL_activity: if s == "with some help": act = iADL_activity[s] act_s = "&nbsp;" * 10 for i,a in enumerate(act): if i < len(act)-1: act_s += a act_s += ";" with_some_help = act_s indent_6 = "&nbsp;" * 10 if iADL_score: iadl_score = iADL_score else: iadl_score = "Not answered" ever_fallen = "Not answered" devices = "Not answered" vision = "Not answered" hearing = "Not answered" paragraph_4_2_1 = Paragraph("<b>Functional status: </b>{}<br/><b>KPS: </b>{}<br/><b>Home care " "service: </b>{}<br/><b>Activities limited by health:</b><br/>{}<b>Not " "limited: </b>{}<br/>{}<b>Limited a little: </b>{}<br/>{}<b>Total “ADL” " "Score: </b>{}<br/><b>Able to:</b><br/>{}<b>Without help: </b>{}<br/>{" "}<b>With some help: </b>{}<br/>{}<b>Total “iADL” Score: </b>{" "}<br/><b>Have you ever fallen: </b>{}<br/><b>Devices: </b>{" "}<br/><b>Vision: </b>{}<br/><b>Hearing: </b>{}".format(function_status, kps, home_care, indent_1, not_limited, indent_2, limited_a_little, indent_3, adl_score, indent_4, without_help, indent_5, with_some_help, indent_6, iadl_score, ever_fallen, devices, vision, hearing )) # 4-3 SOCIAL SUPPORT paragraph_4_3 = Paragraph("SOCIAL SUPPORT", sample_style_sheet['Heading2']) indent_1 = "&nbsp;" * 10 chores = "Not answered" indent_2 = "&nbsp;" * 10 personal = "Not answered" indent_3 = "&nbsp;" * 10 enjoyable = "Not answered" indent_4 = "&nbsp;" * 10 wanted = "Not answered" indent_5 = "&nbsp;" * 10 social_support_score = "Not answered" paragraph_4_3_1 = Paragraph("<b>How often do you have someone:</b><br/>{}<b>To help with chores when sick: " "</b>{}<br/>{}<b>To turn to with a personal problem: </b>{}<br/>{}<b>To do " "something enjoyable with: </b>{}<br/>{}<b>To love and make feel wanted: </b>{" "}<br/>{}<b>Total “Social Support” Score: </b>{}".format(indent_1, chores, indent_2, personal, indent_3, enjoyable, indent_4, wanted, indent_5, social_support_score)) # 4-4 SOCIAL ACTIVITY paragraph_4_4 = Paragraph("SOCIAL ACTIVITY", sample_style_sheet['Heading2']) physical_effects = "Not answered" indent_1 = "&nbsp;" * 10 social_activities = "Not answered" indent_2 = "&nbsp;" * 10 past_6_month = "Not answered" indent_3 = "&nbsp;" * 10 more_less_limite = "Not answered" indent_4 = "&nbsp;" * 10 social_activity_score = "Not answered" paragraph_4_4_1 = Paragraph("<b>Physical and Emotional Health Effects: </b>{}<br/><b>Interfered with social " "activities (past 4 weeks): </b><br/>{}<b>Interfered with your social activities: " "</b>{}<br/>{}<b>Social activity during the past 6 months: </b>{}<br/>{}<b>Your " "social activities more or less limited: </b>{}<br/>{}<b>Total “Interference with " "Social Activity” Score: </b>{}".format(physical_effects, indent_1, social_activities, indent_2, past_6_month, indent_3, more_less_limite, indent_4, social_activity_score)) # 4-5 NUTRITION STATUS paragraph_4_5 = Paragraph("NUTRITION STATUS", sample_style_sheet['Heading2']) weight_change = "Not answered" emotion = "Not answered" ros = "Not answered" admit = "Not answered" denie = "Not answered" depression_score = "Not answered" paragraph_4_5_1 = Paragraph("<b>Weight change over past 6 months: </b>{}<br/><b>EMOTIONAL STATUS: </b>{" "}<br/><b>ROS_PSYS_Distress: (0 - No distress;10 - Extreme distress)</b>{" "}<br/><b>Patient admits: </b>{}<br/><b>Patient denies: </b>{" "}<br/><b>Total “Depression Scale” Score: </b>{}".format(weight_change, emotion, ros, admit, denie, depression_score)) # 4-6 MEDICATIONS paragraph_4_6 = Paragraph("MEDICATIONS", sample_style_sheet['Heading2']) prescribed_medications = "Not answered" indent_1 = "&nbsp;" * 10 number_of_medications = "Not answered" herbs_vitamins_supplements = "Not answered" indent_2 = "&nbsp;" * 10 number_of_herbs_vitamins_supplements = "Not answered" paragraph_4_6_1 = Paragraph("<b>Taking prescribed medications: </b>{}<br/>{}<b>Number of medications per " "day: </b>{}<br/><b>Taking herbs, vitamins, and supplements: </b>{}<br/>{" "}<b>Number of herbs, vitamins, and supplements (per day): </b>{}".format( prescribed_medications, indent_1, number_of_medications, herbs_vitamins_supplements, indent_2, number_of_herbs_vitamins_supplements)) # 4-7 ADDITIONAL SMOKING HISTORY paragraph_4_7 = Paragraph("ADDITIONAL SMOKING HISTORY", sample_style_sheet['Heading2']) isCig = "No" paragraph_4_7_1 = Paragraph("<b>Smoked at least 100 cigarettes in lifetime:</b> {}".format(isCig)) #append all paragraph report.append(paragraph_4) report.append(paragraph_4_des) report.append(paragraph_4_1) report.append(paragraph_4_1_1) report.append(paragraph_4_2) report.append(paragraph_4_2_1) report.append(paragraph_4_3) report.append(paragraph_4_3_1) report.append(paragraph_4_4) report.append(paragraph_4_4_1) report.append(paragraph_4_5) report.append(paragraph_4_5_1) report.append(paragraph_4_6) report.append(paragraph_4_6_1) report.append(paragraph_4_7) report.append(paragraph_4_7_1) pdfTemplate.build(report) ```