RedaktAIDocs

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 response

Session 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 inactivity

For SQLite, implement your own cleanup logic:

store = SQLiteStore(path="sessions.db")
store.delete(f"conv-{old_conversation_id}")

On this page