This blog post is part of The Third Annual C# Advent by Matthew Groves which is a C# advent calendar.
Do you worry you're stuck on the naughty list? Have you forgotten all the good deeds and awesome things you've done over the past year to deserve more than a sack of coal?
Tracking your little wins as they happen is a fantastic way to remember your successes whether you're sharing the list with your boss or Santa Claus or just yourself!
We will use Twilio Autopilot to capture your accomplishments, thus enabling you to keep a log via SMS, voice, WhatsApp, Slack or even your Amazon Alexa or Google Home device!
We're going to save the output of Autopilot to Azure Table Storage via an Azure Function.
This post assumes some basic knowledge of C# and RESTful APIs.
To get started, we will need:
- A Twilio account
- An Azure Account (sign up for free here)
- The latest .NET Core 3.1 SDK (download here)
- Azure Functions Core Tools version 3.X
- Azure CLI version 2.0 or later
- An IDE or code editor with C# support
If you would like to see a full integration of Twilio APIs in a .NET Core application then checkout this free 5-part video series I created. It's separate from this blog post tutorial but will give you a full run down of many APIs at once.
Setting up Twilio Autopilot
Autopilot is a conversational AI platform to build chatbots and virtual assistants with natural language understanding and complete programmability with Autopilot Actions.
To get started, create a new bot from the console and click on the "Start from scratch button".
Give your bot a unique name -- mine was called LittleWins
-- and a brief description. Then click the "Create bot" button.
After a few minutes, the bot will have been created and you will find yourself on the "Create tasks for your bot" page.
The first Task we will need to modify is the "greeting" or "Initiation" task. This is the entry point for our bot. Click on the "Program" link relating to "greeting".
Replace the existing program content with the following:
{
"actions": [
{
"say": "Hello, welcome to the little wins app! This is where you can log all of the little achievements you've had! Tell me what your latest achievement is."
},
{
"listen": {
"tasks": [
"log-achievements",
"nothing-to-log"
]
}
}
]
}
When a message comes in, whether that's voice or text, our bot will respond with the value of say
. The bot will then listen, and based on the user response redirect to one of the two available tasks; log-achievements
or nothing-to-log
.
We haven't created these yet, we will do that soon.
Our bot needs to be trained to recognise natural language. To do this we will give it as many samples of the expected input as possible.
Click the "Switch to train" button on the top right of the task.
We now need to enter as many examples of expected user input as we can.
Some examples could be:
- I did something cool today
- I want to document today's success
- I'd like to log a win, please
In the screenshot below you can see some more examples from my app.
Next, we will handle what to do if the user doesn't want to log anything. This is the "nothing-to-log" task.
Navigate back to "All Tasks" and click on the "Add a task" button.
Name the task nothing-to-log
and click the "Add" button.
Click on the "Program" link for the "nothing-to-log" task. Then copy and paste the code below:
{
"actions": [
{
"say": "Sorry to hear that, come back when you would like to log something!"
}
]
}
This will respond to the user by asking them to come back once they have something to log and ending the communication. We need to add some training to this also, so click the "Switch to train task" button again and add some samples such as the ones below:
- I haven't achieved anything
- Maybe next time
- I have nothing to log.
Now we can create the "log-achievements" task. Do this the same way as the "nothing-to-log" task.
Paste the following code into the "Program" for the "log-achievements" task.
{
"actions": [
{
"collect": {
"name": "collect_comments",
"questions": [
{
"question": "What is the date of this achievement?",
"name": "date",
"type": "Twilio.DATE"
},
{
"question": "Tell me a little bit more about what you did.",
"name": "details"
}
],
"on_complete": {
"redirect": {
"method": "POST",
"uri": "https://XXXXXX.ngrok.io/api/AutopilotTrigger"
}
}
}
}
]
}
Click the "Switch to train task" button again and add some samples such as the ones below, but really, anything you think you might log. The more samples the better, so go to town!
- I made my bed
- I did something that scared me
- I completed my todo list
The above task will ask two questions. The first being when the deed was performed and the second for the details. Once this information is gathered, we redirect to the URL in the "redirect"
node. But we haven't written this yet!!
However, before we rush off and write our endpoint, we can test out our chatbot in the simulator, which you can find in your chatbot's menu in the Twilio console. You may be asked to build your model first, just follow the instructions from the simulator.
You may notice that the chatbot gets stuck. This is because we have a redirect to an API endpoint that we haven't written yet - let's set that up using Azure Functions.
Create an Azure resource group and required resources
Once logged into your Azure portal, we will create a Resource Group, Functions App Service and Azure Function.
To create the Resource Group:
- Click the Hamburger icon in the top left to bring out the left-hand menu,
- go to "Resource groups" and then
- Click the "+ Add" button to add a new resource.
- You will be taken to the "Create a resource group" page where you can enter a name for your resource group, such as "LittleWins"
- choose a suitable region and then
- Click "Review + create", then "create".
Your resource will take a few moments to deploy.
Once deployed, we can create a new "Function App" resource. Click the "Create a resource" button on the top left of the left-hand sliding menu. In the search bar, search for and select "Function App", then click "Create".
First, a "Create Function App” dialogue will come up and you will need to give the app a unique name and add it to the Resource Group that we just created. Second, choose an appropriate location and set "Runtime stack" to ".NET Core". Third, click on the "Next: Hosting" button at the bottom and set the "Hosting Plan" to "Consumption Plan". Fourth, in the "Next: Monitoring" tab, turn off Application Insights - you can always add it later. Finally, click the "Review and create" button at the bottom of the form.
After a few moments, you should have a new Function App Service, which you can then navigate to. In the centre of the screen will be a button called "+ New function". Click on this.
The next screen will give several options, choose the IDE that you are using or, if it's not there, just choose the "Any editor + Core Tools" option.
You should now see a very helpful "cheat sheet" with all the commands that you need to work with your project locally, using the CLI. Especially useful is the "Deploy your code to Azure" command, so you may want to make a note of that for later.
Now that we have the Azure side of everything set up, it's time to look at the code.
The Function App
To save some time, I have created a GitHub repo with the outline project here - clone the outline
branch. It has our table storage context already; however, if you would like to know a little more about implementing table storage in an Azure Function, then check out this blog of mine - Rainy with a chance of Azure Cloud. If you would just like to use the working code, then checkout the complete code in the master
branch.
We will focus on the code in the /LittleWins
directory for this post.
Within this directory, we have two additional directories: Models
and Data
.
The Models
directory contains TableConfiguration.cs
and its interface for setting up the table storage dependency injection. It also has LittleWinEntity.cs
and Message.cs
.
The Data
directory contains our TableDbContext
and its interface. The code in the implementation is complete and handles checking if the table is created and actually writing to it.
Regardless of whether you are coding along or just running the completed code, you will need to rename the file called demo.local.setting.json
to local.setting.json
. This is because local.setting.json
is included in the .gitignore
to protect your secrets.
You will need to update the newly renamed local.setting.json
with your Azure Storage connection string once you have it.
You can find the Table Storage connection string in the Azure portal.
Navigate to the Resource Group that we made earlier and click into the "storage account" which is used for our Table Storage. On the left-hand menu, click into "Access keys" and copy the "Connection string" listed against "key1".
The API endpoint
When a chatbot interaction is complete, Autopilot will make a call to our AutopilotTrigger
endpoint. This is in LittleWins/AutopilotTrigger.cs
.
The TableDbContext
is injected into the class constructor, making it available for us to use.
Right now our endpoint is only returning null. Let's update it.
[FunctionName("AutopilotTrigger")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
HttpRequest req, ILogger log)
{
var requestBody = await req.ReadAsStringAsync();
log.LogInformation(requestBody);
var message = new Message(requestBody);
}
The above code will take the incoming POST request and create a Message
object with the data. We will need to update the Message.cs
file to populate the fields we will need next.
The Message Object
When a new Message
object is created using the public Message(string formQuery)
constructor, we parse through the form data, which is URL-encoded, create a dictionary and then assign the value for "Memory" to the Memory
field. The value for "Memory" is the JSON object that you can inspect in the Autopilot simulator.
We will now need to extract the answers to our two questions, "What is the date of this achievement?" and "Tell me a little bit more about what you did.", and assign them to the properties DateAnswer
and DetailAnswer
respectively.
Parsing through JSON is ugly. There are several ways to do it and, if you needed to create very robust models of your data, I would then model the JSON object in C#; however, we just need two values from the Memory
object. So let's update the Message
constructor code with the following:
public Message(string formQuery)
{
var formData = FormData(formQuery);
Memory = formData["Memory"];
JObject json = JObject.Parse(Memory);
DateAnswer = json["twilio"]["collected_data"]["collect_comments"]["answers"]["date"]["answer"].ToString();
DetailAnswer = json["twilio"]["collected_data"]["collect_comments"]["answers"]["details"]["answer"].ToString();
}
The above way of drilling into a JSON object can be brittle, especially if you update the tasks in Autopilot, so be mindful of this.
Now that we have assigned our answers to the relevant fields in Message
, we can return to our trigger.
[FunctionName("AutopilotTrigger")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
HttpRequest req, ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogInformation(requestBody);
var message = new Message(requestBody);
var date = DateTime.Parse(message.DateAnswer);
var littleWinEntity = new LittleWinEntity(message.DetailAnwser, date);
var result = await _tableContext.InsertOrMergeEntityAsync(littleWinEntity);
}
Adding the above code will create a LittleWinEntity
which we can then use to create a new data entry in our table storage.
When Autopilot calls our API endpoint, it is expecting a response in the form of an "Action". Let's update the code to do this and return the correct response depending on whether the table update was successful.
[FunctionName("AutopilotTrigger")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
HttpRequest req, ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogInformation(requestBody);
var message = new Message(requestBody);
var date = DateTime.Parse(message.DateAnswer);
var littleWinEntity = new LittleWinEntity(message.DetailAnwser, date);
var result = await _tableContext.InsertOrMergeEntityAsync(littleWinEntity);
var jsonResponsePositive = @"{ ""actions"":[{""say"": ""You achievement was saved successfully!""}]}";
var jsonResponseNegative = @"{ ""actions"": [ {""say"": ""There was a problem saving your achievement""}]}";
return result != null
? new ContentResult{Content=jsonResponsePositive, ContentType = "application/json", StatusCode = 200}
: new ContentResult{Content=jsonResponseNegative, ContentType = "application/json", StatusCode = 418};
}
Testing it out
Start your functions from your IDE or from within the CLI with the following command from the root of the project:
func start
Your function will start and you will receive the following message in the console telling you about the HTTP trigger endpoints.
Http Functions:
InboundFunction: [POST] http://localhost:7071/api/AutopilotTrigger
We will want to wire Autopilot to this endpoint using ngrok.
ngrok creates a public facing URL that tunnels to our project running locally so we can test our endpoint without deploying to a server. Follow the instructions on the ngrok site to download and install it.
Once installed, run the following on your command line to start ngrok:
> ngrok http 7071 -host-header="localhost:7071"
If your function isn't running on port 7071, then just update the above command with your port number.
You will then see an output similar to below.
Copy the public-facing URL which should be along the lines of https://12345.ngrok.io
. Now you can return to the Autopilot console and update the "redirect" URL node in the "Program" for the "log-achievements" task.
When you test your chatbot using the simulator, you should get a response from your app stating that your achievement was successfully logged and your JSON object will have a "status" of "complete"!
Once you are happy with your app, you can publish it to the Azure Portal.
An easy way to publish your app is by using the Azure CLI. First, make sure you are logged into your Azure account with the command az login
, which will open a browser and enable you to log in to Azure.
Then, from within "LittleWins" directory, run the command we made a note of previously:
func azure functionapp publish <FUNCTION_APP_NAME>
Don't forget to add your app settings and update your Twilio endpoint to your Function app's URL.
What next?
I'm sure you will want to interact with your new chatbot over various communication channels. That may be via SMS or even using a home assistant such as the Amazon Alexa! You can see all the ways to do this in the documentation.
You may also want to expand on your chatbot's capabilities. Perhaps you would like to ask your chatbot what your last achievement was? Or maybe you'd like to update the chatbot so it logs different users. Whatever you do, I would love to hear about it!
Merry Christmas!
- Email: lporter@twilio.com
- Twitter: @LaylaCodesIt
- GitHub: layla-p