Intelligence Artificielle angularforall.com

- Gemini API : premiers pas avec Google AI

GeminiGoogle-AiApiLlmGemini-ApiMultimodalIa-GenerativeVision-IaChatbotJavascriptPrompt-EngineeringGoogle-CloudAi-Studio
Gemini API : premiers pas avec Google AI

Configurez l'API Gemini de Google : génération de contenu, chat conversationnel et capacités multimodales texte/image pour enrichir vos applications web.

Installation et configuration

Google propose deux SDKs : @google/generative-ai pour les projets JavaScript/TypeScript (Node.js et navigateur) et le SDK Python pour les usages backend. Pour les applications Angular, le SDK JS/TS est adapté.

npm install @google/generative-ai

# TypeScript : les types sont inclus dans le package
# Node.js 18+ requis (fetch API native)
// config/gemini.ts — configuration centralisée
import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from '@google/generative-ai';

// Ne jamais exposer la clé API côté client !
// En Node.js/Angular SSR : utiliser les variables d'environnement
// En Angular client-side : passer par un backend proxy
const API_KEY = process.env['GEMINI_API_KEY'] ?? '';

const genAI = new GoogleGenerativeAI(API_KEY);

// Configuration par défaut partagée
export function getGeminiModel(modelName = 'gemini-2.0-flash') {
  return genAI.getGenerativeModel({
    model: modelName,
    // Safety settings : contrôler la modération du contenu
    safetySettings: [
      {
        category: HarmCategory.HARM_CATEGORY_HARASSMENT,
        threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
      },
      {
        category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
        threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
      }
    ]
  });
}
Sécurité clé API : Ne jamais inclure la clé API dans un bundle Angular client-side (elle sera visible dans les DevTools). Utiliser un endpoint backend (Node.js, Cloud Functions, Firebase Functions) qui proxifie les requêtes vers l'API Gemini.

generateContent() — requête simple

generateContent() envoie un prompt et attend la réponse complète. C'est le mode non-streaming — adapté aux tâches courtes où la latence totale n'est pas critique.

import { getGeminiModel } from './config/gemini';

const model = getGeminiModel('gemini-2.0-flash');

// Forme simple : string
const result = await model.generateContent('Explique REST en 3 phrases.');
console.log(result.response.text());

// Forme complète avec system instruction
const result2 = await model.generateContent({
  contents: [{
    role: 'user',
    parts: [{ text: 'Génère un composant Angular pour un formulaire de login.' }]
  }],
  systemInstruction: {
    role: 'system',
    parts: [{ text: 'Tu es un expert Angular 18. Utilise les Signals et standalone components. Commente chaque partie importante du code.' }]
  }
});

console.log(result2.response.text());

// Métadonnées de l'usage (important pour surveiller les coûts)
const usage = result2.response.usageMetadata;
console.log(`Tokens: ${usage.promptTokenCount} input, ${usage.candidatesTokenCount} output`);

// Vérifier si la réponse a été bloquée par les safety settings
const candidate = result2.response.candidates[0];
if (candidate.finishReason !== 'STOP') {
  console.warn('Réponse incomplète:', candidate.finishReason);
  // Valeurs possibles: STOP, MAX_TOKENS, SAFETY, RECITATION, OTHER
}

generationConfig — contrôler la sortie

generationConfig contrôle comment le modèle génère sa réponse : la température (créativité), la longueur maximale, les formats de sortie structurés.

// generationConfig complet avec explication de chaque paramètre
const config = {
  generationConfig: {
    temperature: 0.7,
    // 0.0 = déterministe (toujours la même réponse)
    // 1.0 = créatif et varié
    // Pour extraction de données: 0.0-0.2
    // Pour contenu créatif: 0.7-1.0

    topP: 0.95,
    // Noyau de probabilité cumulée pour la sélection des tokens
    // 0.95 = considère les tokens jusqu'à 95% de probabilité cumulée

    topK: 40,
    // Limite le pool de tokens candidats aux K plus probables
    // topK=1 = greedy decoding (même effet que temperature=0)

    maxOutputTokens: 2048,
    // Limite la longueur de la réponse (1 token ≈ 4 caractères)
    // Gemini 2.0 Flash : max 8192 tokens output

    candidateCount: 1,
    // Nombre de réponses alternatives générées (1-8)
    // candidateCount > 1 multiplie les coûts proportionnellement

    stopSequences: ['---END---'],
    // Si le modèle génère ces séquences, la génération s'arrête

    responseMimeType: 'application/json',
    // 'text/plain' (défaut) ou 'application/json'
    // Avec 'application/json', Gemini garantit un JSON valide en sortie

    responseSchema: {
      // JSON Schema pour structurer la sortie (si responseMimeType: 'application/json')
      type: 'object',
      properties: {
        title: { type: 'string' },
        tags: { type: 'array', items: { type: 'string' } },
        difficulty: { type: 'string', enum: ['beginner', 'intermediate', 'advanced'] }
      },
      required: ['title', 'tags', 'difficulty']
    }
  }
};

// Extraction JSON structurée garantie
const model = getGeminiModel();
const result = await model.generateContent({
  contents: [{ role: 'user', parts: [{ text: 'Analyse cet article Angular: [contenu]' }] }],
  ...config
});

const parsed = JSON.parse(result.response.text());
// parsed.title, parsed.tags, parsed.difficulty sont garantis présents et typés

Session de chat multi-tours

startChat() crée une session conversationnelle qui maintient l'historique automatiquement. Chaque appel à sendMessage() envoie le message avec tout l'historique précédent, permettant des échanges contextuels.

const model = getGeminiModel();

const chat = model.startChat({
  history: [
    // Historique optionnel pour démarrer avec du contexte
    { role: 'user',  parts: [{ text: 'Je travaille sur une app Angular de e-commerce.' }] },
    { role: 'model', parts: [{ text: 'Super ! Je suis là pour t\'aider sur ton projet e-commerce Angular. Que veux-tu développer ?' }] }
  ],
  generationConfig: {
    temperature: 0.7,
    maxOutputTokens: 2000
  },
  systemInstruction: {
    role: 'system',
    parts: [{ text: 'Tu es un assistant développeur spécialisé Angular. Sois concis et pratique. Inclus toujours des exemples de code.' }]
  }
});

// Premier message de l'utilisateur
const r1 = await chat.sendMessage('Comment implémenter un panier d\'achat avec les Signals ?');
console.log(r1.response.text());

// Le contexte est conservé — Gemini sait qu'on parle d'Angular e-commerce
const r2 = await chat.sendMessage('Et pour persister le panier dans localStorage ?');
console.log(r2.response.text());

// Accéder à l'historique complet
const history = await chat.getHistory();
console.log(`${history.length} messages dans l'historique`);

// Pattern Angular Service pour encapsuler une session de chat
@Injectable({ providedIn: 'root' })
export class GeminiChatService {
  private chat = signal<ChatSession | null>(null);
  private messages = signal<ChatMessage[]>([]);

  initChat(systemPrompt: string) {
    const model = getGeminiModel('gemini-2.0-flash');
    const session = model.startChat({
      systemInstruction: { role: 'system', parts: [{ text: systemPrompt }] }
    });
    this.chat.set(session);
    this.messages.set([]);
  }

  async sendMessage(text: string): Promise<string> {
    const session = this.chat();
    if (!session) throw new Error('Chat not initialized');

    this.messages.update(msgs => [...msgs, { role: 'user', content: text }]);

    const result = await session.sendMessage(text);
    const responseText = result.response.text();

    this.messages.update(msgs => [...msgs, { role: 'assistant', content: responseText }]);
    return responseText;
  }

  getMessages = this.messages.asReadonly();
}

Streaming — réponse progressive

generateContentStream() retourne un flux de chunks de texte. Essentiel pour les interfaces conversationnelles où l'utilisateur doit voir la réponse s'écrire en temps réel plutôt qu'attendre plusieurs secondes.

// Node.js / Backend
const model = getGeminiModel('gemini-2.5-pro');

const stream = await model.generateContentStream(
  'Explique en détail le pattern Repository en Angular avec des exemples.'
);

let fullText = '';
process.stdout.write('\nRéponse: ');

for await (const chunk of stream.stream) {
  const chunkText = chunk.text();
  process.stdout.write(chunkText);  // affichage progressif
  fullText += chunkText;
}

const finalResponse = await stream.response;
console.log('\n\nTokens utilisés:', finalResponse.usageMetadata);

// Angular : intégrer le streaming dans un composant
@Component({
  template: `
    <div class="chat-response">
      {{ streamingResponse() }}
      @if (isStreaming()) {
        <span class="cursor-blink">▊</span>
      }
    </div>
  `
})
export class ChatComponent {
  streamingResponse = signal('');
  isStreaming = signal(false);

  async sendStreamingMessage(prompt: string) {
    this.streamingResponse.set('');
    this.isStreaming.set(true);

    const model = getGeminiModel();
    const stream = await model.generateContentStream(prompt);

    for await (const chunk of stream.stream) {
      this.streamingResponse.update(text => text + chunk.text());
      // Chaque update déclenche un re-render Angular (mode zoneless)
    }

    this.isStreaming.set(false);
  }
}

Multimodal — images et documents

Gemini est natif multimodal — il peut analyser des images, des PDF et même des vidéos dans un même prompt. La fenêtre de 1M tokens permet d'envoyer des documents entiers.

import fs from 'fs';

const model = getGeminiModel('gemini-2.0-flash');

// Analyser une image locale
const imageData = {
  inlineData: {
    data: fs.readFileSync('./mockup.png').toString('base64'),
    mimeType: 'image/png'
  }
};

const r1 = await model.generateContent([
  imageData,
  { text: 'Analyse ce mockup UI et génère le composant Angular correspondant avec Bootstrap 5.' }
]);
console.log(r1.response.text());

// Analyser un PDF (via File API pour les gros fichiers)
import { GoogleAIFileManager } from '@google/generative-ai/server';

const fileManager = new GoogleAIFileManager(API_KEY);

// Upload du PDF (jusqu'à 20 MB en inline, 2 GB via File API)
const uploadResult = await fileManager.uploadFile('./contrat.pdf', {
  mimeType: 'application/pdf',
  displayName: 'Contrat client'
});

const r2 = await model.generateContent([
  {
    fileData: {
      mimeType: uploadResult.file.mimeType,
      fileUri: uploadResult.file.uri  // URI retourné par l'upload
    }
  },
  { text: 'Extrais les clauses importantes de ce contrat au format JSON structuré.' }
]);

console.log(JSON.parse(r2.response.text()));

// Multiple images dans un même prompt
const r3 = await model.generateContent([
  { text: 'Compare ces deux designs UI et explique les différences :' },
  { inlineData: { data: img1Base64, mimeType: 'image/jpeg' } },
  { text: 'VS' },
  { inlineData: { data: img2Base64, mimeType: 'image/jpeg' } }
]);

Function calling — intégrer des APIs

Le function calling permet à Gemini de décider d'appeler des fonctions de ton code pour obtenir des données réelles. Gemini retourne les arguments, ton code exécute la fonction, et tu renvoies le résultat à Gemini qui génère la réponse finale.

import { FunctionDeclarationSchemaType } from '@google/generative-ai';

// Déclarer les fonctions disponibles pour Gemini
const tools = [{
  functionDeclarations: [
    {
      name: 'getProductInfo',
      description: 'Retourne les informations d\'un produit par son ID ou son nom',
      parameters: {
        type: FunctionDeclarationSchemaType.OBJECT,
        properties: {
          productId: { type: FunctionDeclarationSchemaType.STRING, description: 'ID du produit' },
          includeReviews: { type: FunctionDeclarationSchemaType.BOOLEAN, description: 'Inclure les avis' }
        },
        required: ['productId']
      }
    },
    {
      name: 'searchProducts',
      description: 'Recherche des produits par catégorie et mots-clés',
      parameters: {
        type: FunctionDeclarationSchemaType.OBJECT,
        properties: {
          query: { type: FunctionDeclarationSchemaType.STRING },
          category: { type: FunctionDeclarationSchemaType.STRING },
          maxResults: { type: FunctionDeclarationSchemaType.NUMBER }
        },
        required: ['query']
      }
    }
  ]
}];

// Implémentations locales des fonctions
const functionImplementations = {
  getProductInfo: async ({ productId, includeReviews }) => {
    const product = await productService.getById(productId);
    const reviews = includeReviews ? await reviewService.getByProduct(productId) : [];
    return { ...product, reviews };
  },
  searchProducts: async ({ query, category, maxResults = 10 }) => {
    return await productService.search({ query, category, limit: maxResults });
  }
};

// Pipeline function calling
const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash', tools });

async function chatWithTools(userMessage: string) {
  let response = await model.generateContent(userMessage);
  let candidate = response.response.candidates[0];

  // Boucle function calling : Gemini peut appeler plusieurs fonctions
  while (candidate.content.parts.some(p => p.functionCall)) {
    const functionCallParts = candidate.content.parts.filter(p => p.functionCall);
    const functionResults = await Promise.all(
      functionCallParts.map(async part => {
        const { name, args } = part.functionCall;
        const result = await functionImplementations[name](args);
        return { functionResponse: { name, response: { content: result } } };
      })
    );

    // Envoyer les résultats à Gemini pour la réponse finale
    response = await model.generateContent({
      contents: [
        { role: 'user', parts: [{ text: userMessage }] },
        { role: 'model', parts: candidate.content.parts },
        { role: 'function', parts: functionResults }
      ]
    });
    candidate = response.response.candidates[0];
  }

  return response.response.text();
}

// Exemple : "Montre-moi les 3 meilleures laptops sous 1000€ avec leurs avis"
// → Gemini appelle searchProducts({ query: 'laptop', maxResults: 3 })
// → Gemini appelle getProductInfo({ productId: '...', includeReviews: true }) x3
// → Gemini génère une réponse structurée avec les vraies données

Embeddings — recherche sémantique

Les embeddings Gemini transforment du texte en vecteurs numériques (768 dimensions) qui capturent le sens sémantique. Deux textes proches sémantiquement auront des vecteurs proches dans cet espace.

// Modèle d'embedding dédié (plus rapide et moins cher que les modèles de génération)
const embeddingModel = genAI.getGenerativeModel({ model: 'text-embedding-004' });

// Générer un embedding
async function embed(text: string): Promise<number[]> {
  const result = await embeddingModel.embedContent(text);
  return result.embedding.values;  // tableau de 768 numbers
}

// Similarité cosinus entre deux vecteurs
function cosineSimilarity(a: number[], b: number[]): number {
  const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
  const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val ** 2, 0));
  const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val ** 2, 0));
  return dotProduct / (magnitudeA * magnitudeB);
}

// Système de recherche sémantique dans une FAQ
const faqItems = [
  { id: 1, question: 'Comment réinitialiser mon mot de passe ?' },
  { id: 2, question: 'Quels sont les délais de livraison ?' },
  { id: 3, question: 'Comment retourner un article ?' }
];

// Indexation (à faire une fois, résultats à cacher)
const faqEmbeddings = await Promise.all(
  faqItems.map(async item => ({
    ...item,
    embedding: await embed(item.question)
  }))
);

// Recherche sémantique
async function findRelevantFAQ(userQuery: string) {
  const queryEmbedding = await embed(userQuery);

  const scores = faqEmbeddings.map(item => ({
    ...item,
    score: cosineSimilarity(queryEmbedding, item.embedding)
  }));

  return scores.sort((a, b) => b.score - a.score)[0];
}

// Exemple : "je n'arrive pas à me connecter" → trouve "Comment réinitialiser mon mot de passe ?"
const result = await findRelevantFAQ('je n\'arrive pas à me connecter');
console.log(result.question, result.score);  // → "Comment réinitialiser mon mot de passe ?" 0.87

Gestion des erreurs et retry

// Types d'erreurs Gemini API
// 400 Bad Request : prompt ou paramètres invalides
// 403 Forbidden : clé API invalide ou quotas dépassés
// 429 Too Many Requests : rate limiting
// 500 Internal Server Error : erreur côté Google (rare)

// Wrapper avec retry exponentiel pour la 429
async function generateWithRetry(
  model: GenerativeModel,
  prompt: string,
  maxRetries = 3
): Promise<string> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const result = await model.generateContent(prompt);

      // Vérifier si la réponse a été bloquée
      const candidate = result.response.candidates?.[0];
      if (!candidate || candidate.finishReason === 'SAFETY') {
        throw new Error(`Réponse bloquée: ${candidate?.finishReason}`);
      }

      return result.response.text();

    } catch (error: unknown) {
      const isRateLimit = error instanceof Error &&
        error.message.includes('429');
      const isLastAttempt = attempt === maxRetries - 1;

      if (isRateLimit && !isLastAttempt) {
        const delay = Math.pow(2, attempt) * 1000;  // 1s, 2s, 4s
        console.log(`Rate limit, retry dans ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }
  throw new Error('Max retries reached');
}

Modèles et tarification 2025

ModèleContexteTarif inputTarif outputCas d'usage
gemini-2.0-flash1M tokens$0.075/1M$0.30/1MUsage général, multimodal, rapide
gemini-2.5-pro1M tokens$1.25/1M$10.00/1MRaisonnement avancé, code complexe
gemini-2.5-flash1M tokens$0.15/1M$0.60/1MÉquilibre qualité/coût
text-embedding-0042048 tokens$0.00/1M (gratuit)N/AEmbeddings pour RAG, recherche sémantique
Tier gratuit AI Studio : 15 requêtes/minute, 1 million de tokens/minute, 1 500 requêtes/jour pour Gemini 2.0 Flash — suffisant pour prototyper et développer. Passer à Vertex AI (facturation à l'usage) pour la production.
Contexte 1M tokens : 1 million de tokens correspond à environ 750 000 mots — l'équivalent d'un roman de 3000 pages. Cela permet d'analyser des codebases entières, des rapports annuels complets, ou des historiques de conversations très longs sans découpage.

Partager