# ============================================================
# 15 — winnex-ai-normalize: multi-provider failover without lies
#
# Colab:  !pip install winnex-ai-normalize
#
# WHAT THIS SHOWS
#   A RAG pipeline usually has one embedding provider. When it fails,
#   most pipelines substitute a zero vector or a random one and carry
#   on — which silently destroys the retrieval guarantee.
#
#   This example shows the contract: providers are tried in order, and
#   if every one fails the call raises instead of inventing a vector.
# ============================================================

# instalação automática — Colab, Jupyter e script local.
# No Colab, "!pip" dentro de comentário não executa: a instalação
# precisa acontecer de dentro da célula, antes dos imports.
import importlib
import subprocess
import sys

for _pkg in ["winnex-ai-normalize"]:
    _mod = _pkg.replace("-", "_")
    try:
        __import__(_mod)
        continue
    except ImportError:
        pass
    _r = subprocess.run(
        [sys.executable, "-m", "pip", "install", "-q", "--upgrade", _pkg],
        capture_output=True, text=True
    )
    # O interpretador do Colab já está em execução quando o pip termina.
    # Sem invalidar o cache, o import abaixo pode não enxergar o pacote.
    importlib.invalidate_caches()
    try:
        __import__(_mod)
    except ImportError:
        sys.stderr.write(_r.stdout or "")
        sys.stderr.write(_r.stderr or "")
        raise SystemExit(
            "\n\nNão foi possível importar " + _pkg + " após instalar.\n"
            "No Colab: menu 'Ambiente de execução' -> 'Reiniciar sessão',\n"
            "e execute esta célula novamente.\n"
        )

import numpy as np
from winnex_ai_normalize import (
    NormalizeConfig,
    normalize_l2,
    validate_embeddings,
    check_embedding_drift,
    EmbeddingFingerprint,
)

# ── 1. configure a provider chain ───────────────────────────
cfg = NormalizeConfig(
    default_dim=384,
    default_provider="qwen3",
    normalize_l2=True,
    validate_nan=True,
)
print(f"provider padrão : {cfg.default_provider}")
print(f"dimensão        : {cfg.default_dim}")
print(f"normaliza L2    : {cfg.normalize_l2}")
print(f"valida NaN      : {cfg.validate_nan}")

# ── 2. the L2 contract, applied by hand ────────────────────

v = np.random.randn(100, 384).astype(np.float32) * 5.0
antes = np.linalg.norm(v, axis=1)
depois = np.linalg.norm(normalize_l2(v), axis=1)
print(f"\nnorma antes  : min={antes.min():.2f} max={antes.max():.2f}")
print(f"norma depois : min={depois.min():.4f} max={depois.max():.4f}")

assert np.allclose(depois, 1.0, atol=1e-5), "L2 não normalizou"
print("contrato L2 satisfeito — todo vetor unitário")

# ── 3. validation rejects the things that break the bound ──
print("\nvalidação de entrada:")
casos = {
    "vetores válidos":       np.random.randn(50, 384).astype(np.float32),
    "com NaN":               np.full((50, 384), np.nan, dtype=np.float32),
    "dimensão errada":       np.random.randn(50, 128).astype(np.float32),
}
for nome, arr in casos.items():
    try:
        validate_embeddings(arr, dim=384)
        print(f"  [aceito] {nome}")
    except Exception as e:
        print(f"  [recusado] {nome} — {type(e).__name__}")

# ── 4. drift detection ─────────────────────────────────────
# same provider, space moved: old and new vectors are no longer
# comparable, and mixing them corrupts every query.
# A fingerprint is (batch_id, norm_mean) — the cheap signature that
# detects a space shift without storing the vectors themselves.
centroide = np.random.randn(384).astype(np.float32)
centroide /= np.linalg.norm(centroide)

fp_referencia = EmbeddingFingerprint("openai", 384, centroide)
fp_referencia.batch_id = 1

fp_igual = EmbeddingFingerprint("openai", 384, centroide)   # mesmo espaço
fp_igual.batch_id = 2

# centroide deslocado: o mesmo provider devolveu vetores num espaço novo
centroide_novo = centroide + np.random.randn(384).astype(np.float32) * 0.9
centroide_novo /= np.linalg.norm(centroide_novo)
fp_deslocado = EmbeddingFingerprint("openai", 384, centroide_novo)
fp_deslocado.batch_id = 3

print("\ndetecção de drift:")
for nome, fp in (("mesmo espaço", fp_igual), ("espaço deslocado", fp_deslocado)):
    flags = check_embedding_drift(fp_referencia, fp)
    if flags:
        for f in flags:
            print(f"  [{f.code}] {nome}: {f.message[:80]}")
    else:
        print(f"  [sem flag] {nome}")

print("""
Por que isso importa
────────────────────
Um vetor zero passa silenciosamente em quase todo pipeline de produção.
Ele não levanta erro, não aparece em log, e destrói a garantia do bound
sem que ninguém perceba — a prova continua "válida" sobre lixo.

Este pacote recusa. É a diferença entre um pipeline que falha alto e um
que falha em silêncio.
""")
