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:
@@ -79,6 +79,39 @@ describe('openAICompatibleProvider', () => {
|
||||
expect(system).toContain('[PER_1.NOM:M]');
|
||||
});
|
||||
|
||||
it('anonymizeInConversation includes used-ids + history, and the mapping ONLY when opted in', async () => {
|
||||
const content = JSON.stringify({
|
||||
texte_anonymise: '[PER_2.NOM:F]',
|
||||
mapping: { '[PER_2.NOM:F]': 'X' },
|
||||
legende: {},
|
||||
});
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, json: async () => ({ choices: [{ message: { content } }] }) });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const ctx = {
|
||||
history: ['Je suis [PER_1.NOM:F]'],
|
||||
legend: { PER: 'Personne' },
|
||||
usedIds: ['[PER_1.NOM:F]'],
|
||||
mapping: { '[PER_1.NOM:F]': 'Nora Steiner' },
|
||||
};
|
||||
|
||||
// default: real values (mapping) are NOT sent
|
||||
await openAICompatibleProvider(opts).anonymizeInConversation!('x', ctx);
|
||||
let system = JSON.parse(fetchMock.mock.calls[0][1].body).messages[0].content;
|
||||
expect(system).toContain('[PER_1.NOM:F]'); // used-ids present
|
||||
expect(system).toContain('Je suis [PER_1.NOM:F]'); // anonymized history present
|
||||
expect(system).not.toContain('Nora Steiner'); // real value withheld by default
|
||||
|
||||
// opted in: the mapping (real value) is included
|
||||
await openAICompatibleProvider({ ...opts, includeMappingInContext: true }).anonymizeInConversation!(
|
||||
'x',
|
||||
ctx,
|
||||
);
|
||||
system = JSON.parse(fetchMock.mock.calls[1][1].body).messages[0].content;
|
||||
expect(system).toContain('Nora Steiner');
|
||||
});
|
||||
|
||||
it('throws on a non-OK HTTP status (no retry when retries: 0)', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
Reference in New Issue
Block a user