Couverture de l'article Créez votre assistant personnel grâce à Gmail et ChatGPT 4
Retour aux articles

L'agence

WanadevStudio

Créez votre assistant personnel grâce à Gmail et ChatGPT 4

Les LLMs, ou Large Language Models, comme ChatGPT, Llama2, Claude ou encore Mistral nous donne un avant-goût du futur des assistants virtuels tels que nous les connaissons avec Google, Siri ou Alexa. Pourtant, il manque une sacrée touche personnelle avec ces assistants nouvelle génération. Même si OpenAI a mis en place un système permettant d’à peu près personnaliser son expérience avec ChatGPT, nous sommes loin d’avoir des IA qui nous connaissent réellement. La raison est assez simple : ceux-ci n’ont pas accès à vos données. Et si on aidait ces LLMs si puissant en leur donnant des données pour qu’ils sachent répondre à des requêtes très personnalisées ?

Prérequis

Cet article part du principe que vous avez un environnement virtuel Python avec Python3 et pip.

Récupérer vos données depuis Google

La première étape pour obtenir notre assistant personnel est de récupérer les mails de notre boîte de réception. Bien que cet exemple concerne Gmail, on utilisera IMAP. Cela signifie que toute boîte mail prenant en charge ce protocole (en d'autres termes : toutes les boîtes de réception existantes) est compatible avec le code de cet article.

Nous n'allons pas récupérer la boîte de réception entière, car cela peut être très long et lourd. À la place, nous allons récupérer tous les emails reçus au cours des 7 derniers jours. Selon le nombre d'emails que vous recevez, vous pouvez changer ce nombre comme vous le souhaitez.

Si vous utilisez Gmail, vous devez suivre cette page de support pour obtenir un mot de passe d'application afin d'interagir avec Gmail. Pour toute autre boîte de réception, vous pouvez utiliser votre mot de passe habituel ou vous référer à sa documentation spécifique.

Allez, un peu de code maintenant ! Tout d'abord, nous allons nous connecter à notre boîte de réception grâce à IMAP :

# inbox_imap_fetcher.py
import email
import imaplib
import logging
import os

from email.header import decode_header
from datetime import datetime, timedelta

email_user = "your.address@gmail.com"
email_pass = "your application password"

mail = imaplib.IMAP4_SSL("imap.gmail.com")
mail.login(email_user, email_pass)

mail.select('"[Gmail]/All messages"')

Faites attention à la dernière ligne : les guillemets sont obligatoires dans le cas de Gmail. Aussi, ce nom peut changer selon la langue que vous utilisez au sein de Google.

Si vous voulez savoir quelle boîte de réception est disponible pour l'appel de mail.select(), vous pouvez utiliser mail.list() pour récupérer une liste des "dossiers".

Ensuite, nous pouvons récupérer nos messages reçus au cours des 7 derniers jours :

# adaptez l'argument `days` pour récupérer plus ou moins d'emails, selon la taille
# de votre boîte de réception
date_since = (datetime.now() - timedelta(days=7)).strftime("%d-%b-%Y")
result, message_ids = mail.search(None, '(SINCE "{}")'.format(date_since))

# cette partie sera peut-être a adapté selon le mail provider que vous utilisez
if result != 'OK':
    logging.error('Bad response from third-party')

    exit(1)

Ensuite, nous déclarons quelques variables qui vont bientôt être utiles :

# extraire les identifiants de message de la réponse du provider
message_ids = message_ids[0].split()

# et ce tableau contiendra l'intégralité du corps de nos éléments
docs = []
count = 0

La variable count sera utilisée pour afficher une barre de progression. Nous allons le faire grâce à la bibliothèque progressbar2, que vous pouvez installer avec la commande suivante :

pip install progressbar2

Nous devons également installer Langchain, qui va nous aider à donner un peu de contexte au modèle que nous utiliserons (ChatGPT, dans notre cas) :

pip install langchain

Analyse des emails

Nous pouvons désormais analyser nos emails avec le code suivant :

# ajoutez cet import en haut du script
from langchain.schema.document import Document
import progressbar

# ...

with progressbar.ProgressBar(max_value=len(message_ids)) as bar:
    for message_id in message_ids:
        count += 1
        result, message_data = mail.fetch(message_id, "(RFC822)")

        if result != 'OK':
            # on ignore en cas d'erreur, pour simplifier l'exemple
            continue

        raw_email = message_data[0][1]
        msg = email.message_from_bytes(raw_email)
        subject, encoding = decode_header(msg["Subject"])[0]

        # on décode le sujet dans le cas où ce n'est pas une simple chaîne de caractères
        if isinstance(subject, bytes):
            try:
                subject = subject.decode(encoding if encoding is not None else "utf-8")
            except LookupError:
                # le sujet est invalide, on ignore pour simplifier l'exemple à nouveau
                continue

        # ajout du sujet et des métadonnées dans le contenu
        content = f"""
        Subject: {subject}
        From: {msg["From"]}
        Date: {msg["Date"]}
        ===================
        """

        # décodage de chaque partie de l'email si celui-ci est multipart, ou
        # simplement de son contenu s'il est singlepart
        try:
            if msg.is_multipart():
                for part in msg.walk():
                    if part.get_content_type() == "text/plain":
                        body = part.get_payload(decode=True)
                        content += body.decode("utf-8")
            else:
                body = msg.get_payload(decode=True)
                content += body.decode("utf-8")

        except UnicodeDecodeError:
            # on drop les emails invalides pour simplifier l'exemple
            continue

        # ajout du document à la liste en utilisant la classe `Document` de Langchain
        docs.extend([Document(page_content=content, metadata={
            "source": message_id,
            "subject": subject
        })])

        # finalement, on met à jour la barre de progression pour une meilleure expérience utilisateur
        bar.update(count)

mail.logout()

Pour simplifier l'article, la gestion des erreurs dans cet exemple est très basique. Vous pourriez vouloir avoir un meilleur processus de gestion des erreurs avec davantage de logs ou une stratégie de retry par exemple.

Une partie extrêmement importante de ce code se trouve en réalité à la toute fin : nous créons un nouveau document qui sera stocké dans un vector store (ou stockage vectoriel) grâce à DeepLake, comme nous le verrons dans un instant.

Stockage des données

Le Document que nous avons créé dans l'exemple précédent prend deux arguments : un page_content qui représente le corps du message, mais aussi un dictionnaire metadata.

Vous pouvez y mettre n'importe quelles données. Cela peut être utile si vous avez besoin de préfiltrer les messages par exemple, facilitant ensuite la recherche d'informations pertinentes. Un cas d'utilisation de base serait de stocker les différents libellés affectés au message, puis de filtrer sur ces derniers pour réduire le champ de recherche lors de la construction du contexte pour le modèle de langage. En effet, cette étape ne nécessite pas d'intelligence artificielle et peut être faite en amont.

Nous n'allons pas stocker les messages tels quels, bruts et non traités. Nous voulons qu'ils soient stockés en chunks. Voici quelques raisons pour lesquelles nous faisons cela :

  • Réduction de la dimensionnalité : Si un message est très long, il peut contenir beaucoup d'informations redondantes ou moins pertinentes. En le divisant en morceaux, la dimensionnalité des embedding vectors (le format dans lequel les données sont stockées) est réduite, ce qui peut améliorer l'efficacité des calculs et la vitesse de recherche.
  • Concentration sur les parties importantes : Certains segments d'un message peuvent contenir des informations plus cruciales ou discriminantes que d'autres. En stockant ces parties en chunks séparés, plus d'attention peut être accordée à ces éléments lors de la recherche ou de l'analyse.
  • Gestion des longs messages : Si de très longs messages doivent être stockés, les traiter en chunks peut aider à éviter les problèmes liés à la gestion de la mémoire ou du stockage pour de très grands messages. De plus, de nombreux modèles de langage ont une limite à leur fenêtre de contexte et certains messages peuvent ne pas entrer dans cette fenêtre (ce qui provoquera une erreur au moment du requêtage).
  • Recherche plus précise : Si un utilisateur recherche un contenu spécifique au sein d'un message, le stockage en chunks permet des résultats plus précis en mettant en évidence les parties pertinentes.

Nous n'allons pas splitter ces messages nous-même. À la place, nous allons utiliser le RecursiveCharacterTextSplitter de Langchain pour le faire :

from langchain.text_splitter import RecursiveCharacterTextSplitter

# ...

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(docs)

chunk_size et chunk_overlap sont des valeurs arbitraires. Tout comme les hyperparamètres en machine learning, il est recommandé de tester quelles valeurs conviennent le mieux à votre cas d'utilisation.

Nous allons maintenant stocker ces données dans un DeepLake vector store, et nous allons convertir ces documents en vecteurs grâce à l'API d'embeddings d'OpenAI. En effet, DeepLake d'Activeloop permet de stocker des informations sous la forme de vecteurs mathématiques multidimensionnels. Nous verrons plus tard à quel point cela est utile pour trouver des similarités dans les informations et avoir une recherche pertinente dans nos données.

Même si nous n'utilisons ici que du texte, il est important de noter qu'il est possible de vectoriser de nombreux types de ressources comme des PDF, des images, voire des vidéos.

Pour l'instant, nous allons voir comment discuter avec un LLM, ou Large Language Model. L'exemple suivant utilise la solution payante d'OpenAI. Nous verrons plus tard comment adapter votre code pour faire tourner un autre LLM, gratuitement et en local sur votre ordinateur.

Requêter OpenAI

Utiliser OpenAI Embedding pour vectoriser nos données

Utiliser les modèles d'OpenAI est une option payante, mais fournit des réponses rapides et de grande qualité. Comme nous voulons discuter avec GPT4 d'OpenAI, nous utilisons OpenAIEmbeddings pour générer nos vecteurs. Utiliser cette classe nécessite d'avoir un compte OpenAI pour utiliser leur API. Si vous n'avez pas de compte, vous pouvez vous rendre sur cette page.

Une fois sur cette page, vous pouvez cliquer sur votre photo de profil, puis sur "View API Keys". Dans cette page, vous pourrez créer une nouvelle clé. Gardez cette clé quand OpenAI vous la donne, car vous ne pourrez plus la voir ! Si vous la perdez, vous devrez en créer une nouvelle.

Nous pouvons maintenant enregistrer notre clé secrète OpenAI et utiliser DeepLake pour stocker nos documents sous forme d'embeddings OpenAI dans un stockage appelé vector_gmail_openai :

from langchain.vectorstores import DeepLake
from langchain.embeddings.openai import OpenAIEmbeddings
import os

# ...

os.environ['OPENAI_API_KEY'] = 'sk-...' # à remplacer par votre propre clé secrète
DeepLake.from_documents(texts, OpenAIEmbeddings(), dataset_path='vector_gmail_openai')

Si tout va bien, le script devrait se terminer correctement et un nouveau répertoire appelé vector_gmail_openai devrait avoir été créé. Nos mots ont été convertis en vecteurs mathématiques, et ces vecteurs sont prêts à être manipulés.

Il est temps d'utiliser ces données et de parler à GPT4 !

Envoyer du context à GPT

Créons maintenant un nouveau fichier appelé talk.py et ajoutons les imports nécessaires en haut. Nous verrons leur utilisation dans un instant :

# talk.py
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import DeepLake
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA
import os

La prochaine étape est de charger notre data lake ainsi que d'enregistrer à nouveau notre clé secrète, car nous sommes maintenant dans un nouveau script :

os.environ['OPENAI_API_KEY'] = 'sk-...' # à remplacer par votre propre clé secrète
dataset_path = 'vector_gmail_openai'

db = DeepLake(dataset_path=dataset_path, read_only=True, embedding=OpenAIEmbeddings())

Nous ne sommes plus qu'à quelques lignes de parler avec notre ChatGPT "personnalisé" ! Nous devons déclarer un retriever et quelques paramètres : c'est un moment clé, car c'est ainsi que nous configurons les données qui seront envoyées à ChatGPT. En effet, nous ne voulons pas envoyer peut-être des dizaines de gigaoctets d'emails : nous voulons envoyer les emails plus pertinents pour donner à GPT un petit contexte de travail. Rappelez-vous : la fenêtre de contexte de ChatGPT est assez petite selon la version utilisée ! La pertinence des données envoyées est donc cruciale.

retriever = db.as_retriever()
retriever.search_kwargs['k'] = 20
retriever.search_kwargs['distance_metric'] = 'cos'

Ok, quelques explications doivent être faites ici. Tout d'abord, nous récupérons un retriever de notre base de données vectorielle. Cela nous permet de discuter avec notre data lake. Ensuite, nous spécifions comment rechercher les documents pertinents. Le premier argument, k, représente le nombre de documents que nous allons envoyer au maximum au LLM. Nous avons choisi d'envoyer les 20 documents les plus pertinents. Ce nombre peut être adapté pour donner plus de contexte pour, potentiellement, de meilleures réponses, ou moins de contexte pour minimiser les coûts des appels à l'API.

Le deuxième argument, distance_metric, définit comment nous définissons la pertinence d'un document. C'est ici que nous allons comprendre l'intérêt de vectoriser nos données comme vu précédemment. Nous avons choisi d'utiliser le cosinus de deux vecteurs. Pourquoi ? Rappelez-vous que tous nos documents sont stockés sous forme de vecteurs multidimensionnels. Le cosinus mesure l'angle entre deux vecteurs plutôt que leur magnitude absolue. Cela permet de quantifier la similarité directionnelle entre les vecteurs, indépendamment de leur échelle ou de leur magnitude.

Voici quelques raisons pour lesquelles le cosinus est une mesure de similarité pertinente dans l'espace vectoriel :

  • Invariant à l'échelle : cela signifie que la longueur des vecteurs n'altère pas la mesure de similarité. Par exemple, si vous multipliez un vecteur par un facteur constant, le cosinus de l'angle entre ce vecteur et un autre vecteur reste le même.
  • Focus sur la direction : le cosinus se concentre uniquement sur la direction des vecteurs, et non sur leur distance absolue dans l'espace. Cela signifie que les vecteurs pointant dans des directions similaires, même s'ils sont à des distances différentes de l'origine, auront des cosinus similaires.
  • Rapide : le calcul du cosinus est peu coûteux en termes de calcul, ce qui en fait un choix efficace pour les grandes bases de vecteurs.

Dans le cas des word embeddings, les cosinus sont utilisés pour mesurer la similarité sémantique entre les mots. Les mots similaires auront des vectors qui pointent dans des directions similaires, menant à des cosinus proches de 1. Plus le résultat est proche de 1, plus les mots sont similaires et ont donc de grandes chances d'être pertinents pour notre modèle de langage !

Maintenant que nous avons défini comment déterminer quelles informations doivent être envoyées au contexte du LLM, nous pouvons maintenant créer notre objet RetrievalQA qui va nous permettre de discuter avec notre LLM :

# GPT-4 est le meilleur mais aussi le plus cher, vous
# pouvez choisir un autre modèle en vous rendant sur https://platform.openai.com/docs/models
qa = RetrievalQA.from_chain_type(llm=ChatOpenAI(model_name='gpt-4'),
                                 chain_type='stuff',
                                 retriever=retriever)

while True:
    user_input = input('Ask something > ')

    if user_input == "":
        # arrêter le script si l'utilisateur appuie sur Entrée sans texte
        break

    print("\n" + qa.run(user_input) + "\n")

Avant de lancer cet exemple, vous pouvez aider ChatGPT à comprendre que tout le contexte que vous envoyez est en fait le contenu de votre boîte de réception. Par exemple, vous pouvez préfixer user_input avec quelque chose comme "Tout le contexte que je t'envoie est le contenu entier de mes emails. La réponse est forcément dans le contexte que je t'ai envoyé". Vous pouvez expérimenter quel prompt fonctionne le mieux dans votre cas.

Vous pouvez commencer par poser quelques questions basiques dont vous connaissez la réponse, comme "Quel est mon dernier email ?" ou "Quel est le sujet de mon dernier email ?". Vous pouvez ensuite poser des questions plus précises, comme "Quel est le sujet de mon dernier email de mon père ?" ou "Quel est le sujet de mon dernier email de mon père qui parle de la fête de Noël ?".

Voici un exemple du script en action :

[Ask anything]> Give me my latest stats of my Strava 10k September Challenge

############################################################
Based on the provided context, your latest stats for the Strava 10k September Challenge are as follows:

Distance: 15 km
Pace: 5:06 /km
Elapsed Time: 1:16:33
Elevation Gain: 95 m

Congratulations on completing the challenge! Keep up the great work!
############################################################

Bravo, vous avez un assistant (vraiment) personnel !

Utiliser un LLM local

Cette alternative est gratuite et vous permet de garder vos données sur votre ordinateur. Cependant, cela nécessite de grandes ressources de calcul. Nous allons voir comment utiliser un LLM local pour faire tourner notre assistant personnel.

Il n'y a finalement pas tant de code à changer que ça pour que le code soit compatible avec un autre LLM que ChatGPT. Pour cet exemple, nous allons voir comment refactoriser notre code pour utiliser Ollama. Cette bibliothèque permet de faire tourner localement de nombreux LLMs célèbres. Suivez les instructions sur la page de téléchargement puis lancez un des modèles, comme Llama2, en lançant la commande suivante :

ollama run llama2

Avant d'aller plus loin, attendez que le modèle soit entièrement téléchargé et vérifiez que tout fonctionne bien. Ensuite, il n'y a qu'une seule chose à mettre à jour dans notre fichier inbox_imap_fetcher.py : l'embedding que nous utilisons. Mettez à jour la ligne où nous importons OpenAIEmbedding et remplacez-la par celle-ci :

from langchain.embeddings.ollama import OllamaEmbeddings

Cette opération est très intense pour une machine et peut prendre beaucoup de temps selon votre matériel. Vous pouvez aussi utiliser d'autres embeddings, comme GPT4AllEmbeddings par exemple.

Ensuite, allons en bas de notre script et mettons à jour la ligne où nous stockons nos données dans DeepLake :

DeepLake.from_documents(texts, OllamaEmbeddings(), dataset_path='vector_gmail_ollama')

Ça y est, vous avez juste à lancer le script et attendre que les données soient stockées dans DeepLake avec le nouvel embedding !

Allons désormais du côté de notre script talk.py pour mettre à jour le LLM que nous utilisons. En fait, il n'y a encore une fois pas grand-chose à changer. Nous devons mettre à jour la ligne où nous chargeons les données de DeepLake pour utiliser OllamaEmbeddings :

# talk.py
from langchain.embeddings.ollama import OllamaEmbeddings

# ...

# mettre à jour le paramètre `embedding` si vous en utilisez un autre
db = DeepLake(dataset_path=dataset_path, read_only=True, embedding=OllamaEmbeddings())

Nous mettons aussi à jour le modèle que nous utilisons pour discuter :

from langchain.chat_models import ChatOllama

#...

qa = RetrievalQA.from_chain_type(llm=ChatOllama(model="llama2"), chain_type='stuff', retriever=retriever)

Vous êtes normalement désormais en mesure de lancer le script et de discuter avec votre assistant personnel local :

[Ask something]> Give me my latest stats of my Strava 10k September Challenge

############################################################
Sure, here are your latest statistics for the Strava 10k September Challenge:

- Distance: 10 km
- Allure: 5:06 /km
- Time elapsed: 1:16:33
- Elevation gain: 95 m
############################################################

Aller plus loin

Bien que nous faisions ceci avec des emails, vous pouvez utiliser ce code pour n'importe quel type de données. Vous pouvez par exemple utiliser ce code pour discuter avec un LLM une codebase sur laquelle vous travaillez chaque jour, ou même sur des données de votre entreprise. Tout ce qu'il y a à faire est de stocker vos données dans DeepLake et de les vectoriser avec un embedding adapté. Une fois que c'est fait, le code pour discuter avec le LLM ne change pas, si ce n'est le prompt initial donné au LLM.

Bon courage et amusez-vous bien à entraîner votre propre assistant vraiment personnel !

Commentaires

Il n'y a actuellement aucun commentaire. Soyez le premier !