Déconnecté mais jamais limité : optimiser votre application avec le mode hors ligne

Comment optimiser votre application avec le mode hors ligne ?

Le mode hors ligne
Hors ligne

Imaginez une application mobile révolutionnaire, une sorte de génie numérique qui réalise vos désirs les plus fous : elle vous prépare le café matinal, planifie votre journée, vous maintient connecté•e avec vos proches, effectue vos courses, et même – pourquoi pas – elle pourrait se rendre au travail à votre place lorsque la fatigue se fait trop sentir ! Après plusieurs mois d’utilisation, elle est devenue votre indispensable compagnon numérique.

Pourtant, un jour, sans crier gare, votre connexion internet vous abandonne, le wifi se met en grève et vous vous retrouvez coincé•e avec un réseau EDGE des plus chaotiques. La machine à café refuse d’obéir, votre agenda se perd dans les méandres du cyberespace, les conversations avec vos proches ont disparu et vos courses ne se commandent plus automatiquement. Un écran blanc s’affiche, suivit d’un chargement infini… bref, l’application cesse de répondre… Tout semble s’effondrer à cet instant précis. Une application si merveilleuse, si vitale, devient soudainement inutilisable à cause d’une simple coupure d’internet ?

Découvrons ensemble comment cette application prétendument « géniale » peut réellement le devenir, même lorsque la connexion internet flanche.

L’expérience utilisateur au coeur des applications

Lors de la création d’une application mobile, tout tourne autour de l’expérience vécue par les utilisateurs•rices. C’est ce qui guide chaque étape de réflexion. L’exemple précédemment évoqué est extrême, mais il illustre de manière saisissante les conséquences d’une défaillance du réseau. Cependant, des situations plus concrètes peuvent également mettre en lumière l’importance cruciale du mode hors ligne.

Imaginons une application où des employés•ées doivent badger un QR Code pour débuter des missions à des endroits spécifiques. Si ces QR Codes sont placés dans des zones de réseau instable, comme des zones géographiques mortes ou des bâtiments avec une mauvaise réception, l’utilisateur•rice se retrouvera confronté•e à la frustration de voir l’application, vantée par ses supérieurs, dysfonctionner.

Les connexions internet instables ne se limitent pas uniquement à des situations isolées. Elles peuvent provenir de divers facteurs, tels que des connexions limitées (dans des hôtels ou à l’étranger), des pannes de courant, des conditions météorologiques extrêmes, ou même une mauvaise couverture de la part du fournisseur de services.

Lorsque nous entreprenons la création d’une application mobile, la considération de la perte ou de la mauvaise qualité de connexion doit être au cœur de chaque fonctionnalité développée. Cela garantit une expérience optimale de l’application, même lorsque les conditions de connectivité sont difficiles.

Le mode hors ligne vous dites ?

Implémenter le mode hors ligne dans une application mobile permet à celle-ci de continuer à fonctionner et à offrir certaines fonctionnalités, même lorsque l’appareil n’a pas ou peu d’accès à Internet. Cependant, assurer une utilisation continue de l’application ne relève pas de la magie. Certaines fonctionnalités ne peuvent pas fonctionner sans connexion internet, mais il existe diverses façons de gérer la perte de connexion, comme nous l’illustrerons ci-dessous.

Par exemple, une messagerie en temps réel nécessite une connexion active pour fonctionner pleinement. Si ce sujet vous intéresse, nous avons un article ici : Le temps réel en web, quand chaque seconde compte ! Toutefois, même dans ces cas, des solutions alternatives peuvent être envisagées pour améliorer l’expérience utilisateur•rice malgré l’absence de connexion.

Comment améliorer concrètement l’expérience utilisateur•rice grâce au mode hors ligne ?

Prenons pour exemple une application de messagerie instantanée élémentaire. Dans cette application, le premier écran présente la liste des conversations (1), et dès qu’on sélectionne l’une d’elles, l’ensemble de la conversation s’affiche (2).

Image exemple 1
Image exemple 2

Si nous n’avions pas pris en compte les futurs utilisateur•rices, nous aurions supposé que tout fonctionnait parfaitement : le premier écran aurait simplement envoyé une requête via le réseau Internet pour récupérer mes conversations. Une fois la réponse obtenue, elle aurait été affichée dans l’application et voilà, affaire réglée. Cependant, le problème survient si, pour l’une des raisons évoquées précédemment, le réseau venait à disparaître. Dans cette situation, l’application continuerait à charger indéfiniment jusqu’à ce qu’un message d’erreur peu explicite apparaisse du genre “Network error”…

Voyons comment le mode hors ligne peut concrètement enrichir l’expérience utilisateur•rice dans ce contexte spécifique :

La notification visuel

La première étape consiste à informer l’utilisateur•rice que son expérience est altérée et pourquoi. Cela permet d’éviter les frustrations découlant de l’utilisation d’une application dans des conditions où elle ne peut fonctionner pleinement. Nombreuses sont les applications qui ne peuvent pas être utilisées à leur plein potentiel hors connexion. C’est le cas de YouTube, qui bloque la lecture des vidéos, ou de Messenger, qui affiche un bandeau en haut de l’application pour signaler l’absence de connexion. Pour autant, personne n’a l’impression que l’application “bug”.

En ce qui concerne notre application de messagerie, concentrons-nous sur la première interface : les conversations sont stockées dans une base de données, mais je ne peux pas accéder à cette base sans connexion Internet. Ainsi, je pourrais envisager d’afficher une erreur claire et précise, à la manière de YouTube, décrivant explicitement le problème rencontré :

image5

Il est crucial d’employer des termes appropriés, simples et compréhensibles, sans recourir à un langage technique, pour garantir une explication claire, directe et précise.

Ne pas bloquer l’utilisateur

Dans le contexte de notre application, la notification précédente bloque entièrement l’accès aux discussions (sans affecter le reste de l’application). Cependant, une meilleure approche permettrait d’offrir une notification moins intrusive et agressive, tout en donnant à l’utilisateur•rice la possibilité d’accéder à quelques fonctionnalités supplémentaires comme pouvoir visualiser des conversations.

image4
image2
image10

Dans le cas d’une connexion instable, il est préférable d’utiliser une notification visuelle impactante mais non intrusive lorsque la connectivité réseau fluctue plutôt qu’un effet stroboscopique entre l’écran chargé et l’écran d’erreur.

De plus, lors du chargement des données, il est préférable d’éviter les chargements en plein écran qui immobilisent l’utilisateur•rice. À la place, on privilégie les “écrans squelettes” (skeleton loader), qui représentent des espaces (souvent animés) réservés pour les informations en cours de chargement. Un écran squelette imite la structure et l’apparence de la vue complète, donnant ainsi un aperçu visuel du contenu en attente de chargement dans chaque bloc, que ce soit une image, du texte ou tout autre type d’information.

image7
image11

Il est toujours possible d’offrir une façon originale de notifier l’utilisateur que l’application passe en mode hors ligne en offrant un nouveau visuel à l’application. Par exemple passer l’application en noir et blanc :

image3
image9
image1

Le stockage

Pour résoudre la problématique précédente, une solution consisterait à stocker localement  les 10 derniers messages des 10 dernières conversations sur le téléphone. Ainsi, même en cas de connexion faible ou inexistante, l’utilisateur•rice ne serait pas bloqué•e.

Le stockage est une solution efficace pour améliorer l’expérience utilisateur en cas de perte de connexion. Dans notre application, elle permettrait aux utilisateurs•rices de voir les derniers messages d’une conversation comme par exemple ou se situe l’adresse du restaurant où ils ont rendez-vous même si le réseau les a abandonnés.

Cette approche assure une utilisation continue et pratique de l’application, malgré les éventuelles perturbations de la connectivité.

Soumission différée

Une autre façon d’améliorer l’expérience utilisateur est de lui offrir la possibilité d’écrire et d’envoyer un message même en l’absence de connexion. L’utilisateur•rice peut accéder à une conversation, y rédiger un message et l’envoyer. Une notification visuelle claire indiquera que ce message sera transmis dès que le réseau sera rétabli. Cette fonctionnalité garantit une continuité d’utilisation de l’application sans interruption, même lorsque la connexion est instable.

image8

C’est tout ?

Evidemment que non ! En effet, de nombreuses méthodes permettent d’améliorer considérablement une application mobile même lorsque la connexion réseau n’est pas optimale. Nous avons exploré les principaux éléments à prendre en compte lors de la conception d’une application de qualité autour d’un exemple des plus simple. Toutefois, les solutions sont variées et l’innovation persiste à découvrir des techniques avancées pour enrichir davantage l’expérience utilisateur•rice.

Il faut garder en tête que, faire simple et comprendre ce que l’utilisateur•rice va utiliser ou pas est le plus important. Nous pourrions prendre l’exemple d’applications plus complexes comme Google Docs où il est possible de collaborer sur un document en hors connexion et où la logique de fusion des versions à été faite de la plus simple des manières. En effet, le cas d’utilisation reste des moins fréquent, les équipes de Google ont donc opté pour une solution “simple” qui est de fusionner l’ensemble du texte modifié par les auteurs•rices au risque que le texte devienne incompréhensible. Il suffira ensuite de reformuler l’ensemble. Cela évite les pertes de contenu, simplifie le code source et sa compréhension et optimise le temps de développement pour répondre à de vraies problématiques utilisateurs•rices.

Les possibilités sont vastes, mais chaque innovation requiert une exigence, une expertise technique et une connaissance des besoins utilisateur•rice. Chez TheCodingMachine, nous aimons relever ces défis et nous y confrontons avec passion et expertise !

Que peut-on en conclure ?

Le mode hors ligne représente un défi technique avec ses petits challenges si l’on souhaite innover. Cependant, arriver à produire une application de qualité qui répond aux exigences actuelles des consommateurs•rices d’application mobile n’est pas difficile tant que ces derniers•ères sont placés•ées au cœur de la conception.

Auteur : Jérémy Dollé

Langchain : Générer des requêtes SQL depuis une demande en langage naturel

Introduction

Lors d’un échange avec l’un de nos clients, la question de l’automatisation des requêtes SQL est rapidement devenue un sujet de discussion. Notre client nous a fait part du gain de temps potentiel en adoptant une approche où les requêtes SQL pourraient être générées à partir de questions formulées en langage naturel. Intrigués par cette perspective, nous avons décidé d’explorer cette piste.

Afin d’utiliser des LLM (Grand modèle de langage) tout en ayant une structure modulaire et évolutive, notre choix s’est naturellement porté sur le framework Langchain qui semble être l’étoile montante des frameworks IA.

Langchain est une bibliothèque qui utilise le traitement du langage naturel (NLP) qui permet en partie de convertir des phrases en requêtes SQL. Cette approche promet de simplifier l’accès aux données pour les utilisateurs non techniques, tout en améliorant l’efficacité des processus d’analyse de données.

Chez The Coding Machine, nous avons exploré différentes options afin d’automatiser et simplifier nos workflows liés à la gestion des données, notamment dans le cas de multiples statistiques sur une base de données client. Après avoir examiné les avantages potentiels de Langchain, nous avons décidé de mettre ce framework à l’épreuve en utilisant la base de données open source Chinook. Cet article a pour ambition de vous guider à travers notre expérience, en détaillant comment nous avons configuré notre environnement de travail avec Google Colab et Jupyter Notebook.

Notre objectif est de partager cette expérience afin que d’autres puissent tirer parti de ces outils pour améliorer leurs propres processus d’analyse et de gestion des données. Que vous soyez analyste de données, ingénieur ou simplement curieux des nouvelles technologies, cet article vous fournira des outils pratiques pour optimiser votre utilisation des bases de données.

Pourquoi utiliser LangChain ?

Pour répondre à cette question, je vous invite à aller lire l’article d’introduction qui traite de la génération de requêtes depuis un modèle d’intelligence artificielle.

Présentation de la Base de Données Chinook

1. Qu’est-ce que Chinook_DB ?

Chinook_DB est une base de données open source largement utilisée dans l’enseignement et les exemples pratiques de gestion de bases de données relationnelles. Initialement conçue pour représenter un magasin de musique en ligne, Chinook_DB offre une structure de données réaliste et plus ou moins complexe, idéale pour simuler des scénarios réels d’interrogation et d’analyse de données.

Cette base de données comprend plusieurs tables interconnectées représentant divers aspects d’une entreprise de vente de musique. Voici quelques-unes des principales tables et leurs relations :

  • Albums : Contient des informations sur les albums disponibles à la vente.
  • Artistes : Liste des artistes associés à chaque album.
  • Clients : Informations sur les clients qui ont acheté des albums.
  • Commandes : Détails de chaque commande effectuée par les clients.
  • Pistes audio : Liste des pistes audio disponibles à la vente, avec des informations telles que le nom, la durée et l’album auquel elles appartiennent.
  • Employés : Détails sur les employés de l’entreprise.

Chinook_DB est structurée de manière à refléter les relations typiques entre les entités dans une plateforme e-commerce de musique.

2. Utilité de Chinook_DB

Chinook_DB est particulièrement utile pour les démonstrations et les apprentissages pratiques dans le domaine de la gestion de bases de données relationnelles. En raison de sa structure bien définie et de ses données réalistes, Chinook_DB permet aux utilisateurs d’explorer des requêtes SQL et de simuler divers scénarios d’analyse de données. Dans le cadre de notre exploration de Langchain et la génération de requêtes SQL, Chinook_DB offre un environnement idéal pour illustrer l’efficacité et la précision de Langchain dans la transformation des questions en langage naturel en requêtes SQL fonctionnelles.

3. Modèle de données

Langchain - modèle de données Chinook

Environnement de développement

La mise en place d’un environnement de développement est important afin d’explorer efficacement les capacités de Langchain dans la génération de requêtes SQL à partir de langage naturel. Dans cette partie, nous détaillerons les étapes pour configurer votre environnement de travail avec Google Colab et Jupyter Notebook, deux plateformes populaires pour le développement interactif en Python.

1. Choix de l’environnement

Google Colab (cloud) : Google Colab est un service de notebook Jupyter hébergé par Google qui permet d’écrire et d’exécuter du code Python dans le cloud, sans nécessiter de configuration locale. Il offre un accès gratuit à des processeurs et de la RAM pour des calculs intensifs, ce qui peut être avantageux pour le traitement de données volumineuses.

Jupyter Notebook (local) : Jupyter Notebook est une application web open source qui vous permet de créer et de partager des documents contenant du code, des équations, des visualisations et du texte explicatif. Il est idéal pour le développement interactif et l’analyse de données.

En optant pour Google Colab et Jupyter Notebook, nous avons choisi des outils qui non seulement facilitent le développement et le débogage, mais qui améliorent aussi la documentation, la collaboration. Ces plateformes offrent des avantages significatifs par rapport à l’utilisation de fichiers Python séparés, rendant le processus de test et de mise en œuvre de Langchain plus efficace et plus convivial.

2. Installation des outils

Pour commencer, assurez-vous d’avoir un compte Google pour utiliser Google Colab et d’avoir installé Python sur votre machine pour Jupyter Notebook. Voici les étapes générales pour configurer votre environnement :

Google Colab

1. Ouvrez un navigateur web et accédez à Google Colab
2. Créez un nouveau notebook ou importez un notebook existant depuis Google Drive ou GitHub

Avantages :

  • Python est complètement disponible en dernière version et il est très simple d’installer des librairies (“pip install langchain” par exemple)
  • Pas besoin d’un environnement local
  • Partage et collaboration optimale

Jupyter Notebook

1. Installez Jupyter Notebook en utilisant pip si ce n’est pas déjà fait (Les frameworks populaire tel que JetBrains ou Visual Studio ont des modules pour Jupyter)
2. Jupyter pourra alors être complètement intégré à votre IDE

Avantages :

  • Pas de dépendance au cloud
  • Moins de déconnexion intempestive
  • Un environnement local plus souple et maîtrisable

Pratique : Mise en place technique

Dans cette partie, nous allons voir comment utiliser Langchain concrètement étape par étape. L’objectif de cet article est d’arriver à un résultat où nous pourrons poser une question naturelle et le modèle devra nous répondre une requête SQL fonctionnelle puis l’exécuter directement afin d’avoir le résultat.

Notez que nous allons aborder une infime partie de ce que propose Langchain cependant nous ne pouvons pas couvrir la totalité dans cet article, si ce framework vous intéresse, rendez-vous sur la documentation officielle.

⚠️ Attention ⚠️
Comme le précise Langchain, connecter une base de données de production à un LLM n’est pas sans risque, des instructions DML pourraient sans doute être exécutées par une personne malveillante. Il est donc très important de mettre en place une stratégie de sécurité :
– Dupliquer la base de données avec une lecture seule (Master / slave par exemple)
– Anonymiser les informations critiques tels que les users (Données personnelles, mots de passes, etc.) ou les API key

Avant d’aller plus loin, voici les éléments que nous allons aborder :

1. L’utilisation du Toolkit SQL Database

Un toolkit est un ensemble de tools qui permettent d’exécuter un groupe de fonction mis à disposition. Dans notre exemple, le Toolkit SQL Database va nous permettre d’avoir un ensemble de fonctions pré paramétré et notamment avec des prompts déjà mis en place.

2. L’utilisation d’un agent

L’agent, dans notre contexte, est un module proposé par Langchain qui agit comme un intermédiaire entre l’utilisateur et la base de données. Il est capable de comprendre des requêtes en langage naturel, de les traduire en requêtes SQL en suivant les fonctions mise à disposition par le Toolkit.

3. L’utilisation de LangSmith

LangSmith va nous permettre d’avoir une visibilité sur l’utilisation de Langchain, nombres de tokens par requête, coûts, organisation des fonctions, input, output, etc.
Cela permet de déboguer plus facilement du code.

Pour la suite de cet article nous allons utiliser Google Colab, ce qui permettra de vous partager le Notebook de cet article

Installation des paquets

Cette ligne installera tous les paquets nécessaires au code que nous allons utiliser.

!pip install langchain langchain-openai langchain_experimental

Note : Le ! devant pip dans Google Colab est utilisé pour exécuter des commandes de shell directement depuis une cellule du notebook. En mettant !pip, vous dites à Google Colab d’exécuter la commande pip

Variable d’environnement

Google Colab met à disposition un gestionnaire de clés, vous pouvez donc renseigner de façon sécurisée vos secrets et les appeler directement dans le code.

# Getting env variables from Google Colab secrets
from google.colab import userdata
import os

# OpenAI key 
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY')
# LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = userdata.get('LANGCHAIN_API_KEY')

Initialisation de la base de données

Nous allons vérifier que la base de données existe car Colab supprime très régulièrement les données. Si vous souhaitez persister vos données, regardez l’intégration Colab de Google Drive.

from langchain.sql_database import SQLDatabase

db_path = "[path]/chinook_data.db"
if not os.path.exists(db_path):
    raise FileNotFoundError("Database file not found.")

db = SQLDatabase.from_uri(f"sqlite:///{db_path}")

Nous allons maintenant instancier notre classe Chat model afin de traiter la demande de l’utilisateur. LangChain va maintenant nous permettre de créer un agent, celui-ci pourra appeler à de multiples reprises le chat model ainsi que la base de données.

from langchain_openai import ChatOpenAI
from langchain_community.agent_toolkits import create_sql_agent

# Instanciate the class ChatOpenAI
chat_model = ChatOpenAI(model_name='gpt-4o', temperature=0.5)

# Create a new sql agent
agent_executor = create_sql_agent(model, db=db, agent_type="openai-tools", verbose=True)

# Run the user query
agent_executor.invoke("How much artists are in the db ?")

Nous sommes maintenant prêts à réaliser quelques tests. Le paramètre “verbose” va nous permettre de consulter le chemin parcouru par notre Toolkit pour arriver à un résultat. Notez qu’il est également possible (et recommandé) de consulter ce parcours sur LangSmith.

Nous allons prendre pour exemple un cas simple pour l’instant, nous voulons savoir combien d’artistes sont présents dans la base de données.

Notre question sera donc : How many artists are in the db ?

Note : Les tests semblent montrer que les requêtes peuvent être écrites en Français mais nous le ferons en anglais dans cet article

La question est simple, si nous reprenons notre modèle de données, il n’y aucune relation, voici donc la requête :

SELECT COUNT(*) FROM artists;

Lançons maintenant le script :

> Entering new SQL Agent Executor chain...

Invoking: `sql_db_list_tables` with `{}`


albums, artists, customers, employees, genres, invoice_items, invoices, media_types, playlist_track, playlists, tracks

Invoking: `sql_db_schema` with `{'table_names': 'artists'}`

CREATE TABLE artists (
	"ArtistId" INTEGER NOT NULL, 
	"Name" NVARCHAR(120), 
	PRIMARY KEY ("ArtistId")
)

/*
3 rows from artists table:
ArtistId	Name
1	AC/DC
2	Accept
3	Aerosmith
*/

Invoking: `sql_db_query_checker` with `{'query': 'SELECT COUNT(*) as artist_count FROM artists;'}`


```sql
SELECT COUNT(*) as artist_count FROM artists;
```

Invoking: `sql_db_query` with `{'query': 'SELECT COUNT(*) as artist_count FROM artists;'}`


[(275,)]There are 275 artists in the database.

> Finished chain.
{
  'input': 'How much artists are in the db ?',
  'output': 'There are 275 artists in the database.'
}

Le résultat est cohérent et celui attendu. Nous pouvons voir les différentes fonctions utilisées par notre toolkit. Pour aller plus loin, allons jeter un oeil sur LangSmith.

Capture décran 2024 06 27 à 14.38.39

Sans rentrer dans les détails, cela fera peut être l’objet d’un article sur ce sujet, mais nous pouvons constater que 4 différentes fonctions avec des tâches bien précises sont appelées.

1. sql_db_query

Cette fonction a pour objectif de recevoir une requête SQL correcte (syntaxiquement parlant) et de l’exécuter. Dès lors que l’exécution de celle-ci renvoie une erreur, le prompt précise qu’il doit essayer de réécrire la requête.

Si un problème de “Unknown column” est présent, alors il faut rappeler la fonction “sql_db_schema” afin de vérifier le schéma.

Dans le cas où l’exécution de la requête est un problème de syntaxe SQL, alors il rappelle “sql_db_query_checker” et tente de réécrire la requête.

2. sql_db_schema

Cette fonction a pour objectif de recevoir en entrée une liste de tables séparées par des virgules. La sortie est le schéma et les lignes d’exemples des tables de l’entrée.

Une vérification peut être faite afin de vérifier que les tables existent en appelant “sql_db_list_tables”

3. sql_db_list_tables

Renvoie toutes les tables de la base de données.

4. sql_db_query_checker

Cette fonction est appelée avant l’exécution de la requête SQL afin de vérifier sa syntaxe.

Note : Il faut bien comprendre ici que les Tools (ou fonctions) sont appelées par le LLM. Il est capable de comprendre une question et/ou un problème et donc d’appeler un tool en fonction du problème rencontré.

Allons un peu plus loin maintenant, si nous analysons notre modèle de données, l’idéal serait d’avoir une demande qui recherche dans plus de trois tables différentes pour ajouter un peu de complexité.

Imaginons que nous souhaitions connaître les clients les plus éclectiques, cherchons ceux qui ont acheté des pistes dans plus de 9 genres musicaux différents et calculons combien ils ont dépensé au total. Idéalement, nous souhaiterions avoir les clients avec le plus de genres affichés en premier.

Voici le prompt : Identify customers who purchased tracks from more than nine different genres, calculate the total amount spent by these customers and sort the result by highest number of genre

Voici l’output de l’agent : 

> Entering new SQL Agent Executor chain...

Invoking: `sql_db_list_tables` with `{}`


albums, artists, customers, employees, genres, invoice_items, invoices, media_types, playlist_track, playlists, tracks
Invoking: `sql_db_schema` with `{'table_names': 'customers, genres, invoice_items, invoices, tracks'}`



CREATE TABLE customers (
	"CustomerId" INTEGER NOT NULL, 
	"FirstName" NVARCHAR(40) NOT NULL, 
	"LastName" NVARCHAR(20) NOT NULL, 
	"Company" NVARCHAR(80), 
	"Address" NVARCHAR(70), 
	"City" NVARCHAR(40), 
	"State" NVARCHAR(40), 
	"Country" NVARCHAR(40), 
	"PostalCode" NVARCHAR(10), 
	"Phone" NVARCHAR(24), 
	"Fax" NVARCHAR(24), 
	"Email" NVARCHAR(60) NOT NULL, 
	"SupportRepId" INTEGER, 
	PRIMARY KEY ("CustomerId"), 
	FOREIGN KEY("SupportRepId") REFERENCES employees ("EmployeeId")
)

/*
3 rows from customers table:
CustomerId	FirstName	LastName	Company	Address	City	State	Country	PostalCode	Phone	Fax	Email	SupportRepId
1	Luís	Gonçalves	Embraer - Empresa Brasileira de Aeronáutica S.A.	Av. Brigadeiro Faria Lima, 2170	São José dos Campos	SP	Brazil	12227-000	+55 (12) 3923-5555	+55 (12) 3923-5566	luisg@embraer.com.br	3
2	Leonie	Köhler	None	Theodor-Heuss-Straße 34	Stuttgart	None	Germany	70174	+49 0711 2842222	None	leonekohler@surfeu.de	5
3	François	Tremblay	None	1498 rue Bélanger	Montréal	QC	Canada	H2G 1A7	+1 (514) 721-4711	None	ftremblay@gmail.com	3
*/


CREATE TABLE genres (
	"GenreId" INTEGER NOT NULL, 
	"Name" NVARCHAR(120), 
	PRIMARY KEY ("GenreId")
)

/*
3 rows from genres table:
GenreId	Name
1	Rock
2	Jazz
3	Metal
*/


CREATE TABLE invoice_items (
	"InvoiceLineId" INTEGER NOT NULL, 
	"InvoiceId" INTEGER NOT NULL, 
	"TrackId" INTEGER NOT NULL, 
	"UnitPrice" NUMERIC(10, 2) NOT NULL, 
	"Quantity" INTEGER NOT NULL, 
	PRIMARY KEY ("InvoiceLineId"), 
	FOREIGN KEY("TrackId") REFERENCES tracks ("TrackId"), 
	FOREIGN KEY("InvoiceId") REFERENCES invoices ("InvoiceId")
)

/*
3 rows from invoice_items table:
InvoiceLineId	InvoiceId	TrackId	UnitPrice	Quantity
1	1	2	0.99	1
2	1	4	0.99	1
3	2	6	0.99	1
*/


CREATE TABLE invoices (
	"InvoiceId" INTEGER NOT NULL, 
	"CustomerId" INTEGER NOT NULL, 
	"InvoiceDate" DATETIME NOT NULL, 
	"BillingAddress" NVARCHAR(70), 
	"BillingCity" NVARCHAR(40), 
	"BillingState" NVARCHAR(40), 
	"BillingCountry" NVARCHAR(40), 
	"BillingPostalCode" NVARCHAR(10), 
	"Total" NUMERIC(10, 2) NOT NULL, 
	PRIMARY KEY ("InvoiceId"), 
	FOREIGN KEY("CustomerId") REFERENCES customers ("CustomerId")
)

/*
3 rows from invoices table:
InvoiceId	CustomerId	InvoiceDate	BillingAddress	BillingCity	BillingState	BillingCountry	BillingPostalCode	Total
1	2	2009-01-01 00:00:00	Theodor-Heuss-Straße 34	Stuttgart	None	Germany	70174	1.98
2	4	2009-01-02 00:00:00	Ullevålsveien 14	Oslo	None	Norway	0171	3.96
3	8	2009-01-03 00:00:00	Grétrystraat 63	Brussels	None	Belgium	1000	5.94
*/


CREATE TABLE tracks (
	"TrackId" INTEGER NOT NULL, 
	"Name" NVARCHAR(200) NOT NULL, 
	"AlbumId" INTEGER, 
	"MediaTypeId" INTEGER NOT NULL, 
	"GenreId" INTEGER, 
	"Composer" NVARCHAR(220), 
	"Milliseconds" INTEGER NOT NULL, 
	"Bytes" INTEGER, 
	"UnitPrice" NUMERIC(10, 2) NOT NULL, 
	PRIMARY KEY ("TrackId"), 
	FOREIGN KEY("MediaTypeId") REFERENCES media_types ("MediaTypeId"), 
	FOREIGN KEY("GenreId") REFERENCES genres ("GenreId"), 
	FOREIGN KEY("AlbumId") REFERENCES albums ("AlbumId")
)

/*
3 rows from tracks table:
TrackId	Name	AlbumId	MediaTypeId	GenreId	Composer	Milliseconds	Bytes	UnitPrice
1	For Those About To Rock (We Salute You)	1	1	1	Angus Young, Malcolm Young, Brian Johnson	343719	11170334	0.99
2	Balls to the Wall	2	2	1	None	342562	5510424	0.99
3	Fast As a Shark	3	2	1	F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman	230619	3990994	0.99
*/
Invoking: `sql_db_query_checker` with `{'query': 'SELECT c.CustomerId, c.FirstName, c.LastName, COUNT(DISTINCT t.GenreId) AS GenreCount, SUM(i.Total) AS TotalSpent\nFROM customers c\nJOIN invoices i ON c.CustomerId = i.CustomerId\nJOIN invoice_items ii ON i.InvoiceId = ii.InvoiceId\nJOIN tracks t ON ii.TrackId = t.TrackId\nGROUP BY c.CustomerId, c.FirstName, c.LastName\nHAVING GenreCount > 9\nORDER BY GenreCount DESC\nLIMIT 10;'}`

responded: To identify customers who purchased tracks from more than nine different genres and calculate the total amount spent by these customers, I need to join several tables and perform the necessary aggregations.

Here's the plan:
1. Join the `customers`, `invoices`, `invoice_items`, and `tracks` tables to get the necessary data.
2. Group by `CustomerId` and count the distinct `GenreId` for each customer.
3. Filter out customers who purchased from more than nine different genres.
4. Calculate the total amount spent by these customers.
5. Sort the results by the highest number of genres.

Let's write and check the query.

```sql
SELECT c.CustomerId, c.FirstName, c.LastName, COUNT(DISTINCT t.GenreId) AS GenreCount, SUM(i.Total) AS TotalSpent
FROM customers c
JOIN invoices i ON c.CustomerId = i.CustomerId
JOIN invoice_items ii ON i.InvoiceId = ii.InvoiceId
JOIN tracks t ON ii.TrackId = t.TrackId
GROUP BY c.CustomerId, c.FirstName, c.LastName
HAVING GenreCount > 9
ORDER BY GenreCount DESC
LIMIT 10;
```
Invoking: `sql_db_query` with `{'query': 'SELECT c.CustomerId, c.FirstName, c.LastName, COUNT(DISTINCT t.GenreId) AS GenreCount, SUM(i.Total) AS TotalSpent\nFROM customers c\nJOIN invoices i ON c.CustomerId = i.CustomerId\nJOIN invoice_items ii ON i.InvoiceId = ii.InvoiceId\nJOIN tracks t ON ii.TrackId = t.TrackId\nGROUP BY c.CustomerId, c.FirstName, c.LastName\nHAVING GenreCount > 9\nORDER BY GenreCount DESC\nLIMIT 10;'}`


[(57, 'Luis', 'Rojas', 12, 415.6200000000001), (45, 'Ladislav', 'Kovács', 11, 446.6200000000003), (3, 'François', 'Tremblay', 10, 338.62000000000006), (14, 'Mark', 'Philips', 10, 334.6200000000001), (17, 'Jack', 'Smith', 10, 352.62000000000035), (24, 'Frank', 'Ralston', 10, 378.6200000000001), (34, 'João', 'Fernandes', 10, 352.62000000000035), (37, 'Fynn', 'Zimmermann', 10, 388.6200000000001)]Here are the customers who purchased tracks from more than nine different genres, along with the total amount they spent, sorted by the highest number of genres:

1. **Luis Rojas** - Purchased from 12 genres, Total Spent: $415.62
2. **Ladislav Kovács** - Purchased from 11 genres, Total Spent: $446.62
3. **François Tremblay** - Purchased from 10 genres, Total Spent: $338.62
4. **Mark Philips** - Purchased from 10 genres, Total Spent: $334.62
5. **Jack Smith** - Purchased from 10 genres, Total Spent: $352.62
6. **Frank Ralston** - Purchased from 10 genres, Total Spent: $378.62
7. **João Fernandes** - Purchased from 10 genres, Total Spent: $352.62
8. **Fynn Zimmermann** - Purchased from 10 genres, Total Spent: $388.62

Ce cas est très intéressant, car la réponse n’est pas bonne.

Notez qu’il y a une subtilité, dans cette base de données les informations financières sont gérées dans deux tables différentes, invoices et invoice_items.

Capture décran 2024 07 08 à 09.57.09

Invoice_items correspond à l’achat d’une piste. Si un client achète 2 pistes, nous aurons alors 2 lignes liées à invoices.

Pour illustrer cet exemple, prenons un cas concret de la base de données, la facture id 1 a deux lignes rattachées donc un client a acheté 2 pistes pour un prix unitaire de 0,99 €. Nous avons donc 1,98 € dans la colonne du total de la ligne invoices.

Capture décran 2024 07 08 à 10.02.37
Capture décran 2024 07 08 à 10.02.48

Lorsque nous réalisons une requête, si nous ne regroupons pas les résultats (GROUP BY), nous aurons alors les lignes invoices dupliquées autant de fois qu’il y a de ligne invoice_items rattachées. Utiliser alors la fonction SUM ne donnera pas le bon résultat, ce qui est notre problème actuellement.

Si j’exécute cette requête nous pouvons constater ce résultat

SELECT i.* FROM invoices i
LEFT JOIN invoice_items ii ON i.InvoiceId=ii.InvoiceId
WHERE i.InvoiceId=1
Capture décran 2024 07 08 à 10.05.56

Faire un SUM sur Total n’est pas absolument pas la bonne solution.

Si nous reprenons le résultat de notre requête SQL nous pouvons constater que Luis Rojas a dépensé 415,62 €. Ce montant provient de cette fonction SUM(i.Total) AS TotalSpent

Le fait d’avoir écrit the total amount spent by, le LLM se base donc sur la colonne total.

Nous ne pouvons pas maîtriser parfaitement l’interprétation du LLM et c’est d’ailleurs tout son intérêt, nous avons donc trouvé une solution afin de corriger ce petit problème.

L’agent utilisé est “create_sql_agent » et il prend en paramètre un suffixe. Nous pouvons donc créer un prompt complémentaire qui ajoute des instructions spécifiques.

Nous pouvons ajouter le code suivant :

[...]

suffix = """
  Here is some additionnal information about the database, 
  there are two tables that deal with the subject of financial amount. 
  The 'Invoices' table and the 'Invoice_items' table. You must always 
  rely on the 'quantity' and 'UnitPrice' columns in order to make 
  calculations relating to a financial aspect. You should NEVER 
  use the 'Total' column of the 'Invoices' table. Use this information 
  only when necessary.
"""

agent_executor = create_sql_agent(model, db=db, agent_type="openai-tools", verbose=True, suffix=suffix)

[...]

Nous précisons au LLM la subtilité de cette base en ce qui concerne l’aspect financier de la demande. A présent, dès lors que nous demanderons au LLM une information financière, il utilisera cette fonction SUM(ii.UnitPrice * ii.Quantity)

Après avoir relancé le même prompt avec ce changement, voici notre résultat, qui lui est correct !

1. **Luis Rojas** - Purchased from 12 genres, Total Amount Spent: $46.62
2. **Ladislav Kovács** - Purchased from 11 genres, Total Amount Spent: $45.62
3. **François Tremblay** - Purchased from 10 genres, Total Amount Spent: $39.62
4. **Mark Philips** - Purchased from 10 genres, Total Amount Spent: $37.62
5. **Jack Smith** - Purchased from 10 genres, Total Amount Spent: $39.62
6. **Frank Ralston** - Purchased from 10 genres, Total Amount Spent: $43.62
7. **João Fernandes** - Purchased from 10 genres, Total Amount Spent: $39.62
8. **Fynn Zimmermann** - Purchased from 10 genres, Total Amount Spent: $43.62

Conclusion

Les résultats sont plutôt bons, les requêtes sont construites de manière cohérente. Cependant, il faut prendre en compte le nom des tables ainsi que des colonnes de la base de données.

En effet, le modèle de langage interprète les demandes et recherche des cohérences dans le nom des tables et des colonnes. Si les tables sont mal nommées, cela ne fonctionnera pas.

Modifier ces éléments dans une base de données existante peut se révéler assez complexe. Comme vu précédemment, il est donc possible d’ajouter du contexte au LLM afin de créer une logique entre les tables et les colonnes.

Il devient donc très intéressant de travailler par itération sur une base de données existante et les résultats pourront être satisfaisants.

Merci d’avoir suivi cet article, afin de tester par vos propres moyens, vous trouverez le lien ci-dessous afin de tester par vous même.

Google Colab link