Search code examples
pythonflaskdialogflow-estornadowebhooks

Alternatives of Flask APIs in tornado (Dialogflow webhook)


I need to create a webhook for Dialogflow in Tornado server. I had done this previously using flask. I'm a beginner in both of them. The code for flask Flask webhook code :

from flask import render_template
import os
from flask import Flask
from flask import request
from flask import make_response
import json
import time

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route('/webhook', methods=['POST', 'GET'])
def webhook():
    req = request.get_json(silent=True, force=True)
    print(json.dumps(req, indent=4))
    res = makeWebhookResult(req)
    res = json.dumps(res, indent=4)
    print("+++++++++++++++RESPONSE+++++++++++++++++", res)
    r = make_response(res)
    r.headers['Content-Type'] = 'application/json'
    return r

# Right now I'm just printing a response to see if it works properly

def makeWebhookResult(req):
      queryResult = req.get('queryResult').get("queryText")
      speech =  queryResult       
      return {
            "fulfillmentText": 'YOLO',
            "source": 'App'
        }

#./ngrok http 8090
if __name__ == '__main__':
    port = int(os.getenv('PORT', 8090))
    print("Starting app on port %d" % (port))
    app.run(debug=True, port=port, host='localhost')

Now I tried this in tornado like this :

import tornado.ioloop
import tornado.web as web
import tornado
import json
import os

static_root = os.path.join(os.path.dirname(__file__), 'static')


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        template = "./templates/index.html"
        self.render(template)


class Webhook(tornado.web.RequestHandler):
    def prepare(self):
        if self.request.headers.get("Content-Type", "").startswith("application/json"):
            self.req = json.loads(self.request.body)
            print(self.req)
            print(json.dumps(self.req, indent=4))
            response = self.webhook_result(self.req)
            response = json.dumps(response, indent=4)
            response.headers['Content-Type'] = 'application/json'
            return response

    def webhook_result(self, *args):
            queryResult = self.req.get('queryResult').get("queryText")
            speech = queryResult
            print(speech)
            return {
                "fulfillmentText": 'YOLO',
                "source": 'App'
            }

handlers = [
    (r'/', MainHandler),
    (r'(.*)', web.StaticFileHandler, {'path': static_root}),
    (r'/webhook', Webhook,)

]
settings = dict(
    debug=True,
    static_path=static_root
)

application = web.Application(handlers, **settings)
if __name__ == "__main__":
    port = 8090
    application.listen(port)
    print(f"Server running on port : {port}")
    tornado.ioloop.IOLoop.instance().start()

It works fine with Flask. When I try to run this through tornado (using ngrok for tunneling) I get a WARNING:tornado.access:405 POST /webhook (127.0.0.1) 0.83ms

I read tornado's docs but I still can't seem to figure out how do I go about this. I'm assuming the problem lies in the Webhook class. What am I doing wrong here?


Solution

  • Warning shows problem with POST request - and to handle POST you need method def post() in class Webhook.

    It should be post() instead of prepare() (which is for something different).
    And you can use self.write(dictionary) to send it as 'application/json'

    class Webhook(tornado.web.RequestHandler):
    
        def post(self):
            if self.request.headers.get("Content-Type", "").startswith("application/json"):
                data_input = json.loads(self.request.body)
    
                print('data_input:', data_input)
                print('data_input json.dumps:', json.dumps(data_input, indent=4))
    
                data_output = self.webhook_result(data_input) # get as normal dict, not string
    
                print('data_output:', data_output)
                print('data_output json.dumps:', json.dumps(data_output, indent=4))
    
                self.write(data_output) # it will send as JSON
            else:
                self.write({'error': 'Wrong Content-Type'}) # it will send as JSON
    

    BTW: if you send value to webhook_result() then you could get this value - ie as data - and use it instead of self.req

        def webhook_result(self, data):
            speech = data.get('queryResult').get("queryText")
    
            print('speech:', speech)
    
            return {
                "fulfillmentText": 'YOLO',
                "source": 'App'
            }
    

    Code which I tested

    import tornado
    import tornado.web
    import json
    import os
    
    
    static_root = os.path.join(os.path.dirname('.'), 'static')
    
    
    class MainHandler(tornado.web.RequestHandler):
    
        def get(self):
            #self.render("./templates/index.html")
    
            # to test POST request but with wrong Content-Type
            self.write('''<form action="/webhook" method="POST"><button>SUBMIT</button></form>''')
    
    
    class Webhook(tornado.web.RequestHandler):
    
        def post(self):
            if self.request.headers.get("Content-Type", "").startswith("application/json"):
                data_input = json.loads(self.request.body)
    
                print('data_input:', data_input)
                print('data_input json.dumps:', json.dumps(data_input, indent=4))
    
                data_output = self.webhook_result(data_input) # get as normal dict, not string
    
                print('data_output:', data_output)
                print('data_output json.dumps:', json.dumps(data_output, indent=4))
    
                self.write(data_output) # it will send as JSON
            else:
                self.write({'error': 'Wrong Content-Type'}) # it will send as JSON
    
        def webhook_result(self, data):
            speech = data.get('queryResult').get("queryText")
    
            print('speech:', speech)
    
            return {
                "fulfillmentText": 'YOLO',
                "source": 'App'
            }
    
    
    handlers = [
        (r'/', MainHandler),
        (r'/webhook', Webhook),
        # probably it should be as last
        #(r'(.*)', web.StaticFileHandler, {'path': static_root}),
    ]
    
    settings = dict(
        debug=True,
        static_path=static_root
    )
    
    application = tornado.web.Application(handlers, **settings)
    if __name__ == "__main__":
        port = 8090
        application.listen(port)
        print(f"Running: http://127.0.0.1:{port}")
        tornado.ioloop.IOLoop.instance().start()
    

    Code which I used to send POST request with JSON data:

    import requests
    
    url = 'http://127.0.0.1:8090/webhook'
    
    data = {'queryResult': {'queryText': 'Hello World'}}
    
    r = requests.post(url, json=data)
    
    print(r.status_code)
    print(r.headers.get('Content-Type'))
    print(r.json())
    

    BTW: In Flask you could do

    @app.route('/webhook', methods=['POST', 'GET'])
    def webhook():
        data_input = request.get_json(silent=True, force=True)
        data_output = makeWebhookResult(data_input)
        return jsonify(data_output)