Des formules IA dans Grist ? avec un widget?

Bonjour

Contexte

Je travaille sur l’intégration de l’IA générative dans nos processus métiers à la Haute Autorité de Santé.

Un cas d’usage fréquemment utile est l’extraction d’informations depuis des documents. Une difficulté que nous rencontrons est de pouvoir traiter en parallèle un grand nombre de documents.

Typiquement nous souhaitons travailler dans une table avec

  • 1 ligne par document
  • 1 colonne par information extraite via LLM

Solutions envisagées

Nous avons testé plusieurs outils pour cela, dont Elicit dont voici une capture illustrative ci-dessous.

Ces outils ont une performance assez décevante, pour différentes raisons. D’autres ont l’air bien, mais ont un modèle économique basé sur des licences par utilisateurs très onéreuses, et seront impossibles à généraliser.

Solutions dans d’autres tableurs

AirTable a des “champs IA” qui embarquent plusieurs fonctions. En particulier :

Côté tableurs grands publics, il y a la fonction d’appel à un LLM :

  • Excel a une fonction COPILOT, qui permet de générer un contenu depuis une autre colonne + un prompt.
  • GoogleSheet a une fonction IA qui appelle Gemini.

Mais il est moins clair si on pourrait gérer des pièces jointes et en extraire le contenu.

Est-ce que Grist pourrait être une solution ?

Du coup je me demande si Grist pourrait être un moyen de créer de telles fonctionnalités.

J’imaginerai 2 type de formule,

  • appel à un outil d’extraction du contenu depuis un document PDF joint

  • appel à un LLM avec un prompt + le contenu d’autres colonnes

C’est sans doute quelque chose qui devrait être développé dans le cœur de Grist. Je vois 2 issues à ce sujet, celle-ci étant bien construite, mais n’a pas retenu d’attention.

Est-ce que l’on pourrait imaginer le faire via un mécanisme de widget ?

Cela me paraît très ambitieux, mais je ne maîtrise pas les widgets Grist, ni à quels points ils peuvent porter une telle complexité.

Bonjour @pierrealain

Je n’ai pas une réponse « complète et abouti », mais je peux te partager des éléments de réflexion, que nous avions initié pour doter nos widget Grist de fonctionnalité IA.

Cas d’usage : reformulation de texte par IA

Pour simplifier le propos, imaginons le développement d’une fonctionnalité dédiée à la reformulation automatisée de contenu textuel. Le principe est d’optimiser un texte initial saisi par un utilisateur en exploitant la puissance d’un grand modèle de langage (LLM).

  • En entrée : Un texte brut soumis par un utilisateur et stocké dans une table
  • Traitement : La soumission de ce texte à l’IA, guidée par un prompt cadre (un modèle de consigne commun).
  • En sortie : La génération automatique de plusieurs propositions de reformulation distinctes, offrant ainsi un choix de variantes à l’utilisateur.

Et autant faire de la pub à la solution souveraine de la DINUM Albert IA, comme choix d’architecture pour ce service d’IA génératif.


Première solution : développement d’un service web dédié

Conception d’un service proxy backend à un LLM (API Albert)

En premier, le but est de concevoir et de déployer un service intermédiaire (API Gateway / Proxy) faisant office de couche d’abstraction et de sécurité entre les applications front-end que l’on veut construire dans Grist (exemple : un widget) et l’API Albert.

Remarque : Techniquement, il est tout aussi possible de consommer directement l’API Albert sans passer par la conception d’un proxy backend. Mais dans ce cas, on est limité à la simple interrogation des modèles de l’API du LLM… sans autre fonctionnalité.

De plus, dans le cas d’une architecture sans proxy, je ne vois pas comment gérer les flux entre le serveur d’Albert et l’instance Grist où l’on ne disposerait pas des droits d’administration de la plateforme (par example, sur une offre SaaS ou une instance institutionnelle partagée). Et l’autre point d’attention, toujours dans le cas d’une archi sans proxy, concerne l’exposition de la clé d’API au service LLM. Il faut la cacher, et ne surtout pas l’inscrire dans le code côté client.

Plutôt que de consommer directement l’API Albert, l’utilisation d’un proxy backend offre les fonctionnalités suivantes :

  • Sécurisation token d’accès: Centralisation et isolation de la clé d’accès (API Key) côté serveur pour empêcher toute exposition sur le client.
  • Contrôle des accès (CORS) : Gestion stricte des politiques Cross-Origin Resource Sharing pour n’autoriser que les domaines de confiance.
  • Régulation personnalisé du trafic (Rate Limiting) : Limitation du nombre de requêtes pour protéger l’infrastructure, maîtriser les quotas et garantir la disponibilité du service, selon ses propres critères.
  • Consommation fluide : Standardisation des flux d’échanges pour faciliter l’intégration de l’IA générative dans vos outils métiers.
  • Et finalement pouvoir faire évoluer le service à ses propres besoins.

Ce microservice, par nature léger et hautement disponible, peut être développé dans l’écosystème de son choix : Go, Python, Java ou PHP.

Voila une séquence typique.

Conception d’une application de test (mock client)

Avant d’intégrer la consommation de ce service backend directement dans l’écosystème Grist, une phase de test et de validation technique est nécessaire. Pour ce faire, on créer une application web minimaliste (sous la forme d’un simple formulaire) faisant office de client de test.

Ce client se compose d’un formulaire simplifié permettant de simuler l’expérience utilisateur :

  1. Émission : Envoi d’une requête POST contenant le prompt à destination du service proxy backend.
  2. Réception : Traitement de la réponse brute au format JSON.
  3. Rendu : Intégration d’un moteur de rendu Markdown pour convertir la réponse textuelle de l’IA en un affichage HTML propre et structuré.

Ci-dessous un exemple (simpliste et raccourci) d’une implémentation de ce formulaire web en PHP.

<form method="POST">
    <div class="prompt-wrapper">
        <label for="prompt" class="prompt-label">Prompt</label>
        <textarea
            name="prompt"
            id="prompt"
            class="prompt-area"
            placeholder="Write your prompt for ALBERT API here…"
        ><?php echo isset($_POST['prompt']) ? htmlspecialchars($_POST['prompt']) : ''; ?></textarea>
    </div>
    <button type="submit" class="btn">⚡ Send request</button>
</form>

Et de son traitement. La consommation du service proxy backend se fait ici à l’aide d’un simple appel curl. La requête HTTP se fait en POST dans laquelle est injecté le prompt, écrit dans le textarea.

// test-proxy-backend-albert-api.php

$response = null;
$error = null;
$aiMessage = null;
$rawJson = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $prompt = isset($_POST['prompt']) ? trim($_POST['prompt']) : '';

    if ($prompt === '') {
        $error = "The prompt cannot be empty.";
    } else {
        $proxyUrl = "https://my-server.com/proxy-backend-albert-api.php";
        $postFields = "prompt=" . urlencode($prompt);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $proxyUrl);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $rawJson = curl_exec($ch);
        $curlErr = curl_error($ch);
        curl_close($ch);

        if ($curlErr) {
            $error = "cURL error: " . $curlErr;
        } else {
            // Decode JSON
            $decoded = json_decode($rawJson, true);
            if (is_array($decoded) && isset($decoded["choices"][0]["message"]["content"])) {
                $aiMessage = $decoded["choices"][0]["message"]["content"];
            } else if (is_array($decoded) && isset($decoded["error"])) {
                $error = $decoded["error"];
            }
            else {
                $aiMessage = "Unable to extract AI message.";
            }
        }
    }
}

:warning: Attention Ce code est un POC (proof of concept) destiné à un environnement local et de test. L’utiliser tel quel en production présenterait de graves failles de sécurité. Pour une mise en production, il serait nécessaire

  • d’intégrer un token CSRF (unique et éphémère) lié à la session de l’utilisateur.
  • de nettoyer (sanitization) du prompt envoyé en POST pour éviter des injections de scripts (XSS) lors de l’affichage du résultat.
  • de mettre en place des protections contre le Prompt Injection au modèle IA Albert sous-jacent
  • de proteger l’accès au service proxy back-end lui même. En production, le proxy backend lui-même doit exiger une preuve d’identité de la part du client (un token JWT, une session valide ou une clé d’application interne) afin de s’assurer que seules vos applications autorisées (comme vos widgets Grist) consomment les ressources et les quotas d’Albert de votre service back-end. La dans l’exexemple, c’est un proxy anonyme open-bar !

Passage à une implémentation dans Grist

Une fois la chaîne de communication validée par notre client de test, se pose la question de la faisabilité dans Grist. Deux voies semblent envisageables.

Approche par formule Python ?

L’idée sous-jacente est d’exécuter la requête HTTP directement lors du calcul d’une cellule, à la manière d’une formule Excel augmentée. En Python pur, effectuer un appel réseau (l’équivalent d’un cURL) est trivial via des bibliothèques standards ou tierces (comme urllib, requests ou httpx). Cependant, pour des raisons de sécurité évidentes, Grist exécute le code Python de ses formules dans un environnement isolé et standardisé (Sandbox Python)… et pas sur que par défaut, cette Sandbox est un accès au réseau sortant.

Il semblerait naturel qu’elle bloque toute tentative de communication avec l’extérieur afin d’éviter la fuite de données ou l’exécution de scripts malveillants. Les modules permettant d’ouvrir des sockets réseau y sont désactivés et certainement sévèrement bridés.

Mais ce n’est pas impossible, via une configuration réseau très spécifique et permissive (seul l’accès à ce serveur distant autorisé pour ce service).

Voila à quoi ressembler une formule Python (je n’ai pas testé ce code, et comme dit précedemment, il nécessite une amélioration plus que notable du point de vue de la sécurité - proxy anonyme, contrôle du prompt en colonne…)

import json
import urllib.request
import urllib.parse

def request_albert_proxy(texte_source):
    if not texte_source or not str(texte_source).strip():
        return "En attente d'un texte à reformuler..."

    proxy_url = "https://my-server.com/proxy-backend-albert-api.php"
    
    # Construction du prompt métier pour Albert IA
    prompt_consigne = (
        "Agis en tant qu'expert en communication institutionnelle. "
        "Propose 3 reformulations différentes, claires et professionnelles du texte suivant :\n\n"
    )
    prompt_final = f"{prompt_consigne}{texte_source}"
    
    # Encodage des données du formulaire (POST)
    data = urllib.parse.urlencode({"prompt": prompt_final}).encode("utf-8")
    
    # Configuration de la requête avec un timeout de 15 secondes
    req = urllib.request.Request(proxy_url, data=data, method="POST")
    req.add_header("Content-Type", "application/x-www-form-urlencoded")
    
    try:
        # Appel synchrone du proxy backend
        with urllib.request.urlopen(req, timeout=15) as response:
            html_content = response.read().decode("utf-8")
            
        # Décodage du flux JSON retourné par le proxy
        decoded = json.loads(html_content)
        
        # Extraction du message de l'IA (Format standardisé)
        if isinstance(decoded, dict) and "choices" in decoded:
            return decoded["choices"][0]["message"]["content"]
        elif isinstance(decoded, dict) and "error" in decoded:
            return f"Erreur Albert : {decoded['error']}"
        else:
            return "Erreur : Format JSON renvoyé par le proxy invalide."
            
    except urllib.error.URLError as e:
        return f"Erreur de communication (Sandbox/Réseau) : {e.reason}"
    except Exception as e:
        return f"Exception levée lors de l'appel : {str(e)}"

# Exécution de la fonction en passant la colonne contenant le texte d'origine
return request_albert_proxy($Texte_Initial)

Approche par Custom widget

C’est la solution la plus simple. Techniquement, un widget Grist est une simple application web (souvent conforme à l’architecture Single Page Application) embarquée dans une iframe. C’est donc le même mécanisme (requete POST) que le client de test.

Le widget reproduit exactement la logique validée précédemment. Il utilise l’API native du navigateur (via un fetch, ou des requêtes XHR) pour envoyer le prompt et recevoir la réponse JSON du proxy backend.

De plus, le code du widget s’exécute dans le navigateur de l’utilisateur final, et non dans la Sandbox Python du serveur Grist. Il bénéficie donc d’un accès complet au réseau Internet pour dialoguer avec votre proxy.

const handleCallAI = async () => {
    const apiUrl = "https://my-server.com/proxy-backend-albert-api.php";
    const promptForAPI = buildPromptForAPI();

    try {
         const formData = new FormData();
         formData.append("apiKey", apiKey);
         formData.append("prompt", promptForAPI);
         const response = await fetch(apiUrl, {
             method: "POST",
             body: formData
         });

         if (!response.ok) {
                  throw new Error(`Erreur API ALBERT: ${response.status}`);
         }

         const data = await response.json();
         const aiText = data?.choices?.[0]?.message?.content?.trim() || '';
         console.log(aiText);
    } catch (err) {
        console.error(err);
    }

Seconde solution : déporté le traitement IA dans un process ETL (ApacheHOP, n8n, make…) extérieur à Grist

Une autre idée, consiste à déporter le traitement de l’IA vers un outil d’intégration (ETL) de traitement de données extérieur à Grist. Certainement plus simple à mettre en œuvre, et ne mobilise pas de compétences de développement directe.

Dans cette architecture, Grist ne sert plus que de datalake et d’interface de saisie.

Une solution comme n8n, ou Apache Hop prend alors le relais pour orchestrer les appels vers son proxy Albert (si on a conçu des fonctionnalités avancées) ou vers la consommation de l’API Albert directement (si on a juste besoin de consommer les modèles fournis par Albert).

La consommation au service de l’IA peut se faire manuellement (déclenchement par un utilisateur) ou même automatisé via webhook Grist. Cela peut s’organiser en 4 étapes :

  1. La saisie (dans Grist) : L’utilisateur saisit son texte dans une colonne Grist. Par défaut, une colonne de statut passe à "En attente de reformulation IA".
  2. Le déclencheur (Trigger = webhook Grist) : L’ETL détecte cette nouvelle ligne (via un Webhook instantané. Il est aussi possible de configurer une tâche planifiée par Cronicle/Cron pour procéder à des traitements par lots une fois par heure ou par jour).
  3. Le traitement IA (dans l’ETL) : L’ETL extrait le texte, forge le prompt, appelle le proxy sécurisé d’Albert IA, reçoit les 3 propositions et nettoie le résultat. Dans n8n ou Apache HOP, cela revient à configurer une étape (client HTTP Post). Pour chaque ligne (id) envoyé à l’ETL par webhook, ou lu dans la table, l’ETL effectue une requête POST vers l’IA Albert.
  4. La mise à jour (dans Grist) : L’ETL réinjecte les propositions IA directement dans la ligne Grist via l’API REST de Grist, et bascule le statut de la colonne à "Traitement IA Terminé".

Plus d’info sur n8n & Grist

1 « J'aime »

Wow, merci pour cette réponse extrêmement complète !

Précision importante sur mon besoin : j’aimerais que les utilisateurs finaux non techniques puissent facilement créer de nouvelles colonnes IA, en définissant un prompt + les colonnes sources, avec la même flexibilité que les solutions des autres tableurs.

La solution de la formule Python telle qu’indiquée a l’air très bien, mais serait trop complexe pour des utilisateurs finaux tant qu’elle n’est pas intégrée dans le cœur de Grist via une fonction type =AI(prompt et colonnes). Le cœur gérerait alors également l’aspect backend (orchestration, appel API, clés d’API, etc.)

Je n’avais pas pensé au traitement déporté par ETL. C’est un peu ce que l’on a fait dans un projet où l’objectif est d’extraire de nombreuses informations prédéfinies depuis de nombreux documents. Mais on est allé trop loin dans l’usage de Grist, et on va plutôt redévelopper une vrai application web dédiée.

Pour les solutions widget et/ou le traitement déporté par ETL, je me demande comment on pourrait laisser la flexibilité à l’utilisateur non technique de créer ses prompts.

Est-ce qu’on pourrait imaginer un template Grist qui aurait :

  • une table principale avec une ligne par document : colonne pièce jointe, colonne texte extrait en markdown, N colonnes IA initialement vides (voir 2N si on ajoute une colonne pour la source /justification des données extraites)
  • une table de paramétrage des prompts pour chaque colonne IA
  • un/des widgets pour servir d’interface
    • de paramétrage des colonnes IA
    • de chargement des pièces jointes
    • de lancement des actions IA (dans le front client ou des webhooks)

Bonjour,

Je peux vous présenter la solution que nous avons trouvée, ce sera peut-être une piste pour vous :

Nous extrayons des données de documents pdf qui sont déposés dans un répertoire Nextcloud de notre SI. Tout est auto-hébergé, sauf Grist car nous nous servons de l’instance Dinum. Nous ajoutons une brique d’automatisation n8n, qui va être le moteur du workflow.

  1. un agent dépose un document pdf dans un dossier nextcloud
  2. n8n scanne le contenu de ce dossier est déclenche un flux pour chaque document trouvé
  3. un agent IA est créé dans n8n, il est alimenté par une LLM relativement modeste (Gemma 4, en modèle openAI)
  4. l’agent extrait les informations grâce à un prompte bien structuré et un outil de parsing
  5. il construit une réponse, dans notre cas un tableau json contenant les infos recherchées
  6. n8n crée une ligne dans le tableau grist par ligne du tableau json créé par l’IA

Ce modèle peut être facilement adapté pour prendre les pj d’une ligne d’un tableau Grist au lieu de scanner un dossier Nextcloud, et il pourra, par exemple facilement mettre à jour la ligne d’origine plutôt que d’en créer une nouvelle. On pourrait aussi faire travailler l’agent IA à partir d’un prompte que l’usager écrit dans une colonne du tableau Grist

Avec cette méthode, il n’y a pas de complexité ajoutée côté utilisateur, c’est la config du webhook qui permet le déclenchement, y compris sur plusieurs documents en parallèle.

J’espère vous avoir été utile.

Je vous souhaite une bonne journée.

1 « J'aime »