How to build chatbots using DialogFlow, Azure Functions, and F#
Talk To Your Functions
About Me
- Mike Sigsworth
- .NET Developer for 10+ years
- Born and raised in Winnipeg
- Live in Calgary
- Work from home for Clear Measure
How did I get here?
Birthday Greeting
- Asks for user's name and birthday
- Responds with a customized greeting based on birthday
"Happy 14th birthday Kylee!"
"Hi Kathy. Looks like I missed it. But happy 73rd birthday anyway."
"Hey Mike. Happy 40th birthday in advance."
"Wow, a leap year baby! You turn what, 5 this year? Just kidding.
Happy 18th birthday Marco!"
Agents,
Intents,
and Entities
Agents
- A "natural language understanding" module
- Used by your app to transform natural user requests into actionable requests
- A container to orchestrate the conversation flow
- Contains Intents
Intents
- Maps what a user says to what your application should do
- Uses training phrases to learn what users might say
- Annotates those phrases to extract key values into Entities
Entities
-
Define the parameters an intent is seeking to fill
-
May be system defined (name, date, location) or custom built
Fulfillment
-
A backend webhook capable of performing more complex business logic
-
Takes in the intent's context, action, and parameter data
-
Responds with text to be spoken or displayed to the user
Create DialogFlow Agent
- Give agent a name, e.g. "BirthdayGreeting"
- Customize the Welcome Intent
- Create a new intent to get the user's name and birthdate
- Make given-name and birthdate required
- Setup slot-filling
- Enable fulfillment via Webhook
Live Demo
Azure Functions
and F#
Visual Studio Code
F#
.NET Core 2.0
Azure Functions Core Tools
Azure Functions Core Tools
>
=
=
/
/
=
apt
Visual Studio Code
Extensions
Ionide
Azure Functions
Nuget
Installing F#
Visit
fsharp.org
Create the Function App
- Open Visual Studio Code
- Open the Azure Functions sidebar
- Click on Create New Project
- Pick a location on disk
- Choose C# as the project language
- Open in Current Window
Step 1 - Open Visual Studio Code
Step 2 - Open the Azure Functions sidebar
Step 3 - Create New Project
Step 4 - Choose C# as the project language
Step 5 - Project Created
Extension
Recommendations ---->
for workspace
Launch configuration ---->
for debugging
Workspace Settings ---->
for Azure Functions
Tasks to build,
clean, and run ---->
the project
Project is initialized
as a Git repository
with a .gitignore ---->
file tailored for
Azure Functions
development
Azure Functions
host configuration ---->
and local app settings
The project file ---->
Change the extension ---->
to .fsproj
Build Warning
Package 'Microsoft.AspNet.WebApi.Client 5.2.2' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETStandard,Version=v2.0'.
Update the NuGet package:
Microsoft.NET.Sdk.Functions
Create the Function
- The project code is split into three files:
- BirthdayGreeting.fs - Azure Function entry point
- Model.fs - DialogFlow types for serialization
-
Util.fs - Helper functions
- Each file must be referenced in the .fsproj (in the correct order)
<ItemGroup>
<Compile Include="Util.fs"/>
<Compile Include="Model.fs"/>
<Compile Include="BirthdayGreeting.fs"/>
</ItemGroup>
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
BirthdayGreeting.fs
- Azure Function entry point
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
Attribute values are used at build time to generate the function.json file, which until recently had to be manually kept up to date.
The FunctionName attribute defines the name of the function within Azure.
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- In F# the function is simply called run
- It takes 2 parameters req and log
- req is decorated with an HttpTrigger attribute, which configures the binding type defined in the project.json
- The attribute provides the AuthorizationLevel, allowed HTTP methods, and the Route
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
The log parameter is passed in by the runtime, and provides a way to write to the Azure trace log.
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- It's best practice nowadays to write async code
- This is how it's done in F#, using an async computation expression
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Another best practice is proper exception handling
- Google won't approve your app unless you properly support several error codes, including:
- 400, 401, 403, 404, 500, and 503
- Notice how we cast to IActionResult
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Asynchronously read the request body
- Log it to the trace log for good measure
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Use JSON.NET to deserialize the request body into a variable of type DialogFlowRequest
- Model types are defined in Model.fs
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Extract the givenName and birthdate from the request data
- Notice how we can use surrounding double backticks to access the kebab-case properties defined by DialogFlow, i.e. ``given-name``
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Using the getGreeting function defined in Util.fs we get our greeting string
- And again, log it for debugging purposes
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Create our response result using the createDialogFlowResponse function defined in Model.fs
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
- Return the result as a JsonResult
- Notice we must cast the result to an IActionResult
- F# expects a single return type
- IActionResult is implemented by both JsonResult and BadRequestObjectResult
[<FunctionName("BirthdayGreeting")>]
let run
([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = null)>]
req: HttpRequest,
log: TraceWriter) =
log.Info("BirthdayGreeting is processing a request.")
async {
try
use reader = new StreamReader(req.Body)
let! requestBody = reader.ReadToEndAsync() |> Async.AwaitTask
log.Info(sprintf "Raw Request Body: %s" requestBody)
let data = JsonConvert.DeserializeObject<DialogFlowRequest>(requestBody)
log.Info(sprintf "Deserialized Request Body: %A" data)
let givenName = data.queryResult.parameters.``given-name``
let birthdate = data.queryResult.parameters.birthdate.Date
let greeting = getGreeting givenName birthdate
log.Info(sprintf "Responding with greeting: %s" greeting)
let result = createDialogFlowResponse greeting
return JsonResult result :> IActionResult
with ex ->
log.Error(sprintf "Something went horribly wrong!", ex)
return BadRequestObjectResult ex.Message :> IActionResult
} |> Async.RunSynchronously
type QueryParameters = {
``given-name``: string
``last-name``: string
birthdate: System.DateTime
}
type QueryResult = {
parameters: QueryParameters
}
type DialogFlowRequest = {
queryResult: QueryResult
}
type SimpleResponse = {
textToSpeech: string
}
type RichResponseItem = {
simpleResponse: SimpleResponse
}
type RichResponse = {
items: RichResponseItem list
}
type GooglePayload = {
richResponse: RichResponse
}
type Payload = {
google: GooglePayload
}
type DialogFlowResponse = {
payload: Payload
}
let createDialogFlowResponse response =
{ payload =
{ google =
{ richResponse =
{ items =
[{ simpleResponse =
{ textToSpeech = response }}]}}}}
Model.fs
- DialogFlow types
for serialization
{
"queryResult": {
"parameters": {
"given-name": "Mike",
"last-name": "Sigsworth",
"birthdate": "2000-12-29T00:00:00"
}
}
}
{
"payload": {
"google": {
"richResponse": {
"items": [{
"simpleResponse": {
"textToSpeech": "Happy 18th birthday in advance, Mike. Hope it's a good one!"
}
}]
}
}
}
}
Request JSON
Response JSON
let getAge (birthdate: DateTime) =
let today = DateTime.Today
let age = today.Year - birthdate.Year
if (birthdate > today.AddYears(age)) then age - 1 else age
let ordinal (num: int) =
let ones = num % 10
let tens = floor ((num |> float) / 10.0) % 10.0
if (tens = 1.0) then
"th"
else
match ones with
| 1 -> "st"
| 2 -> "nd"
| 3 -> "rd"
| _ -> "th"
let getLeapAge (birthdate: DateTime) =
let today = DateTime.Today
birthdate.Year
|> Seq.unfold (fun year ->
if (year > today.Year) then None
else Some(DateTime.IsLeapYear year, year+1))
|> Seq.filter id
|> Seq.length
let isLeapYearBaby (birthdate: DateTime) =
DateTime.IsLeapYear birthdate.Year &&
birthdate.Month = 2 &&
birthdate.Day = 29
let isBirthday (birthdate: DateTime) =
let today = DateTime.Today
birthdate.Month = today.Month && birthdate.Day = today.Day
let birthdateForYear (year: int) (birthdate : DateTime) =
if (birthdate.Month = 2 && birthdate.Day = 29 && not (DateTime.IsLeapYear year)) then
DateTime(year, 3, 1) // Leap year birthday
else
DateTime(year, birthdate.Month, birthdate.Day)
let isBelated (birthdate: DateTime) =
let tolerance = 150.0
let today = DateTime.Today
let thisYear = today.Year
let lastYear = thisYear - 1
let thisYearsBirthdate = birthdateForYear thisYear birthdate
let lastYearsBirthdate = birthdateForYear lastYear birthdate
today.AddDays(-tolerance) < thisYearsBirthdate && thisYearsBirthdate < today ||
today.AddDays(-tolerance) < lastYearsBirthdate && lastYearsBirthdate < today
let getGreeting givenName birthdate =
let age = getAge birthdate
let ageStr =
if (age > 0) then
sprintf "%d%s " age (ordinal age)
else ""
let msg =
if (isBirthday birthdate) then
sprintf "Happy %sbirthday %s!!" ageStr givenName
elif (isBelated birthdate) then
sprintf "Looks like I missed it %s. But happy belated %sbirthday anyway!" givenName ageStr
else
sprintf "Happy %sbirthday in advance, %s. Hope it's a good one!" ageStr givenName
if (isLeapYearBaby birthdate) then
let leapAge = getLeapAge birthdate
let leapAgeStr =
if (leapAge > 0) then
sprintf "You turn what, %d this year? Just kidding. Anyways... " leapAge
else ""
sprintf "Wow, a leap year baby! %s%s" leapAgeStr msg
else msg
Util.fs
- Helper methods
Running Locally
- Press F5 to clean, build, and run the function app
- This also attaches a debugger
- Application URL:
http://localhost:7071/api/BirthdayGreeting
Testing With Postman
Request
Testing With Postman
Response
DialogFlow Fulfillment
- Use ngrok to create a public URL to a local address
- Ensure local function app is running
- > ngrok http 7071
- Copy https address from ngrok output
- Configure DialogFlow fulfillment
Live Demo
Questions?
@aspnetdev
github.com/mikesigs/talk-to-your-functions
slides.com/mikesigs/talk-to-your-functions
discardchanges.com
Thank you!
Talk To Your Functions
By Mike Sigsworth
Talk To Your Functions
- 245