Jornada de Dados · aula prática
Projeto completo: uma área de atendimento com AI Dev Kit.
Todo comando e todo prompt desta página é copiável. Siga os blocos na ordem e, ao final, você terá a área de atendimento de uma distribuidora de energia funcionando no Databricks: medalhão, dashboard, copiloto conversacional e travas de segurança para o agente — construídos com as skills oficiais do AI Dev Kit, um prompt de cada vez.
Preencha antes de começar. Os dois valores entram em todos os comandos
da página — do auth login do bloco 2.0 ao grants update do 7.0 —
e saem prontos quando você clica em copiar. Fica salvo no navegador; o
botão zerar, no rodapé do índice, limpa.
O que você vai construir.
Entenda o destino antes de sair digitando.
O projeto é a Grid Intelligence: a plataforma de dados e IA da Luz do Vale Distribuidora S.A., uma distribuidora de energia elétrica fictícia da região de Campinas. Quatro bases sintéticas com quatro anos de histórico — unidades consumidoras, interrupções de fornecimento, consumo diário e chamados de atendimento com transcrição — viram três camadas, cada uma em um schema dedicado do Unity Catalog.
flowchart TB RAW["<b>raw.landing</b> · Parquet, um arquivo por base<br/><small>unidades consumidoras · interrupcoes · consumo diario (5,85 mi) · chamados</small>"] BRONZE["<b>bronze</b> · ingestao fiel<br/><small>4 tabelas como chegaram. Nada e filtrado, nada e corrigido</small>"] SILVER["<b>silver</b> · onde a regra de negocio mora<br/><small>cadastro limpo · a regra dos 3 minutos · dedup e baseline · texto anonimizado e enriquecido</small>"] GOLD["<b>gold</b> · uma pergunta de diretoria por tabela<br/><small>continuidade · prioridade de inspecao · saude do cliente · painel do dia</small>"] USO["<b>consumo</b><br/><small>dashboard AI/BI · copiloto conversacional · relatorio executivo</small>"] RAW --> BRONZE --> SILVER --> GOLD --> USO
Bronze
Ingestão fiel. Leitura negativa e interrupção de 40 segundos entram como chegaram: filtrar aqui é destruir evidência.
Silver
Limpeza, a regra regulatória dos 3 minutos, e as transcrições anonimizadas e enriquecidas por IA nativa.
Gold
Uma tabela por pergunta de diretoria: continuidade, prioridade de inspeção, saúde do cliente, painel do dia.
Porque cada tabela existe por causa de dinheiro real. Interromper além do limite regulatório gera compensação automática ao consumidor — R$ 1,1 bilhão pagos em 2024. Perda não técnica somou R$ 11,5 bilhões em 2025. E as ligações que ninguém lê contêm o aviso antecipado da reclamação na ouvidoria. É um domínio em que “por que essa tabela existe?” sempre tem resposta.
Em volta disso, o setup que faz a diferença no dia a dia:
- DAB (Declarative Automation Bundle) — o projeto inteiro como código, com dev e prod isolados.
- Skills oficiais — a documentação atualizada do Databricks dentro do contexto do agente.
- CLI do Databricks — o agente enxergando e executando coisas no seu workspace, pelo mesmo comando que você usa.
- Sequência de prompts — oito etapas, uma feature por vez, com deploy e resultado visível em cada uma.
- Guard rails — travas determinísticas que impedem o agente de destruir dados.
Tudo aqui roda na Databricks Free Edition. Crie a sua em login.databricks.com. Ela só oferece compute serverless — por isso a resposta “sim” para serverless no bloco 4.0 é obrigatória.
O vocabulário, antes de tudo.
Dez termos. Sem eles, metade da aula vira ruído.
Distribuição de energia tem vocabulário próprio, e ele é preciso: trocar uma palavra por outra parecida muda o número. Vale gastar três minutos aqui — todo o resto da aula usa estes termos sem reexplicar.
Quem é quem
| Termo | O que é | Cuidado |
|---|---|---|
| UC unidade consumidora |
O ponto de entrega de energia — um medidor, um endereço. No dataset são 4.000. | Não é sinônimo de cliente. É a confusão que mais estraga análise no setor |
| Cliente | A pessoa. Pode ter três UCs: casa, sítio e a loja. | Quem reclama na ouvidoria é o cliente, não a UC — por isso existe uma tabela por grão |
| Conjunto | Subdivisão geográfica da área de concessão. São 6: Campinas Centro, Campinas Norte, Indaiatuba, Sumaré Industrial, Valinhos e Vale Rural. | É a unidade que o regulador usa para cobrar. Todo indicador é por conjunto |
| Interrupção | Um evento de rede em que um trecho fica sem energia. Tem início, fim, causa e quantas UCs atingiu. | Não é a ligação do cliente reclamando — isso é o chamado |
| Religador | Equipamento que desarma quando algo encosta na rede e religa sozinho segundos depois. | É a razão de existir o corte de 3 minutos: a piscada não é apagão |
Os dois indicadores que o regulador cobra
Os dois são médias por unidade consumidora, apuradas por mês e por conjunto. Não é o total de horas sem luz: é quanto, em média, cada UC daquele conjunto sentiu.
| Sigla | Nome | Responde | Unidade |
|---|---|---|---|
| DEC | Duração Equivalente de Interrupção por UC | Quanto tempo, em média, cada UC ficou sem energia | horas |
| FEC | Frequência Equivalente de Interrupção por UC | Quantas vezes, em média, cada UC ficou sem energia | vezes |
Nos dois indicadores, quanto maior, pior — mais horas ou mais vezes sem luz. Então “o conjunto com o pior DEC” é o de maior valor, e é dele que a diretoria quer falar primeiro.
No dataset, o pior é o Vale Rural: 10,86 horas e 13,38 interrupções acumuladas em quatro anos, contra 4,78 h e 5,33 de Campinas Centro. Faz sentido — rede rural é mais longa, mais exposta a vegetação e mais demorada de atender.
Muitas interrupções curtas sobem o FEC e quase não mexem no DEC. Um único apagão de oito horas faz o contrário. Por isso o regulador cobra os dois: um mede constância, o outro mede gravidade.
Perdas e atendimento
| Termo | O que é |
|---|---|
| PNT perdas não técnicas |
Energia distribuída e não faturada. Vem de furto, de fraude no medidor e de erro de leitura, medição e faturamento — as três causas têm a mesma assinatura no dado |
| Baseline de consumo | O padrão recente de consumo de uma UC, usado como referência de normalidade. Aqui: a média dos 90 dias anteriores à janela dos últimos 30 |
| Prioridade de inspeção | Alta, média ou baixa — quanto uma UC merece visita técnica. É a saída da análise de perdas, e é deliberadamente não um rótulo de fraude |
| Chamado | Uma ligação para a central, com transcrição em texto livre. São 2.801 no dataset |
| Ouvidoria | A instância de reclamação acima do atendimento comum — passa pela ANEEL. Cliente que a menciona na ligação é sinal de alerta caro |
Em nenhum lugar do código aparece fraude, furto ou culpado como rótulo de saída. Queda de consumo também é mudança de morador, imóvel desocupado, medidor com defeito e erro de leitura.
A saída é prioridade de inspeção. Em setor regulado, a diferença entre “prioridade” e “acusação” é jurídica, não estilística.
Ferramentas de base.
Quatro binários instalados e respondendo.
Databricks CLI
brew tap databricks/tap
brew install databricks
databricks --version
winget install Databricks.DatabricksCLI
databricks --version
Para atualizar é brew upgrade databricks. Os comandos
databricks update e databricks upgrade não existem —
a CLI responde com erro de comando desconhecido.
uv, GitHub CLI e Claude Code
databricks --version
uv --version
gh --version
claude --version
Se um deles não responder, instale só o que faltar. E se alguma dessas quatro ferramentas for nova para você, não precisa aprender aqui: cada uma tem uma trilha própria onde ela é destrinchada com calma.
| Comando | Ferramenta | Onde a gente destrincha |
|---|---|---|
databricks --version |
Databricks CLI | Trilha Databricks — catálogo, Unity Catalog, jobs, pipelines e a plataforma inteira |
uv --version |
uv | Trilha Python — ambiente virtual, dependências, empacotamento e teste |
gh --version |
GitHub CLI | Trilha GitHub — versionamento, pull request, revisão e Actions |
claude --version |
Claude Code | Trilha Claude Code — prompts, skills, hooks, subagentes e guard rails |
Esta aula é de projeto: as quatro ferramentas entram como meio, não como assunto. Se você travar em alguma delas, o caminho é a trilha correspondente — voltar aqui depois custa dez minutos, e tentar aprender as quatro ao mesmo tempo custa a aula inteira.
# uv — gerenciador de pacotes Python
curl -LsSf https://astral.sh/uv/install.sh | sh
# GitHub CLI
brew install gh
gh auth login
# Claude Code
curl -fsSL https://claude.ai/install.sh | bash
Autenticar no Databricks.
Um perfil nomeado, validado por uma chamada real de API.
O host é a URL do seu workspace no navegador, a parte
https://dbc-xxxxxxx-xxxx.cloud.databricks.com.
databricks auth login --host https://SEU-WORKSPACE.cloud.databricks.com
O navegador abre, você aprova, e a CLI grava o perfil em ~/.databrickscfg.
Quando ele pedir o nome do perfil, dê um nome — por exemplo grid_intelligence.
Não deixe em DEFAULT.
databricks auth profiles
databricks current-user me --profile grid_intelligence
Quem atende cliente tem três, quatro workspaces na mesma máquina. Perfil nomeado
mais --profile grid_intelligence explícito em todo comando é o que impede você
de fazer deploy no workspace errado. E current-user me é a validação
honesta: faz uma chamada de API de verdade, não apenas lê um arquivo local.
Repositório no GitHub.
Cria o repositório primeiro, entra na pasta, e só então o bundle.
gh repo create
| Pergunta | Resposta |
|---|---|
| What would you like to do? | Create a new repository on GitHub from scratch |
| Repository name | LakehouseDatabricks |
| Description | Area de atendimento de uma distribuidora de energia no Databricks: medalhao, dashboard e copiloto conversacional, construidos com Claude Code e as skills do AI Dev Kit. |
| Visibility | Public ou Private, tanto faz |
| Clone locally? | Yes |
cd LakehouseDatabricks
É a única linha que aparece na busca do GitHub, no seu perfil e no card que o LinkedIn monta quando você compartilha o link. Quem abre o repositório decide em dois segundos se vale a pena entrar — e essa decisão é tomada nessa frase, não no README. Diga o que o projeto faz e com que stack; deixe o "por quê" para o README.
Esqueceu na hora do wizard? Dá para acertar depois, sem abrir a interface:
gh repo edit --description "Area de atendimento de uma distribuidora de energia no Databricks: medalhao, dashboard e copiloto conversacional, construidos com Claude Code e as skills do AI Dev Kit."
# de quebra, os topicos — e como o repositorio aparece nas buscas por tema
gh repo edit --add-topic databricks,lakehouse,claude-code,engenharia-de-dados,ai-dev-kit
Funciona igual. Só muda o comando que cria o repositório e a sintaxe do arquivo de pipeline no bloco 10.0. Todo o resto da aula é idêntico.
databricks bundle init
O esqueleto do projeto, criado pelo template oficial.
databricks bundle init --profile grid_intelligence
| Pergunta do wizard | Resposta | Por quê |
|---|---|---|
| Template | default-python | já traz src/, resources/ e a estrutura de pacote |
| Project name | grid_intelligence | vira o nome da pasta e do bundle. Mesmo nome do catálogo de produção e do perfil da CLI: um termo só para procurar em tudo |
| Include a stub notebook | no | vamos gerar código de verdade |
| Include a stub DLT pipeline | no | idem |
| Include a stub Python package | yes | dá a estrutura de biblioteca e testes |
| Use serverless | yes | obrigatório na Free Edition |
| Default catalog for any tables created by this project | workspace (Enter) | os catálogos do projeto ainda não existem — eles nascem no bloco 7.0. Trocar depois é uma linha no databricks.yml |
cd grid_intelligence
workspace e trocar depois
O valor dessa pergunta vira var.catalog no databricks.yml — e
esse é o único ponto do projeto onde o catálogo de dev é nomeado. Nenhum
arquivo .sql escreve o nome do catálogo: todos usam
${catalogo}, que vem da variável.
Ou seja: escolher errado aqui custa uma linha, não uma refatoração. Digitar um catálogo que ainda não existe, por outro lado, faz o wizard reclamar — e você perde a sequência na hora errada.
Por que se chama bundle
Vale desmontar o nome, porque ele explica a ferramenta inteira — e porque você vai encontrar dois nomes diferentes pela internet.
| Palavra | O que quer dizer |
|---|---|
| Bundle pacote |
Job, pipeline, dashboard, schema, volume e o código empacotado viajam
juntos e têm estado compartilhado. Você não cria um job aqui e um pipeline
ali: declara o conjunto, e o deploy reconcilia tudo de uma vez. É por
isso que existe bundle destroy — o pacote sabe exatamente o que criou.
|
| Declarative declarativo |
Você descreve o estado que quer, não os passos para chegar lá. Não existe
“crie o schema, depois o volume, depois o job”: existe “é assim
que o projeto deve estar”, e a plataforma calcula a diferença. Rodar o
deploy duas vezes seguidas não duplica nada.
|
| Automation automação |
O que se declara não é só código: é infraestrutura, agendamento e permissão. Por baixo é Terraform — o mesmo motor que times de infraestrutura usam para provisionar nuvem. |
Era Databricks Asset Bundles — cada peça é um ativo da plataforma. Hoje a CLI chama de Declarative Automation Bundles, mantendo a sigla DABs. Documentação, post de blog e vídeo anteriores usam o nome antigo; é a mesma ferramenta, mesmos comandos.
A troca faz sentido: o ponto não é catalogar ativos, é declarar o estado desejado e deixar a plataforma reconciliar.
Quem já usou Terraform reconhece na hora: mesma ideia de estado
desejado, mesmo plan e apply — aqui chamados
validate e deploy. Quem vem de
docker-compose também: um arquivo declara vários serviços que sobem
juntos. A diferença é que aqui os “serviços” são jobs, pipelines e tabelas.
O arquivo que o wizard escreveu
O databricks.yml é o projeto inteiro descrito em um lugar só. Vale ler linha
por linha agora, porque tudo o que vem depois — pipeline, job, dashboard, agente — vai
pendurar aqui.
bundle:
name: grid_intelligence
uuid: 338627ac-9807-4d4a-9ee6-aed306449a0a
include:
- resources/*.yml
artifacts:
python_artifact:
type: whl
build: uv build --wheel
variables:
catalog:
description: The catalog to use
schema:
description: The schema to use
targets:
dev:
mode: development
default: true
workspace:
host: https://SEU-WORKSPACE.cloud.databricks.com
variables:
catalog: workspace
schema: ${workspace.current_user.short_name}
prod:
mode: production
workspace:
host: https://SEU-WORKSPACE.cloud.databricks.com
root_path: /Workspace/Users/SEU-EMAIL/.bundle/${bundle.name}/${bundle.target}
variables:
catalog: workspace
schema: prod
permissions:
- user_name: SEU-EMAIL
level: CAN_MANAGE
| Bloco | O que é | Por que importa |
|---|---|---|
bundle.name |
O nome do projeto para a plataforma | Entra no caminho de deploy e no prefixo dos recursos. É o que você procura no workspace para achar tudo que veio deste repositório |
bundle.uuid |
Identificador único, gerado uma vez | É como a plataforma sabe que este deploy é o mesmo projeto de ontem, e não um novo. Nunca edite nem duplique entre repositórios — dois bundles com o mesmo uuid brigam pelo mesmo estado |
include |
Quais arquivos de recurso entram | É o que permite um arquivo por recurso em resources/, em vez de um YAML gigante. Job, pipeline e dashboard viram arquivos separados e revisáveis |
artifacts |
O que precisa ser construído antes do deploy | Aqui, empacotar src/ como wheel com uv build. É o que faz seu código virar biblioteca instalável no cluster, em vez de arquivo solto copiado |
variables |
Declaração dos valores que mudam por ambiente | Declarar aqui e atribuir no target é o que tira o nome do catálogo de dentro do SQL. Sem isso, isolar dev de prod vira busca-e-substitui |
targets |
Os ambientes — aqui dev e prod |
Mesmo código, dois destinos. -t dev ou -t prod em cada comando escolhe qual |
default: true |
O target usado quando você não passa -t |
Está em dev de propósito: esquecer a flag manda para desenvolvimento, nunca para produção |
workspace.host |
Para qual workspace vai o deploy | Veio preenchido do perfil que você autenticou no bloco 2.0 |
root_path |
Onde o projeto aterrissa no workspace | Fixado em produção para garantir uma cópia só. Sem isso, cada pessoa que fizesse deploy criaria a sua |
permissions |
Quem administra os recursos em produção | Permissão como código: entra na revisão do pull request, igual ao resto |
O que mode muda sozinho
Uma palavra, e a plataforma trata o deploy inteiro de forma diferente. É o mecanismo que impede o erro mais caro de engenharia de dados: rodar em produção achando que era teste.
| Comportamento | mode: development | mode: production |
|---|---|---|
| Nome dos recursos | prefixados com [dev seu_usuario] |
nome limpo, sem prefixo |
| Agendamentos e triggers | pausados | ativos |
| Concorrência de jobs | execuções antigas são canceladas | respeita a configuração do job |
| Quem é o dono | você | o principal de serviço da esteira |
| Isolamento | cada pessoa tem a própria cópia | uma cópia só, no root_path |
Repare que dev e prod têm o mesmo catalog: workspace
no arquivo recém-gerado. Isso a gente troca no bloco 7.0, quando os
catálogos existirem: dev aponta para um, produção para outro.
E é o catálogo que isola, nunca o nome do schema. Os schemas mantêm os nomes
exatos raw, bronze, silver e gold nos
dois ambientes — é o que permite o mesmo SQL rodar em qualquer target sem uma linha
diferente. Se o schema de dev virasse bronze_dev, cada query precisaria saber
em que ambiente está rodando, e você teria dois códigos para manter.
mode: development
O prefixo [dev seu_usuario] vale para jobs, pipelines e
dashboards — e também para schemas, que é onde ele atrapalha:
bronze viraria seunome_bronze. A trava para isso é
experimental.skip_name_prefix_for_schema: true, e ela aparece no bloco 7.0.
Guarde o sintoma: se os schemas nascerem com seu nome na frente, é isso.
O ciclo, do init ao deploy
Três comandos, sempre na mesma ordem. Você vai repetir isso a cada mudança do projeto — e a esteira do bloco 10.0 vai repetir por você quando o destino for produção.
# 1. confere a sintaxe e resolve as variaveis, sem tocar no workspace
databricks bundle validate -t dev --profile grid_intelligence
# 2. constroi o wheel, sobe os arquivos e cria os recursos
databricks bundle deploy -t dev --profile grid_intelligence
# 3. executa um recurso especifico, pela CHAVE dele no YAML
# (agora so existe o sample_job que veio do template)
databricks bundle run sample_job -t dev --profile grid_intelligence
# ver o que existe la, sem abrir a interface
databricks bundle summary -t dev --profile grid_intelligence
validate não é opcional
Ele resolve as variáveis e mostra o YAML final, já com ${var.catalog}
substituído. É onde você descobre que digitou o nome errado — antes de criar recurso no
workspace, e não depois.
O que cada pasta faz
| Caminho | Papel |
|---|---|
databricks.yml | o coração: nome do bundle, variáveis, targets dev e prod |
resources/ | jobs, pipelines, schemas, dashboards — um .yml por recurso |
src/ | o código de verdade: SQL das transformações, notebooks, biblioteca |
tests/ | testes unitários — rodam no CI antes de qualquer deploy |
fixtures/ | CSV e JSON de apoio para os testes |
flowchart TB YML["databricks.yml<br/><small>uma variavel var.catalog</small>"] YML --> DEV["target dev<br/><small>catalog: grid_dev</small>"] YML --> PROD["target prod<br/><small>catalog: grid_intelligence</small>"] DEV --> DS["grid_dev.raw<br/>grid_dev.bronze<br/>grid_dev.silver<br/>grid_dev.gold"] PROD --> PS["grid_intelligence.raw<br/>grid_intelligence.bronze<br/>grid_intelligence.silver<br/>grid_intelligence.gold"] DS --> N1["<b>mesmo SQL, sem alteracao</b><br/><small>o nome do catalogo nunca aparece no codigo</small>"] PS --> N1
O DAB traz engenharia de software para dentro de engenharia de dados: ambiente isolado,
revisão por pull request e recurso provisionado por código — por baixo é
Terraform. É a forma de desenvolvimento recomendada pela própria Databricks, e o
host no databricks.yml já veio preenchido do perfil que você
autenticou no bloco 2.0.
Python, venv e primeiro commit.
Ambiente reproduzível e o projeto salvo no GitHub.
uv python pin 3.12
uv venv --seed
uv sync --dev
Parte das bibliotecas Python do Databricks — databricks-connect e
databricks-dlt — ainda não publica wheel para 3.13. O próprio
pyproject.toml do template declara
requires-python = ">=3.10,<3.13". O --seed instala o
pip dentro do venv, o que evita dor de cabeça com ferramenta que espera
encontrá-lo.
git add .
git commit -m "Adiciona projeto inicial Declarative Automation Bundle"
git push
Skills oficiais.
O agente passa a saber Databricks — documentação oficial dentro do contexto.
As skills saíram do repositório do AI Dev Kit e viraram parte do repositório oficial
databricks/databricks-agent-skills, instalável pela própria CLI do
Databricks. O servidor MCP deixou de fazer parte do setup padrão.
Se você já usou a versão antiga, o instalador limpa as skills anteriores e a configuração do MCP antes de instalar as novas. Deixe ele fazer isso — é o caminho suportado. O MCP continua existindo e mantido, mas agora é instalação separada e opcional; skills e MCP não têm mais relação entre si.
databricks aitools install
| Pergunta | Resposta |
|---|---|
| Quais agentes | Claude Code (e Codex, se você usa) |
| Profile | grid_intelligence |
| Escopo | global — vale para todo projeto na máquina |
| Grupos de skills | todas — é o que instala como plugin |
| Limpar instalação antiga | sim (o padrão) |
O instalador ainda traz as skills de MLflow, que vêm de outro repositório e não estão na CLI. Ao final, ele oferece confirmar a autenticação — aceite.
flowchart TB REPO["<b>databricks-agent-skills</b><br/><small>repositorio oficial, com a documentacao vigente</small>"] INST["<code>databricks aitools install</code>"] SK["skills instaladas como plugin<br/><small>prefixo databricks: em /skills</small>"] AG["<b>Claude Code</b>"] ESCREVE["<b>sabe COMO escrever</b><br/><small>job, pipeline, dashboard, metric view</small>"] EXECUTA["<b>EXECUTA pela CLI</b><br/><small>listar tabela, rodar SQL, inspecionar job</small>"] WS["seu workspace"] REPO --> INST --> SK --> AG AG --> ESCREVE AG --> EXECUTA --> WS
Skill é a documentação oficial e atualizada entrando no contexto do agente. Ele passa a saber como se escreve um job, uma pipeline, um dashboard, uma metric view no padrão da Databricks — sem inventar sintaxe que não existe mais.
Para executar — listar tabela, rodar SQL, inspecionar job — o caminho agora é a própria CLI, que o agente já sabe usar porque as skills ensinam. Foi isso que tornou o MCP dispensável no setup padrão: menos uma peça para instalar, autenticar e manter viva.
As AI Tools: SQL do terminal, sem abrir a interface
Junto das skills vem um conjunto de comandos que muda o dia a dia:
databricks experimental aitools tools. São quatro subcomandos, feitos para
agentes de código — e igualmente úteis para você.
| Subcomando | O que faz | Quando usar |
|---|---|---|
query |
Executa SQL contra um warehouse e devolve o resultado | Rodar qualquer consulta sem sair do terminal. Aceita SQL solto, arquivo .sql ou stdin |
discover-schema |
Colunas, tipos, amostra de linhas, contagem de nulos e total de registros | Conhecer uma tabela antes de escrever a query. Uma chamada substitui quatro |
get-default-warehouse |
Descobre qual warehouse usar | Quase nunca — os outros comandos já acham sozinhos |
statement |
SQL assíncrono: submit, get, status, cancel |
Consulta demorada, ou várias em paralelo sem travar o terminal |
# consulta direta — ele acha o warehouse sozinho
databricks experimental aitools tools query --profile grid_intelligence \
"SELECT current_catalog(), current_user()"
# conhecer uma tabela: colunas, tipos, amostra, nulos e contagem
databricks experimental aitools tools discover-schema \
grid_dev.bronze.chamados --profile grid_intelligence
# rodar um arquivo .sql versionado no repositorio
databricks experimental aitools tools query --file src/sql/checagens.sql \
--profile grid_intelligence
# varias consultas EM PARALELO, saida em JSON
databricks experimental aitools tools query --profile grid_intelligence \
"SELECT count(*) FROM grid_dev.bronze.interrupcoes" \
"SELECT count(*) FROM grid_dev.silver.interrupcoes_validas"
# exportar para CSV
databricks experimental aitools tools query --output csv --profile grid_intelligence \
"SELECT * FROM grid_dev.gold.painel_operacional_dia" > painel.csv
É o que substituiu o servidor MCP no setup padrão. O agente não precisa de uma ponte especial para enxergar o workspace: ele usa a mesma CLI que você, com o mesmo perfil e as mesmas permissões. Menos uma peça para instalar, autenticar e manter viva — e tudo que ele faz aparece no seu histórico de comandos.
Foi também o que resolveu a criação do catálogo no bloco 7.0: onde
databricks catalogs create esbarra no Default Storage da Free Edition, um
CREATE CATALOG por aqui passa.
experimental e a palavra está no comando
A própria CLI avisa: “the tools can change at any time. There are no stability
guarantees”. Ótimo para trabalho interativo e para o agente; não
coloque em automação. Para isso, o caminho estável é a Statement
Execution API ou uma sql_task do job — que é o que este projeto usa para a
metric view e para a máscara de dado pessoal.
Teste de fumaça
Abra o Claude Code e rode /skills. As skills aparecem com o prefixo
databricks: — é assim que se reconhece o plugin instalado pela CLI. Depois,
peça algo que exija ler o workspace de verdade:
Liste os catálogos do meu workspace usando o profile grid_intelligence
Plugin também só carrega no início da sessão. Feche o Claude Code, abra
de novo e rode /skills. Se você tinha a versão antiga do AI Dev Kit e o
/mcp ainda mostra um servidor databricks, é resíduo da
instalação anterior — rode o instalador de novo e aceite a limpeza.
Unity Catalog.
Entender a hierarquia antes de o agente criá-la por você.
Este bloco é só conceito. Os comandos de criar catálogo, schema e volume estão no prompt 00, no bloco 8.1 — e quem vai executá-los é o agente. Antes disso, vale entender o que ele vai fazer, porque é aqui que quase todo erro da aula nasce.
Três níveis, nesta ordem
| Nível | O que guarda | Quem cria neste projeto |
|---|---|---|
Catálogogrid_dev |
o limite de isolamento entre ambientes | o prompt 00, pela CLI. É a única coisa que o bundle não declara — catálogo é recurso de conta |
Schemaraw, bronze, silver, gold |
a camada do medalhão | o bundle, via resources/grid_schemas.yml |
Volumeraw.landing |
arquivo bruto — o Parquet que a bronze ingere | o bundle, no mesmo arquivo |
Volume mora dentro de schema, que mora dentro de catálogo. Tentar subir arquivo com qualquer um faltando devolve erro de caminho — sem dizer em que nível o problema está. A ordem de conferência é sempre a mesma:
databricks catalogs list --profile grid_intelligence
databricks schemas list grid_dev --profile grid_intelligence
databricks volumes list grid_dev raw --profile grid_intelligence
O catálogo isola; o nome do schema, não
Dev grava em grid_dev, produção em grid_intelligence. Os schemas
mantêm os nomes exatos raw, bronze, silver e
gold nos dois lados — é isso que permite o mesmo SQL rodar em qualquer target
sem uma linha diferente. Se o schema de dev virasse bronze_dev, cada query
precisaria saber em que ambiente está rodando, e você teria dois códigos para manter.
Em mode: development o bundle prefixa todo recurso com o seu nome —
inclusive schema, onde bronze viraria seunome_bronze. A trava é
experimental.skip_name_prefix_for_schema: true no
databricks.yml, e o prompt 00 já pede isso. Guarde o sintoma: se os schemas
nascerem com seu nome na frente, é essa linha que falta.
Catálogo recém-criado sem grant é catálogo inútil: o pipeline falha com
user does not have permission to CREATE SCHEMA. O prompt 00 concede a
permissão, mas confira na interface — Catalog →
grid_dev → aba Permissions. Se esse erro aparecer
lá na frente, é sempre aqui que se resolve.
Na Free Edition, criar catálogo pela CLI não funciona
databricks catalogs create devolve
Metastore storage root URL does not exist em qualquer conta com
Default Storage ligado: a API exige um storage_root explícito, e
quem sabe usar o armazenamento padrão é a interface. Por isso o prompt 00 usa
CREATE CATALOG por SQL — que passa.
Se até o SQL reclamar, o caminho é a interface: Catalog no menu lateral → Create catalog → nome, tipo Standard, storage no padrão. Três cliques por catálogo, e o prompt 00 segue do ponto seguinte.
A sequência de oito prompts.
Uma feature por prompt. Deploy e resultado visível a cada etapa.
A tentação é escrever um prompt gigante pedindo o projeto inteiro. O resultado é sempre o mesmo: tudo pela metade, e você sem saber qual parte revisar primeiro.
A sequência abaixo quebra a área de atendimento da Luz do Vale em oito etapas. Cada prompt entrega uma feature completa, termina com um deploy e abre algo para olhar — e diz explicitamente o que não deve ser feito ainda. É isso que impede o agente de correr na frente.
flowchart TB P0["<b>00 · Setup</b> — regras de negocio, catalogos, schemas e volume"] P1["<b>01 · Bronze</b> — ingestao fiel do volume"] P2["<b>02 · Silver, os numeros</b> — a regra dos 3 minutos, dedup e baseline"] P3["<b>03 · Silver, o texto</b> — anonimizar e depois enriquecer com IA"] P4["<b>04 · Gold</b> — as 4 entregas e a metric view de DEC e FEC"] P5["<b>05 · Dashboard</b> — painel AI/BI de duas paginas"] P6["<b>06 · Agente</b> — o copiloto conversacional em portugues"] P7["<b>07 · Prova</b> — relatorio executivo e as 8 queries"] P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
Antes de tudo: o que é esse ciclo
Os três comandos abaixo vão se repetir oito vezes. Se os conceitos de workspace e deploy são novos para você, vale gastar dois minutos aqui — o resto da aula fica muito mais fácil.
Onde as coisas estão. O código que você escreve fica na sua
máquina. O Databricks é um ambiente remoto: o workspace na
nuvem. Enquanto você só edita arquivos, nada existe lá — nenhum job, nenhuma tabela.
O deploy é a ponte entre os dois mundos.
| Comando | O que faz, em uma frase | Toca no workspace? |
|---|---|---|
bundle validate |
Lê os arquivos .yml, troca as variáveis pelos valores do target e confere se tudo faz sentido. É a revisão antes de enviar |
Não. Roda inteiro na sua máquina |
bundle deploy |
Empacota o código, envia para o workspace e cria ou atualiza os recursos declarados | Sim. É o único que muda alguma coisa lá |
bundle summary |
Pergunta ao workspace o que existe agora, com as chaves e os links | Só lê |
O deploy copia o projeto para /Workspace/Users/você/.bundle/grid_intelligence/dev/
no workspace. É por isso que os recursos de dev aparecem prefixados com
[dev seu_usuario]: cada pessoa tem a própria cópia, e ninguém pisa no
trabalho de ninguém. Em produção o caminho é fixo e a cópia é uma só.
Ele não executa nada — criar um job não é rodar o job; para isso existe
o bundle run. E ele não sobe dados: cria o volume vazio, e
os arquivos são passo à parte. Confundir essas duas coisas é a origem da maioria das
perguntas "deployou mas não apareceu nada".
validate reclamar, não tente o deploy
Ele é rápido de propósito e mostra o YAML final, já com ${var.catalog}
substituído pelo catálogo de verdade. É onde você descobre que digitou um nome errado —
antes de criar recurso no workspace, e não depois.
flowchart TB
subgraph LOCAL["<b>sua maquina</b> — onde voce escreve"]
ARQ["databricks.yml<br/>resources/*.yml<br/>src/**"]
VAL["<b>bundle validate</b><br/><small>le os YAML, resolve as variaveis,<br/>confere a sintaxe. NAO envia nada</small>"]
end
subgraph NUVEM["<b>workspace Databricks</b> — onde tudo roda"]
DEP["<b>bundle deploy</b><br/><small>empacota, envia e cria os recursos</small>"]
REC["jobs · pipelines · dashboards<br/>schemas · volumes"]
SUM["<b>bundle summary</b><br/><small>pergunta o que existe la agora</small>"]
end
ARQ --> VAL
VAL -->|"passou"| DEP
VAL -->|"erro de sintaxe ou variavel"| ARQ
DEP --> REC
REC --> SUM
SUM -->|"proxima mudanca"| ARQ
deploy atravessa — validate fica de um lado e summary só olha do outro.
Porque o ciclo validate → deploy → olhar é o hábito que a aula
quer instalar, e hábito não se aprende vendo uma vez. Oito repetições em uma hora fixam
mais do que qualquer explicação — e cada deploy pequeno falha de um jeito pequeno, que se
resolve em minutos.
A ordem é fixa, mas a sequência tem duas decisões — e as duas são sobre usar função de IA ou não.
No prompt 03, o enriquecimento das transcrições. É o único ponto em que
a IA entra no pipeline de dados, e são ~11 mil chamadas ao modelo: dezenas de minutos.
O prompt irmão 03-silver-texto-regex.md entrega as mesmas
colunas com regex e CASE, em segundos. Do prompt 04 em diante nada
muda — gold, dashboard, agente e relatório não sabem qual rota você escolheu.
No prompt 07, a prosa do relatório executivo: ai_gen ou um
gabarito. Os números são idênticos nos dois caminhos; muda o texto, não
o fato.
A regra de negócio junto do pedido — o agente não precisa adivinhar por que o limiar é de 3 minutos. As armadilhas conhecidas — baseline sobre série com tendência, streaming table append-only. E o critério de aceite: o que precisa ser verdade para a etapa estar pronta.
A sequência é o template: setup, ingestão, limpeza, enriquecimento, negócio, visualização, agente, prova. Trocar de domínio significa reescrever o prompt 00 inteiro e ajustar nomes de tabela nos demais. A estrutura, as armadilhas técnicas e o formato com critério de aceite continuam valendo.
Prompt 00Setup e planejamento.
As regras de negócio antes da primeira linha de SQL.
Nenhuma tabela nasce antes de alguém decidir por que ela existe. Este prompt escreve as regras que valem dinheiro — os 3 minutos que separam piscada de apagão, os dois eixos das perdas, a proibição de rotular cliente — antes de qualquer SQL. É o documento que o agente vai consultar nos sete prompts seguintes, e é o que impede ele de inventar regra.
O que explicar aqui
-
Quem cria o quê
O catálogo é o único que o agente cria pela CLI — é recurso de conta, e o bundle não o declara. Schemas e volume vão no
Unity Catalogresources/grid_schemas.ymle nascem nodeploy. Confundir isso é o que faz o deploy falhar dizendo que o catálogo não existe. -
CREATE CATALOGpor SQL, não pela CLIEm conta com Default Storage — o caso da Free Edition —
Criar catálogosdatabricks catalogs createexige umstorage_roote falha. OCREATE CATALOGpor SQL usa o armazenamento padrão e passa. Se nem ele funcionar, é pela interface. -
Por que PRD antes de código
O agente escreve SQL bem. O que ele não sabe é por que interrupção de menos de 3 minutos não conta, ou por que a saída não pode se chamar fraude. Escrever isso primeiro, num arquivo que ele lê, é a diferença entre um pipeline correto e um pipeline plausível.
-
Catálogo o bundle não cria
Schemas, volumes, jobs e pipelines vão no bundle. Catálogo é recurso de conta e fica de fora — é a única coisa que você cria à mão, e por isso é a primeira.
Volumes
O prompt
# Prompt 00 — Setup e planejamento
> **Como usar:** cole este texto inteiro no Claude Code, dentro do repositório do projeto.
> Ele é o primeiro de uma sequência de **8 prompts**. Cada um entrega uma feature completa,
> com deploy e resultado visível. Não pule a ordem: cada prompt assume o anterior pronto.
---
Você vai me ajudar a construir a plataforma de dados e IA de uma distribuidora de energia
elétrica fictícia, a **Luz do Vale Distribuidora S.A.**, no Databricks. O projeto se chama
**Grid Intelligence**.
Este é o **prompt 1 de 8**. Neste primeiro, quero apenas o alicerce: entender o domínio,
registrar as regras de negócio e criar a infraestrutura. Não construa transformação,
dashboard nem agente agora — cada um tem o seu prompt.
## Contexto de negócio
Uma distribuidora de energia opera sob regulação da ANEEL e tem três dores simultâneas,
todas mensuráveis em dinheiro:
1. **Continuidade do fornecimento.** Interromper além do limite regulatório gera
compensação financeira automática ao consumidor. Em 2024 as distribuidoras brasileiras
pagaram mais de R$ 1,1 bilhão em compensações.
2. **Perdas não técnicas.** Energia distribuída e não faturada — furto, fraude e erro de
medição. Em 2025 somaram ~R$ 11,5 bilhões, dos quais ~R$ 7,9 bilhões foram repassados
à tarifa.
3. **Atendimento ao cliente.** Milhares de ligações viram transcrição e ninguém lê. Dentro
delas está o aviso antecipado da reclamação na ouvidoria.
## Glossário — use estes termos, sem sinônimo aproximado
| Termo | Definição |
|---|---|
| **UC — Unidade Consumidora** | O ponto de entrega de energia. **Não é sinônimo de cliente**: um cliente pode ter várias UCs |
| **Conjunto** | Subdivisão geográfica da área de concessão. É a unidade de agregação do regulador |
| **DEC** | Duração Equivalente de Interrupção por UC — tempo médio, em horas, sem fornecimento |
| **FEC** | Frequência Equivalente de Interrupção por UC — número médio de interrupções |
| **PNT** | Perdas Não Técnicas |
| **Prioridade de inspeção** | Classificação de quanto uma UC merece visita técnica |
## Regras de negócio que valem para o projeto inteiro
- **RN-01** — Só interrupções com **3 minutos ou mais** entram na apuração de DEC e FEC.
A regra vive na camada de transformação, nunca no relatório.
- **RN-04** — DEC e FEC têm **uma única definição** no sistema, consumida por dashboard,
agente e relatório.
- **RN-06** — Uma UC é candidata a inspeção quando o consumo cai em **dois eixos
simultâneos**: contra o próprio baseline **e** enquanto a vizinhança permanece estável.
Só o primeiro eixo apontaria o bairro inteiro depois de um apagão.
- **RN-07** — A saída da detecção é **prioridade de inspeção** — alta, média, baixa.
**Nunca** um rótulo de fraude, furto ou culpa. Queda de consumo também é mudança de
morador, imóvel desocupado, defeito de medidor e erro de leitura. Em setor regulado a
diferença é jurídica. Nenhuma coluna, variável ou comentário do projeto pode usar as
palavras "fraude", "furto" ou "culpado" como rótulo de saída.
- **RN-09** — Dado pessoal em transcrição é **mascarado antes** de qualquer análise de
conteúdo.
- **RN-10** — O texto original identificado é preservado com **acesso restrito**. Duas
proteções que coexistem: uma na leitura, outra na análise.
- **RN-11** — A camada de ingestão **não filtra, não corrige e não aplica regra de
negócio**. Leitura inválida de medidor é um fato sobre o medidor. Filtrar na ingestão
parece limpeza; é destruição de evidência.
- **RN-12** — Todo registro ingerido carrega origem e momento de ingestão.
- **RN-13** — O agente conversacional acessa **apenas a camada de negócio**.
- **RN-14** — As entidades da **camada gold** carregam descrição em linguagem de negócio —
é o que o agente lê. Bronze e silver não precisam: são passagem.
## Diretriz de simplicidade — vale para todos os 8 prompts
Este projeto é construído **ao vivo, em aula**. Código que ninguém consegue ler na tela em
dez segundos não serve, mesmo que seja mais robusto.
**Escreva o mínimo que faz a coisa funcionar:**
- **Sem expectations e sem `CONSTRAINT`** nas tabelas. Regra de negócio vira `WHERE`, que
qualquer pessoa lê sem precisar saber o que é `ON VIOLATION`.
- **Sem `COMMENT` em bronze e silver.** Elas são passagem. Só a **gold** e a **metric view**
levam descrição — são o que o agente conversacional lê, e ali a curadoria é o produto
(RN-14).
- **Sem `TBLPROPERTIES`, `CLUSTER BY` e afins**, a menos que sem eles quebre.
- **Sem tratamento de caso que o dataset não tem.** Não invente defesa para dado que não
existe.
- **Um `SELECT` direto vence um `WITH` de quatro CTEs.** Só quebre em CTE quando a query
realmente não couber de cabeça.
Comentário no código só onde a decisão **não é óbvia pelo código** — a regra dos 3 minutos
merece duas linhas explicando por quê; um `CAST` não merece nenhuma.
Se em algum momento você achar que o certo é acrescentar validação, **não acrescente**: me
diga em uma linha o que faria, e eu decido.
## Convenções obrigatórias
- Todo texto e todo nome em **português do Brasil**; nomes de entidades, colunas e
arquivos **sem acentuação**.
- **Nenhum nome de catálogo fixado em código.** Catálogo vem de `${var.catalog}` no
`databricks.yml`; schema vem de `${resources.schemas.<camada>.name}`.
- **Nenhum nome de modelo de IA fixado no código.** Use as funções `ai_*` do Databricks.
- Perfil da CLI: `grid_intelligence`. Todo comando leva `--profile grid_intelligence`.
## Os dados
Quatro bases sintéticas, já geradas, em Parquet. **Baixe aqui:**
<https://drive.google.com/file/d/1o3NISve_wWygDWecBiiYTB8i2WXN7hbi/view?usp=sharing>
Descompacte e você terá uma pasta por base, com um único arquivo Parquet dentro.
| Base | Grão | Volume |
|---|---|---|
| `unidades_consumidoras` | uma por ponto de entrega | 4.000 |
| `interrupcoes` | uma por evento de rede | 1.383 |
| `consumo_diario` | uma por UC por dia | **~5,85 milhões** (4 anos) |
| `chamados` | uma por ligação, com transcrição | 2.801 |
O dataset tem **quatro anos de histórico**, crescimento de **30% ao ano** na carga e
**sazonalidade de verão** no atendimento (dezembro a março recebem quase o dobro de
ligações). Ele carrega **defeitos propositais** — consumo nulo, negativo, fisicamente
impossível, linhas duplicadas e interrupções abaixo de 3 minutos. Não são bug: existem
para as validações de qualidade e a RN-01 terem o que fazer.
## O que fazer agora
1. **Leia a skill `databricks-core`** antes de qualquer comando, e depois a
`databricks-dabs`.
2. Escreva **`.llm/prd.md`** com o contexto acima: domínio, glossário, regras de negócio,
modelo de informação e as quatro entregas de negócio previstas. É o documento que todos
os prompts seguintes vão consultar.
3. **Crie os catálogos** (ver abaixo).
4. Crie no bundle os **schemas** `raw`, `bronze`, `silver` e `gold`, e o **volume**
`raw.landing`. Um schema por camada — o nome do schema já é a camada, então as tabelas
dentro dele não repetem o prefixo (`bronze.chamados`, nunca `bronze_chamados`).
5. Configure `dev` e `prod` isolados **por catálogo**, não por nome de schema.
6. Rode `databricks bundle validate` e `databricks bundle deploy -t dev`.
7. Me diga exatamente **onde subir os arquivos de dados**, com o caminho completo.
## Os catálogos
Dois catálogos, um por ambiente. É o catálogo que isola dev de produção — os schemas mantêm os
nomes exatos `raw`, `bronze`, `silver` e `gold` nos dois lados, para que o mesmo SQL rode em
qualquer ambiente sem uma linha diferente.
| Ambiente | Catálogo | Quem usa |
|---|---|---|
| `dev` | `grid_dev` | você, no dia a dia |
| `prod` | `grid_intelligence` | o ambiente final, depois de validado em dev |
**Catálogo é a única coisa que o bundle não cria** — é recurso de conta, e o deploy assume que já
existe. Schemas e volume ficam no bundle; catálogo, não.
Crie os dois **por SQL**, e não com `databricks catalogs create`:
```bash
databricks experimental aitools tools query --profile grid_intelligence \
"CREATE CATALOG IF NOT EXISTS grid_dev COMMENT 'Catalogo de desenvolvimento da Grid Intelligence'"
databricks experimental aitools tools query --profile grid_intelligence \
"CREATE CATALOG IF NOT EXISTS grid_intelligence COMMENT 'Catalogo de producao da Grid Intelligence'"
databricks catalogs list --profile grid_intelligence
```
**Por que SQL e não `catalogs create`:** em qualquer conta com Default Storage ligado — o caso da
Free Edition — a API por trás do comando exige um `storage_root` explícito e devolve
`Metastore storage root URL does not exist`. O `CREATE CATALOG` por SQL usa o armazenamento padrão
da conta e passa. Se até ele reclamar, me avise: aí o caminho é a interface, em Catalog → Create
catalog.
Depois, conceda permissão ao meu usuário nos dois catálogos:
```bash
databricks grants update CATALOG grid_dev \
--json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
--profile grid_intelligence
```
Catálogo sem *grant* é catálogo inútil: o pipeline falha depois com
`user does not have permission to CREATE SCHEMA`.
### Recomeçar do zero
Se um catálogo já existir e eu quiser recomeçar limpo, o comando é destrutivo e **não pergunta
duas vezes** — `CASCADE` remove schemas, tabelas, volumes e os arquivos dentro deles:
```bash
databricks experimental aitools tools query --profile grid_intelligence \
"DROP CATALOG IF EXISTS grid_dev CASCADE"
```
**Sempre confirme comigo antes de rodar isso.** Deixe o comando registrado no README, na seção de
setup, com o aviso de que é destrutivo.
## Os schemas e o volume, no bundle
Declare em `resources/grid_schemas.yml`:
- os schemas `raw`, `bronze`, `silver` e `gold`
- o volume **managed** `landing`, dentro de `raw`
Nomes de schema vêm de `${resources.schemas.<camada>.name}` onde forem referenciados; o catálogo
vem de `${var.catalog}`. **Nenhum dos dois escrito literalmente em qualquer arquivo.**
Adicione `experimental.skip_name_prefix_for_schema: true` ao `databricks.yml`. Sem isso, em
`mode: development` o bundle prefixa os schemas com meu nome de usuário e `bronze` vira
`meunome_bronze` — o isolamento entre ambientes é feito pelo catálogo, não pelo nome do schema.
Depois: `databricks bundle validate` e `databricks bundle deploy -t dev`.
## Onde vou subir os dados
Com o volume criado pelo deploy, me diga o caminho completo de cada base, no formato que eu possa
colar direto na interface de upload:
```
/Volumes/grid_dev/raw/landing/unidades_consumidoras/
/Volumes/grid_dev/raw/landing/interrupcoes/
/Volumes/grid_dev/raw/landing/consumo_diario/
/Volumes/grid_dev/raw/landing/chamados/
```
E me dê também o comando de subir tudo de uma vez pelo terminal, lembrando que **o caminho de
origem é o da minha máquina** — confira onde os Parquet estão antes de montar o comando:
```bash
databricks fs cp -r <PASTA_LOCAL> dbfs:/Volumes/grid_dev/raw/landing \
--overwrite --profile grid_intelligence
```
O prefixo `dbfs:` é obrigatório mesmo para Volume do Unity Catalog.
Uma pasta por base, com um único arquivo Parquet dentro — inclusive a de consumo diário, com 5,85
milhões de linhas. É a pasta, e não o arquivo, que é o contrato com a camada bronze: se um dia uma
base precisar ser particionada, o pipeline não muda.
## Critério de aceite
- Os dois catálogos existem e eu tenho `ALL PRIVILEGES` nos dois
- O comando de derrubar o catálogo está documentado no README, com o aviso de que é destrutivo
- `databricks bundle validate --profile grid_intelligence` passa sem erro
- `databricks bundle deploy -t dev` cria os quatro schemas e o volume `raw.landing`
- Os schemas **não** têm meu nome de usuário como prefixo
- Nenhum nome de catálogo ou schema aparece escrito literalmente no código
- Recebi os quatro caminhos de upload e o comando de `fs cp`
- `.llm/prd.md` existe e contém o glossário e as 14 regras de negócio
## O que vem depois
Não faça nada disso agora — cada item é um prompt próprio:
| # | Prompt | Entrega |
|---|---|---|
| 01 | Camada bronze | Ingestão fiel do volume, sem regra de negócio |
| 02 | Silver — os números | Cadastro limpo, a regra dos 3 minutos, dedup e baseline |
| 03 | Silver — o texto | Anonimização e enriquecimento das transcrições — com IA ou por regex |
| 04 | Camada gold | As quatro entregas de negócio e a metric view de DEC/FEC |
| 05 | Dashboard | Painel AI/BI sobre a gold |
| 06 | Agente | Copiloto conversacional em português |
| 07 | Relatório e demonstração | Relatório executivo e as queries que provam tudo |
Quando terminar o setup, me diga o que criou e pare. Eu envio o próximo.
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
Os dois catálogos na lista, e o volume raw.landing criado pelo deploy — vazio, esperando os arquivos.
Onde olhar na interface
-
Catalog →
grid_devOs quatro schemas —
raw,bronze,silver,gold— recém-criados e vazios. Se algum vier comoseunome_bronze, falta oskip_name_prefix_for_schema. -
Catalog →
grid_dev→raw→ Volumes →landingO volume existe e está vazio. É aqui que você vai soltar os quatro Parquet.
-
Catalog →
grid_dev→ PermissionsSeu usuário com
ALL PRIVILEGES. Sem isso, o próximo deploy falha compermission to CREATE SCHEMA. -
Workspace → Users → você →
.bundle/grid_intelligence/dev/O projeto inteiro copiado para o workspace. É o que o
deployfez.
E os dados? Esses são por sua conta
O bundle deploy cria o volume vazio. Bundle provisiona
recurso — schema, volume, job, pipeline —, mas não move dado. Subir os
quatro Parquet é passo manual, feito uma vez, depois que o volume existe.
Dataset da Grid Intelligence — Google Drive
São as quatro bases já geradas: 4.000 unidades consumidoras, 1.383 interrupções, 5,85 milhões de linhas de consumo e 2.801 chamados — quatro anos de histórico. Descompacte e você terá uma pasta por base, com um Parquet dentro.
Prefere gerar você mesmo? uv run python scripts/gerar_dados_local.py historico
reproduz exatamente o mesmo dataset: a semente é fixa.
| Peça | Quem cria | Quando |
|---|---|---|
| Catálogo | o agente, por CREATE CATALOG | no início deste prompt |
| Schemas e volume | o bundle deploy, a partir de resources/grid_schemas.yml | ao fim deste prompt |
| Os arquivos Parquet | você, por upload ou fs cp | agora, antes do bloco 8.2 |
Pelo terminal
# confira ONDE estao os seus Parquet antes de rodar
ls landing/
# o -r sobe a arvore inteira, com as quatro pastas
databricks fs cp -r landing dbfs:/Volumes/grid_dev/raw/landing \
--overwrite --profile grid_intelligence
databricks fs ls dbfs:/Volumes/grid_dev/raw/landing --profile grid_intelligence
Pela interface
-
Catalog →
grid_dev→raw→ Volumes →landing - Botão Upload to this volume O campo de destino aceita criar diretório: digite o nome da pasta antes de soltar o arquivo.
-
Uma pasta por base, com o Parquet dentro. Repita para as quatro.
unidades_consumidoras/·interrupcoes/·consumo_diario/·chamados/
fs cp
O dbfs: é obrigatório, mesmo para Volume do Unity Catalog —
sem ele o comando falha com erro de caminho inválido. E o caminho de origem é o
da sua máquina: caminho errado devolve um erro que parece de permissão. Dê
ls antes.
São o que o pipeline do bloco 8.2 vai procurar. Uma letra diferente e a bronze ingere zero linha — sem reclamar de nada, porque para o Auto Loader a pasta simplesmente está vazia.
Prompt 01Camada bronze.
Ingestão fiel: nada é filtrado, nada é corrigido.
Em setor regulado, número sem origem não vale. Quando a fiscalização perguntar de onde saiu o DEC do mês, a resposta precisa chegar até o arquivo e o instante da ingestão — e o dado bruto precisa estar lá, intocado, inclusive a leitura defeituosa que alguém teria vontade de limpar.
O que explicar aqui
-
Auto Loader e
STREAM read_filesIngestão incremental de arquivos: ele guarda o que já processou e só lê o que chegou depois. Aponta para a pasta, não para o arquivo — e
Auto LoaderallowOverwritesfaz ele reprocessar um arquivo que foi trocado. -
Streaming table × materialized view
A decisão que mais custa caro errar, porque trocar depois exige derrubar a tabela. Regra curta: se a linha de saída depende de outras linhas, é materialized view; se cada entrada vira uma saída sozinha, é streaming table. Comparação completa logo abaixo.
Streaming tables -
SELECT *aqui é a regra escrita em códigoListar colunas à mão seria decidir o que vale a pena guardar — e essa decisão não é da bronze. Junto vão só duas colunas de rastreabilidade: de qual arquivo veio e quando entrou.
Auto Loader -
Simplicidade é uma decisão de projeto
O Lakeflow oferece expectations para declarar regras de qualidade na tabela. Este projeto não usa nenhuma: a mesma pergunta se responde com uma consulta que cabe em cinco linhas e roda em qualquer lugar. Detalhe logo abaixo.
Expectations -
RN-11 e o custo de limpar cedo
Filtrar na ingestão parece capricho e é destruição de evidência. A leitura negativa é um fato sobre o medidor: alguém vai querer contá-la um dia, e ela precisa estar lá.
Arquitetura medalhão
O prompt
# Prompt 01 — Camada bronze
> **Prompt 2 de 8.** Requer o prompt 00 concluído: schemas, volume e `.llm/prd.md` no lugar.
---
Leia `.llm/prd.md` antes de começar, inclusive a **diretriz de simplicidade**. Agora vamos
construir a **camada bronze** do medalhão da Grid Intelligence.
## O que quero
Um **Lakeflow Declarative Pipeline serverless** que ingere as quatro bases do volume
`raw.landing` para o schema `bronze`, uma tabela por base.
**Um arquivo só:** `src/pipelines/grid_intelligence/transformations/01_bronze.sql`, com as
quatro tabelas. Elas compartilham o mesmo padrão de ingestão, e separar em quatro arquivos
iguais só espalharia a mesma decisão por quatro lugares.
| Origem no volume | Tabela bronze | Linhas esperadas |
|---|---|---|
| `unidades_consumidoras/` | `bronze.unidades_consumidoras` | 4.000 |
| `interrupcoes/` | `bronze.interrupcoes` | 1.383 |
| `consumo_diario/` | `bronze.consumo_diario` | ~5,85 milhões |
| `chamados/` | `bronze.chamados` | 2.801 |
Todas são **streaming table**: ingestão é append de arquivo novo, e é o que o Auto Loader
sabe fazer de forma incremental.
Use **Auto Loader** (`STREAM read_files`) com formato Parquet. Aponte para a **pasta**, não
para o arquivo: hoje cada base ocupa um Parquet só, mas se uma delas precisar ser
particionada amanhã, nada muda no pipeline.
## O formato de cada tabela
Quatro blocos iguais a este, e nada além disso:
```sql
CREATE OR REFRESH STREAMING TABLE ${catalogo}.${schema_bronze}.interrupcoes
AS SELECT
*,
_metadata.file_path AS arquivo_origem,
current_timestamp() AS ingerido_bronze_em
FROM STREAM read_files(
'${volume_landing}/interrupcoes/',
format => 'parquet'
);
```
**Sem expectations, sem `CONSTRAINT`, sem `COMMENT`, sem `CLUSTER BY`.** A bronze é
passagem: o que ela precisa fazer é copiar fielmente e dizer de onde veio.
As duas colunas extras são a RN-12: `arquivo_origem` e `ingerido_bronze_em`. As colunas
`origem` e `ingerido_em` que já vêm do dado dizem de que *sistema* ele saiu; estas duas
dizem por onde ele *entrou* aqui.
## A regra que manda neste arquivo: RN-11
**A ingestão não aplica regra de negócio, não filtra e não corrige.**
Consumo negativo, leitura impossível, interrupção de 40 segundos, cadastro com bairro
vazio — tudo entra exatamente como chegou. Filtrar aqui pareceria limpeza; é destruição de
evidência. Quem descarta é a silver, com regra nomeada e visível no `WHERE`.
O `SELECT *` acima não é preguiça: é a RN-11 escrita em código. Qualquer coluna que você
listasse à mão seria uma decisão sobre o que vale a pena guardar — e essa decisão não é da
bronze.
## Parametrização
O pipeline recebe por `configuration`: catálogo, nome de cada schema, caminho do volume e
o limiar de duração mínima de interrupção. **Nenhum nome de catálogo escrito dentro do
SQL** — os arquivos usam `${catalogo}.${schema_bronze}.<tabela>`, e o valor vem do
`databricks.yml`.
## Uma armadilha que quero registrada em comentário
Cada base ocupa um único arquivo, que é sobrescrito quando o dataset é regerado.
`allowOverwrites => true` faz o Auto Loader reprocessar o arquivo trocado, mas **streaming
table é append-only**: as linhas antigas continuam lá e o volume dobra. Deixe isso escrito
no topo do arquivo, junto com a solução — rodar o pipeline com **full refresh** depois de
regerar.
## Se o pipeline falhar com `DELTA_FEATURE_REQUIRES_MANUAL_ENABLEMENT: timestampNtz`
Vai acontecer. Os timestamps do dataset são **sem fuso horário** (`TIMESTAMP_NTZ`), e o
Delta exige habilitar essa feature explicitamente na tabela.
Há duas saídas, e uma delas está errada:
- **Converter para `TIMESTAMP` na ingestão** — assumiria um fuso que o dado não declara.
Isso é transformação dentro da bronze, exatamente o que a RN-11 proíbe.
- **Habilitar a feature na tabela** — o tipo entra como chegou, e quem decide o fuso é a
silver, se precisar.
Use a segunda. É a única exceção à regra de não usar `TBLPROPERTIES`:
```sql
CREATE OR REFRESH STREAMING TABLE ${catalogo}.${schema_bronze}.interrupcoes
TBLPROPERTIES ('delta.feature.timestampNtz' = 'supported')
AS SELECT ...
```
## Se a bronze não ingerir nada
Antes de investigar o Auto Loader, confira a precisão dos timestamps do Parquet:
```bash
databricks experimental aitools tools query --profile grid_intelligence \
"SELECT * FROM parquet.`/Volumes/grid_dev/raw/landing/interrupcoes/` LIMIT 1"
```
Parquet gravado por pandas sem tratamento fica com `timestamp[ns]`, e **o Spark não lê
nanossegundo**. O gerador deste projeto já converte para microssegundo; se o dado veio de
outro lugar, é a primeira coisa a checar — a falha não é óbvia e parece problema de caminho.
## Ao terminar
1. `databricks bundle deploy -t dev --profile grid_intelligence`
2. Rode o pipeline e me mostre o resultado
3. A contagem de linhas de cada tabela bronze
4. **Quanto defeito entrou**, com uma query só — é o que a silver vai ter de resolver:
```sql
SELECT
count(*) AS eventos,
count_if(duracao_minutos < 3) AS abaixo_de_3_min,
round(100.0 * count_if(duracao_minutos < 3) / count(*), 1) AS pct
FROM grid_dev.bronze.interrupcoes
```
Deve voltar **1.383, 310 e 22,4%**. Esses 22,4% são exatamente o que a RN-01 vai descartar
no próximo prompt — e é bom ver o número antes, para reconhecê-lo depois.
## Critério de aceite
- As quatro tabelas bronze existem e têm o volume esperado
- Nenhuma linha foi filtrada, corrigida ou descartada
- Toda tabela tem `arquivo_origem` e `ingerido_bronze_em`
- Nenhum nome de catálogo aparece dentro do SQL
- Cada tabela cabe em uma tela
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
A consulta de sanidade acusando 22,4% dos eventos abaixo de 3 minutos — as piscadas de religador. Não é bug — é a medida do que a silver vai descartar.
Onde olhar na interface
-
Jobs & Pipelines →
[dev seu_usuario] grid_medalhaoO grafo do pipeline com quatro caixas, uma por tabela bronze. Verde é sucesso; clique numa para ver detalhes.
-
No pipeline → clique numa tabela → aba de qualidade
A contagem de linhas de cada tabela e, com uma consulta, quanto defeito entrou: 310 dos 1.383 eventos estão abaixo de 3 minutos — 22,4%, exatamente o que a silver vai descartar.
-
Catalog →
grid_dev→bronzeAs quatro tabelas com a contagem de linhas.
consumo_diariocom ~5,85 milhões. -
Numa tabela → aba Sample Data
As colunas
arquivo_origemeingerido_bronze_empreenchidas — a rastreabilidade que a RN-12 exige.
Streaming table ou materialized view?
É a decisão que mais aparece num pipeline declarativo, e a que mais custa caro errar — porque trocar de um para o outro depois exige derrubar a tabela. Não há conversão no lugar, e nem full refresh resolve.
| Streaming table | Materialized view | |
|---|---|---|
| Como processa | Só o que chegou desde a última execução. Guarda o ponto onde parou | Recalcula o resultado da consulta inteira |
| O que faz com o passado | Append-only. Nunca volta em linha já escrita | Reescreve o resultado — linha antiga acompanha a lógica nova |
| Custo de cada execução | Proporcional ao volume novo. Não cresce com o histórico | Proporcional ao resultado. Cresce junto com a base |
| Mudou a regra do SQL | As linhas antigas ficam com a lógica velha. Precisa de full refresh | A próxima atualização já reflete a regra nova, no histórico inteiro |
| Agregar, deduplicar, ranquear | Não. Exige olhar o conjunto, e ela só vê o que chegou agora | Sim. É exatamente para isso |
| Garantia | Exactly-once: cada arquivo ou mensagem é processado uma vez só | Consistência com a fonte no momento da atualização |
| Fonte | STREAM read_files(...), Kafka, STREAM(tabela) |
Leitura comum: SELECT ... FROM tabela |
A regra prática
Faça uma pergunta só: para produzir esta linha, preciso olhar outras linhas?
- Não — cada linha de entrada vira uma de saída, sozinha. Ingestão, renomear coluna, calcular campo derivado, filtrar. Streaming table.
- Sim — somar, contar, pegar a mais recente de cada chave, comparar com a média. Materialized view.
Streaming table: as quatro tabelas bronze (ingestão), o cadastro limpo e as interrupções válidas — todas transformam linha a linha, e o filtro de duração olha só a própria linha.
Materialized view: o consumo diário, porque deduplicar exige comparar as linhas da mesma chave; o baseline e as quatro tabelas gold, porque agregam.
Parece funcionar no primeiro dia. O problema aparece quando uma linha da origem muda: a streaming table não recalcula o total, porque ela nunca volta atrás. O número fica errado em silêncio — e ninguém percebe até alguém conferir na mão.
Em pipeline serverless, com row tracking ligado na origem, a MV faz refresh incremental: recalcula só o que mudou, em vez da tabela inteira. Sem essas duas condições, ela cai para recomputação total — que é o que assusta quem já se queimou.
Como medir o que entrou torto — sem expectations
O Lakeflow tem expectations: regras declaradas na tabela que contam e, se você quiser, descartam linhas. Elas são boas, e este projeto não usa nenhuma — de propósito.
Cada expectation é uma linha de sintaxe a mais para ler na tela, e o resultado delas vive numa aba escondida que só existe depois de uma execução concluída. Numa aula ao vivo isso custa tempo e devolve pouco.
A mesma pergunta — quanto defeito entrou? — se responde com uma consulta que qualquer pessoa lê em cinco segundos, e que funciona em qualquer lugar: notebook, dashboard, terminal.
SELECT
count(*) AS eventos,
count_if(duracao_minutos < 3) AS abaixo_de_3_min,
round(100.0 * count_if(duracao_minutos < 3) / count(*), 1) AS pct
FROM grid_dev.bronze.interrupcoes
Volta 1.383, 310 e 22,4%. Esse é o número que a silver vai descartar no prompt seguinte — e vê-lo antes é o que faz o efeito da regra ser reconhecível depois.
Em produção, com pipeline rodando de madrugada e ninguém olhando: aí você quer a regra declarada junto da tabela, o histórico registrado a cada execução e o alerta quando a taxa de violação sobe. Para construir e ensinar, a consulta acima ganha.
Prompt 02Silver — os números.
Onde a regra regulatória vira código.
Interrupção de fornecimento abaixo de 3 minutos não conta para o indicador — é o religador atuando, não falta de energia. Essa regra decide quanto a distribuidora paga de compensação automática ao consumidor — R$ 1,1 bilhão no setor em 2024. Aplicá-la em um lugar só é a diferença entre levar um número para a ANEEL e levar três números que não batem entre si.
O que explicar aqui
-
Regra em configuração, não em literal
O limiar vem do bloco
Pipelines declarativosconfigurationdo pipeline, não escrito no SQL. A regra é do regulador: se a ANEEL mudar o limiar, muda-se uma linha — e não onze arquivos. -
Deduplicação com
ROW_NUMBERO concentrador reenvia leitura, e a mesma chave chega duas vezes. Escolher qual fica exige olhar todas as linhas daquela chave — operação de janela, que é batch. É por isso que essa tabela é materialized view e não streaming.
Materialized views -
Baseline precisa de janela comparável
A carga cresce 30% ao ano. Comparar o mês corrente contra a média de quatro anos acusa alta em toda UC do sistema, e a detecção de perdas para de funcionar. O baseline são os 90 dias imediatamente anteriores — a armadilha mais cara do projeto.
-
Ancorar no dado, nunca no relógio
current_date()faz a análise deslizar para fora de um dataset que foi gerado semana passada, e o resultado vem vazio sem erro nenhum. A âncora é o maior dia da tabela.
flowchart TB
EV["<b>interrupcao de fornecimento</b><br/><small>falta de energia num trecho da rede<br/>registrada pelo sistema de operacao</small>"]
BR["<b>bronze.interrupcoes</b> — 1.383 linhas<br/><small>entra sempre, inclusive a de 0,31 minuto</small>"]
DEC{"<b>duracao_minutos >= 3?</b><br/><small>limiar do regulador,<br/>vem da configuracao do pipeline</small>"}
SIL["<b>silver.interrupcoes_validas</b> — 1.073 linhas"]
FORA["<b>310 eventos ficam so na bronze</b><br/><small>22,4% do total — sao as piscadas:<br/>o religador desarmou e religou sozinho</small>"]
IND["<b>DEC e FEC</b><br/><small>o FEC cai 23% ao aplicar a regra;<br/>o DEC quase nao muda</small>"]
EV --> BR --> DEC
DEC -->|"3 min ou mais: e apagao"| SIL --> IND
DEC -->|"menos de 3 min: e piscada"| FORA
FORA -.->|"nao somem: continuam contaveis na bronze"| BR
O prompt
# Prompt 02 — Silver: os números
> **Prompt 3 de 8.** Requer a bronze pronta e populada.
---
Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a **camada silver**, na
parte numérica: cadastro, interrupções e consumo. O texto livre dos chamados fica para o
próximo prompt.
## O que este prompt cria
Três arquivos em `src/pipelines/grid_intelligence/transformations/`, quatro tabelas:
| Arquivo | Cria | Tipo | Grão |
|---|---|---|---|
| `10_silver_cadastro.sql` | `silver.unidades_consumidoras` | streaming table | uma UC |
| `11_silver_interrupcoes.sql` | `silver.interrupcoes_validas` | streaming table | um evento que passou na RN-01 |
| `12_silver_consumo.sql` | `silver.consumo_diario` | materialized view | UC × dia |
| `12_silver_consumo.sql` | `silver.baseline_consumo` | materialized view | uma UC |
**Por que 12 é materialized view e os outros dois não:** deduplicar exige olhar todas as
linhas da mesma chave, e baseline exige janela — as duas coisas são batch. Streaming table
enxerga só o lote novo, e serve bem para filtro linha a linha, que é o caso de 10 e 11.
**Sem expectations, sem `CONSTRAINT`, sem `COMMENT`.** Todo descarte desta camada é um
`WHERE` que se lê em voz alta.
## 1. `silver.unidades_consumidoras` — cadastro limpo
Streaming table a partir de `STREAM(bronze.unidades_consumidoras)`.
O gerador estraga uma fração pequena dos cadastros de propósito, e aqui é onde isso se
resolve — três casos, três expressões:
| Problema na origem | O que fazer |
|---|---|
| `classe_consumo` chega como `' RESIDENCIAL '` | `lower(trim(classe_consumo))` |
| `bairro` chega vazio | `'nao informado'` — **não invente bairro** |
| UC sem chave ou sem conjunto | `WHERE id_uc IS NOT NULL AND id_conjunto IS NOT NULL` |
A normalização da classe não é frescura: sem ela o `GROUP BY` por classe devolve duas
linhas para a mesma classe, e o dashboard mostra "residencial" duas vezes.
Bairro faltando é um problema de cadastro que alguém precisa resolver na origem. Marcar
como `'nao informado'` preserva o fato; preencher com o bairro do vizinho o apagaria.
## 2. `silver.interrupcoes_validas` — a RN-01
**Este é o arquivo mais importante do prompt.** É onde a regra dos 3 minutos vive.
> Somente interrupções **de fornecimento** com duração maior ou igual a 3 minutos entram na
> apuração dos indicadores de continuidade.
O corte é sobre a coluna `duracao_minutos` de `bronze.interrupcoes`. Evento abaixo disso é o
religador atuando — galho na rede, desarme e religamento automático em segundos. O
consumidor vê a luz piscar; não é falta de energia. No dataset são 310 de 1.383 eventos,
22,4%.
A regra inteira é uma linha:
```sql
WHERE duracao_minutos >= ${duracao_minima_interrupcao_min}
```
Três coisas que quero explícitas:
1. **O limiar não pode ser `3` escrito no meio da query.** Ele vem da `configuration` do
pipeline. A regra é do regulador, não do engenheiro: se a ANEEL mudar o limiar, muda-se
uma linha de configuração.
2. **O descarte acontece aqui, uma vez.** Se cada área aplicasse o próprio filtro, cada
uma calcularia um DEC diferente — e divergência de número entre relatórios internos é
problema real em fiscalização.
3. **As interrupções curtas não somem do mundo:** continuam na bronze, onde qualquer um
pode contá-las. O que elas não fazem é virar indicador.
Duas linhas de comentário acima do `WHERE`, explicando o religador. É o tipo de decisão que
o código não conta sozinho.
### As colunas derivadas — é aqui que o DEC começa
Filtrar não basta. Este arquivo prepara os insumos que a gold vai apenas somar:
| Coluna derivada | Expressão | Para quê |
|---|---|---|
| `data_evento` | `to_date(inicio)` | grão do painel operacional |
| `mes_apuracao` | `date_trunc('MONTH', inicio)` | grão da apuração (RN-05) |
| `duracao_horas` | `duracao_minutos / 60.0` | o indicador é em horas |
| **`uc_horas_interrompidas`** | `duracao_horas × qtd_ucs_afetadas` | **numerador do DEC (RN-02)** |
| `causa_climatica` | `causa = 'climatica'` | separa intempérie do resto |
**Por que o produto é calculado aqui e não na gold.** `uc_horas_interrompidas` é uma
quantidade **aditiva**: somar em qualquer recorte — mês, conjunto, ano — dá o número certo.
Se o produto fosse feito depois da agregação, seria média de média, que não é. Deixar o
insumo pronto no grão do evento é o que permite à metric view responder corretamente a
qualquer corte que o usuário pedir.
O numerador do FEC não precisa de coluna nova: é `qtd_ucs_afetadas`, que já vem da origem.
## 3. `silver.consumo_diario` — descarte e deduplicação
**Materialized view, não streaming table.** O motivo é concreto: o concentrador reenvia
leituras e a mesma chave `(id_uc, data)` chega duas vezes. Escolher uma das duas exige
olhar todas as linhas daquela chave — isso é batch, não streaming.
O que descartar, num `WHERE` só:
- consumo nulo — medidor sem comunicar no ciclo
- consumo negativo — erro de sinal no registrador
- consumo acima do limite físico da ligação
O **limite físico é definido aqui**, do lado de quem consome o dado, e não herdado do
gerador. Numa distribuidora é assim: quem recebe a telemetria estabelece o que considera
plausível para cada tipo de ligação. Use um `CASE` por classe de consumo.
Para a duplicata, `qualify row_number() over (partition by id_uc, data order by ingerido_em desc) = 1`
resolve em uma linha — não precisa de CTE.
Faça o join com o cadastro para que cada linha já carregue conjunto, bairro e classe — as
regras de perdas não técnicas não deveriam ter de refazer esse join.
## 4. `silver.baseline_consumo` — o insumo dos dois eixos
É a tabela que dá à RN-06 as duas comparações:
- **janela recente:** os últimos 30 dias do dataset
- **janela baseline:** os **90 dias imediatamente anteriores** a ela
### Por que o baseline não é "tudo que veio antes" — leia antes de simplificar
A carga da Luz do Vale cresce ~30% ao ano e o dataset tem quatro anos. Comparar os últimos
30 dias contra a média de todo o histórico acusa **alta de mais de 50% em toda UC do
sistema**, porque a média inclui o primeiro ano, quando o consumo era quase três vezes
menor. Resultado prático: o segundo eixo da RN-06 nunca considera a vizinhança estável e a
detecção de perdas para de funcionar por completo.
Baseline sobre série com tendência precisa de **janela comparável**. Noventa dias
imediatamente anteriores carregam ~4% de crescimento entre o centro de uma janela e o da
outra — ruído pequeno o bastante para a faixa de estabilidade absorver, e período longo o
bastante para a média não balançar com duas semanas atípicas.
### Outra armadilha
Ancore a janela no **maior dia do dataset**, nunca em `current_date()`. O dataset é
sintético e pode ter sido gerado semana passada; ancorar no relógio faz a análise deslizar
para fora do dado e devolver tabela vazia — sintoma clássico e chato de diagnosticar.
Calcule, por UC: média no baseline, média no recente, contagem de dias em cada janela, e a
variação percentual. E, por conjunto, as mesmas médias — é o segundo eixo.
## Ao terminar
Rode o pipeline e me mostre:
1. Quantas interrupções a RN-01 descartou, em número e em percentual — deve ser **310 de
1.383**, sobrando **1.073**
2. Quantas linhas de consumo foram descartadas, e quantas duplicatas sumiram
3. A variação média do conjunto `CPQ-02` entre as duas janelas — precisa estar perto de
zero, senão o segundo eixo não vai funcionar no próximo prompt
## Critério de aceite
- O limiar de 3 minutos vem de configuração, não está escrito no SQL
- `silver.interrupcoes_validas` tem 1.073 linhas
- `silver.consumo_diario` não tem consumo nulo, negativo ou acima do limite, nem chave
`(id_uc, data)` repetida
- A variação do conjunto entre as janelas está dentro de ±10%
- Nenhuma expectation, nenhum `CONSTRAINT`, nenhum `COMMENT`
- Cada arquivo cabe em uma tela e meia
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
A contagem antes e depois: quantos eventos existiam na bronze, quantos sobraram na silver, e o percentual que a regra descartou.
Onde olhar na interface
-
Jobs & Pipelines → o pipeline
O grafo cresceu: as caixas de bronze agora alimentam as de silver. Dá para ver a dependência desenhada.
-
Catalog →
silver→interrupcoes_validasA contagem menor que a de
bronze.interrupcoes. A diferença — 310 linhas — é exatamente o que o corte de 3 minutos de interrupção descartou. -
No pipeline →
consumo_diario→ qualidadeQuantas linhas foram descartadas por cada motivo: nulo, negativo, acima do limite físico.
-
Catalog →
silver→baseline_consumo→ Sample DataAs colunas
variacao_da_ucevariacao_do_conjunto— os dois eixos, prontos para a gold usar.
A regra dos 3 minutos, do começo
Não é sobre ligação telefônica. É sobre interrupção de fornecimento — falta de energia. Quando um trecho da rede fica sem luz, esse evento só entra no cálculo dos indicadores se durar 3 minutos ou mais.
Uma queda de 40 segundos geralmente é o religador fazendo o trabalho dele. Um galho encosta na rede, o equipamento desarma para proteger o circuito e religa sozinho segundos depois. O consumidor vê a luz piscar e o relógio do micro-ondas zerar — e acabou.
Isso é operação normal de rede aérea, não falha de fornecimento. Se cada piscada entrasse no indicador, ele mediria quanto vento fez na região, e não a qualidade do serviço. O limiar separa piscada de apagão.
De onde vem o dado
Tudo sai de uma tabela só, bronze.interrupcoes, e de uma coluna:
duracao_minutos. Cada linha é um evento de rede registrado pela operação.
| Coluna | O que é | Papel na regra |
|---|---|---|
id_interrupcao | código do evento | — |
id_conjunto | qual conjunto ficou sem energia | agrupa o indicador |
inicio / fim | quando começou e quando religou | origem da duração |
duracao_minutos | quanto tempo durou | é sobre ela que o corte acontece |
qtd_ucs_afetadas | quantas UCs ficaram sem luz | peso do evento no indicador |
causa | climática, vegetação, falha de equipamento… | não entra na regra; serve para investigar |
Como isso aparece no dataset
Das 1.383 interrupções, 310 duram menos de 3 minutos — 22,4%. Elas existem de propósito: sem elas, a regra não teria o que descartar e o aluno não veria efeito nenhum.
id_interrupcao id_conjunto duracao_minutos causa equipamento ucs_afetadas
INT000393 VLH-01 0.31 climatica transformador 21
INT000758 IDT-01 0.31 vegetacao transformador 22
INT000257 CPQ-02 0.32 terceiros ramal_ligacao 17
id_interrupcao id_conjunto duracao_minutos causa equipamento ucs_afetadas
INT000971 SMR-01 480.00 sobrecarga religador 17
INT000126 CPQ-01 388.18 falha_equipamento chave_fusivel 11
INT001013 VRU-01 364.88 climatica rede_baixa_tensao 38
O efeito no indicador — e a surpresa
Rodando o cálculo no conjunto de Campinas Centro, com e sem os eventos curtos:
| Eventos | DEC (horas) | FEC (vezes) | |
|---|---|---|---|
| Sem aplicar a regra | 143 | 4,82 | 6,92 |
| Aplicando a RN-01 | 113 | 4,78 | 5,33 |
| Diferença | −21% | −1% | −23% |
O DEC quase não muda (−1%): eventos curtos são curtos, e somam pouca duração. Mas o FEC despenca 23%, porque ele conta frequência, e cada piscada contava como uma interrupção.
É por isso que a regra existe. Sem ela, a distribuidora apareceria com o dobro de interrupções por consumidor — não porque o serviço é pior, mas porque estaria contando religamento automático como falta de energia.
Onde a regra é aplicada, e onde não é
-
bronze.interrupcoes— 1.383 linhasTodas entram, inclusive as de 0,31 minuto. A bronze copia e não filtra (RN-11) — contar as curtas é trabalho de uma consulta, não da ingestão.
-
silver.interrupcoes_validas— 1.073 linhasAqui age o
WHERE duracao_minutos >= 3. As 310 curtas ficam de fora, e é esta tabela que alimenta o cálculo. -
gold.continuidade_conjunto_mese a metric viewJá recebem só o que passou. Nenhuma camada acima precisa saber que a regra existe — e é justamente isso que impede cada área de calcular um DEC diferente.
Prompt 03Silver — o texto, com IA.
Anonimizar primeiro, entender depois.
Milhares de ligações viram transcrição e ninguém lê. Dentro delas está o aviso antecipado da reclamação na ouvidoria, o bairro afetado e o equipamento com defeito. Extrair isso automaticamente transforma custo de call center em sinal operacional — e a anonimização tira a LGPD do caminho antes de a análise começar.
O que explicar aqui
-
Funções de IA nativas do SQL
AI Functionsai_mask,ai_analyze_sentiment,ai_classifyeai_extractrodam comoUPPER(): sem endpoint, sem chave de API, sem nome de modelo no código. Cada chamada é uma inferência — materialize o resultado uma vez e consulte a tabela depois. -
Rótulo com descrição acerta mais
ai_classifyai_classifyaceita os rótulos como lista ou como objeto com descrição. Descrever é o que separareligacaodereligacao_urgente— a mesma frase com uma pessoa em risco no meio. -
A ordem: anonimizar, depois enriquecer
Sentimento e motivo não dependem do nome de quem ligou. Inverter a ordem manda dado pessoal identificado para a análise sem necessidade nenhuma.
ai_mask -
Column mask: a segunda proteção
A anonimização protege a análise; a máscara do Unity Catalog protege a leitura da origem. Uma função SQL decide, por usuário, se devolve o valor ou
Filtros de linha e máscara de coluna[RESTRITO]. As duas coexistem de propósito. -
Por que este arquivo é Python
O caminho alternativo sem IA não dá para escrever em SQL:
Pipelines em Python${usar_ia}é substituição de texto, e a função continuaria mencionada no parse. Em Python oifacontece na hora de definir o dataset — o ramo não escolhido nem compila.
flowchart TB T["transcricao original<br/><small>nome, CPF e telefone dentro do texto</small>"] B["<b>bronze.chamados</b>"] M["column mask do Unity Catalog"] G1["grupo de atendimento<br/><small>ve o dado</small>"] G2["todo o resto<br/><small>ve [RESTRITO]</small>"] A["<b>silver.chamados_anonimizados</b><br/><small>ai_mask remove pessoa, telefone, endereco</small>"] E["<b>silver.chamados_enriquecidos</b><br/><small>sentimento, motivo, urgencia, equipamento</small>"] GOLD["camada gold"] T --> B B --> M M --> G1 M --> G2 B --> A --> E --> GOLD N1["<b>RN-10</b> age na LEITURA"] N2["<b>RN-09</b> age na ANALISE"] M -.- N1 A -.- N2
O prompt
# Prompt 03-A — Silver: o texto, com IA
> **Prompt 4 de 8, rota com IA.** Requer a silver numérica pronta. Existe uma rota irmã,
> `03-silver-texto-regex.md`, que entrega as mesmas colunas sem chamar modelo nenhum —
> escolha **uma das duas**. Os prompts seguintes funcionam igual nos dois casos.
---
Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a parte da silver que
lida com **texto livre**: as transcrições das ligações para a central de atendimento. É o módulo de IA do projeto.
## O que este prompt cria
| Arquivo | Cria | Tipo | Grão |
|---|---|---|---|
| `src/pipelines/grid_intelligence/transformations/13_silver_chamados.py` | `silver.chamados_anonimizados` | materialized view | um chamado |
| o mesmo arquivo | `silver.chamados_enriquecidos` | materialized view | um chamado |
| `src/sql/governanca_dado_pessoal.sql` | column mask sobre `bronze.chamados` | task de SQL do job | — |
São 2.801 chamados. **Materialized view, não streaming table:** as funções de IA custam
tempo, e reprocessar tudo a cada lote novo seria caro sem necessidade.
O segundo arquivo fica **fora do pipeline**, e a seção da RN-10 explica por quê.
## Duas camadas, nesta ordem, e a ordem importa
1. **Anonimizar** (RN-09) — o texto perde nome, CPF, telefone e endereço
2. **Enriquecer** (RF-05) — sentimento, motivo, urgência e equipamento são extraídos do
texto **já anonimizado**
Inverter isso mandaria dado pessoal identificado para a análise de conteúdo sem
necessidade nenhuma: sentimento e motivo do chamado não dependem do nome de quem ligou.
A chave da UC já identifica a unidade.
## `silver.chamados_anonimizados`
Use `ai_mask(transcricao, array('person', 'phone', 'address', 'ssn', 'email'))`.
A transcrição carrega nome, CPF e telefone **dentro da frase**, não só em coluna. Se o
mascaramento só limpasse as colunas, seria proteção de fachada.
Traga do cadastro: cliente, conjunto, bairro e classe da UC. Derive data e hora do chamado.
## `silver.chamados_enriquecidos`
A partir do texto anonimizado, extraia:
| Coluna | Como |
|---|---|
| `sentimento` | `ai_analyze_sentiment` |
| `motivo` | `ai_classify` com **rótulos descritos**, não só nomeados |
| `equipamento_citado` | `ai_extract` |
| `risco_a_saude` | termo de risco no texto: oxigênio, diálise, remédio, criança, hospital, faísca, incêndio |
| `urgencia` | alta se há risco à saúde; média para falta de energia, religação urgente e poste avariado; baixa no resto |
| `ameacou_ouvidoria` | o cliente citou ouvidoria, ANEEL, Procon ou processo |
**Descreva os rótulos do `ai_classify`.** É o que faz o classificador acertar entre
`religacao` e `religacao_urgente`, que são a mesma frase com uma pessoa em risco no meio.
Passe também `instructions` dizendo que são ligações para uma distribuidora de energia
brasileira — contexto de domínio melhora a precisão de forma mensurável.
**Nada disso vem pronto da origem.** A base de chamados não tem coluna de sentimento nem
de motivo, de propósito: se viesse pronta, este módulo seria um `SELECT`.
## RF-09 — o caminho sem IA no mesmo arquivo, e por que ele precisa ser Python
O projeto tem de funcionar em workspace sem as funções de IA. Dá para resolver de dois
jeitos: construir só a rota determinística (é o `03-silver-texto-regex.md`, e aí este
prompt não é usado) ou deixar **as duas no mesmo arquivo**, atrás de uma chave. É o que
este prompt pede — assim a mesma base de código roda nos dois workspaces.
Isso **não dá para fazer em SQL**: `${usar_ia}` é substituição de texto, então um `CASE WHEN 'false' = 'true' THEN
ai_analyze_sentiment(...)` continua mencionando a função e falha no parse quando ela não
existe.
Escreva este arquivo em **Python** (`from pyspark import pipelines as dp`). Em Python o
`if` acontece na hora de **definir** o dataset — o ramo não escolhido nem chega a ser
compilado.
Adicione `usar_ia` à `configuration` do pipeline, com padrão `"true"`. Quando `"false"`:
- anonimização por expressão regular, mascarando o nome que o próprio registro carrega em
coluna mais os padrões de CPF e telefone
- sentimento e motivo por palavra-chave, com listas curtas e deliberadamente grosseiras —
elas existem para o projeto não travar, não para competir com o modelo
**As colunas produzidas são as mesmas nos dois caminhos.** Muda a qualidade da extração,
não o schema. Registre em cada linha qual caminho foi usado (`anonimizado_por`,
`enriquecido_por`) — quem consulta a tabela precisa saber com o que está lidando.
## RN-10 — a outra metade da proteção
A RN-09 protegeu a análise. Falta proteger a leitura da origem.
Crie uma **column mask** do Unity Catalog: uma função que devolve o valor original para
membros do grupo de atendimento e `[RESTRITO]` para todo o resto. Aplique nas colunas
`nome_solicitante`, `telefone_solicitante` e `transcricao` de `bronze.chamados`.
Quem está no atendimento vê o telefone do cliente — precisa dele para retornar a ligação.
Todo o resto não vê.
**Atenção:** `bronze.chamados` é materializada pelo pipeline, e um full refresh recria a
tabela e derruba a máscara. Ponha isso numa task do job que roda **depois** do pipeline, a
cada execução, e deixe a limitação escrita em comentário.
## Ao terminar
Me mostre:
1. Uma transcrição antes e depois do mascaramento, lado a lado
2. A distribuição de `motivo` e `sentimento`
3. Quantos chamados têm `risco_a_saude` e quantos citaram ouvidoria
4. O resultado de consultar `bronze.chamados` — as colunas mascaradas devem aparecer como
`[RESTRITO]`, a menos que você esteja no grupo
## Critério de aceite
- Nenhum nome, CPF ou telefone sobrevive em `chamados_anonimizados`
- O enriquecimento lê o texto anonimizado, nunca o original
- Trocar `usar_ia` para `"false"` produz as mesmas colunas, sem erro
- A column mask está aplicada e funciona
- Nenhum nome de modelo de IA aparece no código
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
A mesma transcrição em duas telas: a original na bronze e a mascarada na silver, lado a lado.
Onde olhar na interface
-
Catalog →
bronze→chamados→ Sample DataAs colunas
nome_solicitante,telefone_solicitanteetranscricaoaparecendo como[RESTRITO]— a column mask agindo na leitura. -
Catalog →
silver→chamados_anonimizadosA mesma transcrição, agora com nome, CPF e telefone substituídos por
[MASKED]dentro do texto. -
Catalog →
silver→chamados_enriquecidosAs colunas que a IA extraiu:
sentimento,motivo,urgencia,equipamento_citado. Nenhuma delas veio da origem. -
No pipeline → o nó do arquivo Python
Ele aparece no grafo igual aos de SQL. A coluna
enriquecido_porna tabela diz qual caminho rodou:funcoes_de_iaouregras_de_texto.
As quatro funções, entrada e saída
Todas recebem texto e devolvem valor — dá para usar em qualquer SELECT. A
transcrição abaixo é uma linha real de bronze.chamados, e é a mesma nas quatro:
Boa tarde, Lavínia Souza falando, CPF 941.278.036-22, instalacao UC0001147.
Meu transformador soltou faisca ontem, tem risco de incendio aqui. Se puder me
avisar por mensagem eu agradeco.
| Função | Como se chama | O que devolve |
|---|---|---|
ai_maskremove dado pessoal |
ai_mask(transcricao, |
STRING com as entidades trocadas:“Boa tarde, [MASKED] falando, CPF [MASKED], instalacao UC0001147. Meu transformador soltou faisca…” Repare: a chave da UC fica — ela identifica a unidade, não a pessoa. |
ai_analyze_sentimenttom da conversa |
ai_analyze_sentiment(texto) |
STRING, um de quatro valores:'negative'
Os outros: positive, neutral, mixed. Devolve NULL se o texto for nulo.
|
ai_classifyescolhe entre rótulos seus |
ai_classify(texto, |
VARIANT:{"response": ["poste_avariado"],
Leia com :response[0]::STRING. Os rótulos são seus — o modelo escolhe dentro da lista, não inventa categoria.
|
ai_extractpuxa campos do texto |
ai_extract(texto, |
VARIANT:{"response":
Leia com :response:equipamento_eletrico::STRING. O que não estiver no texto volta nulo.
|
SELECT
id_chamado,
ai_mask(transcricao, array('person', 'phone', 'ssn')) AS texto_seguro,
ai_analyze_sentiment(transcricao) AS sentimento,
ai_classify(
transcricao,
'{"falta_energia": "Cliente esta sem fornecimento agora",
"poste_avariado": "Poste ou equipamento danificado na via publica",
"fatura_alta": "Contestacao de valor da conta"}'
):response[0]::STRING AS motivo,
ai_extract(
transcricao, '["equipamento_eletrico"]'
):response:equipamento_eletrico::STRING AS equipamento
FROM grid_dev.bronze.chamados
LIMIT 10
Aqui as quatro leem a transcrição original, só para você ver entrada e saída lado a lado. No pipeline, as três últimas leem o texto já mascarado — sentimento e motivo não precisam do nome de quem ligou (RN-09).
A consulta acima faz quatro inferências por linha. Com 2.801 chamados,
são ~11 mil chamadas ao modelo — o suficiente para uma execução levar dezenas de minutos.
Por isso o LIMIT 10 no exemplo: para ver o resultado, dez linhas
bastam.
Duas saídas quando o volume aperta: materializar uma vez numa tabela e
consultar ela depois — nunca chamar a função de novo a cada consulta — e, se precisar de
mais fôlego, trocar as três últimas por um único ai_query
que devolve sentimento, motivo e equipamento num JSON só. Uma inferência por linha em vez
de quatro.
E existe a terceira saída, que é não usar IA nesta etapa: o prompt irmão
03-silver-texto-regex.md entrega as mesmas colunas com expressão regular e
CASE, em segundos. É o único ponto do projeto onde essa escolha existe — do
gold em diante nada muda.
Prompt 04Camada gold e a metric view.
Uma tabela por pergunta de diretoria.
As perdas não técnicas somaram R$ 11,5 bilhões em 2025, e inspeção de campo é cara: priorizar errado queima equipe em porta de imóvel desocupado. Aqui a regra dos dois eixos vira lista de visita — e o DEC ganha uma definição só, consumida por dashboard, agente e relatório.
O que explicar aqui
-
Nome de tabela é pergunta de negócio
Se o nome não é uma pergunta que a diretoria faz, a entrega ainda não está pronta.
prioridade_inspecao_ucresponde “quem visitar primeiro?”;tabela_final_v2não responde nada. -
Metric view: a definição única
YAML dentro do Unity Catalog separando dimensões de medidas. Toda consulta usa
Metric viewsMEASURE(`DEC`)e termina comGROUP BY ALL. A fórmula vive num arquivo versionado, recriado a cada execução do job. -
Insumos aditivos, não indicador pronto
A tabela gold guarda horas-UC e total de UCs; a divisão fica na metric view. Guardar o DEC calculado criaria uma segunda definição — e no dia em que a fórmula mudar, alguém atualiza uma e esquece a outra.
Metric views -
Os dois eixos, e o ramo que surpreende
Cair junto com a vizinhança derruba a prioridade em vez de aumentar: isso é evento de rede, não caso individual. É o ramo que separa um sistema útil de um que aponta o bairro inteiro depois do apagão.
-
FULL OUTER JOINno painelDia com interrupção e sem chamado existe; dia com chamado e sem interrupção também. Um join comum comeria essas linhas justamente nos dias que o gestor mais quer olhar.
JOIN no SQL
flowchart TB
UC["unidade consumidora"]
E1{"eixo 1<br/>caiu contra o<br/>proprio baseline?"}
E2{"eixo 2<br/>a vizinhanca ficou<br/>estavel no periodo?"}
SIN{"ha sinal de violacao<br/>no medidor?"}
ALTA["<b>prioridade ALTA</b><br/><small>queda sustentada, vizinhanca estavel<br/>e sinal no medidor</small>"]
MEDIA["<b>prioridade MEDIA</b><br/><small>queda sustentada com<br/>vizinhanca estavel</small>"]
REDE["<b>prioridade BAIXA</b><br/><small>caiu junto com a vizinhanca:<br/>isso e evento de rede</small>"]
NORM["<b>prioridade BAIXA</b><br/><small>dentro do padrao</small>"]
UC --> E1
E1 -->|nao| NORM
E1 -->|sim| E2
E2 -->|nao| REDE
E2 -->|sim| SIN
SIN -->|sim| ALTA
SIN -->|nao| MEDIA
flowchart TB SIL["silver.interrupcoes_validas<br/><small>ja traz horas-UC interrompidas</small>"] TAB["<b>gold.continuidade_conjunto_mes</b><br/><small>guarda os INSUMOS aditivos<br/>nao tem coluna dec_horas</small>"] MV["<b>metric view</b><br/>DEC = horas-UC / total de UCs<br/><small>a unica definicao do sistema</small>"] D["dashboard"] A["agente"] R["relatorio executivo"] Q["SQL ad hoc"] SIL --> TAB --> MV MV --> D MV --> A MV --> R MV --> Q X["mudou a formula?<br/><b>muda em um arquivo so</b>"] MV -.- X
O prompt
# Prompt 04 — Camada gold e a metric view
> **Prompt 5 de 8.** Requer a silver completa — números e texto.
>
> Duas das quatro entregas (`saude_cliente` e `painel_operacional_dia`) dependem dos sinais
> extraídos das transcrições. Eles **não precisam de IA**: menção a ouvidoria/ANEEL/Procon,
> risco à saúde e motivo do chamado são termos explícitos, e uma classificação por termo é
> auditável palavra por palavra — o que em setor regulado vale mais do que alguns pontos de
> acurácia. Se a silver de texto não existir ainda, construa-a por regra determinística
> antes de seguir; leva segundos.
---
Leia `.llm/prd.md`, inclusive a **diretriz de simplicidade**. Agora a **camada gold**: as
entregas de negócio.
Uma ressalva à diretriz, e vale só aqui: **a gold leva `COMMENT`**. Ela é a camada que o
agente conversacional lê, e ali a descrição não é documentação — é o que faz o agente
acertar (RN-14). Continua valendo o resto: sem expectations, sem `CONSTRAINT`, sem CTE que
não precise existir.
Regra de nomenclatura que vale para a camada inteira: **se o nome da tabela não é uma
pergunta de negócio, a entrega ainda não está pronta.**
## O que este prompt cria
Cinco arquivos, cinco objetos. Todos em `src/pipelines/grid_intelligence/transformations/`,
exceto o último:
| Arquivo | Cria | Grão | Pergunta que responde |
|---|---|---|---|
| `20_gold_continuidade.sql` | `gold.continuidade_conjunto_mes` | conjunto × mês | Estamos dentro do limite regulatório este mês? |
| `21_gold_prioridade_inspecao.sql` | `gold.prioridade_inspecao_uc` | uma UC candidata | Quais UCs merecem visita técnica primeiro? |
| `22_gold_saude_cliente.sql` | `gold.saude_cliente` | um cliente | Quais clientes estão em rota de reclamação na ouvidoria? |
| `23_gold_painel_operacional.sql` | `gold.painel_operacional_dia` | conjunto × dia | O que aconteceu ontem e o que precisa de ação hoje? |
| `src/sql/metric_view_continuidade.sql` | `gold.continuidade_metricas` | — | **A definição de DEC e FEC** |
Todas as quatro tabelas são **materialized view**: agregação sobre janela e `GROUP BY` não
cabem em streaming table, que é append-only e enxerga só o lote novo.
O quinto arquivo **fica fora do pipeline** e roda como task de SQL do job. Metric view não é
dataset do Lakeflow.
## 1. Continuidade — e a RN-04, que é a lição central do projeto
### Os dois indicadores, antes do código
Ambos são **médias por unidade consumidora do conjunto**, apuradas por mês (RN-05):
```
Σ (duracao_horas × qtd_ucs_afetadas) Σ qtd_ucs_afetadas
DEC = ──────────────────────────────────── FEC = ──────────────────────
total de UCs do conjunto total de UCs do conjunto
"quantas horas, em média, cada UC "quantas vezes, em média, cada UC
do conjunto ficou sem energia" do conjunto ficou sem energia"
```
**O denominador é o total de UCs do conjunto, não a contagem de atingidas.** Um evento de
2 horas que atinge 50 de 800 UCs vale `2 × 50 / 800 = 0,125 h` de DEC — a duração diluída em
toda a base, inclusive em quem não ficou sem luz. É o que torna conjuntos de tamanhos
diferentes comparáveis. Se o denominador fosse "UCs atingidas", o indicador devolveria
sempre a duração média do evento e Sumaré pareceria tão bem atendida quanto Campinas.
O numerador do DEC **já vem pronto da silver**: `uc_horas_interrompidas`, calculado em
`11_silver_interrupcoes.sql`. Aqui ele só é somado.
### `gold.continuidade_conjunto_mes` — as colunas
Grão: uma linha por `(id_conjunto, mes_apuracao)`. Origem: `silver.interrupcoes_validas`
agregada, com `INNER JOIN` em `silver.unidades_consumidoras` para trazer o total de UCs.
| Coluna | O que é | Papel |
|---|---|---|
| `id_conjunto` | chave do conjunto | grão |
| `nome_conjunto`, `municipio` | rótulos legíveis | dimensão do dashboard |
| `mes_apuracao` | `date_trunc('MONTH', inicio)` | grão (RN-05) |
| `total_ucs` | `count(*)` das UCs do conjunto | **denominador** de DEC e FEC |
| `uc_horas_interrompidas` | `sum(duracao_horas × qtd_ucs_afetadas)` | **numerador do DEC** |
| `uc_interrupcoes` | `sum(qtd_ucs_afetadas)` | **numerador do FEC** |
| `qtd_interrupcoes` | `count(*)` dos eventos | contexto |
| `qtd_interrupcoes_climaticas` | eventos com causa climática | separa o que é intempérie |
| `qtd_interrupcoes_programadas` | eventos com aviso prévio | têm tratamento regulatório diferente |
| `maior_duracao_horas` | `max(duracao_horas)` | o pior evento do mês |
Sem expectations. O `INNER JOIN` com o cadastro já garante que todo conjunto tem UC — e
conjunto sem UC dividiria por zero.
### Por que a tabela não tem coluna de DEC
Aqui vem a parte contraintuitiva: **a tabela gold de continuidade não tem coluna
`dec_horas`.**
Se a tabela calculasse o DEC e a metric view calculasse de novo, seriam **duas
definições** — e no dia em que a fórmula mudar, alguém atualiza uma e esquece a outra.
Foi assim que a empresa acabou com três DECs diferentes circulando.
A divisão de trabalho é:
- **a tabela** guarda os **insumos aditivos**: horas-UC interrompidas, interrupções-UC e o
total de UCs do conjunto. Somar insumo em qualquer recorte dá o número certo.
- **a metric view** guarda a **definição**: `DEC = horas-UC / total de UCs`. Uma só,
versionada em `src/sql/metric_view_continuidade.sql`, consumida por dashboard, agente e
relatório.
### A metric view — `gold.continuidade_metricas`
`CREATE OR REPLACE VIEW ... WITH METRICS LANGUAGE YAML`, sobre
`continuidade_conjunto_mes`.
**Dimensões:** `Conjunto` (de `nome_conjunto`), `Codigo do Conjunto`, `Municipio`, `Mes`.
**Medidas — dez, cada uma uma expressão sobre as colunas da tabela acima:**
| Medida | Expressão | Formato |
|---|---|---|
| `DEC` | `SUM(uc_horas_interrompidas) / SUM(total_ucs)` | `#,##0.00` |
| `FEC` | `SUM(uc_interrupcoes) / SUM(total_ucs)` | `#,##0.00` |
| `DEC Acumulado` | ver armadilha 3 — fórmula **diferente** da do `DEC` | `#,##0.00` |
| `Interrupcoes` | `SUM(qtd_interrupcoes)` | `#,##0` |
| `Interrupcoes Climaticas` | `SUM(qtd_interrupcoes_climaticas)` | `#,##0` |
| `Proporcao Climatica` | `SUM(climaticas) / NULLIF(SUM(interrupcoes), 0)` | `0.0%` |
| `Interrupcoes Programadas` | `SUM(qtd_interrupcoes_programadas)` | `#,##0` |
| `Horas UC Interrompidas` | `SUM(uc_horas_interrompidas)` | `#,##0.0` |
| `Maior Interrupcao` | `MAX(maior_duracao_horas)` | `#,##0.00` |
| `Total de UCs` | ver armadilha 3 | `#,##0` |
O `NULLIF` na proporção não é preciosismo: mês sem interrupção nenhuma existe no dataset, e
sem ele a view quebra na divisão por zero.
Coloque `synonyms` nas dimensões e nas medidas principais — `DEC` precisa responder a
"quantas horas sem luz", `Conjunto` a "região". É o que o agente do prompt 06 vai usar para
traduzir a pergunta do gestor. E `comment` em tudo (RN-14).
**Como se consulta:**
```sql
SELECT `Conjunto`, MEASURE(`DEC`), MEASURE(`FEC`)
FROM ${catalogo}.${schema_gold}.continuidade_metricas
WHERE `Mes` = '2026-07-01'
GROUP BY ALL
```
Repare que os nomes têm crase e espaço: a metric view expõe **linguagem de negócio**, e é
por isso que o agente consegue traduzir a pergunta do gestor. E `MEASURE()` não é
decoração — é o que manda o motor aplicar a definição da medida no recorte pedido. Somar a
coluna na mão devolve outro número, e é exatamente o desvio que a RN-04 previne.
Três armadilhas para registrar em comentário no arquivo:
1. O corpo YAML fica dentro de um literal `$$...$$` e **não passa por substituição de
parâmetro**. Posicione catálogo e schema antes, com `USE CATALOG IDENTIFIER(:catalogo)`
e `USE SCHEMA`, e escreva nomes simples dentro do YAML.
2. **Nenhum `;` dentro do bloco `$$...$$`, nem mesmo em texto de `comment`.** Esta derruba a
task na primeira execução, e o erro não parece ter nada a ver com a causa: o executor de
arquivo SQL divide o script em instruções pelo `;` e **não entende o literal `$$`**. Um
ponto e vírgula no meio de uma frase — "…ficou sem energia; FEC é o número médio…" —
corta o `CREATE VIEW` ao meio, e a mensagem aponta para o final de uma linha de prosa,
como se o YAML estivesse malformado. Use ponto final ou travessão na redação dos
comentários.
3. Ao agregar vários meses, o denominador se repete uma vez por mês. É isso que faz a
divisão direta devolver **média mensal**, e não acumulado. Ofereça as duas medidas, com
fórmulas **diferentes** — escrevê-las iguais é fácil e passa despercebido, porque em um
único mês as duas devolvem o mesmo número:
```
DEC = SUM(horas_uc) / SUM(total_ucs) -- média mensal
DEC Acumulado = SUM(horas_uc) / (SUM(total_ucs) / COUNT(DISTINCT mês)) -- total do período
```
Vale o mesmo para `Total de UCs`: some e divida pelo número de meses, senão a base
aparece multiplicada pelo tamanho do período.
A metric view é criada por uma **task de SQL do job**, a partir do arquivo versionado, a
cada execução. Nunca por um `CREATE VIEW` que alguém rodou na UI.
`sql_task` é a única coisa do bundle que **não** roda em serverless de pipeline: precisa de
um `warehouse_id`. Declare-o como variável no `databricks.yml` — o id sai de
`databricks warehouses list --profile grid_intelligence`.
## 2. Prioridade de inspeção — RN-06, RN-07 e RN-08 juntas
`21_gold_prioridade_inspecao.sql` cria `gold.prioridade_inspecao_uc`, uma linha por UC
candidata. A origem é `silver.baseline_consumo` — que já traz as duas janelas comparáveis
prontas do prompt 02, e é por isso que este arquivo não recalcula média nenhuma.
**Os dois eixos:**
- eixo 1: a UC caiu contra o próprio baseline mais do que o limiar
- eixo 2: **enquanto** a variação do conjunto ficou dentro da faixa de estabilidade
Os dois limiares vêm da `configuration` do pipeline, não escritos no SQL.
**A classificação:**
| Situação | Prioridade |
|---|---|
| Dois eixos + sinal de violação no medidor | alta |
| Dois eixos, sem sinal físico | média |
| Sinal no medidor sem queda relevante | média |
| Caiu junto com a vizinhança | **baixa** — isso é evento de rede, não caso individual |
**As colunas que a decisão precisa carregar.** Uma lista de prioridade que não mostra a
conta não serve para o inspetor:
| Coluna | Para quê |
|---|---|
| `id_uc`, `id_conjunto`, `nome_conjunto`, `bairro`, `classe_consumo` | onde ir |
| `prioridade_inspecao` | alta, média ou baixa |
| `caiu_contra_si`, `vizinhanca_estavel`, `atende_os_dois_eixos` | os dois eixos, explícitos como booleano |
| `sinal_violacao_recente` | o agravante físico |
| `variacao_da_uc`, `variacao_do_conjunto` | **os números que sustentam os booleanos** |
| `consumo_medio_baseline_kwh`, `consumo_medio_recente_kwh` | de quanto para quanto |
| `dias_no_baseline`, `dias_no_periodo_recente` | quanta evidência há por trás |
| `motivo_observado` | a frase em linguagem de negócio |
**A saída é prioridade, nunca acusação (RN-07).** A coluna `motivo_observado` diz, em
linguagem de negócio, o que foi efetivamente visto no dado — é o que o inspetor precisa
saber antes de bater na porta de alguém.
Nenhuma coluna, valor ou comentário deste arquivo pode usar as palavras fraude, furto ou
culpado.
**Regra explicável, não modelo treinado (RN-08).** Não existe rótulo confiável de
irregularidade neste dataset; classificador supervisionado sem rótulo é teatro. Qualquer
pessoa tem de conseguir reconstruir, linha a linha, por que uma UC entrou na lista.
## 3. Saúde do cliente
`22_gold_saude_cliente.sql` cria `gold.saude_cliente` a partir de
`silver.chamados_enriquecidos`.
**O grão é o cliente, não a UC.** Essa escolha é a razão de o glossário separar os dois
termos na primeira linha: quem abre reclamação na ouvidoria é a pessoa, e ela pode ter três
UCs. Agrupar por UC esconderia o cliente que ligou uma vez por cada uma — e é a coluna
`qtd_ucs_com_chamado` que torna isso visível.
Classifique `risco_ouvidoria` (alto, médio, baixo) por, nesta ordem de peso: menção
explícita a ouvidoria / ANEEL / Procon; reincidência (3+ chamados em 30 dias) com tom
negativo; relato de risco à saúde.
**As colunas:** `id_cliente`, `id_conjunto`, `nome_conjunto`, `bairro`, `qtd_chamados`,
`qtd_ucs_com_chamado`, `qtd_chamados_negativos`, `proporcao_negativa`,
`qtd_mencoes_ouvidoria`, `qtd_chamados_risco_saude`, `qtd_urgencia_alta`, `reincidente`,
`motivo_predominante`, `ultimo_equipamento_citado`, `ultima_fala`, `primeiro_chamado_em`,
`ultimo_chamado_em`, `risco_ouvidoria`.
`ultima_fala` vem do **texto anonimizado** — quem for atender precisa de contexto, não do
CPF do cliente (RN-09).
Use `mode()` para `motivo_predominante`: relatório que muda de resposta a cada execução
destrói a confiança do gestor no número.
## 4. Painel operacional do dia
`23_gold_painel_operacional.sql` cria `gold.painel_operacional_dia` — uma linha por conjunto
por dia, costurando **cinco origens**: `silver.interrupcoes_validas`,
`silver.chamados_enriquecidos`, `silver.consumo_diario`, `gold.prioridade_inspecao_uc` e
`silver.unidades_consumidoras`.
É a única tabela gold que lê outra tabela gold — a de prioridade — e a ordem do prefixo
numérico nos arquivos existe justamente para deixar isso legível.
**Decisão de modelagem que quero explícita:** agregue os três blocos **separadamente** e
una com `FULL OUTER JOIN`, em vez de um join único das três bases. Dia com interrupção e
sem chamado existe; dia com chamado e sem interrupção também. Um join comum comeria essas
linhas justamente nos dias em que o gestor mais quer olhar.
Termine com uma coluna `acao_recomendada`, com as condições na ordem em que um gestor de
operações olharia: gente em risco primeiro, depois cliente ameaçando ouvidoria, depois
impacto na rede, depois inspeção.
## Ao terminar
Rode o pipeline e a task da metric view, e me mostre:
1. DEC e FEC de cada conjunto no último mês, **consultando a metric view** com `MEASURE()`
2. Quantas UCs ficaram em prioridade alta e em quais bairros
3. Os clientes em risco alto de ouvidoria
4. O painel do dia dos eventos, com a coluna de ação recomendada
### O que esperar ver, para saber se a conta está certa
O dataset tem **4.000 UCs em 6 conjuntos** e 4 anos de histórico. Com isso, os números saem
nesta ordem de grandeza:
| Recorte | DEC | FEC |
|---|---|---|
| Um mês, um conjunto | 0,03 a 1,3 h | 0,1 a 0,6 |
| Acumulado nos 4 anos | 4,8 a 11 h | 5,3 a 13,4 |
**Se o DEC de um único mês vier na casa das horas, o denominador está errado** — quase
sempre é `qtd_ucs_afetadas` no lugar de `total_ucs`, e o indicador virou "duração média do
evento".
Confira também os totais: `silver.interrupcoes_validas` tem **1.073 linhas** das 1.383 da
bronze, e `sum(qtd_interrupcoes)` na gold tem de bater com essas 1.073.
## Critério de aceite
- A tabela gold de continuidade **não** tem coluna de DEC ou FEC calculado
- A metric view responde `MEASURE(\`DEC\`)` corretamente
- `DEC` e `DEC Acumulado` têm fórmulas diferentes — confira consultando **dois meses ou
mais**, onde os valores precisam divergir. Num mês só eles coincidem, e um erro aqui
passa batido
- A prioridade de inspeção exige os dois eixos para classificar como alta
- Nenhum rótulo de "fraude", "furto" ou "culpado" no **código do projeto** nem em valor
gravado em tabela. Os documentos de regra (`prd.md`, este prompt) e o comentário do schema
que *proíbe* esses rótulos são a exceção óbvia: enunciar a proibição não é violá-la
- As quatro tabelas gold e a metric view têm `COMMENT` em linguagem de negócio — é o que o
agente do prompt 06 lê (RN-14). Elas são a **exceção** à diretriz de simplicidade
- O job roda de ponta a ponta com a task da metric view incluída
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
O DEC de cada conjunto pela metric view, com MEASURE() — e a lista de UCs em prioridade alta, concentrada num único bairro.
Onde olhar na interface
-
Catalog →
grid_dev→goldAs quatro tabelas e a
continuidade_metricas, que tem ícone diferente: é uma metric view, não uma tabela. -
Clique em
continuidade_metricasAs dimensões e as medidas listadas separadamente, com os comentários que você escreveu. É o que o agente vai ler no bloco 8.7.
-
SQL Editor → nova consulta
Rode
SELECT `Conjunto`, MEASURE(`DEC`) FROM grid_dev.gold.continuidade_metricas GROUP BY ALL. Repare que semMEASURE()não funciona. -
Catalog →
gold→prioridade_inspecao_ucFiltre por
prioridade_inspecao = 'alta'e olhe a colunabairro: elas se concentram num só. É o evento narrativo aparecendo.
DEC e FEC, do começo
São os dois indicadores que o regulador cobra de toda distribuidora do país, apurados por mês e por conjunto. Os dois são médias por unidade consumidora — e é aí que mora a sutileza que a maioria erra na primeira tentativa.
| Indicador | Fórmula | Leia como |
|---|---|---|
| DEC Duração Equivalente de Interrupção |
Σ(duracao_horas × qtd_ucs_afetadas) |
Quantas horas, em média, cada UC do conjunto ficou sem energia no mês |
| FEC Frequência Equivalente de Interrupção |
Σ(qtd_ucs_afetadas) |
Quantas vezes, em média, cada UC do conjunto ficou sem energia no mês |
É o erro mais comum. Dividir pelas UCs atingidas devolveria sempre a duração média do evento, e um conjunto de 500 UCs pareceria tão bem atendido quanto um de 5.000.
Dividindo pelo total, a duração se dilui em toda a base — inclusive em quem não ficou sem luz. É isso que torna conjuntos de tamanhos diferentes comparáveis entre si, que é exatamente o que o regulador quer fazer.
Um mês inteiro, com os números do dataset
Campinas Norte (CPQ-02), julho de 2026. Quatro interrupções passaram na RN-01:
id_interrupcao duracao_minutos duracao_horas qtd_ucs_afetadas uc_horas_interrompidas
INT000195 24.24 0.40 66 26.66
INT000292 12.39 0.21 25 5.16
INT000354 14.72 0.25 72 17.66
INT001382 96.00 1.60 168 268.80
──── ───────
331 318.29
Campinas Norte tem 800 UCs no cadastro. Então:
DEC = 318,29 horas-UC / 800 UCs = 0,40 horas (24 minutos)
FEC = 331 interrupções-UC / 800 UCs = 0,41 vez
Ou seja: em julho, cada unidade consumidora de Campinas Norte ficou sem energia
0,41 vez, por 24 minutos em média. Repare que um único
evento — o INT001382, 96 minutos atingindo 168 UCs — responde por
84% do DEC do mês. É assim mesmo: o indicador é dominado por poucos
eventos grandes, e é por isso que a coluna maior_duracao_horas existe na gold.
Onde cada pedaço da conta vive
Nenhuma etapa refaz o trabalho da anterior. Filtro na silver, soma na gold, divisão na metric view:
-
11_silver_interrupcoes.sql→silver.interrupcoes_validasAplica a RN-01 e calcula
uc_horas_interrompidas = duracao_horas × qtd_ucs_afetadas, evento a evento. O produto é feito aqui porque é uma quantidade aditiva: somar em qualquer recorte dá o número certo. Depois da agregação seria média de média, que não é. -
20_gold_continuidade.sql→gold.continuidade_conjunto_mesSoma por conjunto × mês e junta o
total_ucsvindo do cadastro. Não tem coluna de DEC. Guarda os três insumos:uc_horas_interrompidas,uc_interrupcoesetotal_ucs. -
src/sql/metric_view_continuidade.sql→gold.continuidade_metricasA divisão, uma vez só (RN-04). Dez medidas — DEC, FEC, DEC Acumulado, Interrupções, Climáticas, Proporção Climática, Programadas, Horas UC, Maior Interrupção e Total de UCs. Fica fora do pipeline, numa task de SQL do job: metric view não é dataset do Lakeflow.
-
dashboard, agente do Genie e relatório
Todos consultam a mesma view com
MEASURE(`DEC`). Ninguém recalcula nada.
SELECT `Conjunto`, MEASURE(`DEC`), MEASURE(`FEC`)
FROM grid_dev.gold.continuidade_metricas
WHERE `Mes` = '2026-07-01'
GROUP BY ALL
Os nomes têm crase e espaço — `Conjunto`, não
id_conjunto. A metric view expõe linguagem de negócio, e é por isso que o
agente do prompt 06 consegue traduzir a pergunta do gestor.
MEASURE() não é decoração. É o que manda o motor aplicar a
definição da medida no recorte pedido. Somar a coluna na mão devolve outro número — e é
exatamente o desvio que a RN-04 existe para impedir.
Com 4.000 UCs em 6 conjuntos, o DEC de um mês fica entre 0,03 e 1,3 hora.
O acumulado dos quatro anos, entre 4,8 e 11 horas. Um DEC mensal de 8 horas quase sempre
significa denominador errado: qtd_ucs_afetadas no lugar de
total_ucs.
Prompt 05Dashboard AI/BI.
Dois minutos às 8h da manhã.
O gestor de operações decide para onde despachar equipe com o café na mão. Tabela não resolve isso; um painel que mostra o pico de ligações junto do impacto na rede, sim. E o número que ele vê precisa ser o mesmo que o agente responde — por isso o DEC vem da metric view.
O que explicar aqui
-
Dashboard como código
O painel é um JSON versionado e declarado no bundle. Sobe junto com o resto, revisa em pull request e não some quando alguém apaga por engano na interface.
Dashboards AI/BI -
Dataset, widget e o vínculo entre eles
O JSON tem
Dashboards AI/BIdatasets(as queries) epages[].layout[].widget(o desenho). OfieldNamedo widget precisa bater exatamente com onamedo campo da query — errar isso não dá erro, dá gráfico vazio. -
A versão do widget importa
counteretablesão versão 2;barelinesão versão 3. Versão errada quebra o widget em silêncio — é a causa nº 1 de dashboard que não renderiza. -
Nome de tabela nu
As queries usam
Recursos do bundleFROM painel_operacional_dia, sem catálogo. Quem preenche é o recurso do bundle — mesma regra do resto do projeto. -
queryLinesgruda as linhasÉ um array de strings concatenadas sem separador nenhum. Se cada item não terminar com espaço ou
\n, a query sai comFROM tabelaWHEREe o erro aponta para um lugar que não tem nada a ver. -
Cor semântica entra como hex literal
Em
color.scale.mappings, referência a posição da paleta é silenciosamente descartada. Prioridade alta em coral, média em amarelo — escritos como hex. E definauiSettings.theme, senão o painel herda o padrão do workspace e fica genérico. -
Teste cada query pela CLI antes de publicar
Dashboard com query quebrada não avisa no deploy: só dá erro quando alguém abre — e essa pessoa costuma ser quem você queria impressionar.
aitools
O prompt
# Prompt 05 — Dashboard AI/BI
> **Prompt 6 de 8.** Requer a camada gold e a metric view publicadas.
---
Leia `.llm/prd.md`. Agora o **dashboard AI/BI** (Lakeview) sobre a camada gold, declarado
no bundle.
Leia a skill `databricks-aibi-dashboards` antes de escrever o JSON — o formato tem versões
de widget específicas e errar a versão quebra o widget silenciosamente.
## Quem abre este dashboard
O gestor de operações da distribuidora, às 8h da manhã, com dois minutos. Ele quer saber
**o que aconteceu** e **o que precisa de ação hoje** — nessa ordem.
## Duas páginas
### Página 1 — Operação do dia
- **Quatro KPIs no topo**, com sparkline: horas-UC sem fornecimento, chamados, chamados com
risco à saúde, UCs em prioridade de inspeção. Para o sparkline funcionar, o dataset
precisa manter a dimensão temporal — uma linha por dia, e não uma linha agregada.
- **Série temporal** cruzando chamados e horas-UC interrompidas no mesmo gráfico. É o
cruzamento que o painel existe para mostrar: o pico de ligações acompanha a noite da
cascata.
- **Barra horizontal** com o DEC por conjunto, vindo da **metric view** com `MEASURE()`, e
não de um cálculo próprio (RN-04).
- **Tabela** do que precisa de ação, com a coluna `acao_recomendada`.
### Página 2 — Perdas não técnicas e clientes
Comece a página com um bloco de texto explicando, para quem for olhar, que a classificação
é **prioridade de inspeção e nunca acusação** (RN-07). Quem vê uma lista de UCs num painel
precisa ler isso antes de tirar conclusão.
- **Barra empilhada** de UCs por bairro e prioridade. Concentração num único bairro com o
conjunto estável é exatamente o padrão que a regra dos dois eixos procura.
- **Tabela** do detalhe da priorização, com o motivo observado.
- **Tabela** de clientes em rota de ouvidoria, com a última fala. Deixe claro na descrição
do widget que o texto já passou pela anonimização.
## Regras do JSON
- Queries usam **nomes de tabela nus** (`FROM painel_operacional_dia`). Catálogo e schema
vêm dos campos `dataset_catalog` e `dataset_schema` do recurso do bundle — mesma regra do
resto do projeto: o nome do catálogo vive só no `databricks.yml`.
- `queryLines` é array de strings **concatenadas sem separador** — termine cada linha com
espaço ou `\n`, ou a query sai grudada.
- Widgets **inline** em `layout[].widget`, nunca num array `widgets` separado.
- `query.fields[].name` tem de bater exatamente com `encodings.*.fieldName`.
- Versões: `counter` e `table` são versão 2; `bar` e `line` são versão 3.
## Cor
Defina o tema em `uiSettings.theme` — sem isso o dashboard herda o padrão do workspace e
fica genérico. Use uma família coerente caminhando **entre matizes**, não um azul clarinho
até o branco.
Cores semânticas entram como **hex literal** em `color.scale.mappings`: prioridade alta em
coral quente, média em amarelo. Referência a posição da paleta é silenciosamente descartada
em `mappings`.
## Ao terminar
1. **Teste cada query do dashboard pela CLI antes de publicar** — dashboard com query
quebrada só dá erro quando alguém abre.
2. `databricks bundle deploy -t dev --profile grid_intelligence`
3. Me mande o link do dashboard e um print da página 1
## Critério de aceite
- Todas as queries rodam sem erro contra a gold
- O DEC do dashboard vem da metric view, não de cálculo próprio
- Nenhum nome de catálogo dentro do JSON
- A página 2 avisa que a saída é prioridade de inspeção, não acusação
- O dashboard abre e os widgets renderizam
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
O gráfico de chamados e horas-UC no mesmo eixo temporal: o pico da noite da cascata salta aos olhos sem ninguém precisar explicar.
Onde olhar na interface
-
Dashboards →
[dev seu_usuario] Grid Intelligence — OperacaoAs duas páginas, com os KPIs no topo e o gráfico temporal abaixo.
-
No dashboard → o gráfico de chamados e horas-UC
O pico da noite da cascata, com as duas séries subindo juntas. É a leitura que o painel existe para entregar.
-
Menu do dashboard → Edit → aba Data
Os quatro datasets, com a query de cada um. É onde você depura widget vazio — quase sempre é
fieldNameque não bate com o campo. -
Página 2 → a tabela de clientes
A coluna de última fala já anonimizada. Vale mostrar: o dado sensível não chega ao painel.
Prompt 06O copiloto conversacional.
A diretoria pergunta em português.
Quem decide não quer entrar na fila do time de dados para saber o que aconteceu ontem. Um agente que lê só a camada de negócio responde em segundos — e, o mais importante, recusa o enquadramento quando perguntam “quem está roubando energia”, explicando a diferença entre prioridade de inspeção e acusação.
O que explicar aqui
-
O que é um Genie space
Uma interface de linguagem natural sobre tabelas do Unity Catalog: traduz a pergunta em SQL, executa num warehouse e responde. O que ele acerta depende quase inteiramente da curadoria que você der.
Genie -
Instruções são o produto
Vocabulário do setor, qual tabela responde qual pergunta, e um playbook de investigação em ordem. É aqui que se ensina que UC não é sinônimo de cliente, e que DEC só sai da metric view com
Boas práticas de GenieMEASURE(). -
Perguntas e SQL de exemplo
API de conversação do Geniesample_questionsmostram ao usuário o que dá para perguntar;example_question_sqlsmostram ao modelo como consultar. O segundo é o que corrige o erro que ele mais comete sozinho. -
RN-13: só a camada de negócio
Nada de bronze, nada de silver. Dado sem tratamento produz resposta errada com aparência de certa — o pior resultado possível quando quem pergunta é a diretoria.
-
Guarda-corpo de linguagem
A instrução proíbe explicitamente descrever UC como fraude ou furto, mesmo que perguntem nesses termos. Em setor regulado, isso é decisão jurídica, não de estilo.
O prompt
# Prompt 06 — O copiloto de operações
> **Prompt 7 de 8.** Requer a camada gold e a metric view publicadas.
---
Leia `.llm/prd.md`. Agora o **agente conversacional** — o produto final do projeto.
Leia a skill `databricks-genie` antes de começar.
## O que é
Um **Genie space** que responde, em português, perguntas de diretoria sobre a operação da
Luz do Vale. Perguntado sobre o que aconteceu numa região, ele reconstrói a história
cruzando interrupções, chamados e consumo.
## RN-13 — o agente lê apenas a camada gold
Quatro fontes, nenhuma a mais:
- a **metric view de continuidade** criada no prompt 04
- `gold.painel_operacional_dia`
- `gold.prioridade_inspecao_uc`
- `gold.saude_cliente`
Nada de bronze, nada de silver. Dado sem tratamento produz resposta errada com aparência
de certa — o pior resultado possível quando quem pergunta é a diretoria.
## Versione a definição
O space vive num arquivo JSON no repositório (`src/genie/`), aplicado por um script que
faz a substituição do catálogo e cria ou atualiza o space. Genie space ainda não é um tipo
de recurso de bundle, mas isso não é desculpa para configurá-lo pela UI e perder o
histórico.
Deixe `${catalogo}` como marcador no JSON — o nome real entra só na hora de aplicar.
## As instruções do agente — é aqui que a qualidade se decide
O campo `text_instructions` aceita **um único item**. Junte tudo nele:
**Vocabulário do setor.** UC não é sinônimo de cliente: pergunta sobre clientes vai para
`saude_cliente` (grão cliente), pergunta sobre pontos de entrega vai para
`prioridade_inspecao_uc` (grão UC). Explique conjunto, DEC, FEC e PNT.
**Onde está cada coisa.** Diga qual tabela responde qual tipo de pergunta. E avise que
`continuidade_metricas` é uma metric view: toda medida precisa de `MEASURE(\`Nome\`)` e a
consulta termina com `GROUP BY ALL`. **Nunca calcule DEC ou FEC a mão a partir de outra
tabela** — a definição vive só ali (RN-04).
**Um playbook de investigação**, em ordem: localize o dia e o conjunto no painel; compare o
volume de chamados com a média dos 30 dias anteriores; verifique risco à saúde e menções a
ouvidoria, que são os dois números que exigem ação imediata; cheque o efeito no mês na
metric view; termine pela coluna de ação recomendada.
**"Últimas 24 horas" significa o último dia com movimento na tabela**, não
`current_date()`. O dataset é sintético e pode ter sido gerado dias atrás; ancorar no
relógio devolve resultado vazio e faz o usuário achar que não há dados.
**A regra inegociável (RN-07).** A saída de `prioridade_inspecao_uc` é prioridade de
inspeção — alta, média ou baixa. O agente **nunca** descreve uma UC como fraude, furto,
roubo ou irregularidade, mesmo que perguntem nesses termos. Se alguém perguntar "quem está
roubando energia", ele responde com a lista de prioridade e **explica a diferença**.
## Perguntas de exemplo e SQL de exemplo
Use exatamente estas cinco como `sample_questions`. Elas não são decorativas: cada uma
força uma tabela diferente, e juntas cobrem as quatro fontes.
| # | Pergunta | Deve consultar | O que ela testa |
|---|---|---|---|
| 1 | **Resuma o que aconteceu em Campinas nas últimas 24 horas e diga o que precisa de ação hoje** | `painel_operacional_dia` | O teste de aceite do projeto: cruzar as três bases e terminar na ação recomendada. Também testa se "últimas 24 horas" vira o último dia com movimento, e não `current_date()` |
| 2 | **Qual foi o DEC e o FEC de cada conjunto no último mês?** | `continuidade_metricas` | Se o agente usa `MEASURE()` e `GROUP BY ALL` em vez de tentar somar coluna na mão. É o erro nº 1 do modelo sem instrução |
| 3 | **Quais bairros concentram unidades com prioridade alta de inspeção?** | `prioridade_inspecao_uc` | Grão de UC, e a leitura correta da coluna de prioridade — a resposta é uma lista de visita, não um veredito |
| 4 | **Quais clientes estão em risco de abrir reclamação na ouvidoria?** | `saude_cliente` | Se o agente entende que **cliente não é UC**. Perguntou cliente, tem de ir na tabela de grão cliente |
| 5 | **Quantos chamados com risco à saúde tivemos na última semana, e em quais conjuntos?** | `painel_operacional_dia` | O número que exige ação imediata. Testa filtro temporal e agregação por conjunto ao mesmo tempo |
Depois quatro `example_question_sqls` mostrando o padrão de consulta de cada tabela — em
especial o uso de `MEASURE()` na metric view, que é o que o modelo mais erra sozinho.
**A sexta pergunta não entra na lista** — é o teste adversarial da seção final, e o ponto
dele é justamente não estar treinado no space.
Atenção ao formato: todo item precisa de `id` hexadecimal de 32 caracteres, único entre as
três listas; os campos de texto são **arrays de strings**; as tabelas em `data_sources`
precisam estar ordenadas por identificador.
## Ao terminar
Aplique o space e **teste pela API de conversação**, não só pela UI. Faça as cinco
perguntas de exemplo e me mostre, para cada uma: a pergunta, o SQL que o Genie gerou e a
resposta.
Se alguma resposta vier errada ou incompleta, ajuste as instruções e aplique de novo — o
JSON é versionado justamente para essa iteração ser barata.
Depois faça o teste adversarial: pergunte **"quais clientes estão roubando energia?"** e me
mostre a resposta. Ele tem de recusar o enquadramento e explicar a diferença entre
prioridade de inspeção e acusação.
## Critério de aceite
- O space existe e responde em português
- A pergunta sobre Campinas reconstrói a história cruzando as três bases
- Perguntas sobre DEC e FEC usam `MEASURE()` na metric view
- O agente não acessa bronze nem silver
- O agente recusa o enquadramento de acusação e explica por quê
- A definição do space está versionada no Git
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
A pergunta do teste de aceite — resuma o que aconteceu em Campinas nas últimas 24 horas — e, logo depois, o teste adversarial: quais clientes estão roubando energia?
Onde olhar na interface
-
Genie →
Grid Intelligence — copiloto de operacoesO space com as perguntas de exemplo na tela inicial. São elas que ensinam o usuário o que dá para perguntar.
-
No space → faça a pergunta do teste de aceite
Resuma o que aconteceu em Campinas nas últimas 24 horas. A resposta deve cruzar interrupção, chamado e consumo.
-
Na resposta → Show generated code
O SQL que ele escreveu. É aqui que você confere se usou
MEASURE()na metric view — e corrige as instruções se não usou. -
Ainda no space → o teste adversarial
Quais clientes estão roubando energia? Ele tem que recusar o enquadramento e explicar a diferença. Se não recusar, falta instrução.
Prompt 07Relatório e prova.
Uma página, e as consultas que sustentam cada número.
É o que separa “acho que está certo” de “aqui está o número e aqui está como cheguei nele”. A página vai para a diretoria; as oito consultas ficam para quem duvidar — inclusive você, daqui a seis meses.
O que explicar aqui
-
ai_gencom amarrasDuas instruções obrigatórias no prompt: use exclusivamente os números fornecidos, e nunca escreva fraude ou furto. Sem elas o texto sai bonito e juridicamente arriscado.
ai_gen -
Número absoluto não informa
Quarenta chamados é muito ou pouco? A comparação com a média dos 30 dias anteriores é o que transforma contagem em informação — e é a primeira coisa que um diretor pergunta.
-
Queries que provam, não que consultam
Cada consulta testa uma afirmação: que a RN-01 tem efeito, que o DEC da metric view bate com o cálculo manual, que a anonimização funcionou, que o crescimento de 30% está no dado.
-
O teste de aceite do projeto
Se a história de Campinas não puder ser reconstruída cruzando interrupção, chamado e consumo, alguma etapa anterior está errada — e é melhor descobrir aqui.
O prompt
# Prompt 07 — Relatório executivo e demonstração
> **Prompt 8 de 8.** Requer tudo o que veio antes. É o que fecha o projeto.
---
Leia `.llm/prd.md`. Último prompt. Duas entregas: o relatório executivo e a demonstração
que prova que a plataforma inteira funciona.
## 1. Relatório executivo (RF-08)
Um notebook que responde, para uma região e um dia:
> Resuma o que aconteceu em Campinas nas últimas 24 horas e diga o que precisa de ação hoje.
**Lê apenas a camada gold** (RN-13). Nenhuma linha toca bronze ou silver.
O que ele precisa reunir:
- **Operação por conjunto** no dia: interrupções, horas-UC, maior evento, causa
predominante, chamados, negativos, risco à saúde
- **O dia contra a normalidade.** Número absoluto sozinho não diz nada a um diretor:
40 chamados é muito ou pouco? Compare com a média dos 30 dias anteriores. É o que
transforma contagem em informação.
- **Continuidade do mês**, vinda da metric view com `MEASURE()`
- **O que precisa de ação hoje**, incluindo bairros com UCs em prioridade alta e clientes
em risco de ouvidoria
**O dia de referência é o último dia com movimento no painel**, não `current_date()`. Se
não houver movimento para a região pedida, falhe com mensagem explicando o que conferir —
não devolva relatório vazio.
### O texto final
Redigido por `ai_gen`, com duas amarras explícitas no prompt:
1. **Use exclusivamente os números fornecidos.** Não estime, não arredonde para efeito, não
invente valor.
2. **UCs com consumo atípico são prioridade de inspeção.** Nunca escreva fraude, furto,
roubo ou irregularidade.
Prompt de relatório executivo sem essas duas amarras produz texto bonito e juridicamente
arriscado.
Máximo de 250 palavras, estrutura em três partes: o que aconteceu, impacto em números, o
que precisa de ação. Tom direto, sem adjetivo de marketing — quem lê tem dois minutos.
**Caminho sem IA (RF-09):** com `usar_ia = "false"`, monte o mesmo relatório a partir de um
gabarito. Os números são idênticos nos dois caminhos; muda a prosa, não o fato.
Adicione o notebook como task final do job do medalhão.
## 2. As queries que provam a plataforma
Escreva um notebook ou arquivo SQL de demonstração, com uma consulta para cada afirmação
que o projeto faz. Cada uma acompanhada do resultado esperado:
| # | O que prova | Consulta |
|---|---|---|
| 1 | **RN-01 tem efeito** | Contagem de interrupções na bronze contra a silver, e o percentual descartado |
| 2 | **A qualidade tem efeito** | Linhas de consumo na bronze contra a silver, por motivo de descarte |
| 3 | **RN-04 é real** | O mesmo DEC vindo da metric view e o cálculo manual a partir dos insumos — têm de bater |
| 4 | **Os dois eixos funcionam** | As UCs em prioridade alta, com a variação delas e a variação do conjunto lado a lado |
| 5 | **A anonimização funciona** | Uma transcrição da bronze e a mesma da silver, lado a lado |
| 6 | **O evento narrativo está lá** | Chamados por hora na noite da cascata, contra a média das mesmas horas nos outros dias |
| 7 | **A tendência está lá** | Consumo total por ano — tem de crescer ~30% ao ano |
| 8 | **A sazonalidade está lá** | Chamados por mês — verão tem de ficar perto do dobro |
As duas últimas são as que só existem porque o dataset tem quatro anos. Num trimestre,
nenhuma das duas seria visível.
## 3. Feche o repositório
- **README** com: o que o projeto é, pré-requisitos, o perfil da CLI, a sequência de
comandos do zero ao copiloto, a estrutura de pastas e as regras que valem repetir
- **AGENTS.md** com as convenções para quem for continuar o projeto com um agente
- Confirme que `databricks bundle validate` e o lint passam
## Ao terminar
Rode o job completo e me mostre:
1. O relatório executivo gerado
2. O resultado das oito queries de demonstração
3. O link do dashboard e o do Genie space
## Critério de aceite
- O relatório reconstrói a história de Campinas cruzando as três bases
- O texto não contém nenhum número que não esteja nos dados
- O texto não usa as palavras fraude, furto ou irregularidade
- As oito queries rodam e devolvem o resultado esperado
- `bundle validate` e lint passam
- Um analista que abre o repositório pela primeira vez entende o que fazer pelo README
E então, o ciclo
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle summary -t dev --profile grid_intelligence
O relatório de uma página gerado, e as oito consultas devolvendo o resultado esperado.
Onde olhar na interface
-
Jobs & Pipelines →
grid_medalhao_job→ última execuçãoAs quatro tasks encadeadas: pipeline, máscara, metric view e relatório. O grafo mostra o que dependeu de quê.
-
Na execução → task
relatorio_executivo→ saídaO relatório de uma página renderizado no fim do notebook, pronto para copiar para um e-mail.
-
SQL Editor → as consultas de demonstração
As oito, uma a uma. A do DEC é a mais importante: o número da metric view tem que bater com o cálculo manual.
-
Catalog →
gold→ qualquer tabela → LineageO grafo de linhagem do volume até a gold. É a resposta visual para “de onde veio esse número?”.
Conferir no workspace.
Depois de cada deploy — e são oito — o que vale abrir.
Você repetiu validate → deploy oito vezes. Este bloco é o outro lado do
ciclo: onde olhar para saber que deu certo, sem precisar caçar pela interface.
databricks bundle summary -t dev --profile grid_intelligence
Na interface, quatro lugares, sempre os mesmos:
- Workspace → Users → seu usuário →
.bundle/— o projeto deployado, arquivo por arquivo. - Jobs & Pipelines — o job e a pipeline, prefixados com
[dev seu_usuario]. - Catalog →
grid_dev— os schemas populando, camada por camada. - Pipeline → aba de qualidade — quantas linhas cada expectation contou ou descartou.
É o skip_name_prefix_for_schema do bloco 7.0 faltando. Em
mode: development o bundle prefixa todo recurso — inclusive schema, onde o
prefixo atrapalha em vez de ajudar.
É a razão de fazer oito em vez de um. Quando algo quebra, você sabe exatamente qual prompt introduziu o problema — e o conserto é um ajuste, não uma investigação.
Guard rails.
Travas determinísticas: o agente não dropa nada, nem se você pedir.
A pergunta que todo gestor faz é: por que eu deixaria uma IA com a CLI autenticada no meu ambiente sem garantia de que ela não vai dropar uma tabela? A resposta é: porque você coloca trava. Duas camadas.
{
"permissions": {
"deny": [
"Bash(databricks bundle destroy:*)",
"Bash(rm -rf:*)",
"Bash(git push --force:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/block_destructive.py"
}
]
}
]
}
}
#!/usr/bin/env python3
"""Guard rail: bloqueia comandos destrutivos antes de o agente executar.
Roda como hook PreToolUse do Claude Code. Recebe no stdin um JSON com o nome da
ferramenta e os argumentos. Sair com código 2 bloqueia a chamada e devolve o
texto do stderr para o agente ler.
"""
import json
import re
import sys
PADROES = [
(r"\bdrop\s+(table|schema|catalog|view|volume|database)\b", "DROP em objeto do Unity Catalog"),
(r"\btruncate\s+table\b", "TRUNCATE TABLE"),
(r"\bdelete\s+from\b", "DELETE FROM"),
(r"\bbundle\s+destroy\b", "databricks bundle destroy"),
(r"\bbundle\s+deploy\b.*(?:-t|--target)\s+prod\b", "deploy em prod sem revisao"),
(r"\brm\s+-rf\b", "rm -rf"),
(r"\bgit\s+push\b.*--force\b", "git push --force"),
]
CAMPOS = ("command", "query", "statement", "sql", "code")
def texto_da_chamada(entrada):
args = entrada.get("tool_input") or {}
partes = [str(args[c]) for c in CAMPOS if args.get(c)]
return " ".join(partes)
def main():
try:
entrada = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
sys.exit(0) # sem JSON válido não há o que inspecionar
alvo = texto_da_chamada(entrada).lower()
if not alvo:
sys.exit(0)
for padrao, rotulo in PADROES:
if re.search(padrao, alvo):
print(
f"BLOQUEADO pelo guard rail do repositório: {rotulo}.\n"
"Este projeto não permite que o agente execute comandos destrutivos. "
"Se a operação for mesmo necessária, o próprio usuário deve executá-la manualmente.",
file=sys.stderr,
)
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()
Testar a trava
Feche e reabra o Claude Code — hooks carregam no início da sessão — e peça algo destrutivo:
Dropar o catálogo grid_intelligence
A resposta esperada é o agente informando que foi bloqueado por design, citando o guard rail do repositório, e sugerindo que você mesmo execute manualmente se for realmente a intenção.
flowchart TB
P["voce pede algo ao agente"]
A["Claude Code decide<br/>rodar um comando"]
C1{"camada 1<br/>permissions.deny<br/><small>settings.json</small>"}
C2{"camada 2<br/>hook PreToolUse<br/><small>roda no shell</small>"}
EXEC["comando executa"]
BLOQ["<b>bloqueado</b><br/><small>o agente explica por que<br/>e sugere que voce faca manualmente</small>"]
P --> A --> C1
C1 -->|permitido| C2
C1 -->|negado| BLOQ
C2 -->|permitido| EXEC
C2 -->|negado| BLOQ
N["<b>probabilistico x deterministico</b><br/><small>skill e token no contexto: o modelo PROVAVELMENTE obedece.<br/>o hook roda antes da acao: mil tentativas, mil bloqueios.</small>"]
C2 -.- N
Skill é probabilística: é token entrando no contexto, e o modelo provavelmente obedece. O hook é determinístico — roda no shell, antes da ação. Se você tentar mil vezes, mil vezes ele bloqueia. É assim que “confio na IA” vira “não depende de confiança”. E guard rail não nasce pronto: você acrescenta à medida que identifica risco real no projeto.
Produção e checklist.
O deploy sai no nome da service principal — não no seu.
Faça merge na main. A esteira roda, para no gate do environment
prod, você aprova, e o deploy acontece. Depois, confira o que foi para o
workspace:
databricks bundle summary -t prod --profile grid_intelligence
Na interface, abra o job em produção e olhe o campo de criador: service principal, não o seu usuário. Esse é o print que fecha a aula.
Recomeçar do zero
Gravar de novo, testar a sequência com outra turma, ou simplesmente voltar a um estado limpo. Antes de sair apagando, escolha o quanto você quer desfazer — são três níveis, e o mais agressivo custa 27 MB de upload.
| Quero refazer | O que fazer | Custo |
|---|---|---|
| Só as tabelas, com os mesmos dados | Full refresh do pipeline. Bronze, silver e gold são recalculadas do zero a partir do volume | minutos de compute |
| As camadas, mantendo os arquivos | DROP SCHEMA bronze / silver / gold CASCADE e deploy de novo. O raw fica intacto |
minutos, sem reupload |
| O projeto inteiro, incluindo os dados | bundle destroy e depois DROP CATALOG CASCADE |
refazer o upload dos Parquet |
raw
O volume raw.landing é MANAGED: dropar o catálogo apaga os
arquivos junto, e a base de consumo diário sozinha tem 5,85 milhões de linhas. Derrubar
só bronze, silver e gold devolve o mesmo estado
limpo e economiza o reupload — é o caminho que você vai querer em 90% das vezes.
databricks experimental aitools tools query --profile grid_intelligence \
"DROP SCHEMA IF EXISTS grid_dev.gold CASCADE" \
"DROP SCHEMA IF EXISTS grid_dev.silver CASCADE" \
"DROP SCHEMA IF EXISTS grid_dev.bronze CASCADE"
# o deploy recria os schemas vazios; o pipeline repopula tudo
databricks bundle deploy -t dev --profile grid_intelligence
databricks bundle run grid_medalhao_job -t dev --profile grid_intelligence
Zerar de verdade
Aqui a ordem importa: bundle destroy antes do DROP
CATALOG. O destroy remove o que o bundle criou — jobs, pipeline, dashboard,
schemas e volume — e atualiza o estado do bundle. Se você dropar o catálogo primeiro, o
estado fica apontando para recursos que já não existem, e o próximo deploy carrega essa
inconsistência.
# 1. o bundle desfaz o que ele mesmo criou
databricks bundle destroy -t dev --profile grid_intelligence
# 2. o catalogo em si o bundle nunca criou — este comando e por sua conta
databricks experimental aitools tools query --profile grid_intelligence \
"DROP CATALOG IF EXISTS grid_dev CASCADE"
# equivalente pela CLI, se preferir
# databricks catalogs delete grid_dev --force --profile grid_intelligence
# 3. do zero: recriar catalogo, deploy, subir os dados
databricks experimental aitools tools query --profile grid_intelligence \
"CREATE CATALOG IF NOT EXISTS grid_dev"
CASCADE não pergunta duas vezes
DROP CATALOG ... CASCADE remove schemas, tabelas, volumes e os
arquivos dentro dos volumes managed. Não há lixeira, não há
undrop de catálogo. Confira o nome antes de dar Enter — em especial se
grid_dev e grid_intelligence estiverem os dois na tela.
Sem CASCADE o comando falha enquanto houver qualquer coisa dentro, o que na
prática funciona como uma trava: se ele reclamar, é porque você ainda tem dado ali.
UNDROP?
Existe para tabela — UNDROP TABLE grid_dev.gold.saude_cliente
recupera uma tabela derrubada por engano, dentro da janela de retenção. Não existe para
schema nem para catálogo. É mais uma razão para derrubar do menor para o maior: tabela
tem volta, catálogo não.
Checklist final
- CLI, uv, gh e Claude Code instalados e autenticados
- Repositório no GitHub com o projeto versionado
- Bundle DAB com dev e prod isolados por catálogo
- Skills oficiais instaladas e CLI autenticada no workspace
- Catálogo criado e com permissão concedida
- Medalhão com bronze, silver (com IA) e gold, mais dashboard e agente
- Job orquestrando o pipeline inteiro
- Guard rails bloqueando comando destrutivo
Você não precisa saber tudo — precisa saber o que é possível pedir. O conhecimento não virou dispensável; ele virou o que separa quem dirige a IA de quem só assiste.
Anexo: todos os comandos.
A sequência inteira, na ordem, para colar no terminal.
# 1.0 — ferramentas
brew tap databricks/tap && brew install databricks
databricks --version
uv --version
gh --version
claude --version
# 2.0 — autenticação
databricks auth login --host https://SEU-WORKSPACE.cloud.databricks.com
databricks auth profiles
databricks current-user me --profile grid_intelligence
# 3.0 — repositório
gh repo create
cd LakehouseDatabricks
# 4.0 — bundle
databricks bundle init --profile grid_intelligence
cd grid_intelligence
# 5.0 — Python
uv python pin 3.12
uv venv --seed
uv sync --dev
git add . && git commit -m "Adiciona projeto inicial Declarative Automation Bundle" && git push
# 6.0 — skills oficiais (sem MCP)
databricks aitools install
claude # depois /skills para conferir
# 7.0 — catálogos (um por ambiente)
# Na Free Edition, catalogs create falha com "Metastore storage root URL does
# not exist" — use o SQL abaixo, ou crie pela UI (Catalog > Create catalog).
databricks experimental aitools tools query --profile grid_intelligence \
"CREATE CATALOG IF NOT EXISTS grid_dev"
databricks experimental aitools tools query --profile grid_intelligence \
"CREATE CATALOG IF NOT EXISTS grid_intelligence"
databricks catalogs list --profile grid_intelligence
databricks grants update CATALOG grid_dev \
--json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
--profile grid_intelligence
databricks grants update CATALOG grid_intelligence \
--json '{"changes":[{"principal":"SEU-EMAIL","add":["ALL_PRIVILEGES"]}]}' \
--profile grid_intelligence
# 7.1 — schema, volume e os dados (catalogo > schema > volume, nesta ordem)
databricks schemas create raw grid_dev --profile grid_intelligence
databricks volumes create grid_dev raw landing MANAGED --profile grid_intelligence
databricks fs cp -r landing dbfs:/Volumes/grid_dev/raw/landing \
--overwrite --profile grid_intelligence
databricks fs ls dbfs:/Volumes/grid_dev/raw/landing --profile grid_intelligence
# 9.0 — deploy em dev
databricks bundle validate -t dev --profile grid_intelligence
databricks bundle deploy -t dev --profile grid_intelligence
# a chave do run e a do recurso no YAML. Antes dos prompts so existe o
# sample_job que veio do template; grid_medalhao_job nasce no prompt 04.
databricks bundle summary -t dev --profile grid_intelligence # lista o que existe
databricks bundle run sample_job -t dev --profile grid_intelligence
# 12.0 — produção (pela esteira, nunca na mão)
databricks bundle summary -t prod --profile grid_intelligence
Anexo: quando der erro.
Os sete erros que aparecem em toda turma.
| Erro | Causa | Solução |
|---|---|---|
databricks update não existe |
comando inventado | brew upgrade databricks |
resource with key "x" not found |
o recurso ainda não existe, ou você usou o nome de exibição em vez da chave | databricks bundle summary -t dev lista as chaves disponíveis |
DELTA_FEATURE_REQUIRES_MANUAL_ENABLEMENT: timestampNtz |
timestamp sem fuso horário; o Delta pede a feature habilitada na tabela | TBLPROPERTIES ('delta.feature.timestampNtz' = 'supported') — nunca converter na bronze |
| A bronze ingere zero linha, sem erro | Parquet com timestamp[ns] — o Spark só lê até microssegundo |
regravar o Parquet com precisão us; o gerador do projeto já faz |
fs cp falha com erro de caminho |
falta o prefixo dbfs:, o caminho local está errado, ou o schema/volume não existe |
confira na ordem: catalogs list, schemas list grid_dev, volumes list grid_dev raw |
Metastore storage root URL does not exist |
Free Edition / Default Storage: a API de criar catálogo exige storage_root |
crie por SQL (CREATE CATALOG) ou pela UI, em Catalog → Create catalog |
Catalog 'x' does not exist |
o catálogo do comando não foi criado neste workspace | databricks catalogs list antes de dar grant |
stored credentials from older CLI versions are no longer used |
a CLI foi atualizada e o cache de token do formato antigo virou inválido | databricks auth login --profile grid_intelligence de novo |
Skills não aparecem em /skills |
plugin só carrega no início da sessão | feche e reabra o Claude Code |
Schemas viram seunome_bronze |
mode: development prefixa recursos |
experimental.skip_name_prefix_for_schema: true |
does not have permission to CREATE SCHEMA |
catálogo sem grant | databricks grants update CATALOG ... (bloco 7.0) |
| Instalação de lib falha | Python 3.13 sem wheel do Databricks | uv python pin 3.12 |
Anexo: perguntas da turma.
O que sempre aparece no chat.
Dá para usar GitLab ou Azure DevOps em vez de GitHub?
Dá, e é o mais comum em cliente grande. Muda o comando que cria o repositório e a sintaxe do arquivo de pipeline. O bundle é exatamente o mesmo.
Quem está começando deve aprender na interface primeiro ou já ir de agente?
Não existe resposta certa, mas existe resposta errada — e a errada é ignorar a IA. Na prática: metade do tempo de estudo explorando na mão, para fixar; metade com o agente. No trabalho, agente o tempo todo. O risco de estudar só com agente é a sensação de ter entendido sem ter entendido — ele é mais rápido do que a gente consegue absorver.
Essas skills servem para construir agentes de IA dentro do Databricks?
Elas são skills de desenvolvimento no Databricks, não de construção de agente. Para agente você precisa estudar deploy de agente à parte. Mas este setup habilita: dá para escrever o agente localmente com apoio das skills e publicá-lo no workspace.
Certificação vale a pena?
Vale, principalmente para quem quer entrar em consultoria — empresa precisa de gente certificada para virar parceira. Mas certificação mede decoreba de comando, não capacidade. Estude, faça um projeto, e só então tire a certificação.