├── .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 |
--------------------------------------------------------------------------------