Aller au contenu

Gérer les bases via l'API

Vous pouvez tout piloter par API : créer des bases, y ajouter des documents et des extraits (« chunks »), et faire de la recherche directe (sans modèle). La même clé sk- que pour les appels au modèle authentifie ces requêtes.

Fenêtre de terminal
export AUTH="Authorization: Bearer $ASTROLABE_API_KEY"
export BASE="https://api.astrolabe.chat/v1/kb"
MéthodeRouteEffet
GET/v1/kbLister vos bases
POST/v1/kbCréer une base
GET/v1/kb/{slug}Détail d’une base
PATCH/v1/kb/{slug}Modifier (nom, slug, intention, facettes, type)
DELETE/v1/kb/{slug}Supprimer (+ documents + chunks)
GET/v1/kb/{slug}/exportExporter la base entière (bundle .kb.json.gz)
POST/v1/kb/importImporter un bundle dans une base neuve
Fenêtre de terminal
# Créer une base, avec un schéma de facettes (structure des métadonnées)
curl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{
"name": "FAQ Support",
"intent": "FAQ : une question/réponse par extrait.",
"facet_schema": { "facets": [
{ "key": "theme", "values": ["facturation", "securite", "compte"] },
{ "key": "montant", "type": "number", "description": "Montant TTC en euros, sans symbole." },
{ "key": "resolu", "type": "boolean", "default": "false" }
]}
}'
# → { "slug": "faq-support", "name": "FAQ Support", "facet_schema": {…}, … }

Le slug renvoyé est l’identifiant à utiliser partout ensuite (y compris dans knowledge_base lors des appels RAG).

Chaque entrée de facet_schema.facets contraint ce que l’IA doit extraire pour chaque extrait lors du découpage piloté par l’IA :

ChampTypeRôle
keystringRequis. Nom de la facette (ex. theme).
typestringForme de la valeur : text (défaut), number, boolean, date (ISO AAAA-MM-JJ). Force l’IA à renvoyer un nombre/booléen plutôt qu’un texte approximatif.
descriptionstring(optionnel) Intention : décrit en clair ce que l’IA doit mettre dans cette facette.
valuesstring[](optionnel, type text uniquement) Valeurs autorisées → l’IA est contrainte à cet enum.
defaultstring(optionnel) Valeur de repli si l’IA ne sait pas trancher (coercée selon le type).

Une facette sans type est traitée comme text (rétrocompatible). Les valeurs sont ensuite filtrables à la recherche (knowledge_base_facets).

À la création (POST /v1/kb) ou en modification (PATCH), une base a un role :

ValeurUsage
data (défaut)Base de données classique : des faits que l’IA restitue.
processBase de procédures (playbooks) : chaque document décrit, étape par étape, comment mener une analyse (quelles recherches lancer, quelles facettes filtrer, quoi vérifier).

En mode agentique, quand une base process est sélectionnée dans knowledge_base, l’IA la consulte d’abord pour trouver une fiche correspondant à la demande, suit ses étapes sur les bases de données, et se replie sur une démarche autonome si aucune fiche ne correspond. La sélection de la fiche reste à l’IA (pas d’injection forcée). Voir le guide RAG pour un exemple complet.

Fenêtre de terminal
# Créer une base de procédures
curl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{
"name": "Procédures — Fonds Impact",
"role": "process",
"intent": "Playbooks : comment mener nos analyses récurrentes."
}'
# → { "slug": "procedures-fonds-impact", "role": "process", … }

Le role est renvoyé par GET /v1/kb et GET /v1/kb/{slug}. Une base sans role explicite reste data.

MéthodeRouteEffet
GET/v1/kb/{slug}/documentsLister les documents
POST/v1/kb/{slug}/documentsAjouter un document — texte (JSON) ou fichier (multipart)
GET/v1/kb/{slug}/documents/{id}Détail
PATCH/v1/kb/{slug}/documents/{id}Modifier (filename, date, external_id, source_url)
GET/v1/kb/{slug}/documents/{id}/contentLire le texte source (déchiffré) + état des chunks
PUT/v1/kb/{slug}/documents/{id}/contentRemplacer le texte source (sans re-découper)
POST/v1/kb/{slug}/documents/{id}/rechunkRe-découper un document depuis sa source (régénère les chunks)
POST/v1/kb/{slug}/rechunkRe-découper toute la base (job de fond, asynchrone)
GET/v1/kb/{slug}/rechunkÉtat du dernier re-découpage de la base
DELETE/v1/kb/{slug}/documents/{id}Supprimer (+ ses chunks)

{id} accepte l’UUID renvoyé ou votre external_id (voir plus bas).

Fenêtre de terminal
# Ajouter du texte : il est découpé, vectorisé puis chiffré
curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{
"title": "Crédits",
"text": "Les crédits se rechargent dans le portail, par carte via Stripe.",
"date": "2024-03-01",
"external_id": "doc-credits",
"source_url": "https://docs.astrolabe.chat/credits"
}'
# → { "id": "…", "external_id": "doc-credits", "filename": "Crédits",
# "doc_date": "2024-03-01T00:00:00.000Z", "source_url": "https://docs.astrolabe.chat/credits",
# "status": "ready", "chunk_count": 1 }

Plutôt que de retenir l’UUID interne, attachez votre propre external_id (unique par base) à un document. Vous pouvez alors l’adresser directement (GET/PATCH/DELETE /v1/kb/{slug}/documents/{external_id}), et ré-ingérer le même document de façon idempotente :

Fenêtre de terminal
# Même external_id → le document (et ses chunks) est REMPLACÉ (upsert)
curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{
"text": "Texte mis à jour…", "external_id": "doc-credits"
}'
# Puis : récupérer / modifier / supprimer par cet identifiant
curl "$BASE/faq-support/documents/doc-credits" -H "$AUTH"

Le date est la date du contenu (≠ created_at, la date d’insertion en base). Stockée comme doc_date ; les chunks du document en héritent (chunk_date) sauf date explicite. Format YYYY-MM-DD (ou ISO 8601). Exploitable à la recherche via date_range (voir plus bas) : un filtre par plage de dates, là où les facettes ne permettent qu’une égalité.

Attachez à un document un lien vers la ressource d’origine (page web, fichier dans un drive, ticket…) via source_url. C’est purement informatif : stocké en clair, jamais vectorisé, et remonté tel quel à la recherche directe et dans les sources RAG — pour que votre process puisse retrouver la ressource d’origine d’un extrait. Disponible à l’ajout (texte ou fichier) et modifiable par PATCH (chaîne vide ou null pour le retirer).

Chaque document conserve son texte source canonique (chiffré, comme les chunks). Les chunks en sont une projection régénérable : vous pouvez relire la source, la modifier, puis re-découper pour régénérer les chunks et leurs embeddings.

Fenêtre de terminal
# Lire le texte source (déchiffré) + l'état des chunks
curl "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH"
# → { "id": "…", "filename": "Crédits", "has_source": true, "source_format": "markdown",
# "source_origin": "original", "content": "Les crédits se rechargent…",
# "chunk_count": 1, "chunks_stale": false }
# Remplacer le texte source (n'écrit PAS de chunks → ils deviennent « périmés »)
curl -X PUT "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH" -H "Content-Type: application/json" -d '{
"content": "Les crédits se rechargent dans le portail (carte via Stripe) ou par virement.",
"format": "markdown"
}'
# → { …, "chunks_stale": true } ← la source a changé, re-découpez pour régénérer
# Variante « tout-en-un » : remplacer la source ET re-découper dans le même appel
curl -X PUT "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH" -H "Content-Type: application/json" -d '{
"content": "Les crédits se rechargent dans le portail (carte via Stripe) ou par virement.",
"rechunk": "simple"
}'
# → { …, "chunks_stale": false } ← source écrite + chunks régénérés en une requête
# Re-découper depuis la source : régénère les chunks (simple = gratuit ; ai = facturé)
# `chunk_size` (200-6000, défaut 800) règle la taille cible des extraits du découpage simple.
curl -X POST "$BASE/faq-support/documents/doc-credits/rechunk" -H "$AUTH" -H "Content-Type: application/json" -d '{
"chunking": "simple",
"chunk_size": 1200
}'
# → le document (ApiDocument), "chunks_stale": false
  • source_origin : original (source fidèle, captée à l’ingestion ou éditée) ou reconstructed (source recollée depuis les chunks pour un document antérieur — pour un document découpé en IA, ce n’est pas l’original).
  • chunks_stale : true quand la source a été modifiée depuis le dernier découpage — exposé aussi sur le détail du document (GET …/documents/{id}). Re-découpez pour le remettre à jour.
  • POST …/rechunk accepte chunking ("simple" par défaut, ou "ai" + chunk_model), chunk_size (200-6000, défaut 800 — taille cible des extraits du découpage simple), facet_overrides (remplace ceux mémorisés à l’ingestion), bypass_conformite. Il remplace TOUS les chunks du document, y compris ceux ajoutés ou édités à la main.
  • PUT …/content accepte une option rechunk ("simple" ou "ai", + chunk_model, chunk_size, facet_overrides, bypass_conformite) pour écrire la source ET régénérer les chunks en un seul appel (pratique pour l’automatisation). Sans elle, les chunks restent périmés jusqu’au POST …/rechunk.
  • chunk_size est aussi accepté à l’ingestion (POST …/documents, JSON ou champ multipart) pour fixer la taille des extraits du découpage simple du nouveau document.

POST /v1/kb/{slug}/rechunk re-découpe tous les documents de la base depuis leur texte source. Comme l’opération peut porter sur des centaines de documents (et, en ai, appeler un modèle par document), elle s’exécute en job de fond : la requête enfile le travail et répond 202 avec un job_id et l’état initial. Suivez l’avancée avec GET /v1/kb/{slug}/rechunk.

Fenêtre de terminal
# Lancer le re-découpage de toute la base (asynchrone)
curl -X POST "$BASE/faq-support/rechunk" -H "$AUTH" -H "Content-Type: application/json" -d '{
"chunking": "simple",
"chunk_size": 1000
}'
# → 202 { "job_id": "…", "status": { "state": "running", "chunking": "simple", … } }
# Suivre l'avancée (polling)
curl "$BASE/faq-support/rechunk" -H "$AUTH"
# → { "status": { "state": "done", "total": 42, "rechunked": 40, "skipped": 2, "errors": 0, … } }
  • Body : chunking ("simple" par défaut, ou "ai" + chunk_model), chunk_size (mode simple).
  • Les documents sans texte source stocké ne sont pas re-découpables → comptés en skipped.
  • Un seul re-découpage à la fois par base : relancer pendant qu’il tourne renvoie 409.
  • Métré : en ai, chaque document appelle le modèle ; l’embedding des chunks est facturé (simple comme ai) sur la clé la plus récente du compte.

Passez facet_overrides pour imposer des valeurs de facettes sur tous les chunks du document, sans laisser l’IA les deviner :

Fenêtre de terminal
# Découpage simple : catégorie fixée, l'IA n'intervient pas
curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{
"title": "CR réunion 15 jan.",
"text": "…",
"external_id": "cr-2025-01-15",
"facet_overrides": { "categorie": "compte-rendu", "client": "Acme" }
}'
# Découpage IA : l'IA ne remplit que les facettes non couvertes (ex. "date_reunion")
curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{
"title": "CR réunion 15 jan.",
"text": "…",
"chunking": "ai",
"facet_overrides": { "categorie": "compte-rendu" }
}'
ComportementDétail
Mode simpleLes overrides sont appliqués à chaque chunk, les facettes restantes restent {}.
Mode aiL’IA ne voit que les facettes non couvertes (tokens économisés) et sa sortie est fusionnée ; les overrides écrasent toujours ce qu’elle aurait produit.
PrioritéLes overrides gagnent sur les valeurs IA et sur les default du schéma.
FormatObjet JSON ou chaîne JSON (pour les clients no-code comme Make).

Le même endpoint accepte un fichier en multipart/form-data (champ file). Le texte est extrait côté serveur puis découpé, vectorisé et chiffré comme un document texte. Le binaire n’est pas conservé.

Fenêtre de terminal
# Importer un PDF avec facettes forcées
curl "$BASE/faq-support/documents" -H "$AUTH" \
-F "file=@guide.pdf" \
-F "title=Guide d'installation" \
-F "source_url=https://drive.exemple.org/guide.pdf" \
-F 'facet_overrides={"categorie":"guide"}' \
-F "bypass_conformite=false"
# → { "id": "…", "filename": "Guide d'installation", "source_type": "file", "status": "ready", "chunk_count": 12 }

Formats : PDF, DOCX, TXT, MD, RTF, CSV, TSV, HTML (max 15 Mo). Le DOCX et le HTML sont convertis en Markdown (titres, listes et tableaux préservés), ce qui améliore le découpage (notamment le découpage IA) et l’extraction de facettes ; le PDF est extrait en texte à plat. Un .doc (Word legacy) ou un PDF scanné (image, sans couche texte) est refusé (415/422) — convertissez-le d’abord. Le champ title est optionnel (par défaut : le nom du fichier).

MéthodeRouteEffet
GET…/documents/{id}/chunksLister les extraits (déchiffrés, avec date et created_at)
POST…/documents/{id}/chunksAjouter un extrait à la main (content, facets?, date?)
PATCH…/chunks/{chunkId}Modifier (content, facets et/ou date)
DELETE…/chunks/{chunkId}Supprimer

{id} du document = UUID ou external_id. Le date d’un extrait est optionnel : s’il est absent, l’extrait hérite de la date du document.

Fenêtre de terminal
# Ajouter un extrait précis, avec ses facettes et sa date métier
curl "$BASE/faq-support/documents/doc-credits/chunks" -H "$AUTH" -H "Content-Type: application/json" -d '{
"content": "Q : Quels paiements ? R : Carte bancaire et virement SEPA.",
"facets": { "theme": "facturation" },
"date": "2024-03-01"
}'

Modifier le content d’un chunk le re-vectorise (facturé comme un embedding).

POST /v1/search cherche dans toutes vos bases à la fois (au lieu d’une seule), sans IA, et renvoie les documents correspondants. Pour aussi chercher dans une source externe (Airtable, Notion, Pennylane…), synchronisez le connecteur dans une base (cf. Connecteurs API) : ses enregistrements deviennent des documents et sont couverts par cette même recherche.

Deux régimes, via mode :

  • relevance (défaut si query) : classement par pertinence sémantique. L’embedding de la question est facturé. Pagination superficielle (top_k), pas de total.
  • browse (défaut sans query) : exploration — facettes + tri + recherche par titre (le texte query filtre alors le titre du document), avec pagination profonde (limit/offset) et total exact (nombre de documents, pas d’extraits). Aucun embedding facturé. C’est le régime « Algolia interne » : lister, filtrer, trier, paginer un jeu de documents.
Fenêtre de terminal
# Pertinence : classement sémantique
curl "https://api.astrolabe.chat/v1/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"query": "facture impayée Greenloop",
"knowledge_base": ["factures", "crm"]
}'
# Exploration : facettes + tri + pagination profonde
curl "https://api.astrolabe.chat/v1/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"mode": "browse",
"knowledge_base": ["factures"],
"filters": [{ "key": "statut", "op": "in", "value": ["impayée"] }],
"sort": { "by": "date", "dir": "desc" },
"limit": 25,
"offset": 50
}'
{
"query": "",
"mode": "browse",
"searched": ["factures"],
"total": 312,
"offset": 50,
"page_size": 25,
"has_more": true,
"results": [ { "knowledge_base": "factures", "document": "F-2023-6051 — GreenLoop", "external_id": "F-2023-6051", "date": "2023-04-11", "source_url": null, "matched_chunks": 3, "snippet": "", "score": null } ],
"sources": [ { "slug": "factures", "hits": 25 } ]
}
  • knowledge_base (slug ou tableau) : bases à interroger ; absent = toutes celles du compte.
  • filters : mêmes filtres de facettes typés que la recherche par base (eq/neq/gt/gte/lt/lte/in/contains). Une base qui ne déclare pas une facette filtrée est écartée.
  • sort (mode browse) : { "by": "date" | "alpha", "dir": "asc" | "desc" } (alpha = titre). Ignoré en pertinence (classé par score).
  • top_k (mode relevance, défaut 10, max 50) ; limit (mode browse, défaut 25, max 100) + offset (mode browse, pagination profonde).
  • total : nombre exact de documents (mode browse) ; has_more indique s’il reste des pages.
  • Compteurs au grain document : les facettes (/v1/search/facets ci-dessous) comptent des documents, pas des extraits internes.
  • Facettes cross-bases : POST /v1/search/facets (facettes filtrables, partagées d’abord) et POST /v1/search/facet-values ({ "key": "...", "query": "..." } → valeurs contenant le texte).

Note (chiffrement) : le corps des documents est chiffré au repos → pas de recherche plein-texte par mots-clés sur le contenu. L’exploration recherche par titre/external_id (en clair), et cette recherche est tolérante aux fautes (« recrutment » retrouve « recrutement »). Pour chercher dans le contenu, utilisez le mode pertinence (sémantique), lui aussi tolérant aux fautes (embeddings).

Interroger une base sans passer par un modèle : utile pour intégrer la recherche dans vos propres workflows. Renvoie les extraits les plus proches, déchiffrés, avec leur score.

Fenêtre de terminal
curl "$BASE/faq-support/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"query": "comment payer",
"facets": { "theme": "facturation" },
"top_k": 5
}'
{
"group_by": "chunk",
"data": [
{
"n": 1,
"knowledge_base": "faq-support",
"document": "Crédits",
"document_id": "a1b2c3d4-…",
"external_id": "doc-credits",
"source_url": "https://docs.astrolabe.chat/credits",
"score": 0.87,
"facets": { "theme": "facturation" },
"content": "Q : Quels paiements ? R : Carte bancaire et virement SEPA.",
"relation": "semantic"
}
]
}

La réponse contient toujours group_by (granularité utilisée) et data.

query est facultative. Sans requête, la base bascule en mode listing : pas de recherche sémantique, pas de score ("score": null), aucun coût d’embedding — uniquement le filtrage (facets, filters, date_range) et le tri. Idéal pour « tous les documents tels que … » et pour compter / lister de façon exacte (le listing est déterministe, là où la recherche sémantique est approximative et plafonnée).

Chaque résultat porte aussi l’external_id du document (votre identifiant client, s’il est renseigné) à côté de l’UUID interne.

Quand vous connaissez la référence métier d’un document (n° de facture, id CRM, id de note… c’est-à-dire votre external_id), ne partez pas en recherche sémantique approximative : passez external_id pour restreindre la recherche aux documents correspondants (une chaîne, ou un tableau). Combinable avec les autres paramètres.

Fenêtre de terminal
# Retrouver un document précis par sa référence, et lire ses extraits
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"external_id": "F-2025-089"
}'
# Plusieurs références d'un coup
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"external_id": ["F-2025-089", "F-2025-090"], "group_by": "document"
}'

Pour lire le contenu entier d’un document dont vous avez l’external_id, vous pouvez aussi appeler directement GET /v1/kb/{slug}/documents/{external_id} (l’UUID n’est pas nécessaire).

Pour retrouver un document par son nom/titre (et non par le sens de son contenu), passez title_contains : recherche sous-chaîne sur le titre (filename) et l’external_id, tolérante aux fautes (« recrutment » retrouve « recrutement »). Se combine aux filtres, au tri, à la pagination et au total analytique. Le contenu (chiffré) reste couvert par query (sémantique).

Fenêtre de terminal
# Documents dont le titre ressemble à « recrutement », comptés au grain document
curl "$BASE/rh/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"title_contains": "recrutment", "group_by": "document"
}'

top_k est plafonné à 50. En régime analytique (sans query), le défaut est d’ailleurs 50 (on veut généralement tout l’ensemble, pas un top-k). Pour parcourir un ensemble plus grand de façon exhaustive, utilisez offset (mode listing surtout) : offset: 0, puis 50, 100… jusqu’à recevoir moins de top_k résultats.

La réponse porte truncated: true quand la page ne contient pas tout l’ensemble filtré (total > nombre de lignes renvoyées) : ne concluez jamais « tous les X » sur une page tronquée — paginez (ou lisez total pour un simple comptage). Pour lister les valeurs distinctes d’une facette sous condition (toutes les entités concernées), GET …/facets avec le filtre en scope les renvoie toutes d’un coup, sans pagination.

Granularité : extraits, documents ou dates (group_by)

Section intitulée « Granularité : extraits, documents ou dates (group_by) »
  • "chunk" (défaut) : un résultat par extrait.
  • "document" : les extraits d’un même document sont regroupés (le document n’apparaît qu’une fois) et classés par score moyen des extraits trouvés. Chaque résultat porte document_id, matched_chunks (nombre d’extraits trouvés) et chunks (les extraits). C’est la granularité pour COMPTER ou LISTER des enregistrements (réunions, factures, dossiers… — un enregistrement = un document). En régime analytique (sans query), la réponse porte total = le compte exact de documents correspondant aux filtres, calculé côté serveur indépendamment de top_k/offset : pour « combien », lisez total (inutile de paginer). data reste plafonné à top_k (la page de résultats).
  • "date" : un résultat par date métier distincte (chunk_date), pour les questions par jour / chronologiques (combien de jours d’activité, occurrences par date). ⚠️ Plusieurs documents peuvent tomber le même jour : data.length = nombre de dates, ce qui sous-compte les enregistrements. La réponse expose donc, au niveau racine, distinct_documents (= nombre d’enregistrements, ≥ nombre de dates) et, par jour, documents (documents distincts ce jour-là), plus total_dates (nombre de jours, analytique). Pour « combien de rendez-vous avec X en 2025 », utilisez group_by: "document" et lisez total (ou distinct_documents en mode date), jamais le nombre de dates.
Fenêtre de terminal
curl "$BASE/faq-support/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"query": "comment payer",
"group_by": "document",
"top_k": 5
}'
{
"group_by": "document",
"data": [
{
"n": 1,
"knowledge_base": "faq-support",
"document": "Crédits",
"document_id": "a1b2c3d4-…",
"score": 0.86,
"matched_chunks": 2,
"chunks": [ { "n": 1, "content": "", "score": 0.87, "facets": {} } ]
}
]
}

En group_by: "date", la réponse porte distinct_documents (le nombre d’enregistrements) à côté des dates — ici 12 jours mais 13 réunions, car deux ont eu lieu le même jour :

{
"group_by": "date",
"total_dates": 12,
"distinct_documents": 13,
"data": [
{ "n": 1, "date": "2025-05-21", "documents": 2, "matched_chunks": 8, "facets": { }, "chunks": [ ] }
]
}

État courant par entité (group_by: { facet, latest } + having)

Section intitulée « État courant par entité (group_by: { facet, latest } + having) »

Pour les questions du type « combien de X sont actuellement Y » ou « le dernier état de chaque X » (X = une entité : startup, client, fournisseur… ; Y = un état : en alerte, en retard, churné…), passez un group_by objet : la base réduit à un résultat par entité, ramené à son enregistrement le plus récent. La condition d’état courant se pose avec having (≠ filters, voir l’encart) :

Fenêtre de terminal
curl "$BASE/suivi-participations/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"group_by": { "facet": "startup", "latest": true },
"having": { "key": "alerte", "op": "eq", "value": true }
}'
{
"group_by": "entity",
"entity_field": "startup",
"total": 11,
"data": [
{ "entity": "AgriBoost", "date": "2025-06-02", "facets": { "startup": "AgriBoost", "statut": "" } }
]
}
  • total = nombre d’entités distinctes dont le dernier enregistrement satisfait having — exact, indépendant de top_k.
  • aggregate se combine : aggregate: "mrr_keur" donne alors la valeur d’état courant agrégée (ex. le MRR moyen actuel du portefeuille = moyenne du dernier MRR de chaque startup).

En plus de facets (égalité simple), filters permet des comparaisons sur les valeurs de facettes, validées contre le type déclaré de chaque facette (cf. facet_schema) :

Fenêtre de terminal
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"group_by": "document",
"filters": [
{ "key": "montant", "op": "gte", "value": 1000 },
{ "key": "echeance", "op": "lt", "value": "2026-06-15" },
{ "key": "categorie", "op": "in", "value": ["A", "B"] }
],
"sort": { "by": "date", "dir": "desc" }
}'

Opérateurs : eq, neq, gt, gte, lt, lte, in, contains. Les opérateurs disponibles dépendent du type de la facette — nombre/date : comparaisons ; texte : eq/neq/in/ contains ; booléen : eq/neq. Un filtre incohérent (ex. > sur un booléen) ou une valeur incoercible est ignoré (jamais d’erreur). Les dates sont au format ISO AAAA-MM-JJ.

Pour sommer / moyenner une facette numérique (« montant moyen des factures », « total facturé », « MRR cumulé »), passez aggregate = le nom d’une facette numérique, ou une liste de noms. La réponse porte un champ aggregates : un objet par champ avec count, sum, avg, min, max, calculés exactement côté serveur et par enregistrement (un document = une valeur ; une facette dénormalisée sur plusieurs extraits n’est comptée qu’une fois), indépendamment de top_k/pagination. Régime analytique uniquement (sans query).

Fenêtre de terminal
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"facets": { "sens": "client", "type_piece": "facture" },
"aggregate": "montant_ttc_eur"
}'
{
"group_by": "chunk",
"total": 122,
"aggregates": [
{ "field": "montant_ttc_eur", "count": 122, "sum": 435155.91, "avg": 3566.85, "min": 484.13, "max": 12240 }
],
"data": []
}

count = nombre d’enregistrements ayant une valeur numérique (le dénominateur de avg). Les noms non numériques ou inconnus du schéma sont ignorés. N’additionnez jamais les montants à la main : lisez aggregates.

{ "by": …, "dir": "asc" | "desc" }by vaut :

  • "relevance" (défaut avec query) : par score décroissant. Exige une requête.
  • "date" : par date métier (chunk_date), valeurs absentes en dernier.
  • "created" : par date d’insertion en base (created_at) — l’ordre d’ajout, indépendant de la date métier.
  • "alpha" : par nom de document.

Sans query, relevance retombe automatiquement sur date desc.

Deux options avancées, identiques à celles du RAG :

  • date_range { from?, to? } (ISO) : ne retient que les extraits dont la date métier (chunk_date) tombe dans la plage (les extraits sans date sont alors écartés).
  • expand (requête uniquement) : en plus des hits sémantiques, ramène les extraits reliés par facettes (relation métier). Champs : seeds (graines, défaut 3), facets (clés de match ; omis = toutes), match ("all"/"any"), depth (niveaux, défaut 1), max_chunks (plafond, défaut 30, max 200).
Fenêtre de terminal
curl "$BASE/crm/search" -H "$AUTH" -H "Content-Type: application/json" -d '{
"query": "point sur le dossier",
"date_range": { "from": "2024-01-01" },
"expand": { "seeds": 3, "facets": ["client"], "depth": 1 }
}'

Les extraits ramenés par relation portent "relation": "facet" (et leur level) ; les hits sémantiques portent "relation": "semantic". ⚠️ L’expansion ajoute des extraits (donc des tokens en aval) : max_chunks borne le total ; match: "any" + depth > 1 font grossir l’arbre vite.

Une base entière s’exporte en bundle autonome (un fichier .kb.json.gz : métadonnées, documents, extraits déchiffrés et vecteurs embarqués), puis se réimporte ailleurs — autre compte, autre environnement, ou simple sauvegarde. Comme les vecteurs voyagent dans le bundle, l’import est instantané et gratuit : aucun ré-embedding, aucun coût.

Fenêtre de terminal
# Exporter → fichier gzip (le corps EST le .gz, ne pas le décompresser en transit)
curl -L "$BASE/faq-support/export" -H "$AUTH" -o faq-support.kb.json.gz
# Importer → crée une base NEUVE (slug auto-dédupliqué, jamais d'écrasement)
curl "$BASE/import" -H "$AUTH" \
-H "Content-Type: application/gzip" --data-binary @faq-support.kb.json.gz
# → { "slug": "faq-support-2", "document_count": 12, "chunk_count": 240, … }

L’envoi en corps brut (Content-Type: application/gzip ou application/json, --data-binary) est streamé de bout en bout : le bundle n’est jamais chargé en entier en mémoire, ce qui permet d’importer de grosses bases (plusieurs dizaines de milliers de documents) sans risque de saturation. Un envoi multipart/form-data (champ file) reste accepté (pratique depuis un formulaire). Nom et slug de la base créée sont repris du bundle ; on peut les surcharger via ?name= / ?slug= (ou les champs name / slug du formulaire).

PointDétail
CibleL’import crée toujours une base neuve — il ne fusionne ni n’écrase une base existante.
VecteursEmbarqués (base64 float32). Un extrait sans vecteur (bundle fabriqué à la main) est ré-embeddé à l’import (coûte un embedding, requiert une clé).
DimensionLe modèle d’embedding et la dimension (1024) doivent correspondre, sinon l’import est rejeté.
TailleBundle plafonné à 200 Mo (compressé). Import streamé (mémoire bornée) et par lots. Atomique : en cas d’erreur, aucune base partielle n’est créée.
ConfidentialitéLe bundle contient votre texte en clair : traitez-le comme une donnée sensible. À l’import, une clé de chiffrement neuve est générée pour la nouvelle base.

Au portail, ces actions sont les boutons « Exporter » (page d’une base) et « Importer » (liste des bases).

Le facet_schema déclare la structure des facettes (et l’enum des valeurs autorisées pour les facettes texte), mais pas les valeurs réellement présentes dans une base. Cette route les expose — pratique pour construire une UI de filtres ou laisser un agent découvrir sur quoi filtrer :

Fenêtre de terminal
curl "$BASE/factures/facets" -H "$AUTH"
{
"facets": [
{ "key": "theme", "type": "text", "declared_values": ["facturation","securite"],
"values": [ {"value":"facturation","count":2}, {"value":"securite","count":1} ],
"distinct_count": 2, "truncated": false },
{ "key": "montant", "type": "number", "min": 50, "max": 4200, "distinct_count": 3 },
{ "key": "echeance", "type": "date", "min": "2024-03-01", "max": "2026-06-15", "distinct_count": 3 }
]
}
  • text / boolean : values = valeurs présentes triées par fréquence (count), plafonnées (cap 50) → truncated: true au-delà ; distinct_count = nombre total de valeurs distinctes ; declared_values rappelle l’enum du schéma s’il existe.
  • number / date : min, max et distinct_count (pas la liste complète).
  • Couvre toutes les clés rencontrées, y compris des facettes hors schéma.
  • Gratuit (agrégation, pas d’embedding) ; le texte des extraits reste chiffré (seules les facettes, en clair, sont agrégées).

Restreindre à un sous-ensemble (scope) — superlatifs exacts

Section intitulée « Restreindre à un sous-ensemble (scope) — superlatifs exacts »

Ajoutez un scope en query params (JSON encodé) — facets, filters, date_range — pour n’agréger que les extraits correspondants. Les min/max portent alors sur ce filtre : c’est la façon exacte et déterministe de répondre à un superlatif numérique (« le MRR le plus élevé de X », « le montant max de la catégorie A »), sans scanner les extraits.

Fenêtre de terminal
# min/max d'une facette numérique pour une entité donnée
curl "$BASE/factures/facets?facets=%7B%22startup%22%3A%22ReFarm%22%7D" -H "$AUTH"
# → { "facets": [ { "key": "mrr_keur", "type": "number", "min": 18, "max": 127, … }, … ] }
  • Facturation : les opérations qui vectorisent (ajout de texte/chunk, modification de contenu, recherche) coûtent un embedding. Le reste du CRUD (lister, renommer, supprimer, créer une base, lister les valeurs de facettes) est gratuit.
  • Sécurité : le texte des extraits est chiffré au repos (une clé par base) ; il n’est déchiffré que pour vous répondre.
  • Codes : 401 clé invalide · 404 base/document/chunk introuvable · 400 requête invalide.