Exécution de code
Pour une question analytique (le plus élevé, le total, la moyenne, un top-N, un comptage exhaustif) ou multi-étapes (recouper plusieurs sources), faire transiter toutes les données par le modèle coûte cher en tokens et le pousse à calculer « de tête » — peu fiable.
L’outil exécution de code résout ça : le modèle écrit un petit programme JavaScript, exécuté dans un bac à sable isolé qui interroge vos connecteurs API et calcule le résultat. Seul le résultat final revient au modèle ; les données ne polluent pas le contexte. Le calcul est fait par du code (déterministe), pas estimé.
Deux conditions :
- la clé porte le scope
code(Portail → Clés API) ; - la requête passe
tools_enabled: ["code"]— généralement avec un ou plusieurs connecteurs.
curl https://api.miraca.fr/v1/chat/completions \ -H "Authorization: Bearer $MIRACA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral-medium", "messages": [{ "role": "user", "content": "Quelle est la facture la plus élevée de 2026 ?" }], "tools_enabled": ["code", "connection:ma-compta"] }'Le modèle décide seul d’écrire un script ; il reboucle (corrige) si l’exécution renvoie une
erreur, dans la limite de tools_max_steps. Mode agentique (non-streamé), comme le RAG
agentique et les outils web — ils coexistent dans la même boucle.
L’API disponible dans le bac à sable
Section intitulée « L’API disponible dans le bac à sable »Le script appelle vos connecteurs (jamais le réseau brut) :
// appel brut d'une opération → JSON de l'APIconst page = await miraca.connector("ma-compta").call("list-invoices", { limit: 100, cursor });
// agrégat déterministe (pagination complète automatique) → { count, sum, avg, min, max }// `list` = le NOM de l'opération de liste à agréger ; `field` = le champ numérique// (chemin pointé s'il est imbriqué : "properties.amount").const agg = await miraca.connector("ma-compta").aggregate({ list: "list-invoices", field: "amount", filter: '[{"field":"date","operator":"gteq","value":"2026-01-01"}]'});
// les AUTRES paramètres de l'opération, paramètres de chemin compris, vont dans `extraArgs`const deals = await miraca.connector("mon-crm").aggregate({ list: "list-objects", field: "properties.amount", extraArgs: { objectType: "deals", properties: "amount" }});// `filter` n'existe que si l'opération déclare un paramètre `filter`.// `sum: null` (avec une `note`) : aucune valeur numérique sous ce champ, le compte reste exact.
// l'ALGÈBRE analytique complète d'une base : grouper, agréger, classer — en un appelconst top = await miraca.kb("ma-base").query({ facets: { exercice: "2024" }, group_by: "client", // ou ["client","exercice"], ou { date: "month" } aggregate: "montant", // → chaque groupe : { count, sum, avg, min, max } exacts sort: "montant:sum", limit: 3, // having: { metric: "montant:sum", op: "gte", value: 1e4 }});// → top.data = [{ group: { client: "…" }, count, aggregates: [...] }], top.total = nb de groupes
// interroger une base de connaissance (moteur analytique : total / aggregates exacts)const kb = await miraca.kb("ma-base").search({ filters: [{ key: "statut", op: "eq", value: "actif" }], aggregate: "montant", // → aggregates[0] = { count, sum, avg, min, max } group_by: "document" // → total = nombre d'enregistrements EXACT});Quand le système connecté refuse un appel
Section intitulée « Quand le système connecté refuse un appel »Un appel que l’API du connecteur refuse (statut HTTP 400 ou plus) lève une erreur dans
le script : il ne revient jamais comme une donnée. Le message nomme le connecteur, l’opération
et le statut, puis cite la réponse de l’API. Le script s’arrête donc (ou passe dans son
try/catch), et le modèle lit la raison du refus au lieu de conclure à une absence de résultat.
La même règle vaut pour .aggregate() : une page refusée arrête l’agrégat, elle ne compte pas
pour zéro.
.aggregate() ne rend jamais un total partiel. Il suit les paginations courantes (curseur
de premier niveau, paging.next.after, has_more avec starting_after, additional_data).
Quand une liste continue et qu’il ne sait pas en demander la suite (pagination par numéro de
page, par exemple), il lève une erreur qui le dit : le script écrit alors sa propre boucle avec
.call(). Un argument que l’opération ne déclare pas (filter compris) est refusé de la même
façon, au lieu d’être ignoré.
.aggregate() lit un nombre écrit en texte à la française : décimal à point (300.0) ou à
virgule (12,5), milliers séparés par une espace (1 234,56), suivi au plus d’un symbole de
devise ou d’un %, et milliers puis décimales (1,234.56, 1.234,56). Tout autre texte (une date, une adresse, ou 1,234 et 12,345, qui peuvent être un décimal français ou des milliers anglo-saxons) n’est
pas un nombre : il est exclu de la somme, et la réponse porte une note qui en cite des
exemples. Quand un filtre ne trouve aucun enregistrement, la note le dit aussi : la valeur d’un
filtre se compare telle quelle (accents, apostrophes, espaces), et zéro résultat ne prouve pas
qu’il n’y a rien.
try { await miraca.connector("mon-crm").call("get-contact", { id: "inconnu" });} catch (e) { // « Le connecteur « mon-crm » a répondu HTTP 404 à l'opération « get-contact ». … » console.log(e.message);}Deux refus se produisent avant tout appel à l’API, avec un message qui le dit :
- un paramètre que l’opération ne déclare pas ;
- un paramètre de corps déclaré
arrayouobjectqui reçoit une autre forme, par exemple une liste séparée par des virgules à la place d’un tableau. Un tableau ou un objet écrit en JSON dans une chaîne est accepté.
Le script accède aussi à l’espace de travail de la requête : celui
de la conversation dans l’Assistant, celui du fil dans le bac à sable du portail, ou celui
que vous nommez avec workspace:"<slug>" sur /v1/chat/completions. On y trouve les pièces
jointes déposées (à tous les tours), les fichiers déjà produits, et les entrées écrites par le
modèle ou par un script précédent.
// ce qui existe dans l'espace de travail → [{ name, kind, bytes, sheets? }]const entries = await miraca.workspace.list();
// un tableau joint (xlsx / csv), en cellules TYPÉES → { sheet, sheets, columns, rows, records, profile }// `profile` = une fiche par colonne (nature, signe, min/max/somme, distincts, vides) : à lire// avant un tri ou un total — sur des montants tous négatifs, « le plus gros » est le plus négatif.// `rows` ne contient PAS l'en-tête (il est dans `columns`) ; `records` = les mêmes lignes// en objets clé → valeur, plus sûrs qu'un index de colonne.const t = await miraca.workspace.table("transactions.xlsx", { sheet: "Transactions" });const tri = t.records.slice().sort((a, b) => Math.abs(b.Montant) - Math.abs(a.Montant));
// le texte entier d'une entrée, et l'écriture d'un résultat intermédiaireconst note = await miraca.workspace.read("synthese.md");await miraca.workspace.write("totaux.json", { total: 12345.6 });
// un classeur à plusieurs feuilles, avec des formules qui relient une feuille à l'autreawait miraca.file.write({ format: "xlsx", filename: "transactions-triees", sheets: [ { name: "Transactions", columns: t.columns, rows: tri.map((r) => t.columns.map((c) => r[c])) }, { name: "Analyses", columns: ["Indicateur", "Valeur"], rows: [ ["Total", { formula: "SUM(Transactions!E2:E173)" }], ["Frais bancaires", { formula: "SUMIF(Transactions!K:K;\"Frais bancaires\";Transactions!E:E)" }], ] }, ],});C’est la voie à prendre dès qu’il s’agit de trier, agréger, filtrer ou transformer un tableau
joint : les lignes ne passent jamais par la réponse du modèle, rien n’est tronqué, et le
résultat est exact. Sans espace (une tâche, ou un appel d’API sans workspace),
miraca.workspace lève une erreur explicite.
Les colonnes de records se lisent à la typographie près (espace insécable, accent décomposé,
apostrophe typographique) : une colonne introuvable lève une erreur qui liste les colonnes, les
plus proches d’abord, au lieu de rendre undefined, et écrire r["montant (€)"] = 0 met à jour la
colonne existante au lieu d’en créer une seconde. read() lève pour une entrée sans texte lisible
(un fichier Word produit, un tableau : l’erreur renvoie vers table()), et delete() lève pour une
entrée absente, en citant la plus proche, au lieu de rendre un résultat qu’on lirait comme « rien à
supprimer ».
Pour un superlatif ou un agrégat sur un champ que l’API ne sait pas trier (ex. un montant),
préférez aggregate (il pagine tout et calcule côté serveur) ; sinon paginez vous-même dans le
script.
Deux réflexes d’efficacité (coût en tokens) : enchaînez les appels dépendants dans un seul
script (ex. facture → client → participant) plutôt qu’un script par appel ; et ne renvoyez
que le résultat utile via console.log(JSON.stringify(resultat)) — pas le JSON brut complet
d’un enregistrement, qui gonfle le contexte inutilement.
Chaque source doit être autorisée : un connecteur via tools_enabled:["connection:<slug>"],
une base de connaissance via knowledge_base:["<slug>"] + le scope kb:read sur la clé. Le code
ne peut interroger que les sources que vous avez ainsi activées — ce qui permet de recouper
une base et un connecteur dans un seul script (ex. santé d’une participation ⇄ CA facturé).
Les outils d’un serveur MCP ne s’appellent pas depuis un script : ils restent des outils directs de l’assistant, là où une carte de validation peut s’afficher.
Recherche web dans le code
Section intitulée « Recherche web dans le code »Le script peut aussi chercher et lire des pages web, avec la MÊME garde (SSRF) et la MÊME
facturation que l’outil web_search direct :
const r = await miraca.web.search("actualités impact 2026"); // { results: [{ title, url, excerpt, published_at? }] }const p = await miraca.web.fetch("https://exemple.fr/actu"); // { text }, GRATUIT, jamais compté dans la borneUtile pour lire plusieurs pages d’un INDEX (une pagination, plusieurs requêtes à combiner) et ne
renvoyer que ce qui est nouveau ou pertinent, sans faire transiter chaque page par le contexte du
modèle. miraca.web.search est plafonné à un nombre de recherches par script (un message
explicite le dit si vous l’atteignez) ; miraca.web.fetch ne l’est pas. Comme les connecteurs,
la recherche web n’est disponible dans le code que si votre clé a le scope web_search et
que votre organisation a activé la recherche web. Le script d’un agent délégué
n’a pas accès à miraca.web : un agent cherche sur le web par son outil web_search.
Sécurité
Section intitulée « Sécurité »- Isolation : le script s’exécute dans un bac à sable sans réseau ni système de fichiers.
Il ne peut que appeler vos connecteurs via
astrolabe.*; aucune autre sortie possible. - Vos secrets ne sont jamais exposés au script : l’authentification des connecteurs est appliquée côté serveur. Le script reçoit des données, jamais vos clés.
- Permissions : un script ne peut utiliser que les connecteurs auxquels la clé a accès
(scope
connections+ autorisations par connexion). Les opérations en écriture restent soumises à leur autorisation habituelle. - Bornes : temps d’exécution et nombre de pages plafonnés.
Souveraineté & facturation
Section intitulée « Souveraineté & facturation »- L’exécution tourne sur notre infrastructure (UE) ; les données restent dans le pipeline européen. Router vers un modèle cloud reste soumis à l’opt-in habituel.
- Gratuit : seuls les tokens du modèle sont facturés (le calcul et les appels connecteurs ne le sont pas). Et comme les données ne passent plus par le contexte, ces questions analytiques coûtent bien moins de tokens qu’une boucle classique.
Quand l’utiliser (et quand non)
Section intitulée « Quand l’utiliser (et quand non) »| Question | Outil |
|---|---|
| « liste les factures de mars » | connecteur direct (connection:…) |
| « la facture la plus élevée », « le CA total 2026 », « la moyenne par client » | exécution de code |
| recouper plusieurs connecteurs et/ou bases de connaissance dans un même calcul | exécution de code |
| recherche de connaissances non structurées | base de connaissance (RAG) |
Écriture vers un système connecté : une limite
Section intitulée « Écriture vers un système connecté : une limite »Quand votre organisation, ou votre conversation, demande une validation humaine des écritures, un script ne peut pas modifier un système connecté : il dispose de soixante secondes et ne peut donc pas attendre votre décision. L’assistant doit alors appeler l’outil du connecteur directement, hors du code — c’est là que la carte de validation apparaît.
La lecture, elle, reste entière : miraca.connector(...).call() et .aggregate()
fonctionnent normalement sur les opérations de lecture.
.aggregate() ne s’applique qu’à une opération de lecture. Demandé sur une opération
d’écriture, il lève une erreur et rien n’est envoyé au système connecté : une écriture ne
passe que par .call(), où elle est contrôlée comme une écriture.