Skip to content

Commit 1a782ee

Browse files
committed
Initial commit
0 parents  commit 1a782ee

20 files changed

+1775
-0
lines changed

.devcontainer/devcontainer.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2+
// https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/docker-existing-dockerfile
3+
{
4+
"name": "Existing Dockerfile",
5+
6+
// Sets the run context to one level up instead of the .devcontainer folder.
7+
"context": "..",
8+
9+
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
10+
"dockerFile": "../Dockerfile",
11+
12+
// Set *default* container specific settings.json values on container create.
13+
"settings": {},
14+
15+
// Add the IDs of extensions you want installed when the container is created.
16+
"extensions": ["ms-toolsai.jupyter", "ms-python.python", "ms-vscode.live-server"]
17+
18+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
19+
// "forwardPorts": [],
20+
21+
// Uncomment the next line to run commands after the container is created - for example installing curl.
22+
// "postCreateCommand": "apt-get update && apt-get install -y curl",
23+
24+
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust
25+
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
26+
27+
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
28+
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
29+
30+
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
31+
// "remoteUser": "vscode"
32+
}

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.rasa
2+
__pycache__
3+
models
4+
*.csv
5+
*.txt
6+
.ipynb_checkpoints

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.formatting.provider": "black"
3+
}

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM rasa/rasa:2.8.15-full
2+
3+
4+
USER root
5+
6+
RUN pip install black \
7+
ipykernel \
8+
jupyterlab \
9+
pandas
10+
11+
RUN apt update && \
12+
apt install -y git \
13+
make \
14+
wget

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Introduction
2+
3+
This repo contains code to accompany the [How To Build A Chatbot That Unerstands Contional Statements](https://pub.towardsai.net/how-to-build-a-chatbot-that-understands-conditional-statements-39ea5a840d5) blog post on Medium.
4+
5+
# Usage
6+
7+
1. Open this project in a container using VS Code
8+
2. Run `make run-action-server` in a terminal
9+
3. In another terminal, run `make run-bot`
10+
4. Talk to the bot using the [chat widget](ui/bot_ui.html)

actions/__init__.py

Whitespace-only changes.

actions/actions.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# This files contains your custom actions which can be used to run
2+
# custom Python code.
3+
#
4+
# See this guide on how to implement these action:
5+
# https://rasa.com/docs/rasa/custom-actions
6+
7+
8+
# This is a simple example for a custom action which utters "Hello World!"
9+
10+
from typing import Any, Text, Dict, List
11+
12+
from rasa_sdk import Action, Tracker
13+
from rasa_sdk.executor import CollectingDispatcher
14+
from rasa_sdk.events import SlotSet
15+
16+
from actions.spacy_parser import parse_device_instruction
17+
18+
19+
class ActionParseDeviceInstruction(Action):
20+
def __init__(self) -> None:
21+
22+
super().__init__()
23+
24+
def name(self) -> Text:
25+
return "action_parse_device_instruction"
26+
27+
def run(
28+
self,
29+
dispatcher: CollectingDispatcher,
30+
tracker: Tracker,
31+
domain: Dict[Text, Any],
32+
) -> List[Dict[Text, Any]]:
33+
34+
msg = tracker.latest_message["text"]
35+
36+
if_stmt, then_stmt = parse_device_instruction(msg)
37+
38+
if not if_stmt:
39+
dispatcher.utter_message(
40+
response="utter_acknowledge_instruction", then_text=then_stmt
41+
)
42+
else:
43+
dispatcher.utter_message(
44+
response="utter_acknowledge_conditional_instruction",
45+
if_text=if_stmt,
46+
then_text=then_stmt,
47+
)
48+
49+
return [
50+
SlotSet("condition", if_stmt),
51+
SlotSet("action", then_stmt),
52+
SlotSet("action_intent", tracker.latest_message["intent"]["name"]),
53+
]

actions/spacy_parser.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import spacy
2+
from spacy.matcher import Matcher
3+
from spacy.tokens import Token
4+
5+
nlp = spacy.load("en_core_web_md")
6+
matcher = Matcher(nlp.vocab)
7+
Token.set_extension("mytext", default=None, force=True)
8+
9+
10+
DOMAIN = ["temperature", "light", "door", "heater", "window", "music"]
11+
12+
13+
def extract_then_part(domain_token, if_token):
14+
doc = domain_token.doc
15+
verb_token = domain_token.head
16+
17+
while verb_token.pos_ != "VERB":
18+
verb_token = verb_token.head
19+
20+
if not verb_token.is_sent_start:
21+
return doc[verb_token.i :]
22+
else:
23+
return doc[: if_token.i]
24+
25+
26+
def extract_if_part(verb_token, if_token):
27+
doc = verb_token.doc
28+
29+
if if_token.is_sent_start:
30+
return doc[: verb_token.i]
31+
else:
32+
return doc[if_token.i :]
33+
34+
35+
def get_condition_token(doc):
36+
if_token = [t for t in doc if t.pos_ == "SCONJ"]
37+
assert len(if_token) == 1, "Only 1 condition per utterance is allowed"
38+
return if_token[0]
39+
40+
41+
def parse_conditional_statement(doc):
42+
domain_tokens = [t for t in doc if t.lemma_ in DOMAIN]
43+
assert (
44+
len(domain_tokens) == 1
45+
), f"Cannot recognize any objects from the utterance: {doc.text}"
46+
domain_token = domain_tokens[0]
47+
48+
if_token = get_condition_token(doc)
49+
50+
if_stmt = extract_if_part(domain_token.head, if_token)
51+
if_stmt = clean_statement(if_stmt)
52+
53+
then_stmt = extract_then_part(domain_token, if_token)
54+
then_stmt = clean_statement(then_stmt)
55+
56+
return (if_stmt, then_stmt)
57+
58+
59+
def truncate_doc(doc):
60+
ending_pos = ["VERB", "NOUN", "ADJ", "ADV"]
61+
root = get_root(doc)
62+
63+
if doc[-1].head == root and doc[-1].lower_ == "then":
64+
doc = doc[:-1]
65+
66+
while doc[-1].pos_ not in ending_pos:
67+
doc = doc[:-1]
68+
69+
return doc
70+
71+
72+
def adj_pronoun(doc):
73+
patterns = [[{"LOWER": "i", "POS": "PRON"}]]
74+
75+
for token in doc:
76+
token._.mytext = token.text
77+
78+
def f(matcher, doc, i, matches):
79+
match_id, start, end = matches[i]
80+
doc[start]._.mytext = "you"
81+
82+
if doc[start + 1].pos_ == "AUX":
83+
doc[start + 1]._.mytext = "are"
84+
85+
matcher.add("adj_I_pronoun", patterns, on_match=f)
86+
87+
matcher(doc)
88+
89+
return nlp.make_doc(" ".join(token._.mytext for token in doc))
90+
91+
92+
def clean_statement(span):
93+
doc = span.as_doc()
94+
doc = truncate_doc(doc)
95+
doc = adj_pronoun(doc)
96+
97+
return doc
98+
99+
100+
def get_root(doc):
101+
root = [token for token in doc if token.head == token]
102+
return root[0]
103+
104+
105+
def is_conditional_statement(doc):
106+
root = get_root(doc)
107+
children = root.children
108+
109+
return any(token for token in children if token.dep_ == "advcl")
110+
111+
112+
def parse_device_instruction(utterance):
113+
doc = nlp(utterance)
114+
if_stmt = None
115+
then_stmt = None
116+
117+
if is_conditional_statement(doc):
118+
if_stmt, then_stmt = parse_conditional_statement(doc)
119+
if_stmt = str(if_stmt)
120+
then_stmt = str(then_stmt)
121+
122+
else:
123+
then_stmt = str(adj_pronoun(doc))
124+
125+
return if_stmt, then_stmt

config.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# The config recipe.
2+
# https://rasa.com/docs/rasa/model-configuration/
3+
recipe: default.v1
4+
5+
# Configuration for Rasa NLU.
6+
# https://rasa.com/docs/rasa/nlu/components/
7+
language: en
8+
9+
pipeline:
10+
# # No configuration for the NLU pipeline was provided. The following default pipeline was used to train your model.
11+
# # If you'd like to customize it, uncomment and adjust the pipeline.
12+
# # See https://rasa.com/docs/rasa/tuning-your-model for more information.
13+
# - name: WhitespaceTokenizer
14+
# - name: RegexFeaturizer
15+
# - name: LexicalSyntacticFeaturizer
16+
# - name: CountVectorsFeaturizer
17+
# - name: CountVectorsFeaturizer
18+
# analyzer: char_wb
19+
# min_ngram: 1
20+
# max_ngram: 4
21+
# - name: DIETClassifier
22+
# epochs: 100
23+
# constrain_similarities: true
24+
# - name: EntitySynonymMapper
25+
# - name: ResponseSelector
26+
# epochs: 100
27+
# constrain_similarities: true
28+
# - name: FallbackClassifier
29+
# threshold: 0.3
30+
# ambiguity_threshold: 0.1
31+
32+
# Configuration for Rasa Core.
33+
# https://rasa.com/docs/rasa/core/policies/
34+
policies:
35+
# # No configuration for policies was provided. The following default policies were used to train your model.
36+
# # If you'd like to customize them, uncomment and adjust the policies.
37+
# # See https://rasa.com/docs/rasa/policies for more information.
38+
# - name: MemoizationPolicy
39+
# - name: RulePolicy
40+
# - name: UnexpecTEDIntentPolicy
41+
# max_history: 5
42+
# epochs: 100
43+
# - name: TEDPolicy
44+
# max_history: 5
45+
# epochs: 100
46+
# constrain_similarities: true

credentials.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This file contains the credentials for the voice & chat platforms
2+
# which your bot is using.
3+
# https://rasa.com/docs/rasa/messaging-and-voice-channels
4+
5+
rest:
6+
# # you don't need to provide anything here - this channel doesn't
7+
# # require any credentials
8+
9+
10+
#facebook:
11+
# verify: "<verify>"
12+
# secret: "<your secret>"
13+
# page-access-token: "<your page access token>"
14+
15+
#slack:
16+
# slack_token: "<your slack token>"
17+
# slack_channel: "<the slack channel>"
18+
# slack_signing_secret: "<your slack signing secret>"
19+
20+
socketio:
21+
user_message_evt: user_uttered
22+
bot_message_evt: bot_uttered
23+
session_persistence: false
24+
25+
#mattermost:
26+
# url: "https://<mattermost instance>/api/v4"
27+
# token: "<bot token>"
28+
# webhook_url: "<callback URL>"
29+
30+
# This entry is needed if you are using Rasa X. The entry represents credentials
31+
# for the Rasa X "channel", i.e. Talk to your bot and Share with guest testers.
32+
rasa:
33+
url: "http://localhost:5002/api"

0 commit comments

Comments
 (0)