Administrer les formulaires par l’API — le formulaire est un contenu, pas une table

Troisieme notebook de la serie « AI Engine par son API ». Apres le socle (grain 1) et les chatbots (grain 2), une autre fonctionnalite coeur : les formulaires (AI Forms). Ou le grain 2 montrait un chatbot configurable comme document JSON global, ce notebook revele une architecture differente dans le meme plugin : un formulaire AI Engine est un custom post type — un contenu WordPress (mwai_form) — manipule par un CRUD unitaire, dont le corps est du contenu Gutenberg et dont le rendu public passe par un shortcode.

Le cycle complete est demontre par l’API : lister, creer (une coquille vide), remplir et publier, rendre public dans une page, supprimer. Et une limite honnete, mesuree : ce que la version gratuite rend, et ce qui reste derriere la version Pro.

Les sorties de ce notebook proviennent d’une execution reelle contre l’instance locale (voir « Provenance et limites », en fin de fichier). Cette nature de « contenu » n’est pas une metaphore : un mwai_form herite de la mecanique des articles WordPress. Un formulaire possede un identifiant numerique alloue par la base (comme un article), un statut (draft, publish) qui conditionne son rendu public, et un corps serialize dans le meme format que le corps d’un article — des blocs Gutenberg. Tout ce que l’administration WordPress sait faire d’un contenu (le brouillonner, le publier, le rejeter) s’applique donc a un formulaire, et l’API d’AI Engine expose exactement ce vocabulaire editorial : create alloue, update ecrit, delete supprime — un CRUD unitaire par identifiant, jamais une operation en masse.

La consequence pratique pour l’administrateur : la ou la configuration des chatbots se manipule comme un document JSON global (remplacer tout pour modifier un detail), un formulaire se manipule comme une page — on vise un id, on ecrit, on relit. C’est le fil rouge des manipulations ci-dessous : chaque ecriture est suivie d’une relecture qui prouve la persistance, et le deroule suit le cycle de vie complet — lister, creer une coquille, ecrire et publier, rendre public, nettoyer — pour que chaque geste administratif soit vu au moins une fois, avec sa verification. Le statut, precisement, est ce qui rend le formulaire pilotable comme un contenu : draft le tient hors du rendu public (la page porteuse existerait, le shortcode ne rendrait rien d’interactif), publish le met en circulation. Le cycle ci-dessous le montre en action — la coquille nait draft avec un contenu vide, devient publish quand le contenu est ecrit, et la verification finale relit le statut persiste pour le prouver.

Pourquoi administrer par l’API plutot que par l’ecran

La question merite d’etre posee avant la premiere requete : tout ce que fait ce notebook est faisable dans l’ecran d’administration du plugin — creer un formulaire, ecrire son contenu, le publier, l’inserer dans une page. Faire la meme chose par l’API coute plus cher a l’execution et moins cher partout ailleurs. Trois differences concretes, toutes observables dans la suite.

Rejouabilite. Chaque cellule de ce notebook est du code : elle se relance, se copie, se modifie. Un parcours fait a la main dans l’UI n’existe nulle part une fois termine — celui-ci se rejoue sur une instance neuve, et sert de recette de reconstruction : l’instance jetable de ce chantier peut etre reconstruite de zero en executant les notebooks de la serie les uns apres les autres.

Verification. L’API rend un etat lisible en retour d’appel — le notebook peut donc verifier ce qu’il vient de faire (relire apres avoir ecrit, compter des balises dans le rendu), la ou un ecran demande de croire l’ecran. Toutes les mesures de ce fichier — la persistance de la publication, la frontiere gratuite/Pro — sont des verifications, pas des impressions d’ecran.

Difference. Deux etats d’une instance se comparent par leur API : la liste des formulaires avant et apres est un diff. Une UI ne se diff pas. C’est ce qui rend le dernier geste du notebook possible : prouver que l’instance est rendue a son etat initial, formulaire par formulaire.

Quatrieme difference, plus discrete : l’ecran n’a pas d’historique. Une modification faite dans l’interface existe dans l’instant et disparait de la memoire du systeme — WordPress journalise certes l’auteur et la date, mais pas le contenu d’avant ni le geste precis. Un script d’administration, lui, vit dans un depot : chaque changement de contenu de formulaire est un diff lisible, relu, commentable. Sur un projet ou plusieurs mains administrent la meme instance, la difference entre « modifie mardi par quelqu’un » et « modifie par ce commit, voici l’ancien contenu » est la difference entre une instance comprehensible et une instance opaque.

Reste le cout, qu’il ne faut pas cacher : administrer par l’API exige de connaitre le contrat — les routes, les champs qu’elles lisent, les statuts qu’elles rendent. Ce contrat n’est documente nulle part pour ce plugin : il a ete etabli en mesurant, et ce notebook en est la trace. C’est le vrai sens de la serie : chaque notebook « par l’API » est un bout de contrat ecrit par l’experience, publie une fois pour tous les lecteurs.

La serie « AI Engine par son API »

Le projet Livres Agites a mis AI Engine au coeur d’une maison d’edition : bot d’accueil, agents d’ateliers, bibliothecaire documentee par RAG, formulaires dynamiques. Cette serie presente le plugin de maniere reproductible — sans jamais exposer de donnees client :

Notebook Contenu
presenter-ai-engine-par-son-api instance, API, catalogue des chatbots, premiere completion
configurer-chatbots-par-l-api lire, dupliquer, ecrire et interroger des chatbots (document JSON global)
administrer-les-formulaires-par-l-api (ce notebook) le formulaire comme contenu : CRUD, publication, rendu public
notebooks suivants RAG/embeddings, agents MCP, …

Trois niveaux de lecture, dans chaque notebook :

  1. Decouverte — ce que fait la fonctionnalite, vue par l’API ;
  2. Branchement — comment on l’a branchee dans le projet ;
  3. Exercice — reutiliser le pattern sur un cas voisin. Applique a ce notebook : la decouverte est le CRUD complet d’un formulaire (lister, creer, ecrire, publier, rendre public, supprimer), chaque geste verifie par une relecture ; le branchement est la fiche de soumission de la Maison Valmont, inseree dans une page publique du site de demonstration — le meme geste que pour le parcours editorial du projet ; l’exercice fait rejouer les trois gestes administratifs essentiels en isolation — lire proprement un formulaire, le creer en deux temps, purger les brouillons.
# Configuration et helpers. Aucune cle ni adresse de provider n'est stockee
# dans ce fichier : tout vient de instance-jetable/.env (README, etape 5).

import base64
import os
from pathlib import Path

import requests
from dotenv import load_dotenv

# Localisation du .env : a cote du notebook (instance-jetable/.env),
# sinon dans le repertoire courant.
charges = []
for candidat in (Path("instance-jetable/.env"), Path(".env")):
    if candidat.exists():
        load_dotenv(candidat)
        charges.append(str(candidat))
print("Fichiers .env charges :", charges or "(aucun)")

BASE_URL = os.getenv("VALMONT_BASE_URL", "http://localhost:8093").rstrip("/")
ADMIN_USER = os.getenv("VALMONT_ADMIN_USER", "")
APP_PASSWORD = os.getenv("VALMONT_APP_PASSWORD", "")
print("Base URL :", BASE_URL)


def api(route, method="GET", payload=None, params=None):
    """Appel REST WordPress. route est relative, ex. '/mwai/v1/forms/list'."""
    url = BASE_URL + "/wp-json" + route
    entetes = {"Content-Type": "application/json"}
    if ADMIN_USER and APP_PASSWORD:
        creds = base64.b64encode(f"{ADMIN_USER}:{APP_PASSWORD}".encode()).decode()
        entetes["Authorization"] = "Basic " + creds
    reponse = requests.request(method, url, headers=entetes, json=payload,
                               params=params, timeout=120)
    reponse.raise_for_status()
    return reponse.json()


def http_get(chemin):
    """GET public (sans authentification) — pour tester le rendu visiteur."""
    reponse = requests.get(BASE_URL + chemin, timeout=60)
    reponse.raise_for_status()
    return reponse
Fichiers .env charges : ['instance-jetable\\.env']
Base URL : http://localhost:8093

Le contrat du helper, et ce que sa sortie ne dit pas

La cellule de configuration merite une lecture lente, car tout le reste du notebook repose sur elle. Trois decisions y sont prises, et la sortie n’en montre qu’une partie.

La premiere est la frontiere des secrets. Aucune cle, aucun mot de passe, aucune adresse de provider n’est ecrite ici : tout vient d’un fichier .env cherche en deux endroits, et la sortie affiche quel fichier a ete charge — instance-jetable/.env — sans jamais afficher ce qu’il contient. C’est la regle du chantier (aucun secret dans un depot public), mais c’est aussi le pattern transferable : le notebook nomme ses dependances secretes, les charge, et ne les revelera plus jamais. Noter le defaut honnete : sans .env, les variables seraient vides et api() enverrait des requetes non authentifiees — la sortie le dirait ((aucun)), le reste du notebook echouerait proprement route par route.

La deuxieme est l’authentification Application Password : un compte administrateur plus un mot de passe d’application, encodes en Base64 dans l’en-tete Authorization. C’est le mecanisme standard de la REST API de WordPress — ni cookie de session, ni nonce, juste un couple par requete.

La troisieme est la route relative : le helper concatene BASE_URL + /wp-json + route. Toute l’API de ce notebook vit sous /wp-json/mwai/v1/forms/... — prefixe plugin — et la route de creation de page vit sous /wp-json/wp/v2/pages — prefixe coeur. Un helper, deux mondes, la meme signature d’appel.

Deuxieme lecture : ce que le helper ne fait pas. Pas de retry, pas de timeout configure, pas de traduction des codes d’erreur — une requete qui echoue leve une exception Python brute, qui s’affiche dans la sortie de la cellule. Ce minimalisme est une decision, pas un oubli : dans un notebook, l’exception est une sortie comme une autre, souvent plus instructive qu’un message reformule (elle porte le code HTTP, la route, le corps de la reponse). Un script de production envelopperait ; un notebook de mesure montre.

Troisieme lecture : l’ordre des deux recherches de .env n’est pas decoratif. Le fichier est cherche pres du notebook d’abord, puis dans le dossier parent — ce qui permet au meme notebook de s’executer depuis n’importe quel sous-dossier de la serie sans configuration supplementaire. C’est le pattern « configuration par convention » : le notebook ne demande pas ou sont les secrets, il les cherche la ou la serie les range toujours. Le lecteur qui recopie ce notebook ailleurs recopiera le chemin avec — et comprendra l’echec s’il l’oublie.

Deux fonctionnalites, deux styles d’API

Le catalogue des routes (grain 1) cachait une asymetrie instructive. Comparer la famille des chatbots et celle des formulaires :

Chatbots (grain 2) Formulaires (ce notebook)
Nature du stockage entree d’une liste dans les options du plugin custom post type WordPress (mwai_form)
Ecriture POST /settings/chatbots : la liste entiere est remplacee POST /forms/update : un formulaire, par id
Creation ajouter un element a la liste envoyee POST /forms/create : alloue une coquille vide
Suppression retirer l’element de la liste et re-POSTer POST /forms/delete : une route dediee par id
Modele mental document de configuration contenu (titre, corps, statut, publication)

Le meme plugin expose deux styles d’API selon la nature de l’objet. Un chatbot se comporte comme un reglage ; un formulaire se comporte comme un article. Ce n’est pas un detail : cela dicte les operations possibles (versionner, publier, brouillonner un formulaire comme un article) et les pieges (remplacer la liste des chatbots ecrase tout ; oublier de publier un formulaire le laisse invisible). Cette asymetrie a des consequences operationnelles mesurables :

  • Idempotence : viser un formulaire par son id rend l’ecriture rejouable — re-POSTer le meme update conduit au meme etat. Remplacer une liste entiere, a l’inverse, rend chaque ecriture sensible a tout changement concurrent : entre la lecture et l’ecriture, un chatbot ajoute ailleurs disparait.
  • Granularite des erreurs : un update qui vise un id absent echoue pour ce formulaire seul ; une liste remplacee ecrase l’etat de tous les autres objets de la liste meme si l’intention n’en visait qu’un.
  • Cle d’adressage : le CRUD par id invite a l’upsert — chercher par titre, reutiliser si trouve, allouer sinon. C’est exactement le pattern de la cellule d’ecriture ci-dessous, et ce qui rend ce notebook rejouable sans dupliquer les formulaires a chaque execution.

Le tableau se lit alors comme un guide de migration mentale : en passant du cote chatbot au cote formulaire, on echange « le document de configuration » contre « l’objet editorial » — avec tout ce que ce changement apporte en contrepartie (statuts, publication, rendu public par shortcode) et ce qu’il exige en vigilance (un formulaire non publie est invisible, comme un brouillon d’article). Une derniere consequence, plus discrete : l’auditabilite. Un CRUD unitaire par id produit des journaux exploitables par objet — chaque appel d’API vise un formulaire identifie, ce qui se trace, se rejoue et se revert. Une ecriture « liste entiere » ne laisse comme trace que l’etat final de la liste : reconstruire qui a change quoi, et quand, demande alors comparer des snapshots. Pour une maison d’edition ou plusieurs roles touchent aux memes objets, la difference n’est pas cosmetique.

# 1. Lister : l'etat des formulaires (vide sur une instance neuve)
liste = api("/mwai/v1/forms/list")["forms"]
print("Formulaires existants :", [(f["id"], f["title"], f["status"]) for f in liste])
Formulaires existants : [(5, 'Soumission de manuscrit', 'publish')]

Sur l’instance neuve du grain 1, cette liste etait vide. C’est la premiere lecon de ce notebook : « pas de formulaire » n’est pas un etat fige, c’est un etat d’entree — et l’API permet de le remplir.

Creer : une coquille, pas un formulaire

POST /forms/create n’accepte qu’un title — et rend une coquille : un identifiant alloue, un titre, un contenu vide, un statut draft. Envoyer des champs dans le payload de creation est ignore : la creation est une allocation, pas une definition. Le contenu vient ensuite, par la route d’update — exactement comme on cree un article vide avant de l’ecrire. La liste est le point de verite : chaque manipulation de ce notebook commence et finit par elle. Ici, l’execution trouve un formulaire deja present (5, publie) alors que l’expose ci-dessus rappelle que sur l’instance neuve du grain 1 elle etait vide — difference memorable : l’etat d’une instance est ce que les executions precedentes y ont laisse. Un formulaire publie survit aux sessions ; « administrer par l’API » veut dire administrer un etat persistant, pas rejouer un scenario vierge. D’ou l’importance des deux reflexes qui structurent la suite : viser par titre plutot que par identifiant (extrapoler un id est une erreur, comme le montrera l’allocation ci-dessous), et toujours rapporter l’etat final a l’etat initial plutot qu’a un etat suppose.

# 2. Creer : la coquille (title honore, contenu vide, statut draft)
coquille = api("/mwai/v1/forms/create", method="POST",
               payload={"title": "Coquille de demonstration"})["form"]
print("id alloue      :", coquille["id"])
print("titre          :", coquille["title"]["raw"])
print("champs envoyes :", "ignores par la route (seul title est lu)")

detail = api("/mwai/v1/forms/get", params={"id": coquille["id"]})["form"]
print("contenu brut   :", repr(detail["content"]["raw"]))
print("statut         :", detail["status"])
id alloue      : 9
titre          : Coquille de demonstration
champs envoyes : ignores par la route (seul title est lu)
contenu brut   : ''
statut         : draft

La sortie de la creation merite une lecture ligne a ligne, car chacune enseigne quelque chose sur le modele. id alloue : 9 alors que le formulaire existant porte le 5 : les identifiants intermediaires ont ete consommes par d’autres objets de la base (revisions, medias) — un id de formulaire n’est pas un rang dans une liste de formulaires, c’est un numero global d’objet WordPress, alloue sequentiellement quelle que soit la nature de l’objet. Ne jamais supposer des ids consecutifs.

champs envoyes : ignores par la route (seul title est lu) : le payload de creation portait des champs, et l’API ne les a pas rejetes — elle les a silencieusement ignores. Une API qui ignore sans protester est un piege pour le testeur presse : la reussite de l’appel ne prouve pas que l’etat desire est atteint. Enfin statut : draft : la coquille nait brouillon, invisible au public — comme un article vide, elle doit etre ecrite puis publiee pour exister.

Remplir et publier : le formulaire devient un contenu

POST /forms/update ecrit title, content et status. Le contenu d’un formulaire est du contenu WordPress — ici des blocs Gutenberg. On construit la fiche de soumission de la Maison Valmont, on la publie (publish), et on verifie la persistance par une relecture.

Pour que le notebook soit rejouable, la creation suit le pattern upsert par titre : si « Soumission de manuscrit » existe deja, on le reutilise ; sinon, on alloue une coquille. Puis on ecrit toujours le contenu — meme idempotent. Un mot sur le contenu lui-meme, car c’est lui qui fait du formulaire un contenu : le corps envoye n’est pas du JSON de configuration mais du contenu WordPress — ici des blocs Gutenberg, la meme serialisation que le corps d’un article. Ce que l’editeur visuel de WordPress enregistrerait en blocs, l’API l’ecrit en texte structure, bloc par bloc. Toute la richesse du formulaire (textes, structure, champs decrits) voyage donc par cette seule route d’update — create ne fait qu’allouer la coquille vide qui la recevra. Notons aussi le role exact de la relecture : elle passe par la route forms/get, avec le meme id que l’ecriture — lecture et ecriture symetriques autour de l’identifiant, comme pour un article. Si la relecture rendait un autre contenu que celui envoye (un champ silencieusement tronque, un statut non applique), c’est elle qui le revelerait ; sans relecture, l’ecriture resterait une croyance.

# 3. Upsert : trouver ou allouer le formulaire principal, puis ecrire
TITRE = "Soumission de manuscrit"

forms = api("/mwai/v1/forms/list")["forms"]
existant = next((f for f in forms if f["title"] == TITRE), None)
if existant:
    FORM_ID = existant["id"]
    print("Reutilise (upsert) :", TITRE, "id =", FORM_ID)
else:
    FORM_ID = api("/mwai/v1/forms/create", method="POST",
                  payload={"title": TITRE})["form"]["id"]
    print("Alloue :", TITRE, "id =", FORM_ID)

CONTENU = (
    "<!-- wp:paragraph -->\n<p>Soumettez votre manuscrit a la Maison Valmont. "
    "Indiquez le titre, le genre et collez les cent premieres lignes.</p>\n<!-- /wp:paragraph -->\n"
    "<!-- wp:paragraph -->\n<p>Reponse du comite sous six semaines.</p>\n<!-- /wp:paragraph -->"
)

api("/mwai/v1/forms/update", method="POST", payload={
    "id": FORM_ID, "title": TITRE, "content": CONTENU, "status": "publish",
})
print("Ecrit et publie :", FORM_ID)

# Persistance : relecture apres ecriture
relu = api("/mwai/v1/forms/get", params={"id": FORM_ID})["form"]
print("statut persiste :", relu["status"])
print("contenu persiste :", repr(relu["content"]["raw"][:60]), "...")
Reutilise (upsert) : Soumission de manuscrit id = 5
Ecrit et publie : 5
statut persiste : publish
contenu persiste : '<!-- wp:paragraph -->\n<p>Soumettez votre manuscrit a la Mais' ...

Ecrire, puis relire : la difference entre un 200 et un etat

La cellule d’ecriture se termine par une relecture, et ce geste est la partie la plus importante du notebook. La route d’update repond favorablement (Ecrit et publie : 5) — mais une reponse favorable d’une route d’ecriture affirme seulement que la requete a ete comprise, pas que l’etat demande est atteint. La relecture qui suit (statut persiste : publish, contenu relu depuis l’API) transforme l’affirmation en mesure : ecrire puis relire est au REST ce que un test est au code — la preuve que l’effet a eu lieu, et pas seulement qu’il a ete demande.

Le contenu ecrit merite un arret : il est fait de blocs Gutenberg, avec leurs commentaires <!-- wp:paragraph --> dans la sortie relue. Ce n’est pas un caprice de format : le rendu WordPress parse ces commentaires pour structurer la page, et un contenu sans eux passe par un chemin different. Administrer un formulaire par l’API, c’est ecrire du contenu WordPress — avec la grammaire de blocs qui va avec.

Enfin, la premiere ligne de la sortie — Reutilise (upsert) : id = 5 — est le secret de la rejouabilite : sur cette instance, le titre existe deja, donc le formulaire existant est reutilise. Sur une instance neuve, la meme cellule allouerait. Le code est ecrit pour les deux mondes : c’est ce qui permet d’executer ce notebook deux fois sans doublon — et c’est aussi ce qui l’immunise contre les ids variables d’une instance a l’autre.

Le statut persiste (publish) merite une derniere remarque : la publication est un etat durable, pas une propriete de la requete. La requete qui publie rend un compte-rendu, puis disparait ; le statut reste dans l’instance jusqu’a ce qu’une autre requete le change. C’est pour cela que la relecture peut se faire plus tard, dans une autre cellule, avec une autre requete — et qu’un lecteur peut verifier la persistance a la main entre deux cellules. L’API de WordPress se lit mieux avec ce vocabulaire : chaque requete d’ecriture propose une transition, l’instance garde l’etat.

Noter enfin la forme du contenu relu : les commentaires de blocs sont present dans le raw — l’API ne les ajoute pas au rendu, elle les conserve depuis l’ecriture. Autrement dit : celui qui ecrit du contenu par l’API doit ecrire la grammaire de blocs lui-meme ; le plugin ne la synthetisera pas. C’est une frontiere de responsabilite classique entre API generale et couches produit.

Une note d’orthodoxie, pour le lecteur qui connait REST : l’upsert par titre n’est pas la maniere canonique d’ecrire. Le canon veut que creer et mettre a jour soient deux routes distinctes (le POST alloue, le PUT remplace une ressource adressee), et c’est ce que fait le coeur de WordPress pour les pages. Le plugin, lui, offre un seul geste qui decide seul — commode pour un ecran, commode pour un script, mais qui deplace la responsabilite : sans verrou ni unicite imposee, deux executions simultanees du meme upsert peuvent allouer deux fois la coquille. En pratique de notebook (une execution a la fois), le risque est nul ; en pratique d’integration, il merite une seconde lecture. Toujours la meme lecon : le contrat se lit dans le comportement, pas dans le nom de la route.

Le rendu public : un shortcode, comme pour un article

Un formulaire publie ne vit pas seul dans une page : il s’insere par le shortcode [mwai_form id=N]. On cree une page par l’API REST standard de WordPress (/wp/v2/pages — pas une route AI Engine), on y place le shortcode, puis on interroge la page en visiteur non authentifie et on cherche le texte du formulaire dans le HTML rendu. La boucle est bouclee : cree par l’API, ecrit par l’API, rendu au public. Le choix du shortcode plutot que d’un bloc est aussi une information d’architecture : le rendu du formulaire est deconnecte de l’editeur — n’importe quelle page peut porter le [mwai_form id=N], et c’est WordPress qui declenche le rendu au moment ou la page est servie, pas AI Engine qui pousserait le formulaire vers les pages. La frontiere des API est tout aussi nette : /mwai/v1/* administre le plugin, /wp/v2/* administre le site — creer la page porteuse releve de la seconde. Un automate qui voudrait tout faire par l’API du plugin s’arretera ici : la pose du shortcode est un geste WordPress ordinaire.

# 4. Rendre public : page avec shortcode, puis verification en visiteur
SLUG = "soumettre-un-manuscrit"
page = api("/wp/v2/pages", params={"slug": SLUG})
if page:
    PAGE_ID = page[0]["id"]
    print("Page existante reutilisee : " + str(PAGE_ID))
else:
    PAGE_ID = api("/wp/v2/pages", method="POST", payload={
        "title": "Soumettre un manuscrit", "status": "publish",
        "content": "<!-- wp:shortcode -->\n[mwai_form id=\"" + str(FORM_ID) + "\"]\n<!-- /wp:shortcode -->",
    })["id"]
    print("Page creee : " + str(PAGE_ID))

html = http_get("/" + SLUG + "/").text
present = "Soumettez votre manuscrit" in html
print("Page publique HTTP 200, formulaire rendu :", present)

# Mesure du rendu : le contenu est-il du texte affiche ou des champs ?
fragment = html[html.find("Soumettez votre manuscrit"):]
print("Balises <input> dans le rendu :", fragment.count("<input"))
print("Blocs <p> dans le rendu       :", fragment.count("<p"))
Page existante reutilisee : 6
Page publique HTTP 200, formulaire rendu : True
Balises <input> dans le rendu : 0
Blocs <p> dans le rendu       : 3

Pourquoi la mesure compte dans un fragment, pas dans la page

La mesure precedente ne compte pas les balises de la page entiere : elle decoupe un fragment a partir de la phrase du formulaire. La page complete contient le theme, la recherche, la navigation — autant de sources de <input> et de <p> qui n’ont rien a voir avec le shortcode rendu. Isoler le fragment fait de 0 un zero qui parle ; comptee sur la page entiere, la meme mesure serait un bruit. La methode se retient avec sa condition d’observation : compter, oui — mais dans la tranche que le shortcode a produite.

La sortie du rendu se lit en trois temps. Page existante reutilisee : 6 : la recherche par slug a trouve la page d’une execution anterieure — le meme reflexe upsert que pour les formulaires, applique au site, et la preuve qu’un rejeu n’empile pas les pages. Page publique HTTP 200, formulaire rendu : True : la page repond sans authentification (c’est la seule preuve honnete de publicite — une requete authentifiee prouverait l’acces, pas la publication) et le texte du formulaire est bien present dans le HTML servi. Reste la mesure decisive : Balises <input> : 0, Blocs <p> : 3 — le visiteur recoit de la prose, aucun champ de saisie. La methode — compter les balises du HTML rendu — est entierement reutilisable, comme le detaille la section suivante.

Ce que dit la mesure : gratuit rend du contenu, Pro rend des champs

La mesure ci-dessus est le coeur de l’observation honnete de ce notebook : sur la version gratuite (3.7.0, wordpress.org), le formulaire rendu est du contenu (des paragraphes) — zero champ de saisie. Les formulaires pilotes par l’IA — champs dynamiques, validation par modele, conditions — vivent dans la version Pro : le code gratuit n’enregistre que deux shortcodes (mwai_form et mwai_chatbot, verifie dans le plugin au moment du developpement), sans les shortcodes de champs individuels.

Ce n’est pas une limite cachee : c’est une frontiere de produit, mesurable par l’API et par le rendu. La lecon pour le projet : ce que le comparatif appelle « AI Forms (text/image/audio/file avec logique conditionnelle) » suppose la version Pro — sur la gratuite, l’API administrative existe deja (c’est ce notebook), mais le formulaire n’est pas encore un formulaire.

La mesure elle-meme est un patron reutilisable : compter les balises du HTML rendu (<input>, <p>, …) recupere en visiteur anonyme. Rejouee telle quelle sur une instance Pro, la meme cellule de mesure afficherait des <input> non nuls — la frontiere gratuite/Pro est donc verifiable par le meme script, sans lire la documentation du produit : c’est la difference entre une affirmation de comparatif et une mesure de terrain.

Il faut aussi noter ce que la mesure ne dit pas : l’absence de champs dans le rendu gratuit ne veut pas dire que le contenu du formulaire est perdu — il est bien stocke, publie, et rendu ; simplement, la version gratuite en fait de la prose publique (une fiche, une consigne) et non un formulaire interactif. Le statut editorial est complet ; c’est l’interactivite qui est un produit payant.

Supprimer : une route dediee, un retour a l’etat initial

POST /forms/delete supprime par id — pas de remplacement de liste comme pour les chatbots. On supprime la coquille de demonstration de la cellule 2 et on verifie la liste finale : il ne reste que le formulaire principal, publie. La sortie finale confirme le geste d’hygiene : la coquille de demonstration est supprimee par son id, et la liste finale — [(5, 'Soumission de manuscrit', 'publish')] — est identique a la liste de depart. Le notebook est idempotent : chaque execution repart du meme etat initial et y revient, sans accumuler coquilles ni pages. C’est la difference entre une demonstration qui pollue son instance et une demonstration qui se nettoie — et c’est ce qui autorise le rejeu en toute confiance, sur la meme instance, autant de fois que necessaire. Relire cette mesure apres coup est un exercice sain : la page publique repond, le texte est la, les paragraphes sont la — tout ce que la version gratuite promet est tenu. Ce qui manque n’est pas un defaut de fonctionnement mais une couche de produit : les champs. C’est exactement ce que la section precedente a nomme.

# 5. Supprimer la coquille, verifier l'etat final
api("/mwai/v1/forms/delete", method="POST", payload={"id": coquille["id"]})
print("Coquille supprimee :", coquille["id"])

final = api("/mwai/v1/forms/list")["forms"]
print("Etat final :", [(f["id"], f["title"], f["status"]) for f in final])
Coquille supprimee : 9
Etat final : [(5, 'Soumission de manuscrit', 'publish')]

La symetrie mesuree : partir en laissant l’instance comme on l’a trouvee

Comparer la sortie finale a la premiere sortie du notebook : avant tout, (5, 'Soumission de manuscrit', 'publish') ; apres tout, exactement la meme liste. Le notebook a alloue un identifiant (9), ecrit, publie, cree une page, supprime — et l’etat observable est identique a l’entree. Cette symetrie n’est pas un accident : elle est la consequence de deux decisions prises plus haut (l’upsert par titre, qui reutilise au lieu de dupliquer, et la suppression finale de la coquille de demonstration). Un script d’administration qui laisse derriere lui des objets orphelins empoisonne la prochaine execution ; celui-ci se rejoue indefiniment.

La suppression elle-meme illustre la derniere difference de style avec les chatbots : une route dediee par id, pas un remplacement de liste. Supprimer un formulaire est une operation unitaire et precise — on ne peut pas, par construction, effacer les autres formulaires par erreur d’adresse. La ou le style « liste globale » des chatbots expose la faute de frappe qui ecrase tout, le style « contenu » expose au pire… le mauvais id, relu par la liste finale.

Reste une trace volontaire : la page creee (ou reutilisee) demeure. L’etat « logique » (formulaires) est symetrique ; l’etat « physique » (pages, ids consommes) ne l’est pas — un id alloue n’est jamais rendu. C’est une propriete generale des systemes d’allocation, et une limite honnete de toute promesse de « nettoyage parfait ».

Pourquoi un id alloue n’est-il jamais rendu ? Parce que le compteur de contenus WordPress est un compteur monotone global : il designe des objets par ordre de creation, tous types confondus (articles, pages, formulaires), et n’est jamais decremente — un id libere serait reattribue, ce qui casserait toute reference externe encore vivante (liens, caches, historiques). Le cout de cette decision d’architecture est visible dans ce notebook : l’instance termine avec un id de plus qu’elle n’a commence. Le benefice est invisible, et immense : aucun id ne designe jamais deux objets differents dans la vie d’une instance.

La lecon pour l’administrateur : ne jamais ecrire un script qui suppose la valeur d’un id d’une execution a l’autre — ni pour nettoyer, ni pour adresser. L’id se lit (par la liste), il ne se devine pas.

Ce qu’on en a fait dans le projet Livres Agites

Dans le projet d’origine (AI Engine Pro), les formulaires sont la porte d’entree du workflow editorial : soumission de manuscrit avec champs conditionnels, pieces jointes, validations. Le parcours 4 (04-Cas-Usage-livresagites/livresagites-parcours.md) les decrit comme une machine a etats — un formulaire a logique de branchement a plus de chemins que de champs, et trois grandeurs emergent (chemins atteignables, cout LLM, champs morts). Le compagnon stdlib auditer-un-formulaire-conditionnel.ipynb enumere ces chemins sur un fixture synthetique. Ce notebook en est la face administrative : comment un formulaire vit dans WordPress — coquille, contenu, publication, rendu, suppression — avant meme que la logique conditionnelle n’entre en jeu. Cette complementarite est le vrai sujet : l’audit du compagnon demarre la ou ce notebook s’arrete. Ici, le formulaire est un contenu qu’on administre — coquille, corps, statut, page porteuse ; la-bas, le contenu devient une machine — conditions, branchements, chemins morts. Un integrateur qui ne connaitrait que la face administrative s’etonnerait qu’un formulaire ait « plus de chemins que de champs » ; un auditeur qui ne connaitrait que la machine oublierait que tout ce comportement vit dans un contenu WordPress ordinaire, soumis aux memes statuts et au meme rendu par shortcode. Les deux lectures sont necessaires pour piloter un formulaire reel. Pour l’etudiant, la lecon de structure : une meme fonctionnalite peut exposer plusieurs faces outillees — administrative (ce notebook), comportementale (l’audit de chemins), et le rendu public (ce que le visiteur voit). Choisir la bonne face pour une question donnee est une competence d’architecte, pas un raffinement.

Exercices

Trois exercices, du plus simple au plus integre. Les fonctions sont a completer ; api() et les donnees des cellules precedentes sont disponibles. Chaque exercice se verifie d’une ligne de test. La progression n’est pas decorative : l’exercice 1 isole la lecture defensive (un formulaire absent doit donner None, pas une exception) ; l’exercice 2 enchaine les deux temps d’ecriture (allocation puis ecriture, le geste le plus caracteristique de cette API) ; l’exercice 3 combine lecture filtrante et suppression conditionnelle, avec un garde-fou : ne toucher que les draft. Ce sont les trois gestes qu’un administrateur automatise en premier — et chacun se verifie par une ligne, sans orchestration.

Exercice 1 — lire un formulaire proprement

Completez lire_formulaire(form_id) : elle retourne le document du formulaire (id, titre, statut), ou None s’il n’existe pas — sans lever d’exception. Indice : la route forms/get repond-elle par une erreur exploitable quand l’id n’existe pas, ou par un document vide ? Testez les deux cas (un id present dans la liste, un id invente) et traitez-les de facon distincte — c’est tout l’enjeu du « proprement ».

def lire_formulaire(form_id):
    """Retourne le document du formulaire form_id, None s'il est absent."""
    # A COMPLETER : forms/get avec params={'id': ...} et gestion du cas absent
    return None

Exercice 2 — creer un formulaire complet en deux temps

Completez creer_formulaire(titre, contenu) : elle alloue la coquille (create), l’ecrit et la publie (update), puis retourne l’identifiant — le pattern create-shell-then-update de ce notebook. Indice : deux appels, dans cet ordre — create pour obtenir l’id (le titre suffit), puis update avec l’id, le contenu et publish. La ligne de test verifie que le formulaire apparait dans la liste au statut attendu, avec le contenu passe en argument.

def creer_formulaire(titre, contenu):
    """Alloue, ecrit et publie ; retourne l'id du formulaire cree."""
    # A COMPLETER : forms/create puis forms/update (status='publish')
    return None

Exercice 3 — purger les brouillons

Completez purger_brouillons() : elle liste les formulaires, supprime ceux au statut draft et retourne la liste des identifiants supprimes. Attention a ne toucher QUE les drafts. Indice : la liste rend deja le statut de chaque formulaire — il suffit de filtrer avant de supprimer. La ligne de test verifie qu’apres passage, la liste ne contient plus aucun draft et que les formulaires publish sont intacts.

def purger_brouillons():
    """Supprime tous les formulaires au statut draft ; retourne leurs ids."""
    # A COMPLETER : forms/list, filtrer status=='draft', forms/delete sur chacun
    return []

Provenance et limites

  • Instance testee : http://localhost:8093, montee via instance-jetable/docker-compose.jetable.example.yml, AI Engine 3.7.0 (version gratuite, wordpress.org), corpus synthetique « Maison Valmont ».
  • Determinisme : contrairement aux notebooks 1 et 2, aucune completion LLM ici — toutes les sorties sont deterministes au rejeu pres des identifiants alloues (l’upsert par titre evite les doublons).
  • Frontiere gratuite/Pro : mesuree par le rendu (paragraphes sans champs) et par lecture du code du plugin au developpement (deux shortcodes enregistres : mwai_form, mwai_chatbot). Les champs IA dynamiques sont une fonctionnalite Pro.
  • Endpoints verifies ici (firsthand) : /mwai/v1/forms/list, /forms/create, /forms/get, /forms/update, /forms/delete, /wp/v2/pages (creation + lecture publique).
  • Frontieres : pas de donnees client, pas de secret, pas d’IP de provider — regles du chantier CoursIA. Sur le rejeu : ce notebook est deterministe dans ses etats (la liste finale est toujours le formulaire principal, publie) mais pas dans ses identifiants — la coquille de demonstration recoit un id frais a chaque execution (9 ici), et la page porteuse est reutilisee plutot que recreee grace a la recherche par slug. C’est le compromis honnete d’une demonstration vivante : les valeurs citees dans les sorties (ids, numero de page) sont celles d’une execution reelle, reproductibles a ces identifiants pres.

La frontiere gratuite/Pro mesuree ici complete le tableau de la serie : le catalogue (grain 1) annoncait les formulaires comme fonctionnalite ; ce grain montre que l’API administrative est entierement presente en version gratuite, et que seule la couche interactive (les champs rendus au visiteur) est Pro. Le diagnostic se rejoue avec la cellule de mesure, sur n’importe quelle instance — c’est la definition d’une limite verifiee plutot qu’affirmee. Enfin, la frontiere des API traverse ce notebook lui-meme : les cinq routes /mwai/v1/forms/* administrent le plugin, mais la page porteuse du shortcode passe par /wp/v2/pages — une demonstration complete du formulaire rendu public exige les deux vocabulaires. C’est le cas chaque fois qu’un plugin WordPress expose a la fois des objets propres (ici mwai_form) et une presence dans le site (page, shortcode) : l’automatisation complete croise forcement les deux API.

Le determinisme au rejeu, regle par regle

La section ci-dessus dit « deterministes au rejeu pres les identifiants alloues » — cette nuance merite d’etre depliee, car elle decrit exactement ce qu’un lecteur verra en rejouant. Les chaines sont stables : les titres, les statuts, le contenu relu, la presence de la phrase dans le HTML — tout cela est fonction de l’instance, pas de son histoire. Les identifiants ne le sont pas : le 9 de la coquille vaudra autre chose au rejeu (l’instance a consomme des ids depuis), et la page reutilisee aura le numero qu’elle a. Une comparaison naive de sorties au caractere pres echouera donc toujours — la comparaison juste se fait sur les chaines et les statuts, pas sur les ids.

Deuxieme nuance : la sortie du lister (cellule 1) depend de l’etat de l’instance — vide sur une machine neuve, peuplee sur une machine servie. Les scripts de ce notebook y sont immunises (upsert par titre), les lectures ne le sont pas : un humain qui relit les sorties de commit doit s’attendre a cet ecart et le lire comme un signe d’etat, pas comme une contradiction.

Troisieme nuance, la plus discrete : ce notebook ne fait aucune completion LLM. Toute son autorite vient de ce que chaque affirmation est soit la reponse d’une route, soit une mesure sur du HTML rendu — deux sources qu’un lecteur peut reproduire avec un curl et un navigateur. C’est la definition operationnelle du « firsthand » de ce chantier : pas une confiance, une reproduction.

Pour finir, la liste de ce qu’un lecteur peut reproduire a la main, sans rien executer — la serie appelle cela le firsthand :

  • la liste initiale : curl avec l’en-tete d’authentification, sur la route forms/list du fichier .env ;
  • la publication : la meme requete, relue, sur une autre machine ;
  • la visibilite : ouvrir l’URL de la page dans un navigateur et chercher la phrase du formulaire ;
  • la symetrie : relister apres suppression et comparer les couples (titre, statut).

Quatre gestes, deux outils (un terminal, un navigateur), zero confiance demandee. C’est la promesse que cette section scelle : tout ce que le notebook affirme est soit une sortie commitee lisible, soit reproductible par ces gestes. Il n’existe pas de troisieme categorie.

Retour au sommet