Skip to content

Commit e05d027

Browse files
authored
Merge pull request #1 from auroraapi/feature/conversation-flow
Feature/conversation flow
2 parents d092c93 + ed4364a commit e05d027

File tree

16 files changed

+320
-13
lines changed

16 files changed

+320
-13
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,61 @@ for speech in continuously_listen(silence_len=0.5):
224224
# do something to actually turn off the lamp
225225
print("Turning off the lamp")
226226
```
227+
228+
### Dialog Builder
229+
230+
You can create a high-level outline of a conversation using the Dialog Builder, available in the Aurora Dashboard. Once you have create the dialog, its ID will be available to you in the "Conversation Details" sidebar (under "Conversation ID"). You can use this ID to create and run the entire conversation in just a few lines of code:
231+
232+
```python
233+
# Import the package
234+
import auroraapi as aurora
235+
from auroraapi.dialog import Dialog
236+
237+
# Set your application settings
238+
aurora.config.app_id = "YOUR_APP_ID" # put your app ID here
239+
aurora.config.app_token = "YOUR_APP_TOKEN" # put your app token here
240+
241+
# Create the Dialog with the ID from the Dialog Builder
242+
dialog = Dialog("DIALOG_ID")
243+
244+
# Run the dialog
245+
dialog.run()
246+
```
247+
248+
If you've used a UDF (User-Defined Function) in the Dialog Builder, you can write the corresponding function and register it to the dialog using the `set_function` function. The UDF must take one argument, which is the dialog context. You can use it to retrieve data from other steps in the dialog and set custom data for future use (both in UDFs and in the builder).
249+
250+
If the UDF in the dialog builder has branching enabled, then you can return `True` or `False` to control which branch is taken.
251+
252+
```python
253+
from auroraapi.dialog import Dialog
254+
255+
def udf(context):
256+
# get data for a particular step
257+
data = context.get_step_data("step_id")
258+
# set some custom data
259+
context.set_user_data("id", "some data value")
260+
# return True to take the upward branch in the dialog builder
261+
return True
262+
263+
dialog = Dialog("DIALOG_ID")
264+
dialog.set_function("udf_id", udf)
265+
dialog.run()
266+
```
267+
268+
Here, `step_id` is the ID of a step in the dialog builder and `udf_id` is the ID of the UDF you want to register a function for. `id` is an arbitrary string you can use to identify the data you are setting.
269+
270+
You can also set a function when creating the `Dialog` that lets you handle whenever the current step changes or context is changed.
271+
272+
```python
273+
from auroraapi.dialog import Dialog
274+
275+
def handle_update(context):
276+
# this function is called whenever the current step is changed or
277+
# whenever the data in the context is updated
278+
# you can get the current dialog step like this
279+
step = context.get_current_step()
280+
print(step, context)
281+
282+
dialog = Dialog("DIALOG_ID", on_context_update=handle_update)
283+
dialog.run()
284+
```

auroraapi/api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from auroraapi.audio import AudioFile
44

55
BASE_URL = "https://api.auroraapi.com"
6+
# BASE_URL = "http://localhost:3000"
67
TTS_URL = BASE_URL + "/v1/tts/"
78
STT_URL = BASE_URL + "/v1/stt/"
89
INTERPRET_URL = BASE_URL + "/v1/interpret/"
10+
DIALOG_URL = BASE_URL + "/v1/dialog/"
911

1012
class APIException(Exception):
1113
""" Raise an exception when querying the API """
@@ -48,8 +50,8 @@ def get_tts(text):
4850
r.raw.read = functools.partial(r.raw.read, decode_content=True)
4951
return AudioFile(r.raw.read())
5052

51-
def get_interpret(text):
52-
r = requests.get(INTERPRET_URL, params=[("text", text)], headers=get_headers())
53+
def get_interpret(text, model):
54+
r = requests.get(INTERPRET_URL, params=[("text", text), ("model", model)], headers=get_headers())
5355
handle_error(r)
5456
return r.json()
5557

@@ -63,3 +65,8 @@ def get_stt(audio, stream=False):
6365
r = requests.post(STT_URL, data=d, headers=get_headers())
6466
handle_error(r)
6567
return r.json()
68+
69+
def get_dialog(id):
70+
r = requests.get(DIALOG_URL + id, headers=get_headers())
71+
handle_error(r)
72+
return r.json()

auroraapi/dialog/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from auroraapi.dialog.dialog import *

auroraapi/dialog/context.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class DialogContext(object):
2+
def __init__(self, on_update = lambda ctx: None):
3+
self.step = {}
4+
self.user = {}
5+
self.udfs = {}
6+
self.current_step = None
7+
self.on_update = on_update
8+
9+
def set_step_data(self, key, value):
10+
self.step[key] = value
11+
self.on_update(self)
12+
13+
def get_step_data(self, key, default=None):
14+
if not key in self.step:
15+
return default
16+
return self.step[key]
17+
18+
def set_user_data(self, key, value):
19+
self.user[key] = value
20+
self.on_update(self)
21+
22+
def get_user_data(self, key, default=None):
23+
if not key in self.user:
24+
return default
25+
return self.user[key]
26+
27+
def set_current_step(self, step):
28+
self.current_step = step
29+
self.on_update(self)
30+
31+
def get_current_step(self):
32+
return self.current_step

auroraapi/dialog/dialog.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import json
2+
from auroraapi.api import get_dialog
3+
from auroraapi.dialog.context import DialogContext
4+
from auroraapi.dialog.graph import Graph
5+
from auroraapi.dialog.util import assert_callable, parse_date
6+
7+
class DialogProperties(object):
8+
def __init__(self, id, name, desc, appId, dateCreated, dateModified, graph, **kwargs):
9+
self.id = id
10+
self.name = name
11+
self.desc = desc
12+
self.app_id = appId
13+
self.date_created = parse_date(dateCreated)
14+
self.date_modified = parse_date(dateModified)
15+
self.graph = Graph(graph)
16+
17+
class Dialog(object):
18+
def __init__(self, id, on_context_update=None):
19+
self.dialog = DialogProperties(**get_dialog(id)["dialog"])
20+
self.context = DialogContext()
21+
if on_context_update != None:
22+
assert_callable(on_context_update, "The 'on_context_update' parameter must be a function that accepts one argument")
23+
self.context.on_update = on_context_update
24+
25+
def set_function(self, id, func):
26+
assert_callable(func, "Function argument to 'set_function' for ID '{}' must be callable and accept one argument".format(id))
27+
self.context.udfs[id] = func
28+
29+
def run(self):
30+
curr = self.dialog.graph.start
31+
while curr != None and curr in self.dialog.graph.nodes:
32+
step = self.dialog.graph.nodes[curr]
33+
edge = self.dialog.graph.edges[curr]
34+
self.context.set_current_step(step)
35+
curr = step.execute(self.context, edge)

auroraapi/dialog/graph.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from auroraapi.dialog.step import DIALOG_STEP_MAP
2+
3+
class GraphEdge(object):
4+
def __init__(self, left = "", right = "", prev = ""):
5+
self.left = left if len(left) > 0 else None
6+
self.right = right if len(right) > 0 else None
7+
self.prev = prev if len(prev) > 0 else None
8+
9+
def next(self):
10+
if self.left != None:
11+
return self.left
12+
return self.right
13+
14+
def next_cond(self, cond):
15+
return self.left if cond else self.right
16+
17+
class Graph(object):
18+
def __init__(self, graph):
19+
self.start = graph["start"]
20+
self.edges = { node_id: GraphEdge(**edges) for (node_id, edges) in graph["edges"].items() }
21+
self.nodes = { node_id: DIALOG_STEP_MAP[node["type"]](node) for (node_id, node) in graph["nodes"].items() }

auroraapi/dialog/step/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from auroraapi.dialog.step.speech import SpeechStep
2+
from auroraapi.dialog.step.listen import ListenStep
3+
from auroraapi.dialog.step.udf import UDFStep
4+
5+
DIALOG_STEP_MAP = {
6+
"speech": SpeechStep,
7+
"listen": ListenStep,
8+
"udf": UDFStep,
9+
}

auroraapi/dialog/step/listen.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from auroraapi.text import Text
2+
from auroraapi.speech import listen_and_transcribe
3+
4+
from auroraapi.dialog.step.step import Step
5+
from auroraapi.dialog.util import parse_optional
6+
7+
class ListenStep(Step):
8+
def __init__(self, step):
9+
super().__init__(step)
10+
data = step["data"]
11+
self.model = data["model"]
12+
self.interpret = data["interpret"]
13+
self.step_name = data["stepName"]
14+
self.listen_settings = {
15+
"length": parse_optional(data["length"], float, 0),
16+
"silence_len": parse_optional(data["silenceLen"], float, 0.5),
17+
}
18+
19+
def execute(self, context, edge):
20+
text = Text()
21+
while len(text.text) == 0:
22+
text = listen_and_transcribe(**self.listen_settings)
23+
res = text.interpret(model=self.model) if self.interpret else text
24+
context.set_step_data(self.step_name, res)
25+
return edge.next()

auroraapi/dialog/step/speech.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import re, functools
2+
from auroraapi.text import Text
3+
from auroraapi.dialog.step.step import Step
4+
from auroraapi.dialog.util import is_iterable
5+
6+
def resolve_path(context, path):
7+
step, *components = path.split(".")
8+
obj = None
9+
if step == "user":
10+
obj = context.user
11+
elif step in context.step:
12+
obj = context.step[step].context_dict()
13+
if not is_iterable(obj):
14+
return None
15+
16+
while len(components) > 0:
17+
curr = components.pop(0)
18+
# TODO: handle arrays
19+
if not is_iterable(obj) or curr not in obj:
20+
return None
21+
obj = obj[curr]
22+
return obj
23+
24+
25+
class SpeechStep(Step):
26+
def __init__(self, step):
27+
super().__init__(step)
28+
self.text = step["data"]["text"]
29+
self.step_name = step["data"]["stepName"]
30+
31+
def execute(self, context, edge):
32+
# upon execution, first find all templates and replace them with
33+
# the collected value in the conversation
34+
replacements = []
35+
for match in re.finditer(r'(\${(.+?)})', self.text):
36+
val = resolve_path(context, match.group(2))
37+
# TODO: do something on data not found
38+
replacements.append({ "original": match.group(1), "replacement": val })
39+
40+
text = functools.reduce(lambda t, r: t.replace(r["original"], r["replacement"]), replacements, self.text)
41+
sp = Text(text).speech()
42+
context.set_step_data(self.step_name, sp)
43+
sp.audio.play()
44+
return edge.next()

auroraapi/dialog/step/step.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import json
2+
3+
class Step(object):
4+
def __init__(self, step):
5+
self.id = step["id"]
6+
self.type = step["type"]
7+
self.raw = step
8+
9+
def __repr__(self):
10+
return json.dumps(self.raw, indent=2)
11+
12+
def execute(self, context, edge):
13+
raise NotImplementedError()

0 commit comments

Comments
 (0)