Aller au contenu
beeraw
Choix techniques ·

Brancher un assistant IA sur une application qui existe déjà

Rendre une application de gestion interne pilotable par un assistant IA : 95 actions, et aucune qui contourne les règles que l'écran applique déjà.

Je l'avais annoncé à la fin du billet précédent : voici comment l'application de gestion interne de ce client du bâtiment est devenue pilotable par un assistant IA compatible MCP. C'est d'ailleurs la nouveauté qui avait le plus besoin du filet. Concrètement, on peut consulter, créer ou modifier des achats, des chantiers, des pointages, des congés, des documents ou des fournisseurs depuis une conversation, au lieu de remplir un formulaire. En tout, 95 actions sur 23 types d'objets, de quoi couvrir l'ensemble des écrans.

Sur ce journal, j'ai déjà parlé d'un outil pensé dès le premier jour pour être tenu par un agent. Ici, c'est l'inverse (et c'est le cas le plus courant) : une application qui tourne depuis des années, avec ses écrans, ses rôles et ses règles, et à qui on demande d'ouvrir une deuxième porte d'entrée.

La tentation du raccourci

La façon rapide de faire, je la connais bien : écrire à côté de l'application une petite couche qui parle directement à la base. Un outil « créer un achat » construit un achat, l'enregistre, et voilà. Ça marche dès le premier jour… et on se retrouve avec deux façons de créer un achat.

Celle de l'écran connaît toutes les règles que des années d'usage y ont déposées : le fournisseur obligatoire, le brouillon qu'on peut laisser sans montant, le congé déjà posé qu'on n'écrase pas. L'autre ne connaît que celles auxquelles on a pensé en l'écrivant (autant dire pas toutes). Et j'ai déjà raconté, à propos d'un autre projet, ce qui se passe quand une même règle existe en deux exemplaires : les deux finissent par diverger.

Le cahier des charges tenait donc en une phrase : l'assistant ne peut rien faire que l'utilisateur ne pourrait pas faire lui-même.

Par la même porte que l'écran

J'ai posé une règle stricte : quand l'assistant écrit quelque chose, il passe par les mêmes formulaires Symfony que l'écran, puis par les mêmes services métier. Le formulaire valide, le service applique les règles, et aucun des deux ne se demande qui a rempli les champs.

Ça suppose que les règles vivent dans des services communs, et pas dans les contrôleurs des écrans. C'est là que j'ai passé l'essentiel du temps, bien plus que sur l'écriture des outils eux-mêmes. Un outil MCP bien fait est très court, puisqu'il ne décide de rien. Il se contente de :

  • recevoir l'appel et ses paramètres ;
  • remplir le formulaire de l'écran ;
  • appeler le service métier commun ;
  • mettre en forme la réponse.

Il ne valide rien à sa façon, n'applique aucune règle à lui, n'écrit jamais directement en base et ne décide pas de ce qui est permis.

L'avantage le plus concret, c'est qu'une règle qui change ne change qu'à un seul endroit : l'écran comme l'assistant l'appliquent dès la livraison suivante, sans que personne ait à se rappeler qu'il existe une deuxième porte (moi le premier).

Pas plus de droits que l'écran

Chaque utilisateur se connecte avec un jeton personnel, et l'assistant agit avec les droits de cet utilisateur. Chaque outil déclare le rôle exigé par l'action équivalente à l'écran : le même, et surtout pas un rôle plus large « pour simplifier » (la tentation existe, je ne vais pas vous mentir).

Une déclaration, ça peut être fausse, alors elle est vérifiée : les tests comparent, outil par outil, le rôle déclaré avec celui de l'écran correspondant, et un outil plus permissif que son écran fait échouer la suite. Le garde-fou du premier billet joue aussi ici : un nouvel outil que personne ne teste est refusé.

Et puis il y a un effet de bord bien pratique : le serveur ne montre à chacun que les outils qu'il a le droit d'utiliser. Un utilisateur sans accès aux congés ne voit même pas les outils de congés. Du coup, l'assistant ne tente pas ce qu'il ne connaît pas, et ne perd pas son temps à s'excuser d'un refus.

Tout ou rien

Chaque appel s'exécute dans une seule transaction, annulée entièrement en cas d'échec.

À l'écran, un enregistrement raté se voit : l'utilisateur a le message d'erreur sous les yeux, il revient en arrière, il corrige. Un assistant, lui, a tendance à enchaîner. Si le troisième appel échoue, il peut très bien continuer avec le quatrième en s'appuyant sur un état qui n'existe qu'à moitié. Avec la transaction, soit tout est enregistré, soit rien. L'assistant n'en devient pas plus malin (ce serait trop beau), mais il n'y a plus de morceaux à ramasser derrière lui.

Ce qui sort

Reste la réponse. Un objet renvoyé tel quel emporte tout ce qu'il contient, y compris ce qu'on n'avait aucune intention de montrer. Les résultats passent donc par des « presenters », des classes qui servent uniquement à choisir ce qui sort. Un mot de passe ou un jeton n'y figure jamais, quel que soit l'outil et quel que soit le profil.

Ils fonctionnent en liste blanche : un champ ajouté demain à une entité ne sortira pas tant que quelqu'un n'aura pas décidé qu'il doit sortir. Si on oublie, il reste caché, tout simplement.

Au passage

Brancher un assistant sur toute l'application oblige à la parcourir en entier, et forcément on tombe sur des choses. L'export des achats, par exemple, existe désormais en CSV et en Excel, et il respecte les filtres de la liste : on exporte ce qu'on voit.

Dans le prochain et dernier billet de la série, je parle justement de ce qu'on voit : les petites choses de l'interface qui changent le quotidien.