Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/conversation flow #2

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
language: python
python:
- "3.5"
- "3.6"
- "3.5-dev"
- "3.6-dev"
addons:
apt:
packages:
- portaudio19-dev
install:
- python setup.py install
script:
- make test
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ test:
python3 setup.py test

clean:
find . -name "*.pyc" -delete
find . -name "__pycache__" -delete
find . -name "*.pyc" -exec rm -rf {} \+
find . -name "__pycache__" -exec rm -rf {} \+
rm -rf .pytest_cache htmlcov .coverage

clean-all: clean
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The SDK is currently in a pre-alpha release phase. Bugs and limited functionalit

## Installation

**The Recommended Python version is 3.0+**
**The Recommended Python version is 3.5+**

The Python SDK currently does not bundle the necessary system headers and binaries to interact with audio hardware in a cross-platform manner. For this reason, before using the SDK, you need to install `PortAudio`:

Expand Down Expand Up @@ -254,9 +254,11 @@ from auroraapi.dialog import Dialog

def udf(context):
# get data for a particular step
data = context.get_step_data("step_id")
data = context.get_step("step_id")
# set some custom data
context.set_user_data("id", "some data value")
context.set_data("id", "some data value")
# you can get the data later
assert context.get_data("id") == "some data value"
# return True to take the upward branch in the dialog builder
return True

Expand All @@ -275,9 +277,11 @@ from auroraapi.dialog import Dialog
def handle_update(context):
# this function is called whenever the current step is changed or
# whenever the data in the context is updated
# you can get the current dialog step like this
step = context.get_current_step()
print(step, context)
#
# you can get the current and previous dialog steps like this
curr = context.get_current_step()
prev = context.get_current_step()
print(curr, prev, context)

dialog = Dialog("DIALOG_ID", on_context_update=handle_update)
dialog.run()
Expand Down
72 changes: 0 additions & 72 deletions auroraapi/api.py

This file was deleted.

85 changes: 85 additions & 0 deletions auroraapi/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from auroraapi.api.backend import CallParams, Credentials
from auroraapi.audio import AudioFile

DIALOG_URL = "/v1/dialog/"
INTERPRET_URL = "/v1/interpret/"
STT_URL = "/v1/stt/"
TTS_URL = "/v1/tts/"

def get_tts(config, text):
""" Performs a TTS query

Args:
config: an instance of globals.Config that contains the backend to be used
and the credentials to be sent along with the request
text: the text to convert to speech

Returns:
The raw WAV data returned from the TTS service
"""
return config.backend.call(CallParams(
path=TTS_URL,
credentials=Credentials.from_config(config),
query={ "text": text },
chunked=True,
response_type="raw",
))

def get_interpret(config, text, model):
""" Performs an Interpret query

Args:
config: an instance of globals.Config that contains the backend to be used
and the credentials to be sent along with the request
text: the text to interpret
model: the model to use to interpret

Returns:
A dictionary representation of the JSON response from the Interpret service
"""
return config.backend.call(CallParams(
path=INTERPRET_URL,
credentials=Credentials.from_config(config),
query={ "text": text, "model": model },
))

def get_stt(config, audio, stream=False):
""" Performs an STT query

Args:
config: an instance of globals.Config that contains the backend to be used
and the credentials to be sent along with the request
audio: either an instance of an AudioFile or a function that returns a
generator that supplies the audio data to be sent to the backend. If it is
the latter, then the `stream` argument should be set to `True` as well.
stream: pass `True` if the body is a function that returns a generator

Returns:
A dictionary representation of the JSON response from the STT service
"""
return config.backend.call(CallParams(
path=STT_URL,
method="POST",
credentials=Credentials.from_config(config),
# audio can either be an AudioFile (in case all of the data is known) or
# it can be a generator function, which emits data as it gets known. We need
# to modify the request based on whether stream is True, in which case we assume
# that audio is a generator function
body=(audio() if stream else audio.get_wav())
))

def get_dialog(config, id):
""" Gets a dialog from the Dialog service

Args:
config: an instance of globals.Config that contains the backend to be used
and the credentials to be sent along with the request
id: the ID of the dialog to get

Returns:
A dictionary representation of the JSON response from the Dialog service
"""
return config.backend.call(CallParams(
path=DIALOG_URL + id,
credentials=Credentials.from_config(config),
))
104 changes: 104 additions & 0 deletions auroraapi/api/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
class Credentials(object):
""" Wrapper for passing credentials to the backend

This class contains the credentials to be sent with each API request. Normally
the credentials are specified by the developer through the globals._config object,
but that must be converted to an object of this type to be passed to the backend
because they need to be converted to headers

Attributes:
app_id: the application ID ('X-Application-ID' header)
app_token: the application token ('X-Application-Token' header)
device_id: the device ID ('X-Device-ID' header)
"""
def __init__(self, app_id=None, app_token=None, device_id=None):
""" Creates an instance of Credentials with the given information """
self.app_id = app_id
self.app_token = app_token
self.device_id = device_id

@property
def headers(self):
"""
Returns:
A dictionary representation of the credentials to be sent to the
Aurora API service
"""
return {
"X-Application-ID": self.app_id,
"X-Application-Token": self.app_token,
"X-Device-ID": self.device_id,
}

@staticmethod
def from_config(c):
""" Creates a Credentials instance from a globals.Config instance """
return Credentials(c.app_id, c.app_token, c.device_id)

class CallParams(object):
""" Parameters to be used when constructed a call to the backend

This class encapsulates the different parameters that can be used to configure
a backend call and provides sensible defaults

Attributes:
method: the HTTP method to use (default "GET")
path: the path relative to the base URL (default "/")
credentials: an instance of Credentials providing the authorization
credentials to be sent along with the request (default Credentials())
headers: a dictionary of additional headers to be sent (default {})
query: a dictionary of querystring paramters to be encoded into the URL
and sent (default {})
body: any encodeable object or generator function that provides the data
to be sent in the body of the request. If a generator function is provided
then the `chunked` attribute should also be set to True (default None)
chunked: a boolean indicating whether or not to use Transfer-Encoding: chunked
(default False)
response_type: one of ['json', 'raw', 'text']. If 'json', then the call expects
a JSON response and returns a python dictionary containing the JSON data. If
'raw', then the call reads the raw data from the stream and returns it. If
'text', then the call returns the data as-is (default 'json')
"""
def __init__(self, **kwargs):
""" Creates a CallParams object from the given keyword arguments

See the class docstring for the valid keyword arguments, their meanings, and
default values. The attributes and parameters correspond exactly.
"""
self.method = kwargs.get("method", "GET")
self.path = kwargs.get("path", "/")
self.credentials = kwargs.get("credentials", Credentials())
self.headers = kwargs.get("headers", {})
self.query = kwargs.get("query", {})
self.body = kwargs.get("body", None)
self.chunked = kwargs.get("chunked", False)
self.response_type = kwargs.get("response_type", "json")

class Backend(object):
""" An abstract class describing how to execute a call on a particular backend

This class is responsible for executing a call to an arbitary backend given a
CallParams object. It is designed so that it can be swapped out to provide any
kind of behavior and contact any kind of backend (mock, staging, production, etc)

Attributes:
base_url: The base URL of the service to reach
timeout: the timeout (in seconds) to use for the request (default 60)

"""
def __init__(self, base_url, timeout=60):
""" Creates a Backend object and initializes its attributes """
self.base_url = base_url
self.timeout = timeout

def call(self, params):
""" Call the backend with the given parameters

This method must be implemented in subclasses to actually handle a call. If
left unimplemented, it will default to the base class implementation and
raise an exception.

Args:
params: an instance of CallParams, the parameters to use to call the backend
"""
raise NotImplementedError()
56 changes: 56 additions & 0 deletions auroraapi/api/backend/aurora.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import requests, functools, json, inspect
from auroraapi.api.backend import Backend
from auroraapi.errors import APIException

# The base URL to use when contacting the Aurora service
BASE_URL = "https://api.auroraapi.com/v1"

class AuroraBackend(Backend):
""" An implementation of the Backend that calls the Aurora production servers """

def __init__(self, base_url=BASE_URL):
""" Creates a Backend instance with the production server BASE_URL """
super().__init__(base_url)

def call(self, params):
""" Executes a call to the backend with the given parameters

Args:
params: an instance of CallParams, the parameters to use to construct a
request to the backend

Returns:
The response from the backend. Its type is dictated by the `response_type`
property on the `params` object.

Raises:
Any exception that can be the `requests` library can be raised here. In
addition:

APIException: raised when an API error occurs on the backend (status code != 200)
"""
r = requests.request(
params.method,
self.base_url + params.path,
data=params.body,
stream=params.chunked,
params=params.query.items(),
headers={ **params.credentials.headers, **params.headers },
timeout=self.timeout
)

if r.status_code != requests.codes.ok:
if "application/json" in r.headers["content-type"]:
raise APIException(**r.json())
# Handle the special case where nginx doesn't return a JSON response for 413
if r.status_code == 413:
raise APIException(code="RequestEntityTooLarge", message="Request entity too large", status=413, type="RequestEntityTooLarge")
# A non JSON error occurred (very strange)
raise APIException(message=r.text, status=r.status_code)

if params.response_type == "json":
return r.json()
if params.response_type == "raw":
r.raw.read = functools.partial(r.raw.read, decode_content=True)
return r.raw.read()
return r.text
Loading