So, we were looking for a way to test the conversational flow of our chatbots that we are developing with Google’s Dialogflow...
For a few reasons I won’t go into here, we didn’t want to use Botium, although we’re still keen to try that on a future project. Our requirements were pretty simple, we wanted to design a chatbot conversation and be able to run a test like this:
I say: Hello
Expect response: How are you today? What can I help you with?
I say: Help with bills
Expect response: I can help with that. I will need your Account Number to proceed, is that OK?
I say: etc. etc.
We are no longer using Version 1 of Dialogflow’s API and have moved to Version 2 now that it is the recommended version. This, however, introduces some new challenges regarding authentication because Version 2 uses service accounts, doing away with the client access tokens of Version 1.
This articles discusses some of the challenges we faced gaining secure access to Dialogflow’s V2 API and how we managed to get around them.
TL;DR
The final code to do this in Python 3.8 is:
import dialogflow_v2 as dialogflow
from google.oauth2 import service_account
import uuid
# Authenticate and send a test message to Dialogflow chat agentcreds = service_account.Credentials. from_service_account_file(‘chatcreds.json’)
session_client = dialogflow.SessionsClient(credentials=creds)
session = session_client.session_path(”, str(uuid.uuid4()))
text_input = dialogflow.types.TextInput(text=’hello’, language_code=’en-US’)
query_input = dialogflow.types.QueryInput(text=text_input)
response = session_client.detect_intent(session=session, query_input=query_input)# Print the response
print(‘=’ * 20)
print(‘Query text: {}’.format(response.query_result.query_text))
print(‘Detected intent: {} (
confidence: {})\n’.format(
response.query_result.intent.display_name,
response.query_result.intent_detection_confidence))
print(‘Fulfillment text: {}\n’.format(response.query_result.fulfillment_text))
Attempt 1
First off, we followed this article Quickstart: Interactions with the API. The problem with this article though is that it seems to imply that you can just fire a request at the Dialogflow V2 API and you will get a sensible response from the detect_intent call. But if you follow the example, you end up with the following response:
{
"error": {
"code": 401,
"message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
So, what’s happening here? Well, what the article doesn’t say is that you first need to ensure that you are making the call within a session that’s been authenticated using OAuth2.
Attempt 2
So, after a bit of reading around the subject, particularly this excellent article on Medium, we got hold of the JSON file to authenticate with the chat agent (the short version of which is to click on the gear icon next to the agent name in the Dialogflow UI, then in the General tab, click the name of the Service Account to open Google Cloud’s IAM & Admin screen. Here you will see the Service Accounts which can access your chat agent. Ignore the pre-existing Key ID, and instead click on the three dots, choose Create key, and create a JSON key. Finally, download the file that is given to you).
So, we’re on the home straight right? Well although the Medium article listed above is great, it didn’t quite get us to where we needed to be – in particular how to set up and authenticate via Python3.8.
We briefly toyed with the idea of implementing this as bespoke HTTP calls, but quickly realised that installing Google’s own APIs would be far more straightforward and reliable. So we got hold of the Python client library for Dialogflow by running:
pip install dialogflow
And so we were ready to follow Google’s tutorial:
https://googleapis.dev/python/dialogflow/latest/gapic/v2/api.html
Sadly, it’s a little sparse on the details of exactly what the parameters for the individual calls should be. After a fair bit of trial-and-error, and cobbling together a few examples led us to this…
import dialogflow_v2 as dialogflow
import uuidcreds = {
"type": "service_account",
"project_id": "my_project_id",
"private_key_id": "abcdefgh0123456",
"private_key": "-----BEGIN PRIVATE KEY----- -----END PRIVATE KEY-----",
"client_email": "my@serviceaccount.com",
"client_id": "111111111111111111111111111",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ xxxxxxxxxxxx.iam.gserviceaccount.com"
}session_client = dialogflow.SessionsClient(credentials=creds)
session = session_client.session_path('', str(uuid.uuid4()))
text_input = dialogflow.types.TextInput(text='hello', language_code='en-US')
query_input = dialogflow.types.QueryInput(text=text_input)
response = session_client.detect_intent(session=session, query_input=query_input)
But that just results in the following error:
google.api_core.exceptions.ServiceUnavailable: 503 Getting metadata from plugin failed with error: 'dict' object has no attribute 'before_request'
Attempt 3 (Success!)
Queue a fair bit of frustration and head-scratching, until stumbling across this Issue Report, which was the final piece in the jigsaw. Basically, the credentials can’t be passed as a Python dict, and need to be loaded directly from a file to work. So replacing the initialisation of the creds variable with:
creds = service_account.Credentials. from_service_account_file('chatcreds.json')
and, adding an import for the service_account using:
from google.oauth2 import service_account
and it works! And not only that, we can now store the JSON file with the credentials away securely without any prying eyes as a result of it being in the source repository or loading from configuration variables which in themselves would need to be stored away somewhere. So our final working code becomes (where chatcreds.json is the JSON file containing the authentication credentials that you downloaded from Dialogflow earlier):
import dialogflow_v2 as dialogflow
from google.oauth2 import service_account
import uuid
creds = service_account.Credentials. from_service_account_file('chatcreds.json')
session_client = dialogflow.SessionsClient(credentials=creds)
session = session_client.session_path('', str(uuid.uuid4()))
text_input = dialogflow.types.TextInput(text='hello', language_code='en-US')
query_input = dialogflow.types.QueryInput(text=text_input)
response = session_client.detect_intent(session=session, query_input=query_input)