IA avec Flutter et Dart: boostez vos applications avec Gemini
Cet article est dédié à l’intégration de l’API de Gemini avec Flutter et Dart. Nous allons voir à peu après une bonne partie de ce qu’il est possible de faire avec Gemini et son intégration dans Flutter.
Vous pouvez voir la démo, pour avoir un aperçu global de ce qui sera vu dans cet article.
Pre-requis
Ce tutoriel étant centré sur l’API de Gemini, vous devez avoir des notions de bases dans Flutter et Dart afin de bien le suivre et le comprendre.
C’est quoi Gemini?
Gemini est une suite de modèles génératifs d’intelligence artificielle créée par Google pour d’abord alimenter sa gamme de produits dont son chat bot Bard (qui désormais s’appel Gemini).
Gemini est dit “multimodal” ce qui signifie qu’il peut générer du contenu à partir de différents types du contenu tel que du texte, des images et même des vidéos et des audio (avec Gemini 1.5 qui n’est pas encore utilisable avec l’API au moment où j’écris cet article — Avril 2024).
Dans cet article nous allons nous focaliser sur deux models:
Gemini Pro
C’est le modèle le plus basique qui permet de générer du texte à partir du texte; il a pour identifiant gemini-pro.
Gemini Pro Vision
Ce modèle, en plus d’avoir la possibilité de générer du texte à partir du texte, il peut également le faire à partir des images et même à partir du texte et des images combinés; il a pour identifiant gemini-pro-vision.
Pourquoi intégrer Gemini dans son application?
L’intégration de l’API de Gemini peut s’avérer très utile pour les développeurs afin de faciliter certaines tâches en profitant de la puissance de l’intelligence artificielle.
Voici une liste non exhaustive de cas d’utilisations:
- Un chat bot adapté qui répond uniquement aux questions qui concernent les produits que vous vendez dans votre application.
- Traduction automatique des commentaires publiés par les utilisateurs dans votre application.
- Suppression automatique des commentaires à caractère haineux ou autre de votre application.
- Classification automatique d’un avis positif ou négatif sur un produit dans votre application.
Comme vous pouvez le constater, il y a une infinité de possibilités de choses que vous pouvez faire avec l’API de Gemini et cela sans avoir de connaissance approfondie sur le machine learning ou le deep learning. Votre seule limite c’est votre imagination 😊.
Installation
Nous allons d’abord procéder à l’installation des outils de développement avant les tests; on suppose que vous avez tout l’environnement de Flutter prêt à l’emploi.
Il vous faut ajouter le package google_generative_ai dans votre fichier pubspec.yaml.
Récupération de la clé d’API
Pour utiliser l’API de Gemini, il est obligatoire d’avoir une clé d’API, que vous pouvez récupérer dans Google AI Studio.
Utilisation de la clé
Comme il est indiqué lorsque vous gênerez une clé d’API, il est primordial de la protéger en faisant en sorte que celle-ci ne soit pas visible publiquement.
Sur ce, en Flutter nous allons placer la clé directement dans la commande de lancement du code source qui sera:
flutter run --dart-define=API_KEY=$YOUR_API_KEY
Remplacez $YOUR_API_KEY par la clé que vous aviez généré.
Dans le cas où vous voudriez générer l’Apk, l’app bundle, l’ipa,… vous devez également passer le paramètre de votre clé dans la commande comme suit;
flutter build ipa --dart-define=API_KEY=$YOUR_API_KEY
On peut ainsi récupérer la clé, sans avoir à l’afficher dans le code:
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');
Lorsque vous utilisez un IDE tel que Android Studio ou VS Code, vous pouvez configurer cette commande.
Android Studio
- Ouvrez votre projet
- Cliquez sur la liste déroulante “main.dart” au dessus de votre fenêtre.
- Cliquez sur “Edit Configurations…”
- Dans le champ “Additional run args”, ajoutez “ — dart-define= API_KEY=$YOUR_API_KEY”
- Cliquez sur “Apply” ensuite sur “Ok”.
Votre configuration devrait ressembler à ceci:
Visual Studio Code
- Ouvrez le projet
- Cliquez sur “Run and Debug” dans la barre latérale gauche.
- Cliquez sur “create a launch.json file”.
- Sélectionnez “Dart & Flutter”
- Ajoutez “ “args”: [“ — dart-define=API_KEY=$YOUR_API_KEY”] ” dans le premier objet de configurations.
- Enregistrez le fichier.
Votre fichier de configuration devrait ressembler à ceci:
Tests pratiques
Place à la pratique maintenant.
Les tests sont répartis en quatre catégories;
- Génération du texte à partir du texte
- Génération du texte à partir du texte et des images
- Création d’un chat
- Des interactions plus rapides avec le streaming.
Génération du texte à partir du texte
Comme dit plus haut, le modèle gemini-pro permet de générer du texte à partir du texte. Vous fournissez au modèle votre prompt (texte) et en retour il vous donne une réponse sous forme de texte, aussi simple que ça 😎.
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');
// user gemini-pro model
final model = GenerativeModel(model: 'gemini-pro', apiKey: apiKey);
final content = [Content.text(input)];
final response = await model.generateContent(content);
output = response.text?? 'No response';
On initialise notre clé d’API que l’on passe avec l’identifiant du modèle en paramètre du constructeur GenerativeModel, ensuite on passe le prompt en paramètre de la méthode generateContent et enfin on n’a plus qu’à attendre le résultat de notre requête que l’on peut récupérer comme texte.
Il est possible de placer plusieurs contenus (Content) dans generateContent, par exemple au-dessus du prompt de l’utilisateur, vous pouvez ajouter d’autres contenus qui représentent par exemple les instructions que vous donnez au modèle afin d’améliorer l’efficacité des résultats.
Voici le résultat:
Le code source complet est sur GitHub, le lien se trouve a la fin de cet article.
Génération du texte à partir du texte et des images
Pour générer du texte à partir du texte et des images en même temps, il faut utiliser le modèle gemini-pro-vision.
Il est capable de prendre jusqu’à 16 images dans une même requête, ces images doivent avoir l’un des formats suivants: PNG, JPEG, WEBP, HEIC, HEIF.
Les images sont envoyées sous forme de bytes à travers la classe Uint8List, il est donc important de convertir nos images avant de les passer en paramètre, on crée une fonction pour cela.
Future<Uint8List?> getImageBytes(String assetPath) async {
try {
ByteData byteData = await rootBundle.load(assetPath);
return byteData.buffer.asUint8List();
}
catch (e) {
debugPrint('Error loading image: $e');
}
return null;
}
}
Dans notre exemple, nous allons utiliser des images provenant des assets, vous pouvez également utiliser les images uploadées par l’utilisateur ou encore à partir des adresses web, du moment que vous pouvez les convertir en Uint8List avant.
Voici notre code:
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');
// for text and image input, use gemini-pro-vision model
final model = GenerativeModel(model: 'gemini-pro-vision', apiKey: apiKey);
final (firstImage, secondImage, thirdImage) = await (
getImageBytes('assets/images/image-input-1.png'),
getImageBytes('assets/images/image-input-2.png'),
getImageBytes('assets/images/image-input-3.png'),
).wait;
if (firstImage == null || secondImage == null || thirdImage == null) {
output = 'Error loading images';
return;
}
final response = await model.generateContent([
Content.multi(
[
TextPart(input),
DataPart('image/png', firstImage),
DataPart('image/png', secondImage),
DataPart('image/png', thirdImage),
]
)
]);
output = response.text?? 'No response';
On initialise le modèle gemini-pro-vision en passant également la clé de l’api en paramètre.
On commence par convertir nos 3 images provenant des assets à travers la méthode créée précédemment et on vérifie si la conversion s’est bien passé, dans le cas contraire on arrête l’exécution.
Ensuite on utilise la même méthode generateContent à laquelle on donne du contenu, cette fois on utilise Content.multi pour donner du texte et des images en même temps. On utilise TextPart pour le texte et DataPart pour les autres données telles que les images dans notre cas. (il sera possible plus tard de mettre des audio ou encore des vidéos).
Enfin, on peut récupérer le résultat de notre requête dans response.text.
Le résultat:
Création d’un chat
Dans les précédents tests, chaque prompt avaient ses propres résultats et ne dépendaient pas des précédents; dans le cas d’un chat, il s’agit d’une conversation, les messages dépendent donc les uns des autres.
Gemini permet de faire cela à partir de la méthode startChat du modèle, il est même possible de donner un historique de conversation ce qui permettra au modèle d’adapter ses réponses par rapport à cet historique; ensuite Gemini gère seul la suite de la conversation.
Pour gérer plus facilement les messages, on crée une classe MessageItem.
class MessageItem {
final String text;
final bool isSender;
MessageItem(this.text, this.isSender);
Content get content {
if (isSender) {
return Content.text(text,);
}
return Content.model([TextPart(text)]);
}
}
À travers isSender, on peut savoir si c’est le message du modèle ou de l’utilisateur ce qui nous permettra de placer le message à l’endroit approprié.
On crée également une propriété content qui permet de récupérer le contenu à envoyer au modèle, on utilise Content.text pour l’utilisateur et Content.model pour le modèle (celui-ci ne peut être passé manuellement au modèle que dans les messages d’historique, ensuite le modèle génère lui seul ses messages).
Initialisation
const apiKey = String.fromEnvironment('API_KEY', defaultValue: '');
final model = GenerativeModel(
model: 'gemini-pro',
apiKey: apiKey,
generationConfig: GenerationConfig(maxOutputTokens: 5000)
);
messages = [
MessageItem('Bonjour, comment puis-je vous aider?', false),
MessageItem('Bonjour,', true),
];
chat = model.startChat(
history: List.generate(messages.reversed.length, (index) => messages.reversed.toList()[index].content),
);
On initialise le modèle gemini-pro en passant également la clé de l’API en paramètre.
Il est également possible d’utiliser gemini-pro-vision dans le cas où vous voulez envoyer des images mais pour le moment les résultats ne sont pas efficaces dans le cas d’un chat.
Lors de l’initialisation, vous avez sans doute remarqué, un paramètre dont on n’a pas parlé pour le moment generationConfig, il permet de personnaliser les configurations par défaut du modèle; dans notre exemple on limite le total des tokens renvoyés à 5000.
Les tokens sont en quelque sorte les groupes de caractères ou mots qu’un modèle peut recevoir ou renvoyer, chaque modèle a ses limites que vous pouvez voir dans la documentation.
Limiter le nombre de tokens entrants ou sortant peut s’avérer utile, dans le cas où l’on aimerait par exemple avoir des réponses courtes ou encore pour limiter les requêtes par utilisateurs et éviter ainsi d’avoir une grosse facture si on a une application très utilisée.
Gemini dispose d’une méthode qui compte le nombre de tokens à partir des caractères qu’on lui envoie, voir Count tokens pour en savoir plus.
On initialise le chat à travers la méthode startChat du modèle en lui donnant un historique de messages (optionnel), le premier message du tableau c’est la réponse du modèle au dernier message qui est celui de l’utilisateur.
Le tableau est à l’envers afin de bien l’afficher dans une ListView dans l’interface graphique.
Comme dit plus haut initialiser des messages peut s’avérer utiles dans le cas où l’on veut que le modèle réponde d’une certaine manière aux prochains messages.
Envoi de messages
MessageItem newMessage = MessageItem(input, true);
messages.insert(0, MessageItem(input, true));
final response = await chat.sendMessage(newMessage.content);
if (response.text?.isNotEmpty?? false) {
messages.insert(0, MessageItem(response.text!, false));
}
L’envoi de messages est plus simple que l’initialisation du chat 😊. On ajoute un objet MessageItem au début du tableau avec le texte de l’utilisateur, ensuite on l’envoie à travers la méthode sendMessage qui prend le contenu en paramètre et retourne le contenu de la réponse que l’on ajoute aussi au tableau en spécifiant qu’il s’agit de la réponse du modèle.
Le markdown
Avant de passer à l’étape suivante, je voudrais parler du markdown.
Quand Gemini retourne un long texte, il le retourne sous forme de markdown qui est un langage de balisage léger qui permet de formater le texte.
On peut ainsi avoir ce genre de réponse:
Comme vous pouvez le constater, Gemini a utilisé le markdown dans sa réponse; rendre le message de cette manière à l’utilisateur n’est pas approprié.
On va donc utiliser le package flutter_markdown qui permet de formater ce langage.
Il peut-être utilisé de cette manière:
MarkdownBody(
data: text,
onTapLink: (text, href, title) {
// open link
},
styleSheet: MarkdownStyleSheet(
p: textStyle,
a: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
),
textAlign: WrapAlignment.start,
),
)
Une fois qu’on l’utilise, notre texte est formaté comme il se doit.
Voici la démo complète de cette section:
Des interactions plus rapides avec le streaming
Lorsque vous utilisez Gemini Chat, Chat GPT ou encore Microsoft Copilot, vous remarquerez que la réponse vient au fur et à mesure que le traitement s’effectue; il est également possible de le faire avec l’API de Gemini.
Par défaut, le modèle retourne la réponse après avoir terminé le processus complet de génération ce qui peut être lent pour l’utilisateur, en utilisant le streaming la réponse sera reçue partie après partie ce qui permettra à l’utilisateur de commencer la lecture sans avoir à attendre la réponse complète.
Pour illustrer cela, on va utiliser le même exemple de chat, l’initialisation se fait exactement de la même manière, la différence se fera au niveau de l’envoi du message.
MessageItem newMessage = MessageItem(input, true);
messages.insert(0, MessageItem(input, true));
final response = chat.sendMessageStream(newMessage.content);
messages.insert(0, MessageItem('', false));
await for (final chunk in response) {
String text = messages[0].text + (chunk.text?? '');
messages[0] = MessageItem(text, false);
}
Plutôt que d’utiliser sendMessage, on utilise sendMessageStream pour recevoir la réponse par partie.
Une fois qu’on a la réponse, on l’écoute et on met à jour le message en y ajoutant le texte de la partition générée.
Pour la génération simple de texte à partir du texte ou à partir des images, vous devez utiliser la méthode generateContentStream plutôt que generateContent.
Google AI Studio
Google AI studio est un IDE utilisable à partir d’un navigateur web qui permet de tester vos prompts, les modifier pour avoir de meilleures réponses avant de pouvoir les intégrer dans votre technologie dont Flutter.
Pour faire des tests avec Google AI Studio, vous n’avez pas besoin d’une clé d’API vous pouvez donc faire autant de tests que vous voulez sans avoir à être facturé.
Il permet aussi de voir en temps réel le nombre de tokens utilisés; ce qui vous permettra de mieux paramétrer cela dans votre code.
Il est possible d’exporter directement le code en cURL, Javascript, Python, Kotlin et Swift. Malheureusement il n’est pas encore possible d’exporter le code en Dart au moment où j’écris cet article; vous pouvez vous référer au code Javascript qui est assez similaire.
Google AI Studio propose 3 types de tests:
- Freeform prompts
- Structured prompts
- Chat prompts
Freeform prompts
Il permet de créer différents types de tests en roue libre pour vous permettre de tester l’efficacité de vos prompts. Il peut prendre du texte ou encore des images par rapport au modelé que vous aurez specifier.
Sur cette capture, on fait un test sur une image avec plusieurs entrées possibles. Vous pouvez specifier une entrée, en sélectionnant le texte de votre prompt et ensuite en cliquant sur “Text Input”.
Une fois que vous cliquez sur “Run”, vous verrez apparaitre les réponses par rapport aux entrées spécifiées, ce qui vous emmènera à modifier votre prompt si les réponses ne vous conviennent pas.
Structured prompts
Si vous voulez contrôler la manière dont le modèle renvoi les réponses, ce cas d’utilisation est le mieux adapté pour cela. Il permet de fournir des instructions, des exemples d’entrée / sortie (jusqu’à 500), ce qui fera que Gemini adaptera ses réponses par rapport aux exemples que vous lui avez fournis.
Chat prompts
Utilisez ce cas d’utilisation lorsque vous voulez expérimenter une conversation comme on l’a vu plus haut.
Sur cet exemple on instruit le modèle de répondre avec un nom specific et de répondre uniquement aux questions relatives au basket-ball; lors de l’implementation les deux premiers messages seront dans l’historique et ne seront donc pas visibles à l’utilisateur.
Code source
Retrouvez le code source complet de tout ce qui a été vu sur GitHub. Si mon travail vous a plu, n’hésitez pas à laisser une étoile pour m’encourager à partager davantage d’articles.
Conclusion
L’intégration de l’intelligence artificielle avec Flutter dans votre application a travers l’API de Gemini améliorera votre application avec une meilleure expérience utilisateur, ou encore des fonctionnalités innovantes qui vous démarqueront de vos concurrents.
Références
🚀 Restons Connectés !
Merci d’avoir lu cet article ! Si vous avez aimé le contenu, n’hésitez pas à me suivre sur les réseaux sociaux pour rester informé(e) des dernières mises à jour, astuces et partages autour de Flutter et du développement.
👥 Suivez-moi sur :
Twitter: lyabs243
LinkedIn: Loïc Yabili
Rejoignez notre communauté grandissante et engageante pour des discussions passionnantes sur le développement Flutter ! 🚀✨
Merci pour votre soutien continu ! 🙌✨