Context Compiler
A Huwise / Opendatasoft data portal compiled into tested context for AI agents. Development portal: data.education.gouv.fr.
A portal already gives agents an API for every dataset. What agents lack is context: which of five similar datasets is the right one, what a field means, how its values are written. This project compiles the portal's catalog into one short card per dataset, serves the cards over MCP with search boosted by real usage, and measures with a golden set whether the cards make an agent answer better.
Try it
Live, read-only MCP server (streamable HTTP): https://context.smiro.dev/mcp. Tools:
find_dataset, explain_dataset, query (ODSQL is checked against the real schema
before it reaches the portal).
claude mcp add --transport http context https://context.smiro.dev/mcp --header "X-Domain: data.education.gouv.fr"
Then ask: « Combien de collèges publics en Côte-d'Or ? » → 58.
For agents without MCP: llms.txt and 314 pages in datasets/. Source: github.com/SergeMiro/context-compiler-huwise.
Traps an agent falls into
Writing 30 reference queries by hand failed 11 times on the first run, on exactly these:
| trap | example |
|---|---|
| one department, three formats | Côte-d'Or is '021' in the directory, '21' in IPS datasets, "COTE D'OR" as a label |
| years stored as dates | session='2025' → API type error; year(session)=2025 works |
| numbers stored as text | avg(ips) fails on IPS écoles; order_by ips_etab desc sorts as strings |
| case differs | academie='DIJON' vs libelle_academie='Dijon' |
Cards carry real sample values for key fields and a hint for every date field, so the agent reads the format instead of guessing it.
Usage signal
The portal publishes the log of its own MCP server (39 675 agent calls, Feb–Nov 2026). It is used as a search signal: plain BM25 does not put the directory in the top 5 for « combien d'écoles en France »; with the signal it is #1.
| most used by agents | calls |
|---|---|
fr-en-annuaire-educationAnnuaire de l'éducation | 3389 |
fr-en-adresse-et-geolocalisation-etablissements-premier-et-second-degreAdresse et géolocalisation des établissements d'enseignement des premier et second degrés | 2186 |
fr-en-baccalaureat-par-academieLe baccalauréat par académie | 1006 |
fr-en-ecoles-effectifs-nb_classesEffectifs d’élèves par école | 1001 |
fr-en-indicateurs-de-resultat-des-lycees-gt_v2Indicateurs de valeur ajoutée des lycées d'enseignement général et technologique | 815 |
Does it help? Same agent, two toolsets
No full eval run yet: manage.py eval data.education.gouv.fr --mode both.
One card
Short card (≤ 400 tokens) returned by find_dataset, version 1.
## fr-en-annuaire-education — Annuaire de l'éducation Annuaire de l'éducation : données sur les établissements publics et privés ouverts situés en France. Le jeu de données couvre le premier degré, le second degré, les établissements médico-sociaux, les Centre d'Information et d'Orientation ainsi que les établissements administratifs. 68563 rows · updated 09.11.2026 · Licence Ouverte v2.0 (Etalab) · DNE - Ministère de l'Education Nationale Keys: identifiant_de_l_etablissement, code_postal (e.g. '02100'), code_commune, code_departement (e.g. '059'), code_academie, code_region, siren_siret Fields: nom_etablissement, type_etablissement, statut_public_prive, nom_commune, ecole_maternelle, ecole_elementaire, voie_generale, voie_technologique, voie_professionnelle, web, restauration, hebergement, … +52
One card edit
The improve loop proposes one edit per failing dataset; it is kept only if failing questions get fixed and no passing one breaks. A human reviews the diff in Django admin.
fr-en-annuaire-education · note · human · proposed
## fr-en-annuaire-education — Annuaire de l'éducation Annuaire de l'éducation : données sur les établissements publics et privés ouverts situés en France. Le jeu de données couvre le premier degré, le second degré, les établissements médico-sociaux, les Centre d'Information et d'Orientation ainsi que les établissements administratifs. Sources : données ONISEP et RAMSESE 68563 rows · updated 09.11.2026 · Licence Ouverte v2.0 (Etalab) · DNE - Ministère de l'Education Nationale Themes: Données de référence, structures, unités d'enseignement, établissements, écoles, EPLE, public, privé, internat, demi-pension, restauration, cité scolaire, CFA, lycées, collèges, lycées professionnels ### Fields - identifiant_de_l_etablissement · text · key · numéro de l'unité administrative immatriculée (uai) - code_postal · text · key · code postal · e.g. '02100', '01000', '02200', '01100', '02000' - code_commune · text · key · code insee de la commune - code_departement · text · key · code du département · e.g. '059', '013', '062', '069', '077' - code_academie · text · key · code de l'académie - code_region · text · key · Code_region - siren_siret · text · key · numéro SIRET de l'établissement (constitué du numéro SIREN sur 9 chiffres et du numéro NIC sur 5 chiffres) - nom_etablissement · text · nom de l'établissement - type_etablissement · text · type regroupé d'établissement : école, collège, lycée, EREA, ... · e.g. 'Ecole', 'Collège', 'Lycée', 'Médico-social', 'Service Administratif' - statut_public_prive · text · Statut public ou privé, si applicable · e.g. 'Public', 'Privé' - nom_commune · text · nom de la commune · e.g. 'Agen', 'Abbeville', 'Agde', 'Achères', 'Acoua' - ecole_maternelle · int · indique si l'établissement est une école maternelle · e.g. '1', '0' - ecole_elementaire · int · indique si l'établissement est une école élémentaire · e.g. '1', '0' - voie_generale · text · indique si l'établissement dispense les enseignements de la voie générale · e.g. '0', '1' - voie_technologique · text · indique si l'établissement dispense les enseignements de la voie technologique · e.g. '0', '1' - voie_professionnelle · text · indique si l'établissement dispense les enseignements de la voie professionnelle · e.g. '0', '1' - web · text · site web - restauration · int · indique si l'établissement dispose d'un service de restauration · e.g. '1', '0' - hebergement · int · indique si l'établissement dispose d'un service d'hébergement · e.g. '0', '1' - ulis · int · indique si l'établissement dispose d'une unité localisée pour l'inclusion scolaire · e.g. '0', '1' - apprentissage · text · indique si l'établissement propose des formations par apprentissage · e.g. '0', '1' - segpa · text · indique si l'établissement dispose d'une "section d'enseignement général et professionnel adapté" · e.g. '0', '1' - section_arts · text · indique si l'établissement possède une section arts · e.g. '0', '1' - section_cinema · text · indique si l'établissement possède une section cinéma · e.g. '0', '1' - section_theatre · text · indique si l'établissement possède une section théâtre · e.g. '0', '1' - section_sport · text · indique si l'établissement possède une section sport · e.g. '0', '1' - section_internationale · text · indique si l'établissement possède une section internationale · e.g. '0', '1' - section_europeenne · text · indique si l'établissement possède une section européenne · e.g. '0', '1' - lycee_agricole · text · indique si l'établissement est un lycée agricole · e.g. '0', '1' - lycee_militaire · text · indique si l'établissement est un lycée militaire · e.g. '0', '1' - lycee_des_metiers · text · Indique si l'établissement est labellisé "lycée des métiers" · e.g. '0', '1' - post_bac · text · Indique si l'établissement propose des formations après le baccalauréat · e.g. '0', '1' - appartenance_education_prioritaire · text · Indique si l'établissement est situé en réseau d'éducation prioritaire (REP), ou réseau d'éducation prioritaire renforcé (REP+) · e.g. 'REP', 'REP+' - greta · text · indique si l'établissement fait partie d'un GRETA (groupement d'établissements) · e.g. '0', '1' - fiche_onisep · text · url de la fiche onisep - type_contrat_prive · text · dans le cas d'un établissement privé, libellé du type de contrat - libelle_departement · text · nom du département · e.g. 'Nord', 'Bouches-du-Rhône', 'Pas-de-Calais', 'Rhône', 'Seine-et-Marne' - libelle_academie · text · nom de l'académie · e.g. 'Versailles', 'Lille', 'Créteil', 'Nantes', 'Bordeaux' - libelle_region · text · nom de la région · e.g. 'Ile-de-France', 'Auvergne-Rhône-Alpes', 'Hauts-de-France', 'Nouvelle-Aquitaine', 'Occitanie' - coordx_origine · double · première coordonnée géographique de l'établissement dans le système de coordonnées légal - coordy_origine · double · deuxième coordonnée géographique de l'établissement dans le système de coordonnées légal - epsg_origine · text · code EPSG du système de coordonnées légal - nom_circonscription · text · nom de la circonscription · e.g. 'Pôle Ecole Inclusive', 'Circonscription d'inspection du 1er degr', 'Circonscription 1er degré IEN Ecoles Pri', 'Circonscription d'inspection du 1er degr', 'Circonscription d'inspection du 1er degr' - latitude · double · latitude WGS84 de l'établissement - longitude · double · longitude WGS84 de l'établissement - precision_localisation · text · précision de la localisation géographique Valeurs possibles 1/ l'adresse ponctuelle a été vérifiée par un collecteur: 1. BÂTIMENT 2/ l'adresse ponctuelle corres - date_ouverture · date · date d'ouverture de l'établissement · filter by year: year(date_ouverture)=2025 - date_maj_ligne · date · date de mise à jour des informations · filter by year: year(date_maj_ligne)=2025 - … 23 more fields not shown ### Limits - records: limit <= 100 per call; to count rows use limit=0 and read total_count; string literals in single quotes; codes are text: keep leading zeros.
code_departement a 3 chiffres : Côte-d'Or = '021', Nord = '059' (les jeux IPS utilisent code_du_departement = '21')
piège observé en écrivant les questions de contrôle
Architecture
COLD PATH (commands / Celery; the only place an LLM is ever called)
Explore API --scan--> Portal / Dataset / Field (MariaDB = single source of truth)
| compile (deterministic)
v
Card v1..vN <-- kept ContextEdit
+------------+-------------+
v v v
ES `cards` Neo4j llms.txt + datasets/<id>.md
eval: EvalCase -> agent -> EvalRun / EvalResult
improve: failures -> LLM proposes edit -> compile -> eval -> kept | reverted
HOT PATH (MCP, no LLM, no writes except the ToolCall log)
find_dataset(question) -> ES (filter + routing by portal) + usage boost -> short cards
explain_dataset(id) -> full card + joinable datasets + lineage (Neo4j)
query(id, odsql) -> validator -> Explore API (client key passed through, limit <= 100)
Stack, the same as Huwise: Django + DRF, MariaDB, Celery + RabbitMQ, Elasticsearch, Neo4j, MCP Python
SDK. find_dataset p95 13 ms, explain_dataset p95 23 ms in process.