TP 6 - Introduction à l'utilisation de l'API d'OpenAI (intelligence artificielle)

Retour TPs

Ce TP est optionnel. Il vous donnera une illustration pratique en Java (avec Jakarta EE) du cours que vous avez suivi sur l'IA.

N'oubliez pas d'utiliser Git et GitHub.

Introduction

Dans ce TP vous allez écrire une petite application qui va ressembler à ChatGPT, en utilisant l'API d'OpenAI, le créateur de ChatGPT.

Il n'existe pas à ce jour de librairie Java officielle pour faciliter l'utilisation de l'API d'OpenAI. L'application que vous allez écrire utilisera donc directement un client REST de Jakarta EE pour envoyer des requêtes aux endpoints REST de l'API OpenAI, avec des documents JSON explicitement construits dans l'application. Vous verrez le format des documents JSON utilisé par l'API et comment écrire un client REST, et manipuler des documents JSON avec Jakarta EE. Vous apprendrez aussi quelques compléments sur JSF.

Après avoir écrit cette application en utilisant directement l'API OpenAI, vous la récrirez avec l'aide de la librairie LangChain4j qui permet un plus haut niveau d'abstraction et qui facilite les échanges avec les LMs en général (pas seulement les modèles d'OpenAI).

Si vous avez de l'imagination, vous pourrez tirer le plus grand parti de ce que vous aurez appris. Par exemple en écrivant une application d'aide à l'apprentissage de langues étrangères.

Ce TP est destiné à ceux qui seraient intéressés par l'utilisation des APIs d'intelligence artificielle dans leurs application.

Un bonus pour la note finale sera attribué à ceux qui termineront le TP, et qui m'expliqueront comment ils pourraient écrire un projet Jakarta EE qui utilise l'API d'OpenAI, en s'inspirant de ce TP.

Ressources

OpenAI : https://openai.com
API de OpenAI : https://platform.openai.com/
Documentation sur l'API : https://platform.openai.com/docs/api-reference/chat
Guide pour utiliser l'API de complétion : https://platform.openai.com/docs/guides/text-generation/chat-completions-api

Récupérer une clé secrète pour l'API d'OpenAI

C'est la première étape indispensable.

Pour cela il faut

  1. Aller sur le le site d'OpenAI https://platform.openai.com/ et cliquez en haut à droite sur "Sign up".
  2. Quand vous avez un compte, retournez sur le site d'OpenAI pour entrer sous votre compte (Log in en haut à droite). Cliquez à gauche sur "API keys". Vous arrivez sur https://platform.openai.com/api-keys.
  3. Dans la nouvelle page, clic sur « + Create new secret key ».
  4. Donnez un nom à la clé secrète et clic sur "Create secret key". Copiez tout de suite la clé dans un endroit protégé de votre ordinateur. Cliquez sur "Done".

Cette clé secrète devra être intégrée à toutes les requêtes que vous enverrez à l'API d'OpenAI.

Pour éviter de mettre cette clé secrète dans votre code Java, vous allez la mettre comme valeur de la variable d'environnement CHATGPT_KEY (vous pouvez choisir un autre nom mais il faudra alors changer le code Java) de votre système d'exploitation. Le code Java récupèrera la valeur de cette variable. Pour Windows, allez dans le panneau de configuration de Windows, clic sur "Système et sécurité" > Système > Liens connexes "Paramètres avancés du système" > "Variables d'environnement..." et, dans les variables utilisateur pour vous, ajoutez la nouvelle variable d'environnement CHATGPT_KEY avec la valeur de clé fournie par OpenAI.

Coûts

L'utilisation de cette API est payante mais vous paierez seulement ce que vous utilisez (pay-as-you-go) et vous avez un crédit de 5 $ à dépenser en 3 mois, ce qui devrait suffire pour faire beaucoup de tests car les coûts ne sont pas élevés. La page des coûts est https://openai.com/pricing. Puisque vous allez faire uniquement des tests, choisissez le modèle GPT-3.5-turbo pour lequel le prix pour l'échange de 1000 tokens (environ 170 mots en moyenne pour le français et l'anglais) entre votre application et l'API coûte seulement 0.0005 $ en input et 0.0015 $ en output. Donc, si vous échangez environ un million de mots entre l'utilisateur et l'API (total des mots envoyés dans les questions et reçus dans les réponses), il vous en coûtera un peu plus de 5 €. Pour tester, posez des questions simples et courtes qui génèreront des réponses simples et courtes. Si vous voulez faire une réelle conversation avec l'API, pensez que vous allez devoir envoyer tout le début de la conversation avec chaque nouvelle question (ou remarque) de l'utilisateur. L'application ci-dessous affiche le nombre de tokens utilisés à chaque requête (en mode "debug", voir la fin du document JSON envoyé en réponse à la requête).

De toute façon vous ne risquez rien. Lorsque votre période d'essai sera terminée ou bien lorsque vous aurez épuisé les 5 $, vous recevrez un message d'erreur du type "Too many requests". Pour continuer à utiliser l'API il faudra alors recharger votre compte en allant sur https://platform.openai.com/account/billing/overview et en cliquant sur "Add payment details" (vous pouvez aussi créer un nouveau compte pour continuer à tester l'API...).

Utilisation directe de l'API

Description de l'application

Je vous propose d'écrire une application qui fait à peu près ce que fait ChatGPT : l'utilisateur pose des questions et l'API lui donne des réponses.

Libre à vous d'étoffer cette application avec des fonctionnalités supplémentaires ou bien d'utiliser l'API dans d'autres applications que vous aurez écrites. Si vous avez de l'imagination, je pense que vous pourrez faire de belles choses.

L'application n'utilisera pas de librairie pour OpenAI. Pour mieux comprendre comment l'API d'OpenAI fonctionne, vous allez créer un client REST qui va envoyer des requêtes REST à l'API. Votre application utilisera la réponse de l'API pour extraire les parties intéressantes pour elle. J'ai ajouté un mode "debug" qui vous permettra de voir facilement le format des documents JSON échangés.

Cette application n'aura qu'une seule page JSF. Elle permettra à l'utilisateur de poser ses questions et de voir les réponses de l'API.

Cette page n'aura qu'un seul backing bean qui s'occupera de tout. Normalement il devrait utiliser une librairie ou des classes à part pour s'occuper de l'interface avec l'API mais, pour simplifier et en première approche, les requêtes vers l'API seront lancées depuis le backing bean.

La difficulté du code vient de 2 points :

  • Comprendre le format des requêtes REST et des réponses. Pour cela il faut utiliser la documentation fournie par OpenAI.
  • Utiliser la spécification "Jakarta JSON Processing" pour manipuler les documents JSON échangés entre l'application et l'API.

A l'occasion vous apprendrez aussi quelques compléments sur JSF.

Création de l'application

Comme dans les applications précédemment écrites dans les TPs, créez une application Web avec Maven.

Page JSF

Voici à quoi va ressembler l'interface utilisateur de votre application (en mode "debug", avec JSON des requêtes affiché):

Interface de l'application "ChtGPT"

Vous aurez essentiellement des <h:textarea> pour afficher la question de l'utilisateur, la réponse de l'API et, si le mode "debug" a été activé, les documents JSON échangés. Un bouton permet d'envoyer les questions de l'utilisateur et un autre d'activer ou non le mode "debug". A droite, la conversation depuis le début entre l'utilisateur et l'API. Des boutons permettent de copier rapidement le contenu de chaque textarea en cas de besoin.

Créez la page JSF comme vous l'avez fait dans les TPs précédents.

Code de la page JSF. Code fichier mycsslayout.css et script.js (à placer au bon endroit, révisez le cours).

Vous remarquerez comment il est possible d'emboîter des <h:panelGrid> et d'utiliser CSS pour créer une interface utilisateur plus complexe que ce que vous avez fait jusqu'à maintenant.

Backing bean

La portée du backing bean sera "view" afin de garder facilement les informations sur la conversation (c'est un chat...) entre l'utilisateur et l'API.

Ce backing bean contient des propriétés pour conserver l'état de la conversation : question, réponse, début de la conversation,... En effet, l'API d'OpenAI est sans état et c'est au client de garder l'état de la conversation (tous les messages échangés depuis le début de la conversation).

Le backing bean utilise le endpoint v1/chat/completions de l'API OpenAI qui permet de générer une réponse à une suite de messages envoyées à l'API. Ce endpoint est décrit ici. Il faut envoyer une requête POST avec divers paramètres dont 2 sont requis :

  • messages : la liste des messages déjà échangés dans la conversation ;
  • model : le modèle de langage à utiliser ; vous utiliserez "gpt-3.5-turbo" (ou une version plus récente).

Le backing bean doit utiliser la clé secrète en la passant dans le header "Authorization", "Bearer <la clé secrète>". Pour éviter d'avoir à écrire en dur la clé secrète dans le code, ajoutez une variable d'environnement dans votre système d'exploitation ; je l'ai appelée CHATGPT_KEY dans mon code. La clé est utilisée au début de la méthode envoyer() qui envoie les questions à l'API.

Le document JSON à envoyer à l'API est construite dans la méthode creerJsonRequete(String nouvelleQuestion) qui utilise la spécification JSON-P pour construire un objet JSON qui est transformé en String pour être inséré dans le corps de la requête.

La réponse est récupérée dans la méthode et la méthode String extractReponse(String json) extrait le texte de la réponse de l'API à la question posée.

Au passage, les valeurs des propriétés du backing bean reçoivent leur valeur pour être affichées dans la page JSF. La méthode prettyPrinting du code du backing bean ci-dessous n'est pas indispensable ; elle permet un meilleur affichage du document JSON de la requête, si le mode "debug" est mis par l'utilisateur.

Code du backing bean.

A vous de jouer maintenant pour comprendre ce code et en tirer parti pour écrire votre propre application qui utilisera de l'intelligence artificielle.

Meilleure répartition des tâches

Tout le code Java est dans le backing bean. Il serait préférable que le backing bean délègue à d'autres classes tout ce qui n'est pas directement lié à la page JSF. En particulier le traitement JSON et le maintien de l'état de la conversation dans une classe, ainsi que le lancement de la requête vers l'API OpenAI dans une autre classe.

Récrivez l'application précédente pour que le backing bean délègue à d'autres classes tout ce qui n'est pas directement lié à l'interface utilisateur JSF.

Faites à votre idée si vous voulez, mais le partage des tâches n'est pas totalement évident. Voici une possibilité :

  • Le backing bean utilise un bean CDI JsonUtil qui gère l'état de la conversation et les manipulations JSON. L'état de la conversation contient le modèle utilisé (gpt-3.5-turbo par défaut), le rôle système, l'objet JSON qui contient les informations depuis le début de la conversation (pour ne pas avoir à le récréer à chaque nouvelle requête de la conversation). Cette classe contient la méthode action du formulaire de la page JSF, la méthode qui est exécutée quand l'utilisateur soumet une nouvelle question au chat.
  • Quand la classe JsonUtil a préparé le code JSON de la nouvelle requête, elle délègue l'envoi de la requête à l'API OpenAI à un autre bean CDI OpenAIClient qui gère l'interface avec l'API OpenAI. Cette classe récupère la clé secrète et envoie une requête à l'API OpenAI.
  • Une classe RequeteOpenAIException est une exception levée si une mauvaise requête est envoyée à l'API OpenAI.

Correction

Page JSF (n'oubliez pas les fichiers JavaScript et CSS)
Backing bean
JsonUtil
OpenAIClient
RequeteException

Utiliser l'API pour écrire une classe de ressource REST

Ajoutez à l'application une classe de ressource Java (application REST, comme vue dans le cours) annotée @Path("/guide_touristique") dont une méthode annotée @GET @Path("ville_ou_pays/{ville_ou_pays}") retourne un document JSON qui contient une liste JSON des 2 (pour limiter le nombre de tokens de la réponse) principaux endroits à visiter dans un pays.

Inspirez-vous du rôle "Guide touristique" du code du backing bean de la question précédente, en ajoutant que vous voulez le format JSON pour la réponse (en particulier les endroits à visiter dans une array JSON). Ajoutez aussi un exemple de ce que vous voulez pour fixer les noms des membres JSON de la réponse.

Utilisez la réponse de l'API dans la méthode REST de la classe de ressource. Si vous n'avez jamais écrit d'application REST avec JAX-RS, inspirez-vous du code généré par NetBeans dans le TP 1 et de cette page.

Pour tester, vous pouvez tout simplement taper l'URL de la ressource dans votre navigateur en remplaçant le <context-path> ci-dessous pour que l'URL lance votre application) : http://localhost:8080/<context-path>/resources/guide_touristique/ville_ou_pays/France

Explications pour cet URL :

  • Pour avoir le contexte, il suffit de lancer l'application et d'examiner l'URL de lancement ; le contexte est la partie qui est comprise entre "localhost:8080/" et "/index.xhtml", par exemple "utilisationchatgpt-1.0-SNAPSHOT" si l'URL de lancement est "http://localhost:8080/utilisationchatgpt-1.0-SNAPSHOT/index.xhtml".
  • "resources" (attention, un seul "s") vient de la classe de configuration REST.
  • "ville_ou_pays" vient de l'annotation de la méthode associée à la requête GET (dans la classe de ressource).
  • Evidemment vous pouvez choisir d'autres valeurs en modifiant le code ou le nom de l'application.

Voici le type d'affichage que vous devriez avoir dans le navigateur (le format exact dépend des noms que vous avez choisis pour l'exemple que vous avez donné dans le rôle "Guide touristique") :
{ "ville_ou_pays": "France", "endroits_a_visiter": ["Eiffel Tower", "Louvre Museum"], "prix_moyen_repas": 30 }

Vous pouvez aussi utiliser cette page HTML. Vous pouvez utiliser les noms de l'affichage JSON précédent pour écrire votre code. Pour assurer votre code, il faut mettre dans la requête adressée à ChatGPT un exemple du code JSON que vous souhaitez avoir en réponse. Vous pourrez ainsi choisir les noms des attributs JSON, pour le pays, les endroits à visiter et le coût moyen d'un repas. Ne cliquez pas sur le lien, enregistrez sur votre ordinateur le fichier HTML. Dans le code JavaScript de la page, remplacez le <context-path> dans l'URL passée en paramètre à xhr.open (utilisationchatgpt-1.0-SNAPSHOT) pour qu'il correspond à celui de votre application).

Correction

Classe de ressource
Classe de configuration REST

Utilisation de LangChain4j - Assistant interactif

LangChain est un framework open source qui ajoute une couche d'abstraction aux API des LMs.

Cette couche d'abstraction améliore la portabilité des applications (il est plus simple de changer de LLM) et facilite l'utilisation des API. Elle permet surtout d'enchainer plusieurs interactions avec des LMs, et des traitements complémentaires.

Exemples de traitements complémentaires :

  • Lecture de fichiers ou d'une base de données, qui contiennent des données ou des règles de procédures particulières à un domaine ou à une entreprises pour les faire prendre en compte par les LMs.
  • Accès Internet pour vérifier une information donnée par un LM (pour limiter les hallucinations).

LangChain4j est une librairie Java pour LangChain.

Le but de cet exercice est de récrire la précédente application en utilisant LangChain4j :

  • Ecrivez un chat comme au début de ce TP, sans le mode debug (on se place à un plus haut niveau d'abstraction. LangChain permet de changer facilement de modèle de langage). L'utiilisateur peut choisir le type d'assistant au début de la session.

Dépendance Maven à ajouter pour LangChain4j :

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j</artifactId>
   <version>0.27.1</version>
</dependency>

Dépendance Maven à ajouter pour l'utilisation de l'API d'OpenAI avec LangChain4j :

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j-open-ai</artifactId>
   <version>0.27.1</version>
</dependency>

Correction

Cette correction utilise la notion de service AI de LangChain4j (évolution de la notion de chaîne de LangChain, mieux adaptée au langage Java). Cette notion facilite l'écriture pour les cas plus complexes mais peut aussi être utlisée pour les cas simples. Les échanges avec le LM sont représentés par une interface Java dont LangChain4j fournira automatiquement un objet proxy.

Page JSF (n'oubliez pas les fichiers JavaScript et CSS)
Backing bean
OpenAiClient.java
Assistant.java

Utilisation de LangChain4j - Classe de ressource

Créez un nouveau projet dans lequel vous récrivez la classe de ressource REST en utilisant LangChain4j.

Ajoutez une nouvelle interface Java bien adaptée aux questions posées à l'API OpenAI, avec une annotation @SystemMessage qui reprend le rôle alloué à l'API dans la classe de ressource REST déjà écrite plus haut.

Testez comme pour l'exercice précédent sur la classe de ressource REST.

Correction

Classe de ressource
Classe de configuration REST
Interface GuideTouristique.java
OpenAiClientForGuideTouristique.java

Utilisation de LangChain4j et de WebSocket pour le streaming

ChatGPT affiche ses réponses en streaming : il commence à afficher chaque réponse dès qu'il a son début, sans attendre d'avoir la réponse complète.

Vous allez faire de même.

Pour cela vous allez utiliser une option de LangChain4j qui permet d'obtenir de l'API OpenAI la réponse token par token. Quand le serveur de votre application obtient un token de l'API OpenAI, il utilise un WebSocket pour faire afficher ce token dans la page JSF qui fait l'interface avec l'utilisateur.

Page JSF
Backing bean Bb.java
Classe OpenAiClient.java
InterfaceAssistant.java


Retour TPs