Automatisez vos Conversations : Créez un Bot Signal en Python !

BrunoBruno
7 min read

Contexte

Le monde des messageries instantanées est en constante évolution. Les plateformes comme Signal ou WhatsApp offrent des fonctionnalités de plus en plus avancées pour personnaliser votre expérience utilisateur. Mais qu'est-ce que l'on peut faire pour aller encore plus loin ? En créant un bot qui interagit avec ces plateformes, vous pouvez automatiser des tâches et même créer des applications plus complexes !

Dans cet article, nous allons explorer comment créer un bot Python qui lit les messages reçus sur Signal et répond à ceux-ci en utilisant l'API d'OpenWebUI.

Prérequis

Pour pouvoir exécuter le script, il est nécessaire de mettre en place certains prérequis.

Pourquoi un bucket S3 ?

L’idée ici est de donner une “conscience” à notre bot de conversations qui va lui permettre de se souvenir de l’historique de chaque conversation pour une meilleure pertinence dans ses réponses. Cet historique sera stocké sur un bucket S3 AWS, libre à vous d’utiliser d’autres solutions open-source comme Minio.

Les conversations seront stockées dans un fichier JSON sur un bucket S3 et notre bot ira récupérer son contenu à chaque démarrage.

Pour l’interaction avec l’API Signal, je me suis appuyé sur Dockerized Signal Messenger REST API. Se référer au projet directement pour l’installation du container et l’enregistrement de ce dernier à l’API Signal.

Le code

Le code est composé de plusieurs modules :

  1. generic_libs : Contient des fonctions génériques pour interagir avec les APIs de Signal et OpenWebUI.

    • signal_lib : Permet d'interagir avec l'API de Signal.

    • openwebui_lib : Permet d'interagir avec l'API d'OpenWebUI.

    • s3_lib : Permet d'accéder à un bucket S3 pour stocker la conversation.

  2. Le script principal : Le code qui permet de démarrer le bot et gérer les messages reçus sur Signal.

Les librairies utilisées

Les librairies suivantes sont nécessaires pour fonctionner correctement :

  • boto3 : Permet d’intéragir avec les contenus des buckets S3.

Les variables nécessaires

Pour fonctionner correctement, notre script Python nécessite certaines variables d’environnement à définir. Voici la liste des variables nécessaires :

  1. SIGNAL_PHONE_NUMBER : Le numéro de téléphone associé à votre compte Signal.

  2. AWS_ACCESS_KEY_ID : L'ID clé d'accès à votre compte AWS (pour l'utilisation du stockage S3).

  3. AWS_SECRET_ACCESS_KEY : La clé secrète d'accès à votre compte AWS.

  4. OPENWEBUI_API_KEY : Le code d'API pour interagir avec l'interface utilisateur d'OpenWebUI.

Ces variables sont renseignées dans le dictionnaire CONFIG du fichier main.py

CONFIG = {
    'phone_number': os.environ['SIGNAL_PHONE_NUMBER'],
    's3_access_key': os.environ['AWS_ACCESS_KEY_ID'],
    's3_secret_key': os.environ['AWS_SECRET_ACCESS_KEY'],
    'openwebui_api_key': os.environ['OPENWEBUI_API_KEY'],
    'signal_url': 'http://127.0.0.1:8081',
    'openwebui_url': 'http://127.0.0.1:8080',
    's3_bucket': 'signal.bot',
    's3_key_prefix': 'conversation_history.json',
    'conversation_history_file': '/tmp/conversation_history.json',
    'pause_duration': 5 # seconds
}

Le fonctionnement

Lorsque le script principal est exécuté, il vérifie continuellement les messages reçus sur Signal en utilisant l'API de Signal.

# signal_lib.py
def fetch_signal_messages(url) -> (tuple[str, str]):
    """
    Fetch signal messages from the specified URL.

    :param url: Signal message URL.
    :return: Tuple containing the source number and message text.
    """
    source_number=None
    message=None

    try :
        response=requests.get(url)
        data=json.loads(response.text)
        # print(f'Requête reçue: {data}')
        for element in data:
            if 'dataMessage' in element['envelope']:
                if 'groupInfo' in element['envelope']['dataMessage']:
                    source_number=element['envelope']['dataMessage']['groupInfo'].get('groupId')
                else:
                    source_number=element['envelope'].get('sourceNumber')
                message=element['envelope']['dataMessage'].get('message')
                logging.info(f'Source Number: {source_number}')
                logging.info(f'Message: {message}')
        return source_number, message
    except requests.exceptions.RequestException as e:
        # Handle any exceptions that occur during request
        print(f"Error fetching signal messages: {e}")
        return None, None

Lorsqu'il trouve un nouveau message, il traite celui-ci et l’envoie à l'API d'OpenWebUI pour générer une réponse grâce au modèle LLM chargé.

# openwebui_lib.py
def send_message_to_openwebui(url, source_number, message, conversation_history, openwebui_api_key):
    """
    Send a message to OpenWebUI.

    :param url: OpenWebUI API URL.
    :param source_number: Source number of the message.
    :param message: Message text to send.
    :param conversation_history: Conversation history dictionary.
    :param openwebui_api_key: OpenWebUI API key.
    :return: The response from OpenWebUI.
    """

    # Create a new conversation history entry for the source number if not exists
    if not source_number in conversation_history:
        conversation_history[source_number] = []

    try:
        # Create a new conversation history entry for the source number
        conversation_history[source_number].append({'role':'user','content':message})

        # Define the API request data and headers
        donnees = {
            'model': 'signalbot',
            'messages': conversation_history[source_number]
        }
        en_tetes = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {openwebui_api_key}'
        }

        # Send a POST request to the OpenWebUI API
        response = requests.post(url, json=donnees, headers=en_tetes)

        # Parse the JSON response and extract the message text
        data = json.loads(response.text)
        msg = data['choices'][0]['message']['content']

        # Update the conversation history with the new message
        conversation_history[source_number].append({'role':'assistant','content':msg})

        return msg

    except requests.exceptions.RequestException as e:
        # Handle any exceptions that occur during request
        print(f"Error sending message to OpenWebUI: {e}")
        return None

Le modèle LLM que doit utiliser l’API OpenWebUI est renseigné dans le fichier generic_libs/openwebui_lib.py.

         # Define the API request data and headers
 25         donnees = {
 26             'model': 'signalbot',
 27             'messages': conversation_history[source_number]
 28         }
 29         en_tetes = {
 30             'Content-Type': 'application/json',
 31             'Authorization': f'Bearer {openwebui_api_key}'
 32         }

Le modèle utilisé ici se nomme signalbot, c’est un modèle créé à partir du modèle llama3.1 mais avec un prompt système personnalisé.

Libre à vous de créer et gérer vos propres prompts pour une personnalisation des réponses du bot.
Reportez-vous à cet article pour opérer ce type de changements.

La réponse générée sera renvoyée à l’API Signal pour renvoi vers l’utilisateur ou groupe Signal correspondant.

# signal_lib.py
def send_message_to_signal_cli(url, phone_number, recipient, msg) -> None:
    """
    Send a message to Signal CLI.

    :param url: Signal CLI API URL.
    :param phone_number: Phone number of the sender.
    :param recipient: Recipient number of the message.
    :param msg: Message text to send.
    :return: None.
    """

    # Define the API request data and headers
    donnees = {
        'message': f'{msg}',
        'number': f'{phone_number}',
        'recipients': [f'{recipient}']
    }
    en_tetes = {
        'Content-Type': 'application/json'
    }

    # Send a POST request to the Signal CLI API
    try:
        requests.post(url, json=donnees, headers=en_tetes)

    except requests.exceptions.RequestException as e:
        # Handle any exceptions that occur during request
        print(f"Error sending message to Signal CLI: {e}")

Au démarrage du bot, le fichier conversation_history.json stocké sur le bucket S3 est téléchargé localement à l’emplacement défini par la variable conversation_history_file.

# s3_lib.py
def download_file(bucket, object_name, file_name) -> bool:
    """
    Download a file from an S3 bucket.

    :param object_name: File to download.
    :param bucket: Bucket to download from.
    :param file_name: Local path where the file should be saved.
    :return: True if file was downloaded, else False.
    """
    # Use boto3 library to interact with AWS S3
    s3 = boto3.client('s3')

    try:
        # Upload the file to the specified bucket and object name
        s3.download_file(bucket, object_name, Filename=file_name)

        # If successful, return True
        return True

    except Exception as e:
        # Handle any exceptions that occur during upload
        print(f"Error downloading file: {e}")
        return False

Un fichier conversation_history.json vierge est créé au démarrage du bot s’il n’existe pas (le fichier n’a pas été téléchargé depuis S3).

# main.py
def load_conversation_history() -> (dict):
    """
    Load the conversation history.

    :return: A dictionary containing the conversation history.
    """

    try:
        with open(CONFIG['conversation_history_file'], 'r') as file:
            return json.load(file)
    except FileNotFoundError:
        # Le fichier n'existe pas, créez-le avec un objet JSON vide
        with open(CONFIG['conversation_history_file'], 'w') as file:
            file.write(json.dumps({}))
        return {}

Il sera complété à chaque interaction pour permettre à notre bot de garder en mémoire l’historique des messages échangés pour chaque groupe/utilisateur.

C’est la fonction send_message_to_openwebui du fichier generic_libs/openwebui_lib.py qui est en charge d’alimenter le fichier d’historique.

# openwebui_lib.py
def send_message_to_openwebui(url, source_number, message, conversation_history,openwebui_api_key):
    """
    Send a message to OpenWebUI.

    :param url: OpenWebUI API URL.
    :param source_number: Source number of the message.
    :param message: Message text to send.
    :param conversation_history: Conversation history dictionary.
    :param openwebui_api_key: OpenWebUI API key.
    :return: The response from OpenWebUI.
    """

    # Create a new conversation history entry for the source number if not exists
    if not source_number in conversation_history:
        # conversation_history.update({source_number: []})
        conversation_history[source_number] = []

    try:
        # Create a new conversation history entry for the source number
        conversation_history[source_number].append({'role':'user','content':message})
[...]
        # Update the conversation history with the new message
        conversation_history[source_number].append({'role':'assistant','content':msg})

Par défaut, ce fichier se trouve sur /tmp/conversation_history.json.

Lorsque le script est arrêté, ce fichier est envoyé vers le bucket S3 configuré pour sauvegarde.

# s3_lib.py
def upload_file(file_name, bucket, object_name) -> bool:
    """
    Upload a file to an S3 bucket.

    :param file_name: File to upload.
    :param bucket: Bucket to upload to.
    :param object_name: S3 object name.
    :return: True if file was uploaded, else False.
    """
    # Use boto3 library to interact with AWS S3
    s3 = boto3.client('s3')

    try:
        # Upload the file to the specified bucket and object name
        s3.upload_file(file_name, bucket, object_name)

        # If successful, return True
        return True

    except Exception as e:
        # Handle any exceptions that occur during upload
        print(f"Error uploading file: {e}")
        return False

Le code dans son intégralité se trouve sur le projet GitLab signal-conversational-bot.

Conclusion

Créer un bot conversationnel pour Signal en Python offre une opportunité passionnante d'automatiser des tâches et d'améliorer l'interaction avec les utilisateurs. En utilisant des outils comme l'API Dockerized Signal Messenger REST et OpenWebUI, vous pouvez concevoir un bot capable de gérer des conversations de manière intelligente et personnalisée. L'intégration d'un stockage S3 pour conserver l'historique des conversations permet d'améliorer la pertinence des réponses du bot. Ce projet démontre comment les développeurs peuvent tirer parti des technologies modernes pour créer des solutions innovantes et adaptées à leurs besoins spécifiques.

0
Subscribe to my newsletter

Read articles from Bruno directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bruno
Bruno

Depuis août 2024, j'accompagne divers projets sur l'optimisation des processus DevOps. Ces compétences, acquises par plusieurs années d'expérience dans le domaine de l'IT, me permettent de contribuer de manière significative à la réussite et l'évolution des infrastructures de mes clients. Mon but est d'apporter une expertise technique pour soutenir la mission et les valeurs de mes clients, en garantissant la scalabilité et l'efficacité de leurs services IT.