feat: multi-turn conversation support (0.4.0)
anonymizeTurn(text, session) threads a serializable AnonymizerSession
({mapping, legend, history}) so one entity keeps one id across a whole chat
(applyKnown reuse + usedIds + de-collision merge). Adds conversation()
in-memory wrapper and an optional LlmProvider.anonymizeInConversation(text, ctx)
for rich cross-turn context; providers without it fall back to the batch path.
openAICompatibleProvider gains includeMappingInContext (default false — only
send real values to a trusted anonymizer endpoint) + historyMaxTurns (default
10). Verified live vs Gemma 4: Nora stays PER_1 across 4 turns (name/email/AVS/
IBAN), Yanis = PER_2; 41 tests, 98% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
36
README.md
36
README.md
@@ -108,6 +108,42 @@ const { anon, mapping, legend } = await anonymizer.anonymizeChunks(retrievedChun
|
||||
// pass `mapping` to deanonymize()/makeStreamDeanonymizer(), and `legend` to the downstream LLM.
|
||||
```
|
||||
|
||||
### Multi-turn conversations
|
||||
|
||||
In a chat, anonymizing each message independently would renumber entities every turn (Adil could become
|
||||
`PER_1` in turn 2 while Oussama was `PER_1` in turn 1). `anonymizeTurn` threads a serializable
|
||||
`AnonymizerSession` so **one entity keeps one id across the whole conversation**:
|
||||
|
||||
```ts
|
||||
let session; // persist this per conversation (Redis/DB); pass it back each turn
|
||||
for (const message of userTurns) {
|
||||
const turn = await anonymizer.anonymizeTurn(message, session);
|
||||
session = turn.session; // { mapping, legend, history } — carries coreference forward
|
||||
send(turn.anon, turn.legend); // → the downstream chatbot (placeholders only)
|
||||
}
|
||||
// restore the bot's placeholder-bearing reply for the user:
|
||||
anonymizer.deanonymize(botReply, session.mapping);
|
||||
```
|
||||
|
||||
Or the stateful wrapper for in-memory use:
|
||||
|
||||
```ts
|
||||
const conv = anonymizer.conversation();
|
||||
await conv.anonymize('Bonjour, je suis Oussama'); // → Oussama = [PER_1…]
|
||||
await conv.anonymize('Mon amie Adil …'); // → Oussama stays [PER_1…], Adil = [PER_2…]
|
||||
conv.deanonymize(botReply);
|
||||
```
|
||||
|
||||
Under the hood: known values are re-substituted locally (`applyKnown`), the provider is told which ids are
|
||||
taken, and new entities are de-collided into the session — so numbering never drifts.
|
||||
|
||||
**Trust boundary & the `mapping`.** The real boundary is "keep PII out of the _downstream chatbot_" — it
|
||||
only ever receives placeholders + `legend`. Your **anonymizer** endpoint already sees the cleartext it's
|
||||
asked to anonymize, so if (and only if) it's a _trusted_ processor you may give it richer context — set
|
||||
`includeMappingInContext: true` on `openAICompatibleProvider` to also send the `mapping` for maximum
|
||||
cross-turn accuracy. It's **off by default**; keep it off for third-party endpoints you don't trust with
|
||||
raw values.
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
|
||||
Reference in New Issue
Block a user