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) <noreply@anthropic.com>
This commit is contained in:
Mobiletic
2026-07-01 15:22:00 +01:00
parent d119c366b7
commit dcb0a3a448
9 changed files with 771 additions and 124 deletions

6
examples/.env.example Normal file
View File

@@ -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/<product_id>/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

24
examples/README.md Normal file
View File

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

428
examples/RESULTS.md Normal file
View File

@@ -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`

287
examples/demo.mjs Normal file
View File

@@ -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 35 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`,
);