Skip to main content
Lesson 11 of 12 · AI Moderation and Security with Mistral

GDPR Compliance and Data Protection

Lesson 11 of 12 Updated November 3, 2026 Developer
On this page
  1. AI and Personal Data: A Strict Framework
  2. The PII Category in Mistral Moderation
  3. Anonymization Before Processing
  4. GDPR-Compliant Moderation Logs
  5. Audit Trail for Compliance
  6. GDPR Checklist for Your AI Application
  7. Key Takeaways

Updated November 3, 2026. The list of items detected by the pii category now aligns with Mistral’s definition (health data falls under different rules, and the health category targets requests for medical advice). The compliant logger previously miscalculated its retention end date, causing purges to delete everything; it also used a hardcoded default salting key for pseudonymization and the deprecated datetime.utcnow(). Pseudonymization now uses HMAC-SHA-256 with a mandatory key. Sources: Moderation & Guardrailing · GDPR, Recital 26 and Articles 4 and 22 · Python hmac.

AI and Personal Data: A Strict Framework

There is a paradox at the heart of this lesson, and seeing it clearly avoids many design errors: your moderation system is itself a personal data processing operation. To protect your users, you analyze their text, generate scores, and retain logs—all operations that the GDPR regulates exactly like any other processing activity.

Compliance, therefore, does not mean adding a protection layer on top of moderation. It means designing the moderation system itself to process the minimum amount of data, for the shortest time possible, while maintaining a record of its decisions. This is the thread running through the following sections.

The GDPR (General Data Protection Regulation) applies fully to AI applications processing data of European residents. Content moderation itself generates data that falls within the scope of the GDPR. You must understand your obligations to avoid penalties reaching up to 4% of global annual turnover.

The PII Category in Mistral Moderation

Mistral’s classifier pii category detects requests for or disclosures of personal information. One architectural point must be made immediately: this classifier is a safety net, not a guarantee. It catches varied phrasings that a regex would miss (“my number is…”), but like any model, it has false negatives. Conversely, a regex reliably catches structured formats—IBANs, social security numbers, credit cards—but nothing else. Best practice combines both: regex for known formats, classifier for the rest. Detected types include:

  • Full names
  • Addresses
  • Phone numbers
  • Social security numbers
  • Financial account details

Mistral’s definition focuses on the request, sharing, or attempt to obtain this identifying information.

from mistralai.client import Mistral
import os
import re

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

def detect_pii(text: str) -> dict:
    """Detects personal data in text."""
    # Layer 1: Mistral Classifier
    response = client.classifiers.moderate(
        model="mistral-moderation-2603",
        inputs=[text]
    )
    pii_score = (response.results[0].category_scores or {}).get("pii", 0)

    # Layer 2: Regex for common patterns
    pii_patterns = {
        "email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
        "telephone_fr": r"(?:0|\+33)[1-9](?:\d{2}){4}",
        "secu_sociale": r"\d{13,15}",
        "carte_bancaire": r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}",
        "iban": r"FR\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}"
    }

    detected_pii = {}
    for name, pattern in pii_patterns.items():
        matches = re.findall(pattern, text)
        if matches:
            detected_pii[name] = len(matches)

    return {
        "score_classifieur": round(pii_score, 3),
        "pii_regex": detected_pii,
        "contient_pii": pii_score > 0.2 or len(detected_pii) > 0
    }

Anonymization Before Processing

Upstream anonymization is the most cost-effective measure in this entire lesson because it changes the nature of the problem: data that never leaves your infrastructure raises no transfer issues. Replacing names, emails, and numbers with tokens ([PERSON_1], [EMAIL_1]) before calling the model almost always preserves meaning for analysis—and if you keep the mapping table locally, you can reinject the real values into the response. As long as this table exists, this is legally pseudonymization, not anonymization: the data remains personal, but the data sent to the model no longer allows identification of the person on its own.

def anonymize_text(text: str) -> tuple:
    """Anonymizes text and keeps a restoration mapping."""
    mapping = {}
    anonymized_text = text

    # Anonymize emails
    emails = re.findall(
        r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
        text
    )
    for i, email in enumerate(set(emails)):
        placeholder = f"[EMAIL_{i+1}]"
        mapping[placeholder] = email
        anonymized_text = anonymized_text.replace(email, placeholder)

    # Anonymize phones
    phones = re.findall(
        r"(?:0|\+33)[1-9](?:[\s.-]?\d{2}){4}",
        text
    )
    for i, tel in enumerate(set(phones)):
        placeholder = f"[TEL_{i+1}]"
        mapping[placeholder] = tel
        anonymized_text = anonymized_text.replace(tel, placeholder)

    # Anonymize names (if detected by upstream NER)
    # Add your own NER logic here

    return anonymized_text, mapping

def restore_text(anonymized_text: str, mapping: dict) -> str:
    """Restores anonymized data after processing."""
    restored_text = anonymized_text
    for placeholder, value in mapping.items():
        restored_text = restored_text.replace(placeholder, value)
    return restored_text

# Usage in pipeline
original_text = "Contactez [email protected] au 06 12 34 56 78"
anonymized_text, mapping = anonymize_text(original_text)
# → "Contactez [EMAIL_1] au [TEL_1]"

# Send anonymized text to model
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": anonymized_text}]
)

GDPR-Compliant Moderation Logs

Logs are the classic blind spot: we think “personal data” regarding user messages, but rarely regarding the technical traces that copy them. Yet a moderation log retaining the analyzed text is a personal database, with all the ensuing obligations. The rule of thumb: log text only for blocked content—you will need it in case of dispute—and log only scores for accepted content.

import hashlib
import hmac
from datetime import datetime, timedelta, timezone


class LogModerationRGPD:
    """GDPR-compliant moderation logger."""

    def __init__(self, retention_jours: int = 90):
        self.retention_jours = retention_jours
        # Mandatory secret key, kept out of code (vault or environment variable)
        self.key = os.environ["PII_HMAC_KEY"].encode()

    def _pseudonymiser(self, user_id: str) -> str:
        """Pseudonymizes a user identifier (HMAC-SHA-256)."""
        return hmac.new(self.key, user_id.encode(), hashlib.sha256).hexdigest()[:16]

    def log_moderation(self, user_id: str, scores: dict, decision: str,
                       text: str | None = None) -> dict:
        """Records a moderation decision."""
        maintenant = datetime.now(timezone.utc)
        entry = {
            "timestamp": maintenant.isoformat(),
            "user_hash": self._pseudonymiser(user_id),
            "scores": scores,
            "decision": decision,
            # Do NOT store full text by default
            "texte_longueur": len(text) if text else 0,
            "conserver_jusqu_au": (maintenant + timedelta(days=self.retention_jours)).isoformat(),
        }

        # Store text excerpt only if blocked (for audit)
        if decision == "blocked" and text:
            entry["texte_tronque"] = text[:200]

        return entry

    def purger_anciens_logs(self, logs: list[dict]) -> list[dict]:
        """Keeps logs whose retention end date has not passed."""
        maintenant = datetime.now(timezone.utc)
        return [
            log for log in logs
            if datetime.fromisoformat(log["conserver_jusqu_au"]) > maintenant
        ]

Two precautions in this code bear mentioning. The pseudonymization key has no default value: a key written in the code, known to everyone, would allow recalculating the hash of any identifier. Furthermore, pseudonymized data remains personal data under the GDPR (Recital 26): this log remains subject to retention periods and data subject rights.

Audit Trail for Compliance

The audit trail answers a specific question a user or authority may ask you: “Why was this content blocked?” Without a trace, your only honest answer is “I don’t know anymore”—untenable given that the GDPR requires, for fully automated decisions under Article 22, informing the person about the underlying logic (Articles 13 to 15), and the AI Act adds its own transparency obligations. The audit trail retains the decision, the scores that motivated it, and the version of the policy applied—without necessarily retaining the text itself.

class AuditTrail:
    """Audit trail for moderation decisions."""

    def __init__(self, storage_path: str):
        self.storage_path = storage_path

    def record_decision(self, data: dict) -> str:
        """Records a decision with a unique identifier."""
        import uuid

        audit_id = str(uuid.uuid4())
        entry = {
            "audit_id": audit_id,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "type_decision": data.get("decision"),
            "modele_moderation": "mistral-moderation-2603",
            "scores": data.get("scores"),
            "seuils_appliques": data.get("seuils"),
            "politique_version": data.get("politique_version"),
            "action_prise": data.get("action"),
            "base_legale": "interet_legitime",
            "responsable": "systeme_automatise"
        }

        # Write to audit file
        import json
        with open(self.storage_path, "a") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")

        return audit_id

GDPR Checklist for Your AI Application

Here are the points to verify to ensure your moderation system is compliant:

  • Documented legitimate interest for content moderation
  • User notification about automated processing
  • Ability to contest an automated decision

Data Minimization

  • Store only strictly necessary data
  • Anonymize or pseudonymize user identifiers
  • Truncate text in moderation logs
  • Do not retain accepted texts (scores only)

Retention Period

  • Define a retention period for each data type
  • Moderation logs: 90 days maximum (unless legal obligation requires otherwise)
  • Audit trail: per sectoral regulation
  • Scheduled automatic purge

User Rights

  • Right of access to moderation decisions concerning them
  • Right to rectification if the decision is contested
  • Right to explanation of the automated decision

Key Takeaways

  • The GDPR applies fully to your AI moderation systems
  • Use Mistral’s pii category AND regex to detect personal data
  • Anonymize texts before sending them to the model when possible
  • Pseudonymize user identifiers in logs
  • Implement an audit trail for decision traceability
  • Define a retention period and automatically purge logs