Skip to content

Open In Colab

How to develop a Kili Webhook - example

Context

This notebook is an end-to-end example that you can follow to create a project, register a first webhook and activate it on this project for the corresponding events in Kili.

For more information on the available events, please refer to the documentation.

Webhooks are really similar to the plugins, except they are self hosted, and will require a webservice deployed on your premise, callable by Kili (You can implement a header-based security).

If you are a Europe SaaS user, plugins & webhooks are available for paying-customers in beta for now. If you are a US/ On Premise customer, only webhooks are available as of version 1.128.0.

Webhook allow you to easily access your custom code, manage the CI/CD of the version of the plugin deployed, and easily integrate it with your own stack.

If you are looking for a more of-the-shelf capability, you can have a look at the plugins development tutorial.

NB: The webhook capabilities of Kili are under active development, and compatible with version 2.128.0 and later of Kili. Don't hesitate to reach out via Github or the Kili support to provide feedback.

Step 1: Instantiate Kili

!pip install kili
from kili.client import Kili

kili = Kili()

Step 2: Create the project

First, we need to create a new project. In our example, we will use an IMAGE type project with the following jsonInterace:

json_interface = {
    "jobs": {
        "JOB_0": {
            "content": {
                "categories": {
                    "OBJECT_A": {
                        "children": [],
                        "name": "Object A",
                        "color": "#733AFB",
                        "id": "category1",
                    },
                    "OBJECT_B": {
                        "children": [],
                        "name": "Object B",
                        "color": "#3CD876",
                        "id": "category2",
                    },
                },
                "input": "radio",
            },
            "instruction": "Categories",
            "isChild": False,
            "tools": ["rectangle"],
            "mlTask": "OBJECT_DETECTION",
            "models": {},
            "isVisible": True,
            "required": 1,
            "isNew": False,
        }
    }
}
title = "Webhooks test project"
description = "My first project with a webhook"
input_type = "IMAGE"

project = kili.create_project(
    title=title, description=description, input_type=input_type, json_interface=json_interface
)
project_id = project["id"]

print(f"Created project {project_id}")
Created project clfcblkni05pq0jrq8wgib142

Upload an asset:

content_array = ["https://storage.googleapis.com/label-public-staging/car/car_1.jpg"]
names_array = ["landscape2"]

kili.append_many_to_dataset(
    project_id=project_id,
    content_array=content_array,
    external_id_array=names_array,
    disable_tqdm=True,
)

asset_id = list(kili.assets(project_id=project_id, fields=["id"], disable_tqdm=True))[0]["id"]

This project has one job of bounding box creation with two categories.

With our plugin, we want to make sure that the labelers don't create more than one bounding box of category A.

To iterate on the plugin code, you can refer to the plugins development tutorial.

Step 3: Write & host the webhook

The webhook rely on the same handlers provided by the plugins. For maximum compatibility, we encourage you to define it with the same base class. Below is an example with FastAPI webservice.

# file plugin.py
"""
Custom module with basic plugin example
"""
from typing import Dict

from kili.plugins import PluginCore


def check_rules_on_label(label: Dict):
    """
    Custom function to check rules on label.
    For basic rules, a handy object is `search` that \
        provides various analytics on objects
    For more advanced use-cases, you might need to \
        fetch the complete `jsonResponse`
    """

    issues_array = []
    for job_dot_category, nb_of_objects in label['search']['numberOfAnnotationsByObject'].items():
        if job_dot_category == "JOB_0.OBJECT_A":
            if nb_of_objects > 1:
                issues_array.append({
                    'text': f'There are too many BBox ({nb_of_objects}) - Only 1 BBox of Object A accepted',
                    'mid': None}
                )
    return issues_array


def _get_area(bounding_box):
    """
    Custom helper to compute size of Kili Bounding boxes
    """
    x_array = [point['x'] for point in bounding_box]
    y_array = [point['y'] for point in bounding_box]
    width = max(x_array) - min(x_array)
    height = max(y_array) - min(y_array)
    return width * height


class PluginHandler(PluginCore):
    """
    Custom plugin instance
    """

    def check_complex_rules_on_label(self, asset_id: str):
        """
        Custom method to check if a box is larger than 33% of the image
        For basic rules, a handy object is `search` that \
            provides various analytics on objects
        In this more complex use-case, we will \
            fetch the complete `jsonResponse`
        """
        json_response = self.kili.labels(
            asset_id=asset_id,
            project_id=self.project_id,
            fields=['jsonResponse'],
            disable_tqdm=True
        )[0]['jsonResponse']

        issues_array = []
        for annotation in json_response['JOB_0']['annotations']:
            bounding_box = annotation['boundingPoly'][0]['normalizedVertices']
            area = _get_area(bounding_box)
            # Refuse bounding boxes larger than 0.33
            if area > 0.33:
                issues_array.append({
                    'text': 'BBox too large',
                    'mid': annotation["mid"]
                })

        return issues_array

    def on_submit(self, label: Dict, asset_id: str) -> None:
        """
        Dedicated handler for Submit action
        """
        self.logger.info("On submit called")

        issues_array = check_rules_on_label(label)

        issues_array += self.check_complex_rules_on_label(asset_id)

        project_id = self.project_id

        if len(issues_array) > 0:
            print(f'Creating {len(issues_array)} issues...')

            for issue in issues_array:
                print(issue)

                self.kili.create_issues(
                    label_id_array=[label['id']],
                    project_id=project_id,
                    text_array=[issue['text']],
                    object_mid_array=[issue['mid']]
                )

            self.logger.warning("Issue created!")

            self.kili.send_back_to_queue(asset_ids=[asset_id])

        else:
            self.logger.info('No issues encountered')

You will need to deploy this on your premise for this to work. Easy solutions are FastAPI, with a few lines of codes. For this demo, we will use https://webhook.site that will allow us to explore the payload of the calls.

"""
Basic app for kili webhook
Note: Don't host it locally, it won't work as Kili can't call your localhost
"""
# file main.py
import os
from typing import Dict
from fastapi import FastAPI
from kili.client import Kili

# Assuming your plugin is in a file  `plugin.py` in the same folder
from plugin import PluginHandler

app = FastAPI()
kili = Kili()


@app.post("/")
def main(raw_payload: Dict):
    """
    Basic endpoint to receive kili events
    """
    event_type = raw_payload.get('eventType')
    project_id = raw_payload.get('logPayload').get('projectId')

    if not project_id:
        print('Invalid projectId')
        return

    plugin = PluginHandler(kili, project_id)

    if not event_type:
        print('Invalid event')
        return

    payload = raw_payload.get('payload')
    label = payload.get('label')
    asset_id = payload.get('asset_id')

    if event_type == 'onSubmit':
        plugin.on_submit(label, asset_id)

    if event_type == 'onReview':
        plugin.on_review(label, asset_id)

Step 4: Register & activate the webhook

from kili.exceptions import GraphQLError
import requests


# we get a new webhook listener
res = requests.post("https://webhook.site/token")
uuid = res.json()["uuid"]
webhook_url_from_browser = f"https://webhook.site/#!/{uuid}"

webhook_name = "Webhook bbox count"
webhook_url = f"https://webhook.site/{uuid}"
print(webhook_url_from_browser)
webhook_security_header = "custom header"

try:
    kili.create_webhook(
        plugin_name=webhook_name, webhook_url=webhook_url, header=webhook_security_header
    )
except GraphQLError as error:
    print(str(error))
https://webhook.site/#!/f81dfe6a-****-****-****-4b6dfe4b0721
kili.activate_plugin_on_project(plugin_name=webhook_name, project_id=project_id)
Plugin with name "Webhook bbox count" activated on project "clfcblkni05pq0jrq8wgib142"
INFO:kili.services.plugins:Plugin with name "Webhook bbox count" activated on project "clfcblkni05pq0jrq8wgib142"





'Plugin with name Webhook bbox count successfully activated'

Note: Similar to plugins, you have access to the methods kili.update_webhook & kili.deactivate_plugin_on_project for iterations on your code.

Step 5: Webhook in action

After that, you can test it by labelling in the Kili interface or just by uploading the following label.

When you add the label that contains errors, you will see a new issue automatically created in the Kili app, if you have deployed the webhook. Else, you can visit the webhook site to check incoming events.

json_response = {
    "JOB_0": {
        "annotations": [
            {
                "boundingPoly": [
                    {
                        "normalizedVertices": [
                            {"x": 0.15, "y": 0.84},
                            {"x": 0.15, "y": 0.31},
                            {"x": 0.82, "y": 0.31},
                            {"x": 0.82, "y": 0.84},
                        ]
                    }
                ],
                "categories": [{"name": "OBJECT_A"}],
                "children": {},
                "mid": "20221124161451411-13314",
                "type": "rectangle",
            },
            {
                "boundingPoly": [
                    {
                        "normalizedVertices": [
                            {"x": 0.79, "y": 0.20},
                            {"x": 0.79, "y": 0.13},
                            {"x": 0.91, "y": 0.13},
                            {"x": 0.91, "y": 0.20},
                        ]
                    }
                ],
                "categories": [{"name": "OBJECT_A"}],
                "children": {},
                "mid": "20221124161456406-47055",
                "type": "rectangle",
            },
            {
                "boundingPoly": [
                    {
                        "normalizedVertices": [
                            {"x": 0.87, "y": 0.36},
                            {"x": 0.87, "y": 0.27},
                            {"x": 0.99, "y": 0.27},
                            {"x": 0.99, "y": 0.36},
                        ]
                    }
                ],
                "categories": [{"name": "OBJECT_A"}],
                "children": {},
                "mid": "20221124161459298-45160",
                "type": "rectangle",
            },
        ]
    }
}
kili.append_labels(
    json_response_array=[json_response], asset_id_array=[asset_id], label_type="DEFAULT"
)
[{'id': 'clfcblncs0h550js5golxg96s'}]

If you used & hosted the base webhook provided, the webhook should:

  • Create an issue with information that three bboxes were found, instead of one
  • Create an issue with info that the first bbox is too large
  • Send the asset back to the labeling queue (status ONGOING)

If you haven't deployed your webhook just yet, you can still visit the address here :

print(f"Go to my webhook: {webhook_url_from_browser}")
try:
    # If your webhook is live !
    kili.issues(project_id=project_id, fields=["comments.text", "objectMid"])
except GraphQLError as error:
    print(str(error))
Go to my webhook: https://webhook.site/#!/f81dfe6a-****-****-****-4b6dfe4b0721

Woah! Amazing! Well done :) 🚀

Let's test now to post a proper label, this one for example:

json_response = {
    "JOB_0": {
        "annotations": [
            {
                "boundingPoly": [
                    {
                        "normalizedVertices": [
                            {"x": 0.15, "y": 0.84},
                            {"x": 0.15, "y": 0.31},
                            {"x": 0.82, "y": 0.31},
                            {"x": 0.82, "y": 0.84},
                        ]
                    }
                ],
                "categories": [{"name": "OBJECT_A"}],
                "children": {},
                "mid": "20221124161451411-13314",
                "type": "rectangle",
            }
        ]
    }
}
kili.append_labels(
    json_response_array=[json_response], asset_id_array=[asset_id], label_type="DEFAULT"
)

print(f"Go to my webhook: {webhook_url_from_browser}")
Go to my webhook: https://webhook.site/#!/f81dfe6a-****-****-****-4b6dfe4b0721

The status of your asset should have now changed to LABELED. In this webhook, previous issues remain but you can solve them through the API as well.

Well done! You can now iterate on the script. To learn how to avoid latency when building and deploying your plugin, refer to the plugins development tutorial.