Volver a la wiki

Servicio de anonimización para LLM (anonymize_for_ai)

Descripción

Módulo: core/workspace_api.py
Función: _anonymize_for_ai(data: dict) -> dict
Propósito: Preparar datos agregados de múltiples tenants para análisis de IA sin revelar identidad de clientes.

Qué hace

Anonimiza datos antes de enviar a un LLM externo (Google AI Studio, Claude, etc.). Preserva la distribución (números) que el LLM necesita para análisis, elimina identificadores que revelarían clientes.

Transformaciones

CampoAntesDespuésRazón
tenant_usage[].name"ACME Corp""org-1"No revelar identidad de cliente
tenant_usage[].id11Preservado (referencia opaca)
tenant_usage[].racks55Preservado (distribución, no es secreto)
slow_queries[].querySELECT * FROM x WHERE password='hunter2'SELECT * FROM x WHERE password='***'Redactar secretos
db_stats.size_mb100100Preservado (no es sensible)

Algoritmo

def _anonymize_for_ai(data):
    """Quita identificadores de tenant y redacta secretos ANTES de enviar a un LLM externo.
    
    El análisis de rendimiento agrega datos de TODAS las organizaciones (nombres de
    cliente en ``tenant_usage``, texto de queries en ``slow_queries``). Mandar eso a
    Google AI Studio sería una fuga cross-tenant de toda la plataforma. El LLM solo
    necesita la *distribución* (conteos por org), no la identidad real: sustituimos el
    nombre por una etiqueta opaca ``org-<id>`` y redactamos posibles secretos del texto
    de las queries. Trabaja sobre una copia (no muta la respuesta devuelta al caller).
    """
    d = copy.deepcopy(data)
    
    # 1. Anonimizar names en tenant_usage
    for row in d.get("tenant_usage", []) or []:
        if isinstance(row, dict) and "name" in row:
            row["name"] = f"org-{row.get('id', '?')}"
    
    # 2. Redactar secretos en slow_queries
    for q in d.get("slow_queries", []) or []:
        if isinstance(q, dict) and q.get("query"):
            q["query"] = redact_secrets(str(q["query"]))
    
    return d

Características de robustez:

Fuentes de datos

Input

Output

Diccionario transformado, listo para pasar a AIOperations:

# Llamada (perf-review)
return AIOperations.detect_anomalies(_anonymize_for_ai(data), tone=tone)

# Llamada (finops)
return AIOperations.capacity_recommendation(_anonymize_for_ai(data), tone=tone)

El LLM recibe datos sin identificadores, pero con la misma forma y distribución.

Dependencias

from monitoring.services.ai_providers.sanitize import redact_secrets

Invocaciones

  1. _ai_analyze_perf(data, tone="technical") (core/workspace_api.py)

    • Llamada: /api/workspace/perf-review?ai=true
    • Paso: AIOperations.detect_anomalies(_anonymize_for_ai(data), tone=tone)
  2. _ai_analyze_finops(data, tone="technical") (core/workspace_api.py)

    • Llamada: /api/workspace/finops?ai=true
    • Paso: AIOperations.capacity_recommendation(_anonymize_for_ai(data), tone=tone)

Ambas envueltas en try/except que logea errores redactados:

except Exception as e:
    logger.warning("workspace perf-review AI analysis failed: %s", redact_secrets(str(e)))
    return "AI analysis temporarily unavailable"

Tests

tests/api/test_config_ia.py:

  1. test_anonymize_for_ai_strips_org_names_and_redacts_queries()

    • Verifica: tenant_usage[0].name == "org-1" (redactado)
    • Verifica: tenant_usage[0].racks == 5 (preservado)
    • Verifica: "hunter2" no está en slow_queries[0].query (secreto redactado)
    • Verifica: db_stats.size_mb == 100 (no sensible, preservado)
  2. test_anonymize_for_ai_does_not_mutate_original()

    • Input: {"tenant_usage": [{"id": 1, "name": "ACME Corp"}]}
    • Llamada a _anonymize_for_ai(data)
    • Verifica: El dict original mantiene "ACME Corp" (deep copy, no mutación)
  3. test_anonymize_for_ai_tolerates_missing_keys()

    • Entrada incompleta: {"costs": {}} (no tenant_usage/slow_queries)
    • Verifica: No crashea, devuelve igual

Seguridad

Threat Model

No es una panacea

Alternativas consideradas

Véase también

Subir