Aller au contenu

Espaces de travail

Un espace de travail est un jeu d’entrées nommées qui survivent d’un appel à l’autre : les fichiers que vous déposez, les fichiers que le modèle produit, et les valeurs intermédiaires qu’il enregistre (une synthèse, des totaux en JSON). Le modèle y accède par des outils, un script run_code y accède en cellules typées, et votre application y accède par l’API REST décrite ici.

C’est ce qui permet de trier, agréger ou transformer un tableau sans qu’aucune ligne ne passe par la réponse du modèle : vous déposez un classeur, le script le lit avec miraca.workspace.table(...), calcule, et produit un nouveau fichier. Le résultat est exact quelle que soit la taille du tableau, là où une recopie ligne à ligne dans un appel d’outil est lente, coûteuse et coupée au plafond de tokens.

Dans l’Assistant, chaque conversation possède son propre espace, alimenté automatiquement par les pièces jointes (voir Fichiers dans le chat). Cette page décrit les espaces que vous nommez et pilotez par l’API.

NatureD’où elle vientCe qu’elle porte
attachmentun fichier déposé (par l’API ou en pièce jointe)son texte extrait (même moteur que les bases de connaissance) et, pour un classeur, un csv ou un tsv, son rendu tabulaire typé (les montants sont des nombres, les dates des chaînes ISO)
fileun fichier produit par le modèle ou par un script (create_file, miraca.file.write)la référence du fichier généré ; son contenu se relit à la demande
text / jsonune valeur écrite par le modèle, par un script, ou par vousun texte, ou un JSON validé (une entrée json qui ne serait pas du JSON est refusée : elle mentirait sur sa nature)

Chaque entrée a un nom (unique dans l’espace, insensible à la casse), une origine (user, model, code, tool) et, quand un rendu tabulaire existe, la liste de ses feuilles avec leur nombre de lignes et de colonnes. Un nom déjà pris est remplacé.

L’origine tool désigne le résultat entier d’un outil (la réponse JSON d’un connecteur, le résultat d’un serveur MCP), déposé par Miraca quand il est volumineux : le modèle n’en reçoit alors que ce qui tient dans sa fenêtre, et relit ou traite le reste depuis l’espace. C’est une copie de travail. Elle est effacée 24 heures après la dernière activité de la discussion (pour un espace nommé par l’API : après sa dernière écriture), un espace n’en garde que les 40 plus récentes, et son nom ne porte que le connecteur ou le serveur, l’outil et un condensé, jamais les arguments de l’appel.

Un nom est rangé en forme Unicode composée (NFC) : un fichier venu de macOS, dont le nom arrive en forme décomposée, est donc rendu avec un nom différent à l’octet près de celui que vous avez envoyé, identique à l’œil. Les deux formes désignent la même entrée, en lecture comme en écriture.

Un nom recopié par un modèle est retrouvé à la typographie près. Un modèle qui recopie un nom lu dans la liste le déforme souvent : une espace insécable devient une espace, une apostrophe typographique est redressée, un accent est recomposé. Quand un nom ne correspond à aucune entrée à l’identique, la lecture et la suppression (par un outil, un script ou l’API) le rapprochent des noms de l’espace : si une seule entrée s’écrit de la même façon, c’est elle. Si plusieurs entrées s’écrivent de la même façon, aucune n’est choisie : l’erreur les nomme. Si aucune ne correspond, l’erreur cite d’abord l’entrée la plus proche, puis la liste de l’espace. L’écriture, elle, garde le nom exact.

Ce que le modèle ou un script écrit ne remplace jamais ce que vous avez déposé. Un fichier produit ou une valeur écrite sous le nom d’une entrée que vous avez déposée est rangé sous un nom voisin (ventes (2).xlsx), et la réponse indique le nom retenu. Votre fichier, son tableau et sa pastille dans la conversation restent intacts. Relire un document de connecteur, ou déposer à nouveau votre propre fichier, remplace l’entrée comme avant.

Ce que devient un tableau déposé. Les noms de colonnes sont rangés en forme composée, les blancs (retour à la ligne compris) repliés en une espace et les bords coupés ; une colonne sans nom s’appelle col_N, et un nom en double reçoit un suffixe (Montant, Montant (2)) : aucune colonne n’est perdue dans records. Un csv, un tsv ou un txt est lu en UTF-8, ou en windows-1252 quand il est massivement invalide en UTF-8 (l’export « CSV » d’Excel sous Windows) : les accents ne deviennent plus des caractères de remplacement. Un fichier presque entièrement en UTF-8 valide, avec quelques octets isolés qui n’en sont pas, reste lu en UTF-8 : ces octets deviennent � et la note d’extraction en donne le nombre. Les montants à la française (1 234,56, avec une espace insécable, fine insécable ou fine comme séparateur de milliers) sont des nombres ; 12,500 reste lu comme 12,5, la virgule est toujours une décimale.

Passez workspace: "<slug>" dans une requête de chat : le modèle reçoit les outils workspace_list, workspace_read, workspace_write et workspace_delete, avec la consigne de lire un tableau dans le code plutôt que d’en recopier les lignes. Ce paramètre suffit — vous n’avez pas besoin d’activer un autre outil pour que l’espace soit lisible. Il reçoit aussi l’index de l’espace : ce qui s’y trouve, avec le nombre de lignes et la forme des colonnes de chaque tableau. Un espace inexistant est créé au premier usage. Le paramètre est consommé par la plateforme : il n’atteint jamais le fournisseur du modèle.

{
"model": "mistral-small",
"workspace": "cloture-2026",
"tools_enabled": ["code"],
"messages": [
{ "role": "user", "content": "Trie transactions.xlsx par montant décroissant et produis le classeur." }
]
}

Un tableau volumineux est RÉSUMÉ dans la question

Section intitulée « Un tableau volumineux est RÉSUMÉ dans la question »

Quand vous joignez un classeur ou un csv un peu gros à une question, son contenu n’est pas recopié en entier dans le message envoyé au modèle : celui-ci reçoit un aperçu — les premières lignes de chaque feuille, la forme des colonnes et le nombre réel de lignes — et va chercher le reste dans l’espace de travail quand il en a besoin. Sur un classeur de 172 lignes, cela représente 793 tokens au lieu de 10 650, sans rien perdre : les lignes sont lues par le code, en cellules typées, et les calculs portent sur la totalité.

L’aperçu ne s’applique qu’aux tableaux (xlsx, csv, tsv) au-delà d’une certaine taille, et seulement quand le modèle dispose d’outils pour lire la suite. Un document texte, un PDF ou un petit fichier continuent d’être transmis intégralement.

Le scénario type : déposer d’abord, interroger ensuite. Votre application dépose un fichier par l’API REST (ci-dessous), puis pose sa question avec le même workspace. Le modèle voit le fichier dans l’index et, s’il a le scope code, le lit dans un script plutôt que d’en recopier les lignes. Après la réponse, vous relisez ce qui a été écrit dans l’espace, ou téléchargez le fichier produit.

Un fichier produit dans l’espace (document Word, présentation, page HTML, RTF, classeur) se relit par ce qui l’a produit, pas par ses octets : workspace_read rend le markdown d’un document, les diapositives d’une présentation (sous la forme des arguments de create_file), la page telle qu’elle a été écrite, avec une consigne qui dit comment le modifier : rappeler create_file avec le même nom de fichier et la source entière. La nouvelle version remplace l’ancienne dans l’espace. La lecture d’un classeur décrit ses feuilles ; sa spec complète, formules comprises, se lit dans un script :

const spec = JSON.parse(await miraca.workspace.read("budget.xlsx"));
spec.sheets[0].rows.push(["Total", { formula: "SUM(B2:B13)" }]);
await miraca.file.write(spec); // même nom : une nouvelle version

Les formats dont le fichier est déjà sa source (md, txt, csv, tsv, json, js, svg) se relisent tels quels. Voir Modifier un fichier, ou le reprendre dans une autre discussion.

Un document lu par un connecteur y est déposé automatiquement

Section intitulée « Un document lu par un connecteur y est déposé automatiquement »

Quand un script lit un document par un connecteur (un export Google Doc, une page, un fichier texte : toute réponse qui n’est pas du JSON et dépasse un millier de caractères), l’hôte le dépose lui-même dans l’espace de travail, sous un nom stable (<connecteur> · <opération> · <référence> · <condensé>.txt), et le dit à l’assistant. Aux questions suivantes, l’assistant le relit par workspace_read au lieu de le rechercher et de le retélécharger : l’historique d’une conversation ne conserve que le texte des messages, jamais les résultats d’outils, et sans ce dépôt un document lu au premier message était retrouvé et relu à chaque question. Le condensé porte sur tous les arguments de l’appel : relire le même document (mêmes arguments, dans n’importe quel ordre) remplace l’entrée, il n’en crée pas une seconde, et deux documents différents ont toujours deux entrées.

Avec tools_enabled: ["code"], le script accède à l’espace par miraca.workspace :

// ce qui existe → [{ name, kind, bytes, sheets? }]
const entries = await miraca.workspace.list();
// le texte entier d'une entrée
const note = await miraca.workspace.read("synthese.md");
// un tableau en cellules TYPÉES → { sheet, sheets, columns, rows, records, profile }
// `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 ; `profile` = une
// fiche par colonne : nature, signe, bornes, somme, valeurs distinctes, vides.
const t = await miraca.workspace.table("transactions.xlsx", { sheet: "Transactions" });
const montant = t.profile.find((p) => p.name === "Montant");
// montant.sign === "négatif" : des dépenses stockées avec leur signe — « le plus gros »
// est alors le plus négatif. Le profil est là pour qu'un script (et le modèle) le voie.
const total = t.records.reduce((s, r) => s + r.Montant, 0);
// écrire (texte ou objet, sérialisé en JSON) et supprimer
await miraca.workspace.write("totaux.json", { total });
await miraca.workspace.delete("brouillon.txt");

table() accepte le nom d’une feuille ou son index ; sans sheet, c’est la première. Une entrée sans rendu tabulaire (un PDF, un texte) lève une erreur explicite : lisez-la avec read(). À l’inverse, read() sur une entrée qui n’a pas de texte (un fichier Word produit, un classeur lu comme un texte) lève une erreur, qui renvoie vers table() pour un tableau : elle ne rend jamais une chaîne vide qu’on lirait comme un document vide.

Les colonnes de records se lisent à la typographie près. r["Montant (€)"] rend la colonne Montant (€) même quand son nom contient une espace insécable ou un accent décomposé, quand l’apostrophe est typographique, ou quand le nom a été coupé par … dans un aperçu (si un seul nom commence ainsi). Un nom qui ne correspond à aucune colonne lève une erreur qui liste les colonnes, les plus proches d’abord, au lieu de rendre undefined : un total sur une colonne absente ne se calcule plus en silence. Écrire sous une graphie voisine (r["montant (€)"] = 0) met à jour cette colonne au lieu d’en créer une seconde. Deux colonnes qui s’écrivent de la même façon sont signalées comme ambiguës. Les enregistrements restent des objets ordinaires (JSON.stringify, Object.keys, étalement, miraca.file.write).

Le profil de colonnes. Chaque entrée de profile décrit une colonne : kind (nombre, texte, date, booléen, vide, mixte), empty (cellules vides), puis selon la nature min, max, sum, negatives, positives, zeros et sign (négatif, positif, mixte, nul) pour un nombre, from/to et distinct pour une date, distinct pour un texte, trues/falses pour un booléen. Le même profil est donné à l’assistant quand il lit un tableau, et sa forme (nature, signe, vides, sans aucune valeur) est listée dans son contexte à chaque tour : c’est ce qui lui permet de voir qu’une consigne comme « du plus gros au plus faible » a deux lectures sur une colonne entièrement négative, et de dire celle qu’il retient.

Toutes les routes exigent une clé sk- portant le scope chat. Le propriétaire de l’espace est celui de la clé (le compte, ou l’organisation pour une clé d’organisation) : une clé ne voit que ses espaces.

RouteRôle
GET /v1/workspacesliste les espaces (slug, kind, dates)
POST /v1/workspaces { slug }crée un espace (201), ou rend l’existant (200)
GET /v1/workspaces/{slug}l’espace et l’index de ses entrées (404 s’il n’existe pas)
DELETE /v1/workspaces/{slug}supprime l’espace et ses entrées (204)
GET /v1/workspaces/{slug}/entriesl’index seul
POST /v1/workspaces/{slug}/entriesécrit une entrée : JSON { name, kind?, content } ou multipart (champ file, name? pour renommer) ; l’espace est créé s’il n’existe pas
GET /v1/workspaces/{slug}/entries/{name}lit une entrée : ?format=text (défaut, par tranches) ou ?format=table&sheet=… ; 404 qui cite l’entrée la plus proche puis la liste de l’espace, 409 si le nom désigne plusieurs entrées
PUT /v1/workspaces/{slug}/entries/{name} { content, kind? }remplace le contenu (201 à la création, 200 ensuite)
DELETE /v1/workspaces/{slug}/entries/{name}supprime l’entrée (204, 404 si absente, 409 si le nom désigne plusieurs entrées)

Un slug fait de 1 à 64 caractères (lettres, chiffres, ., -, _, :), commence par une lettre ou un chiffre, et se compare sans la casse. Le nom d’une entrée s’encode dans l’URL (encodeURIComponent) : les espaces et les accents y passent. En lecture et en suppression, un nom qui ne correspond à aucune entrée à l’identique est rapproché des noms de l’espace à la typographie près (espace insécable, apostrophe typographique, forme Unicode) ; la réponse rend le nom réel de l’entrée.

Fenêtre de terminal
curl -X POST https://api.miraca.fr/v1/workspaces/cloture-2026/entries \
-H "Authorization: Bearer sk-..." \
-F "file=@transactions.xlsx"
{
"entry": {
"name": "transactions.xlsx",
"kind": "attachment",
"origin": "user",
"mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"bytes": 18432,
"sheets": [{ "name": "Transactions", "rows": 172, "columns": 11 }],
"file_id": null,
"updated_at": "2026-09-09T14:03:11.000Z"
},
"extraction": { "truncated": false }
}

Formats acceptés : ceux des fichiers dans le chat (PDF, Word, Excel, CSV, TSV, texte, Markdown, HTML, RTF), 15 Mo au plus (413 au-delà). Un fichier illisible (PDF scanné, format inconnu) est refusé en 400 avec le motif. Le champ name renomme l’entrée ; comme c’est l’extension qui décide du format lu, un nom donné sans extension hérite de celle du fichier. truncated vaut true quand le rendu tabulaire dépasse 50 000 lignes par feuille : les premières lignes sont conservées, et le texte l’annonce.

Fenêtre de terminal
curl "https://api.miraca.fr/v1/workspaces/cloture-2026/entries/transactions.xlsx?format=table&sheet=Transactions" \
-H "Authorization: Bearer sk-..."
{
"name": "transactions.xlsx",
"sheet": "Transactions",
"sheets": ["Transactions", "Paramètres"],
"columns": ["Date", "Libellé", "Montant", "Catégorie"],
"rows": [
["2026-01-05", "Frais bancaires", -76.37, "Frais bancaires"],
["2026-01-06", "Virement client", 1200, "Ventes"]
],
"profile": [
{ "name": "Date", "kind": "date", "from": "2026-01-05", "to": "2026-01-06", "distinct": 2, "empty": 0, "filled": 2 },
{ "name": "Libellé", "kind": "texte", "distinct": 2, "empty": 0, "filled": 2 },
{ "name": "Montant", "kind": "nombre", "sign": "mixte", "min": -76.37, "max": 1200, "sum": 1123.63,
"negatives": 1, "positives": 1, "zeros": 0, "empty": 0, "filled": 2 },
{ "name": "Catégorie", "kind": "texte", "distinct": 2, "empty": 0, "filled": 2 }
]
}

rows ne porte pas l’en-tête. profile est la fiche de chaque colonne (voir « Dans un script run_code ») ; la liste des entrées porte la même information sous forme réduite, sheets[].fields (nature, signe, vides), sans aucune valeur. Sans rendu tabulaire (un PDF), ou pour une feuille inconnue, la réponse est un 400 qui nomme les feuilles disponibles.

Fenêtre de terminal
curl "https://api.miraca.fr/v1/workspaces/cloture-2026/entries/synthese.md?offset=0&limit=8000" \
-H "Authorization: Bearer sk-..."

La réponse porte text, offset, next_offset (null quand il ne reste rien) et total_chars, avec les métadonnées de l’entrée. limit vaut 8 000 caractères par défaut, 32 000 au plus. Pour une pièce jointe, text est le texte extrait (une table Markdown par feuille pour un classeur) ; pour un fichier binaire produit (Word, PowerPoint), il n’y a pas de texte et la réponse le dit (note). ⚠️ Une entrée file dont le fichier sous-jacent a été supprimé (rétention, ou suppression manuelle depuis la page Fichiers) le dit aussi, mais avec une note DISTINCTE (deleted: true) : ce n’est pas un format non lisible, c’est un fichier qu’il faut régénérer.

Fenêtre de terminal
curl -X POST https://api.miraca.fr/v1/workspaces/cloture-2026/entries \
-H "Authorization: Bearer sk-..." -H "Content-Type: application/json" \
-d '{ "name": "parametres.json", "kind": "json", "content": { "exercice": 2026, "seuil": 500 } }'

content accepte une chaîne ou un objet (sérialisé côté serveur, forme pratique depuis un outil no-code). Plafond : 4 Mo par entrée, 200 entrées par espace.

  • Le propriétaire est la clé. Un espace n’est jamais désigné par un identifiant de compte dans la requête : c’est la clé qui dit à qui il appartient, et une clé d’organisation ne voit que les espaces de l’organisation.
  • Le contenu est chiffré au repos, sous une clé propre à chaque espace (même schéma à enveloppe que les bases de connaissance et les conversations). Seules les métadonnées (noms, natures, tailles, feuilles) restent en clair : c’est ce que l’index liste.
  • Les espaces de conversation sont inaccessibles par clé. Leur slug commence par conversation: ; ce préfixe est refusé par l’API (400) comme par le paramètre workspace. Une conversation appartient à une personne, pas à une clé.
  • Supprimer un espace supprime ses entrées. Les fichiers produits référencés par une entrée file restent, eux, soumis à leur propre conservation.