# Deploy — Vanna 2.0 (ClubPetro IA)

Guia de deploy do agente conversacional sobre `gold.sales` (ClickHouse Cloud) com chat embarcável via Web Component.

## 1. Arquitetura de runtime

```
Cliente (browser do dashboard)
        │  HTTPS (SSE/WebSocket)
        ▼
┌──────────────────────────────────────┐
│ Servidor app (FastAPI + uvicorn)     │
│  • server.py (VannaFastAPIServer)    │
│  • Agent (build_agent / agent.py)    │
│  • RLSClickHouseRunner (rls_runner)  │
│  • TenantAwareChromaMemory           │
│  • EventSink (events.vanna_ai)       │
└──────────────────────────────────────┘
        │ HTTPS 8443         │ HTTPS
        ▼                    ▼
  ClickHouse Cloud      OpenAI API
   (db `gold`,         (LLM + embeddings
    user `wren_ia`)     opcional)
        ▲
        │ INSERT
        └── events.vanna_ai (telemetria de turnos)

Estado local (no servidor app):
  • ./chroma_db/         — vetor store persistente
  • ./data_storage/      — CSVs intermediários por usuário (run_sql → visualize_data)
  • ~/.cache/chroma/     — modelo ONNX de embedding (~80 MB, baixado no 1º boot)
```

Componente único, **stateful no disco** (ChromaDB + cache do modelo). Não escala horizontalmente sem volume compartilhado ou re-treino por instância.

## 2. Pré-requisitos do alvo

| Item | Versão / Observação |
|------|---------------------|
| Python | 3.10+ recomendado (3.9 funciona mas gera warnings de LibreSSL/urllib3) |
| Node.js | 18+ (apenas para buildar o bundle do webcomponent) |
| Disco | ≥ 2 GB livres para `chroma_db/`, `data_storage/`, cache do modelo ONNX e venv |
| RAM | ≥ 2 GB (ChromaDB + ONNX runtime + uvicorn worker) |
| Egress HTTPS | OpenAI (`api.openai.com`) e ClickHouse Cloud (porta 8443) |

## 3. Build de artefatos

Dois passos, executados **antes** do deploy ou no pipeline:

```bash
# 1. Bundle do webcomponent (gera vanna/frontends/webcomponent/dist/vanna-components.js, ~7.5 MB).
#    Esse arquivo é gitignored — precisa ser produzido no host de build.
cd vanna/frontends/webcomponent
npm install
npm run build
cd -

# 2. Dependências Python (instala vanna em modo editável).
python -m venv venv
source venv/bin/activate
pip install -e ./vanna
pip install -r requirements.txt   # se houver; senão instalar extras manualmente
```

Ao subir versão do upstream:
```bash
cd vanna && git pull && cd ..
pip install -e ./vanna
cd vanna/frontends/webcomponent && npm run build && cd -
```

## 4. Variáveis de ambiente (`.env` ou secret manager)

Carregadas via `load_dotenv()` em `agent.py` e `train.py`. Nunca commitar.

| Variável | Obrigatória | Descrição |
|----------|-------------|-----------|
| `OPENAI_API_KEY` | ✅ | Chave da API. Use uma key dedicada por ambiente para auditoria. |
| `OPENAI_MODEL` | recomendada | Default upstream pode mudar; fixe explicitamente (ex.: `gpt-5.4-mini`). |
| `OPENAI_TEMPERATURE` | opcional | Default `1.0` (compat com `gpt-5*`). Para modelos que aceitam ajuste, use `0.2` para SQL determinístico. |
| `CLICKHOUSE_HOST` | ✅ | Hostname Cloud (ex.: `xxx.us-central1.gcp.clickhouse.cloud`). |
| `CLICKHOUSE_PORT` | ✅ | `8443`. |
| `CLICKHOUSE_DATABASE` | ✅ | `gold`. |
| `CLICKHOUSE_USER` | ✅ | `wren_ia` (com GRANTs columnares — ver §6). |
| `CLICKHOUSE_PASSWORD` | ✅ | Secret. |
| `CLICKHOUSE_SECURE` | ✅ | `true` (HTTPS). |
| `RLS_PROGRAM_ID` / `RLS_STORE_ID` / `RLS_USER_ID` | apenas CLI | Defaults para `python ask.py`. **Não são lidos pelo servidor web** — o resolver web pega de query string. |

Em produção use secret manager (AWS Secrets Manager, GCP Secret Manager, Vault, sealed-secrets) montando como env vars no container — não copie `.env` para a imagem.

## 5. Sequência de bootstrap

```bash
# Primeira vez OU sempre que mudar GRANT/RLS_TABLES/RLS_INTERNAL_COLS:
rm -rf chroma_db/
python train.py    # popula chroma_db/ com schema das tabelas em RLS_TABLES

# Subir o servidor:
uvicorn server:app --host 0.0.0.0 --port 8765 --workers 1
```

> ⚠️ **Use `--workers 1`** ou um único processo por volume `chroma_db/`. Múltiplos workers escrevendo no mesmo SQLite do Chroma corrompem o index. Para escalar, ver §11.

> ⚠️ **Primeiro turno depois do boot** demora ~30s extras: ChromaDB baixa o modelo ONNX de embedding (~80 MB) para `~/.cache/chroma/`. Pré-aquecer no build se quiser remover essa latência.

## 6. ClickHouse — usuário e GRANTs

A defesa de RLS é **em duas camadas**: (a) `additional_table_filters` injetado pelo `RLSClickHouseRunner` por request, (b) GRANT no usuário.

Setup do usuário (executar como admin no ClickHouse Cloud):

```sql
-- 1. Criar usuário (ou rotacionar senha)
CREATE USER wren_ia IDENTIFIED BY '<senha-forte>';

-- 2. SELECT apenas nas colunas seguras de gold.sales
GRANT SELECT(
  program_id, store_id,           -- necessárias pro RLS, escondidas do LLM via RLS_INTERNAL_COLS
  sale_id, cartao_do_cliente, nome_da_rede, nome_da_loja, tipo_loja,
  categoria_loja, nome_do_cliente, categoria_do_cliente, nome_do_atendente,
  produto, categoria_do_produto, quantidade_total_produto, valor_total_produto,
  desconto, pontuacao_produto, voucher_aplicado_venda, data_da_compra,
  fidelizada, e_combustivel
) ON gold.sales TO wren_ia;

-- 3. Negar a view exportável (defesa em profundidade — o runner também filtra)
-- (não conceder SELECT em gold.vw_relatorios_exportaveis_analitico_vendas)

-- 4. INSERT na tabela de telemetria de turnos
GRANT INSERT ON events.vanna_ai TO wren_ia;
```

> ⚠️ `REVOKE SELECT(col) ON system.tables` é **silenciosamente ignorado** em ClickHouse Cloud (acesso herdado de role default não-revogável). A blindagem de introspecção é **app-side**, via regex no `RLSClickHouseRunner` (`_FORBIDDEN_SCHEMA_RE`, `_INTROSPECTION_STMT_RE`). Não desabilite essas guards.

> ⚠️ `SELECT *` em `gold.sales` falha com `ACCESS_DENIED` porque exige permissão em todas as colunas. O LLM é instruído a listar colunas; manter assim.

Schema da tabela de telemetria:

```sql
CREATE TABLE events.vanna_ai (
  ts            DateTime64(3, 'UTC')   DEFAULT now64(3),
  turn_id       String,
  program_id    String,
  store_id      String,
  user_id       String,
  question      String,
  response      String,
  status        Enum8('success'=1, 'error'=2, 'partial'=3),
  error_message String,
  tools_called  Array(String),
  sql_executed  String,
  chart_type    String,
  chart_title   String,
  duration_ms   UInt32
) ENGINE = MergeTree
ORDER BY (program_id, store_id, ts);
```

> Confira o schema atual em `events_sink.py` antes de criar — manter alinhado.

## 7. Multi-tenant (`program_id` / `store_id`)

Identidades vêm na query string dos endpoints `chat_sse` / `chat_websocket` / `chat_poll`. O `RequestContextUserResolver`:

1. Lê `program_id`, `store_id`, `user_id` de `request_context.query_params`.
2. Valida com regex `^[A-Za-z0-9_-]+$`.
3. Levanta `PermissionError` se ausente/inválido — request é rejeitada antes de qualquer SQL.

**Responsabilidade do app cliente** (dashboard que embarca o chat): preencher os 3 IDs com o tenant logado **no servidor** (não no cliente do browser, que poderia ser modificado). O snippet de embed (§9) recebe os IDs do backend do cliente e injeta no atributo `sse-endpoint`.

## 8. Reverse proxy / TLS

`<vanna-chat>` é embarcado em sites de terceiros (dashboard ClubPetro). Requisitos:

- **HTTPS obrigatório** — browsers bloqueiam mixed-content (página HTTPS chamando SSE HTTP).
- **CORS configurado** no `VannaFastAPIServer` (`server.py:config.cors`) listando o domínio do dashboard. Sem isso, o EventSource é bloqueado.
- **Cookies / autenticação**: o servidor extrai `RequestContext` de cookies, headers, query params. Se o dashboard já tem sessão própria, prefira passar IDs assinados (JWT) em vez de IDs crus na query string.
- **Buffering desabilitado** para `/api/vanna/v2/chat_sse` no proxy (nginx: `proxy_buffering off`, Cloudflare: incompatível com SSE em planos free — usar WebSocket ou TCP proxy).
- **Timeouts**: turnos longos (LLM + SQL + viz) podem levar 60-120s. Configure `proxy_read_timeout` ≥ 180s.

Exemplo nginx para SSE:

```nginx
location /api/vanna/v2/chat_sse {
    proxy_pass http://localhost:8765;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 180s;
    chunked_transfer_encoding on;
}
```

## 9. Embed no app cliente

Quatro peças, **na ordem**, no HTML do dashboard (snippet completo em `static/embed-demo.html`):

```html
<!-- 1. Fonte -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">

<!-- 2. Tema (sobrescreve design tokens via adoptedStyleSheets) -->
<link rel="stylesheet" href="https://SEU-VANNA/vanna-theme.css">

<!-- 3. Patch de Shadow DOM + carregamento do tema (copiar bloco de embed-demo.html) -->
<script>/* monkey-patch attachShadow para adoptar /vanna-theme.css em cada shadow root */</script>

<!-- 4. Bundle e instância -->
<script type="module" src="https://SEU-VANNA/static/vanna-components.js"></script>

<vanna-chat
  id="chat"
  api-base="https://SEU-VANNA"
  sse-endpoint="/api/vanna/v2/chat_sse?program_id=PROG&store_id=LOJA&user_id=USR"
  ws-endpoint="/api/vanna/v2/chat_websocket?program_id=PROG&store_id=LOJA&user_id=USR"
  poll-endpoint="/api/vanna/v2/chat_poll?program_id=PROG&store_id=LOJA&user_id=USR"
  startingstate="minimized"
  theme="light"
  title="ClubPetro IA">
</vanna-chat>

<script>
  customElements.whenDefined("vanna-chat").then(() => {
    const chat = document.getElementById("chat");
    chat.showProgress = false;     // sidebar
    if (chat.windowState !== "minimized") chat.windowState = "minimized";
  });
</script>
```

> ⚠️ Atributo é **`startingstate`** (lowercase, sem hífen) — Lit converte `startingState` para lowercase, não kebab-case.

> ⚠️ `showProgress` é boolean Lit; **não desliga via HTML attribute** (qualquer presença = true). Sempre via JS após `whenDefined`.

`PROG`/`LOJA`/`USR` devem ser substituídos pelo backend do dashboard a cada request, baseando-se na sessão autenticada.

## 10. Containerização (Docker)

`Dockerfile` recomendado:

```dockerfile
# Build stage — bundle do webcomponent
FROM node:18-alpine AS frontend-build
WORKDIR /build
COPY vanna/frontends/webcomponent ./
RUN npm install && npm run build

# Runtime
FROM python:3.11-slim
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential git && rm -rf /var/lib/apt/lists/*

COPY vanna/ ./vanna/
COPY --from=frontend-build /build/dist ./vanna/frontends/webcomponent/dist
RUN pip install --no-cache-dir -e ./vanna

COPY *.py ./
COPY static/ ./static/

# Pré-baixar modelo ONNX de embedding pra evitar latência no 1º request
RUN python -c "from chromadb.utils.embedding_functions import DefaultEmbeddingFunction; DefaultEmbeddingFunction()"

EXPOSE 8765
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8765", "--workers", "1"]
```

Volumes persistentes:
- `/app/chroma_db` — **obrigatório** persistir; perda implica re-rodar `train.py`.
- `/app/data_storage` — efêmero (CSVs intermediários); ok perder, mas afeta `visualize_data` se reciclar entre o `run_sql` e a visualização do mesmo turno.

Healthcheck:
```yaml
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8765/health"]   # endpoint do upstream
  interval: 30s
  timeout: 5s
  retries: 3
```

## 11. Escalabilidade

Single-process por design (ChromaDB SQLite). Opções para escalar:

| Estratégia | Quando | Trade-off |
|------------|--------|-----------|
| Vertical (mais CPU/RAM) | Tráfego baixo-médio | Simples; ainda single-tenant de instância |
| Múltiplas instâncias com volume read-only de `chroma_db` | Read-heavy, sem `save_text_memory` em runtime | Memórias salvas pelo LLM (tool `save_text_memory`) só persistem na instância que recebeu o turno; perde-se aprendizado |
| Migrar `ChromaAgentMemory` para Chroma server (HTTP) ou outro vetor store (Postgres+pgvector, Qdrant) | Multi-instância com aprendizado compartilhado | Mudança de integração — não é só config; ver `agent.py` |

ChromaDB **não tem locking inter-process** no modo embedded — deploy multi-worker sem coordenação corrompe o index.

## 12. Re-treino e mudanças de schema

Quando rodar `python train.py`:
- ✅ Coluna nova adicionada ao GRANT em `gold.sales`.
- ✅ Coluna revogada (some do contexto do LLM).
- ✅ Editou `RLS_TABLES` ou `RLS_INTERNAL_COLS`.
- ✅ Editou `system_prompt.py` — **não precisa**, o prompt é lido a cada boot do agente.

Sequência segura:
```bash
# 1. Em janela de baixo tráfego, ou em instância de staging
rm -rf chroma_db/
python train.py
# 2. Reiniciar uvicorn (carregar memórias novas no enhancer)
```

> Em produção zero-downtime: faça em uma instância nova, troque o tráfego (blue/green), descarte a antiga.

## 13. Observabilidade

- **`events.vanna_ai`**: 1 row por turno (pergunta, resposta, SQL, tools, status, duração). Use para dashboards Grafana ou queries ad-hoc para entender uso e detectar regressão.
- **Logs uvicorn**: stdout/stderr; capture com agente do cloud (Cloud Logging, CloudWatch, Loki).
- **OpenAI usage**: dashboard da OpenAI para custo por modelo + key.
- **ClickHouse query log**: `system.query_log` filtrado por `user='wren_ia'` mostra todo SQL emitido pelo agente — útil em auditoria de RLS.

Não há `/metrics` Prometheus pronto — adicionar via `prometheus-fastapi-instrumentator` se necessário (não está no escopo atual).

## 14. Custos

Dimensionar antes de escalar acesso:
- **OpenAI**: cada turno chama LLM 1-N vezes (planejamento + tool calls + síntese). `gpt-5.4-mini` é mais barato; `gpt-5` consome significativamente mais. Cache de prompt do upstream Vanna ajuda em conversas longas — ver `claude-api` skill se trocar provider.
- **ClickHouse Cloud**: cobrado por compute-hour + storage. Queries do agente raramente escaneiam tudo (filtros RLS reduzem escopo), mas perguntas amplas ("ranking de produtos no ano") podem virar full scans.
- **Egress de bundle**: `vanna-components.js` ~7.5 MB. Servir via CDN se o dashboard tiver volume alto.

## 15. Segurança — checklist pré-prod

- [ ] `.env` fora da imagem; secrets via secret manager.
- [ ] Senha do `wren_ia` rotacionada e diferente de staging.
- [ ] GRANTs revisados (apenas as 20 colunas seguras + `INSERT events.vanna_ai`).
- [ ] CORS limitado ao(s) domínio(s) do dashboard cliente.
- [ ] HTTPS obrigatório (HSTS recomendado).
- [ ] `program_id`/`store_id` populados pelo backend do cliente, **nunca** pelo browser.
- [ ] Rate limit no proxy (ex.: 30 req/min por IP) — protege contra abuso da OpenAI key.
- [ ] Logs **não** persistem `OPENAI_API_KEY` nem senha do CH (verificar uvicorn access log + qualquer logger custom).
- [ ] `RLSClickHouseRunner` regex guards (`_FORBIDDEN_SCHEMA_RE`, `_INTROSPECTION_STMT_RE`) **ativas**.
- [ ] Smoke test pós-deploy: `python ask.py "quantos clientes únicos compraram em janeiro?"` retorna resultado filtrado por `RLS_PROGRAM_ID`/`RLS_STORE_ID`.
- [ ] Smoke test de RLS: tentar query com `program_id` de outro tenant via curl — deve retornar `PermissionError` (resolver) ou 0 rows (filtro injetado).
- [ ] Smoke test de introspecção: pedir ao agente "liste as tabelas do banco" — runner deve rejeitar com mensagem app-side.

## 16. Cuidados conhecidos

- **Bundle gitignored**: `vanna/frontends/webcomponent/dist/vanna-components.js` precisa ser buildado em todo deploy. Se o pipeline pular o `npm run build`, o servidor sobe mas o `<vanna-chat>` nunca registra (404 no `/static/vanna-components.js`).
- **`pip install -e './vanna[clickhouse]'` falha no macOS** com "non-local file URIs are not supported". Instale extras separados.
- **Primeiro request lento**: download do ONNX (~80 MB). Pré-aquecer no Dockerfile (§10).
- **`adoptedStyleSheets` strip de `@import`**: `vanna-theme.css` não pode importar Google Fonts via CSS. Use `<link>` no `<head>` da página host.
- **`startingstate` (não `starting-state`)**: `@property() startingState` no Lit gera atributo lowercase, sem kebab-case. Usar com hífen é silenciosamente ignorado.
- **Filtros RLS via `dict`** quebram com `CANNOT_PARSE_QUOTED_STRING`. `_format_table_filter_map` em `rls_runner.py` constrói o Map literal manualmente — não trocar por `client.query(..., settings={dict})`.
- **`events.vanna_ai` insert falha silenciosamente** se o GRANT estiver faltando — não derruba o chat, mas perde telemetria. Verificar contagem de rows pós-deploy.

## 17. Rollback

Estado mutável: `chroma_db/` (vetor store) e o schema de `events.vanna_ai`. Para reverter:

1. Manter snapshot do `chroma_db/` por versão (`chroma_db.v1.tar.gz`) antes de cada `train.py` em prod.
2. Tag do bundle (`vanna-components.<hash>.js`) — versionar arquivo se cliente embarca via CDN.
3. Reverter código via tag git → restaurar `chroma_db/` correspondente → reiniciar uvicorn.

Não há migração de schema na app — `events.vanna_ai` é append-only; mudança de schema requer `ALTER TABLE` no CH.
