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 :
- Émission : Envoi d’une requête
POST contenant le prompt à destination du service proxy backend.
- Réception : Traitement de la réponse brute au format
JSON.
- 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.";
}
}
}
}
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 :
- 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".
- 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).
- 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.
- 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