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

  1. Open Visual Studio Code
  2. Open the Azure Functions sidebar
  3. Click on Create New Project
  4. Pick a location on disk
  5. Choose C# as the project language
  6. 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