[PYTHON] Tips for making interactive bots on LINE

Hello, this is Ninomiya of LIFULL CO., LTD. I was involved in the prototype implementation of the LINE bot this year.

For LINE development, SDK is also available in languages such as Python, and you can easily start making a simple bot. However, if you try to create an application that is more complicated than a certain level, the implementation will have a little difficulty in implementing it, such as managing scenarios and generating FlexMessage json.

For those who implement it next, I will share the points I devised mainly in the implementation of LINE bot.

Enabled to manage scenarios in configuration file

I made it possible to overlook the scenario of the dialogue to some extent in the configuration file. The implementation is such that the regular expression is set by trigger_message in senario, and the controller set by ʻendpoint` is called when it matches.

config.json


//Comments cannot be entered due to the specifications of json, but they are included for convenience.
{
  "scenario": {
    "initial": {
      "trigger_message": "Start the bot",
      "endpoint": "controllers.initial",
      "possible_replies": [
        "initial"
      ]
    },
    //A response to search for a product. at the postback event"method: first_search, target:Book"Assumed that a value such as is sent dynamically
    "search_products": {
      "trigger_message": "method: search_products, target: (?P<target>[\\s\\S]+)",
      "endpoint": "controllers.search_products",
      "possible_replies": [
        "selection"
      ]
    },
    //Default choice
    "default": {
      "endpoint": "controllers.default",
      "possible_replies": [
        "default"
      ]
    }
 },
  "replies": {
    "initial": {
      "view": "views.initial",
      "template": [
        {
          "mode": "text",
          "text": "Select the function you want to call"
        },
        {
          "mode": "flex",
          "template": "flex_messages/buttons.tpl.json",
          "alt_text": "Choices"
        }
      ]
    }, 
    "products": {
      "view": "views.search_result",
      "template": [
        {
          "mode": "text",
          "text": "I searched for a product"
        },
        {
          "mode": "flex",
          "template": "flex_messages/products.tpl.json",
          "alt_text": "Recommended products"
        }
      ]
    }
  }
}

[Finite automaton] of dialogue with bot (https://ja.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E3%82%AA%E3%83%BC%E3%83 Considering% 88% E3% 83% 9E% E3% 83% 88% E3% 83% B3), the message of the bot is made with the image that the state is selected by the button to select the next transition of the user. However, since the content of the postback event is created in the message instead of the configuration file, it does not manage what kind of transition the user makes.

Since I implemented it in Python, I used importlib to call the controller. Also, I implemented it in json, but it's hard to comment, so either use yaml or eg Django dispatcher It may be better to be able to write a script that associates the input message with the class you want to execute, such as / http / urls /).

trigger_message is parsed with a regular expression, and the controller calls it as an argument as follows. This time, as postback event, it is like "the user presses the button prepared here", so the regular expression is triggered. (Post), but if you want to respond with free text, you can set Context (controller to start) and Entity (argument) with tools such as Dialogflow. ..

script/controllers/search_products/__init__.py


from script.controllers._utils import AbstractController, Response, render, UserContext
from script.models import search_model

class Controller(AbstractController):
    """Controller that gives the first choice"""

    def call(self, user_context: UserContext, target: str) -> Response:
        """Controller call processing

        Args:
            user_context:Class that wraps id etc.
            target:What you parsed with a named capture of a regular expression
        Returns:
Reply to user

        """
        products = search_model.search(target)
        return render("initial", proc=lambda view: view.render(products=products))

FlexMessage was assembled with the template engine (jinja2)

In LINE, you can use Messages with a flexible layout called FlexMessage.

To do this, you need to assemble a complex FlexMessage json and send LINE's Reply API. I first made FlexMessage with a Python dictionary, but it was quite difficult, so I referred to this article and used the template engine (jinja2) template. I prepared it and implemented it in view.

However, if you designed it using Simulator, it would be tedious to rewrite it into the SDK model. The source will also be longer. In such a case, it seems better to embed the value in JSON using the template engine and convert the result to the SDK model. A method called new_from_json_dict () is provided to build a model from JSON.

I implemented it in jinja2, but I don't think it will change much in other languages and libraries.

https://palletsprojects.com/p/jinja/

{
  "type": "carousel",
  "contents": [
{% for product in products %}
    {
      "type": "bubble",
      "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
          {
            "type": "box",
            "layout": "horizontal",
            "contents": [
              {
                "type": "image",
                "url": {{ product.image | json_escape }},
                "size": "full",
                "aspectMode": "cover",
                "action": {
                  "type": "uri",
                  "uri": {{ product.url | json_escape }}
                }
              }
            ]
          },
//The following is omitted

The actual state of json_escape here is json.dumps, and the process of "escaping the string and adding " to ensure that the correct json is assembled" is performed.

class JsonTemplete(object):
    ENV = Environment(loader=FileSystemLoader(
        str(Path(__file__).resolve().parent / "../templetes"), encoding='utf8'))
    ENV.filters["json_escape"] = json.dumps

#The following is omitted

Load this into the SDK with method called new_from_json_dict and use it.

    def __build_contents(self, content):
        """Sort the types of containers in FlexSendMssage.
        Args:
            content (dict):Container information
        Returns:
            BubbleContainer/CarouselContainer:Container in FlexSendMessage
        """
        t = content["type"]
        if t == "bubble":
            return BubbleContainer.new_from_json_dict(content)
        elif t == "carousel":
            return CarouselContainer.new_from_json_dict(content)
        else:
            raise RuntimeError(f"It is an unsupported container: {t}")

However, in the case of "the grammar is correct for json, but it does not meet the LINE message specifications", the problem that debugging is a little troublesome remains. I can't think of a good solution for this, so please let me know if you have any knowledge.

Other points that I struggled with in the LINE project

Depending on the user's smartphone model and settings, the appearance of FlexMessage changed (for example, the characters were larger than expected and line breaks were inserted), and sometimes it did not look as expected. You can't specify the font size in detail, so you shouldn't aim for a very elaborate design.

Also, as is often said in other articles, it will be smooth if you adjust the appearance with FlexMssageSimulator and embed the contents of the button in the template to implement it. ..

at the end

I hope this article will be helpful for those who are starting to implement chatbots.

This article is the first day article of LIFULL Advent Calendar. Thank you for your continued support.

Recommended Posts

Tips for making interactive bots on LINE
Tips for making small tools in python
Tips for using Realsense SR300 on MacBook in 2020
Procedure for creating a Line Bot on AWS Lambda
Search for large files on Linux from the command line