Appearance
Intégrer l'IA dans Angular avec Genkit
L'intelligence artificielle transforme la façon dont nous développons des applications web. Avec Angular 20 et Genkit, vous pouvez désormais créer des expériences utilisateur intelligentes et interactives en quelques lignes de code.
Imaginez une application de restaurant où les clients peuvent demander des suggestions de plats personnalisées. Au lieu de parcourir un menu statique, ils décrivent leurs préférences ("quelque chose de végétarien et épicé") et l'IA génère instantanément des recommandations adaptées. C'est exactement ce que nous allons construire ensemble !
Qu'est-ce que Genkit ?
Genkit est un framework open-source développé par Google qui simplifie l'intégration de l'IA dans vos applications. Il vous permet de :
- Unifier l'accès à plusieurs modèles IA (Gemini, OpenAI, Anthropic, etc.)
- Créer des workflows intelligents avec des chaînes d'inférences
- Déployer facilement sur Cloud Functions, Cloud Run ou tout serveur Node.js
Pourquoi Genkit avec Angular ?
Angular 20 introduit une section dédiée "Build with AI" qui facilite l'intégration de l'IA. La combinaison d'Angular et Genkit offre une architecture robuste pour créer des applications intelligentes avec une expérience utilisateur fluide.
Prérequis
Avant de commencer, assurez-vous d'avoir :
Outil | Version | Installation |
---|---|---|
Node.js | ≥ 20 LTS | nodejs.org |
Angular CLI | Dernière version | npm install -g @angular/cli |
Genkit CLI | Dernière version | npm install -g genkit-cli |
Clé API | Gemini ou autre | Google AI Studio |
Création du projet
Étape 1 : Initialiser le projet Angular avec SSR
Nous allons créer un projet Angular avec le rendu côté serveur (SSR) pour pouvoir utiliser Genkit :
bash
ng new restaurant-ai-app --ssr --server-routing
cd restaurant-ai-app
Importance du SSR
Genkit nécessite un environnement serveur pour fonctionner. Le SSR d'Angular nous fournit cette infrastructure nécessaire.
Étape 2 : Installation des dépendances Genkit
Installons les packages nécessaires pour intégrer Genkit :
bash
# Core Genkit
npm install genkit
# Plugin pour Google AI (Gemini)
npm install @genkit-ai/googleai
# Middleware Express pour exposer les flows
npm install @genkit-ai/express
Configuration de l'environnement
Configuration des variables d'environnement
Créez un fichier .env
à la racine du projet :
bash
GEMINI_API_KEY=votre_clé_api_ici
Sécurité des clés API
Ne jamais inclure vos clés API dans le code source. Utilisez toujours des variables d'environnement ou un gestionnaire de secrets en production.
Création du workflow IA
Définition du Flow Genkit
Créons notre premier flow IA. Créez le fichier src/genkit/menu-suggestion-flow.ts
:
typescript
import { genkit } from 'genkit';
import { googleAI } from '@genkit-ai/googleai';
import { z } from 'zod';
// Configuration de Genkit avec le plugin Google AI
const ai = genkit({
plugins: [googleAI()]
});
/**
* Flow pour générer des suggestions de menu basées sur un thème
*
* Ce flow utilise Gemini pour créer des suggestions de plats personnalisées.
* Il prend en entrée un thème (ex: "végétarien épicé") et génère une suggestion
* de plat avec description.
*
* @example
* ```typescript
* const result = await menuSuggestionFlow({ theme: "italien moderne" });
* console.log(result.menuItem); // "Risotto aux champignons truffés..."
* ```
*/
export const menuSuggestionFlow = ai.defineFlow(
{
name: 'menuSuggestionFlow',
inputSchema: z.object({
theme: z.string().describe('Le thème ou style de cuisine souhaité')
}),
outputSchema: z.object({
menuItem: z.string().describe('La suggestion de plat générée'),
description: z.string().describe('Description détaillée du plat'),
price: z.string().describe('Prix estimé du plat')
}),
streamSchema: z.string(),
},
async ({ theme }, { sendChunk }) => {
// Génération en streaming pour une meilleure UX
const { stream, response } = ai.generateStream({
model: googleAI.model('gemini-2.0-flash'),
prompt: `Tu es un chef cuisinier créatif. Crée un plat unique pour un restaurant ${theme}.
Réponds au format JSON avec :
- menuItem: nom du plat (maximum 50 caractères)
- description: description appétissante (maximum 150 caractères)
- price: prix en euros (format "XX€")
Sois créatif et original !`,
});
// Envoi des chunks en temps réel
for await (const chunk of stream) {
sendChunk(chunk.text);
}
const { text } = await response;
try {
// Parse de la réponse JSON
const parsed = JSON.parse(text);
return {
menuItem: parsed.menuItem || 'Plat surprise',
description: parsed.description || 'Une création unique de notre chef',
price: parsed.price || '25€'
};
} catch (error) {
// Fallback en cas d'erreur de parsing
return {
menuItem: 'Plat du jour',
description: text.substring(0, 150),
price: '20€'
};
}
}
);
Comprendre defineFlow
La méthode defineFlow
est le cœur de Genkit. Elle permet de créer un workflow IA réutilisable avec :
name
: Identifiant unique du flow pour le monitoring et les logsinputSchema
: Validation Zod des données d'entrée (sécurité et typage)outputSchema
: Structure attendue de la réponse finalestreamSchema
: Type des données envoyées en streaming (ici du texte)
Comprendre sendChunk
La fonction sendChunk
permet d'envoyer des données en temps réel au client :
- Streaming en temps réel : Chaque morceau de texte généré est immédiatement envoyé
- Meilleure UX : L'utilisateur voit la réponse se construire progressivement
- Réactivité : Pas d'attente de la réponse complète avant affichage
Exposition du Flow côté serveur
Modifions le fichier src/server.ts
pour exposer notre flow avec support du streaming :
typescript
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './main.server';
import { expressHandler } from '@genkit-ai/express';
import { menuSuggestionFlow } from './genkit/menu-suggestion-flow';
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
// Configuration pour les requêtes JSON
server.use(express.json());
// Exposition du flow IA classique (réponse complète)
server.post('/api/menu-suggestion', expressHandler(menuSuggestionFlow));
// Endpoint de streaming pour une expérience temps réel
server.post('/api/menu-suggestion-stream', async (req, res) => {
const { theme } = req.body;
if (!theme) {
return res.status(400).json({ error: 'Theme requis' });
}
// Configuration des headers pour le streaming
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// Utilisation du flow avec streaming
const flowResult = await menuSuggestionFlow({ theme });
// Le streaming est géré automatiquement par Genkit
// Les chunks sont envoyés via sendChunk dans le flow
// Fin du stream
res.end();
} catch (error) {
console.error('Erreur streaming:', error);
res.status(500).end('Erreur lors de la génération');
}
});
const commonEngine = new CommonEngine();
// ... reste de la configuration serveur
Configuration du streaming serveur
Le streaming côté serveur nécessite :
- Headers HTTP appropriés :
Content-Type: text/plain
,Transfer-Encoding: chunked
- Pas de cache :
Cache-Control: no-cache
pour éviter la mise en cache - Connexion persistante :
Connection: keep-alive
pour maintenir le flux
Création de l'interface utilisateur
Service pour l'IA
Créons un service pour gérer les interactions avec notre flow IA. Créez src/app/core/services/ai.service.ts
:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
/**
* Service pour gérer les interactions avec l'IA
*
* Ce service encapsule les appels vers nos flows Genkit et fournit
* une interface simple pour les composants Angular.
*/
@Injectable({
providedIn: 'root'
})
export class AiService {
private http = inject(HttpClient);
/**
* Génère une suggestion de menu basée sur un thème
*
* Cette méthode appelle notre flow Genkit pour obtenir une suggestion
* de plat personnalisée selon les préférences de l'utilisateur.
*
* @param theme - Le thème ou style de cuisine souhaité
* @returns Observable contenant la suggestion générée
*
* @example
* ```typescript
* this.aiService.generateMenuSuggestion('cuisine asiatique fusion')
* .subscribe(suggestion => {
* console.log(suggestion.menuItem);
* });
* ```
*/
generateMenuSuggestion(theme: string): Observable<{
menuItem: string;
description: string;
price: string;
}> {
return this.http.post<{
menuItem: string;
description: string;
price: string;
}>('/api/menu-suggestion', { theme });
}
}
Interface utilisateur avec streaming
Avant tout
Créons l'interface utilisateur avec streaming en temps réel dans src/app/features/menu/menu.component.ts
:
typescript
import { Component, signal, resource, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
/**
* Composant pour la génération de suggestions de menu par IA avec streaming
*
* Ce composant utilise l'API `resource` d'Angular pour afficher les réponses
* de l'IA en temps réel, créant une expérience utilisateur fluide.
*/
@Component({
selector: 'app-menu',
imports: [FormsModule],
template: `
<div class="menu-generator">
<h2>🍽️ Générateur de Menu IA</h2>
<div class="input-section">
<label for="theme">Décrivez vos envies :</label>
<input
id="theme"
type="text"
[(ngModel)]="themeInput"
placeholder="Ex: végétarien épicé, cuisine italienne moderne..."
class="theme-input"
(keyup.enter)="startGeneration()"
/>
<button
(click)="startGeneration()"
[disabled]="menuResource.isLoading() || !themeInput().trim()"
class="generate-btn"
>
@if (menuResource.isLoading()) {
<span class="spinner"></span> Génération...
} @else {
✨ Générer une suggestion
}
</button>
</div>
@if (menuResource.value()) {
<div class="suggestion-card">
<div class="streaming-content">
{{ menuResource.value()!.value }}
@if (menuResource.isLoading()) {
<span class="cursor">|</span>
}
</div>
</div>
}
@if (menuResource.error()) {
<div class="error-message">
❌ Erreur lors de la génération. Veuillez réessayer.
</div>
}
</div>
`
})
export class MenuComponent {
// Signal pour l'input utilisateur
themeInput = signal('');
// Signal pour déclencher la génération
private triggerGeneration = signal<string | null>(null);
/**
* Resource pour gérer le streaming des réponses IA
*
* Cette resource utilise l'API streaming d'Angular pour afficher
* les réponses de l'IA en temps réel, chunk par chunk.
*/
menuResource = resource({
request: () => this.triggerGeneration(),
stream: async ({ request }) => {
if (!request) return null;
// Signal pour accumuler les chunks reçus
const streamData = signal<{ value: string } | { error: unknown }>({
value: "",
});
try {
// Appel à notre API de streaming
const response = await fetch('/api/menu-suggestion-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/plain'
},
body: JSON.stringify({ theme: request })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (!response.body) {
throw new Error('Pas de body dans la réponse');
}
// Lecture du stream avec ReadableStream
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Décodage du chunk et mise à jour du signal
const chunkText = decoder.decode(value, { stream: true });
streamData.update((prev) => {
if ("value" in prev) {
return { value: prev.value + chunkText };
} else {
return { error: chunkText };
}
});
}
} catch (error) {
streamData.set({ error: error });
}
return streamData;
},
});
/**
* Démarre la génération d'une suggestion de menu
*
* Cette méthode déclenche le resource streaming en mettant à jour
* le signal triggerGeneration avec le thème saisi par l'utilisateur.
*/
startGeneration(): void {
const theme = this.themeInput().trim();
if (!theme) {
return;
}
// Déclenche la resource en mettant à jour le signal
this.triggerGeneration.set(theme);
}
}
Comprendre resource
avec streaming
L'API resource
d'Angular est parfaite pour le streaming IA :
request
: Signal qui déclenche la resource quand il changestream
: Fonction asynchrone qui gère le flux de données en temps réel- Réactivité : Mise à jour automatique du template à chaque chunk reçu
- Gestion d'état :
isLoading()
,value()
,error()
automatiquement gérés
Test et développement
Lancement de l'application
Pour tester votre application :
bash
# Définir la clé API
export GEMINI_API_KEY=votre_clé_api
# Lancer l'application
ng serve
Votre application sera accessible sur http://localhost:4200
.
Interface de développement Genkit
Lancez genkit start
dans un autre terminal pour accéder à l'interface de développement Genkit avec les métriques et logs.
Avantages du streaming avec Angular
Le streaming offre plusieurs avantages pour les applications IA :
- Perception de rapidité : L'utilisateur voit immédiatement que quelque chose se passe
- Engagement utilisateur : L'effet "machine à écrire" maintient l'attention
- Gestion des timeouts : Évite les timeouts sur les longues générations
- Feedback progressif : Possibilité d'arrêter si la réponse ne convient pas
Déploiement
Firebase Functions
Pour déployer sur Firebase Functions :
bash
# Installation Firebase CLI
npm install -g firebase-tools
# Initialisation
firebase init functions
# Configuration des secrets
firebase functions:secrets:set GEMINI_API_KEY
# Déploiement
firebase deploy --only functions
Cloud Run
Pour déployer sur Google Cloud Run :
bash
# Build de l'application
ng build
# Création de l'image Docker
docker build -t restaurant-ai-app .
# Déploiement sur Cloud Run
gcloud run deploy restaurant-ai-app \
--image restaurant-ai-app \
--set-secrets GEMINI_API_KEY=gemini-key:latest
Bonnes pratiques
Sécurité
- Jamais de clés API côté client : Toujours utiliser des variables d'environnement
- Validation des entrées : Valider et nettoyer les données utilisateur
- Rate limiting : Implémenter des limites de requêtes pour éviter les abus
Performance
- Mise en cache : Cacher les réponses fréquentes pour réduire les coûts
- Streaming : Utiliser le streaming pour une meilleure UX
- Optimisation des prompts : Créer des prompts précis pour de meilleures réponses
Observabilité
- Logs structurés : Utiliser des logs JSON pour faciliter le monitoring
- Métriques : Suivre les temps de réponse et taux d'erreur
- Interface Genkit : Utiliser l'interface de développement pour le debugging
Gestion des erreurs
Toujours implémenter une gestion d'erreur robuste avec des fallbacks appropriés. Les modèles IA peuvent parfois être indisponibles ou retourner des réponses inattendues.
TIP
Vous avez des exemples sur https://github.com/angular/examples/tree/main