# 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 ''; -- 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 `` é 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 ``` > ⚠️ 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 `` 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 `` no `` 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..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.