Giving a voice to Jira

The problem

What if you could talk to Jira to create a ticket instead of having to interrupt what you’re doing to open a new tab, and painstakingly scroll through the ticket creation form?

It was brought to my attention by multiple Sqreeners, namely Arnaud, Project Manager and Jira aficionado that Jira alone isn’t convenient to use, so I attempted to fix it. Interested in conversational bots, and having developed an Alexa Skill for Amazon Alexa during McHacks V, I decided this time to try Google’s solution: Dialogflow, for the Google Assistant.

The solution: an assistant for Jira

Built for the Google Assistant using Google Dialogflow, AWS API Gateway, AWS Lambda and the Jira REST API.

AWS Jira workflow

How Google handles conversations

To conversate Google Dialogflow uses “intents”. These are phrases that will trigger a response, for example, to create a new ticket. These phrases can contain “entities”, keywords that can be passed stored by Dialogflow and sent in a request. This is, for example, the type of issue a user is encountering, and all possible types would be grouped under a same entity. Finally, once you have collected all the information, you can send a request to a webhook with the input the user gave you. This is considered a “fulfillment”, and is used to both send a request, and receive a response to display to the user. This can be for example a ticket creation request, with a response indicating that the ticket was created.

How the ticket is created

Ticket information with issue type, summary, description and components is sent to the backend

Backend composed of an API Gateway and a Lambda Function. The former gives an endpoint to receive the request, and routes it to the latter.

How to do it

1. Google

Go to the Dialogflow Console. There, create an agent.

The aim is for the Google Assistant to be able to ask the following questions to create a request:

ConversationWe’ll start by creating intent, for the user to ask to create a new ticket. To do so, go to the “Intents” section, and create an intent. We’ll call it “New Ticket”. We need to add training phrases, examples of how our intent is called, to train Dialogflow. We’ll begin with “Add a ticket” and “create a ticket”, and save.

Now, we know that there are four different issue types in Jira: “Bug”, “Story”, “Task”, “Epic”. Where Dialogflow truly shines is when we add entities. In the “Entities” section, create a new entity, and call it “issue-type”. There, add the four issue types, as such:

Jira Issue TypeBack in the “New Ticket” intent, add the phrases “new bug”, and “create a story”. Dialogflow automatically recognizes that the phrases contain issue types!

Jira New Ticket PhrasesNow, the issue type is required to create a ticket. Thus, we need to prompt the user for the issue type if he only mentions a ticket. To do so, click on “Manage parameters and action”.

Manage parameters and action buttonThere, tick the “Required box for issue-type”, then click on “Define prompts”. You can add as many different sentences to ask for the issue type, but for now, we’ll just write “What type of issue is this?”.

The next parameter we want to know is the title of the issue.

Add a “title” parameter. Since the title could be any type of input, enter @sys.any as the entity. This functions acts like a wildcard. Make the parameter required, and add the prompt: “What’s the issue?”

The title should be prompted before the issue type if it is missing. Thus, use the bi-directional arrow at the right of the parameter to move the title above “issue-type”.

With this new parameter, add example sentences. We’ll need to manually label the text we add as a title since their entity is a wildcard. Enter the phrase “new bug agent too slow”. Then, highlight “agent too slow”, and select “@sys.any:title”. I did this for the phrases below:

New Ticket phrases with title]The agent should inform the user that we are creating a new issue, and then ask if a component should be assigned. In the “Responses” section, add the following response:

Ok, starting new $issue-type: $title

Do you want to assign a component?

There are now to cases for a follow-up intent, that depends on whether or not the user wants to assign a component or not

We’ll start with the easiest: the user doesn’t assign a component. Back on the “Intents” section, hover over the “New Ticket” intent, and click on “Add follow-up intent”, select “no”.

Go to the newly created intent. Notice, sample negative answers were added; these suffice. Name the intent “New Ticket – no component”.

For the case where components are given, add a follow-up intent again to “New Ticket”, but select “custom”. Name the intent “New Ticket – add component”. In this case, components are different teams among Sqreen, such as the backend, or the team working on the ruby agent, so the list of these components needs to be hard-coded.

Go to the “Entities” section, create an entity called “components”. Here, I only add a few components, and synonyms to account for spelling and different ways to formulate a component.

Note, for more entries and synonyms, you could prepare the entries in a batch and add them as CSV. To do so, click on the 3 dots next to the “Save” button, and select “switch to raw mode”

Raw componentsNow that we have a components entity, let’s add training phrases to the “New Ticket – add component” intent. The user should only say the components, or say something like “components are (…)”.

Add “agent ruby” as a training phrase. Notice, components is automatically added as a parameter.

In the parameters, check “is list” for the parameter. This allows you to list multiple components. Then, add the phrase “components are backend, agent-ruby, agent-php”. Originally, “backend” and and “agent-ruby” are highlighted as two different components parameters, we want this list to only be under one parameter though.

Components Phrases UnfixedClick on the X next to both components parameters to remove them, then select “backend, agent-ruby, agent-php” in the phrase, and select “@components:components”. Notice now that all components are under the same “components” parameter.

To inform the user of the component(s) he chose, we’ll add a response telling him what he added, and now ask him to describe the issue. Add this response to the intent:

Ok adding components $components!
Could you describe the issue?

Now, for both cases, component or no component, we want to add a follow-up intent to get a description of the issue. However, the title and the issue type from the original intent aren’t automatically carried on to the last intent. To do so, add a title and issue type parameter for both the component and no component intent. Since the parameters come from the “NewTicket” intent, their values are stored in the NewTicket-followup context. Thus, add the following to both intent parameters:

Passed ParametersNow we’ll add the follow-up intent for each case, to add the description.

To begin, add a follow-up intent to “NewTicket – add component”, make it a custom one, and name it “NewTicket – add component – add description”. Like for the title, the description can be any input. Thus in the parameters, add a “description” parameter like so:

Description ParameterNote, the prompt is simply: “What’s the description?”.

Now, we’ll add training phrases, and label the description in each of them:

Description PhrasesFor the moment, we’ll add a response to this intent, but our Lambda Function later will send a response for the user instead:

Ok, creating $issue-type with description: $description!

Again, we’ll add the parameters from the previous intent to this intent. This way, the ticket creation request sent to the Lambda Function will have all the necessary data:

Intent ParametersThis is the last intent before the request is sent to the Lambda Function. Thus, in the “Responses” section if the intent, tick the following box:

The input should then be sent to the backend. Go to the “Fulfillment” section of the intent, and tick both boxes:

Now, do the same with the case where no components are given. Create a custom follow-up intent called “New Ticket – no component – add description”, with the following parameters:

Intent ParametersNote that here, there is no “components” parameter, since in this case, we know that the user didn’t give any components. Also, add the same response, and tick the same options as for the previous intent:

Intent settingsWe will link the fulfillment to our Lambda Function once it is created. In the meantime, we’ll make our agent invokable with the Google Assistant.

Go to the “Integrations” section of the agent. On the Google Assistant integration, select “Integration Settings”. There, click on “Test”. As you can see, the invocation is currently generic. We’ll change that.

Test invocation

Go to the “Invocation” section. There, add a “Display Name”, which is the name we’ll use to invoke our agent. Certain keywords are restricted by Google such as “ok” or “google”, so “Ok Jira” can’t be used as a name. As a workaround, you can prefix “Jira” by a name. I called my assistant “Arnaud Jira” as such.

At this point, you can already use the Google Assistant, and invoke your agent to test it!

Now, we’ll add the backend of the project, starting with the Lambda Function.

2. Lambda Function

For our function to work, you need to obtain a Jira API token for your username. If you don’t know how to do so, you can check here. Once this is done, you will set your Jira URL, username, and API token as environment variables. Since we assume that the assistant will only create an issue for one project, we also set the project key as an environment variable. Setting these variables in your environment allows you to change them without having to add sensitive data to your code. We’ll add these before our lambda function.

import os

JIRA_URL = os.environ["JIRA_URL"]
JIRA_USERNAME = os.environ["JIRA_USERNAME"]
JIRA_TOKEN = os.environ["JIRA_TOKEN"]
PROJECT_KEY = os.environ["PROJECT_KEY"]

A lambda function takes two parameters: event and context. The former is what interests us as it contains the payload of the request.

To begin our lambda handler, we need obtain the information about the ticket to be created. This information is found in the payload’s queryResult > parameters. So, we’ll start by retrieving the required parameters: the title, issue type, and description.

import os
# omitted for brievety
def lambda_handler(event, context):
parameters = event["queryResult"]["parameters"]

title = parameters["title"]
issue_type = parameters["issue-type"]
description = parameters["description"]

To create a ticket, the Jira REST API requires the payload to be a json dictionary, with a field key containing the information about the ticket. We’ll start forming the value of field.

import os
# Omitted for brevity
PROJECT_KEY = os.environ["PROJECT_KEY"]

def lambda_handler(event, context):
    parameters = event["queryResult"]["parameters"]

    title = parameters["title"]
    issue_type = parameters["issue-type"]
    description = parameters["description"]

    fields = {
        "project": {"key": PROJECT_KEY},
        "summary": title.capitalize(),
        "issuetype": {"name": issue_type.capitalize()},
        "description": description.capitalize()
    }

Since components are optional, we’ll only add them to the request if the query contains any, The Jira API allows you to specify components using different identifiers. Since we ask for components, we thus declare them by their name.

# Omitted for brievety
def lamda_handler(event, context):
    # Omitted for brievety
    components = parameters.get("components")
    if components:
        fields["components"] = [{"name": component}
                                for component in components]

Now that our ticket request if formatted, we’re ready to send it to the Jira API, using the URL we set. To post to the API, we’ll use the requests library. We’ll identify ourselves with the Jira username and token we set, and specify that our payload is in JSON. As a note, libraries that do not come by default with python are not automatically installed by AWS. you need to add them as folders in the of your function. The easiest way to do so is to run this for requests:

pip install -t /. --upgrade requests

This saves the library and its dependencies to the current directory. With this done, we can post our request.

import os

JIRA_URL = os.environ["JIRA_URL"]
JIRA_USERNAME = os.environ["JIRA_USERNAME"]
JIRA_TOKEN = os.environ["JIRA_TOKEN"]
PROJECT_KEY = os.environ["PROJECT_KEY"]

def lambda_handler(event, context):
    # Omitted for brievety
    rv = requests.post(JIRA_URL + "/rest/api/2/issue/",
                       json={"fields": fields},
                       headers={"Content-Type": "application/json"},
                       auth=(JIRA_USERNAME, JIRA_TOKEN))

Now that we have posted our request, we need to check that the ticket has successfully been created in order to answer the user.

The HTTP Response code for a successful creation is 201. Thus, if the response code to our request is such, we should tell the user that the ticket was created. Additionally, when the creation is successful, the Jira API returns the key of the new ticket. We will respond to the user with a simple message, including the newly created ticket’s key. We’ll also include an answer in case the ticket fails to be created.

import requests
# Omitted for brievety
def lambda_handler(event, context):
    # Omitted for brievety
    rv = requests.post(JIRA_URL + "/rest/api/2/issue/",
                        json={"fields": fields},
                        headers={"Content-Type": "application/json"},
                        auth=(JIRA_USERNAME, JIRA_TOKEN))
    if rv.status_code == 201:
        response_data = rv.json()
        ticket_key = response_data["key"]
        return {
            "fulfillmentText": "Ticket created with key {}".format(ticket_key)
        }

    return {"fulfillmentText": "Hm... Something went wrong. \n Try again!"}

As a note, fulfillmentText is just one of the possible types of keys you can return. This is the most simple, and just returns text that will be printed to the screen and read by Google Assistant as-is.

Now, the function works. All it needs is to be triggered. For that, we’ll create an AWS API Gateway, which will give us the URL to send the Dialogflow request to.

3. API Gateway

At the top of the page, in services, select API Gateway. Create a new blank API by selecting the “New API” option in the “Create new API” section. We’ll call the API okJira, and create it. We’ve now created an endpoint, but haven’t specified a method to access it. Since we’re only creating tickets, we’ll only accept POST requests. To do so, select the
“Actions” dropdown, and click on “Create method”.

Create MethodFrom there, select POST at the method. In the setup, all you need to add your lambda function.

In order for the API to be public, it needs to be deployed. To do so, click again on “Actions”, and this time select “Deploy API”. You will be asked to select a stage. Since we don’t have any, create a new one, and call it “development” for example. At the top, you will now see an “Invoke URL”. This is the URL that Dialogflow will send its request to. We’ll now add the URL to Dialogflow.

4. Linking to Dialogflow

Back in the Dialogflow console, go to the Fulfillment section of your agent. There, enable “Webhooks”, add the URL of your gateway, and save.

WebhookYour agent is now connected to your Lambda function! From there, you can now talk to your assistant, and a ticket will be created when you request it.

Opportunities to expand

Using the Google Assistant does have its difficulties. Your voice transcription is not always as accurate as typing, and you can’t interrupt your conversation with the assistant. Dialogflow does offer integrations with other platforms, and this is what makes this technology truly shine: would you rather use Messenger, Slack, or even Skype instead, with little difficulties to port your bot logic? I made the test with Slack, and it was almost effortless, and it turned out that Sqreeners preferred it to the Google Assistant since you could type and interrupt yourself. Plus, they already use Slack in their workflow, so there is no need for much change.

About the author

Oscar is a Computer Science student at the University of Toronto. Python programmer and workplace freshman, he is a fan of tech, good food, and traveling, and hopes to be able to combine all these interests in the future. Oscar was a summer intern at Sqreen.