vanna-clubpetro/docs/deploy.md
leonardosalazar-cp 1d152c0dce Initial commit: Vanna 2.0 deployment for ClubPetro
Wrapper application around upstream Vanna with:
- Tenant-aware ChromaDB memory (per program/store)
- ClickHouse RLS runner with introspection guards
- PT-BR system prompt and chat translations
- Custom Plotly chart generator (ranked bar, datetime coercion)
- Embed bootstrap (theme pierce + i18n + markdown) shared by demo and React app
- Event sink for chat turn observability
2026-04-29 17:22:05 -03:00

17 KiB

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:

# 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:

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

# 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):

-- 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:

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. 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:

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):

<!-- 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:

# 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_dbobrigató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:

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.pynão precisa, o prompt é lido a cada boot do agente.

Sequência segura:

# 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.