diff --git a/docs/arquitetura/fluxo-de-dados.md b/docs/arquitetura/fluxo-de-dados.md index 9dbe9a5..8ce29a8 100644 --- a/docs/arquitetura/fluxo-de-dados.md +++ b/docs/arquitetura/fluxo-de-dados.md @@ -1,48 +1,53 @@ # Fluxo de Dados -## Pipeline Diário +## Pipeline -O pipeline de dados é executado diariamente às **4AM UTC** (1AM Brasília) via GitHub Actions. +O pipeline de dados é composto por dois estágios independentes: + +1. **Scraping** (repo `scraper`): Via Airflow DAGs, a cada 15 minutos +2. **Enrichment** (repo `data-platform`): Via GitHub Actions, diário às 4AM UTC ### Diagrama de Sequência Completo ```mermaid sequenceDiagram - participant GH as GitHub Actions - participant SC as Scraper Container + participant AF as Airflow DAGs + participant API as Scraper API (Cloud Run) participant GOV as Sites gov.br participant EBC as Sites EBC participant PG as PostgreSQL + participant GH as GitHub Actions participant CF as Cogfy API participant EMB as Embeddings API participant TS as Typesense - participant AF as Airflow (6AM UTC) + participant AF2 as Airflow (6AM UTC) participant HF as HuggingFace rect rgb(227, 242, 253) - Note over GH,SC: ETAPA 1: Scraping gov.br - GH->>SC: Trigger main-workflow (4AM UTC) - SC->>GOV: Requisições HTTP (160+ sites) - GOV-->>SC: HTML das páginas - SC->>SC: Parse HTML → Markdown - SC->>SC: Gera unique_id (MD5) - SC->>PG: postgres.insert(new_articles) + Note over AF,API: ETAPA 1: Scraping (a cada 15min, via Airflow) + AF->>API: POST /scrape/agencies + API->>GOV: Requisições HTTP (~155 sites) + GOV-->>API: HTML das páginas + API->>API: Parse HTML → Markdown + API->>API: Gera unique_id (MD5) + API->>PG: postgres.insert(new_articles) end rect rgb(255, 243, 224) - Note over SC,EBC: ETAPA 2: Scraping EBC - SC->>EBC: Requisições HTTP (sites EBC) - EBC-->>SC: HTML das páginas - SC->>SC: Parse especializado EBC - SC->>PG: postgres.insert(ebc_articles, allow_update=True) + Note over AF,EBC: ETAPA 2: Scraping EBC (a cada 15min) + AF->>API: POST /scrape/ebc + API->>EBC: Requisições HTTP (sites EBC) + EBC-->>API: HTML das páginas + API->>API: Parse especializado EBC + API->>PG: postgres.insert(ebc_articles, allow_update=True) end rect rgb(255, 253, 231) - Note over SC,CF: ETAPA 3: Upload para Cogfy - SC->>PG: get_news(date_range) - PG-->>SC: Registros - SC->>CF: POST /records (batch 1000) - CF-->>SC: IDs dos registros + Note over GH,CF: ETAPA 3: Upload para Cogfy (diário 4AM UTC) + GH->>PG: get_news(date_range) + PG-->>GH: Registros + GH->>CF: POST /records (batch 1000) + CF-->>GH: IDs dos registros end rect rgb(232, 245, 233) @@ -53,37 +58,36 @@ sequenceDiagram end rect rgb(252, 228, 236) - Note over SC,PG: ETAPA 5: Enriquecimento - SC->>CF: GET /records (busca por unique_id) - CF-->>SC: themes + summary - SC->>SC: Mapeia códigos → labels - SC->>SC: Calcula most_specific_theme - SC->>PG: postgres.update(enriched_data) + Note over GH,PG: ETAPA 5: Enriquecimento + GH->>CF: GET /records (busca por unique_id) + CF-->>GH: themes + summary + GH->>GH: Mapeia códigos → labels + GH->>GH: Calcula most_specific_theme + GH->>PG: postgres.update(enriched_data) end rect rgb(255, 248, 225) - Note over SC,EMB: ETAPA 6: Embeddings - SC->>PG: get_news_without_embeddings() - PG-->>SC: Notícias sem vetores - SC->>EMB: POST /embed (batch 100) - EMB-->>SC: Vetores 768-dim - SC->>PG: postgres.update(embeddings) + Note over GH,EMB: ETAPA 6: Embeddings + GH->>PG: get_news_without_embeddings() + PG-->>GH: Notícias sem vetores + GH->>EMB: POST /embed (batch 100) + EMB-->>GH: Vetores 768-dim + GH->>PG: postgres.update(embeddings) end rect rgb(243, 229, 245) Note over TS,PG: ETAPA 7: Indexação Typesense - GH->>TS: Trigger typesense-sync - TS->>PG: iter_news_for_typesense() - PG-->>TS: Batches de 5000 - TS->>TS: Upsert documentos + GH->>PG: iter_news_for_typesense() + PG-->>GH: Batches de 5000 + GH->>TS: Upsert documentos end rect rgb(225, 245, 254) - Note over AF,HF: ETAPA 8: Sync HuggingFace - AF->>PG: Query novos registros - PG-->>AF: Registros do dia anterior - AF->>AF: Cria parquet shard - AF->>HF: Upload shard + Note over AF2,HF: ETAPA 8: Sync HuggingFace + AF2->>PG: Query novos registros + PG-->>AF2: Registros do dia anterior + AF2->>AF2: Cria parquet shard + AF2->>HF: Upload shard end ``` @@ -91,41 +95,21 @@ sequenceDiagram ### Etapa 1: Scraping gov.br -**Workflow**: `main-workflow.yaml` → job `scraper` +**Repo**: `scraper` — via Airflow DAGs (a cada 15 min) -```bash -data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD -``` +- ~158 DAGs dinâmicas chamam `POST /scrape/agencies` na Scraper API (Cloud Run) +- Cada agência é raspada independentemente +- Parse HTML → Markdown, gera `unique_id = MD5(agency + published_at + title)` +- Insert direto no PostgreSQL -**Processo**: - -1. Carrega URLs de `src/data_platform/scrapers/site_urls.yaml` (~160+ URLs) -2. Para cada URL, instancia `WebScraper` -3. Navega por páginas com paginação (`?b_start:int=N`) -4. Extrai campos: title, date, url, image, category, tags -5. Faz fetch do conteúdo completo de cada notícia -6. Converte HTML → Markdown com `markdownify` -7. Gera `unique_id = MD5(agency + published_at + title)` -8. Insere no PostgreSQL via `PostgresManager.insert()` - -**Retry Logic**: -```python -@retry(tries=5, delay=2, backoff=3, jitter=(1,3)) -def fetch_page(url): ... -``` +→ Veja [Módulo Scraper](../modulos/scraper.md) para detalhes. ### Etapa 2: Scraping EBC -**Workflow**: `main-workflow.yaml` → job `ebc-scraper` - -```bash -data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update -``` - -**Diferenças**: +**Repo**: `scraper` — via Airflow DAG `scrape_ebc` +- DAG chama `POST /scrape/ebc` na Scraper API - Scraper especializado (`EBCWebScraper`) -- Estrutura HTML diferente dos sites gov.br - `allow_update=True` permite sobrescrever registros existentes ### Etapa 3: Upload para Cogfy @@ -303,15 +287,12 @@ data-platform sync-typesense --start-date YYYY-MM-DD ## Execução Manual -### Scraping de período específico +### Enrichment de período específico ```bash -# Via CLI -data-platform scrape --start-date 2024-01-01 --end-date 2024-01-31 - -# Via GitHub Actions -gh workflow run main-workflow.yaml \ - -f start-date=2024-01-01 \ - -f end-date=2024-01-31 +# Via GitHub Actions (enrichment pipeline) +gh workflow run main-workflow.yaml -R destaquesgovbr/data-platform \ + -f start_date=2024-01-01 \ + -f end_date=2024-01-31 ``` ### Enriquecimento manual diff --git a/docs/modulos/data-platform.md b/docs/modulos/data-platform.md index 7d9d277..36bf60c 100644 --- a/docs/modulos/data-platform.md +++ b/docs/modulos/data-platform.md @@ -7,9 +7,8 @@ Repositório centralizado para toda a infraestrutura de dados do DestaquesGovBr. ## Visão Geral -O **data-platform** unifica toda a lógica de dados que anteriormente estava distribuída em múltiplos repositórios (`scraper`, `typesense`). Ele é responsável por: +O **data-platform** é responsável por enriquecimento, embeddings e armazenamento de dados do DestaquesGovBr. A coleta (scraping) é feita pelo repo standalone [scraper](https://github.com/destaquesgovbr/scraper). -- **Coleta**: Scrapers para gov.br e EBC - **Armazenamento**: Gerenciamento do PostgreSQL (fonte de verdade) e HuggingFace (distribuição) - **Enriquecimento**: Integração com Cogfy para classificação temática e sumários - **Embeddings**: Geração de vetores para busca semântica @@ -19,9 +18,9 @@ O **data-platform** unifica toda a lógica de dados que anteriormente estava dis ```mermaid graph TB - subgraph "Coleta" - S1[Scraper Gov.br] - S2[Scraper EBC] + subgraph "Coleta (repo scraper)" + S[Scraper API
Cloud Run] + DAG_S[DAGs Airflow
~158 agências + EBC] end subgraph "Armazenamento" @@ -29,17 +28,17 @@ graph TB HF[(HuggingFace
Dados Abertos)] end - subgraph "Processamento" + subgraph "Processamento (data-platform)" COG[Cogfy
Enriquecimento] EMB[Embeddings API
Vetores 768-dim] end - subgraph "Indexação" + subgraph "Indexação (data-platform)" TS[Typesense
Busca] end - S1 --> PG - S2 --> PG + DAG_S -->|HTTP POST| S + S -->|INSERT| PG PG --> COG COG --> PG PG --> EMB @@ -56,10 +55,7 @@ data-platform/ │ ├── managers/ # Gerenciadores de storage │ │ ├── postgres_manager.py # Acesso ao PostgreSQL │ │ ├── dataset_manager.py # Acesso ao HuggingFace -│ │ └── storage_adapter.py # Abstração dual-write -│ ├── scrapers/ # Scrapers de notícias -│ │ ├── scrape_manager.py # Gov.br -│ │ └── ebc_scrape_manager.py +│ │ └── storage_adapter.py │ ├── cogfy/ # Integração Cogfy │ │ ├── cogfy_manager.py │ │ ├── upload_manager.py @@ -69,8 +65,9 @@ data-platform/ │ │ ├── collection.py │ │ └── indexer.py │ ├── jobs/ # Jobs de processamento +│ │ ├── enrichment/ +│ │ ├── embeddings/ │ │ ├── typesense/sync_job.py -│ │ ├── embeddings/embedding_generator.py │ │ └── hf_sync/ │ ├── models/ # Modelos Pydantic │ │ └── news.py @@ -84,16 +81,6 @@ data-platform/ ## CLI - Comandos Disponíveis -### Scraping - -```bash -# Raspar sites gov.br -data-platform scrape --start-date 2025-01-01 --end-date 2025-01-31 - -# Raspar sites EBC (Agência Brasil, TV Brasil) -data-platform scrape-ebc --start-date 2025-01-01 -``` - ### Enriquecimento ```bash @@ -134,43 +121,6 @@ data-platform typesense-delete --confirm data-platform sync-hf --start-date 2025-01-01 ``` -## Storage Adapter - -O **StorageAdapter** é a camada de abstração que permite transição gradual entre backends de armazenamento. - -### Modos de Operação - -| Modo | Descrição | -|------|-----------| -| `POSTGRES` | Escreve apenas no PostgreSQL | -| `HUGGINGFACE` | Escreve apenas no HuggingFace (legado) | -| `DUAL_WRITE` | Escreve em ambos para transição segura | - -### Configuração - -```bash -# Variáveis de ambiente -STORAGE_BACKEND=postgres # postgres | huggingface | dual_write -STORAGE_READ_FROM=postgres # De onde ler os dados -``` - -### Uso no Código - -```python -from data_platform.managers.storage_adapter import StorageAdapter, StorageBackend - -adapter = StorageAdapter( - backend=StorageBackend.DUAL_WRITE, - read_from=StorageBackend.POSTGRES -) - -# Insert transparente - escreve em ambos -inserted = adapter.insert(news_list, allow_update=False) - -# Read - sempre do backend configurado em read_from -news = adapter.get(filters={"agency_key": "mec"}, limit=100) -``` - ## PostgresManager Gerenciador de acesso ao PostgreSQL com connection pooling e cache. @@ -226,17 +176,13 @@ COGFY_COLLECTION_ID=xxx # Embeddings EMBEDDINGS_API_URL=https://embeddings-api-xxx.run.app EMBEDDINGS_API_KEY=xxx - -# Storage -STORAGE_BACKEND=postgres -STORAGE_READ_FROM=postgres ``` ## Workflows GitHub Actions | Workflow | Trigger | Descrição | |----------|---------|-----------| -| `main-workflow.yaml` | Diário (4AM UTC) | Pipeline completo: scrape → enrich → embed → sync | +| `main-workflow.yaml` | Diário (4AM UTC) | Pipeline de enrichment: upload-cogfy → enrich → embed → sync | | `typesense-maintenance-sync.yaml` | Diário (10AM UTC) | Sync incremental Typesense | | `composer-deploy-dags.yaml` | Push | Deploy de DAGs no Airflow | diff --git a/docs/modulos/scraper.md b/docs/modulos/scraper.md index cffeff3..5c21b4f 100644 --- a/docs/modulos/scraper.md +++ b/docs/modulos/scraper.md @@ -1,54 +1,152 @@ -# Módulo: Scraper (Arquivado) +# Módulo: Scraper -!!! warning "Módulo Migrado" - Este módulo foi migrado para o repositório **data-platform**. - O repositório `scraper` foi arquivado. - - **Novo repositório**: [github.com/destaquesgovbr/data-platform](https://github.com/destaquesgovbr/data-platform) +!!! info "Repositório" + **GitHub**: [destaquesgovbr/scraper](https://github.com/destaquesgovbr/scraper) --- -## Documentação Atualizada +## Visão Geral + +O **scraper** é um repositório standalone que coleta notícias de ~155 agências governamentais (gov.br) e da EBC (Agência Brasil, TV Brasil, etc.). + +A coleta é orquestrada por **DAGs Airflow** (Cloud Composer) que chamam uma **API FastAPI** hospedada no Cloud Run. Os dados são inseridos diretamente no PostgreSQL (insert-only). + +``` +DAGs Airflow (a cada 15min) + → HTTP POST para Cloud Run API + → Scraper faz fetch do site gov.br + → Parse HTML → Markdown + → INSERT no PostgreSQL +``` + +## Arquitetura + +```mermaid +graph TB + subgraph "Cloud Composer" + DAG1[~158 DAGs scrape_agency] + DAG2[scrape_ebc] + end + + subgraph "Cloud Run" + API[Scraper API
FastAPI] + end + + subgraph "Armazenamento" + PG[(PostgreSQL
Cloud SQL)] + end + + subgraph "Sites" + GOV[~155 sites gov.br] + EBC[Sites EBC] + end + + DAG1 -->|HTTP POST| API + DAG2 -->|HTTP POST| API + API --> GOV + API --> EBC + API -->|INSERT| PG +``` + +## Estrutura do Repositório + +``` +scraper/ +├── src/govbr_scraper/ +│ ├── api.py # FastAPI (Cloud Run) +│ ├── scrapers/ +│ │ ├── webscraper.py # Scraper gov.br +│ │ ├── scrape_manager.py # Coordenador gov.br +│ │ ├── ebc_webscraper.py # Scraper EBC +│ │ ├── ebc_scrape_manager.py # Coordenador EBC +│ │ └── config/ +│ │ └── site_urls.yaml # URLs por agência +│ ├── storage/ +│ │ ├── storage_adapter.py +│ │ └── postgres_manager.py +│ └── models/ +│ └── news.py # Modelos Pydantic +├── dags/ # DAGs Airflow +│ ├── scrape_agencies.py # ~158 DAGs dinâmicas +│ ├── scrape_ebc.py # 1 DAG EBC +│ └── config/ +│ └── site_urls.yaml +├── docker/Dockerfile +└── .github/workflows/ + ├── scraper-api-deploy.yaml + ├── composer-deploy-dags.yaml + └── tests.yaml +``` + +## API Endpoints + +A API roda no Cloud Run e é chamada pelas DAGs Airflow. + +| Método | Endpoint | Descrição | +|--------|----------|-----------| +| `POST` | `/scrape/agencies` | Raspa sites gov.br (aceita lista de agências, datas) | +| `POST` | `/scrape/ebc` | Raspa sites EBC | +| `GET` | `/health` | Health check | + +### Request: `/scrape/agencies` + +```json +{ + "start_date": "2025-01-01", + "end_date": "2025-01-02", + "agencies": ["mec", "mds"], + "allow_update": false, + "sequential": true +} +``` -A funcionalidade de scraping agora faz parte do repositório unificado `data-platform`. Consulte: +## DAGs Airflow -- **[Data Platform](data-platform.md)** - Visão geral do repositório unificado -- **[Fluxo de Dados](../arquitetura/fluxo-de-dados.md)** - Pipeline completo de coleta e enriquecimento -- **[PostgreSQL](../arquitetura/postgresql.md)** - Banco de dados central (fonte de verdade) +| DAG | Quantidade | Schedule | Descrição | +|-----|-----------|----------|-----------| +| `scrape_{agency_key}` | ~158 | A cada 15min | 1 DAG por agência gov.br | +| `scrape_ebc` | 1 | A cada 15min | Sites EBC | -## Principais Mudanças +Cada DAG: -| Antes (scraper) | Agora (data-platform) | -|-----------------|----------------------| -| Repositório separado | Repositório unificado | -| HuggingFace como fonte de verdade | PostgreSQL como fonte de verdade | -| `python src/main.py scrape` | `data-platform scrape` | -| DatasetManager (HF) | PostgresManager + StorageAdapter | +- Chama a API no Cloud Run via HTTP POST +- Retry: 2x com backoff de 5min +- Timeout: 15min por execução -## CLI Atual +→ Veja [Airflow DAGs](../workflows/airflow-dags.md) para detalhes de deploy e configuração. + +## Variáveis de Ambiente ```bash -# Raspagem de sites gov.br -data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD +DATABASE_URL=postgresql://... # PostgreSQL connection string +STORAGE_BACKEND=postgres # Sempre postgres +LOG_LEVEL=INFO # Loguru level +``` + +## Deploy + +| Componente | Destino | Workflow | +|-----------|---------|----------| +| **API** | Cloud Run | `scraper-api-deploy.yaml` | +| **DAGs** | Cloud Composer (`{bucket}/scraper/`) | `composer-deploy-dags.yaml` | -# Raspagem de sites EBC -data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD +→ Veja [Docker Builds](../workflows/docker-builds.md) para detalhes de build da imagem. + +## Desenvolvimento Local + +```bash +# Instalar dependências +poetry install -# Upload para Cogfy (enriquecimento) -data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD +# Rodar testes +poetry run pytest -# Buscar enriquecimento do Cogfy -data-platform enrich --start-date YYYY-MM-DD --end-date YYYY-MM-DD +# Rodar API localmente +poetry run uvicorn govbr_scraper.api:app --reload ``` -## Arquivos Principais (Novo Local) +## Links Relacionados -| Componente | Localização | -|------------|-------------| -| WebScraper | `src/data_platform/scrapers/webscraper.py` | -| EBCWebScraper | `src/data_platform/scrapers/ebc_webscraper.py` | -| ScrapeManager | `src/data_platform/scrapers/scrape_manager.py` | -| PostgresManager | `src/data_platform/managers/postgres_manager.py` | -| StorageAdapter | `src/data_platform/managers/storage_adapter.py` | -| site_urls.yaml | `src/data_platform/scrapers/site_urls.yaml` | -| agencies.yaml | `src/data_platform/scrapers/agencies.yaml` | +- **[Data Platform](data-platform.md)** — Enriquecimento, embeddings e indexação (pipeline pós-scraping) +- **[Pipeline de Dados](../workflows/scraper-pipeline.md)** — Visão completa do pipeline scraping + enrichment +- **[PostgreSQL](../arquitetura/postgresql.md)** — Banco de dados central diff --git a/docs/onboarding/setup-backend.md b/docs/onboarding/setup-backend.md index bf36295..f11b950 100644 --- a/docs/onboarding/setup-backend.md +++ b/docs/onboarding/setup-backend.md @@ -170,23 +170,11 @@ python -c "from src.data_platform.managers.postgres_manager import PostgresManag --- -## 6. Executar o Scraper Localmente +## 6. Scraper (Repo Separado) -### Scraping de um período específico - -```bash -# Raspar notícias dos últimos 7 dias -data-platform scrape --start-date $(date -v-7d +%Y-%m-%d) --end-date $(date +%Y-%m-%d) - -# Raspar período específico -data-platform scrape --start-date 2024-12-01 --end-date 2024-12-03 -``` - -### Scraping EBC - -```bash -data-platform scrape-ebc --start-date 2024-12-01 --end-date 2024-12-03 --allow-update -``` +!!! note "Scraper Standalone" + O scraping agora é feito pelo repositório separado [destaquesgovbr/scraper](https://github.com/destaquesgovbr/scraper), via API FastAPI + DAGs Airflow. + Veja [Módulo Scraper](../modulos/scraper.md) para setup local. --- @@ -296,12 +284,6 @@ docker run --env-file .env data-platform data-platform --help # Ver ajuda data-platform --help -# Scraping gov.br -data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD - -# Scraping EBC -data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update - # Upload para Cogfy data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD diff --git a/docs/workflows/airflow-dags.md b/docs/workflows/airflow-dags.md index 64206c2..9e13781 100644 --- a/docs/workflows/airflow-dags.md +++ b/docs/workflows/airflow-dags.md @@ -1,6 +1,6 @@ # Airflow DAGs (Cloud Composer) -O projeto utiliza **Cloud Composer 3** (Apache Airflow gerenciado) para orquestração de pipelines de dados, especialmente a sincronização entre PostgreSQL e HuggingFace. +O projeto utiliza **Cloud Composer 3** (Apache Airflow gerenciado) para orquestração de pipelines de dados: scraping de notícias (repo `scraper`) e sincronização entre PostgreSQL e HuggingFace (repo `data-platform`). !!! info "Cloud Composer" **Ambiente**: `destaquesgovbr-composer` @@ -18,11 +18,16 @@ graph TB TRIGGER[Triggerer] end - subgraph "DAGs" + subgraph "DAGs (data-platform)" DAG1[sync_postgres_to_huggingface] DAG2[test_postgres_connection] end + subgraph "DAGs (scraper)" + DAG3[~158x scrape_agency] + DAG4[scrape_ebc] + end + subgraph "Recursos GCP" PG[(PostgreSQL
Cloud SQL)] SM[Secret Manager] @@ -37,13 +42,66 @@ graph TB SCHED --> WORKER WORKER --> DAG1 WORKER --> DAG2 + WORKER --> DAG3 + WORKER --> DAG4 DAG1 --> PG DAG1 --> HF + DAG3 -->|HTTP POST| CR[Cloud Run
Scraper API] + DAG4 -->|HTTP POST| CR + CR --> PG SM --> WORKER ``` +## Organização de DAGs no Bucket + +O Composer armazena DAGs de múltiplos repos em subdiretórios do mesmo bucket: + +``` +gs://{COMPOSER_BUCKET}/dags/ +├── data-platform/ # DAGs do repo data-platform +│ ├── sync_postgres_to_huggingface.py +│ └── test_postgres_connection.py +└── scraper/ # DAGs do repo scraper + ├── scrape_agencies.py # ~158 DAGs dinâmicas + ├── scrape_ebc.py + └── config/ + └── site_urls.yaml +``` + +Cada repo tem seu próprio workflow `composer-deploy-dags.yaml` que faz `gsutil rsync` para seu subdiretório. + ## DAGs Disponíveis +### DAGs do Scraper (repo `scraper`) + +#### `scrape_{agency_key}` (~158 DAGs dinâmicas) + +Cada agência gov.br gera uma DAG de scraping independente. + +| Configuração | Valor | +|--------------|-------| +| **Schedule** | `*/15 * * * *` (a cada 15 min) | +| **Catchup** | Desabilitado | +| **Retries** | 2 (com backoff de 5 min) | +| **Timeout** | 15 min | + +Cada DAG faz HTTP POST para a Scraper API no Cloud Run (`POST /scrape/agencies` com a agência específica). + +#### `scrape_ebc` + +DAG para scraping dos sites EBC (Agência Brasil, TV Brasil). + +| Configuração | Valor | +|--------------|-------| +| **Schedule** | `*/15 * * * *` (a cada 15 min) | +| **Retries** | 2 (com backoff de 5 min) | + +Faz HTTP POST para `POST /scrape/ebc` no Cloud Run. + +→ Veja [Módulo Scraper](../modulos/scraper.md) para detalhes da API e do repo. + +### DAGs do Data Platform (repo `data-platform`) + ### `sync_postgres_to_huggingface` Sincroniza notícias do PostgreSQL para o HuggingFace diariamente. @@ -172,7 +230,7 @@ apache-airflow-providers-postgres>=5.10.2 apache-airflow-providers-google>=10.14.0 sqlalchemy>=1.4.52 requests>=2.31.0 -pandas>=2.0.0 +pyyaml>=6.0 ``` ### Environment Variables @@ -219,22 +277,16 @@ As connections são gerenciadas via **Secret Manager**: ### Via GitHub Actions -O workflow `composer-deploy-dags.yaml` faz deploy automático em push: - -```yaml -# .github/workflows/composer-deploy-dags.yaml -on: - push: - paths: - - 'src/data_platform/dags/**' - -jobs: - deploy: - steps: - - name: Upload DAGs to GCS - run: | - gsutil -m cp -r src/data_platform/dags/* \ - gs://${{ env.COMPOSER_BUCKET }}/dags/ +Cada repo tem seu próprio workflow `composer-deploy-dags.yaml` que sincroniza para o subdiretório correspondente: + +**Repo `data-platform`**: +```bash +gsutil -m rsync -r -d src/data_platform/dags/ gs://{BUCKET}/dags/data-platform/ +``` + +**Repo `scraper`**: +```bash +gsutil -m rsync -r -d dags/ gs://{BUCKET}/dags/scraper/ ``` ### Manual via gcloud @@ -245,8 +297,11 @@ BUCKET=$(gcloud composer environments describe destaquesgovbr-composer \ --location us-central1 \ --format="value(config.dagGcsPrefix)") -# Upload das DAGs -gsutil -m cp -r src/data_platform/dags/* $BUCKET/ +# Upload das DAGs do data-platform +gsutil -m rsync -r -d src/data_platform/dags/ $BUCKET/data-platform/ + +# Upload das DAGs do scraper +gsutil -m rsync -r -d dags/ $BUCKET/scraper/ ``` ## Monitoramento diff --git a/docs/workflows/docker-builds.md b/docs/workflows/docker-builds.md index 687a213..3b15582 100644 --- a/docs/workflows/docker-builds.md +++ b/docs/workflows/docker-builds.md @@ -8,14 +8,15 @@ O projeto usa Docker para containerização de dois serviços principais: | Serviço | Imagem | Registry | | ------- | --------- | ------------------------- | -| Scraper | `scraper` | GitHub Container Registry | +| Scraper API | `scraper` | GCP Artifact Registry | | Portal | `portal` | GCP Artifact Registry | ```mermaid flowchart LR - subgraph "Scraper" - A[Push tag] --> B[Build] - B --> C[GHCR] + subgraph "Scraper API" + A[Push main] --> B[Build] + B --> F1[Artifact Registry] + F1 --> G1[Cloud Run] end subgraph "Portal" @@ -27,114 +28,42 @@ flowchart LR --- -## Build do Scraper +## Build do Scraper API -**Arquivo**: `scraper/.github/workflows/docker-build.yaml` +**Arquivo**: `scraper/.github/workflows/scraper-api-deploy.yaml` -### Trigger +O workflow faz build da imagem Docker e deploy automático no Cloud Run em push para `main`. -```yaml -on: - push: - tags: - - "v*" # Apenas tags de versão - workflow_dispatch: # Manual -``` - -### Workflow +### Trigger ```yaml -name: Build Docker Image - on: push: - tags: - - "v*" + branches: [main] + paths: + - 'src/**' + - 'docker/**' + - 'pyproject.toml' workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} ``` -### Dockerfile do Scraper +### Pipeline -```dockerfile -FROM python:3.12-slim - -WORKDIR /app - -# Instalar dependências do sistema -RUN apt-get update && apt-get install -y \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Instalar Poetry -RUN pip install poetry +1. Build da imagem Docker +2. Push para GCP Artifact Registry +3. Deploy no Cloud Run (`destaquesgovbr-scraper-api`) -# Copiar arquivos de dependência -COPY pyproject.toml poetry.lock ./ - -# Instalar dependências -RUN poetry config virtualenvs.create false \ - && poetry install --no-dev --no-interaction --no-ansi - -# Copiar código -COPY src/ ./src/ - -# Comando padrão -CMD ["python", "src/main.py", "--help"] -``` +### Dockerfile -### Tags geradas - -Para tag `v1.2.3`: - -- `ghcr.io/destaquesgovbr/scraper:1.2.3` -- `ghcr.io/destaquesgovbr/scraper:1.2` -- `ghcr.io/destaquesgovbr/scraper:latest` +Localizado em `scraper/docker/Dockerfile`. ### Execução ```bash -# Via tag (dispara automático) -git tag v1.2.3 -git push origin v1.2.3 +# Deploy automático em push para main # Manual -gh workflow run docker-build.yaml +gh workflow run scraper-api-deploy.yaml -R destaquesgovbr/scraper ``` --- @@ -212,27 +141,6 @@ CMD ["node", "server.js"] --- -## GitHub Container Registry (GHCR) - -### Autenticação - -```bash -# Login via CLI -echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin -``` - -### Pull de imagem - -```bash -docker pull ghcr.io/destaquesgovbr/scraper:latest -``` - -### Permissões - -O workflow usa `GITHUB_TOKEN` automático com permissão `packages: write`. - ---- - ## GCP Artifact Registry ### Autenticação @@ -252,16 +160,16 @@ docker push us-east1-docker.pkg.dev/PROJECT_ID/destaquesgovbr/portal:TAG ## Build Local -### Scraper +### Scraper API ```bash cd scraper # Build -docker build -t scraper . +docker build -f docker/Dockerfile -t scraper-api . # Executar -docker run --env-file .env scraper python src/main.py --help +docker run --env-file .env -p 8080:8080 scraper-api ``` ### Portal diff --git a/docs/workflows/scraper-pipeline.md b/docs/workflows/scraper-pipeline.md index 2deaea7..c9832b4 100644 --- a/docs/workflows/scraper-pipeline.md +++ b/docs/workflows/scraper-pipeline.md @@ -1,234 +1,101 @@ -# Workflow: Pipeline do Scraper +# Workflow: Pipeline de Coleta e Enriquecimento -> Pipeline diário de coleta e enriquecimento de notícias. - -**Arquivo**: `data-platform/.github/workflows/main-workflow.yaml` +> Pipeline completo de coleta de notícias (scraper) e enriquecimento (data-platform). ## Visão Geral -O pipeline é executado diariamente às **4AM UTC** (1AM Brasília) e consiste em 7 jobs sequenciais: +O pipeline é dividido em dois estágios independentes, em repositórios separados: + +1. **Scraping** (repo `scraper`): Via Airflow DAGs, a cada 15 minutos +2. **Enrichment** (repo `data-platform`): Via GitHub Actions, diário às 4AM UTC ```mermaid flowchart LR - A[scraper] --> B[ebc-scraper] - B --> C[upload-to-cogfy] - C --> D[wait-cogfy] - D --> E[enrich-themes] - E --> F[generate-embeddings] - F --> G[sync-typesense] -``` - ---- - -## Trigger - -```yaml -on: - schedule: - - cron: '0 4 * * *' # 4AM UTC diário - workflow_dispatch: # Manual - inputs: - start-date: - description: 'Data inicial (YYYY-MM-DD)' - required: false - end-date: - description: 'Data final (YYYY-MM-DD)' - required: false -``` - -### Execução automática - -- **Horário**: 4AM UTC (1AM Brasília) -- **Frequência**: Diária -- **Dias cobertos**: Últimos 3 dias (para capturar atualizações) - -### Execução manual - -Via GitHub Actions UI ou CLI: - -```bash -# Últimos 3 dias (padrão) -gh workflow run main-workflow.yaml - -# Período específico -gh workflow run main-workflow.yaml \ - -f start-date=2024-12-01 \ - -f end-date=2024-12-03 + subgraph "Scraping (Airflow, a cada 15min)" + DAG[~158 DAGs] -->|HTTP POST| API[Scraper API
Cloud Run] + API --> PG[(PostgreSQL)] + end + + subgraph "Enrichment (GitHub Actions, diário)" + UC[upload-cogfy] --> W[wait-cogfy] + W --> EN[enrich-themes] + EN --> EMB[generate-embeddings] + EMB --> TS[sync-typesense] + end + + PG -.->|lê dados novos| UC ``` --- -## Jobs Detalhados +## Estágio 1: Scraping (Airflow) -### Job 1: `scraper` +**Repositório**: [destaquesgovbr/scraper](https://github.com/destaquesgovbr/scraper) -Raspa notícias dos sites gov.br e insere no PostgreSQL. +### Como funciona -```yaml -scraper: - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Scrape gov.br sites - run: | - data-platform scrape \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \ - --end-date ${{ inputs.end-date || steps.dates.outputs.end }} - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} -``` - -**Duração**: ~30-60 minutos (dependendo do período) +- ~158 DAGs dinâmicas (1 por agência gov.br) + 1 DAG EBC +- Cada DAG roda a cada **15 minutos** +- A DAG faz HTTP POST para a Scraper API no Cloud Run +- A API raspa o site, parseia HTML → Markdown, e insere no PostgreSQL -### Job 2: `ebc-scraper` +### DAGs -Raspa notícias dos sites EBC (Agência Brasil, etc). - -```yaml -ebc-scraper: - needs: scraper - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Scrape EBC sites - run: | - data-platform scrape-ebc \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \ - --end-date ${{ inputs.end-date || steps.dates.outputs.end }} \ - --allow-update - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} -``` +| DAG | Schedule | Descrição | +|-----|----------|-----------| +| `scrape_{agency_key}` (~158) | `*/15 * * * *` | Raspa 1 agência gov.br | +| `scrape_ebc` | `*/15 * * * *` | Raspa sites EBC | -**Duração**: ~10-20 minutos - -### Job 3: `upload-to-cogfy` - -Envia notícias do PostgreSQL para classificação no Cogfy. - -```yaml -upload-to-cogfy: - needs: ebc-scraper - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Upload to Cogfy - run: | - data-platform upload-cogfy \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \ - --end-date ${{ inputs.end-date || steps.dates.outputs.end }} - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - COGFY_API_KEY: ${{ secrets.COGFY_API_KEY }} - COGFY_COLLECTION_ID: ${{ secrets.COGFY_COLLECTION_ID }} -``` +### API Endpoints -**Duração**: ~5-10 minutos +| Método | Endpoint | Descrição | +|--------|----------|-----------| +| `POST` | `/scrape/agencies` | Raspa sites gov.br | +| `POST` | `/scrape/ebc` | Raspa sites EBC | +| `GET` | `/health` | Health check | -### Job 4: `wait-cogfy` +### Deploy -Aguarda processamento no Cogfy. +| Componente | Destino | Workflow | +|-----------|---------|----------| +| API | Cloud Run | `scraper-api-deploy.yaml` | +| DAGs | Composer bucket `{bucket}/scraper/` | `composer-deploy-dags.yaml` | -```yaml -wait-cogfy: - needs: upload-to-cogfy - runs-on: ubuntu-latest - steps: - - name: Wait for Cogfy processing - run: sleep 1200 # 20 minutos -``` - -**Duração**: 20 minutos (fixo) - -### Job 5: `enrich-themes` - -Busca resultados do Cogfy e atualiza PostgreSQL. +--- -```yaml -enrich-themes: - needs: wait-cogfy - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Enrich with themes - run: | - data-platform enrich \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \ - --end-date ${{ inputs.end-date || steps.dates.outputs.end }} - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - COGFY_API_KEY: ${{ secrets.COGFY_API_KEY }} - COGFY_COLLECTION_ID: ${{ secrets.COGFY_COLLECTION_ID }} -``` +## Estágio 2: Enrichment Pipeline (GitHub Actions) -**Duração**: ~10-20 minutos +**Repositório**: [destaquesgovbr/data-platform](https://github.com/destaquesgovbr/data-platform) -### Job 6: `generate-embeddings` +**Arquivo**: `data-platform/.github/workflows/main-workflow.yaml` -Gera embeddings para notícias sem vetores. +### Trigger ```yaml -generate-embeddings: - needs: enrich-themes - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Generate embeddings - run: | - data-platform generate-embeddings \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - EMBEDDINGS_API_URL: ${{ secrets.EMBEDDINGS_API_URL }} +on: + schedule: + - cron: '0 4 * * *' # 4AM UTC diário + workflow_dispatch: + inputs: + start_date: + description: 'Start date (YYYY-MM-DD)' + end_date: + description: 'End date (YYYY-MM-DD)' + cogfy_wait_minutes: + description: 'Minutes to wait for Cogfy (default: 20)' ``` -**Duração**: ~10-15 minutos +### Jobs -### Job 7: `sync-typesense` +| # | Job | Descrição | Duração | +|---|-----|-----------|---------| +| 1 | `upload-to-cogfy` | Envia notícias novas para classificação no Cogfy | ~5-10 min | +| 2 | `wait-cogfy` | Aguarda processamento no Cogfy | 20 min (fixo) | +| 3 | `enrich-themes` | Busca temas e sumários do Cogfy, atualiza PostgreSQL | ~10-20 min | +| 4 | `generate-embeddings` | Gera vetores 768-dim via Embeddings API (Cloud Run) | ~10-15 min | +| 5 | `sync-typesense` | Sincroniza dados enriquecidos para o Typesense | ~5-10 min | -Sincroniza dados do PostgreSQL para o Typesense. - -```yaml -sync-typesense: - needs: generate-embeddings - runs-on: ubuntu-latest - container: - image: ghcr.io/destaquesgovbr/data-platform:latest - steps: - - name: Sync to Typesense - run: | - data-platform sync-typesense \ - --start-date ${{ inputs.start-date || steps.dates.outputs.start }} - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - POSTGRES_USER: ${{ secrets.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} - TYPESENSE_HOST: ${{ secrets.TYPESENSE_HOST }} - TYPESENSE_API_KEY: ${{ secrets.TYPESENSE_API_KEY }} -``` - -**Duração**: ~5-10 minutos +**Duração total**: ~50-75 minutos --- @@ -236,54 +103,49 @@ sync-typesense: ```mermaid sequenceDiagram - participant GH as GitHub Actions - participant DP as Data Platform Container - participant GOV as Sites gov.br - participant EBC as Sites EBC + participant AF as Airflow DAGs + participant API as Scraper API (Cloud Run) + participant GOV as Sites gov.br / EBC participant PG as PostgreSQL + participant GH as GitHub Actions participant CF as Cogfy participant EMB as Embeddings API participant TS as Typesense - Note over GH: 4AM UTC - Trigger + Note over AF: A cada 15 min - GH->>DP: Job: scraper - DP->>GOV: Fetch ~160+ sites - GOV-->>DP: HTML pages - DP->>DP: Parse → Markdown - DP->>PG: Insert articles + AF->>API: POST /scrape/agencies + API->>GOV: Fetch sites + GOV-->>API: HTML pages + API->>API: Parse → Markdown + API->>PG: INSERT articles - GH->>DP: Job: ebc-scraper - DP->>EBC: Fetch EBC sites - EBC-->>DP: HTML pages - DP->>PG: Insert/Update articles + Note over GH: 4AM UTC - Trigger diário - GH->>DP: Job: upload-to-cogfy - DP->>PG: Load articles - DP->>CF: POST records (batch) + GH->>PG: Load new articles + GH->>CF: POST records (batch) Note over GH: Wait 20 min - GH->>DP: Job: enrich-themes - DP->>CF: GET processed records - CF-->>DP: Themes + Summary - DP->>PG: Update with enrichment + GH->>CF: GET processed records + CF-->>GH: Themes + Summary + GH->>PG: UPDATE with enrichment - GH->>DP: Job: generate-embeddings - DP->>PG: Get news without embeddings - DP->>EMB: POST texts (batch) - EMB-->>DP: Vectors 768-dim - DP->>PG: Update with embeddings + GH->>PG: Get news without embeddings + GH->>EMB: POST texts (batch) + EMB-->>GH: Vectors 768-dim + GH->>PG: UPDATE with embeddings - GH->>DP: Job: sync-typesense - DP->>PG: Get news for indexing - DP->>TS: Upsert documents + GH->>PG: Get news for indexing + GH->>TS: Upsert documents ``` --- ## Secrets Necessárias +### Repo `data-platform` (enrichment) + | Secret | Descrição | Usado em | |--------|-----------|----------| | `POSTGRES_HOST` | Host do Cloud SQL | Todos os jobs | @@ -296,112 +158,41 @@ sequenceDiagram | `TYPESENSE_HOST` | Host do Typesense | sync-typesense | | `TYPESENSE_API_KEY` | API Key do Typesense | sync-typesense | -### Configurar secrets +### Repo `scraper` -```bash -# Via GitHub CLI -gh secret set POSTGRES_HOST --body "10.x.x.x" -gh secret set POSTGRES_DB --body "destaquesgovbr" -gh secret set POSTGRES_USER --body "admin" -gh secret set POSTGRES_PASSWORD --body "xxxxx" -gh secret set COGFY_API_KEY --body "sk-xxxxx" -gh secret set COGFY_COLLECTION_ID --body "uuid-xxxxx" -gh secret set EMBEDDINGS_API_URL --body "https://embeddings-xxx.run.app" -gh secret set TYPESENSE_HOST --body "10.x.x.x" -gh secret set TYPESENSE_API_KEY --body "xxxxx" -``` +| Secret | Descrição | +|--------|-----------| +| `DATABASE_URL` | Connection string PostgreSQL | +| GCP SA credentials | Para deploy no Cloud Run e Composer | --- ## Monitoramento -### Ver status do workflow +### Scraping (Airflow) ```bash -# Listar execuções recentes -gh run list --workflow=main-workflow.yaml - -# Ver detalhes de uma execução -gh run view - -# Ver logs -gh run view --log +# Acessar Web UI do Airflow +gcloud composer environments describe destaquesgovbr-composer \ + --location us-central1 \ + --format="value(config.airflowUri)" ``` -### Via interface GitHub - -1. Acessar repositório no GitHub -2. Aba "Actions" -3. Selecionar "main-workflow" -4. Ver execuções e logs - ---- - -## Tratamento de Erros - -### Falha em scraping - -- Jobs posteriores **não executam** (dependência) -- Artigos com erro são **skipados** (não bloqueia) -- Logs detalhados disponíveis - -### Falha em upload Cogfy - -- Retry automático (3 tentativas) -- Enriquecimento não executa -- Dados ficam sem classificação até próxima execução - -### Falha em embeddings - -- Notícias sem embeddings são marcadas -- Próxima execução tenta novamente - -### Falha em sync Typesense - -- Portal continua funcionando com dados antigos -- Próxima execução tenta novamente - ---- - -## Execução Manual (Dispatch) - -### Para período específico +### Enrichment (GitHub Actions) ```bash -gh workflow run main-workflow.yaml \ - -f start-date=2024-01-01 \ - -f end-date=2024-01-31 -``` - -### Para reprocessar +# Listar execuções recentes +gh run list --workflow=main-workflow.yaml -R destaquesgovbr/data-platform -```bash -# Reprocessar últimos 7 dias -gh workflow run main-workflow.yaml \ - -f start-date=$(date -v-7d +%Y-%m-%d) \ - -f end-date=$(date +%Y-%m-%d) +# Ver detalhes de uma execução +gh run view -R destaquesgovbr/data-platform ``` --- -## Duração Total - -| Job | Duração Típica | -|-----|----------------| -| scraper | 30-60 min | -| ebc-scraper | 10-20 min | -| upload-to-cogfy | 5-10 min | -| wait-cogfy | 20 min (fixo) | -| enrich-themes | 10-20 min | -| generate-embeddings | 10-15 min | -| sync-typesense | 5-10 min | -| **Total** | **~90-155 min** | - ---- - ## Sync HuggingFace (Separado) -O sync para o HuggingFace é feito via DAG no Cloud Composer, não faz parte deste workflow. +O sync para o HuggingFace é feito via DAG no Cloud Composer (repo `data-platform`), não faz parte dos pipelines acima. → Veja [Airflow DAGs](./airflow-dags.md) para detalhes. @@ -409,8 +200,8 @@ O sync para o HuggingFace é feito via DAG no Cloud Composer, não faz parte des ## Links Relacionados -- [Data Platform](../modulos/data-platform.md) - Repositório unificado +- [Módulo Scraper](../modulos/scraper.md) - Detalhes do scraper standalone +- [Data Platform](../modulos/data-platform.md) - Repositório de enrichment - [PostgreSQL](../arquitetura/postgresql.md) - Fonte de verdade -- [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) - Visão geral do pipeline -- [Integração Cogfy](../modulos/cogfy-integracao.md) - Classificação LLM -- [Docker Builds](./docker-builds.md) - Build da imagem +- [Airflow DAGs](./airflow-dags.md) - DAGs de sync e scraping +- [Docker Builds](./docker-builds.md) - Build das imagens