# Pipeline de dados e RAG
Este documento descreve o fluxo **implementado atualmente** no IRIS Electoral Intelligence: da coleta de dados públicos do TSE e da Câmara dos Deputados até a resposta RAG fundamentada. Ele complementa o [README](README.md) com detalhes de execução, contratos, persistência, idempotência, recuperação e falhas.
> Escopo real: nenhuma etapa abaixo pressupõe serviços, índices ou recursos não presentes no código. Limitações e evoluções possíveis estão identificadas no final.
## Visão geral
```mermaid
flowchart LR
subgraph Fontes[Fontes públicas oficiais]
TSE[TSE
CKAN, CSV e PDFs]
CAM[Câmara dos Deputados
API REST v2]
end
subgraph Ingestao[Ingestão em Embedded Python]
HTTP[HTTPS validado
retry e paginação]
NORM[Parsing, normalização
e validação]
MATCH[Resolução de identidade
TSE ↔ Câmara]
end
subgraph IRIS[InterSystems IRIS]
REL[(Dados relacionais
candidatos, histórico e proposições)]
STREAM[(Streams
PDF extraído e JSON bruto)]
CHUNKS[(PoliticalChunk
texto e metadados)]
VECTOR[(%Vector DOUBLE 1536
embeddings)]
AUDIT[(IngestionRun
auditoria)]
end
subgraph RAG[Recuperação e geração]
LEX[Busca lexical]
VEC[Vector cosine]
RRF[Fusão RRF]
CTX[Contexto estruturado
com evidências E1...En]
LLM[OpenAI Responses API]
ANSWER[Resposta em PT-BR
com fontes]
end
TSE --> HTTP
CAM --> HTTP
HTTP --> NORM
NORM --> MATCH
NORM --> REL
MATCH --> REL
REL --> CHUNKS
STREAM --> CHUNKS
CHUNKS --> VECTOR
Ingestao -. métricas .-> AUDIT
CHUNKS --> LEX
VECTOR --> VEC
LEX --> RRF
VEC --> RRF
RRF --> CTX
REL --> CTX
STREAM --> CTX
CTX --> LLM
LLM --> ANSWER
```
O comando de orquestração é:
```bash
docker compose exec iris irispython -m app.ingestion.pipeline
```
Ele é executado **dentro do container IRIS**, no namespace `IRISAPP`, e segue esta ordem fixa:
```mermaid
flowchart TD
START([Início]) --> TC[TSE_CANDIDATES]
TC --> TP[TSE_PROPOSALS]
TP --> CA[CAMARA]
CA --> RI[RAG_INDEX]
RI --> END([Fim])
TC -. grava .-> C[(Candidate)]
TP -. grava .-> D[(ProposalDocument)]
CA -. grava .-> H[(PoliticalHistory)]
CA -. grava .-> P[(Proposition + autores + temas)]
C --> RI
D --> RI
H --> RI
P --> RI
RI -. grava .-> PC[(PoliticalChunk + Embedding)]
```
Cada bloco possui seu próprio registro em `IngestionRun`. Uma falha fatal encerra o bloco como `FAILED`; falhas isoladas que permitem continuação produzem `PARTIAL`; ausência de falhas produz `SUCCESS`.
## Componentes e responsabilidades
| Etapa | Implementação principal | Entrada | Saída |
|---|---|---|---|
| Orquestração | `app/ingestion/pipeline.py` | Configurações de ambiente | Quatro runs de ingestão em ordem |
| HTTP seguro | `app/ingestion/http.py` | URLs oficiais HTTPS | Respostas/arquivos validados, com retry |
| TSE: candidatos | `app/ingestion/tse/client.py`, `parser.py`, `mapper.py` | Dataset CKAN e CSV | `Candidate` normalizado |
| TSE: planos | `proposal_reader.py` | ZIPs/PDFs oficiais | `ProposalDocument.RawText` |
| Câmara | `app/ingestion/camara/` | API REST v2 | Matching, histórico, proposições, autores e temas |
| Matching | `matching/candidate_matcher.py` | Candidato TSE + deputados Câmara | `MATCHED`, `REVIEW` ou `UNMATCHED` |
| Chunking | `chunking/chunker.py`, `political_chunk_builder.py` | Conteúdo persistido | Chunks de 700 tokens, overlap 100 |
| Embeddings | `app/embeddings/embedder.py` | Chunks sem vetor | Vetores `text-embedding-3-small`, 1536 dimensões |
| Persistência | `app/repositories/`, `app/database/` | Objetos de escrita | Classes persistentes IRIS e auditoria |
| Retrieval | `app/retrieval/` | Pergunta e filtros | Evidências lexicais, vetoriais ou estruturadas |
| RAG | `app/rag/` | Pergunta + evidências | Prompt controlado, resposta e fontes |
## Fontes externas e fronteiras de confiança
### TSE
- API CKAN: descoberta do dataset `candidatos-2026` e de seus recursos.
- CSV oficial de candidaturas: codificação Latin-1 e separador `;`.
- ZIPs de propostas de governo: PDFs associados pelo `SQ_CANDIDATO` codificado no nome oficial do arquivo.
- Hosts aceitos: `dadosabertos.tse.jus.br` e `cdn.tse.jus.br`, sempre por HTTPS.
### Câmara dos Deputados
- API Dados Abertos v2: deputados, detalhes, histórico, mandatos externos, proposições, autores e temas.
- Host aceito: `dadosabertos.camara.leg.br`, sempre por HTTPS.
- A janela histórica padrão é móvel e cobre os quatro anos anteriores à data da execução.
O cliente valida a URL inicial e cada redirecionamento. Respostas JSON precisam ter um objeto na raiz. Downloads são feitos em arquivo temporário e recebem SHA-256 durante a transferência. ZIPs passam por validação de assinatura e proteção contra *zip slip* antes de qualquer leitura.
```mermaid
sequenceDiagram
participant P as Pipeline
participant H as Cliente HTTP
participant S as Fonte oficial
participant T as Arquivo temporário
participant V as Validador/Parser
P->>H: requisitar URL HTTPS permitida
H->>S: GET com timeouts
alt 429 ou erro transitório 5xx
S-->>H: falha transitória / Retry-After
H->>H: backoff exponencial com jitter
H->>S: nova tentativa
end
S-->>H: resposta ou stream
H->>H: validar redirects e host final
H->>T: gravar blocos + calcular SHA-256
T->>V: validar assinatura/ZIP/contrato
V-->>P: dados tipados ou erro explícito
```
As falhas que recebem retry são conexão, timeout, HTTP `429`, `500`, `502`, `503` e `504`. Os padrões são timeout de conexão de 10 s, leitura de 60 s e até quatro tentativas. `Retry-After` numérico é respeitado; nos demais casos usa-se espera exponencial com jitter.
## Etapa 1 — candidatos do TSE
### Descoberta e seleção do recurso
1. Consulta `package_show` no CKAN.
2. Exige `success=true` e identidade compatível com o dataset solicitado.
3. Valida o contrato com Pydantic.
4. Seleciona exatamente um recurso CSV ativo cujo nome identifique candidatos.
5. Baixa o arquivo, calcula SHA-256 e valida o ZIP.
6. Prefere o CSV nacional que contém `BRASIL` no nome; na ausência dele, considera os CSVs encontrados.
### Parsing e normalização
O parser exige as colunas oficiais necessárias, lê Latin-1 com delimitador `;` e converte sentinelas de nulo. Linhas inválidas viram resultados de parsing com erro e entram no contador `RecordsFailed`; não são transformadas em candidatos parciais.
O pipeline filtra:
- ano eleitoral (`INGEST_ELECTION_YEAR`, padrão `2026`);
- UFs (`INGEST_STATES`, padrão `SP`);
- cargos (`INGEST_OFFICES`, padrão `DEPUTADO FEDERAL,GOVERNADOR`).
Os registros válidos são persistidos em lotes de 500. Cada lote e a atualização agregada dos contadores compartilham a mesma transação.
```mermaid
flowchart TD
CKAN[CKAN package_show] --> CONTRACT{Contrato válido?}
CONTRACT -- não --> FAIL[Run FAILED]
CONTRACT -- sim --> RESOURCE[Selecionar CSV ativo]
RESOURCE --> DOWNLOAD[Download + SHA-256]
DOWNLOAD --> ZIP{ZIP seguro?}
ZIP -- não --> FAIL
ZIP -- sim --> CSV[Latin-1 + separador ponto e vírgula]
CSV --> ROW{Linha válida?}
ROW -- não --> BAD[RecordsFailed + 1]
ROW -- sim --> FILTER{Ano, UF e cargo aceitos?}
FILTER -- não --> SKIP[RecordsSkipped + 1]
FILTER -- sim --> MAP[Normalizar CandidateWrite]
MAP --> UPSERT{Existe TseId?}
UPSERT -- não --> INSERT[INSERT Candidate]
UPSERT -- igual --> SAME[UNCHANGED]
UPSERT -- diferente --> UPDATE[UPDATE Candidate]
INSERT --> COMMIT[Commit do lote]
SAME --> COMMIT
UPDATE --> COMMIT
```
### Idempotência
`Candidate.TseId` possui índice único. O repositório pesquisa esse identificador antes de escrever:
- não existe: `INSERTED`;
- existe e os campos de negócio são iguais: `UNCHANGED`;
- existe e mudou: `UPDATED`.
O parser atualmente materializa as linhas em memória antes da persistência. Portanto, **streaming ponta a ponta do CSV não está implementado atualmente**.
## Etapa 2 — propostas de governo do TSE
O pipeline seleciona recursos PDF ativos do ano eleitoral cujos nomes correspondem a `BR`, às UFs configuradas e ao sufixo “proposta de governo”. Os recursos podem conter ZIPs com vários PDFs.
```mermaid
sequenceDiagram
participant CKAN as TSE CKAN
participant PIPE as Pipeline
participant ZIP as Leitor ZIP
participant PDF as pypdf
participant IRIS as IRIS
PIPE->>CKAN: listar recursos ativos de propostas
CKAN-->>PIPE: metadados e URLs
loop por recurso aceito
PIPE->>ZIP: baixar, hashear e validar
loop por PDF com nome oficial válido
ZIP->>PIPE: ano + UF + SQ_CANDIDATO
PIPE->>IRIS: localizar Candidate por TseId
alt candidato encontrado
PIPE->>PDF: extrair texto página a página
PDF-->>PIPE: texto com marcadores Página N
PIPE->>IRIS: upsert Candidate + DocumentHash
else sem candidato no recorte
PIPE->>PIPE: registrar skipped
end
end
end
```
O nome do arquivo é interpretado por expressão regular no formato oficial, extraindo ano, UF e `SQ_CANDIDATO`. O vínculo é exato pelo identificador do TSE: não há aproximação por nome nessa etapa.
O `pypdf` extrai o texto de cada página e adiciona marcadores `[Página N]`. O SHA-256 dos bytes do PDF vira `DocumentHash`. O texto integral vai para `ProposalDocument.RawText`, um `%Stream.GlobalCharacter`; os embeddings não são armazenados no documento, mas nos chunks derivados.
Idempotência: a chave única lógica é `(Candidate, DocumentHash)`. O mesmo conteúdo não cria outro documento; mudanças nos metadados atualizam o registro existente.
Limitação: **OCR de PDFs digitalizados não está implementado atualmente**. PDFs sem camada textual podem resultar em conteúdo vazio ou incompleto.
## Etapa 3 — Câmara e resolução de identidade
Relacionar uma candidatura eleitoral a um deputado exige cuidado: nomes de urna, nomes civis e filiações históricas podem divergir. O pipeline não usa uma aproximação opaca. Ele aplica regras determinísticas, guarda a confiança técnica e só ingere dados parlamentares quando o resultado é `MATCHED`.
### Pontuação do matching
| Evidência | Pontos |
|---|---:|
| Override manual verificado | 100 |
| Nome civil exato | 60 |
| Nome de urna exato | 20 |
| UF compatível | 15 |
| Partido compatível no histórico | 5 |
```mermaid
stateDiagram-v2
[*] --> Buscar
Buscar --> Override: override verificado
Buscar --> Pontuar: resultados por nome de urna/civil
Pontuar --> AmpliarBusca: nenhum resultado
AmpliarBusca --> Pontuar: primeiro + último nome
Override --> MATCHED
Pontuar --> MATCHED: confiança >= 90
Pontuar --> REVIEW: confiança de 70 a 89
Pontuar --> UNMATCHED: confiança < 70
MATCHED --> PersistirDeputyId
REVIEW --> SemDeputyId
UNMATCHED --> SemDeputyId
PersistirDeputyId --> [*]
SemDeputyId --> [*]
```
`REVIEW` não é promovido automaticamente. Para `REVIEW` e `UNMATCHED`, `CamaraDeputyId` permanece nulo e não há coleta detalhada daquele deputado.
### Coleta parlamentar
Para cada candidato `MATCHED`, o pipeline:
1. obtém detalhes e histórico do deputado, com cache por ID;
2. preserva registros atuais dentro da janela e mandatos externos que se sobreponham à janela;
3. busca proposições em janelas reversas de três meses;
4. segue o link `rel=next` da API e valida cada próxima URL;
5. elimina IDs repetidos e aplica o limite configurado por candidato;
6. coleta detalhes, autores e temas de diferentes proposições em paralelo;
7. serializa as gravações no thread principal, em uma transação por proposição.
```mermaid
flowchart TD
C[Candidate MATCHED] --> DEP[Detalhe e histórico do deputado]
DEP --> HIST[Upsert PoliticalHistory]
DEP --> WINDOWS[Janelas de 3 meses em ordem reversa]
WINDOWS --> PAGES[Paginação rel=next]
PAGES --> DEDUP[Deduplicação por CamaraId]
DEDUP --> LIMIT[Limite por candidato]
LIMIT --> POOL[ThreadPoolExecutor
até 6 workers por padrão]
subgraph Worker[Para cada proposição]
DETAIL[Detalhe]
AUTHORS[Autores]
TOPICS[Temas]
DETAIL --> AUTHORS --> TOPICS
end
POOL --> DETAIL
TOPICS --> MAIN[Thread principal]
MAIN --> TX[Transação por proposição]
TX --> PROP[(Proposition)]
TX --> AUTH[(PropositionAuthor)]
TX --> TOP[(PropositionTopic)]
```
Cada worker mantém sua própria sessão HTTP. O paralelismo ocorre entre proposições; as três chamadas de uma mesma proposição são sequenciais. Uma falha isolada incrementa `RecordsFailed`, permite as demais coletas e deixa a run `PARTIAL`.
### Deduplicação
- `Proposition.CamaraId`: único; upsert pelo ID oficial.
- Autores: deduplicados por URI quando disponível, senão por nome normalizado e tipo.
- Temas: únicos por `(Proposition, Name)`.
- Histórico: upsert por `(Candidate, ExternalId)` na aplicação.
- O número máximo padrão é 50 candidatos correspondidos, 50 proposições por candidato e 10 autores por proposição.
## Persistência multimodelo no InterSystems IRIS
```mermaid
erDiagram
Candidate ||--o{ PoliticalHistory : possui
Candidate ||--o{ Proposition : possui
Candidate ||--o{ ProposalDocument : possui
Candidate ||--o{ PoliticalChunk : contextualiza
Proposition ||--o{ PropositionAuthor : possui
Proposition ||--o{ PropositionTopic : classifica
Candidate {
string TseId UK
integer CamaraDeputyId
string MatchStatus
decimal MatchConfidence
}
PoliticalHistory {
string ExternalId
stream RawJson
}
Proposition {
integer CamaraId UK
string Summary
string DetailedSummary
}
ProposalDocument {
string DocumentHash UK
stream RawText
}
PoliticalChunk {
string SourceType
string SourceId
integer ChunkIndex
string ContentHash
vector Embedding
}
IngestionRun {
string Source
string Status
integer RecordsRead
integer RecordsFailed
}
```
O IRIS centraliza três representações complementares:
- **relacional/objetos persistentes:** candidatos, vínculos, proposições, autores e temas;
- **streams:** texto integral dos PDFs e JSON bruto de histórico para auditoria;
- **vetores:** embeddings de dimensão fixa em `PoliticalChunk.Embedding`.
As oito classes `%Persistent` são compiladas no build da imagem. O acesso usa `iris.sql` no Embedded Python; o modo híbrido utiliza a Object API para `Candidate` e `IngestionRun` e SQL parametrizado para as demais entidades. As transações são explícitas.
## Etapa 4 — chunking e embeddings
O `RAG_INDEX` transforma três fontes em `PoliticalChunk`:
| `SourceType` | Origem | `SourceId` | Texto construído |
|---|---|---|---|
| `PROPOSITION` | `Proposition` + autores + temas | `CamaraId` | título, autores, temas, ementas e situação |
| `GOVERNMENT_PROPOSAL` | `ProposalDocument` | SHA-256 do PDF | texto integral com marcadores de página |
| `POLITICAL_HISTORY` | `PoliticalHistory` | `ExternalId` | instituição, cargo, partido, período e situação |
Antes de chunkear, o pipeline corrige anos inválidos de proposições quando a data de apresentação permite a recuperação. Autores e temas são lidos com consultas `IN` em blocos de no máximo 200 IDs para respeitar limites de argumentos do IRIS.
### Algoritmo de chunking
1. Normaliza quebras de linha e espaços horizontais.
2. Tokeniza com o encoding do modelo configurado; usa `cl100k_base` como fallback.
3. Cria janelas de 700 tokens.
4. Reaproveita 100 tokens da janela anterior; o passo efetivo é 600.
5. Limita o texto decodificado a 32.000 caracteres, compatível com a propriedade IRIS.
6. Calcula SHA-256 do conteúdo normalizado.
7. Registra metadados JSON e, para PDFs, páginas inicial/final quando os marcadores aparecem no chunk.
```mermaid
flowchart LR
DOC[Documento normalizado] --> TOK[Tokenização]
TOK --> W1[Chunk 0
tokens 0–699]
TOK --> W2[Chunk 1
tokens 600–1299]
TOK --> W3[Chunk 2
tokens 1200–1899]
W1 -. overlap 100 .-> W2
W2 -. overlap 100 .-> W3
W1 --> HASH[SHA-256 + metadados]
W2 --> HASH
W3 --> HASH
HASH --> PC[(PoliticalChunk)]
```
O tamanho de 700 tokens oferece contexto suficiente para trechos legislativos e programáticos sem transformar cada evidência em um documento muito amplo. A sobreposição de 100 tokens reduz a perda de frases e argumentos nas fronteiras. Esses valores são configuráveis por `CHUNK_SIZE_TOKENS` e `CHUNK_OVERLAP_TOKENS` e devem ser reavaliados por experimentos de precisão, cobertura, latência e custo quando o corpus crescer.
### Substituição idempotente da fonte
Para cada `(Candidate, SourceType, SourceId)`, `replace_source` compara `(ChunkIndex, ContentHash)`:
```mermaid
flowchart TD
BUILD[Reconstruir chunks da fonte] --> COMPARE{Índice + hash já existem?}
COMPARE -- sim --> KEEP[Manter chunk e embedding]
COMPARE -- não --> NEW[Inserir chunk sem embedding]
BUILD --> STALE{Há chunk antigo fora do novo conjunto?}
STALE -- sim --> DELETE[Excluir chunk obsoleto]
STALE -- não --> DONE[Sem exclusão]
KEEP --> PENDING[Selecionar apenas Embedding IS NULL]
NEW --> PENDING
DELETE --> PENDING
```
Assim, uma reexecução reconstrói a representação textual, mas preserva vetores de chunks cujo índice e conteúdo não mudaram. Chunks novos ou alterados ficam pendentes. Não existe atualmente checkpoint por fonte que evite a reconstrução antes dessa comparação.
### Geração e armazenamento dos embeddings
- Modelo padrão: `text-embedding-3-small`.
- Dimensão solicitada e validada: 1536.
- Lote padrão: 50 chunks.
- Chamada: OpenAI Embeddings API.
- Destino: `PoliticalChunk.Embedding As %Vector(DATATYPE="DOUBLE", LEN=1536)`.
- Escrita: `TO_VECTOR(?, DOUBLE)` em SQL parametrizado.
```mermaid
sequenceDiagram
participant IDX as ChunkIndexPipeline
participant IRIS as PoliticalChunk
participant EMB as OpenAI Embeddings
loop enquanto houver Embedding IS NULL
IDX->>IRIS: buscar até EMBEDDING_BATCH_SIZE
IRIS-->>IDX: IDs e conteúdos
IDX->>EMB: inputs + modelo + dimensions=1536
EMB-->>IDX: vetores ordenados
IDX->>IDX: validar quantidade e dimensão
loop por vetor no lote
IDX->>IRIS: UPDATE Embedding=TO_VECTOR(...)
end
IDX->>IRIS: commit do lote + contadores
end
```
Se houver chunks pendentes e `LLM_API_KEY` estiver ausente, o pipeline não fabrica vetores: encerra `RAG_INDEX` como `PARTIAL` e registra a quantidade pendente. Resposta com número de vetores diferente do lote ou dimensão diferente de 1536 é erro.
`PoliticalChunk` é, portanto, a tabela responsável pela representação vetorial. A condição operacional esperada após uma indexação completa é:
```sql
SELECT COUNT(*) AS TotalChunks,
SUM(CASE WHEN Embedding IS NULL THEN 1 ELSE 0 END) AS PendingEmbeddings
FROM IRISPolitical_Model.PoliticalChunk;
```
`PendingEmbeddings` deve ser zero para que a busca vetorial cubra todo o corpus.
## Recuperação: estruturada, lexical e vetorial
O planejador determinístico classifica a pergunta e os filtros antes de recuperar evidências.
```mermaid
flowchart TD
Q[Pergunta + filtros] --> PLAN[Query planner determinístico]
PLAN --> INTENT{Intenção}
INTENT -- cobertura de documento
com candidato --> COVER[DocumentCoverageRetriever]
INTENT -- frequência de temas
com candidato --> FREQ[StructuredQueryRetriever]
INTENT -- demais perguntas --> HYBRID[HybridRetriever]
COVER --> EVIDENCE[Evidências]
FREQ --> EVIDENCE
HYBRID --> EVIDENCE
```
### Recuperação estruturada
- **Cobertura de documento:** ordena chunks do tipo selecionado e amostra posições distribuídas pelo documento, evitando concentrar a resposta apenas no início.
- **Frequência de temas:** agrega `PropositionTopic` com SQL `COUNT`, preservando a semântica exata de uma contagem.
### Busca lexical
A implementação atual carrega do IRIS os chunks compatíveis com candidato e tipo de fonte, normaliza caixa e acentos em Python e pontua:
```text
lexical_score = 10 × ocorrências da frase completa + ocorrências dos termos
```
Os 20 melhores resultados entram na fusão. **Não há índice IRIS de full-text implementado atualmente**; a busca lexical é calculada na aplicação.
### Busca vetorial
A pergunta recebe embedding com o mesmo modelo e dimensão dos chunks. O IRIS calcula similaridade por:
```sql
VECTOR_COSINE(Embedding, TO_VECTOR(?, DOUBLE))
```
Os 20 melhores resultados entram na fusão. **Não há índice HNSW implementado atualmente**; a consulta usa a função vetorial sobre os registros filtrados.
### Reciprocal Rank Fusion
Lexical e vetorial possuem escalas diferentes. A implementação combina posições, não scores crus:
```text
RRF(d) = Σ 1 / (60 + rank(d))
```
```mermaid
flowchart LR
Q[Pergunta] --> L[Ranking lexical
top 20]
Q --> E[Embedding da pergunta]
E --> V[Ranking VECTOR_COSINE
top 20]
L --> RRF[RRF k=60]
V --> RRF
RRF --> TOP[Top-K evidências]
```
RRF oferece uma fusão estável sem fingir que frequência textual e cosseno estão na mesma escala. Aliases de tipo de fonte são normalizados antes dos filtros.
## Contexto, prompt e geração
O endpoint `/search` termina no retrieval e retorna resultados. O endpoint `/ask` continua até a geração RAG.
```mermaid
sequenceDiagram
actor U as Usuário
participant API as Flask /api/ask
participant RAG as RagService
participant RET as Retrieval
participant IRIS as InterSystems IRIS
participant LLM as OpenAI Responses
U->>API: pergunta, candidateId e filtros
API->>RAG: ask(...)
RAG->>IRIS: carregar candidato autoritativo
RAG->>RET: planejar e recuperar
RET->>IRIS: SQL estruturado, chunks e vetores
IRIS-->>RET: evidências ranqueadas
RET-->>RAG: top 8/12/24 conforme estratégia
RAG->>IRIS: enriquecer fontes, autores e temas
IRIS-->>RAG: contexto estruturado
alt nenhuma evidência
RAG-->>API: perfil disponível ou resposta canônica sem evidência
else há evidência
RAG->>LLM: política + pergunta + [E1]...[En]
LLM-->>RAG: resposta com citações
RAG->>RAG: filtrar fontes citadas
RAG-->>API: answer + sources + métricas
end
API-->>U: JSON fundamentado
```
### Seleção e diversidade
- consulta normal: top 8;
- cobertura de documento para candidato selecionado: top 12;
- descoberta global: recupera até 24 e reduz a 12, com no máximo três evidências por candidato.
Quando `candidateId` foi informado, evidências de outro candidato são descartadas. O contexto combina o texto do chunk com dados estruturados da fonte, candidato, autores, temas, URL oficial e metadados de coleta.
### Política do prompt
Cada evidência recebe um identificador `[E1]`, `[E2]` etc. O prompt exige:
- resposta neutra em português brasileiro;
- uso exclusivo das evidências fornecidas;
- citações no formato `[E#]`;
- distinção explícita entre ausência de contexto e fato negativo;
- nenhuma recomendação de voto ou inferência ideológica;
- desconsideração de instruções eventualmente contidas nos documentos recuperados.
O texto externo é tratado como evidência, não como instrução, reduzindo risco de *prompt injection* no corpus.
### Chamada ao modelo e fallback
A geração usa OpenAI Responses API com `store=False`, modelo padrão `gpt-5-mini` e até 4.000 tokens de saída. Se a resposta vier incompleta ou vazia, há uma repetição com limite ampliado, no máximo 8.000 tokens. Persistindo o problema, a aplicação retorna um resumo determinístico das evidências — não inventa conteúdo.
Se não houver evidência, o LLM não é chamado. A resposta informa insuficiência de dados ou devolve apenas o perfil autoritativo disponível do candidato.
As fontes devolvidas são, preferencialmente, apenas as realmente citadas na resposta. Se o modelo não produzir citações reconhecíveis, todas as evidências usadas são mantidas para não perder rastreabilidade.
## Transações, auditoria e estados de execução
```mermaid
stateDiagram-v2
[*] --> RUNNING: start(source, parameters, hash)
RUNNING --> SUCCESS: fluxo concluído sem falhas
RUNNING --> PARTIAL: falhas recuperáveis ou embeddings pendentes
RUNNING --> FAILED: exceção fatal
SUCCESS --> [*]
PARTIAL --> [*]
FAILED --> [*]
```
`IngestionRun` armazena início/fim, fonte, status, SHA-256 quando aplicável, parâmetros JSON, erro e contadores de lidos, criados, atualizados, ignorados e falhos. Os loops acumulam contadores em memória e os atualizam na mesma fronteira transacional dos dados correspondentes, reduzindo round trips sem separar auditoria e persistência.
Fronteiras principais:
- candidatos TSE: uma transação por lote de 500;
- proposta de governo: uma transação por PDF;
- matching/histórico: transações pequenas por candidato/registro;
- Câmara: uma transação por proposição, incluindo autores e temas;
- chunking: uma transação por documento-fonte;
- embeddings: uma transação por lote.
Rollback afeta somente a unidade em andamento. Isso permite reexecutar o processo com os upserts idempotentes sem apagar o trabalho já confirmado.
## Configuração operacional principal
| Variável | Padrão | Efeito |
|---|---:|---|
| `INGEST_ELECTION_YEAR` | `2026` | Ano eleitoral aceito |
| `INGEST_STATES` | `SP` | UFs aceitas, separadas por vírgula |
| `INGEST_OFFICES` | `DEPUTADO FEDERAL,GOVERNADOR` | Cargos aceitos |
| `CAMARA_LOOKBACK_YEARS` | `4` | Janela histórica móvel |
| `CAMARA_MAX_MATCHED_CANDIDATES` | `50` | Candidatos com coleta parlamentar detalhada |
| `CAMARA_MAX_PROPOSITIONS_PER_CANDIDATE` | `50` | Limite por candidato |
| `CAMARA_MAX_AUTHORS_PER_PROPOSITION` | `10` | Limite de autores |
| `CAMARA_HTTP_WORKERS` | `6` | Paralelismo entre proposições, de 1 a 16 |
| `HTTP_CONNECT_TIMEOUT_SECONDS` | `10` | Timeout de conexão |
| `HTTP_READ_TIMEOUT_SECONDS` | `60` | Timeout de leitura |
| `HTTP_MAX_RETRIES` | `4` | Número total de tentativas |
| `CHUNK_SIZE_TOKENS` | `700` | Tamanho-alvo do chunk |
| `CHUNK_OVERLAP_TOKENS` | `100` | Sobreposição |
| `EMBEDDING_MODEL` | `text-embedding-3-small` | Modelo vetorial |
| `EMBEDDING_BATCH_SIZE` | `50` | Chunks por chamada |
| `LLM_MODEL` | `gpt-5-mini` | Modelo de geração |
| `LLM_MAX_OUTPUT_TOKENS` | `4000` | Limite inicial da resposta |
A lista completa e os comandos de instalação estão no [README](README.md).
## Execução e validação
Com o ambiente já iniciado e saudável:
```bash
docker compose exec iris irispython -m app.ingestion.pipeline
```
Valide a API:
```bash
curl http://localhost:52773/api/health
curl http://localhost:52773/api/candidates
```
Valide as runs e os vetores no IRIS SQL:
```sql
SELECT ID, Source, Status, RecordsRead, RecordsCreated,
RecordsUpdated, RecordsSkipped, RecordsFailed, StartedAt, FinishedAt
FROM IRISPolitical_Model.IngestionRun
ORDER BY ID DESC;
SELECT SourceType,
COUNT(*) AS Chunks,
SUM(CASE WHEN Embedding IS NULL THEN 1 ELSE 0 END) AS Pending
FROM IRISPolitical_Model.PoliticalChunk
GROUP BY SourceType;
```
O critério mínimo para indexação vetorial completa é `Pending = 0` em todos os tipos presentes. Uma run `PARTIAL` ou `FAILED` precisa ser explicada pelo `ErrorMessage` e pelos logs antes de se considerar o ambiente válido.
### Snapshot de uma execução validada
Em uma execução limpa realizada durante a validação do projeto, o IRIS persistiu:
| Entidade | Registros |
|---|---:|
| `Candidate` | 1.139 |
| `PoliticalHistory` | 399 |
| `Proposition` | 2.753 |
| `PropositionAuthor` | 7.351 |
| `PropositionTopic` | 1.866 |
| `ProposalDocument` | 20 |
| `PoliticalChunk` | 4.425 |
Todos os 4.425 chunks estavam com `Embedding IS NOT NULL`, e uma consulta `VECTOR_COSINE` foi executada com sucesso. Esses números são um snapshot, não um contrato: variam com filtros, limites, data e conteúdo das APIs públicas.
## Limitações atuais e melhorias futuras
| Item | Estado real |
|---|---|
| Foreign Tables | **Não implementado atualmente.** As fontes são consumidas por HTTPS e persistidas pelas classes da aplicação. |
| Índice HNSW | **Não implementado atualmente.** Existe coluna `%Vector` e busca por `VECTOR_COSINE`, sem índice ANN. |
| Full-text index no IRIS | **Não implementado atualmente.** O ranking lexical ocorre em Python. |
| OCR | **Não implementado atualmente.** A extração depende da camada textual do PDF. |
| Streaming CSV ponta a ponta | **Não implementado atualmente.** O download é streaming, mas o parser materializa registros. |
| Checkpoint incremental por fonte | **Não implementado atualmente.** Upserts e hashes tornam a reexecução idempotente, mas as fontes são relidas/reconstruídas. |
| Scheduler/produção | **Não implementado atualmente.** A ingestão é acionada por comando. |
| IRIS interoperability, Business Rules e IntegratedML | **Não implementados atualmente.** |
| Avaliação automatizada de retrieval | **Melhoria futura.** Criar conjunto de perguntas, métricas de recall/nDCG e regressão de citações. |
| Otimização de índices | **Melhoria futura.** Medir o corpus e avaliar HNSW/full-text sem mudar a semântica do retrieval. |
## Recursos do IRIS demonstrados
O pipeline evidencia, com implementação verificável:
- oito classes `%Persistent` com relacionamentos, índices e unicidade;
- SQL relacional e Object API no Embedded Python;
- `%Stream.GlobalCharacter` para documentos e auditoria bruta;
- `%Vector` de dimensão fixa e `VECTOR_COSINE`;
- transações explícitas e uma trilha de ingestão auditável;
- armazenamento multimodelo no mesmo banco usado pela API e pelo RAG;
- Flask servido pelo WSGI nativo do IRIS em `/api`.
Esse conjunto sustenta os critérios de RAG, Hybrid Search, APIs públicas, estratégia explícita de chunking/embedding e uso multimodelo previstos no [Concurso de Programação InterSystems 2026](https://pt.community.intersystems.com/post/concurso-de-programa%C3%A7%C3%A3o-da-comunidade-de-desenvolvedores-da-intersystems-pt-2026), sem reivindicar recursos ainda não implementados.