Architecture

Sur cette page

Le Hub est un programme Rust qui donne à un modèle deux outils, execute_script et get_state, et atteint chaque application par un module côté Hub et un pont côté application. Cette page montre comment les éléments s’enchaînent, les trois façons d’atteindre une application et un script mesuré contre des appels séparés.

Les éléments

  • Client MCP : de votre côté. Il lance libremodular-mcp serve et appelle les deux outils.
  • Hub : un seul processus, libremodular-mcp. Il répond au client, vérifie la cible, lit le script avant de l’envoyer et écrit le journal d’audit.
  • Module : dans le Hub, un par application. Il sait comment atteindre l’application.
  • Pont : dans l’application, à côté d’elle, ou un processus Python par script. Il exécute le script et renvoie ce qu’il a imprimé, son result, ou l’erreur propre à l’application.
  • Application : GIMP, Krita, Blender, Inkscape ou LibreOffice. Elle fait le travail.

Le Hub ne connaît l’interface d’aucune application. Un script s’écrit dans le Python propre à l’application, et le module et le pont sont les seules pièces qui dépendent de l’application.

Le Hub est un exécutable unique d’environ 4,3 Mo. En dehors du chargeur dynamique, ldd liste deux bibliothèques : libc.so.6 et libgcc_s.so.1. Le paquet installe un second exécutable, libremodular-launch, qui lance LibreOffice et son pont.

Deux outils

La réponse du Hub à tools/list nomme exactement ["execute_script", "get_state"].

execute_script(target_app, script_content) exécute un script Python complet dans l’application que désigne target_app et renvoie un objet JSON : status, duration_ms, stdout, stderr, result, error avec la trace d’erreur propre à l’application, truncated, et reason quand le Hub a refusé le script. get_state(target_app) renvoie l’état de l’application : ses documents, ses calques ou ses objets, le chemin d’une capture PNG quand il y en a une, et content_origin, qui dit que le texte vient d’un document et non du Hub. La cible hub liste les applications configurées sans en contacter aucune.

Trois façons d’atteindre une application

Le Hub a été lancé avec toutes les applications activées et aucun pont démarré. get_state sur hub a listé sept cibles, dans ces modes :

  • tcp : gimp, krita, blender. Un pont chargé dans l’application écoute sur un port local, et le Hub s’y connecte.
  • uno : libreoffice-writer, libreoffice-calc, libreoffice-impress. Un pont autonome se connecte au port UNO sur lequel LibreOffice écoute, et les trois cibles le partagent.
  • cli : inkscape. Le Hub lance un processus Python par script ; il n’y a pas de pont à démarrer.

Seul inkscape affichait connected: true : en mode cli, rien n’a besoin d’être lancé, et le Hub avait trouvé le programme. Dans les modes tcp et uno, le Hub ne démarre, ne ferme ni n’arrête jamais une application à la demande du modèle ; l’opérateur démarre le pont, comme le dit le guide de chaque application.

Une application exécute un script à la fois. Un script qui dépasse le délai du Hub reçoit la réponse timeout, et la requête suivante reçoit busy jusqu’à ce que l’application soit libre ; la page Sécurité le mesure.

Ce qu’un pont demande

Les quatre fichiers de pont que le paquet installe sont générés à partir d’un noyau commun et d’un adaptateur par application. Ils importent la bibliothèque standard de Python et, en dehors d’elle, la seule interface de l’application. Chaque ligne donne le fichier, sa longueur et ce qu’il importe hors de la bibliothèque standard :

  • blender_bridge.py, 1 091 lignes : bpy.
  • gimp_bridge.py, 1 130 lignes : gi.
  • krita_bridge.py, 1 094 lignes : krita, PyQt5, PyQt6.
  • uno_bridge.py, 1 203 lignes : uno, com.

La liste vient de l’analyse syntaxique de chaque fichier, pas de son exécution. La page Compatibilité donne, pour chaque application, la version avec laquelle ces ponts ont été essayés.

Un script contre des appels séparés

Un modèle qui pilote une application geste par geste fait un appel par geste. Un modèle qui écrit tout le travail fait un seul appel. Le Hub traite les deux de la même façon : on peut donc chronométrer les deux sur le même Hub, la même application et le même travail.

Le travail consiste à ajouter N éléments à un document : N rectangles dans Inkscape, N cubes dans Blender. Pour chaque N, dix répétitions ont chronométré un appel de execute_script qui fait tout le travail, puis N appels qui font un geste chacun. Le document était vidé avant chaque mesure, et deux appels d’échauffement précédaient les répétitions. Le temps est lu dans le client, de l’écriture de la requête à la lecture de la réponse ; dans Inkscape, le travail est ce script,

python · Script écrit
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
for i in range(20):
    root.add(inkex.Rectangle.new(i * 12, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))

et chacun des vingt appels séparés est le même script, la boucle étant remplacée par une ligne, par exemple le troisième :

python · Script écrit
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
root.add(inkex.Rectangle.new(36, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))

Médiane de dix répétitions, avec la plus petite et la plus grande entre parenthèses. Le client est celui de la démonstration, avec un chronomètre autour de chaque appel.

Inkscape 1.4, mode cli (un processus Python par appel) :

  • N = 1 : un script 141,7 ms (141,7 à 151,7) ; un appel 141,7 ms (141,5 à 161,9) ; rapport ×1,0.
  • N = 5 : un script 146,7 ms (141,6 à 152,0) ; cinq appels 738,7 ms (718,6 à 749,3) ; rapport ×5,0.
  • N = 20 : un script 151,7 ms (141,6 à 151,9) ; vingt appels 2 933,8 ms (2 874,0 à 3 046,2) ; rapport ×19,3.

Blender 4.3.2, mode tcp (l’application reste en marche) :

  • N = 1 : un script 1,5 ms (1,3 à 2,7) ; un appel 1,2 ms (1,0 à 2,1) ; rapport ×0,8.
  • N = 5 : un script 3,5 ms (3,3 à 6,1) ; cinq appels 5,9 ms (5,5 à 8,3) ; rapport ×1,7.
  • N = 20 : un script 17,8 ms (16,7 à 20,4) ; vingt appels 30,5 ms (27,2 à 32,3) ; rapport ×1,7.

En mode cli, chaque appel lance un processus Python et charge inkex, ce qui coûte environ 0,14 s quel que soit le travail : vingt gestes en appels séparés coûtent 19,3 fois l’appel unique. En mode tcp, l’application tourne déjà, et les 19 appels supplémentaires ont ajouté 12,7 ms, soit environ 0,7 ms chacun. Une seconde exécution de toute la mesure a donné des médianes qui différaient d’au plus 10 ms sur Inkscape, sauf pour les vingt appels séparés (87 ms), et d’au plus 1,3 ms sur Blender ; le rapport à N = 20 était de ×19,9 sur Inkscape et de ×1,7 sur Blender.

SuiteSécurité →