Lesson 5 — Google Apps Script et Gemini

Traitement par lot, menu IA et vérification du script

6 min read

This content is not translated yet. Showing the original version.

Traitement par lot, menu IA et vérification du script

Le menu

onOpen est un déclencheur simple : une fonction au nom réservé que Google lance automatiquement à l'ouverture du classeur.

files/resumer.gs (extrait)
function onOpen() {
  SpreadsheetApp.getUi().createMenu('IA')
    .addItem('Traiter les e-mails', 'traiterEmails')
    .addToUi();
}

Une réponse structurée par e-mail

On demande un JSON conforme à un schéma, avec des listes fermées (enum) :

files/resumer.gs (extrait)
function analyserEmail_(objet, texte) {
  const schema = {
    type: 'OBJECT',
    properties: {
      resume: { type: 'STRING' },
      categorie: { type: 'STRING', enum: CATEGORIES },
      priorite: { type: 'STRING', enum: ['haute', 'normale', 'basse'] },
    },
    required: ['resume', 'categorie', 'priorite'],
  };
  const prompt = 'Tu tries la boîte de réception d\'une petite entreprise. ' + ... + texte;
  return JSON.parse(appelerGemini_(prompt, schema));
}

La boucle sur la feuille

files/resumer.gs (extrait)
const COL = { OBJET: 1, TEXTE: 2, RESUME: 3, CATEGORIE: 4, PRIORITE: 5, STATUT: 6 };
const LIMITE_MS = 5 * 60 * 1000; // on s'arrête avant la limite de 6 min par exécution

function traiterEmails() {
  const debut = Date.now();
  const feuille = SpreadsheetApp.getActive().getSheetByName('Emails');
  const valeurs = feuille.getDataRange().getValues(); // ligne 0 = en-têtes
  let traitees = 0;
  for (let i = 1; i < valeurs.length; i++) {
    if (valeurs[i][COL.STATUT - 1]) continue;        // déjà traité ou validé : on n'y touche pas
    if (Date.now() - debut > LIMITE_MS) break;       // relancez le menu pour continuer
    const r = analyserEmail_(valeurs[i][COL.OBJET - 1], valeurs[i][COL.TEXTE - 1]);
    feuille.getRange(i + 1, COL.RESUME, 1, 4).setValues([[r.resume, r.categorie, r.priorite, 'À valider']]);
    traitees++;
  }
  SpreadsheetApp.getActive().toast(traitees + ' e-mail(s) traité(s). Relisez la colonne Statut.');
}
flowchart TD
  S["Menu IA > Traiter les e-mails"] --> L["Lire toute la feuille en une fois"]
  L --> B{"Ligne suivante ?"}
  B -- "non" --> T["toast : n e-mails traités"]
  B -- "oui" --> St{"Statut rempli ?"}
  St -- "oui" --> B
  St -- "non" --> Tm{"Plus de 5 min écoulées ?"}
  Tm -- "oui" --> T
  Tm -- "non" --> G["analyserEmail_() puis écrire C:F + 'À valider'"]
  G --> B
Choix de conceptionRaison
Lire la feuille en une fois (getValues)chaque lecture de cellule est un appel lent ; une seule lecture est bien plus rapide
Écrire après chaque lignesi le script s'arrête, le travail déjà fait est gardé
Sauter les lignes avec un statutrelancer ne repaie pas ce qui est fait et n'écrase jamais une ligne validée
S'arrêter à 5 minla limite est de 6 min par exécution : on garde une marge
Statut « À valider »aucune action automatique : un humain relit

Vous relancez le menu après qu'il s'est arrêté à la ligne 120 sur 300. Que se passe-t-il ?

Pourquoi écrire le résultat dans la feuille après chaque e-mail, plutôt qu'une seule fois à la fin ?

Vérifier le script hors de Google

Le fichier .gs ne s'exécute que chez Google, mais c'est du JavaScript : on peut le vérifier sur son ordinateur avec Node.js.

  1. Syntaxe : node --check refuse l'extension .gs, on copie donc le fichier en .js. Les appels à SpreadsheetApp, UrlFetchApp… ne gênent pas : --check ne fait que lire le code sans l'exécuter.
  2. Comportement : test_gs.mjs exécute le script avec des doublures des services Google et une feuille « Emails » en mémoire.
terminal
cp resumer.gs /tmp/resumer.js && node --check /tmp/resumer.js && echo "syntaxe OK"
node test_gs.mjs                                    # doublures, sans clé
GEMINI_API_KEY=... REEL=1 node test_gs.mjs          # vrais appels à Gemini via curl
files/test_gs.mjs (extrait)
ctx.traiterEmails()
assert.equal(lignes[1][5], 'À valider')
assert.deepEqual(lignes[3], ['Déjà vu', 'Texte', 'ok', 'information', 'basse', 'Validé'])  // intacte

Exercice : un statut « Erreur »

Aujourd'hui, si Gemini échoue sur une ligne, toute l'exécution s'arrête. Modifiez la boucle pour écrire Erreur : <message> dans la colonne Statut de la ligne fautive et continuer avec la suivante.

Voir la solution
    try {
      const r = analyserEmail_(valeurs[i][COL.OBJET - 1], valeurs[i][COL.TEXTE - 1]);
      feuille.getRange(i + 1, COL.RESUME, 1, 4).setValues([[r.resume, r.categorie, r.priorite, 'À valider']]);
      traitees++;
    } catch (e) {
      feuille.getRange(i + 1, COL.STATUT).setValue('Erreur : ' + String(e.message).slice(0, 80));
    }

Pour retraiter une ligne en erreur, il suffit de vider sa cellule Statut et de relancer le menu.

Résultat obtenu

Vérifications exécutées le 2026-10-15 :

node --check, puis REEL=1 node test_gs.mjs
node --check OK
RESUMER(A2) -> Le client exige d'être remboursé sous 48 heures pour sa commande 1042 cassée, sinon il laissera un avis.
 | Le client demande le remboursement sous 48 heures de sa commande n° 1042 arrivée cassée, sous peine de laisser un avis négatif. | reclamation | haute | À valider
 | Le client demande si l'entreprise est ouverte le samedi matin. | information | basse | À valider
 | ok | information | basse | Validé
2 e-mail(s) traité(s). Relisez la colonne Statut. (3 appels réels)
OK

Trois appels réels : un pour RESUMER (le second, identique, est servi par le cache) et deux pour les e-mails sans statut. La ligne « Validé » n'a pas été touchée.

  • J'ai un menu IA qui traite la feuille Emails
  • Je sais pourquoi le script lit en une fois, écrit à chaque ligne et s'arrête à 5 minutes
  • Je sais vérifier la syntaxe d'un .gs avec node --check
  • Aucune ligne validée n'est jamais écrasée
Next