Aller au contenu

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 :

  1. la clé porte le scope code (Portail → Clés API) ;
  2. la requête passe tools_enabled: ["code"] — généralement avec un ou plusieurs connecteurs.
Fenêtre de terminal
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.

Le script appelle vos connecteurs (jamais le réseau brut) :

// appel brut d'une opération → JSON de l'API
const 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 appel
const 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
});

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é array ou object qui 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édiaire
const 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'autre
await 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.

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 borne

Utile 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.

  • 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.
  • 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.
QuestionOutil
« 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 calculexécution de code
recherche de connaissances non structuréesbase de connaissance (RAG)

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.