From e13768f12d4213407478c3abe642739931e75546 Mon Sep 17 00:00:00 2001 From: yaMimis Date: Tue, 10 Nov 2026 10:41:48 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20sistema=20de=20infra=20do=20terraform?= =?UTF-8?q?=20e=20organiza=C3=A7=C3=A3o=20do=20iniciar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 21 ++++- .github/workflows/ci.yml | 38 +++++++- Dockerfile | 30 ++++--- README.md | 17 +++- docker-compose.yml | 32 +++++-- docs/ROTEIRO_DEMO.md | 61 +++++++++++++ infra/.gitkeep | 0 infra/.terraform.lock.hcl | 23 +++++ infra/README.md | 27 ++++++ infra/main.tf | 114 ++++++++++++++++++++++++ infra/outputs.tf | 7 ++ infra/variables.tf | 33 +++++++ infra/versions.tf | 14 +++ iniciar.ps1 | 160 ++++++++++++++++++++++++++++++---- src/app/componentes.py | 23 +++-- tests/app/test_componentes.py | 10 +++ 16 files changed, 563 insertions(+), 47 deletions(-) create mode 100644 docs/ROTEIRO_DEMO.md delete mode 100644 infra/.gitkeep create mode 100644 infra/.terraform.lock.hcl create mode 100644 infra/README.md create mode 100644 infra/main.tf create mode 100644 infra/outputs.tf create mode 100644 infra/variables.tf create mode 100644 infra/versions.tf diff --git a/.dockerignore b/.dockerignore index 9b52363..3d29ffd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,17 +1,30 @@ -# Controle de versão +# Controle de versão e CI .git/ .gitignore +.github/ # Ambientes virtuais e cache do Python .venv/ +venv/ __pycache__/ *.pyc *.pyo *.pyd .pytest_cache/ +.ipynb_checkpoints/ -# Variáveis de ambiente sensíveis +# Segredos .env -# Dados brutos pesados que não devem ir para a imagem da aplicação -data/raw/ \ No newline at end of file +# Dados brutos e modelo: entram por volume, nunca pela imagem +data/raw/ +modelos_salvos/*.joblib + +# O que a aplicação não usa para rodar (deixa a imagem menor e o build mais rápido) +notebooks/ +tests/ +skills/ +infra/ +docs/figuras/ +docs/bpmn/ +iniciar.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4832ea..4fdf5b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,46 @@ jobs: python -m pip install --upgrade pip pip install -r requirements-dev.txt + # O mesmo SQL que o Docker roda na 1ª criação do banco, em ordem. + # Quebrou um CREATE, uma view ou uma regra da tabela de decisões → CI vermelha. + - name: Aplicar o esquema do banco (db/0*.sql) + env: + PGPASSWORD: postgres + run: | + for f in db/0*.sql; do + echo "== $f" + psql -h localhost -U postgres -d auditor_db -v ON_ERROR_STOP=1 -q -f "$f" + done + + - name: Conferir as regras da tabela de decisões (teste SQL do Marcos) + env: + PGPASSWORD: postgres + run: psql -h localhost -U postgres -d auditor_db -v ON_ERROR_STOP=1 -f db/testes/05_teste_decisoes.sql + # Validações - name: Rodar Bateria de Testes do Pytest env: # Aponta os testes para o banco temporário criado no passo 2 DATABASE_URL: postgresql://postgres:postgres@localhost:5432/auditor_db run: | - pytest tests/ -v \ No newline at end of file + pytest tests/ -v + + build-da-imagem: + name: Build da imagem Docker + runs-on: ubuntu-latest + steps: + - name: Clonar o repositório + uses: actions/checkout@v4 + + - name: Configurar o Buildx + uses: docker/setup-buildx-action@v3 + + # Garante que o Dockerfile continua construindo. Cache do GitHub: só o 1º build é lento. + - name: Construir a imagem (sem publicar) + uses: docker/build-push-action@v6 + with: + context: . + push: false + tags: auditor-preco-justo:ci + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 366382b..3a4d03a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,33 @@ +# syntax=docker/dockerfile:1 +# Imagem única do Auditor de Preço Justo: a mesma serve a API (padrão) e o painel +# (o docker-compose troca o comando). Ordem pensada para o cache: o que muda pouco +# (sistema e bibliotecas) vem antes do código, que muda sempre. FROM python:3.14-slim +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + WORKDIR /app -#instalação de pacotes e libs -RUN apt-get update && apt-get install -y \ +# Pacotes do sistema para compilar o psycopg2 +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ && rm -rf /var/lib/apt/lists/* -# requirements-genai.txt inclui o requirements.txt + busca nas normas (sentence-transformers, -# torch CPU, pgvector) e o cliente do Gemini, usados pelo parecer na API. +# Bibliotecas: requirements-genai.txt inclui o requirements.txt + busca nas normas +# (sentence-transformers, torch CPU, pgvector), Gemini e LangChain. +# O cache do pip fica guardado entre builds (BuildKit): se um requirement mudar, +# só ele é baixado de novo, e não o PyTorch inteiro (~1 GB). COPY requirements.txt requirements-genai.txt ./ -RUN pip install --no-cache-dir -r requirements-genai.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install -r requirements-genai.txt -#copia codigo fonte do projeto local +# Código do projeto (o .dockerignore deixa de fora dados, notebooks, testes e segredos) COPY . . -EXPOSE 8000 +EXPOSE 8000 8501 -#p/ definir o comando que sera iniciado quando o container executar -#servidor uvicorn web, rodando fastapi, na porta 8000 com conexao de fora do container -CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +# Padrão: API FastAPI na porta 8000, aceitando conexões de fora do container +CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index 7564247..7bdfe76 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,16 @@ tests/ testes Pré-requisitos: Git, Python 3.14 e Docker Desktop. +**Atalho (Windows):** com o banco já carregado (passos 2 a 5 abaixo, uma vez), use o `iniciar.ps1`: + +| Comando | O que faz | +| --- | --- | +| `.\iniciar.ps1` | confere `.env` e modelo, sobe o banco, aplica a tabela de decisões, indexa as normas (se faltar), sobe API e painel e abre o navegador. Constrói a imagem só se ela não existir. | +| `.\iniciar.ps1 -Build` | o mesmo, reconstruindo a imagem (mudou código ou requirements) | +| `.\iniciar.ps1 -Parar` | para tudo; os dados do banco ficam guardados | + +O primeiro build leva alguns minutos (PyTorch); os seguintes reaproveitam o cache e uma imagem única serve API e painel. + 1. Clone o repositório e instale as dependências: ``` git clone https://github.com/yaMimis/Auditor-Project.git @@ -103,8 +113,7 @@ Pré-requisitos: Git, Python 3.14 e Docker Desktop. **Normas do parecer** (busca no pgvector), uma vez, com `pip install -r requirements-genai.txt`: ``` - python -m genai.src.dividir - python -m genai.src.indexar + python genai/src/indexar.py ``` Sem esse passo o parecer funciona, mas sai sem fontes. @@ -112,7 +121,7 @@ Pré-requisitos: Git, Python 3.14 e Docker Desktop. ``` docker compose up --build api streamlit ``` - O primeiro build demora mais: a imagem instala o PyTorch (CPU) para a busca nas normas. + O primeiro build demora mais: a imagem instala o PyTorch (CPU) para a busca nas normas. Depois, `docker compose up -d` sobe sem reconstruir; use `--build` só quando mudar código ou requirements. - Painel: http://localhost:8501 (sem o modelo, use os exemplos da aba de revisão) - API: http://localhost:8000/docs (`GET /` informa se o modelo foi carregado) - Para os dados da carteira, o painel lê `docs/resultados/carteira_auditoria.csv` e `equidade.json`. @@ -132,6 +141,8 @@ Pré-requisitos: Git, Python 3.14 e Docker Desktop. - [`notebooks/dados/01_eda.ipynb`](notebooks/dados/01_eda.ipynb): análise exploratória e as figuras dos slides (`docs/figuras/`). - [`docs/bpmn/`](docs/bpmn/): processo em BPMN 2.0. - [`genai/FORMATO_JSON.md`](genai/FORMATO_JSON.md) e [`genai/REGRAS_PARECER.md`](genai/REGRAS_PARECER.md): contrato e regras do parecer. +- [`docs/ROTEIRO_DEMO.md`](docs/ROTEIRO_DEMO.md): roteiro da demo, plano B e perguntas prováveis. +- [`infra/`](infra/README.md): o mesmo ambiente em Terraform (provider Docker, local, sem custo). - [`skills/`](skills/README.md): guia do projeto por setor (estrutura, ambiente, dados, modelo, juro justo, parecer, API, painel, governança). ## Governança e ética diff --git a/docker-compose.yml b/docker-compose.yml index e1f6a38..31a2e3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,9 @@ -#docker compose build (para rodar as imagens) -#docker compose up -d (para rodar o container e colocar o app no ar) +# Auditor de Preço Justo: banco (PostgreSQL + pgvector), API (8000) e painel (8501). +# docker compose up -d sobe tudo (usa a imagem já construída) +# docker compose up -d --build reconstrói a imagem antes (mudou código ou requirements) +# docker compose down para tudo (os dados do banco ficam no volume) +# No Windows, o atalho é o .\iniciar.ps1. + services: postgres: image: pgvector/pgvector:pg16 @@ -12,11 +16,17 @@ services: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - - ./db:/docker-entrypoint-initdb.d + - ./db:/docker-entrypoint-initdb.d # só na 1ª criação do volume (subpastas são ignoradas) + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d auditor_db"] + interval: 5s + timeout: 5s + retries: 20 restart: unless-stopped api: build: . + image: auditor-preco-justo:latest # a mesma imagem serve o painel: um build só container_name: auditor_api ports: - "8000:8000" @@ -29,11 +39,20 @@ services: - ./modelos_salvos:/app/modelos_salvos # pacote_prever_v1.joblib (fora do Git) - cache_modelos_hf:/root/.cache/huggingface # modelo de busca nas normas: baixa uma vez só depends_on: - - postgres + postgres: + condition: service_healthy + healthcheck: + # A 1ª subida baixa o modelo de busca nas normas (~0,5 GB): por isso o start_period longo. + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=5)"] + interval: 10s + timeout: 10s + retries: 6 + start_period: 180s restart: unless-stopped streamlit: - build: . + image: auditor-preco-justo:latest # construída pelo serviço api + pull_policy: never container_name: auditor_streamlit command: streamlit run src/app/main.py --server.port=8501 --server.address=0.0.0.0 ports: @@ -43,7 +62,8 @@ services: volumes: - ./docs/resultados:/app/docs/resultados # carteira_auditoria.csv e equidade.json do lote depends_on: - - api + api: + condition: service_healthy restart: unless-stopped volumes: diff --git a/docs/ROTEIRO_DEMO.md b/docs/ROTEIRO_DEMO.md new file mode 100644 index 0000000..a36c8dc --- /dev/null +++ b/docs/ROTEIRO_DEMO.md @@ -0,0 +1,61 @@ +# Roteiro da demo — Auditor de Preço Justo + +**Versão:** 1.0 · **Data:** 10/11/2026 · **Responsável:** Yasmin (App e infraestrutura) · **Revisor:** grupo · **Status:** rascunho para o ensaio + +Demo ao vivo de cerca de **8 minutos**, no dia 15/11, seguida da mesma sequência gravada em vídeo (plano B). Cada bloco diz o que mostrar na tela, a frase-chave e o ponto do guia que ele prova. + +--- + +## 1. Antes de começar (no dia, 30 min antes) + +- [ ] Docker Desktop aberto; rodar `.\iniciar.ps1` e esperar o "Pronto". +- [ ] Em http://localhost:8000/: `modelo_carregavel: true` e `banco_disponivel: true`. +- [ ] Fazer **uma auditoria de aquecimento** pelo formulário: a 1ª chamada ao Gemini e à busca nas normas é a mais lenta. +- [ ] Preencher "Analista responsável" no topo da seção de auditoria. +- [ ] Navegador em tela cheia, zoom em 90% a 100%, abas abertas: painel (8501) e documentação da API (8000/docs). +- [ ] Vídeo gravado aberto em outra janela, pronto para o plano B. + +## 2. Sequência + +| # | Tempo | Na tela | Frase-chave | Prova | +|---|---|---|---|---| +| 1 | 0:00 | Capa do painel | "Para cada contrato: qual o risco, se o juro é justo para esse risco e se clientes de mesmo risco pagam diferente. Quem decide é o analista." | Ideia e enquadramento | +| 2 | 0:45 | Aba **Revisão de contrato** → formulário (3 etapas) → **Executar auditoria** | "Gênero e idade ficam registrados só para auditoria: não entram no modelo nem no parecer." | Governança: atributos sensíveis só auditam | +| 3 | 1:45 | Cartões, régua e motivos do resultado | "O juro justo vem do fluxo de caixa: custo do dinheiro da época, perda esperada e margem da safra, com margem de incerteza do bootstrap." | Modelo e juro justo | +| 4 | 2:45 | Parecer "Gerado por IA", fontes e referência de mercado do BC | "A IA só escreve. Recomendação e números vêm do código, e cada número do texto é conferido; se falhar, entra o template." | GenAI com checagem | +| 5 | 3:45 | **Caso de exemplo → Acima do justo · 98ffaaa4-0b86** (faixa D: cobrado 39,0%, justo 34,0%, margem 31,7%–36,8%) | "Indício a investigar, não conclusão." | Linguagem de indício | +| 6 | 4:30 | **Caso de exemplo → Revisão manual · 1cb66730-5f46** | "Dados fora da faixa de treino: o sistema não arrisca um número e devolve ao analista." | Revisão manual | +| 7 | 5:00 | Decisão do analista: justificativa + **Revisar preço** com nova taxa | "A decisão é humana e fica gravada no banco, com o parecer e a justificativa." | LGPD art. 20 | +| 8 | 5:45 | Aba **Auditoria da carteira**: cartões, faixas e equidade | "Gênero: −0,53 pp; idade 18–25 × 35–44: +5,45 pp. Os dois intervalos não cruzam o zero: indício a investigar, com as ressalvas de proxies e qualidade do dado." | Auditoria de equidade | +| 9 | 7:00 | Fila de revisão com a coluna **Decisão atual**; **Revisar** abre o contrato | "A carteira inteira vira uma fila priorizada pela distância da margem." | Uso operacional | +| 10 | 7:30 | Aba **Registro de decisões** → Exportar | "Trilha completa: quem decidiu, o quê, por quê e se discordou do sistema." | Rastreabilidade | + +## 3. Plano B + +| Problema na hora | O que fazer | +|---|---| +| Sem internet (Gemini fora) | Seguir: o parecer sai pelo **template de reserva** e o painel mostra isso. Usar como prova do fallback. | +| Modelo não carrega | Usar só **Caso de exemplo** (casos reais já calculados). | +| Banco fora | As decisões ficam na sessão com aviso; mostrar a exportação. | +| Docker não sobe | Passar o **vídeo gravado**. | + +## 4. Perguntas prováveis da banca + +- **"Por que dados da Estônia?"** Não há base pública brasileira de contratos individuais; o Bondora é substituto do comportamento de crédito, e a análise jurídica é hipotética. +- **"O juro justo é absoluto?"** Não: é relativo à safra (a margem iguala a mediana do justo à do cobrado em cada ano). Mede quem está caro ou barato **perto dos pares da mesma época**. +- **"O backtest confirma a auditoria?"** Resposta combinada com o Samuel (pendência abaixo). +- **"E se a IA inventar um número?"** A conferência por código recusa e entra o template. Na avaliação, 40/40 recomendações certas. +- **"Por que país não está na equidade?"** País é controle de risco (o default varia muito entre EE, ES e FI), não grupo auditado. + +## Pendências + +- [ ] Ensaio cronometrado com o grupo +- [ ] Gravar o vídeo (resolução 1080p, com áudio) +- [ ] Combinar com o Samuel a resposta sobre o backtest +- [ ] Definir quem fala em cada bloco + +## Histórico + +| Versão | Data | Mudança | +|---|---|---| +| 1.0 | 10/11/2026 | Primeira versão do roteiro. | diff --git a/infra/.gitkeep b/infra/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl new file mode 100644 index 0000000..1c8f290 --- /dev/null +++ b/infra/.terraform.lock.hcl @@ -0,0 +1,23 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/kreuzwerker/docker" { + version = "3.9.0" + constraints = "~> 3.0" + hashes = [ + "h1:o/2lXPHpHZr4fuVmCgcJu8Dk3/bnL6qp8fr0JM36bBM=", + "zh:0ead8281830e9b9496651282235d9a139ba1b1b6ff79e395eb8c78658dc446b9", + "zh:0f17d37d8d3872df3fb75c68b5272e0c981343f53b506a9675b4405191edd3ef", + "zh:11d50b37323874427c6d2a08b737d3c7707c8301fdd236c94485cf2828d0b14b", + "zh:32f6f9b847446054e2db3d72886ef2f1d1aa51a6d0dac42340b07dad18e3f28f", + "zh:5ea5c67668b5dcbda560dc6104b788a9bfc974d52f02f7886889b77cc0e5d248", + "zh:5fb19a0b07edc344cd3ddeeb9cfb3d183089deb7a6a94a7b22a583aa1712596b", + "zh:602a7ece444e2a142ec5245abb98e7a1a990a68afae2df63b6c85ec084f0c5d7", + "zh:693dce278524ad8a6d6c9dd7a01bcd63bb85189639198f8d0b044ab0e5099401", + "zh:72e9911568103576c6a78fa38841cfd45eeb88ad22a2c649eb140a377a5b3c26", + "zh:956b62b6857cbb467b50158601f01b1203daa34cbd447dcc7f044c327e878b68", + "zh:9d372bac0d4479868b34485fb4966ba7bb525938f818b6a625f4977004ea83f9", + "zh:e06658a51427f9f53dbdb06263406fc1bc56d1a4fb5e7eb660d7cdfc22f596bd", + "zh:eee38dadf672b946419af25160eae7c03fc2afbb14f39f2f1d2a7404d647e2f7", + ] +} diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..ae62e45 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,27 @@ +# infra/ — o ambiente como código (Terraform) + +Descreve com Terraform o mesmo ambiente do `docker-compose.yml`: rede, volumes, banco (PostgreSQL + pgvector), API e painel. Usa o provider **Docker** e roda **no próprio computador**: não precisa de conta em nuvem nem gera custo. É a base para levar o projeto a uma nuvem depois, trocando o provider. + +O dia a dia continua pelo `.\iniciar.ps1` ou pelo `docker compose`. O Terraform é uma alternativa de implantação, não um passo obrigatório. + +## Como usar + +Pré-requisitos: [Terraform](https://developer.hashicorp.com/terraform/install) ≥ 1.6 e Docker Desktop ligado. + +1. Pare o ambiente do compose (usa as mesmas portas): `docker compose down` +2. Construa a imagem da aplicação (uma vez): `docker compose build api` +3. Na pasta `infra/`: + ``` + terraform init + $env:TF_VAR_google_api_key = "" # opcional; sem ela o parecer sai pelo template + terraform apply + ``` + No Linux/macOS, acrescente `-var 'docker_host=unix:///var/run/docker.sock'` (e use `export TF_VAR_google_api_key=...`). +4. Abra os endereços que o `terraform apply` mostra no fim (painel em http://localhost:8501). +5. Para remover tudo: `terraform destroy` (apaga também o volume do banco deste ambiente). + +## Observações + +- Os containers e volumes têm o prefixo `auditor_tf_`, para não se misturar com os do compose. +- O banco começa vazio, como no compose: o esquema de `db/` roda sozinho na 1ª criação; as cargas e a indexação das normas seguem o README da raiz, apontando para a porta 5432. +- A chave do Gemini nunca vai para arquivo: só pela variável de ambiente `TF_VAR_google_api_key`. Os arquivos de estado (`*.tfstate`) ficam fora do Git, pois podem conter a chave. diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..31434c3 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,114 @@ +# Infraestrutura do Auditor de Preço Justo como código. +# Descreve o mesmo ambiente do docker-compose.yml (banco, API e painel) com o provider +# Docker do Terraform, rodando no próprio computador. Para a nuvem, troca-se o provider +# e os recursos de container mantendo a mesma estrutura. + +locals { + raiz = abspath("${path.module}/..") + url_do_banco = "postgresql://postgres:postgres@auditor_tf_postgres:5432/auditor_db" +} + +resource "docker_network" "auditor" { + name = "auditor_tf_rede" +} + +resource "docker_volume" "dados_banco" { + name = "auditor_tf_postgres_data" +} + +resource "docker_volume" "cache_modelos" { + name = "auditor_tf_cache_modelos_hf" +} + +resource "docker_image" "postgres" { + name = "pgvector/pgvector:pg16" + keep_locally = true +} + +# Construída antes pelo compose (o Dockerfile usa recursos do BuildKit). +resource "docker_image" "app" { + name = var.imagem_app + keep_locally = true +} + +resource "docker_container" "postgres" { + name = "auditor_tf_postgres" + image = docker_image.postgres.image_id + restart = "unless-stopped" + env = ["POSTGRES_USER=postgres", "POSTGRES_PASSWORD=postgres", "POSTGRES_DB=auditor_db"] + + networks_advanced { + name = docker_network.auditor.name + } + ports { + internal = 5432 + external = 5433 + } + volumes { + volume_name = docker_volume.dados_banco.name + container_path = "/var/lib/postgresql/data" + } + volumes { + host_path = "${local.raiz}/db" + container_path = "/docker-entrypoint-initdb.d" + read_only = true + } + healthcheck { + test = ["CMD-SHELL", "pg_isready -U postgres -d auditor_db"] + interval = "5s" + timeout = "5s" + retries = 20 + } + wait = true +} + +resource "docker_container" "api" { + name = "auditor_tf_api" + image = docker_image.app.image_id + restart = "unless-stopped" + env = compact([ + "DATABASE_URL=${local.url_do_banco}", + var.google_api_key == "" ? "" : "GOOGLE_API_KEY=${var.google_api_key}", + ]) + + networks_advanced { + name = docker_network.auditor.name + aliases = ["api"] + } + ports { + internal = 8000 + external = var.porta_api + } + volumes { + host_path = "${local.raiz}/modelos_salvos" + container_path = "/app/modelos_salvos" + read_only = true + } + volumes { + volume_name = docker_volume.cache_modelos.name + container_path = "/root/.cache/huggingface" + } + depends_on = [docker_container.postgres] +} + +resource "docker_container" "painel" { + name = "auditor_tf_streamlit" + image = docker_image.app.image_id + restart = "unless-stopped" + command = ["streamlit", "run", "src/app/main.py", "--server.port=8501", "--server.address=0.0.0.0"] + env = ["API_URL=http://api:8000"] + + networks_advanced { + name = docker_network.auditor.name + } + ports { + internal = 8501 + external = var.porta_painel + } + volumes { + host_path = "${local.raiz}/docs/resultados" + container_path = "/app/docs/resultados" + read_only = true + } + depends_on = [docker_container.api] +} diff --git a/infra/outputs.tf b/infra/outputs.tf new file mode 100644 index 0000000..520a70b --- /dev/null +++ b/infra/outputs.tf @@ -0,0 +1,7 @@ +output "painel" { + value = "http://localhost:${var.porta_painel}" +} + +output "api" { + value = "http://localhost:${var.porta_api}/docs" +} diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 0000000..34253de --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,33 @@ +variable "docker_host" { + description = "Onde está o Docker. Windows (Docker Desktop): npipe; Linux/macOS: unix:///var/run/docker.sock" + type = string + default = "npipe:////./pipe/docker_engine" +} + +variable "imagem_app" { + description = "Imagem da aplicação, construída antes com: docker compose build api" + type = string + default = "auditor-preco-justo:latest" +} + +variable "google_api_key" { + description = "Chave do Gemini para o parecer. Passe por TF_VAR_google_api_key; vazia = template de reserva" + type = string + default = "" + sensitive = true +} + +variable "porta_api" { + type = number + default = 8000 +} + +variable "porta_painel" { + type = number + default = 8501 +} + +variable "porta_banco" { + type = number + default = 5432 +} diff --git a/infra/versions.tf b/infra/versions.tf new file mode 100644 index 0000000..ee82e0f --- /dev/null +++ b/infra/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.6" + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0" + } + } +} + +# Docker local: nenhuma conta em nuvem, nenhum custo. +provider "docker" { + host = var.docker_host +} diff --git a/iniciar.ps1 b/iniciar.ps1 index 2ed644f..a594366 100644 --- a/iniciar.ps1 +++ b/iniciar.ps1 @@ -1,26 +1,150 @@ -# 1. criar o .env com a chave do gemini, database url e o cliente id sal pois o .env é ignorado no github -# 2. Para o Postgres local se estiver rodando (evita conflito) -Stop-Service postgresql* -ErrorAction SilentlyContinue +# iniciar.ps1 - sobe o ambiente completo do Auditor de Preco Justo (banco, API e painel). +# +# Uso (PowerShell, na raiz do projeto): +# .\iniciar.ps1 sobe tudo; constroi a imagem so se ela ainda nao existir +# .\iniciar.ps1 -Build reconstroi a imagem antes (mudou codigo ou requirements) +# .\iniciar.ps1 -Parar para tudo (os dados do banco continuam no volume) +# .\iniciar.ps1 -SemNavegador nao abre o navegador no final +# Se o Windows bloquear o script, rode antes, na mesma janela: +# Set-ExecutionPolicy Bypass -Scope Process +# +# PARA RODAR A INTERFACE +# cd C:\caminho\do\Auditor-Project +# git pull +# .\iniciar.ps1 +# CASO GIT PULL TRAGA ALGUMA ATT, APLICAR: +# .\iniciar.ps1 -Build +# PARA FINALIZAR +# .\iniciar.ps1 -Parar + + + +param( + [switch]$Build, + [switch]$Parar, + [switch]$SemNavegador +) + +# "Continue": avisos que o docker escreve no stderr nao derrubam o script; +# cada passo confere o resultado pelo $LASTEXITCODE. +$ErrorActionPreference = "Continue" +Set-Location $PSScriptRoot +$IMAGEM = "auditor-preco-justo:latest" +$inicio = Get-Date + +function Aviso($texto) { Write-Host "[aviso] $texto" -ForegroundColor Yellow } +function Passo($texto) { Write-Host "`n== $texto" -ForegroundColor Cyan } +function Ok($texto) { Write-Host "[ok] $texto" -ForegroundColor Green } +function Parar($texto) { Write-Host "[erro] $texto" -ForegroundColor Red; exit 1 } + +function Saude($container) { + $s = docker inspect -f "{{.State.Health.Status}}" $container 2>$null + return "$s".Trim() +} -# 3. Sobe o container do banco +# 0. Docker instalado e ligado? +docker info *> $null +if ($LASTEXITCODE -ne 0) { Parar "O Docker nao esta rodando. Abra o Docker Desktop e rode o script de novo." } + +if ($Parar) { + docker compose down + Ok "Ambiente parado. Os dados do banco continuam guardados." + exit 0 +} + +# 1. .env (fica fora do Git): cria um modelo se nao existir, sem sobrescrever o seu +Passo "1/6 Conferindo o .env e o modelo" +if (-not (Test-Path ".env")) { + @" +CLIENTE_ID_SAL=TROQUE_PELA_CHAVE_DO_GRUPO +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/auditor_db +GOOGLE_API_KEY=TROQUE_PELA_CHAVE_DO_GEMINI +"@ | Set-Content -Path ".env" -Encoding ascii + Aviso "Criei um .env modelo. Preencha CLIENTE_ID_SAL (chave do grupo) e GOOGLE_API_KEY (peca a Maria)." +} +$env_txt = Get-Content ".env" -Raw +if ($env_txt -notmatch "(?m)^GOOGLE_API_KEY=\S+" -or $env_txt -match "TROQUE_PELA_CHAVE_DO_GEMINI") { + Aviso "GOOGLE_API_KEY nao preenchida: o parecer vai sair pelo template de reserva (sem IA)." +} else { Ok ".env com a chave do Gemini" } + +New-Item -ItemType Directory -Force -Path "modelos_salvos" | Out-Null +if (-not (Test-Path "modelos_salvos\pacote_prever_v1.joblib")) { + Aviso "Falta modelos_salvos\pacote_prever_v1.joblib (peca ao Samuel): o formulario nao calcula; use 'Caso de exemplo'." +} else { Ok "modelo em modelos_salvos\" } + +# 2. Banco: sobe e espera o healthcheck do compose +Passo "2/6 Subindo o banco" docker compose up -d postgres +if ($LASTEXITCODE -ne 0) { + Parar "O banco nao subiu. Se a porta 5432 estiver ocupada por um PostgreSQL local, pare o servico dele (services.msc) ou use o docker-compose.override.yml com a porta 5433." +} +$pronto = $false +foreach ($i in 1..30) { + if ((Saude "auditor_postgres") -eq "healthy") { $pronto = $true; break } + Start-Sleep -Seconds 2 +} +if (-not $pronto) { Parar "O banco nao respondeu em 60 s. Veja: docker logs auditor_postgres --tail 30" } +Ok "banco pronto" -# 4. Aguarda 3 segundos para o banco inicializar totalmente -Start-Sleep -Seconds 3 +# 3. Tabela de decisoes do analista (pode reaplicar sem problema) +Passo "3/6 Aplicando a tabela de decisoes" +docker cp "db\05_decisoes_analista.sql" auditor_postgres:/tmp/05.sql | Out-Null +docker exec auditor_postgres psql -q -U postgres -d auditor_db -v ON_ERROR_STOP=1 -f /tmp/05.sql *> $null +if ($LASTEXITCODE -ne 0) { Parar "Erro ao aplicar db\05_decisoes_analista.sql. Rode o comando a mao para ver a mensagem." } +Ok "tabela decisao_analista" -# 5. Aplica as tabelas atualizadas do Marcos -docker cp db\05_decisoes_analista.sql auditor_postgres:/tmp/05.sql -docker exec auditor_postgres psql -U postgres -d auditor_db -v ON_ERROR_STOP=1 -f /tmp/05.sql +$contratos = docker exec auditor_postgres psql -tA -U postgres -d auditor_db -c "SELECT count(*) FROM emprestimo" 2>$null +if ("$contratos".Trim() -eq "0") { + Aviso "O banco esta sem os contratos (cargas do README, passo 5). O painel funciona, mas sem a referencia de mercado do BC." +} -# 6. Indexa as normas jurídicas no pgvector -python genai/src/indexar.py +# 4. Imagem: so constroi se nao existir ou se pedirem -Build +Passo "4/6 Imagem da aplicacao" +docker image inspect $IMAGEM *> $null +if ($Build -or $LASTEXITCODE -ne 0) { + Write-Host "Construindo a imagem (a primeira vez leva alguns minutos: PyTorch e bibliotecas)..." + docker compose build api + if ($LASTEXITCODE -ne 0) { Parar "Erro no build da imagem. Role a tela para ver o motivo." } + Ok "imagem construida" +} else { + Ok "imagem ja existe (use -Build se mudou codigo ou requirements)" +} -# 7. Sobe a API e a Interface -docker compose up --build api streamlit +# 5. Normas do parecer no pgvector (dentro do container: nao precisa de Python local) +Passo "5/6 Normas do parecer" +$trechos = docker exec auditor_postgres psql -tA -U postgres -d auditor_db -c "SELECT count(*) FROM trechos_normas" 2>$null +if ("$trechos".Trim() -match "^[1-9]") { + Ok "normas ja indexadas ($("$trechos".Trim()) trechos)" +} else { + docker compose run --rm --no-deps api python genai/src/indexar.py + if ($LASTEXITCODE -ne 0) { Aviso "Nao consegui indexar as normas: o parecer funciona, mas sai sem fontes." } + else { Ok "normas indexadas" } +} -#para rodar esse script no powershell adm -# cd C:\Users\Aluno\Desktop\sua-pasta\Auditor-Project -# .\iniciar.ps1 +# 6. API e painel em segundo plano; espera a API ficar saudavel +Passo "6/6 Subindo a API e o painel" +docker compose up -d api streamlit +if ($LASTEXITCODE -ne 0) { Parar "Erro ao subir a API ou o painel. Veja: docker compose logs api --tail 30" } +Write-Host -NoNewline "Aguardando a API (a primeira subida baixa o modelo de busca nas normas)" +$pronto = $false +foreach ($i in 1..48) { + $s = Saude "auditor_api" + if ($s -eq "healthy") { $pronto = $true; break } + if ($s -eq "unhealthy") { break } + Write-Host -NoNewline "." + Start-Sleep -Seconds 5 +} +Write-Host "" +if (-not $pronto) { Parar "A API nao ficou pronta. Veja: docker logs auditor_api --tail 40" } + +$status = (Invoke-WebRequest -UseBasicParsing http://localhost:8000/ -TimeoutSec 10).Content | ConvertFrom-Json +if ($status.modelo_carregavel) { Ok "modelo carregavel" } else { Aviso "modelo nao encontrado em modelos_salvos\" } +if ($status.banco_disponivel) { Ok "banco disponivel" } else { Aviso "a API nao ve o banco ou a tabela de decisoes" } -#caso de erro de segurança -# Set-ExecutionPolicy Unrestricted -Scope Process \ No newline at end of file +$min = [math]::Round(((Get-Date) - $inicio).TotalMinutes, 1) +Write-Host "`nPronto em $min min." -ForegroundColor Green +Write-Host " Painel: http://localhost:8501" +Write-Host " API: http://localhost:8000/docs" +Write-Host " Logs: docker compose logs -f api streamlit" +Write-Host " Parar: .\iniciar.ps1 -Parar" +if (-not $SemNavegador) { Start-Process "http://localhost:8501" } diff --git a/src/app/componentes.py b/src/app/componentes.py index 94a56e1..ed3091f 100644 --- a/src/app/componentes.py +++ b/src/app/componentes.py @@ -127,10 +127,23 @@ def html_kpi(rotulo: str, valor: str, unidade: str = "", sub: str = "", extra: s """) +def casas_juros(r: dict) -> int: + """Casas decimais dos juros: 1; ou 2 quando dois juros diferentes ficariam iguais com 1 casa + (mesma regra do parecer, genai/src/checagem.py: evita "30,6% abaixo de 30,6%").""" + juros = [v for v in (r.get("juro_cobrado"), r.get("juro_justo"), r.get("juro_justo_min"), + r.get("juro_justo_max")) if v is not None] + for i, a in enumerate(juros): + for b in juros[i + 1:]: + if abs(a - b) > 1e-9 and pct(a) == pct(b): + return 2 + return 1 + + def kpis_revisao(r: dict) -> list[str]: """Os 4 cartões do topo: risco, juro cobrado, juro justo e situação.""" situacao = r.get("situacao_preco", "revisao_manual") s = SITUACOES.get(situacao, SITUACOES["revisao_manual"]) + casas = casas_juros(r) ic = r.get("pd_ic95") ic_txt = f"IC 95%: {pct(ic[0])} – {pct(ic[1])}" if ic else "Chance estimada de não pagar" faixa = r.get("faixa") @@ -140,7 +153,7 @@ def kpis_revisao(r: dict) -> list[str]: if r.get("juro_justo") is None: justo_sub = "Não calculado: dados fora da faixa de treino" else: - justo_sub = f"Margem de incerteza: {pct(r.get('juro_justo_min'))} – {pct(r.get('juro_justo_max'))}" + justo_sub = f"Margem de incerteza: {pct(r.get('juro_justo_min'), casas)} – {pct(r.get('juro_justo_max'), casas)}" if r.get("juro_pares_mediana") is not None: # segunda opinião: não muda a situação justo_sub += f"
Mediana dos pares: {pct(r['juro_pares_mediana'])}" @@ -156,8 +169,8 @@ def kpis_revisao(r: dict) -> list[str]: return [ html_kpi("Risco de inadimplência", pd_valor, "", esc(ic_txt), extra=f'
{faixa_html}
'), - html_kpi("Juro cobrado", pct(r.get("juro_cobrado")), UNIDADE_JURO, "Taxa do contrato"), - html_kpi("Juro justo calculado", pct(r.get("juro_justo")), UNIDADE_JURO if r.get("juro_justo") else "", + html_kpi("Juro cobrado", pct(r.get("juro_cobrado"), casas), UNIDADE_JURO, "Taxa do contrato"), + html_kpi("Juro justo calculado", pct(r.get("juro_justo"), casas), UNIDADE_JURO if r.get("juro_justo") else "", justo_sub), # justo_sub só tem números formatados por pct() e texto fixo html_kpi("Situação do preço", f'{s["icone"]} {esc(s["rotulo"])}', "", esc(sit_sub), destaque=s), ] @@ -189,9 +202,9 @@ def html_barra_juros(r: dict) -> str:
-
Justo {pct(justo)}
+
Justo {pct(justo, casas_juros(r))}
-
Cobrado {pct(cobrado)}
+
Cobrado {pct(cobrado, casas_juros(r))}
{marcas}
diff --git a/tests/app/test_componentes.py b/tests/app/test_componentes.py index 48f5188..9030f13 100644 --- a/tests/app/test_componentes.py +++ b/tests/app/test_componentes.py @@ -115,3 +115,13 @@ def test_parecer_mostra_referencia_do_bc_so_quando_existe(): html = html_parecer({**base, "contexto_mercado": frase}) assert "Referência de mercado (Brasil)" in html and "<b>" in html # texto escapado assert "Referência de mercado (Brasil)" not in html_parecer({**base, "contexto_mercado": None}) + + +def test_duas_casas_quando_os_juros_ficariam_iguais(): + """Caso C07 da avaliação do parecer: cobrado 30,56% e limite 30,58% não podem virar 30,6% e 30,6%.""" + r = dict(POR_SITUACAO["abaixo_do_justo"], juro_cobrado=0.3056, juro_justo=0.33, juro_justo_min=0.3058, + juro_justo_max=0.36) + cartoes = kpis_revisao(r) + assert "30,56%" in cartoes[1] and "30,58%" in cartoes[2] + assert "Cobrado 30,56%" in html_barra_juros(r) + assert "24,9%" in kpis_revisao(POR_SITUACAO["acima_do_justo"])[1] # caso normal: 1 casa