├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── ask ├── __init__.py ├── alexa_io.py ├── config │ ├── __init__.py │ └── config.py ├── data │ ├── README │ └── amazon_builtin_slots.tsv ├── intent_schema.py └── write_sample.py ├── examples ├── README ├── basic │ ├── lambda_function.py │ └── speech_assets │ │ ├── sample_intent_schema.json │ │ └── utterances.txt ├── twitter │ ├── config.py │ ├── lambda_function.py │ ├── speech_assets │ │ ├── custom_slots │ │ │ ├── ORDINAL.list │ │ │ └── TWITTER_MESSAGE.list │ │ ├── intent_schema.json │ │ └── utterances.txt │ └── twitter.py └── useful_science │ ├── lambda_function.py │ ├── speech_assets │ ├── custom_slots │ │ └── Categories.list │ ├── intent_schema.json │ └── utterances.txt │ └── useful_science.py ├── lambda_function.py ├── setup.cfg ├── setup.py └── tests ├── README ├── __init__.py ├── context.py ├── fixtures ├── __init__.py └── requests.py ├── test-data └── sample_request.json ├── test_alexa.py ├── test_request.py └── test_response_builder.py /.gitignore: -------------------------------------------------------------------------------- 1 | #Ignore keys (Essential!) 2 | keys/* 3 | *.pyc 4 | */*.pyc 5 | !keys/README -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anjishnu Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ask-alexa-pykit 2 | 3 | Release Version : Master (Unstable! For a stable release, check out the 0.5 branch) 4 | 5 | ask-alexa-pykit 0.5 is out! 6 | 7 | A minimalist framework for developing apps (skills) for the Amazon Echo's SDK: The Alexa Skills Kit (ASK). 8 | 9 | Projects that use this library 10 | -------------- 11 | 12 | - Rap Battle Alexa - http://devpost.com/software/rapbattlealexa 13 | - Twitter Reader (Official Twitter Skill for Alexa) 14 | - How much is it worth - https://www.hackster.io/minus-et-cortex/how-much-it-worth-07e190 15 | - Useful Science - https://github.com/anjishnu/ask-useful-science 16 | - University of Pennsylvania, Deep Learning Methods for Automated Discourse - (http://dialog-systems-class.org/assignment1.html) 17 | - Phillipe Loher's Tutorial: https://mldavidson.phillipe.com/wp-content/uploads/2016/11/ml-alexa.pptx 18 | 19 | Other Github Projects 20 | - https://github.com/kfowlks/aws_alexa_comcast_data_usage_lambda 21 | - https://github.com/btelle/poolmon 22 | - https://github.com/mijdavis2/ask-shrugs 23 | - https://github.com/geoaxis/ask-sweden 24 | - https://github.com/cameron-gagnon/CookmATE 25 | - https://github.com/ysak-y/alexa-sqs-home-manager 26 | - https://github.com/ysak-y/alexa-irkit 27 | - https://github.com/mastash3ff/Alexa-IsStreamable 28 | - https://github.com/peterpanning/AlexaTransit 29 | - https://github.com/ysak-y/yeelight-with-alexa 30 | - https://github.com/SydneyDockerMeetupAWS/meetup-code 31 | - https://github.com/kdietze3/alexa-binary-clock 32 | - https://github.com/hassanshamim/thebaker 33 | - https://github.com/btelle/alexa-boardgame-answers 34 | - https://github.com/amphy/alexa-league 35 | - https://github.com/potykion/empatika-internship 36 | - https://github.com/mirkoprescha/alexa-my-reminders 37 | - https://github.com/kdietze3/alexa-transaction-or-no-transaction 38 | - https://github.com/pombredanne/autopy-1 39 | - https://github.com/ktseytlin/AlexaComplimentMe 40 | - https://github.com/j-c-h-e-n-g/alexa-ramdass 41 | - https://github.com/bugbiteme/EMC-NorCal-SE-AlexaChallenge-2016 42 | - https://github.com/daphnei/nn_chatbot 43 | - https://github.com/ysak-y/alexa-doc 44 | - https://github.com/ckeitt/alexa-genius 45 | - https://github.com/amandarice/AnimalTranslator 46 | - https://github.com/armandofox/alexa-marantz-py 47 | - https://github.com/vlnk/alexa-meets-dmn 48 | - https://github.com/amandarice/CodeComments 49 | 50 | 51 | If this library helps you build some dialog systems or do some interesting research - please remember to cite it! 52 | 53 | ``` 54 | @Misc{kumarask2015, 55 | author = {Anjishnu Kumar}, 56 | title = {ASK Alexa PyKit}, 57 | howpublished = {\url{github.com/anjishnu/ask-alexa-pykit}}, 58 | year = {2015} 59 | } 60 | ``` 61 | Let me know if you know any other projects that use or build on top of ask-alexa-pykit. 62 | 63 | 64 | What does this library do? 65 | ---------------- 66 | - Remove boiler plate from Alexa Skills Kit Code - maps intents directly to their handler functions. 67 | - Provide utils to quickly and effectively generate and manipulate sample utterances and the intent schema. 68 | - Provides python objects to quickly build alexa responses. 69 | - Automatic session management using session variables - your code gets access to a really simple interface for session management, you just add key value pairs to, or delete things from, a python dictionary, and this library along with the ASK platform takes care of the rest. 70 | 71 | To use this code for your own skill, simply generate training data, and an intent schema definition and edit lambda_function.py to add handler functions for the intents and requests that your skill supports - this should be enough to get started. 72 | 73 | Note: Unless someone asks me to reconsider - I am now only going to do further releases of this library for AWS Lambda - the core library is concise enough that it can be included into any python server framework with just a few imports. The old releases (for cherrypy) will contain the infuriating request validation parts of the library which can be useful for people who don't want to use the Lambda or LambdaProxy approach to skill development. 74 | 75 | 76 | # What's new? 77 | 78 | Latest changes: 79 | 80 | - There's a pypi repo now https://pypi.python.org/pypi/ask-alexa-pykit/ - so you should be able to do `pip install ask-alexa-pykit` to use 'ask' as a standard python library or `pip install ask-alexa-pykit --target new_skill_folder` to install it into a directory (which will be your AWS Lambda Function directory). 81 | 82 | - Added an actual intent_schema.py module - thin wrapper around the JSON object which allows for easy manipulation, creation and serialization/deserialization. This module also doubles as the generate intent schema script now, with hooks to interactively generate the object. 83 | 84 | - The scripts folder is gone now - and the scripts themselves have been moved into the main alexa.ask module, which means that they can stay in sync with the Intent Schema and other config parameters without much fuss. 85 | 86 | - The annotation API has changed (become simpler) and the intent map computation and routing now happens under the hood. As of version 0.4 the VoiceHandler object now maintains an internal mapping of handler functions and now takes on the responsibility of handing off incoming requests to the right function end point. 87 | 88 | - Now there's only one module that a user has to be aware of. We've fully factored out the alexa specific bits into the ask library and you don't need to see how the mappings are computed. 89 | 90 | - The Request class got a minor upgrade - the addition of a 'metadata' field, which allows the developer to easily extend code to inject session, user or device specific metadata (after, for instance, querying a database) into a request object before it gets passed to the annotated handler functions. 91 | 92 | - The interface to the ask library function is now uniformly exposed to developers. A voice handler is now a subclass of a ResponseBuilder so that as a user all your really need to do is `from ask import alexa` 93 | 94 | - Improved session handling - no need to pass back the session attributes - just edit them in the dict and they'll automatically get propogated. 95 | 96 | - Python 2/3 dual compatibility 97 | 98 | Basic overview of Classes: 99 | 100 | - The Request object contains information about the Alexa request - such as intent, slots, userId etc. 101 | 102 | - A VoiceHandler is an object that internally stores a mapping from intents and requests to their corresponding handler functions. These mappings are specified by a simple annotation scheme (see lambda_function.py for an example) 103 | 104 | - An alexa (VoiceHandler) annotated class (specified with an annotation) takes a request as an input, performs some arbitrary logic on top of it, and returns a Response. 105 | 106 | - The ResponseBuilder is an encapsulated way to construct responses for a VoiceHandler. A Response can be constructed by called ResponseBuilder.create_response. 107 | 108 | 109 | Step 1: Download Code 110 | ----------- 111 | 112 | Method 1: 113 | 114 | $ git clone https://github.com/anjishnu/ask-alexa-pykit.git 115 | 116 | Make sure you're in a python lambda release branch. E.g 117 | 118 | 119 | $ cd ask-alexa-pykit 120 |
121 | $ git checkout python_lambda_0.5_release
122 |
123 | Otherwise your build my be broken since the master branch is not always stable. 124 |
125 | Method 2: 126 |
127 | 128 | $ mkdir my_new_skill 129 |
130 | $ pip install ask-alexa-pykit --target my_new_skill 131 |
132 |
133 | 134 | ask-alexa-pykit is now installed in your my_new_skill directory. Just import the library and start hacking. 135 | 136 | Step 2: Create a intent schema for your app 137 | ---------- 138 | Skip this if you're trying the included basic example and use sample_intent_schema.json as your INTENT_SCHEMA. 139 | 140 |
141 | $ python -m ask.intent_schema -i FILEPATH 142 | 143 | 144 | This script takes you through the process of generating an intent schema for your app- which defines how Alexa's language understanding system interprets results. 145 | After the process is complete, it asks you whether you the intent schema stored at the appropriate location. 146 | 147 | Step 3: Generate training data and upload to Amazon. 148 | -------------- 149 | 150 | 3(a): 151 | Create a file containing your training examples and upload to Amazon. 152 | I've created a script which loads in the intent schema and does some validation and prompting while you type utterances, but I haven't played around with it enough to know if it actually helps. 153 |
154 | $ python -m ask.write_sample -i INTENT_SCHEMA -o TRAINING_DATA_OUTPUT_LOCATION 155 |
156 | This script prompts you to enter valid training data in the format defined by the ASK (https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/defining-the-voice-interface). You toggle through the different intents by pressing enter with blank input. Play around with it and see if you find it intuitive. 157 | 158 | 3(b): 159 | Once you are done, this script generates a file called utterance.txt with all your training data in it, ready to be uploaded to your skill: https://developer.amazon.com/edw/home.html#/skills 160 | 161 | Step 4: Add your business logic 162 | -------------- 163 | 164 | Skip this if you're just trying to run the included basic example. 165 | 166 | Go to lambda_function.py and add handler functions to the code for your specific request or intent. 167 | This is what a handler function for NextRecipeIntent looks like. 168 | 169 | @alexa.intent("NextRecipeIntent") 170 | def next_recipe_intent_handler(request): 171 | """ 172 | You can insert arbitrary business logic code here 173 | """ 174 | return alexa.create_response("Getting Next Recipe ...") 175 | 176 | Step 5: Package your code for Lambda 177 | ---------------- 178 | 179 | Package the code folder for AWS Lambda. Detailed instructions are here: http://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html 180 | 181 | For the basic example included in the package simply zip the folder: 182 | 183 |
184 | $ cd ask-alexa-pykit 185 |
186 | $ zip -r ask-lambda.zip * 187 |
188 | 189 | Step 6: Create a Lambda Function 190 | ----- 191 | - Go to console.aws.amazon.com 192 | - Click on Lambda 193 | - Select [Virgina] on the top right. (ASK source is only available in Virginia) 194 | - Click on Create Lambda Function 195 | - Skip the Select Blueprint Section 196 | - In the configure step: choose a name e.g. alexa-pykit-demo 197 | - Choose Runtime as Python 2.7 198 | - Code Entry Type - Upload a zip file. 199 | - Upload ask-lambda.zip 200 | - For Role : create a new basic execution role 201 | - Press Next, and then Create Function 202 | - In the event source configuration pick event source type - Alexa Skills Kit. 203 | 204 | Step 7: Associate Lambda Function with Alexa Skill 205 | ------ 206 | Add the ARN code for the Lambda Function you've just created as the Lambda ARN (Amazon Resource Name) Endpoint in the Skill Information tab of your skill's information on https://developer.amazon.com/edw/home.html#/skills/list 207 | 208 | Note an ARN code is at the top of you Lambda page and starts with something like: arn:aws:lambda:us... 209 | 210 | 211 | Contributing 212 | --------------- 213 | 214 | - The master branch is meant to be stable. I usually work on unstable stuff on a personal branch. 215 | - Fork the master branch ( https://github.com/[my-github-username]/ask-alexa-pykit/fork ) 216 | - Create your branch (git checkout -b my-branch) 217 | - Commit your changes (git commit -am 'added fixes for something') 218 | - Push to the branch (git push origin my-branch) 219 | - Create a new Pull Request 220 | - And you're done! 221 | 222 | - Bug fixes, bug reports and new documentation are all appreciated! 223 | 224 | Credits: Anjishnu Kumar 2015 225 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjishnu/ask-alexa-pykit/a47c278ca7a60532bbe1a9b789f6c37e609fea8b/__init__.py -------------------------------------------------------------------------------- /ask/__init__.py: -------------------------------------------------------------------------------- 1 | from . import alexa_io 2 | 3 | ''' 4 | Setting up some nice abstractions around good object oritented code. 5 | ''' 6 | 7 | ResponseBuilder = alexa_io.ResponseBuilder 8 | alexa = alexa_io.VoiceHandler() 9 | Request = alexa_io.Request 10 | Response = alexa_io.Response 11 | -------------------------------------------------------------------------------- /ask/alexa_io.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | RAW_RESPONSE = """ 4 | { 5 | "version": "1.0", 6 | "response": { 7 | "outputSpeech": { 8 | "type": "PlainText", 9 | "text": "Some default text goes here." 10 | }, 11 | "shouldEndSession": False 12 | } 13 | }""" 14 | 15 | 16 | class Request(object): 17 | """ 18 | Simple wrapper around the JSON request 19 | received by the module 20 | """ 21 | def __init__(self, request_dict, metadata=None): 22 | self.request = request_dict 23 | self.metadata = metadata or {} 24 | self.session = self.request.get('session', {}).get('attributes', {}) 25 | if self.intent_name(): 26 | self.slots = self.get_slot_map() 27 | 28 | def request_type(self): 29 | return self.request["request"]["type"] 30 | 31 | def intent_name(self): 32 | if "intent" not in self.request["request"]: 33 | return None 34 | return self.request["request"]["intent"]["name"] 35 | 36 | def is_intent(self): 37 | if self.intent_name() is None: 38 | return False 39 | return True 40 | 41 | def user_id(self): 42 | return self.request["session"]["user"]["userId"] 43 | 44 | def access_token(self): 45 | try: 46 | return self.request['session']['user']['accessToken'] 47 | except: 48 | return None 49 | 50 | def session_id(self): 51 | return self.request["session"]["sessionId"] 52 | 53 | def get_slot_value(self, slot_name): 54 | try: 55 | return self.request["request"]["intent"]["slots"][slot_name]["value"] 56 | except: 57 | """Value not found""" 58 | return None 59 | 60 | def get_slot_names(self): 61 | try: 62 | return self.request['request']['intent']['slots'].keys() 63 | except: 64 | return [] 65 | 66 | def get_slot_map(self): 67 | return {slot_name: self.get_slot_value(slot_name) 68 | for slot_name in self.get_slot_names()} 69 | 70 | 71 | class Response(object): 72 | def __init__(self, json_obj): 73 | self.json_obj = json_obj 74 | 75 | def __repr__(self): 76 | return json.dumps(self.json_obj, indent=4) 77 | 78 | def with_card(self, title, content=None, subtitle=None, card_type='Simple'): 79 | new_obj = dict(self.json_obj) 80 | new_obj['response']['card'] = ResponseBuilder.create_card(title, content, 81 | subtitle, card_type) 82 | return Response(new_obj) 83 | 84 | def with_reprompt(self, message, is_ssml): 85 | new_obj = dict(self.json_obj) 86 | new_obj['response']['reprompt'] = ResponseBuilder.create_speech(message, is_ssml) 87 | return Response(new_obj) 88 | 89 | def set_session(self, session_attr): 90 | self.json_obj['sessionAttributes'] = session_attr 91 | 92 | def to_json(self): 93 | return dict(self.json_obj) 94 | 95 | 96 | class ResponseBuilder(object): 97 | """ 98 | Simple class to help users to build responses 99 | """ 100 | base_response = eval(RAW_RESPONSE) 101 | 102 | @classmethod 103 | def create_response(self, message=None, end_session=False, card_obj=None, 104 | reprompt_message=None, is_ssml=None): 105 | """ 106 | message - text message to be spoken out by the Echo 107 | end_session - flag to determine whether this interaction should end the session 108 | card_obj = JSON card object to substitute the 'card' field in the raw_response 109 | """ 110 | response = dict(self.base_response) 111 | if message: 112 | response['response'] = self.create_speech(message, is_ssml) 113 | response['response']['shouldEndSession'] = end_session 114 | if card_obj: 115 | response['response']['card'] = card_obj 116 | if reprompt_message: 117 | response['response']['reprompt'] = self.create_speech(reprompt_message, is_ssml) 118 | return Response(response) 119 | 120 | @classmethod 121 | def respond(self, *args, **kwargs): 122 | return self.create_response(*args, **kwargs) 123 | 124 | @classmethod 125 | def create_speech(self, message=None, is_ssml=False): 126 | data = {} 127 | if is_ssml: 128 | data['type'], data['ssml'] = "SSML", message 129 | else: 130 | data['type'] = "PlainText" 131 | data['text'] = message 132 | return {"outputSpeech": data} 133 | 134 | @classmethod 135 | def create_card(self, title=None, subtitle=None, content=None, card_type="Simple"): 136 | """ 137 | card_obj = JSON card object to substitute the 'card' field in the raw_response 138 | format: 139 | { 140 | "type": "Simple", #COMPULSORY 141 | "title": "string", #OPTIONAL 142 | "subtitle": "string", #OPTIONAL 143 | "content": "string" #OPTIONAL 144 | } 145 | """ 146 | card = {"type": card_type} 147 | if title: card["title"] = title 148 | if subtitle: card["subtitle"] = subtitle 149 | if content: card["content"] = content 150 | return card 151 | 152 | 153 | class VoiceHandler(ResponseBuilder): 154 | """ Decorator to store function metadata 155 | Functions that are annotated with this label are 156 | treated as voice handlers """ 157 | 158 | def __init__(self): 159 | """ 160 | >>> alexa = VoiceHandler() 161 | >>> request = 162 | >>> @alexa.intent('HelloWorldIntent') 163 | ... def hello_world(request): 164 | ... return alexa.create_response('hello world') 165 | >>> alexa.route_request(request) 166 | """ 167 | self._handlers = { "IntentRequest" : {} } 168 | self._default = '_default_' 169 | 170 | def default(self, func): 171 | ''' Decorator to register default handler ''' 172 | 173 | self._handlers[self._default] = func 174 | 175 | return func 176 | 177 | def intent(self, intent): 178 | ''' Decorator to register intent handler''' 179 | 180 | def _handler(func): 181 | self._handlers['IntentRequest'][intent] = func 182 | return func 183 | 184 | return _handler 185 | 186 | def request(self, request_type): 187 | ''' Decorator to register generic request handler ''' 188 | 189 | def _handler(func): 190 | self._handlers[request_type] = func 191 | return func 192 | 193 | return _handler 194 | 195 | def route_request(self, request_json, metadata=None): 196 | 197 | ''' Route the request object to the right handler function ''' 198 | request = Request(request_json) 199 | request.metadata = metadata 200 | # add reprompt handler or some such for default? 201 | handler_fn = self._handlers[self._default] # Set default handling for noisy requests 202 | 203 | if not request.is_intent() and (request.request_type() in self._handlers): 204 | ''' Route request to a non intent handler ''' 205 | handler_fn = self._handlers[request.request_type()] 206 | 207 | elif request.is_intent() and request.intent_name() in self._handlers['IntentRequest']: 208 | ''' Route to right intent handler ''' 209 | handler_fn = self._handlers['IntentRequest'][request.intent_name()] 210 | 211 | response = handler_fn(request) 212 | response.set_session(request.session) 213 | return response.to_json() 214 | -------------------------------------------------------------------------------- /ask/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjishnu/ask-alexa-pykit/a47c278ca7a60532bbe1a9b789f6c37e609fea8b/ask/config/__init__.py -------------------------------------------------------------------------------- /ask/config/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the basic config file, encapsulating all configuration options 3 | ALL FILES SHOULD LOAD THEIR CONFIGURATIONS FROM THIS CENTRAL LOCATION 4 | """ 5 | from __future__ import print_function 6 | import os 7 | import json 8 | 9 | # ---- Helper Functions ---- 10 | 11 | # Get path relative to the current file 12 | path_relative_to_file = lambda rel_path: os.path.normpath(os.path.join(os.path.dirname(__file__), rel_path)) 13 | 14 | # Load a json file as an object 15 | load_json_schema = lambda schema_location : json.load(open(schema_location)) 16 | 17 | 18 | def read_from_user(input_type, *args, **kwargs): 19 | ''' 20 | Helper function to prompt user for input of a specific type 21 | e.g. float, str, int 22 | Designed to work with both python 2 and 3 23 | Yes I know this is ugly. 24 | ''' 25 | 26 | def _read_in(*args, **kwargs): 27 | while True: 28 | try: tmp = raw_input(*args, **kwargs) 29 | except NameError: tmp = input(*args, **kwargs) 30 | try: return input_type(tmp) 31 | except: print ('Expected type', input_type) 32 | 33 | return _read_in(*args, **kwargs) 34 | 35 | # Location of AMAZON.BUILTIN slot types 36 | BUILTIN_SLOTS_LOCATION = path_relative_to_file(os.path.join('..', 'data', 'amazon_builtin_slots.tsv')) 37 | 38 | def load_builtin_slots(): 39 | ''' 40 | Helper function to load builtin slots from the data location 41 | ''' 42 | builtin_slots = {} 43 | for index, line in enumerate(open(BUILTIN_SLOTS_LOCATION)): 44 | o = line.strip().split('\t') 45 | builtin_slots[index] = {'name' : o[0], 46 | 'description' : o[1] } 47 | return builtin_slots 48 | 49 | -------------------------------------------------------------------------------- /ask/data/README: -------------------------------------------------------------------------------- 1 | All non python data files go in this directory -------------------------------------------------------------------------------- /ask/data/amazon_builtin_slots.tsv: -------------------------------------------------------------------------------- 1 | AMAZON.LITERAL Passes the words for the slot value with no conversion, bad accuracy and poor generalization 2 | AMAZON.NUMBER Converts numeric words (five) into digits (such as 5) and optimizes Alexa accuracy on the same. 3 | AMAZON.DATE Converts words that indicate dates (today, tomorrow, or july) into a date format (such as 2015-07-00T9) 4 | AMAZON.TIME Converts words that indicate time (four in the morning, two p m) into a time value (16:00). 5 | AMAZON.DURATION Converts words that indicate durations (five minutes) into a numeric duration (5M). 6 | AMAZON.US_CITY Improves Alexa's recognition performance on all major US cities 7 | -------------------------------------------------------------------------------- /ask/intent_schema.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Abstractions around IntentSchema class. 3 | ''' 4 | from __future__ import print_function 5 | import json 6 | from collections import OrderedDict 7 | from argparse import ArgumentParser 8 | import os 9 | from .config.config import read_from_user, load_builtin_slots 10 | 11 | 12 | class IntentSchema(object): 13 | ''' 14 | Wrapper class to manipulate Intent Schema 15 | ''' 16 | def __init__(self, json_obj=None): 17 | if json_obj: 18 | # Use existing intent schema 19 | self._obj = json_obj 20 | else: 21 | # Create one from scratch 22 | self._obj = OrderedDict({ "intents" : [] }) 23 | 24 | # These intents are basically always needed 25 | # for certification 26 | self.add_intent('AMAZON.HelpIntent') 27 | self.add_intent('AMAZON.StopIntent') 28 | self.add_intent('AMAZON.CancelIntent') 29 | 30 | def add_intent(self, intent_name, slots=None): 31 | if not slots: slots = [] 32 | intent = OrderedDict() 33 | intent ['intent'], intent['slots'] = intent_name, slots 34 | self._obj['intents'].append(intent) 35 | 36 | 37 | def build_slot(self, slot_name, slot_type): 38 | slot = OrderedDict() 39 | slot['name'], slot['type'] = slot_name, slot_type 40 | return slot 41 | 42 | def __str__(self): 43 | return json.dumps(self._obj, indent=2) 44 | 45 | 46 | def get_intents(self): 47 | return self._obj['intents'] 48 | 49 | def get_intent_names(self): 50 | return [intent['intent'] for intent in self.get_intents()] 51 | 52 | @classmethod 53 | def interactive_build(self, fpath=None): 54 | intent_schema = IntentSchema.from_filename(fpath) 55 | print ("How many intents would you like to add") 56 | num = read_from_user(int) 57 | for i in range(num): 58 | intent_schema._add_intent_interactive(intent_num=i+1) 59 | return intent_schema 60 | 61 | def save_to_file(self, filename): 62 | with open(filename, 'w') as fp: 63 | print(self, file=fp) 64 | 65 | def _add_intent_interactive(self, intent_num=0): 66 | ''' 67 | Interactively add a new intent to the intent schema object 68 | ''' 69 | print ("Name of intent number : ", intent_num) 70 | slot_type_mappings = load_builtin_slots() 71 | intent_name = read_from_user(str) 72 | print ("How many slots?") 73 | num_slots = read_from_user(int) 74 | slot_list = [] 75 | for i in range(num_slots): 76 | print ("Slot name no.", i+1) 77 | slot_name = read_from_user(str).strip() 78 | print ("Slot type? Enter a number for AMAZON supported types below," 79 | "else enter a string for a Custom Slot") 80 | print (json.dumps(slot_type_mappings, indent=True)) 81 | slot_type_str = read_from_user(str) 82 | try: slot_type = slot_type_mappings[int(slot_type_str)]['name'] 83 | except: slot_type = slot_type_str 84 | slot_list += [self.build_slot(slot_name, slot_type)] 85 | self.add_intent(intent_name, slot_list) 86 | 87 | 88 | @classmethod 89 | def from_filename(self, filename): 90 | ''' 91 | Build an IntentSchema from a file path 92 | creates a new intent schema if the file does not exist, throws an error if the file 93 | exists but cannot be loaded as a JSON 94 | ''' 95 | if os.path.exists(filename): 96 | with open(filename) as fp: 97 | return IntentSchema(json.load(fp, object_pairs_hook=OrderedDict)) 98 | else: 99 | print ('File does not exist') 100 | return IntentSchema() 101 | 102 | def from_filename(fname): 103 | return IntentSchema.from_filename(fname) 104 | 105 | 106 | if __name__ == '__main__': 107 | 108 | parser = ArgumentParser() 109 | parser.add_argument('--intent_schema', '-i', required=True) 110 | parser.add_argument('--overwrite', '-o', action='store_true', 111 | default=False) 112 | 113 | args = parser.parse_args() 114 | 115 | if not args.overwrite: 116 | print ('In APPEND mode.') 117 | intent_schema = IntentSchema.interactive_build(args.intent_schema) 118 | 119 | else: 120 | print ('In OVERWRITE mode.') 121 | intent_schema = IntentSchema.interactive_build() 122 | 123 | print ("Write to file:", args.intent_schema,"? (y/n)") 124 | dec = read_from_user(str).strip().lower() 125 | 126 | if dec == "y": 127 | intent_schema.save_to_file(args.intent_schema) 128 | 129 | elif dec == "n": 130 | pass 131 | -------------------------------------------------------------------------------- /ask/write_sample.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import readline 3 | import json 4 | import re 5 | from .config.config import read_from_user 6 | from argparse import ArgumentParser 7 | try: 8 | from intent_schema import IntentSchema 9 | except: 10 | from ask.intent_schema import IntentSchema 11 | 12 | 13 | def print_description(intent): 14 | print ("<> Enter data for <{intent}> OR Press enter with empty string to move onto next intent" 15 | .format(intent=intent["intent"])) 16 | print ("<> Enter '<' to delete last training utterance") 17 | print ("<> Sample utterance to remind you of the format:") 18 | print (">> what is the recipe for {ravioli|Food} ?") 19 | if len(intent["slots"]) > 0: 20 | print ("<> Available slots for this intent") 21 | for slot in intent["slots"]: 22 | print (" - - ", slot["name"], "".format(slot["type"])) 23 | 24 | 25 | def validate_input_format(utterance, intent): 26 | """ TODO add handling for bad input""" 27 | slots = {slot["name"] for slot in intent["slots"]} 28 | split_utt = re.split("{(.*)}", utterance) 29 | banned = set("-/\\()^%$#@~`-_=+><;:") # Banned characters 30 | 31 | for token in split_utt: 32 | if (banned & set(token)): 33 | print (" - Banned character found in substring", token) 34 | print (" - Banned character list", banned) 35 | return False 36 | 37 | if "|" in token: 38 | split_token = token.split("|") 39 | if len(split_token)!=2: 40 | print (" - Error, token is incorrect in", token, split_token) 41 | return False 42 | 43 | word, slot = split_token 44 | if slot.strip() not in slots: 45 | print (" -", slot, "is not a valid slot for this Intent, valid slots are", slots) 46 | return False 47 | return True 48 | 49 | 50 | def lowercase_utterance(utterance): 51 | split_utt = re.split("({.*})", utterance) 52 | def lower_case_split(token): 53 | if "|" in token: 54 | phrase, slot = token.split("|") 55 | return "|".join([phrase.strip().lower(), slot.strip()]) 56 | else: 57 | return token.lower() 58 | return " ".join([lower_case_split(token) for token in split_utt]) 59 | 60 | 61 | def generate_training_data(schema): 62 | print ("Loaded intent schema, populating intents") 63 | training_data = [] 64 | for intent in schema.get_intents(): 65 | print_description(intent) 66 | keep_prompting = True 67 | while keep_prompting: 68 | utterance = read_from_user(str, 69 | str(len(training_data))+". "+intent["intent"]+'\t') 70 | if utterance.strip() == "": 71 | keep_prompting = False 72 | elif utterance.strip() == "<": 73 | print (" - Discarded utterance: ", training_data.pop()) 74 | elif validate_input_format(utterance, intent): 75 | training_data.append("\t".join([intent["intent"], lowercase_utterance(utterance)])) 76 | else: 77 | print (" - Discarded utterance:", utterance) 78 | return training_data 79 | 80 | 81 | if __name__ == '__main__': 82 | parser = ArgumentParser() 83 | parser.add_argument('--intent_schema', '-i', required=True) 84 | parser.add_argument('--output', '-o', default='utterances.txt') 85 | args = parser.parse_args() 86 | intent_schema = IntentSchema.from_filename(args.intent_schema) 87 | with open(args.output, 'w') as utterance_file: 88 | utterance_file.write("\n".join(generate_training_data(intent_schema))) 89 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | This folder contains examples of alexa skill templates to make building a skill easy. -------------------------------------------------------------------------------- /examples/basic/lambda_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this file we specify default event handlers which are then populated into the handler map using metaprogramming 3 | Copyright Anjishnu Kumar 2015 4 | Happy Hacking! 5 | """ 6 | 7 | from ask import alexa 8 | 9 | def lambda_handler(request_obj, context=None): 10 | ''' 11 | This is the main function to enter to enter into this code. 12 | If you are hosting this code on AWS Lambda, this should be the entry point. 13 | Otherwise your server can hit this code as long as you remember that the 14 | input 'request_obj' is JSON request converted into a nested python object. 15 | ''' 16 | 17 | metadata = {'user_name' : 'SomeRandomDude'} # add your own metadata to the request using key value pairs 18 | 19 | ''' inject user relevant metadata into the request if you want to, here. 20 | e.g. Something like : 21 | ... metadata = {'user_name' : some_database.query_user_name(request.get_user_id())} 22 | 23 | Then in the handler function you can do something like - 24 | ... return alexa.create_response('Hello there {}!'.format(request.metadata['user_name'])) 25 | ''' 26 | return alexa.route_request(request_obj, metadata) 27 | 28 | 29 | @alexa.default() 30 | def default_handler(request): 31 | """ The default handler gets invoked if no handler is set for a request type """ 32 | return alexa.respond('Just ask').with_card('Hello World') 33 | 34 | 35 | @alexa.request("LaunchRequest") 36 | def launch_request_handler(request): 37 | ''' Handler for LaunchRequest ''' 38 | return alexa.create_response(message="Hello Welcome to My Recipes!") 39 | 40 | 41 | @alexa.request("SessionEndedRequest") 42 | def session_ended_request_handler(request): 43 | return alexa.create_response(message="Goodbye!") 44 | 45 | 46 | @alexa.intent('GetRecipeIntent') 47 | def get_recipe_intent_handler(request): 48 | """ 49 | You can insert arbitrary business logic code here 50 | """ 51 | 52 | # Get variables like userId, slots, intent name etc from the 'Request' object 53 | ingredient = request.slots["Ingredient"] # Gets an Ingredient Slot from the Request object. 54 | 55 | if ingredient == None: 56 | return alexa.create_response("Could not find an ingredient!") 57 | 58 | # All manipulations to the request's session object are automatically reflected in the request returned to Amazon. 59 | # For e.g. This statement adds a new session attribute (automatically returned with the response) storing the 60 | # Last seen ingredient value in the 'last_ingredient' key. 61 | 62 | request.session['last_ingredient'] = ingredient # Automatically returned as a sessionAttribute 63 | 64 | # Modifying state like this saves us from explicitly having to return Session objects after every response 65 | 66 | # alexa can also build cards which can be sent as part of the response 67 | card = alexa.create_card(title="GetRecipeIntent activated", subtitle=None, 68 | content="asked alexa to find a recipe using {}".format(ingredient)) 69 | 70 | return alexa.create_response("Finding a recipe with the ingredient {}".format(ingredient), 71 | end_session=False, card_obj=card) 72 | 73 | 74 | 75 | @alexa.intent('NextRecipeIntent') 76 | def next_recipe_intent_handler(request): 77 | """ 78 | You can insert arbitrary business logic code here 79 | """ 80 | return alexa.create_response(message="Getting Next Recipe ... 123") 81 | -------------------------------------------------------------------------------- /examples/basic/speech_assets/sample_intent_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "slots": [ 5 | { 6 | "type": "AMAZON.LITERAL", 7 | "name": "Ingredient" 8 | } 9 | ], 10 | "intent": "GetRecipeIntent" 11 | }, 12 | { 13 | "slots": [], 14 | "intent": "NextRecipeIntent" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/basic/speech_assets/utterances.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjishnu/ask-alexa-pykit/a47c278ca7a60532bbe1a9b789f6c37e609fea8b/examples/basic/speech_assets/utterances.txt -------------------------------------------------------------------------------- /examples/twitter/config.py: -------------------------------------------------------------------------------- 1 | TWITTER_CONSUMER_KEY = 'FOO' 2 | TWITTER_CONSUMER_SECRET = 'BAR' 3 | -------------------------------------------------------------------------------- /examples/twitter/lambda_function.py: -------------------------------------------------------------------------------- 1 | from ask import alexa 2 | from config import TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET 3 | from twitter import local_cache as twitter_cache 4 | from twitter import (post_tweet, get_home_tweets, get_retweets_of_me, 5 | get_my_favourite_tweets, get_my_favourite_tweets, 6 | get_latest_twitter_mentions, search_for_tweets_about, 7 | get_user_latest_tweets, get_user_twitter_details, 8 | geo_search, closest_trend_search, list_trends) 9 | 10 | 11 | # Run this code once on startup to load twitter keys into credentials 12 | server_cache_state = twitter_cache.get_server_state() 13 | if 'twitter_keys' not in server_cache_state: 14 | server_cache_state['twitter_keys'] = (TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) 15 | 16 | 17 | def default_handler(request): 18 | """ The default handler gets invoked if no handler is set for a request """ 19 | return launch_request_handler(request) 20 | 21 | 22 | @alexa.request(request_type="LaunchRequest") 23 | def launch_request_handler(request): 24 | """ Annotate functions with @VoiceHandler so that they can be automatically mapped 25 | to request types. Use the 'request_type' field to map them to non-intent requests """ 26 | 27 | user_id = request.access_token() 28 | if user_id in twitter_cache.users(): 29 | 30 | user_cache = twitter_cache.get_user_state(user_id) 31 | user_cache["amzn_id"]= request.user_id() 32 | base_message = "Welcome to Twitter, {} . How may I help you today ?".format(user_cache["screen_name"]) 33 | print (user_cache) 34 | if 'pending_action' in user_cache: 35 | base_message += " You have one pending action . " 36 | print ("Found pending action") 37 | if 'description' in user_cache['pending_action']: 38 | print ("Found description") 39 | base_message += user_cache['pending_action']['description'] 40 | return r.create_response(base_message) 41 | 42 | card = r.create_card(title="Please log into twitter", card_type="LinkAccount") 43 | return r.create_response(message="Welcome to twitter, looks like you haven't logged in!" 44 | " Log in via the alexa app.", card_obj=card, 45 | end_session=True) 46 | 47 | 48 | @alexa.request("SessionEndedRequest") 49 | def session_ended_request_handler(request): 50 | return alexa.create_response(message="Goodbye!") 51 | 52 | 53 | @alexa.intent(intent='PostTweet') 54 | def post_tweet_intent_handler(request): 55 | """ 56 | Use the 'intent' field in the VoiceHandler to map to the respective intent. 57 | """ 58 | tweet = request.get_slot_value("Tweet") 59 | tweet = tweet if tweet else "" 60 | if tweet: 61 | user_state = twitter_cache.get_user_state(request.access_token()) 62 | def action(): 63 | return post_tweet(request.access_token(), tweet) 64 | 65 | message = "I am ready to post the tweet, {} ,\n Please say yes to confirm or stop to cancel .".format(tweet) 66 | user_state['pending_action'] = {"action" : action, 67 | "description" : message} 68 | return r.create_response(message=message, end_session=False) 69 | else: 70 | # No tweet could be disambiguated 71 | message = " ".join( 72 | [ 73 | "I'm sorry, I couldn't understand what you wanted to tweet .", 74 | "Please prepend the message with either post or tweet ." 75 | ] 76 | ) 77 | return alexa.create_response(message=message, end_session=False) 78 | 79 | 80 | @alexa.intent(intent="SearchTrends") 81 | def find_trends_handler(request): 82 | 83 | uid = request.access_token() 84 | user_cache = twitter_cache.get_user_state(uid) 85 | resolved_location = False 86 | message = "" 87 | location = request.get_slot_value("Location") 88 | should_end_session = True 89 | 90 | if not location: 91 | # Get trends for user's current location 92 | user_details = get_user_twitter_details(uid) 93 | location = user_details[0]['location'] 94 | if location: 95 | message += "Finding trends near you . " 96 | else: 97 | message += "I could not figure out where you are, please set it up on your twitter account . " 98 | 99 | if location: 100 | 101 | response = geo_search(request.access_token(), location) # convert natural language text to location 102 | top_result = response['result']['places'][0] 103 | lon, lat = top_result['centroid'] 104 | trend_params = {"lat" : lat, "long" : lon} 105 | trend_location = closest_trend_search(request.access_token(), trend_params) # find closest woeid which has trends 106 | woeid = trend_location[0]['woeid'] 107 | trends = list_trends(request.access_token(), trend_location[0]['woeid']) # List top trends 108 | trend_lst = [trend['name'] for trend in trends[0]['trends']] 109 | message += "The top trending topics near {0} are, ".format(trend_location[0]['name']) 110 | message += "\n".join(["{0}, {1}, ".format(index+1, trend) for index, trend in enumerate(trend_lst)]) 111 | 112 | return alexa.create_response(message=message, end_session=should_end_session) 113 | 114 | 115 | @alexa.intent(intent="AMAZON.HelpIntent") 116 | def help_intent_handler(request): 117 | msg = ("I can do several things for you on twitter! " 118 | "I can tell you about the top tweets on your home page, or the last tweets you favourited . " 119 | "I can also tell you about recent tweets that mention you, or were posted by you . " 120 | "When I am reading out a list of tweets, you can stop me and ask me to tell you about the tweet in more detail, or ask me to post a reply to it . " 121 | "And of course, whenever post a tweet, say 'post hello world' or 'tweet hello world'. I am not good with hashtags or trending topics just yet, but I'm working on it! ") 122 | return r.create_response(message=msg) 123 | 124 | 125 | @alexa.intent(intent="AMAZON.StopIntent") 126 | def stop_intent__handler(request): 127 | return cancel_action_handler(request) 128 | 129 | 130 | @alexa.intent(intent="AMAZON.CancelIntent") 131 | def cancel_intent_handler(request): 132 | return cancel_action_handler(request) 133 | 134 | 135 | MAX_RESPONSE_TWEETS = 3 136 | 137 | def tweet_list_handler(request, tweet_list_builder, msg_prefix=""): 138 | 139 | """ This is a generic function to handle any intent that reads out a list of tweets""" 140 | # tweet_list_builder is a function that takes a unique identifier and returns a list of things to say 141 | tweets = tweet_list_builder(request.access_token()) 142 | print (len(tweets), 'tweets found') 143 | if tweets: 144 | twitter_cache.initialize_user_queue(user_id=request.access_token(), 145 | queue=tweets) 146 | text_to_read_out = twitter_cache.user_queue(request.access_token()).read_out_next(MAX_RESPONSE_TWEETS) 147 | message = msg_prefix + text_to_read_out + ", say 'next' to hear more, or reply to a tweet by number." 148 | return alexa.create_response(message=message, 149 | end_session=False) 150 | else: 151 | return alexa.create_response(message="Sorry, no tweets found, please try something else", 152 | end_session=False) 153 | 154 | 155 | @alexa.intent(intent="SearchTweets") 156 | def search_tweets_handler(request): 157 | search_topic = request.get_slot_value("Topic") 158 | max_tweets = 3 159 | if search_topic: 160 | message = "Searching twitter for tweets about {} . ".format(search_topic) 161 | def search_tweets_builder(uid): 162 | params = { 163 | "q" : search_topic, 164 | "result_type" : "popular" 165 | } 166 | return search_for_tweets_about(request.access_token(), params) 167 | return tweet_list_handler(request, tweet_list_builder=search_tweets_builder, msg_prefix=message) 168 | else: 169 | return r.create_response("I couldn't find a topic to search for in your request") 170 | 171 | 172 | @alexa.intent(intent="FindLatestMentions") 173 | def list_mentions_handler(request): 174 | return tweet_list_handler(request, tweet_list_builder=get_latest_twitter_mentions, msg_prefix="Looking for tweets that mention you.") 175 | 176 | 177 | @alexa.intent(intent="ListHomeTweets") 178 | def list_home_tweets_handler(request): 179 | return tweet_list_handler(request, tweet_list_builder=get_home_tweets) 180 | 181 | 182 | @alexa.intent(intent="UserTweets") 183 | def list_user_tweets_handler(request): 184 | """ by default gets tweets for current user """ 185 | return tweet_list_handler(request, tweet_list_builder=get_user_latest_tweets, msg_prefix="Looking for tweets posted by you.") 186 | 187 | 188 | @alexa.intent(intent="RetweetsOfMe") 189 | def list_retweets_of_me_handler(request): 190 | return tweet_list_handler(request, tweet_list_builder=get_retweets_of_me, msg_prefix="Looking for retweets.") 191 | 192 | 193 | @alexa.intent(intent="FindFavouriteTweets") 194 | def find_my_favourites_handler(request): 195 | return tweet_list_handler(request, tweet_list_builder=get_my_favourite_tweets, msg_prefix="Finding your favourite tweets.") 196 | 197 | 198 | def focused_on_tweet(request): 199 | """ 200 | Return index if focused on tweet False if couldn't 201 | """ 202 | slots = request.get_slot_map() 203 | if "Index" in slots and slots["Index"]: 204 | index = int(slots['Index']) 205 | 206 | elif "Ordinal" in slots and slots["Index"]: 207 | parse_ordinal = lambda inp : int("".join([l for l in inp if l in string.digits])) 208 | index = parse_ordinal(slots['Ordinal']) 209 | else: 210 | return False 211 | 212 | index = index - 1 # Going from regular notation to CS notation 213 | user_state = twitter_cache.get_user_state(request.access_token()) 214 | queue = user_state['user_queue'].queue() 215 | if index < len(queue): 216 | # Analyze tweet in queue 217 | tweet_to_analyze = queue[index] 218 | user_state['focus_tweet'] = tweet_to_analyze 219 | return index + 1 # Returning to regular notation 220 | twitter_cache.serialize() 221 | return False 222 | 223 | """ 224 | Definining API for executing pending actions: 225 | action = function that does everything you want and returns a 'message' to return. 226 | description = read out in case there is a pending action at startup. 227 | other metadata will be added as time progresses 228 | """ 229 | 230 | @alexa.intent("ReplyIntent") 231 | def reply_handler(request): 232 | message = "Sorry, I couldn't tell which tweet you want to reply to. " 233 | slots = request.get_slot_map() 234 | user_state = twitter_cache.get_user_state(request.access_token()) 235 | should_end_session = True 236 | if not slots["Tweet"]: 237 | return reply_focus_handler(request) 238 | else: 239 | can_reply = False 240 | if slots['Tweet'] and not (slots['Ordinal'] or slots['Index']): 241 | user_state = twitter_cache.get_user_state(request.access_token()) 242 | if 'focus_tweet' in user_state: # User is focused on a tweet 243 | can_reply = True 244 | else: 245 | index = focused_on_tweet(request) 246 | if index: can_reply = True 247 | 248 | if can_reply: # Successfully focused on a tweet 249 | index, focus_tweet = user_state['focus_tweet'] 250 | tweet_message = "@{0} {1}".format(focus_tweet.get_screen_name(), 251 | slots['Tweet']) 252 | params = {"in_reply_to_status_id": focus_tweet.get_id()} 253 | 254 | def action(): 255 | print ("Performing action! lambda functions are awesome!") 256 | message = post_tweet(request.access_token(), tweet_message, params) 257 | del user_state['focus_tweet'] 258 | return message 259 | 260 | should_end_session = False 261 | message = "I am ready to post the tweet, {}. Please say yes to confirm or stop to cancel.".format(slots['Tweet']) 262 | user_state['pending_action'] = {"action" : action, 263 | "description" : message } 264 | 265 | return alexa.create_response(message=message, end_session=should_end_session) 266 | 267 | 268 | @alexa.intent("YesIntent") 269 | def confirm_action_handler(request): 270 | message = "okay." 271 | user_state = twitter_cache.get_user_state(request.access_token()) 272 | should_end_session = True 273 | if 'pending_action' in user_state: 274 | params = user_state['pending_action'] 275 | # Perform action 276 | message = params['action']() 277 | if 'message' in params: 278 | message = params['message'] 279 | if 'callback' in params: 280 | params['callback']() 281 | del user_state['pending_action'] 282 | print ("successfully executed command") 283 | message = message + " would you like me to do anything else ? " 284 | should_end_session = False 285 | return alexa.create_response(message, end_session=should_end_session) 286 | 287 | 288 | @alexa.intent("AMAZON.CancelIntent") 289 | def cancel_action_handler(request): 290 | message = "okay." 291 | user_state = twitter_cache.get_user_state(request.access_token()) 292 | should_end_session = True 293 | if 'pending_action' in user_state: 294 | del user_state['pending_action'] # Clearing out the user's pending action 295 | print ("cleared user_state") 296 | message += " i won't do it. would you like me to do something else ? " 297 | should_end_session = False 298 | return r.create_response(message, end_session=should_end_session) 299 | 300 | 301 | @alexa.intent("ReplyFocus") 302 | def reply_focus_handler(request): 303 | msg = "Sorry, I couldn't tell which tweet you wanted to reply to." 304 | index = focused_on_tweet(request) 305 | if index: 306 | return alexa.create_response(message="Do you want to reply to tweet {} ? If so say reply, followed by your message".format(index)) 307 | return alexa.create_response(message=msg, end_session=False) 308 | 309 | 310 | @alexa.intent("MoreInfo") 311 | def more_info_handler(request): 312 | index = focused_on_tweet(request) 313 | if index: 314 | user_state = twitter_cache.get_user_state(request.access_token()) 315 | index, tweet = user_state['focus_tweet'] 316 | message = " ".join(["details about tweet number {}.".format(index+1), tweet.detailed_description(), 317 | "To reply, say 'reply' followed by your message"]) 318 | return alexa.create_response(message=message, end_session=False) 319 | return reply_focus_handler(request) 320 | 321 | @alexa.intent("NextIntent") 322 | def next_intent_handler(request): 323 | """ 324 | Takes care of things whenver the user says 'next' 325 | """ 326 | 327 | message = "Sorry, couldn't find anything in your next queue" 328 | end_session = True 329 | if True: 330 | user_queue = twitter_cache.user_queue(request.access_token()) 331 | if not user_queue.is_finished(): 332 | message = user_queue.read_out_next(MAX_RESPONSE_TWEETS) 333 | if not user_queue.is_finished(): 334 | end_session = False 335 | message = message + ". Please, say 'next' if you want me to read out more. " 336 | return alexa.create_response(message=message, 337 | end_session=end_session) 338 | 339 | 340 | @alexa.intent(intent="PreviousIntent") 341 | def previous_intent_handler(request): 342 | user_queue = twitter_cache.user_queue(request.access_token()) 343 | if user_queue and user_queue.has_prev(): 344 | message = user_queue.read_out_prev() 345 | else: 346 | message = "I couldn't find anything to repeat" 347 | return alexa.create_response(message=message) 348 | -------------------------------------------------------------------------------- /examples/twitter/speech_assets/custom_slots/ORDINAL.list: -------------------------------------------------------------------------------- 1 | first 2 | second 3 | third 4 | fourth 5 | fifth 6 | sixth 7 | seventh 8 | eighth 9 | ninth 10 | tenth 11 | eleventh 12 | twelfth 13 | thirteenth 14 | fourteenth 15 | fifteenth 16 | sixteenth 17 | seventeenth 18 | eighteenth 19 | nineteenth 20 | twentieth -------------------------------------------------------------------------------- /examples/twitter/speech_assets/custom_slots/TWITTER_MESSAGE.list: -------------------------------------------------------------------------------- 1 | i really like parties 2 | about the song of ice and fire| 3 | i really like cake and amazon echo is super awesome 4 | really tough day today 5 | first day at work 6 | why is the nature of reality not transparent to my machinations 7 | life is awesome ! 8 | lol 9 | what the fuck man 10 | why doesn't anything work like it's supposed to 11 | hello world 12 | dave i can't let you do that 13 | Mobility & cars: manufacturers won't brag about units but occupancy? But how long until the Service works? 14 | Republicans have convinced themselves that America is doing terribly, but it isn't 15 | something random 16 | what is up 17 | dude this is awesome 18 | i went to the super bowl today and it was awesome 19 | my name is alexa 20 | one two three 21 | one 22 | one two 23 | one two three four 24 | one two three one two three 25 | something has got to give, right? 26 | At great personal inconvenience, by design of the DNC 27 | 1% of the world's population--73m people--have been forced from their homes because of armed conflict in the past 4 years. 28 | Now that we have $10 Android phones @ Walmart, I think it's safe to say the future finally is evenly distributed. 29 | On the ground in Paris 30 | London is French tonight 31 | fuck you 32 | New blog post: Shaking things up a bit with a short story on AI, A Cognitive Discontinuity 33 | After a successful bilateral visit to UK, it's time for the G20 Summit in Turkey. 34 | Dear Friends, thank you for all the prayers and and all the #PrayForParis. We know you mean well. 35 | all this hatred all this fire 36 | i love you 37 | you are the best friend ever 38 | happy birthday 39 | hey 40 | everything is temporary 41 | hello kitty 42 | Cab company to no longer provide cabs, will instead focus on beautifying mobile app for cab booking -------------------------------------------------------------------------------- /examples/twitter/speech_assets/intent_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "PostTweet", 5 | "slots": [ 6 | { 7 | "name": "Tweet", 8 | "type": "TWITTER_MESSAGE" 9 | } 10 | ] 11 | }, 12 | { 13 | "intent": "NextIntent", 14 | "slots": [] 15 | }, 16 | { 17 | "intent": "ListHomeTweets", 18 | "slots": [] 19 | }, 20 | { 21 | "intent": "FindLatestMentions", 22 | "slots": [] 23 | }, 24 | { 25 | "intent": "FindFavouriteTweets", 26 | "slots": [] 27 | }, 28 | { 29 | "intent": "RetweetsOfMe", 30 | "slots": [] 31 | }, 32 | { 33 | "intent": "SearchTweets", 34 | "slots": [ 35 | { 36 | "name": "Topic", 37 | "type": "LITERAL" 38 | } 39 | ] 40 | }, 41 | { 42 | "intent": "AMAZON.StopIntent", 43 | "slots": [] 44 | }, 45 | { 46 | "intent": "UserTweets", 47 | "slots": [] 48 | }, 49 | { 50 | "intent": "AMAZON.HelpIntent", 51 | "slots": [] 52 | }, 53 | { 54 | "intent": "AMAZON.CancelIntent", 55 | "slots": [] 56 | }, 57 | { 58 | "intent": "YesIntent", 59 | "slots": [] 60 | }, 61 | { 62 | "intent": "SearchTrends", 63 | "slots": [ 64 | { 65 | "name": "Location", 66 | "type": "AMAZON.US_CITY" 67 | } 68 | ] 69 | }, 70 | { 71 | "intent": "MoreInfo", 72 | "slots": [ 73 | { 74 | "name": "Index", 75 | "type": "AMAZON.NUMBER" 76 | }, 77 | { 78 | "name": "Ordinal", 79 | "type": "ORDINAL" 80 | } 81 | ] 82 | }, 83 | { 84 | "intent": "ReplyIntent", 85 | "slots": [ 86 | { 87 | "name": "Index", 88 | "type": "AMAZON.NUMBER" 89 | }, 90 | { 91 | "name": "Ordinal", 92 | "type": "ORDINAL" 93 | }, 94 | { 95 | "name": "Tweet", 96 | "type": "TWITTER_MESSAGE" 97 | } 98 | ] 99 | }, 100 | { 101 | "intent": "ReplyFocus", 102 | "slots": [ 103 | { 104 | "name": "Index", 105 | "type": "AMAZON.NUMBER" 106 | }, 107 | { 108 | "name": "Ordinal", 109 | "type": "ORDINAL" 110 | } 111 | ] 112 | }, 113 | { 114 | "intent": "PreviousIntent", 115 | "slots": [] 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /examples/twitter/speech_assets/utterances.txt: -------------------------------------------------------------------------------- 1 | PostTweet i want to post a tweet to twitter 2 | PostTweet please post a tweet 3 | PostTweet post a tweet 4 | PostTweet post a tweet please 5 | PostTweet i would like to post a tweet 6 | PostTweet post {Tweet} 7 | PostTweet tweet {Tweet} on twitter 8 | PostTweet tweet {Tweet} 9 | PostTweet post {Tweet} on twitter 10 | PostTweet please post {Tweet} on twitter 11 | PostTweet post {Tweet} 12 | PostTweet tweet {Tweet} 13 | PostTweet post a message {Tweet} 14 | PostTweet tweet a message {Tweet} 15 | PostTweet update my status {Tweet} 16 | PostTweet create a message {Tweet} 17 | PostTweet note to self {Tweet} 18 | NextIntent next please 19 | NextIntent next 20 | NextIntent more 21 | NextIntent tell me more 22 | NextIntent what next 23 | PreviousIntent go back 24 | PreviousIntent previous 25 | PreviousIntent what was the last tweet 26 | ListHomeTweets list my home tweets 27 | ListHomeTweets what is new 28 | ListHomeTweets show me tweets on my home page 29 | ListHomeTweets what are the tweets on my twitter wall 30 | ListHomeTweets what are the top tweets 31 | ListHomeTweets read out some tweets 32 | ListHomeTweets what is up 33 | ListHomeTweets what is happening 34 | ListHomeTweets what happening on my homepage 35 | ListHomeTweets list home page tweets 36 | ListHomeTweets what are the top tweets on my homepage 37 | ListHomeTweets my homepage please 38 | ListHomeTweets what are the tweets on my timeline 39 | ListHomeTweets tell me my homepage 40 | ListHomeTweets what is on my homepage 41 | ListHomeTweets what is on my time line 42 | ListHomeTweets show me my timeline 43 | ListHomeTweets what is on my home page 44 | ListHomeTweets what is new 45 | SearchTweets search for tweets about {harry potter|Topic} 46 | SearchTweets search for tweets about {something important|Topic} 47 | SearchTweets find me some tweets {a topic of my choosing|Topic} 48 | SearchTweets search for {obama|Topic} 49 | SearchTweets look for some tweets about {modi | Topic} 50 | SearchTweets please search for some tweets 51 | SearchTweets find tweets about {life|Topic} 52 | SearchTweets find tweets about {yo|Topic} 53 | SearchTweets show me the top tweets about {the paris terror attacks|Topic} 54 | FindLatestMentions tell me my latest mentions 55 | FindLatestMentions what are my latest mentions 56 | FindLatestMentions has anyone mentioned me 57 | FindLatestMentions if anybody has mentioned me 58 | FindLatestMentions who mentioned me recently 59 | FindLatestMentions are there any tweets that mention me 60 | FindLatestMentions do I have any replies 61 | FindLatestMentions are there any tweets for me 62 | FindLatestMentions tweets mentioning me 63 | FindLatestMentions list my mentions 64 | FindFavouriteTweets list my favorite tweets 65 | FindFavouriteTweets list my favorites 66 | FindFavouriteTweets what are my favorites 67 | FindFavouriteTweets tweets i have favourited 68 | FindFavouriteTweets tell me my recent favorites 69 | FindFavouriteTweets for my favorite tweets 70 | RetweetsOfMe my retweets 71 | RetweetsOfMe who retweeted me 72 | RetweetsOfMe list my retweets 73 | RetweetsOfMe list my artie's' 74 | RetweetsOfMe show me my artie's 75 | RetweetsOfMe show my artie's 76 | RetweetsOfMe what are my retweets 77 | RetweetsOfMe has anyone retweeted me recently 78 | RetweetsOfMe do i have any retweets 79 | RetweetsOfMe if i have any retweets 80 | RetweetsOfMe someone has retweeted me 81 | UserTweets tweets by me 82 | UserTweets some tweets i have posted 83 | UserTweets show my tweets 84 | UserTweets tell me my last few tweets 85 | UserTweets my tweets 86 | UserTweets tweets written by me 87 | UserTweets tweets i wrote 88 | UserTweets find some tweets i wrote 89 | UserTweets find some tweets written by me 90 | UserTweets my own tweets 91 | UserTweets what are the last tweet posted by me 92 | UserTweets what are the last tweets posted by me 93 | MoreInfo more info about tweet {Index} 94 | MoreInfo tell me more about tweet number {Index} 95 | MoreInfo tell me more about the {Ordinal} tweet 96 | MoreInfo more info on number {Index} please 97 | MoreInfo tell me more about number {Index} 98 | MoreInfo who wrote number {Index} 99 | ReplyIntent reply saying {Tweet} 100 | ReplyIntent reply {Tweet} 101 | ReplyIntent post reply {Tweet} 102 | ReplyFocus reply to tweet number {Index} 103 | ReplyFocus reply to tweet {Index} 104 | ReplyFocus reply to tweet {Index} please 105 | ReplyIntent reply to tweet number {Index} {Tweet} 106 | ReplyFocus please reply to the {Ordinal} tweet 107 | ReplyIntent please reply to the {Ordinal} saying {Tweet} 108 | ReplyIntent please reply {Tweet} 109 | ReplyFocus post reply to the {Ordinal} 110 | ReplyFocus post reply to this one 111 | ReplyFocus please reply to the {Ordinal} tweet 112 | ReplyIntent post a reply to tweet number {Index} {Tweet} 113 | ReplyIntent write a reply saying {Tweet} 114 | ReplyIntent post a reply saying {Tweet} 115 | ReplyIntent post a reply to number {Index} saying {Tweet} 116 | ReplyIntent post a reply and say {Tweet} 117 | ReplyIntent reply saying {Tweet} 118 | ReplyIntent please reply to this saying {Tweet} 119 | ReplyIntent reply {Tweet} 120 | ReplyIntent post reply {Tweet} 121 | ReplyIntent please post reply {Tweet} 122 | ReplyIntent please reply {Tweet} 123 | YesIntent hell yeah 124 | YesIntent yes 125 | YesIntent yup 126 | YesIntent sure 127 | YesIntent go ahead 128 | YesIntent do it 129 | YesIntent please do 130 | AMAZON.StopIntent no stop 131 | AMAZON.StopIntent no 132 | AMAZON.StopIntent nope 133 | AMAZON.StopIntent nada 134 | AMAZON.StopIntent please no 135 | AMAZON.StopIntent please don't 136 | SearchTrends what is trending right now 137 | SearchTrends what is trending in {Location} 138 | SearchTrends what trends in {Location} 139 | SearchTrends tell me trends in {Location} 140 | SearchTrends please tell me the trends in {Location} 141 | SearchTrends what is trending near me 142 | SearchTrends find the top trending hashtags in {Location} 143 | SearchTrends what are the trending topics in {Location} 144 | SearchTrends {Location} trends please 145 | SearchTrends what are the top trends near me 146 | SearchTrends what are the top hashtags near me 147 | SearchTrends which hashtags are trending near me 148 | SearchTrends which hashtags are trending 149 | SearchTrends trends please 150 | -------------------------------------------------------------------------------- /examples/twitter/twitter.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import jsonpickle 3 | from requests_oauthlib import OAuth1 4 | from urllib.parse import parse_qs, urlencode 5 | import cherrypy 6 | from collections import defaultdict 7 | import json 8 | import os 9 | import re 10 | from collections import defaultdict 11 | 12 | # For readable serializations 13 | jsonpickle.set_encoder_options('json', sort_keys=True, indent=4) 14 | 15 | 16 | class LocalCache(object): 17 | """ Generic class for encapsulating twitter credential caching """ 18 | server_data_template = "{}.server" 19 | user_data_template = "{0}.user.{1}" 20 | 21 | def __init__(self, backup = "tmp/twitter.cache"): 22 | self.backup = backup #Unique identifier for the backup of this cache 23 | self.memcache = { 24 | "users" : defaultdict(lambda : {}), 25 | "server": defaultdict(lambda : {}) 26 | } 27 | self.deserialize() 28 | 29 | def users(self): 30 | return self.memcache['users'] 31 | 32 | def set_user_state(self, user_id, state): 33 | self.memcache['users'][user_id] = state 34 | 35 | def update_user_state(self, user_id, state = {}): 36 | self.memcache['users'][user_id].update(state) 37 | 38 | def get_user_state(self, user_id): 39 | return self.memcache['users'][user_id] 40 | 41 | def clear_user_state(self, user_id): 42 | return self.memcache['users'][user_id].clear() 43 | 44 | def update_server_state(self, state_dict): 45 | self.memcache['server'].update(state_dict) 46 | 47 | def get_server_state(self): 48 | return self.memcache['server'] 49 | 50 | def clear_server_state(self): 51 | return self.memcache['server'].clear() 52 | 53 | def initialize_user_queue(self, user_id, queue): 54 | self.memcache['users'][user_id]['user_queue'] = ReadableQueue(queue) 55 | 56 | def user_queue(self, user_id): 57 | if 'user_queue' in self.memcache['users'][user_id]: 58 | return self.memcache['users'][user_id]['user_queue'] 59 | 60 | def server_fname(self): 61 | return self.server_data_template.format(self.backup) 62 | 63 | def user_fname(self, user): 64 | return self.user_data_template.format(self.backup, user) 65 | 66 | def deserialize(self): 67 | cache_loaded = False 68 | if os.path.exists(self.server_fname()) and not os.path.isdir(self.backup): 69 | try: 70 | self.memcache = { "server" : {}, 71 | "users" : {} } 72 | 73 | with open(self.server_fname()) as backupfile: 74 | print ("Attempting to reload cache") 75 | self.memcache['server'] = jsonpickle.decode(backupfile.read()) 76 | 77 | print ("Server cache loaded", json.dumps(self.memcache, indent=4)) 78 | for user in self.memcache['server']['user_list']: 79 | # Try to load as much user data as possible 80 | if os.path.exists(self.user_fname(user)): 81 | print ("found path for user", user) 82 | with open(self.user_fname(user)) as userfile: 83 | user_data = jsonpickle.decode(userfile.read()) 84 | self.memcache['users'][user] = user_data 85 | cache_loaded = True 86 | except Exception as e: 87 | print ("Cache file corrupted...") 88 | raise e 89 | if not cache_loaded: 90 | print ("Cache could not be loaded") 91 | pass 92 | else: 93 | print ("CACHE LOADED SUCCESSFULLY!") 94 | 95 | 96 | def serialize(self): 97 | json_to_serialize = self.memcache['server'] 98 | user_list = list(self.users().keys()) 99 | json_to_serialize.update({"user_list" : user_list}) 100 | with open(self.server_fname(), 'w') as backup_server: 101 | # Serialize Server: 102 | json_encoded = jsonpickle.encode(json_to_serialize) 103 | backup_server.write(json_encoded) 104 | 105 | for user in user_list: 106 | user_data = self.get_user_state(user) 107 | json_encoded = jsonpickle.encode(user_data) 108 | with open(self.user_fname(user), 'w') as userfile: 109 | userfile.write(json_encoded) 110 | 111 | 112 | 113 | class ReadableQueue(object): 114 | def __init__(self, queue=[], pos=0): 115 | self.hashmap = { "queue" : [(i, e) for i,e in enumerate(queue)], 116 | "pos" : pos } 117 | return 118 | 119 | def queue(self): 120 | return self.hashmap['queue'] 121 | 122 | def is_empty(self): 123 | return len(self.queue()) == 0 124 | 125 | def is_finished(self): 126 | return self.pos() == len(self.queue()) 127 | 128 | def pos(self): 129 | return self.hashmap['pos'] 130 | 131 | def set_pos(self, val): 132 | self.hashmap['pos'] = val 133 | 134 | def get_next(self, offset=1): 135 | 136 | if self.pos() < len(self.queue()): 137 | temp_queue = self.queue()[self.pos(): self.pos() + offset] 138 | self.set_pos(self.pos() + offset) 139 | if self.pos() > len(self.queue()): self.set_pos(len(self.queue())) 140 | return temp_queue 141 | 142 | 143 | def read_out_next(self, offset=1): 144 | return " ".join([readable.read_out(index) for index,readable in self.get_next(offset)]) 145 | 146 | def has_prev(self): 147 | return self.pos() > 0 148 | 149 | def get_prev(self, offset=1): 150 | if self.pos() > 0: 151 | self.set_pos(self.pos() - offset) 152 | if self.pos() < 0: 153 | offset = offset + self.pos() 154 | # [1, current(2), 3] get_prev(offeset=3) 155 | # pos :=> -2, offset :=> 3-2 = 1, pos :=> 0, then read 0 to 1 156 | self.set_pos(0) 157 | return self.queue()[self.pos() : offset] 158 | return None 159 | 160 | def read_out_prev(self, offset=1): 161 | return " ".join([readable.read_out() for readable in self.get_prev(offset)]) 162 | 163 | 164 | #Local cache caches tokens for different users 165 | local_cache = LocalCache() 166 | 167 | 168 | 169 | def strip_html(text): 170 | """ Get rid of ugly twitter html """ 171 | def reply_to(text): 172 | replying_to = [] 173 | split_text = text.split() 174 | for index, token in enumerate(split_text): 175 | if token.startswith('@'): replying_to.append(token[1:]) 176 | else: 177 | message = split_text[index:] 178 | break 179 | rply_msg = "" 180 | if len(replying_to) > 0: 181 | rply_msg = "Replying to " 182 | for token in replying_to[:-1]: rply_msg += token+"," 183 | if len(replying_to)>1: rply_msg += 'and ' 184 | rply_msg += replying_to[-1]+". " 185 | return rply_msg + " ".join(message) 186 | 187 | text = reply_to(text) 188 | text = text.replace('@', ' ') 189 | return " ".join([token for token in text.split() 190 | if ('http:' not in token) and ('https:' not in token)]) 191 | 192 | 193 | class Tweet(object): 194 | def __init__(self, json_obj): 195 | self.tweet = json_obj 196 | 197 | def get_id(self): 198 | return self.tweet['id'] 199 | 200 | def get_raw_text(self): 201 | return self.tweet['text'] 202 | 203 | def _process_text(self): 204 | text = strip_html(self.tweet['text']) 205 | user_mentions = self.tweet['entities']['user_mentions'] 206 | text = text.replace('@', 'at ') 207 | for user in user_mentions: 208 | text = text.replace(user['screen_name'], user['name']) 209 | return text 210 | 211 | def get_screen_name(self): 212 | return self.tweet['user']['screen_name'] 213 | 214 | def get_user_name(self): 215 | return self.tweet['user']['name'] 216 | 217 | def read_out(self, index): 218 | text = self._process_text() 219 | return "tweet number {num} by {user} : {text} ,".format(num=index+1, 220 | user=self.get_user_name(), 221 | text = text) 222 | 223 | def detailed_description(self): 224 | response_builder = ["This tweet was posted by {user_name} whose twitter handle is {screen_name} the account description reads: {description}." 225 | .format(screen_name=self.tweet['user']['screen_name'], 226 | user_name=self.tweet['user']['name'], 227 | description=self.tweet['user']['description'])] 228 | if self.tweet['retweeted']: 229 | response_builder += ["It's been retweeted {} times.".format(self.tweet['retweet_count'])] 230 | if self.tweet['favorited']: 231 | response_builder += ["{} people have favorited it.".format(self.tweet['favorites_count'])] 232 | if self.tweet["in_reply_to_screen_name"]: 233 | response_builder += ["it was posted in response to user {}.".format(self.tweet['in_reply_to_screen_name'])] 234 | response_builder += ["the text of the tweet is, {}.".format(self._process_text())] 235 | return " ".join(response_builder) 236 | 237 | def user_mentions(self): 238 | return self.tweet['user_mentions'] 239 | 240 | 241 | def get_cached_access_pair(uid): 242 | if uid in local_cache.users(): 243 | access_token = local_cache.get_user_state(uid)['access_token'] 244 | access_secret = local_cache.get_user_state(uid)['access_secret'] 245 | return access_token, access_secret 246 | else: 247 | raise ValueError 248 | 249 | 250 | def get_request_token(callback_url=None): 251 | url = "https://api.twitter.com/oauth/request_token" 252 | consumer_key, consumer_secret = local_cache.get_server_state()['twitter_keys'] 253 | 254 | auth = OAuth1(consumer_key, consumer_secret) 255 | params = { "oauth_callback" : callback_url } 256 | r = requests.post(url, auth=auth, params=params) 257 | response_obj = parse_qs(r.text) 258 | local_cache.update_server_state({ "request_token" : response_obj['oauth_token'][0], 259 | "request_secret": response_obj['oauth_token_secret'][0] }) 260 | return response_obj['oauth_token_secret'], response_obj['oauth_token'] 261 | 262 | 263 | def authenticate_user_page(callback_url="", metadata=None): 264 | url = "https://api.twitter.com/oauth/authenticate" 265 | oauth_secret, oauth_token = get_request_token(callback_url) 266 | local_cache.update_server_state({'metadata' : metadata }) 267 | 268 | params = { "force_login" : True, 269 | "oauth_token": oauth_token } 270 | r = requests.get(url, params=params) 271 | return r.text 272 | 273 | 274 | def post_tweet(user_id, message, additional_params={}): 275 | """ 276 | Helper function to post a tweet 277 | """ 278 | url = "https://api.twitter.com/1.1/statuses/update.json" 279 | params = { "status" : message } 280 | params.update(additional_params) 281 | r = make_twitter_request(url, user_id, params, request_type='POST') 282 | print (r.text) 283 | return "Successfully posted a tweet {}".format(message) 284 | 285 | 286 | def get_access_token(oauth_token, oauth_verifier): 287 | url = "https://api.twitter.com/oauth/access_token" 288 | params = {"oauth_verifier" : oauth_verifier} 289 | 290 | server_state = local_cache.get_server_state() 291 | request_token = server_state['request_token'] 292 | request_secret = server_state['request_secret'] 293 | consumer_key, consumer_secret = server_state['twitter_keys'] 294 | 295 | auth = OAuth1(consumer_key, consumer_secret, request_token, request_secret) 296 | 297 | r = requests.post(url, params = params, auth=auth) 298 | response_obj = parse_qs(r.text) 299 | 300 | uid = response_obj['oauth_token'][0] 301 | print ("Access token", uid) 302 | 303 | 304 | local_cache.set_user_state(user_id = uid, 305 | state = { "access_token" : response_obj['oauth_token'][0], 306 | "access_secret" : response_obj['oauth_token_secret'][0], 307 | 'twitter_user_id': response_obj['user_id'][0], 308 | 'screen_name' : response_obj ['screen_name'][0] 309 | }) 310 | local_cache.serialize() 311 | 312 | fragments = { 313 | "state" : local_cache.get_server_state()['metadata']['state'], 314 | "access_token" : uid, 315 | "token_type" : "Bearer" 316 | } 317 | return urlencode(fragments) 318 | 319 | 320 | 321 | 322 | def get_twitter_auth(user_id): 323 | consumer_key, consumer_secret = local_cache.get_server_state()['twitter_keys'] 324 | access_token, access_secret = get_cached_access_pair(user_id) 325 | return OAuth1(consumer_key, consumer_secret, access_token, access_secret) 326 | 327 | 328 | def process_tweets(tweet_list): 329 | """ Clean tweets and enumerate, preserving only things that we are interested in """ 330 | return [Tweet(tweet) for tweet in tweet_list] 331 | 332 | 333 | def make_twitter_request(url, user_id, params={}, request_type='GET'): 334 | """ Generically make a request to twitter API using a particular user's authorization """ 335 | if request_type == "GET": 336 | return requests.get(url, auth=get_twitter_auth(user_id), params=params) 337 | elif request_type == "POST": 338 | return requests.post(url, auth=get_twitter_auth(user_id), params=params) 339 | 340 | 341 | 342 | def get_user_twitter_details(user_id, params={}): 343 | url = "https://api.twitter.com/1.1/users/lookup.json" 344 | user_cache = local_cache.get_user_state(user_id) 345 | params.update({"user_id": user_cache['twitter_user_id'] }) 346 | response = make_twitter_request(url, user_id, params) 347 | return response.json() 348 | 349 | 350 | def geo_search(user_id, search_location): 351 | """ 352 | Search for a location - free form 353 | """ 354 | url = "https://api.twitter.com/1.1/geo/search.json" 355 | params = {"query" : search_location } 356 | response = make_twitter_request(url, user_id, params).json() 357 | return response 358 | 359 | 360 | def closest_trend_search(user_id, params={}): 361 | #url = "https://api.twitter.com/1.1/trends/place.json" 362 | url = "https://api.twitter.com/1.1/trends/closest.json" 363 | response = make_twitter_request(url, user_id, params).json() 364 | return response 365 | 366 | 367 | def list_trends(user_id, woe_id): 368 | url = "https://api.twitter.com/1.1/trends/place.json" 369 | params = { "id" : woe_id } 370 | response = make_twitter_request(url, user_id, params).json() 371 | return response 372 | 373 | 374 | def read_out_tweets(processed_tweets, speech_convertor=None): 375 | """ 376 | Input - list of processed 'Tweets' 377 | output - list of spoken responses 378 | """ 379 | return ["tweet number {num} by {user}. {text}.".format(num=index+1, user=user, text=text) 380 | for index, (user, text) in enumerate(processed_tweets)] 381 | 382 | 383 | def request_tweet_list(url, user_id, params={}): 384 | return process_tweets(make_twitter_request(url, user_id).json()) 385 | 386 | 387 | def get_home_tweets(user_id, input_params={}): 388 | url = "https://api.twitter.com/1.1/statuses/home_timeline.json" 389 | print ("Trying to get home tweets") 390 | response = request_tweet_list(url, user_id) 391 | return response 392 | 393 | 394 | def get_retweets_of_me(user_id, input_params={}): 395 | """ returns recently retweeted tweets """ 396 | url = "https://api.twitter.com/1.1/statuses/retweets_of_me.json" 397 | print ("trying to get retweets") 398 | return request_tweet_list(url, user_id) 399 | 400 | 401 | def get_my_favourite_tweets(user_id, input_params = {}): 402 | """ Returns a user's favourite tweets """ 403 | url = "https://api.twitter.com/1.1/favorites/list.json" 404 | return request_tweet_list(url, user_id) 405 | 406 | 407 | def get_user_latest_tweets(user_id, params={}): 408 | url = "https://api.twitter.com/1.1/statuses/user_timeline.json?" 409 | return request_tweet_list(url, user_id, params) 410 | 411 | 412 | def get_latest_twitter_mentions(user_id): 413 | url = "https://api.twitter.com/1.1/statuses/mentions_timeline.json" 414 | return request_tweet_list(url, user_id) 415 | 416 | 417 | def search_for_tweets_about(user_id, params): 418 | """ Search twitter API """ 419 | url = "https://api.twitter.com/1.1/search/tweets.json" 420 | response = make_twitter_request(url, user_id, params) 421 | return process_tweets(response.json()["statuses"]) 422 | -------------------------------------------------------------------------------- /examples/useful_science/lambda_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this file we specify default event handlers which are then populated into the handler map using metaprogramming 3 | Copyright Anjishnu Kumar 2015 4 | Happy Hacking! 5 | """ 6 | 7 | from ask import alexa 8 | import useful_science 9 | 10 | def lambda_handler(request_obj, context={}): 11 | ''' All requests start here ''' 12 | return alexa.route_request(request_obj) 13 | 14 | @alexa.default 15 | def default_handler(request): 16 | """ The default handler gets invoked if no handler is set for a request """ 17 | return alexa.create_response(message="Just ask") 18 | 19 | 20 | @alexa.request("LaunchRequest") 21 | def launch_request_handler(request): 22 | return alexa.create_response(message="Welcome to Useful Science. We summarize complicated " 23 | "scientific publications in easy to understand forms!", 24 | reprompt_message='What would you like to know more about? ' 25 | 'You can ask for the latest posts or posts about a particular topic') 26 | 27 | 28 | @alexa.request(request_type="SessionEndedRequest") 29 | def session_ended_request_handler(request): 30 | return alexa.create_response(message="Goodbye!") 31 | 32 | @alexa.intent('GetPosts') 33 | def get_posts_intent_handler(request): 34 | 35 | def resolve_slots(text): 36 | if text in useful_science.categories: 37 | return text 38 | return 'new' 39 | 40 | category_text = request.slots['Category'] 41 | category = resolve_slots(category_text) 42 | post = useful_science.post_cache.get_post(category) 43 | 44 | card_content = "{0} Link: {1}".format(post['summary'], 45 | post['permalink']) 46 | 47 | card = alexa.create_card(title=post['meta_title'], 48 | subtitle=post['categories'], 49 | content=card_content) 50 | 51 | return alexa.create_response(message=post['summary'], 52 | end_session=True, 53 | card_obj=card) 54 | 55 | 56 | @alexa.intent('AMAZON.HelpIntent') 57 | def help_intent_handler(request): 58 | cat_list = [cat for cat in useful_science.categories] 59 | pre = cat_list[:-1] 60 | post = cat_list[-1:] 61 | formatted = " ".join(map(lambda x : x+",", pre) + ['and'] + post) 62 | message = ["You can ask for posts in the following categories - ", 63 | formatted] 64 | return alexa.create_response(message=' '.join(message)) 65 | 66 | 67 | @alexa.intent('AMAZON.StopIntent') 68 | def stop_intent_handler(request): 69 | return alexa.create_response(message="Goodbye!") 70 | -------------------------------------------------------------------------------- /examples/useful_science/speech_assets/custom_slots/Categories.list: -------------------------------------------------------------------------------- 1 | new 2 | creativity 3 | fitness 4 | health 5 | happiness 6 | nutrition 7 | productivity 8 | sleep 9 | persuasion 10 | parenting 11 | education -------------------------------------------------------------------------------- /examples/useful_science/speech_assets/intent_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "slots": [ 5 | { 6 | "type": "Categories", 7 | "name": "Category" 8 | } 9 | ], 10 | "intent": "GetPosts" 11 | }, 12 | { 13 | "slots": [], 14 | "intent": "AMAZON.HelpIntent" 15 | }, 16 | { 17 | "slots": [], 18 | "intent": "AMAZON.StopIntent" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/useful_science/speech_assets/utterances.txt: -------------------------------------------------------------------------------- 1 | GetPosts tell me the something 2 | GetPosts tell me something new 3 | GetPosts tell me about {Category} 4 | GetPosts what is new in {Category} 5 | GetPosts something about {Category} 6 | GetPosts {Category} fact 7 | GetPosts something useful 8 | GetPosts get me a {Category} fact 9 | GetPosts {Category} information 10 | GetPosts {Category} info 11 | GetPosts tell me the something interesting 12 | AMAZON.HelpIntent what are the categories 13 | AMAZON.HelpIntent what can I ask about 14 | -------------------------------------------------------------------------------- /examples/useful_science/useful_science.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Wrappers around useful science API calls 3 | ''' 4 | 5 | import requests 6 | from datetime import datetime 7 | import random 8 | import json 9 | 10 | categories = { 11 | "new" : 0, 12 | "creativity": 1, 13 | "fitness": 2, 14 | "health": 3, 15 | "happiness": 4, 16 | "nutrition": 5, 17 | "productivity": 6, 18 | "sleep": 7, 19 | "persuasion": 8, 20 | "parenting": 9, 21 | "education": 10 22 | } 23 | 24 | 25 | def most_recent_25_posts(): 26 | end_point = 'http://www.usefulscience.org/api/posts' 27 | return requests.get(end_point).json()['posts'] 28 | 29 | def most_recent_25_posts_by_category(category_id): 30 | if not category_id: 31 | return most_recent_25_posts() 32 | end_point = "http://www.usefulscience.org/api/posts/{}".format(category_id) 33 | response = requests.get(end_point) 34 | return response.json()['posts'] 35 | 36 | 37 | ''' Simple cache to keep updated with API ''' 38 | _1_DAY = 60 * 60 * 24 39 | _1_MINUTE = 60 40 | 41 | class SimplePostsCache(object): 42 | ''' 43 | Seconds 44 | ''' 45 | def __init__(self, refresh_rate=_1_MINUTE): 46 | 47 | self.cache = {cat_id : list() 48 | for cat_name, cat_id in categories.items()} 49 | self.refresh_rate = refresh_rate # Seconds 50 | # Splitting refresh times by category so that one unlucky person doesn't 51 | # have to wait for 10 API calls to complete 52 | self.last_refresh = {cat_id : datetime.now() 53 | for cat_name, cat_id in categories.items()} 54 | for cat_id in self.last_refresh: 55 | self.refresh_cache(cat_id) 56 | 57 | def refresh_cache(self, cat_id): 58 | ''' 59 | Repopulate cache 60 | ''' 61 | self.cache[cat_id] = most_recent_25_posts_by_category(cat_id) 62 | self.last_refresh[cat_id] = datetime.now() 63 | print ('Cache refresh at...', str(self.last_refresh[cat_id])) 64 | # print (json.dumps(self.cache, indent=4)) 65 | 66 | 67 | def get_post(self, category): 68 | cat_id = categories[category] 69 | if ((datetime.now() - self.last_refresh[cat_id]).seconds 70 | > self.refresh_rate): 71 | # Time for a refresh ! 72 | self.refresh_cache(cat_id) 73 | return random.choice(self.cache[cat_id])['post'] 74 | 75 | 76 | post_cache = SimplePostsCache(_1_MINUTE) 77 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this file we specify default event handlers which are then populated into the handler map using metaprogramming 3 | Copyright Anjishnu Kumar 2015 4 | Happy Hacking! 5 | """ 6 | 7 | from ask import alexa 8 | 9 | def lambda_handler(request_obj, context=None): 10 | ''' 11 | This is the main function to enter to enter into this code. 12 | If you are hosting this code on AWS Lambda, this should be the entry point. 13 | Otherwise your server can hit this code as long as you remember that the 14 | input 'request_obj' is JSON request converted into a nested python object. 15 | ''' 16 | 17 | metadata = {'user_name' : 'SomeRandomDude'} # add your own metadata to the request using key value pairs 18 | 19 | ''' inject user relevant metadata into the request if you want to, here. 20 | e.g. Something like : 21 | ... metadata = {'user_name' : some_database.query_user_name(request.get_user_id())} 22 | 23 | Then in the handler function you can do something like - 24 | ... return alexa.create_response('Hello there {}!'.format(request.metadata['user_name'])) 25 | ''' 26 | return alexa.route_request(request_obj, metadata) 27 | 28 | 29 | @alexa.default 30 | def default_handler(request): 31 | """ The default handler gets invoked if no handler is set for a request type """ 32 | return alexa.respond('Just ask').with_card('Hello World') 33 | 34 | 35 | @alexa.request("LaunchRequest") 36 | def launch_request_handler(request): 37 | ''' Handler for LaunchRequest ''' 38 | return alexa.create_response(message="Hello Welcome to My Recipes!") 39 | 40 | 41 | @alexa.request("SessionEndedRequest") 42 | def session_ended_request_handler(request): 43 | return alexa.create_response(message="Goodbye!") 44 | 45 | 46 | @alexa.intent('GetRecipeIntent') 47 | def get_recipe_intent_handler(request): 48 | """ 49 | You can insert arbitrary business logic code here 50 | """ 51 | 52 | # Get variables like userId, slots, intent name etc from the 'Request' object 53 | ingredient = request.slots["Ingredient"] # Gets an Ingredient Slot from the Request object. 54 | 55 | if ingredient == None: 56 | return alexa.create_response("Could not find an ingredient!") 57 | 58 | # All manipulations to the request's session object are automatically reflected in the request returned to Amazon. 59 | # For e.g. This statement adds a new session attribute (automatically returned with the response) storing the 60 | # Last seen ingredient value in the 'last_ingredient' key. 61 | 62 | request.session['last_ingredient'] = ingredient # Automatically returned as a sessionAttribute 63 | 64 | # Modifying state like this saves us from explicitly having to return Session objects after every response 65 | 66 | # alexa can also build cards which can be sent as part of the response 67 | card = alexa.create_card(title="GetRecipeIntent activated", subtitle=None, 68 | content="asked alexa to find a recipe using {}".format(ingredient)) 69 | 70 | return alexa.create_response("Finding a recipe with the ingredient {}".format(ingredient), 71 | end_session=False, card_obj=card) 72 | 73 | 74 | 75 | @alexa.intent('NextRecipeIntent') 76 | def next_recipe_intent_handler(request): 77 | """ 78 | You can insert arbitrary business logic code here 79 | """ 80 | return alexa.create_response(message="Getting Next Recipe ... 123") 81 | 82 | 83 | if __name__ == "__main__": 84 | 85 | import argparse 86 | parser = argparse.ArgumentParser() 87 | parser.add_argument('--serve','-s', action='store_true', default=False) 88 | args = parser.parse_args() 89 | 90 | if args.serve: 91 | ### 92 | # This will only be run if you try to run the server in local mode 93 | ## 94 | print('Serving ASK functionality locally.') 95 | import flask 96 | server = flask.Flask(__name__) 97 | @server.route('/') 98 | def alexa_skills_kit_requests(): 99 | request_obj = flask.request.get_json() 100 | return lambda_handler(request_obj) 101 | server.run() 102 | 103 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | where=./tests/ 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='ask-alexa-pykit', 4 | version='0.5.6', 5 | description="Minimalist SDK for developing skills for Amazon's Alexa Skills Kit for Amazon Echo Dot Tap FireTV etc", 6 | author='Anjishnu Kumar', 7 | author_email='anjishnu.kr@gmail.com', 8 | url='https://github.com/anjishnu/ask-alexa-pykit', 9 | packages=['ask', 'ask.config'], 10 | keywords=['alexa', 'amazon echo', 'ask', 'ask-alexa-pykit', 'skill'], 11 | package_data={'ask.config': ['../data/*']}, 12 | license='MIT', 13 | ) 14 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | This folder contains dedicated unit tests and test data -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjishnu/ask-alexa-pykit/a47c278ca7a60532bbe1a9b789f6c37e609fea8b/tests/__init__.py -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | import ask 7 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from requests import * 2 | -------------------------------------------------------------------------------- /tests/fixtures/requests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | TEST_FULL_REQUEST_DICT = { 4 | "session": { 5 | "sessionId": "SessionId.d461672c-2997-4d9d-9a8c-a67834acb9aa", 6 | "application": { 7 | "applicationId": "amzn1.echo-sdk-ams.app.a306b3a3-3331-43c1-87bd-87d29d16fac8" 8 | }, 9 | "user": { 10 | "userId": "amzn1.account.AGBATYSC32Y2QVDQKOWJUUJNEYFA", 11 | "accessToken": "fillertoken-fix-later" 12 | }, 13 | "new": True 14 | }, 15 | "request": { 16 | "type": "IntentRequest", 17 | "requestId": "EdwRequestId.b22db637-b8f9-43c0-ae0c-1a9b35a02610", 18 | "timestamp": 1447911387582, 19 | "intent": { 20 | "name": "YesIntent", 21 | "slots": { 22 | "example1": { 23 | "value": "value1" 24 | }, 25 | "example2": { 26 | "value": "value2" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | TEST_SPARSE_REQUEST_DICT = { 34 | "session": { 35 | "sessionId": "SessionId.d461672c-2997-4d9d-9a8c-a67834acb9aa", 36 | "application": { 37 | "applicationId": "amzn1.echo-sdk-ams.app.a306b3a3-3331-43c1-87bd-87d29d16fac8" 38 | }, 39 | "user": { 40 | "userId": "amzn1.account.AGBATYSC32Y2QVDQKOWJUUJNEYFA", 41 | }, 42 | "new": True 43 | }, 44 | "request": { 45 | "type": "IntentRequest", 46 | "requestId": "EdwRequestId.b22db637-b8f9-43c0-ae0c-1a9b35a02610", 47 | "timestamp": 1447911387582, 48 | } 49 | } 50 | 51 | TEST_SESSION_ENDED_REQUEST = { 52 | "version": "1.0", 53 | "session": { 54 | "new": False, 55 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 56 | "application": { 57 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 58 | }, 59 | "attributes": { 60 | "supportedHoroscopePeriods": { 61 | "daily": True, 62 | "weekly": False, 63 | "monthly": False 64 | } 65 | }, 66 | "user": { 67 | "userId": "amzn1.account.AM3B00000000000000000000000" 68 | } 69 | }, 70 | "request": { 71 | "type": "SessionEndedRequest", 72 | "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 73 | "timestamp": "2015-05-13T12:34:56Z", 74 | "reason": "USER_INITIATED" 75 | } 76 | } 77 | 78 | TEST_LAUNCH_REQUEST = { 79 | "version": "1.0", 80 | "session": { 81 | "new": True, 82 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 83 | "application": { 84 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 85 | }, 86 | "attributes": {}, 87 | "user": { 88 | "userId": "amzn1.account.AM3B00000000000000000000000" 89 | } 90 | }, 91 | "request": { 92 | "type": "LaunchRequest", 93 | "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 94 | "timestamp": "2015-05-13T12:34:56Z" 95 | } 96 | } 97 | 98 | 99 | TEST_INTENT_REQUEST = { 100 | "version": "1.0", 101 | "session": { 102 | "new": False, 103 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 104 | "application": { 105 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 106 | }, 107 | "attributes": { 108 | "supportedHoroscopePeriods": { 109 | "daily": True, 110 | "weekly": False, 111 | "monthly": False 112 | } 113 | }, 114 | "user": { 115 | "userId": "amzn1.account.AM3B00000000000000000000000" 116 | } 117 | }, 118 | "request": { 119 | "type": "IntentRequest", 120 | "requestId": " amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 121 | "timestamp": "2015-05-13T12:34:56Z", 122 | "intent": { 123 | "name": "GetZodiacHoroscopeIntent", 124 | "slots": { 125 | "ZodiacSign": { 126 | "name": "ZodiacSign", 127 | "value": "virgo" 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | UNRECOGNIZED_INTENT_REQUEST = { 135 | "version": "1.0", 136 | "session": { 137 | "new": False, 138 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", 139 | "application": { 140 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 141 | }, 142 | "attributes": { 143 | "supportedHoroscopePeriods": { 144 | "daily": True, 145 | "weekly": False, 146 | "monthly": False 147 | } 148 | }, 149 | "user": { 150 | "userId": "amzn1.account.AM3B00000000000000000000000" 151 | } 152 | }, 153 | "request": { 154 | "type": "IntentRequest", 155 | "requestId": " amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", 156 | "timestamp": "2015-05-13T12:34:56Z", 157 | "intent": { 158 | "name": "thisisnottheintentyouarelookingfor", 159 | "slots": { 160 | "ZodiacSign": { 161 | "name": "ZodiacSign", 162 | "value": "virgo" 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/test-data/sample_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "sessionId": "SessionId.d461672c-2997-4d9d-9a8c-a67834acb9aa", 4 | "application": { 5 | "applicationId": "amzn1.echo-sdk-ams.app.a306b3a3-3331-43c1-87bd-87d29d16fac8" 6 | }, 7 | "user": { 8 | "userId": "amzn1.account.AGBATYSC32Y2QVDQKOWJUUJNEYFA" 9 | }, 10 | "new": true 11 | }, 12 | "request": { 13 | "type": "IntentRequest", 14 | "requestId": "EdwRequestId.b22db637-b8f9-43c0-ae0c-1a9b35a02610", 15 | "timestamp": 1447911387582, 16 | "intent": { 17 | "name": "NextRecipeIntent", 18 | "slots": {} 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/test_alexa.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_equal, assert_dict_equal, assert_true 2 | 3 | from .context import ask 4 | from .fixtures.requests import (TEST_SESSION_ENDED_REQUEST, TEST_LAUNCH_REQUEST, 5 | TEST_INTENT_REQUEST, UNRECOGNIZED_INTENT_REQUEST) 6 | 7 | 8 | class TestVoiceHandler(object): 9 | 10 | def teardown(self): 11 | reload(ask) 12 | 13 | def test_defaults_on_init(self): 14 | assert_equal(ask.alexa._default, '_default_') 15 | assert_dict_equal(ask.alexa._handlers, {"IntentRequest": {}}) 16 | 17 | def test_default_handler_decorator(self): 18 | 19 | @ask.alexa.default 20 | def some_logic(): 21 | pass 22 | 23 | stored_function = ask.alexa._handlers[ask.alexa._default] 24 | assert_equal(stored_function, some_logic) 25 | 26 | def test_request_handler_decorator(self): 27 | request_type = "bar" 28 | 29 | @ask.alexa.request(request_type) 30 | def request_logic(): 31 | pass 32 | 33 | stored_function = ask.alexa._handlers[request_type] 34 | assert_equal(stored_function, request_logic) 35 | 36 | def test_intent_handler_decorator(self): 37 | intent_type = "baz" 38 | 39 | @ask.alexa.intent(intent_type) 40 | def intent_logic(): 41 | pass 42 | 43 | stored_function = ask.alexa._handlers['IntentRequest'][intent_type] 44 | assert_equal(stored_function, intent_logic) 45 | 46 | 47 | class TestVoiceHandlerRouteRequest(object): 48 | 49 | @classmethod 50 | def setUpClass(cls): 51 | 52 | @ask.alexa.intent('GetZodiacHoroscopeIntent') 53 | def intent_logic(request): 54 | return ask.Response({'intent_handler_called': True}) 55 | 56 | @ask.alexa.default 57 | def default_logic(request): 58 | return ask.Response({'default_handler_called': True}) 59 | 60 | @ask.alexa.request('SessionEndedRequest') 61 | def request_logic(request): 62 | return ask.Response({'request_handler_called': True}) 63 | 64 | 65 | @classmethod 66 | def tearDownClass(cls): 67 | reload(ask) # in case these tests get run before TestVoiceHandler 68 | 69 | def test_routes_to_default_handler(self): 70 | req_json = UNRECOGNIZED_INTENT_REQUEST 71 | 72 | response = ask.alexa.route_request(req_json) 73 | assert_true(response['default_handler_called']) 74 | 75 | def test_routes_to_intent_handler(self): 76 | req_json = TEST_INTENT_REQUEST 77 | 78 | response = ask.alexa.route_request(req_json) 79 | assert_true(response['intent_handler_called']) 80 | 81 | def test_routes_to_request_handler(self): 82 | req_json = TEST_SESSION_ENDED_REQUEST 83 | 84 | response = ask.alexa.route_request(req_json) 85 | assert_true(response['request_handler_called']) 86 | -------------------------------------------------------------------------------- /tests/test_request.py: -------------------------------------------------------------------------------- 1 | from unittest import skip 2 | 3 | from nose.tools import * 4 | 5 | from .context import ask 6 | from .fixtures.requests import TEST_FULL_REQUEST_DICT, TEST_SPARSE_REQUEST_DICT 7 | 8 | 9 | 10 | class TestStandardRequest(object): 11 | 12 | def setUp(self): 13 | self.example = ask.Request(TEST_FULL_REQUEST_DICT) 14 | 15 | def tearDown(self): 16 | self.example = None 17 | 18 | def test_request_stores_request_dict(self): 19 | assert_equal(self.example.request, TEST_FULL_REQUEST_DICT) 20 | 21 | def test_request_stores_metadata(self): 22 | metadata = {'cute': 'puppy'} 23 | r = ask.Request(TEST_FULL_REQUEST_DICT, metadata=metadata) 24 | 25 | assert_equal(r.metadata, metadata) 26 | 27 | def test_request_metadata_is_blank_if_not_provided(self): 28 | assert_equal(self.example.metadata, {}) 29 | 30 | def test_request_returns_request_type(self): 31 | req_type = self.example.request_type() 32 | 33 | assert_equal(req_type, 'IntentRequest') 34 | 35 | def test_request_returns_intent_name(self): 36 | intent_name = self.example.intent_name() 37 | 38 | assert_equal(intent_name, 'YesIntent') 39 | 40 | def test_request_is_intent(self): 41 | res = self.example.is_intent() 42 | 43 | assert_true(res) 44 | 45 | def test_request_returns_user_id(self): 46 | user_id = self.example.user_id() 47 | 48 | assert_equal(user_id, "amzn1.account.AGBATYSC32Y2QVDQKOWJUUJNEYFA") 49 | 50 | def test_request_returns_access_token(self): 51 | token = self.example.access_token() 52 | 53 | assert_equal(token, "fillertoken-fix-later") 54 | 55 | def test_request_returns_session_id(self): 56 | session_id = self.example.session_id() 57 | 58 | assert_equal(session_id, "SessionId.d461672c-2997-4d9d-9a8c-a67834acb9aa") 59 | 60 | def test_request_returns_slot_value(self): 61 | val1 = self.example.get_slot_value("example1") 62 | val2 = self.example.get_slot_value("example2") 63 | 64 | assert_equal(val1, "value1") 65 | assert_equal(val2, "value2") 66 | 67 | def test_request_returns_slot_names(self): 68 | names = self.example.get_slot_names() 69 | 70 | assert_items_equal(names, ["example1", "example2"]) 71 | 72 | def test_request_returns_slot_map(self): 73 | slot_map = self.example.get_slot_map() 74 | expected = {'example1': 'value1', 'example2': 'value2'} 75 | 76 | assert_equal(slot_map, expected) 77 | 78 | def test_request_slots_property_assigned_on_init(self): 79 | slot_map = self.example.get_slot_map() 80 | slots = self.example.slots 81 | 82 | assert_equal(slots, slot_map) 83 | assert_is_not_none(slots) 84 | 85 | 86 | class TestSparseRequest(object): 87 | def setUp(self): 88 | self.example = ask.Request(TEST_SPARSE_REQUEST_DICT) 89 | 90 | def tearDown(self): 91 | self.example = None 92 | 93 | def test_intent_name_with_no_intent(self): 94 | assert_is_none(self.example.intent_name()) 95 | 96 | def test_is_intent_returns_False_with_no_intent(self): 97 | assert_false(self.example.is_intent()) 98 | 99 | def test_access_token_returns_None(self): 100 | assert_is_none(self.example.access_token()) 101 | 102 | def test_slot_value_returns_None(self): 103 | assert_is_none(self.example.access_token()) 104 | 105 | def test_slot_names_returns_empty_list(self): 106 | assert_equal(self.example.get_slot_names(), []) 107 | 108 | def test_slot_map_returns_empty_dict(self): 109 | assert_equal(self.example.get_slot_map(), {}) 110 | 111 | 112 | class TestEmptyRequest(object): 113 | 114 | #@raises(KeyError) 115 | @skip('Unsure proper functionality. Pass or raise better error?') 116 | def test_empty_request(self): 117 | ask.Request({}) 118 | -------------------------------------------------------------------------------- /tests/test_response_builder.py: -------------------------------------------------------------------------------- 1 | from unittest import skip 2 | 3 | from nose.tools import assert_equal, assert_dict_equal 4 | 5 | from .context import ask 6 | 7 | RAW_TEMPLATE = { 8 | "version": "1.0", 9 | "response": { 10 | "outputSpeech": { 11 | "type": "PlainText", 12 | "text": "Some default text goes here." 13 | }, 14 | "shouldEndSession": False 15 | } 16 | } 17 | 18 | 19 | class TestResponeHandler(object): 20 | def setup(self): 21 | self.default_speech = {"outputSpeech": {"type": "PlainText", 22 | "text": None} 23 | } 24 | 25 | def test_response_builder_stores_base_response(self): 26 | assert_equal(RAW_TEMPLATE, ask.ResponseBuilder.base_response) 27 | 28 | def test_speech_defaults(self): 29 | output = ask.ResponseBuilder.create_speech() 30 | 31 | assert_dict_equal(output, self.default_speech) 32 | 33 | def test_speech_takes_message(self): 34 | message = 'My New Message' 35 | output = ask.ResponseBuilder.create_speech(message=message) 36 | 37 | assert_equal(output['outputSpeech']['text'], message) 38 | 39 | def test_speech_can_return_ssml_message(self): 40 | message = 'Yet another message' 41 | 42 | output = ask.ResponseBuilder.create_speech(message=message, is_ssml=True) 43 | 44 | assert_equal(output['outputSpeech']['type'], 'SSML') 45 | assert_equal(output['outputSpeech']['ssml'], message) 46 | 47 | def test_create_card_defaults(self): 48 | card = ask.ResponseBuilder.create_card() 49 | 50 | assert_dict_equal(card, {'type': 'Simple'}) 51 | 52 | def test_create_card_adds_kwargs_when_present(self): 53 | expected = {'type': 'Simple', 'title': 'Welcome'} 54 | output = ask.ResponseBuilder.create_card(title='Welcome') 55 | assert_dict_equal(output, expected) 56 | 57 | expected = {'type': 'Simple', 'subtitle': 'some words'} 58 | output = ask.ResponseBuilder.create_card(subtitle='some words') 59 | assert_dict_equal(output, expected) 60 | 61 | expected = {'type': 'Simple', 'content': 'interesting info'} 62 | output = ask.ResponseBuilder.create_card(content='interesting info') 63 | assert_dict_equal(output, expected) 64 | 65 | expected = {'type': 'Something else'} 66 | output = ask.ResponseBuilder.create_card(card_type='Something else') 67 | assert_dict_equal(output, expected) 68 | 69 | @skip('Until it actually tests something useful') 70 | def test_create_response_defaults(self): 71 | output = ask.ResponseBuilder.create_response() 72 | 73 | assert_dict_equal(ask.ResponseBuilder.base_response, output) 74 | --------------------------------------------------------------------------------