From dcb0a3a448c31fc4c78865fa095d4e6631745c02 Mon Sep 17 00:00:00 2001 From: Mobiletic Date: Wed, 1 Jul 2026 15:22:00 +0100 Subject: [PATCH] docs: add examples/ (live demo + RESULTS) and slim the README examples - examples/demo.mjs: live demo (5 single messages up to 5 people with rare auto-coined attributes + 5 escalating multi-turn conversations with long-range back-references); self-references the package via exports - examples/RESULTS.md: its generated output (10 examples) - examples/.env.example + examples/README.md; `npm run example` script - README: replace the long examples section with two representative examples (dense 5-person message + 6-turn conversation) and a badge/link to examples/RESULTS.md for the full set - eslint ignores examples/**, prettier ignores generated examples/RESULTS.md, .gitignore un-ignores .env.example Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + .prettierignore | 1 + README.md | 145 +++----------- eslint.config.js | 2 +- examples/.env.example | 6 + examples/README.md | 24 +++ examples/RESULTS.md | 428 ++++++++++++++++++++++++++++++++++++++++++ examples/demo.mjs | 287 ++++++++++++++++++++++++++++ package.json | 1 + 9 files changed, 771 insertions(+), 124 deletions(-) create mode 100644 examples/.env.example create mode 100644 examples/README.md create mode 100644 examples/RESULTS.md create mode 100644 examples/demo.mjs diff --git a/.gitignore b/.gitignore index c03a5b1..893b214 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage/ .DS_Store .env .env.* +!.env.example diff --git a/.prettierignore b/.prettierignore index 012fbaa..d75a1db 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ dist coverage node_modules package-lock.json +examples/RESULTS.md diff --git a/README.md b/README.md index 768480a..b2d42ea 100644 --- a/README.md +++ b/README.md @@ -252,141 +252,40 @@ Three distinct outputs, with different sensitivities: ## Examples -Real learner ↔ platform chat messages, anonymized **live by Gemma 4** (via Infomaniak) with no regex -fallback. All personal data below is **fictional**. `mapping` is the secret re-identification key (kept by -the operator); `legend` is safe to send to the downstream model. Every case restores identically. +Two examples produced **live by Gemma 4** on fictional Swiss e-learning data. `mapping` is the secret +re-identification key (kept by the operator); `legend` is safe to send downstream. The model even **coins +its own abbreviations** for attributes not in the base prompt (passport, permit, blood type, …). -| # | Scenario | Sensitive data detected | Notable capability | Round-trip | -| --- | ----------------------- | --------------------------------------------------- | ----------------------------------------- | ---------- | -| 1 | Course signup | name, e-mail | baseline detection | ✅ | -| 2 | Login problem | + username, phone, signup date | coins `[PER_1.ID_USER:LOGIN]` on the fly | ✅ | -| 3 | Billing / IBAN change | + address, **two IBANs**, AVS | old vs new IBAN kept **distinct** | ✅ | -| 4 | HR enrolls staff | **3 people** + org, e-mails, phones, DOB, IBAN | coreference (same person → same id) + org | ✅ | -| 5 | Parent + minor + health | 3 people, DOB, address, contacts, IBAN, maiden name | minor/guardian/doctor kept distinct | ✅ | +[![See all examples](https://img.shields.io/badge/examples-see_all-6f42c1)](./examples/RESULTS.md)   +5 single messages of rising complexity **+** 5 multi-turn conversations, with full mappings & legends. -### Case 1 — course signup (baseline) +### A dense single message — 5 people + rare attributes **User message:** -> Bonjour, je suis Jean Dupont et mon adresse e-mail est jean.dupont@yopmail.com. Je viens de m'inscrire à la formation « Bureautique de base » et je voulais confirmer que tout est en ordre. Merci d'avance ! +> Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) … Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent … Le Dr Farah Haddad (n° RCC V123456) a ouvert le dossier médical DM-2025-0417 … l'agent Kevin Zbinden de la caisse Helvetia … Noah utilise l'identifiant biométrique BIO-7729. -**Anonymized — what the model sees:** +**Anonymized — what the model sees** (5 distinct people `PER_1…PER_5` + `ORG_1`; `Noah` reused at the end): -> Bonjour, je suis [PER_1.PRENOM:M] [PER_1.NOM:M] et mon adresse e-mail est [PER_1.EMAIL:PERSO]. Je viens de m'inscrire à la formation « Bureautique de base » et je voulais confirmer que tout est en ordre. Merci d'avance ! +> Note de dossier complète : le patient mineur [PER_1.PRENOM:U] [PER_1.NOM:U] (né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin B+, numéro d'assuré maladie [PER_1.ASSUR_ID:U], allergique aux arachides) … Ses parents, [PER_2.PRENOM:F] [PER_2.NOM:F] (mère, permis de séjour [PER_2.PERMIS:C], [PER_2.TELEPHONE:U]) et [PER_3.PRENOM:M] [PER_3.NOM:M] (père, passeport italien [PER_3.PASSPORT:U]), cosignent … Le Dr [PER_4.PRENOM:F] [PER_4.NOM:F] (n° RCC [PER_4.RCC:U]) a ouvert le dossier médical DM-2025-0417 … l'agent [PER_5.PRENOM:M] [PER_5.NOM:M] de la caisse [ORG_1.NOM:Assurance] … [PER_1.PRENOM:U] utilise l'identifiant biométrique [PER_1.BIO_ID:U]. -| `mapping` 🔒 (secret) | value | | `legend` 🏷️ (shareable) | meaning | -| --------------------- | ----------------------- | --- | ----------------------- | ------------------ | -| `[PER_1.PRENOM:M]` | Jean | | `PER` | Personne | -| `[PER_1.NOM:M]` | Dupont | | `PRENOM` / `NOM` | Prénom / Nom | -| `[PER_1.EMAIL:PERSO]` | jean.dupont@yopmail.com | | `EMAIL` · `M` · `PERSO` | E-mail · M · Perso | +Attributes **coined by the model** (not in the prompt): `ASSUR_ID`, `PERMIS`, `PASSPORT`, `RCC`, `BIO_ID`. -Note the course name « Bureautique de base » is **left intact** — it isn't personal data. +### A multi-turn conversation — 4 people; blood group recalled turn 1 → turn 6 -### Case 4 — HR enrolls staff (coreference + organization) +| Turn | User message | Anonymized (model sees) | +| ---- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| 1 | …mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+. | …mon fils mineur, [PER_1.PRENOM:U] [PER_1.NOM:U], né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin [PER_1.SANG:B+]. | +| 3 | Moi, sa mère Delphine Rieder … permis de séjour de type C. | Moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F] … permis de séjour de type [PER_2.PERMIS:C]. | +| 4 | Son père, Marco Baumann, passeport italien YA9087654, … | Son père, [PER_3.PRENOM:M] [PER_1.NOM:U], passeport italien [PER_3.PASSPORT:ITA], … | +| 5 | Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah. | Le [PER_4.TITRE:MED] [PER_4.PRENOM:F] [PER_4.NOM:F] a ouvert le dossier médical [PER_1.DOSSIER:MED] pour [PER_1.PRENOM:U]. | +| 6 | Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge… | Rappel : le groupe sanguin [PER_1.SANG:B+] de [PER_1.PRENOM:U] doit figurer… | -**User message:** +Noah stays `PER_1` across all six turns, and **his blood group `[PER_1.SANG:B+]` from turn 1 is reused in +turn 6** — long-range coreference holds. (The father shares the surname, so `[PER_1.NOM:U]` is reused for +it — the same value maps to the same token.) -> Bonjour, je suis Sophie Meyer, responsable formation chez Nestlé Suisse (sophie.meyer@nestle.com, +41 21 924 11 11). Je souhaite inscrire deux collaborateurs à la formation « Sécurité au travail » qui débute le 03/03/2026 : Marc Rossi, né le 12.03.1990 (marc.rossi@nestle.com), et Amélie Girard, joignable au 078 321 65 43. La facture est à adresser à notre comptabilité, IBAN CH93 0076 2011 6238 5295 7. Marc Rossi avait déjà suivi une formation l'an dernier — pouvez-vous réactiver son ancien compte plutôt que d'en créer un nouveau ? - -**Anonymized — what the model sees** (note **Marc Rossi → `PER_2` in both mentions**, and the org as `ORG_1`): - -> Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], responsable formation chez [ORG_1.NOM:Entreprise] ([PER_1.EMAIL:F], [PER_1.TELEPHONE:F]). Je souhaite inscrire deux collaborateurs à la formation « Sécurité au travail » qui débute le 03/03/2026 : [PER_2.PRENOM:M] [PER_2.NOM:M], né le [PER_2.DATE_NAISSANCE:1990] ([PER_2.EMAIL:M]), et [PER_3.PRENOM:F] [PER_3.NOM:F], joignable au [PER_3.TELEPHONE:F]. La facture est à adresser à notre comptabilité, IBAN [ORG_1.IBAN:Comptabilite]. [PER_2.PRENOM:M] [PER_2.NOM:M] avait déjà suivi une formation l'an dernier — pouvez-vous réactiver son ancien compte plutôt que d'en créer un nouveau ? - -**`mapping` 🔒 (secret — kept on your side):** - -| Placeholder | Real value | -| ----------------------------- | -------------------------- | -| `[PER_1.PRENOM:F]` | Sophie | -| `[PER_1.NOM:F]` | Meyer | -| `[ORG_1.NOM:Entreprise]` | Nestlé Suisse | -| `[PER_1.EMAIL:F]` | sophie.meyer@nestle.com | -| `[PER_1.TELEPHONE:F]` | +41 21 924 11 11 | -| `[PER_2.PRENOM:M]` | Marc | -| `[PER_2.NOM:M]` | Rossi | -| `[PER_2.DATE_NAISSANCE:1990]` | 12.03.1990 | -| `[PER_2.EMAIL:M]` | marc.rossi@nestle.com | -| `[PER_3.PRENOM:F]` | Amélie | -| `[PER_3.NOM:F]` | Girard | -| `[PER_3.TELEPHONE:F]` | 078 321 65 43 | -| `[ORG_1.IBAN:Comptabilite]` | CH93 0076 2011 6238 5295 7 | - -**`legend` 🏷️ (shareable — sent to the downstream model):** `PER`=Personne, `PRENOM`=Prénom, `NOM`=Nom de -famille, `F`=Féminin, `M`=Masculin, `ORG`=Organisation, `EMAIL`=Adresse e-mail, `TELEPHONE`=Numéro de -téléphone, `DATE_NAISSANCE`=Date de naissance, `IBAN`=Numéro de compte bancaire. - -
-More examples — Case 2 (login), Case 3 (two IBANs), Case 5 (minor + health) - -#### Case 2 — login problem (coins an abbreviation for the username) - -**User message:** - -> Salut, moi c'est Marie Favre. J'ai créé mon compte avec l'e-mail marie.favre@bluewin.ch le 15/02/2026 mais je n'arrive plus à me connecter. Mon identifiant est mfavre et vous pouvez me joindre au 079 456 78 90. Pouvez-vous réinitialiser mon accès à la formation « Machiniste » ? - -**Anonymized:** - -> Salut, moi c'est [PER_1.PRENOM:F] [PER_1.NOM:F]. J'ai créé mon compte avec l'e-mail [PER_1.EMAIL:PERSO] le [PER_1.DATE_CREATION:2026] mais je n'arrive plus à me connecter. Mon identifiant est [PER_1.ID_USER:LOGIN] et vous pouvez me joindre au [PER_1.TELEPHONE:MOBILE]. Pouvez-vous réinitialiser mon accès à la formation « Machiniste » ? - -| Placeholder | Real value | -| ---------------------------- | ---------------------- | -| `[PER_1.PRENOM:F]` | Marie | -| `[PER_1.NOM:F]` | Favre | -| `[PER_1.EMAIL:PERSO]` | marie.favre@bluewin.ch | -| `[PER_1.DATE_CREATION:2026]` | 15/02/2026 | -| `[PER_1.ID_USER:LOGIN]` | mfavre | -| `[PER_1.TELEPHONE:MOBILE]` | 079 456 78 90 | - -`ID_USER` and `DATE_CREATION` are **coined by the model** — not in the base vocabulary — and documented in -the legend. - -#### Case 3 — billing / IBAN change (two different IBANs kept distinct) - -**User message:** - -> Bonjour, je m'appelle Luc Berset, domicilié au 14 avenue de la Gare, 1700 Fribourg. J'ai un souci avec le paiement de la formation « Comptabilité PME » (CHF 1'200.–) : mon IBAN CH93 0076 2011 6238 5295 7 n'est plus valide, je souhaite le remplacer par CH56 0483 5012 3456 7800 9. Si besoin, mon numéro AVS est le 756.1234.5678.90. Merci de mettre à jour mon dossier. - -**Anonymized** (old and new IBAN get **distinct** placeholders via context): - -> Bonjour, je m'appelle [PER_1.PRENOM:M] [PER_1.NOM:M], domicilié au [PER_1.ADRESSE:Rue], [PER_1.ADRESSE:NPA] [PER_1.ADRESSE:Ville]. J'ai un souci avec le paiement de la formation « Comptabilité PME » (CHF 1'200.–) : mon IBAN [PER_1.IBAN:Ancien] n'est plus valide, je souhaite le remplacer par [PER_1.IBAN:Nouveau]. Si besoin, mon numéro AVS est le [PER_1.AVS:Suisse]. Merci de mettre à jour mon dossier. - -| Placeholder | Real value | -| ----------------------- | -------------------------- | -| `[PER_1.PRENOM:M]` | Luc | -| `[PER_1.NOM:M]` | Berset | -| `[PER_1.ADRESSE:Rue]` | 14 avenue de la Gare | -| `[PER_1.ADRESSE:NPA]` | 1700 | -| `[PER_1.ADRESSE:Ville]` | Fribourg | -| `[PER_1.IBAN:Ancien]` | CH93 0076 2011 6238 5295 7 | -| `[PER_1.IBAN:Nouveau]` | CH56 0483 5012 3456 7800 9 | -| `[PER_1.AVS:Suisse]` | 756.1234.5678.90 | - -#### Case 5 — parent, minor child and health (dense coreference) - -**User message:** - -> Bonjour, je vous écris au sujet de mon fils, Lucas Favre, né le 04.07.2011, que j'aimerais inscrire à la formation junior « Robotique » à Lausanne. Étant mineur, c'est moi, sa mère Camille Favre, qui gère le dossier — vous pouvez me joindre au 021 555 12 34 ou à camille.favre@bluewin.ch, nous habitons au 8 chemin des Vignes, 1009 Pully. Lucas est asthmatique ; son médecin, le Dr Nadia Benali (cabinet à Renens), a établi un certificat le 15.05.2024. Par ailleurs, j'avais moi-même suivi la formation « Photographie » en 2023 sous mon nom de jeune fille, Camille Rochat — mes deux comptes peuvent-ils être fusionnés ? Le paiement se fera depuis mon IBAN CH56 0483 5012 3456 7800 9. - -**Anonymized** (son = `PER_1`, mother = `PER_2` incl. her maiden name, doctor = `PER_3`): - -> Bonjour, je vous écris au sujet de mon fils, [PER_1.PRENOM:M] [PER_1.NOM:M], né le [PER_1.DATE_NAISSANCE:2011], que j'aimerais inscrire à la formation junior « Robotique » à [LOC_1.VILLE:Lausanne]. Étant mineur, c'est moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F], qui gère le dossier — vous pouvez me joindre au [PER_2.TELEPHONE:Fixe] ou à [PER_2.EMAIL:Privé], nous habitons au [PER_2.ADRESSE:Rue], [PER_2.ADRESSE:NPA] [PER_2.ADRESSE:Ville]. [PER_1.PRENOM:M] est asthmatique ; son médecin, le Dr [PER_3.PRENOM:F] [PER_3.NOM:F] (cabinet à [LOC_2.VILLE:Renens]), a établi un certificat le 15.05.2024. Par ailleurs, j'avais moi-même suivi la formation « Photographie » en 2023 sous mon nom de jeune fille, [PER_2.PRENOM:F] [PER_2.NOM_JEUNE_FILLE:F] — mes deux comptes peuvent-ils être fusionnés ? Le paiement se fera depuis mon IBAN [PER_2.IBAN:Principal]. - -| Placeholder | Real value | -| ------------------------------------------------- | ---------------------------------- | -| `[PER_1.PRENOM:M]` / `[PER_1.NOM:M]` | Lucas / Favre | -| `[PER_1.DATE_NAISSANCE:2011]` | 04.07.2011 | -| `[PER_2.PRENOM:F]` / `[PER_2.NOM:F]` | Camille / Favre | -| `[PER_2.NOM_JEUNE_FILLE:F]` | Rochat | -| `[PER_2.TELEPHONE:Fixe]` | 021 555 12 34 | -| `[PER_2.EMAIL:Privé]` | camille.favre@bluewin.ch | -| `[PER_2.ADRESSE:Rue/NPA/Ville]` | 8 chemin des Vignes / 1009 / Pully | -| `[PER_3.PRENOM:F]` / `[PER_3.NOM:F]` | Nadia / Benali | -| `[LOC_1.VILLE:Lausanne]` / `[LOC_2.VILLE:Renens]` | Lausanne / Renens | -| `[PER_2.IBAN:Principal]` | CH56 0483 5012 3456 7800 9 | - -The health detail ("asthmatique") is kept as non-identifying context, and the reference chain -("mon fils" / "sa mère" / "son médecin") is resolved into three distinct entities. - -
+**[→ See all 10 examples, with full `mapping` and `legend` tables](./examples/RESULTS.md)** ## How this helps with the nLPD diff --git a/eslint.config.js b/eslint.config.js index 13834f6..2b59139 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,7 @@ import tseslint from 'typescript-eslint'; import prettier from 'eslint-config-prettier'; export default tseslint.config( - { ignores: ['dist/**', 'coverage/**', 'node_modules/**'] }, + { ignores: ['dist/**', 'coverage/**', 'node_modules/**', 'examples/**'] }, js.configs.recommended, ...tseslint.configs.recommended, prettier, diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 0000000..e896cfd --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,6 @@ +# Copy to examples/.env and fill in — .env is git-ignored, never commit your key. +# Any OpenAI-compatible endpoint works; the package appends "/chat/completions". +INFOMANIAK_AI_BASE_URL= # e.g. https://api.infomaniak.com/2/ai//openai/v1 +INFOMANIAK_AI_API_KEY= # your API token +GEMMA_MODEL= # model id (e.g. google/gemma-4-31B-it) +PRIVACY_GEMMA_TIMEOUT_MS=60000 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..88b457d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# Live example + +[`demo.mjs`](./demo.mjs) runs `@mobiletic/anonymizer` against a real +OpenAI-compatible LLM (built with Gemma 4 via Infomaniak) on a set of fictional +Swiss e-learning chats: 5 single messages of rising complexity (up to 5 people, +with rare attributes the model must coin itself) and 5 multi-turn conversations +(with long-range back-references across turns). It writes the formatted output to +[`RESULTS.md`](./RESULTS.md). + +> This is a **live example**, not a CI unit test — it needs an API key and calls a +> real endpoint. The deterministic test suite lives in [`../test`](../test). + +## Run + +```bash +cd .. +npm install +npm run build # examples/demo.mjs imports the built package +cp examples/.env.example examples/.env # then fill in your endpoint + key +npm run example # → regenerates examples/RESULTS.md +``` + +`examples/.env` is git-ignored. All personal data in the scripts and results is +**fictional**, generated for demonstration. diff --git a/examples/RESULTS.md b/examples/RESULTS.md new file mode 100644 index 0000000..3989b46 --- /dev/null +++ b/examples/RESULTS.md @@ -0,0 +1,428 @@ +## Examples + +> Real learner ↔ platform chat messages anonymized **live by Gemma 4** (via Infomaniak). All personal +> data is **fictional**. `mapping` is the secret re-identification key (kept by the operator); `legend` +> is safe to send downstream. **🆕 = an abbreviation the model coined on its own** — the attribute was +> not in the base prompt (e.g. passport, matricule, residence permit, plate, blood type, medical record). + +### Single messages (complexity rising) + +#### Case 1 — 1 personne — inscription simple + +**User message:** + +> Bonjour, je suis Camille Perret, mon e-mail est camille.perret@yopmail.com. Je souhaite m'inscrire à la formation « Bureautique de base ». + +**Anonymized (what the model sees):** + +> Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], mon e-mail est [PER_1.EMAIL:Professionnel]. Je souhaite m'inscrire à la formation « Bureautique de base ». + +**`mapping` 🔒 (secret):** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:F]` | Camille | +| `[PER_1.NOM:F]` | Perret | +| `[PER_1.EMAIL:Professionnel]` | camille.perret@yopmail.com | + +**`legend` 🏷️ (shareable):** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `F` | Féminin | | +| `NOM` | Nom de famille | | +| `EMAIL` | Adresse e-mail | | + +**Round-trip:** ✅ restored identically · 4784 ms + +#### Case 2 — 2 personnes — collègue + matricule + +**User message:** + +> Salut, c'est Diego Fontana (identifiant dfontana, 079 400 11 22). Je m'inscris avec mon collègue Rui Almeida à la formation « Machiniste » ; le matricule interne de Rui est EMP-88231. + +**Anonymized (what the model sees):** + +> Salut, c'est [PER_1.PRENOM:M] [PER_1.NOM:M] (identifiant [PER_1.ID:USER], [PER_1.TELEPHONE:MOBILE]). Je m'inscris avec mon collègue [PER_2.PRENOM:M] [PER_2.NOM:M] à la formation « Machiniste » ; le matricule interne de [PER_2.PRENOM:M] est [PER_2.ID:EMP]. + +**`mapping` 🔒 (secret):** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:M]` | Diego | +| `[PER_1.NOM:M]` | Fontana | +| `[PER_1.ID:USER]` | dfontana | +| `[PER_1.TELEPHONE:MOBILE]` | 079 400 11 22 | +| `[PER_2.PRENOM:M]` | Rui | +| `[PER_2.NOM:M]` | Almeida | +| `[PER_2.ID:EMP]` | EMP-88231 | + +**`legend` 🏷️ (shareable):** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `M` | Masculin | | +| `NOM` | Nom de famille | | +| `ID` | Identifiant | 🆕 coined | +| `USER` | Identifiant utilisateur | 🆕 coined | +| `TELEPHONE` | Numéro de téléphone | | +| `MOBILE` | Téléphone mobile | 🆕 coined | +| `EMP` | Matricule employé | 🆕 coined | + +**Round-trip:** ✅ restored identically · 8443 ms + +#### Case 3 — 3 personnes + org — admission (passeport, permis, matricule) + +**User message:** + +> Dossier d'admission : Léna Hofmann, née le 03.11.1998, passeport suisse X1234567 et matricule étudiant 18-402-901, est parrainée par sa tutrice Sofia Marchetti et par son employeur, la société Béton Léman SA. Léna dispose aussi d'un permis de séjour de type B. + +**Anonymized (what the model sees):** + +> Dossier d'admission : [PER_1.PRENOM:F] [PER_1.NOM:F], née le [PER_1.DATE_NAISSANCE:1998], passeport suisse [PER_1.PASSPORT:CH] et matricule étudiant [PER_1.MATRICULE:ETUDIANT], est parrainée par sa tutrice [PER_2.PRENOM:F] [PER_2.NOM:F] et par son employeur, la société [ORG_1.NOM:Entreprise]. [PER_1.PRENOM:F] dispose aussi d'un permis de séjour de type B. + +**`mapping` 🔒 (secret):** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:F]` | Léna | +| `[PER_1.NOM:F]` | Hofmann | +| `[PER_1.DATE_NAISSANCE:1998]` | 03.11.1998 | +| `[PER_1.PASSPORT:CH]` | X1234567 | +| `[PER_1.MATRICULE:ETUDIANT]` | 18-402-901 | +| `[PER_2.PRENOM:F]` | Sofia | +| `[PER_2.NOM:F]` | Marchetti | +| `[ORG_1.NOM:Entreprise]` | Béton Léman SA | + +**`legend` 🏷️ (shareable):** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `F` | Féminin | | +| `NOM` | Nom de famille | | +| `DATE_NAISSANCE` | Date de naissance | | +| `PASSPORT` | Numéro de passeport | 🆕 coined | +| `CH` | Suisse | 🆕 coined | +| `MATRICULE` | Numéro de matricule | 🆕 coined | +| `ETUDIANT` | Étudiant | 🆕 coined | +| `ORG` | Organisation | | + +**Round-trip:** ✅ restored identically · 9567 ms + +#### Case 4 — 4 personnes + org — RH (permis conduire, plaque, LAMal, groupe sanguin) + +**User message:** + +> Bonjour, ici Patrick Nussbaum, responsable RH chez Migros Vaud (patrick.nussbaum@migros.ch, +41 21 924 11 11). J'inscris trois collaborateurs à « Sécurité au travail » : Chloé Aebischer (permis de conduire 5501234567, plaque VD 148 552), Deniz Yilmaz (numéro d'assuré LAMal 756.9911.2233.44, groupe sanguin A+) et Amir Haddad (permis de séjour C, matricule EMP-40127). Merci d'adresser la facture à l'IBAN CH70 0076 2011 6238 5295 7. + +**Anonymized (what the model sees):** + +> Bonjour, ici [PER_1.PRENOM:M] [PER_1.NOM:M], responsable RH chez [ORG_1.NOM:Entreprise] ([PER_1.EMAIL:Professionnel], [PER_1.TELEPHONE:Fixe]). J'inscris trois collaborateurs à « Sécurité au travail » : [PER_2.PRENOM:F] [PER_2.NOM:F] (permis de conduire [PER_2.PERMIS:Conduite], plaque [PER_2.PLAQUE:Vehicule]), [PER_3.PRENOM:M] [PER_3.NOM:M] (numéro d'assuré LAMal [PER_3.ASSURANCE:LAMal], groupe sanguin [PER_3.SANG:Type]) et [PER_4.PRENOM:M] [PER_4.NOM:M] (permis de séjour [PER_4.PERMIS:Residence], matricule [PER_4.MATRICULE:Employe]). Merci d'adresser la facture à l'IBAN [ORG_1.IBAN:Entreprise]. + +**`mapping` 🔒 (secret):** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:M]` | Patrick | +| `[PER_1.NOM:M]` | Nussbaum | +| `[ORG_1.NOM:Entreprise]` | Migros Vaud | +| `[PER_1.EMAIL:Professionnel]` | patrick.nussbaum@migros.ch | +| `[PER_1.TELEPHONE:Fixe]` | +41 21 924 11 11 | +| `[PER_2.PRENOM:F]` | Chloé | +| `[PER_2.NOM:F]` | Aebischer | +| `[PER_2.PERMIS:Conduite]` | 5501234567 | +| `[PER_2.PLAQUE:Vehicule]` | VD 148 552 | +| `[PER_3.PRENOM:M]` | Deniz | +| `[PER_3.NOM:M]` | Yilmaz | +| `[PER_3.ASSURANCE:LAMal]` | 756.9911.2233.44 | +| `[PER_3.SANG:Type]` | A+ | +| `[PER_4.PRENOM:M]` | Amir | +| `[PER_4.NOM:M]` | Haddad | +| `[PER_4.PERMIS:Residence]` | C | +| `[PER_4.MATRICULE:Employe]` | EMP-40127 | +| `[ORG_1.IBAN:Entreprise]` | CH70 0076 2011 6238 5295 7 | + +**`legend` 🏷️ (shareable):** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `M` | Masculin | | +| `NOM` | Nom de famille | | +| `ORG` | Organisation | | +| `EMAIL` | Adresse électronique | | +| `TELEPHONE` | Numéro de téléphone | | +| `F` | Féminin | | +| `PERMIS` | Numéro de permis | 🆕 coined | +| `PLAQUE` | Plaque d'immatriculation | 🆕 coined | +| `ASSURANCE` | Numéro d'assurance | 🆕 coined | +| `SANG` | Groupe sanguin | 🆕 coined | +| `MATRICULE` | Numéro de matricule | 🆕 coined | +| `IBAN` | Numéro de compte bancaire | | + +**Round-trip:** ✅ restored identically · 20162 ms + +#### Case 5 — 5+ personnes — dossier médical complet (biométrie, RCC, dossier médical, allergie) + +**User message:** + +> Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent l'autorisation. Le suivi est assuré par le Dr Farah Haddad (n° RCC V123456), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent Kevin Zbinden de la caisse Helvetia. Pour accéder au laboratoire, Noah utilise l'identifiant biométrique BIO-7729. + +**Anonymized (what the model sees):** + +> Note de dossier complète : le patient mineur [PER_1.PRENOM:U] [PER_1.NOM:U] (né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin B+, numéro d'assuré maladie [PER_1.ASSUR_ID:U], allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, [PER_2.PRENOM:F] [PER_2.NOM:F] (mère, permis de séjour [PER_2.PERMIS:C], [PER_2.TELEPHONE:U]) et [PER_3.PRENOM:M] [PER_3.NOM:M] (père, passeport italien [PER_3.PASSPORT:U]), cosignent l'autorisation. Le suivi est assuré par le Dr [PER_4.PRENOM:F] [PER_4.NOM:F] (n° RCC [PER_4.RCC:U]), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent [PER_5.PRENOM:M] [PER_5.NOM:M] de la caisse [ORG_1.NOM:Assurance]. Pour accéder au laboratoire, [PER_1.PRENOM:U] utilise l'identifiant biométrique [PER_1.BIO_ID:U]. + +**`mapping` 🔒 (secret):** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:U]` | Noah | +| `[PER_1.NOM:U]` | Baumann | +| `[PER_1.DATE_NAISSANCE:2013]` | 12.06.2013 | +| `[PER_1.ASSUR_ID:U]` | 756.2211.9988.77 | +| `[PER_2.PRENOM:F]` | Delphine | +| `[PER_2.NOM:F]` | Rieder | +| `[PER_2.PERMIS:C]` | C | +| `[PER_2.TELEPHONE:U]` | 021 555 12 34 | +| `[PER_3.PRENOM:M]` | Marco | +| `[PER_3.NOM:M]` | Baumann | +| `[PER_3.PASSPORT:U]` | YA9087654 | +| `[PER_4.PRENOM:F]` | Farah | +| `[PER_4.NOM:F]` | Haddad | +| `[PER_4.RCC:U]` | V123456 | +| `[PER_5.PRENOM:M]` | Kevin | +| `[PER_5.NOM:M]` | Zbinden | +| `[ORG_1.NOM:Assurance]` | Helvetia | +| `[PER_1.BIO_ID:U]` | BIO-7729 | + +**`legend` 🏷️ (shareable):** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `U` | Inconnu/Neutre | | +| `NOM` | Nom de famille | | +| `DATE_NAISSANCE` | Date de naissance | | +| `ASSUR_ID` | Numéro d'assuré | 🆕 coined | +| `F` | Féminin | | +| `PERMIS` | Type de permis de séjour | 🆕 coined | +| `C` | C | 🆕 coined | +| `TELEPHONE` | Numéro de téléphone | | +| `M` | Masculin | | +| `PASSPORT` | Numéro de passeport | 🆕 coined | +| `RCC` | Numéro RCC | 🆕 coined | +| `ORG` | Organisation | | +| `BIO_ID` | Identifiant biométrique | 🆕 coined | + +**Round-trip:** ✅ restored identically · 19150 ms + +### Multi-turn conversations (ids stay stable; new attributes appear as it escalates) + +#### Conversation 1 — Inscription — rappel du matricule (T1 → T4) + +| Turn | User message | Anonymized (model sees) | +| --- | --- | --- | +| 1 | Bonjour, je m'appelle Yasmine Cottier, matricule étudiant 21-305-118, je m'inscris à « Développement web ». | Bonjour, je m'appelle [PER_1.PRENOM:F] [PER_1.NOM:F], matricule étudiant [PER_1.MATRICULE:Etudiant], je m'inscris à « Développement web ». | +| 2 | Mon e-mail est yasmine.cottier@gmx.ch et mon téléphone 078 331 22 10. | Mon e-mail est [PER_1.EMAIL:Perso] et mon téléphone [PER_1.TELEPHONE:Mobile]. | +| 3 | Ma camarade Inès Lefort aimerait aussi rejoindre le cours. | Ma camarade [PER_2.PRENOM:F] [PER_2.NOM:F] aimerait aussi rejoindre le cours. | +| 4 | Pouvez-vous rattacher mon matricule 21-305-118 au dossier d'Inès pour le projet de groupe ? | Pouvez-vous rattacher mon matricule [PER_1.MATRICULE:Etudiant] au dossier d'[PER_2.PRENOM:F] pour le projet de groupe ? | + +**Cumulative `mapping` 🔒:** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:F]` | Yasmine | +| `[PER_1.NOM:F]` | Cottier | +| `[PER_1.MATRICULE:Etudiant]` | 21-305-118 | +| `[PER_1.EMAIL:Perso]` | yasmine.cottier@gmx.ch | +| `[PER_1.TELEPHONE:Mobile]` | 078 331 22 10 | +| `[PER_2.PRENOM:F]` | Inès | +| `[PER_2.NOM:F]` | Lefort | + +**`legend` 🏷️:** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `F` | Féminin | | +| `NOM` | Nom de famille | | +| `MATRICULE` | Numéro de matricule | 🆕 coined | +| `EMAIL` | Adresse e-mail | | +| `TELEPHONE` | Numéro de téléphone | | + +**Auto-coined attributes:** `MATRICULE` + +#### Conversation 2 — Identité — même nom de famille + rappel du passeport (T1 → T4) + +| Turn | User message | Anonymized (model sees) | +| --- | --- | --- | +| 1 | Ici Tomás Varga, passeport P-CH 4451220, je confirme mon inscription. | Ici [PER_1.PRENOM:M] [PER_1.NOM:M], passeport [PER_1.PASSPORT:CH], je confirme mon inscription. | +| 2 | J'ai également un permis de séjour de type C. | J'ai également un permis de séjour de type C. | +| 3 | Mon épouse Ana Varga s'inscrit aussi ; son passeport est P-CH 4451221. | Mon épouse [PER_2.PRENOM:F] [PER_1.NOM:M] s'inscrit aussi ; son passeport est [PER_2.PASSPORT:CH]. | +| 4 | Utilisez le passeport P-CH 4451220 pour la facturation à mon nom. | Utilisez le passeport [PER_1.PASSPORT:CH] pour la facturation à mon nom. | + +**Cumulative `mapping` 🔒:** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:M]` | Tomás | +| `[PER_1.NOM:M]` | Varga | +| `[PER_1.PASSPORT:CH]` | P-CH 4451220 | +| `[PER_2.PRENOM:F]` | Ana | +| `[PER_2.PASSPORT:CH]` | P-CH 4451221 | + +**`legend` 🏷️:** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `M` | Masculin | | +| `NOM` | Nom de famille | | +| `PASSPORT` | Numéro de passeport | 🆕 coined | +| `CH` | Suisse | 🆕 coined | +| `F` | Féminin | | + +**Auto-coined attributes:** `PASSPORT`, `CH` + +#### Conversation 3 — Transport — 3 personnes + rappel de la plaque (T1 → T4) + +| Turn | User message | Anonymized (model sees) | +| --- | --- | --- | +| 1 | Salut, c'est Bruno Steiner, plaque d'immatriculation GE 289 774, je viens en voiture. | Salut, c'est [PER_1.PRENOM:M] [PER_1.NOM:M], plaque d'immatriculation [VEH_1.PLAQUE:GE], je viens en voiture. | +| 2 | Mes collègues Farah Haddad et Kevin Zbinden covoiturent avec moi. | Mes collègues [PER_2.PRENOM:F] [PER_2.NOM:F] et [PER_3.PRENOM:M] [PER_3.NOM:M] covoiturent avec moi. | +| 3 | Le permis de conduire de Farah est 6602345678. | Le permis de conduire de [PER_2.PRENOM:F] est [PER_2.PERMIS:ID]. | +| 4 | Réservez la place de parking pour la plaque GE 289 774, merci. | Réservez la place de parking pour la plaque [VEH_1.PLAQUE:GE], merci. | + +**Cumulative `mapping` 🔒:** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:M]` | Bruno | +| `[PER_1.NOM:M]` | Steiner | +| `[VEH_1.PLAQUE:GE]` | GE 289 774 | +| `[PER_2.PRENOM:F]` | Farah | +| `[PER_2.NOM:F]` | Haddad | +| `[PER_3.PRENOM:M]` | Kevin | +| `[PER_3.NOM:M]` | Zbinden | +| `[PER_2.PERMIS:ID]` | 6602345678 | + +**`legend` 🏷️:** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `M` | Masculin | | +| `NOM` | Nom de famille | | +| `VEH` | Véhicule | 🆕 coined | +| `PLAQUE` | Plaque d'immatriculation | 🆕 coined | +| `GE` | Genève | 🆕 coined | +| `F` | Féminin | | +| `PERMIS` | Numéro de permis de conduire | 🆕 coined | +| `ID` | Identifiant | 🆕 coined | + +**Auto-coined attributes:** `VEH`, `PLAQUE`, `GE`, `PERMIS`, `ID` + +#### Conversation 4 — Santé — rappel du numéro LAMal (T1 → T5) + +| Turn | User message | Anonymized (model sees) | +| --- | --- | --- | +| 1 | Bonjour, je suis Salomé Girard, numéro d'assurée LAMal 756.9911.2233.44, apprentie infirmière. | Bonjour, je suis [PER_1.PRENOM:F] [PER_1.NOM:F], numéro d'assurée LAMal [PER_1.LAMAL:ASSURANCE], apprentie infirmière. | +| 2 | Mon groupe sanguin est A+ et je suis allergique au latex. | Mon groupe sanguin est [PER_1.SANG:TYPE] et je suis allergique au latex. | +| 3 | Mon superviseur de stage est le Dr Killian Roux. | Mon superviseur de stage est le Dr [PER_2.PRENOM:M] [PER_2.NOM:M]. | +| 4 | Ma collègue Priya Nair, groupe sanguin O-, partage le même planning. | Ma collègue [PER_3.PRENOM:F] [PER_3.NOM:F], groupe sanguin [PER_3.SANG:TYPE], partage le même planning. | +| 5 | Facturez le stage sur mon assurance LAMal 756.9911.2233.44. | Facturez le stage sur mon assurance LAMal [PER_1.LAMAL:ASSURANCE]. | + +**Cumulative `mapping` 🔒:** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:F]` | Salomé | +| `[PER_1.NOM:F]` | Girard | +| `[PER_1.LAMAL:ASSURANCE]` | 756.9911.2233.44 | +| `[PER_1.SANG:TYPE]` | A+ | +| `[PER_2.PRENOM:M]` | Killian | +| `[PER_2.NOM:M]` | Roux | +| `[PER_3.PRENOM:F]` | Priya | +| `[PER_3.NOM:F]` | Nair | +| `[PER_3.SANG:TYPE]` | O- | + +**`legend` 🏷️:** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `F` | Féminin | | +| `NOM` | Nom de famille | | +| `LAMAL` | Numéro d'assurance maladie | 🆕 coined | +| `ASSURANCE` | Assurance | 🆕 coined | +| `SANG` | Groupe sanguin | 🆕 coined | +| `TYPE` | Type sanguin | 🆕 coined | +| `M` | Masculin | | + +**Auto-coined attributes:** `LAMAL`, `ASSURANCE`, `SANG`, `TYPE` + +#### Conversation 5 — Dossier complexe — 4 personnes, rappel du groupe sanguin (T1 → T6) + +| Turn | User message | Anonymized (model sees) | +| --- | --- | --- | +| 1 | Je vous écris pour mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+. | Je vous écris pour mon fils mineur, [PER_1.PRENOM:U] [PER_1.NOM:U], né le [PER_1.DATE_NAISSANCE:2013], groupe sanguin [PER_1.SANG:B+]. | +| 2 | Son numéro d'assuré maladie est 756.2211.9988.77. | Son numéro d'assuré maladie est [PER_1.AVS:SUI]. | +| 3 | Moi, sa mère Delphine Rieder, je gère le dossier ; mon permis de séjour est de type C. | Moi, sa mère [PER_2.PRENOM:F] [PER_2.NOM:F], je gère le dossier ; mon permis de séjour est de type [PER_2.PERMIS:C]. | +| 4 | Son père, Marco Baumann, passeport italien YA9087654, cosigne l'autorisation. | Son père, [PER_3.PRENOM:M] [PER_1.NOM:U], passeport italien [PER_3.PASSPORT:ITA], cosigne l'autorisation. | +| 5 | Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah. | Le [PER_4.TITRE:MED] [PER_4.PRENOM:F] [PER_4.NOM:F] a ouvert le dossier médical [PER_1.DOSSIER:MED] pour [PER_1.PRENOM:U]. | +| 6 | Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge d'accès au laboratoire. | Rappel : le groupe sanguin [PER_1.SANG:B+] de [PER_1.PRENOM:U] doit figurer sur son badge d'accès au laboratoire. | + +**Cumulative `mapping` 🔒:** + +| Placeholder | Real value | +| --- | --- | +| `[PER_1.PRENOM:U]` | Noah | +| `[PER_1.NOM:U]` | Baumann | +| `[PER_1.DATE_NAISSANCE:2013]` | 12.06.2013 | +| `[PER_1.SANG:B+]` | B+ | +| `[PER_1.AVS:SUI]` | 756.2211.9988.77 | +| `[PER_2.PRENOM:F]` | Delphine | +| `[PER_2.NOM:F]` | Rieder | +| `[PER_2.PERMIS:C]` | C | +| `[PER_3.PRENOM:M]` | Marco | +| `[PER_3.PASSPORT:ITA]` | YA9087654 | +| `[PER_4.TITRE:MED]` | Dr | +| `[PER_4.PRENOM:F]` | Farah | +| `[PER_4.NOM:F]` | Haddad | +| `[PER_1.DOSSIER:MED]` | DM-2025-0417 | + +**`legend` 🏷️:** + +| Abbrev. | Meaning | | +| --- | --- | --- | +| `PER` | Personne | | +| `PRENOM` | Prénom | | +| `U` | Inconnu | | +| `NOM` | Nom de famille | | +| `DATE_NAISSANCE` | Date de naissance | | +| `SANG` | Groupe sanguin | 🆕 coined | +| `AVS` | Numéro d'assuré | | +| `SUI` | Suisse | 🆕 coined | +| `F` | Féminin | | +| `PERMIS` | Type de permis de séjour | 🆕 coined | +| `C` | Permis d'établissement | 🆕 coined | +| `M` | Masculin | | +| `PASSPORT` | Numéro de passeport | 🆕 coined | +| `ITA` | Italie | 🆕 coined | +| `TITRE` | Titre professionnel | 🆕 coined | +| `MED` | Médical | 🆕 coined | +| `DOSSIER` | Numéro de dossier | 🆕 coined | + +**Auto-coined attributes:** `SANG`, `SUI`, `PERMIS`, `C`, `PASSPORT`, `ITA`, `TITRE`, `MED`, `DOSSIER` diff --git a/examples/demo.mjs b/examples/demo.mjs new file mode 100644 index 0000000..c264d6e --- /dev/null +++ b/examples/demo.mjs @@ -0,0 +1,287 @@ +import { Anonymizer, openAICompatibleProvider, AnonymizationError } from '@mobiletic/anonymizer'; +import { writeFileSync } from 'node:fs'; + +// ── LLM-only (no regex patterns) → everything is handled purely by the model. +// Infomaniak is a trusted processor (it already sees the cleartext being anonymized), +// so we feed the anonymizer the running mapping for the sharpest cross-turn coreference. +const anonymizer = new Anonymizer({ + llm: openAICompatibleProvider({ + baseUrl: process.env.INFOMANIAK_AI_BASE_URL, + apiKey: process.env.INFOMANIAK_AI_API_KEY, + model: process.env.GEMMA_MODEL, + timeoutMs: Number(process.env.PRIVACY_GEMMA_TIMEOUT_MS ?? 60000), + retries: 2, + includeMappingInContext: true, + }), + logger: { warn: (m) => console.warn(' ⚠ ', m) }, +}); + +// Entities/attributes/context codes present in the default prompt. Anything in a +// result legend that is NOT here was COINED by the model on its own. +const BASE_VOCAB = new Set([ + 'PER', + 'ORG', + 'LOC', + 'NOM', + 'PRENOM', + 'DATE_NAISSANCE', + 'AGE', + 'ADRESSE', + 'EMAIL', + 'TELEPHONE', + 'AVS', + 'IBAN', + 'NSS', + 'M', + 'F', + 'U', + 'Mineur', + 'Adulte', + 'Ville', + 'Rue', + 'NPA', + 'Pays', + 'Entreprise', + 'Ecole', +]); +const coined = (legend) => Object.keys(legend).filter((k) => !BASE_VOCAB.has(k)); + +// ── Single-message cases — complexity rises, and cases 3–5 carry RARE attributes +// NOT in the prompt (passport, matricule, residence permit, plate, health-insurance +// number, blood type, medical-record no., allergy) that the model must detect + name itself. +// All personal data is FICTIONAL. +const cases = [ + { + title: '1 personne — inscription simple', + text: "Bonjour, je suis Camille Perret, mon e-mail est camille.perret@yopmail.com. Je souhaite m'inscrire à la formation « Bureautique de base ».", + }, + { + title: '2 personnes — collègue + matricule', + text: "Salut, c'est Diego Fontana (identifiant dfontana, 079 400 11 22). Je m'inscris avec mon collègue Rui Almeida à la formation « Machiniste » ; le matricule interne de Rui est EMP-88231.", + }, + { + title: '3 personnes + org — admission (passeport, permis, matricule)', + text: "Dossier d'admission : Léna Hofmann, née le 03.11.1998, passeport suisse X1234567 et matricule étudiant 18-402-901, est parrainée par sa tutrice Sofia Marchetti et par son employeur, la société Béton Léman SA. Léna dispose aussi d'un permis de séjour de type B.", + }, + { + title: '4 personnes + org — RH (permis conduire, plaque, LAMal, groupe sanguin)', + text: "Bonjour, ici Patrick Nussbaum, responsable RH chez Migros Vaud (patrick.nussbaum@migros.ch, +41 21 924 11 11). J'inscris trois collaborateurs à « Sécurité au travail » : Chloé Aebischer (permis de conduire 5501234567, plaque VD 148 552), Deniz Yilmaz (numéro d'assuré LAMal 756.9911.2233.44, groupe sanguin A+) et Amir Haddad (permis de séjour C, matricule EMP-40127). Merci d'adresser la facture à l'IBAN CH70 0076 2011 6238 5295 7.", + }, + { + title: '5+ personnes — dossier médical complet (biométrie, RCC, dossier médical, allergie)', + text: "Note de dossier complète : le patient mineur Noah Baumann (né le 12.06.2013, groupe sanguin B+, numéro d'assuré maladie 756.2211.9988.77, allergique aux arachides) est inscrit à l'atelier « Robotique junior ». Ses parents, Delphine Rieder (mère, permis de séjour C, 021 555 12 34) et Marco Baumann (père, passeport italien YA9087654), cosignent l'autorisation. Le suivi est assuré par le Dr Farah Haddad (n° RCC V123456), qui a ouvert le dossier médical DM-2025-0417. L'assurance est gérée par l'agent Kevin Zbinden de la caisse Helvetia. Pour accéder au laboratoire, Noah utilise l'identifiant biométrique BIO-7729.", + }, +]; + +// ── Multi-turn conversations — escalating, with rarer attributes surfacing later. +// Harder conversations: 4+ turns, up to 4 people, and a LONG-RANGE back-reference +// (an attribute stated in turn 1 is recalled in a later turn) to stress session continuity. +const conversations = [ + { + title: 'Inscription — rappel du matricule (T1 → T4)', + turns: [ + "Bonjour, je m'appelle Yasmine Cottier, matricule étudiant 21-305-118, je m'inscris à « Développement web ».", + 'Mon e-mail est yasmine.cottier@gmx.ch et mon téléphone 078 331 22 10.', + 'Ma camarade Inès Lefort aimerait aussi rejoindre le cours.', + "Pouvez-vous rattacher mon matricule 21-305-118 au dossier d'Inès pour le projet de groupe ?", + ], + }, + { + title: 'Identité — même nom de famille + rappel du passeport (T1 → T4)', + turns: [ + 'Ici Tomás Varga, passeport P-CH 4451220, je confirme mon inscription.', + "J'ai également un permis de séjour de type C.", + "Mon épouse Ana Varga s'inscrit aussi ; son passeport est P-CH 4451221.", + 'Utilisez le passeport P-CH 4451220 pour la facturation à mon nom.', + ], + }, + { + title: 'Transport — 3 personnes + rappel de la plaque (T1 → T4)', + turns: [ + "Salut, c'est Bruno Steiner, plaque d'immatriculation GE 289 774, je viens en voiture.", + 'Mes collègues Farah Haddad et Kevin Zbinden covoiturent avec moi.', + 'Le permis de conduire de Farah est 6602345678.', + 'Réservez la place de parking pour la plaque GE 289 774, merci.', + ], + }, + { + title: 'Santé — rappel du numéro LAMal (T1 → T5)', + turns: [ + "Bonjour, je suis Salomé Girard, numéro d'assurée LAMal 756.9911.2233.44, apprentie infirmière.", + 'Mon groupe sanguin est A+ et je suis allergique au latex.', + 'Mon superviseur de stage est le Dr Killian Roux.', + 'Ma collègue Priya Nair, groupe sanguin O-, partage le même planning.', + 'Facturez le stage sur mon assurance LAMal 756.9911.2233.44.', + ], + }, + { + title: 'Dossier complexe — 4 personnes, rappel du groupe sanguin (T1 → T6)', + turns: [ + 'Je vous écris pour mon fils mineur, Noah Baumann, né le 12.06.2013, groupe sanguin B+.', + "Son numéro d'assuré maladie est 756.2211.9988.77.", + 'Moi, sa mère Delphine Rieder, je gère le dossier ; mon permis de séjour est de type C.', + "Son père, Marco Baumann, passeport italien YA9087654, cosigne l'autorisation.", + 'Le Dr Farah Haddad a ouvert le dossier médical DM-2025-0417 pour Noah.', + "Rappel : le groupe sanguin B+ de Noah doit figurer sur son badge d'accès au laboratoire.", + ], + }, +]; + +const line = (c = '─') => console.log(c.repeat(78)); +const fmt = (o) => JSON.stringify(o, null, 2).replace(/\n/g, '\n '); +const results = []; +const convResults = []; + +async function runCase(i, { title, text }) { + line('═'); + console.log(`CAS ${i} — ${title}`); + line(); + console.log('MESSAGE :', text); + try { + const t0 = Date.now(); + const { anon, mapping, legend } = await anonymizer.anonymize(text); + const ms = Date.now() - t0; + const restored = anonymizer.deanonymize(anon, mapping); + const ok = restored === text; + console.log(`ANONYMISÉ : ${anon} (${ms} ms)`); + console.log('MAPPING 🔒 :', fmt(mapping)); + console.log('LEGEND 🏷️ :', fmt(legend), ' coined:', coined(legend).join(', ') || '—'); + console.log(ok ? '✓ round-trip identique' : '✗ round-trip DIFFÈRE'); + results.push({ i, title, text, anon, mapping, legend, ms, ok }); + } catch (err) { + const msg = err instanceof AnonymizationError ? err.message : String(err); + console.error('✗', msg); + results.push({ i, title, text, error: msg }); + } + console.log(); +} + +async function runConversation(i, { title, turns }) { + line('═'); + console.log(`CONVERSATION ${i} — ${title}`); + line(); + const conv = anonymizer.conversation(); + const rows = []; + for (const text of turns) { + try { + const { anon } = await conv.anonymize(text); + const ok = conv.deanonymize(anon) === text; + console.log(` utilisateur : ${text}`); + console.log(` anonymisé : ${anon}`); + rows.push({ text, anon, ok }); + } catch (err) { + console.error(' ✗', err?.message ?? String(err)); + rows.push({ text, error: err?.message ?? String(err) }); + } + } + const s = conv.session(); + console.log('MAPPING 🔒 cumulé :', fmt(s.mapping)); + console.log('coined :', coined(s.legend).join(', ') || '—'); + convResults.push({ i, title, rows, mapping: s.mapping, legend: s.legend }); + console.log(); +} + +// ── README-ready Markdown ─────────────────────────────────────────────────── +const esc = (v) => String(v).replace(/\|/g, '\\|'); +function kvTable(obj, [h1, h2], markCoined = false) { + const rows = Object.entries(obj); + if (!rows.length) return '_(vide)_'; + return [ + `| ${h1} | ${h2} |`, + '| --- | --- |', + ...rows.map(([k, v]) => `| \`${esc(k)}\` | ${esc(v)} |`), + ].join('\n'); +} +function legendTable(legend) { + const rows = Object.entries(legend); + if (!rows.length) return '_(vide)_'; + return [ + '| Abbrev. | Meaning | |', + '| --- | --- | --- |', + ...rows.map(([k, v]) => `| \`${esc(k)}\` | ${esc(v)} | ${BASE_VOCAB.has(k) ? '' : '🆕 coined'} |`), + ].join('\n'); +} + +function toMarkdown() { + const parts = [ + '## Examples', + '', + '> Real learner ↔ platform chat messages anonymized **live by Gemma 4** (via Infomaniak). All personal', + '> data is **fictional**. `mapping` is the secret re-identification key (kept by the operator); `legend`', + '> is safe to send downstream. **🆕 = an abbreviation the model coined on its own** — the attribute was', + '> not in the base prompt (e.g. passport, matricule, residence permit, plate, blood type, medical record).', + '', + '### Single messages (complexity rising)', + '', + ]; + for (const r of results) { + parts.push(`#### Case ${r.i} — ${r.title}`, ''); + if (r.error) { + parts.push(`> ${r.text}`, '', `❌ \`${r.error}\``, ''); + continue; + } + parts.push( + '**User message:**', + '', + `> ${r.text}`, + '', + '**Anonymized (what the model sees):**', + '', + `> ${r.anon}`, + '', + '**`mapping` 🔒 (secret):**', + '', + kvTable(r.mapping, ['Placeholder', 'Real value']), + '', + '**`legend` 🏷️ (shareable):**', + '', + legendTable(r.legend), + '', + `**Round-trip:** ${r.ok ? '✅ restored identically' : '⚠️ differs'} · ${r.ms} ms`, + '', + ); + } + + parts.push('### Multi-turn conversations (ids stay stable; new attributes appear as it escalates)', ''); + for (const c of convResults) { + parts.push( + `#### Conversation ${c.i} — ${c.title}`, + '', + '| Turn | User message | Anonymized (model sees) |', + '| --- | --- | --- |', + ); + c.rows.forEach((row, idx) => { + const shown = row.error ? `❌ ${row.error}` : esc(row.anon); + parts.push(`| ${idx + 1} | ${esc(row.text)} | ${shown} |`); + }); + parts.push( + '', + '**Cumulative `mapping` 🔒:**', + '', + kvTable(c.mapping, ['Placeholder', 'Real value']), + '', + '**`legend` 🏷️:**', + '', + legendTable(c.legend), + '', + ); + const co = coined(c.legend); + if (co.length) parts.push(`**Auto-coined attributes:** ${co.map((k) => `\`${k}\``).join(', ')}`, ''); + } + return parts.join('\n'); +} + +console.log('\n@mobiletic/anonymizer — démonstration live (Infomaniak)\n'); +console.log( + `Modèle: ${process.env.GEMMA_MODEL || '(non défini)'} · Endpoint: ${process.env.INFOMANIAK_AI_BASE_URL || '(non défini)'}\n`, +); + +for (let i = 0; i < cases.length; i++) await runCase(i + 1, cases[i]); +for (let i = 0; i < conversations.length; i++) await runConversation(i + 1, conversations[i]); + +writeFileSync(new URL('./RESULTS.md', import.meta.url), toMarkdown()); +line('═'); +console.log( + `Terminé. Single: ${results.filter((r) => r.ok).length}/${results.length} round-trips OK · ` + + `${convResults.length} conversations. Résultats écrits dans RESULTS.md`, +); diff --git a/package.json b/package.json index ea9929e..dea6c33 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "scripts": { "build": "tsup", + "example": "npm run build && node --env-file=examples/.env examples/demo.mjs", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage",