Multi-Turn Conversations
Maintain entity mappings across conversation turns
In a multi-turn conversation, entities detected in earlier turns must be recognized in later turns. Rdakt AI handles this through session persistence.
The Problem
Without session persistence, each request is independent:
- Turn 1: "Tell me about John Smith" →
<PERSON_1>mapped to "John Smith" - Turn 2: "What's his email?" → LLM responds with
<PERSON_1>but the middleware doesn't know what<PERSON_1>is anymore
The Solution
Use a persistent session store and a consistent session_key:
from rdakt_ai import RdaktMiddleware, SQLiteStore
store = SQLiteStore(path="sessions.db")
# Same session_key for all turns in a conversation
middleware = RdaktMiddleware(
store=store,
session_key=f"conversation-{conversation_id}",
)The session accumulates mappings:
- Turn 1: Session learns
<PERSON_1>→ "John Smith" - Turn 2: Session already knows
<PERSON_1>, detects "jane@acme.com" →<EMAIL_1> - Turn 3: Session knows both mappings, deanonymizes both in the response
Session Key Strategy
The session_key determines which requests share a session. Common patterns:
# Per conversation (most common)
session_key = f"conv-{conversation_id}"
# Per user (all conversations share mappings)
session_key = f"user-{user_id}"
# Per user per conversation
session_key = f"user-{user_id}-conv-{conversation_id}"Example: FastAPI Chat Endpoint
from fastapi import FastAPI
from rdakt_ai import RdaktMiddleware, SQLiteStore
import httpx
from openai import AsyncOpenAI
app = FastAPI()
store = SQLiteStore(path="sessions.db")
@app.post("/chat/{conversation_id}")
async def chat(conversation_id: str, message: str):
middleware = RdaktMiddleware(
inner=httpx.AsyncHTTPTransport(),
store=store,
session_key=f"conv-{conversation_id}",
)
client = AsyncOpenAI(
http_client=httpx.AsyncClient(transport=middleware),
)
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": message}],
)
return {"response": response.choices[0].message.content}Session Lifecycle
Request 1 → Detect → Anonymize → Store mappings → Forward → Deanonymize response
↓
Session Store (SQLite/Redis)
↑
Request 2 → Detect → Anonymize → Load + append mappings → Forward → Deanonymize responseSession mappings only grow — they are never removed during a conversation. This ensures that even if the LLM references an entity from many turns ago, it will be deanonymized correctly.
Cleanup
For Redis, use TTL to automatically expire old sessions:
session:
store: redis
ttl: 3600 # Expire after 1 hour of inactivityFor SQLite, implement your own cleanup logic:
store = SQLiteStore(path="sessions.db")
store.delete(f"conv-{old_conversation_id}")