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

Retour TPs

Ce TP vous donnera une illustration pratique en Java (avec Jakarta EE) de l'utilisation d'une API IA.

N'oubliez pas d'utiliser Git et GitHub.

Introduction

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

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. Vous auriez pu écrire une application qui utilise directement les endpoints REST de l'API OpenAI, avec des documents JSON explicitement construits dans l'application. Si vous êtes intéressé par cet accès direct aux endpoints ou bien si vous voulez savoir comment manipuler les documents JSON en Java, vous pouvez faire ces exercices après avoir récupéré une clé secrète pour l'API d'OpenAI, comme il est indiqué ci-dessous, et après avoir pris connaissance des coûts liés à l'utilisation de cette API

En attendant une éventuelle spécification Java ou Jakarta EE pour l'utilisation de l'intelligence artificielle, vous allez utiliser le framework LangChain4j qui est actuellement un standard de facto pour l'utilisation des LLM en Java, et en particulier pour faire du RAG. Elle permet un plus haut niveau d'abstraction et facilite l'utilisation des LLMs 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.

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. Si vous ne voyez pas "API keys", cliquez en haut sur "Dashboard" pour faire apparaitre le bon menu à gauche.
  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). Lorsque j'ai commencé à travailler avec l'API les nouveaux comptes recevaient un crédit de 5 $ à dépenser en 3 mois, ce qui était largement suffisant pour faire de très nombreux tests car les coûts ne sont pas élevés. Malheureusement il semble que le crédit de 5 $ n'existe plus en 2024. En ce cas vous allez devoir acheter des crédits ; 5 euros seront largement suffisants pour tester vos TPs.

Pour ajouter des crédits avec une carte bancaire, allez sur https://platform.openai.com/account/billing/overview. Clic sur "Payment methods" et sur "Add payment details".

Si vos crédit sont épuisés, au moment où votre programme va lancer une requête vers l'API vous recevez un message d'erreur du type "Too many requests". A tout moment vous pouvez consulter ce qu'il vous reste sur votre compte à l'adresse https://platform.openai.com/settings/organization/billing/overview.

Pour testez vos TPs, choisissez le modèle le moins cher. A la date de l'écriture de ces lignes il s'agit du modèle GPT-4o-mini, pour lequel le prix pour l'échange de 1 000 000 tokens (environ 170 000 mots en moyenne pour le français et l'anglais) entre votre application et l'API coûte seulement 0.075 $ en input et 0.300 $ en output. Pour réduire les coûts, 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 de l'utilisateur. La page des coûts est https://openai.com/pricing ; vérifiez-la car OpenAI les modifie souvent les tarifs (le plus souvent à la baisse).

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.

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 :

Interface utilisateur du chat

Voici à quoi va ressembler l'interface utilisateur de votre application après 3 échanges avec l'API d'OpenAI (affichés dans la zone de droite) :

Exemple de chat

Cette page aura un backing bean. Ce backing bean utilisera un bean CDI qui utilisera LangChain4j pour envoyer des requêtes à l'API d'OpenAI.

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

Page JSF et backing bean

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

Elle contient essentiellement des <h:textarea> pour afficher la question de l'utilisateur, la réponse de l'API et l'historique de la conversation depuis le début entre l'utilisateur et l'API.

Une liste déroulante permet de choisir le rôle de l'assistant (API de OpenAI) : "helpful assistant", "traducteur français-anglais" ou "guide touristique". Le choix ne peut être effectué qu'une seule fois par session. Pour changer de rôle, l'utilisateur doit cliquer sur le bouton "Nouveau chat", ce qui démarre un nouveau chat. Pour simplifier, n'offrez que le choix "helpful assistant" pour votre première itération de développement.

Un bouton permet d'envoyer les questions de l'utilisateur et d'effacer la dernière question et la dernière réponse pour nettoyer les zones avant une nouvelle question.

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.

Git et GitHub...

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, historique 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 injecte une instance de la classe OpenAiClient à laquelle il délègue l'interface avec l'API d'OpenAI.

Git et GitHub...

Classe pour travailler avec les LLMs

La classe Java OpenAiClient est la classe "métier", celle qui est liée aux LLMs, en particulier à l'API OpenAI.

Elle utilise la clé secrète que vous avez récupérée sur votre compte chez OpenAI. Vous devez copier sa valeur dans la variable d'environnement de votre système d'exploitation CHATGPT_KEY (vous pouvez choisir un autre nom si vous voulez) et elle sera lue dans le constructeur de la classe. C'est un bon moyen pour que sa valeur n'apparaisse pas dans votre code source (pensez en particulier que vous allez le pousser sur GitHub !).

Cette classe s'appuie sur LangChain4j.

LangChain4j

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

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 aisément plusieurs interactions avec des LLMs, 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 entreprise pour les faire prendre en compte par les LLMs.
  • Accès Internet pour vérifier une information donnée par un LLM (pour limiter les hallucinations).

LangChain4j est une librairie Java pour LangChain.

Dépendance Maven à ajouter pour LangChain4j (pour les 2 dépendances suivantes, choisissez le dernier numéro de version, à la place de 0.33.0 ; le numéro doit être le même pour les 2 dépendances) :

<dependency>
   <groupId>dev.langchain4j</groupId>
   <artifactId>langchain4j</artifactId>
   <version>0.33.0</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.33.0</version>
</dependency>

Références pour LangChain4j

Code de la classe OpenAiClient

Service AI

LangChain4j utilise la notion de "service IA" qui est une é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.

Un service IA définit un comportement pour des échanges de messages entre l’application et le LLM.

Le développeur définit une interface qui contient les méthodes qui correspondent aux interactions que son application aura avec le LLM.

L'instance Java qui va envoyer les requêtes au LLM est une instance d'une interface Java que vous allez créer.Vous mettez dans cette interface les méthodes que vous voulez. LangChain4j fournira automatiquement une implémentation de cette interface avec un objet proxy (vous n'avez pas besoin d'implémenter l'interface). Les implémentations des méthodes de l'interface seront des échanges de messages (questions et réponses) entre l’application et le LLM ; LangChain4j tient compte des types des paramètres, du type retour et des annotations des méthodes pour écrire ces implémentations.

Pour ce projet l'interface sera très simple, avec une seule interaction qu'on appelera chat (on peut choisir le nom que l'on veut) : une question est envoyée au LLM et il répond :

public interface Assistant {
  String chat(String prompt);
}

La classe AIServices de LangChain4j permet de créer une "instance de cette interface" (en fait c'est une instance de la la classe "proxy" créée par LangChain4j, qui implémente l'interface). La classe OpenAiClient utilise cette instance une pour envoyer des messages à l'API OpenAI.

Variables d'instance de OpenAiClient

  • String systemRole : le rôle que l'utilisateur choisira pour l'assistant IA.
  • Assistant assistant : Assistant est l'interface que vous avez défini pour décrire les interactions avec le LLM.
  • ChatMemory chatMemory : la mémoire utilisée par l'assistant pour garder l'historique de la conversation. En effet, l'API d'OpenAI, comme toutes les API des LLMs, 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). ChatMemory est une interface de LangChain4j qui facilite la gestion de l'état.

Constructeur de OpenAiClient

  1. Récupère la valeur de la clé secrète OpenAI avec la méthode Java standard System.getenv.
  2. Utilise l'interface ChatLanguageModel pour enregistrer la clé secrète pour les futurs appels à l'API d'OpenAI :
    ChatLanguageModel model = OpenAiChatModel.withApiKey(openAiKey);
  3. Crée l'assistant en utilisant la classe AiServices fournie par LangChain4j :
    this.chatMemory = MessageWindowChatMemory.withMaxMessages(10);
    this.assistant = AiServices.builder(Assistant.class)
                               .chatLanguageModel(model)
                               .chatMemory(chatMemory)
                               .build();

Méthodes de OpenAiClient

2 méthodes :

  • Un setter pour le rôle système (celui indiqué par la liste déroulante de la page JSF). Il ajoute ce rôle à la mémoire, comme instance de type SystemMessage pour qu'il soit pris en compte par le LLM.
  • Une méthode qui envoie une requête au LLM et qui reçoit une réponse en retour. Cette méthode utilise l'instance de l'interface Assistant (instance dont la classe est implémentée par LangChain4j comme c'est expliqué plus haut).

Test de l'application

Testez.

Git et GitHub si tout va bien.

Autres rôles

Ajoutez le rôle de traducteur. Pour cela, ajoutez le rôle dans la liste déroulante de la page JSF en utilisant le backing bean.

Voici un exemple de description (vous pouvez les écrire en français ; il semble que l'API d'OpenAI comprenne un peu mieux l'anglais mais la différence est minime) :

String role = """
        You are an interpreter. You translate from English to French and from French to English.
        If the user type a French text, you translate it into English.
        If the user type an English text, you translate it into French.
        If the text contains only one to three words, give some examples of usage of these words in English.
        """;

Ajoutez d'autres rôles si vous avez le temps. Par exemple,

role = """
        Your are a travel guide. If the user type the name of a country or of a town,
        you tell them what are the main places to visit in the country or the town
        are you tell them the average price of a meal.
       """;

Correction

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

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

Vous allez créer une nouvelle application REST qui contient une classe de ressource Java 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, et donc le coût des tests) 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 un 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, des transparents donnés dans le support d'introduction à ce cours, 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 générée par NetBeans.
  • "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).

Création et test de l'application

Créez un nouveau projet dans lequel vous copiez les classes OpenAiClient et les interfaces qu'elle utilise. Ajoutez la classe de ressource REST. Ne copiez pas la page JSF ni son backing bean.

Pour définir l'assistant LangChain4j "guide touristique" vous allez utiliser une possibilité offerte par LangChain4j : les méthodes des assistants peuvent être annotées par @SystemMessage qui reprend le rôle alloué à l'API décrit au début de cette section. Le contenu du message sera automatiquementUne aide ?

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

Optionnel : Utilisation de LangChain4j et de WebSocket pour recevoir les réponses token par token

ChatGPT affiche ses réponses en streaming : il n'attend d'avoir généré toute la réponse pour l'envoyer à l'utilisateur. Il envoie les mots (plus exactement les tokens) de la réponse dès qu'il les a générés. 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.

Les échanges entre le serveur et le client vont utiliser un websocket.Vous allez ajouter un websocket dans la page JSF de l'application qui fait l'interface avec l'utilisateur avec la balise <f:websocket>. Les questions de l'utilisateur sont envoyées dans ce socket. Sur le serveur, les tokens de la réponse de OpenAI sont envoyés dans le websocket dès qu'ils sont reçus. Sur le client HTTP, un listener JavaScript traite les tokens envoyés par le serveur. Une balise <f:ajax> incluse dans la balise <f:websocket> traite l'événement personnalisé "streamingfinished" (message que le serveur envoie dans le websocket quand l'API OpenAI a terminé sa réponse ; une autre valeur aurait pu être choisie) avec un listener, pour signaler que l'historique de la conversation peut être mis à jour.

Pour les réponses courtes, il n'y aura pas un grand changement car les tokens sont envoyés à grande vitesse mais si la réponse est longue, ou si le réseau est lent, vous verrez bien les tokens s'afficher au fur et à mesure.

Tutoriel Jakarta EE sur WebSocket : https://jakarta.ee/learn/docs/jakartaee-tutorial/current/web/websocket/websocket.html et https://jakarta.ee/learn/docs/jakartaee-tutorial/current/web/faces-ws/faces-ws.html (pour utilisation avec JSF).

Correction

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

Retour TPs