Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 

Repository files navigation

Sistema de Extração de Dados de PDF

Uma solução eficiente em custo e com alta precisão para extrair dados estruturados de PDFs usando IA. Foco em baixo custo, previsibilidade e simplicidade operacional.

Visão Geral

O sistema extrai informações estruturadas de PDFs que já possuem texto embutido (sem OCR). Combina:

  • Extração de texto local (PyPDF2)
  • Preenchimento de campos ausentes por heurísticas baseado em dados quando confiável
  • Redução de tokens por por heurísticas baseado em dados
  • Preenchimento de campos ausentes via LLM (OpenAI GPT-5-mini)
  • Cache adaptativo com TTL em SQLite (texto e resultados), além de cache por sessão
  • API web (Flask) e interface web simples para upload e teste

Metas de engenharia:

  • Minimizar chamadas ao LLM (cache + prompts focados só nos campos faltantes)
  • Código limpo, tipado, documentado e orientado a classes
  • Segurança básica para publicação (segredos via .env, .gitignore, etc.)

Arquitetura

Pipeline: PDF (bytes) → Parser (texto) → Prefill de resultados prévios(busca na memória cache ou inferência por heuristicas) → LLM (somente campos faltantes com texto do documento reduzido pelas heuristicas) → Ordenação dos campos → Inserção de dados sobre o preenchimento no BD(Base de Conhecimento) → Cache (texto + resultado)

Componentes principais (class-based):

  • extractor/parsing_layer.py
    • PDFTextExtractor: extrai texto do PDF (PyPDF2). Implementação atual usa a primeira página; é trivial expandir para todas as páginas.
  • extractor/llm_layer.py
    • LLMClient: encapsula OpenAI e expõe fill_missing(text, schema, current). Retorna JSON estrito e preenche apenas chaves faltantes.
  • extractor/cache_layer.py
    • AdaptiveCache: cache em SQLite com compressão (zlib) e TTL. Armazena texto por pdf_sha e resultado por (schema_key, pdf_sha).
  • extractor/heuristics_layer.py
    • HeuristicsEngine: Aplica estratégias baseadas em padrões históricos em documentos do mesmo tipo para reduzir os tokens enviados a LLM e evitar chamadas ao LLM quando possível.
  • extractor/pattern_store.py
    • PatternStore: Armazena texto por pdf_sha e informações sobre posição das palavras chaves(elemento de busca) e respectivos valores(resultado da busca) oriundos da extração da LLM no texto.
  • services/extraction_service.py
    • ExtractionService: orquestrador do pipeline. Respeita cache por sessão (conjuntos seen_text/seen_results) e coordena Parser, Cache e LLM.
  • app.py (Flask)
    • Camada HTTP. Mantém sessão e delega para ExtractionService. Não contém regra de negócio.
  • Frontend (templates/static)
    • index.html, app.js, styles.css: UI simples para envio de PDF e schema JSON.

Cache e Performance

  • Cache de texto: por SHA-256 do PDF (pdf_sha). Guarda texto comprimido e invalida por TTL.
  • Cache de resultado: por (schema_key, pdf_sha). Guarda texto comprimido (fotografia do input usado) e JSON do resultado.
  • Reuso por sessão (política atualizada):
    • Resultado: só é reutilizado se a combinação {schema_key}:{pdf_sha} já tiver sido produzida e “liberada” nesta sessão (seen_results).
    • Prefill: agora considera apenas resultados previamente autorizados nesta sessão para o mesmo pdf_sha (evita contaminação entre usuários/modos).
    • Texto do PDF: é armazenado no SQLite, mas a sessão só lê do cache após ter processado esse pdf_sha ao menos uma vez na própria sessão (o primeiro uso parseia e persiste; os seguintes nesta sessão fazem HIT).
  • Classificação da fonte (meta.source):
    • cache: hit exato de resultado (schema_key+pdf_sha) permitido na sessão.
    • hybrid: quando todos os campos vieram de prefill (sem LLM) ou prefill+LLM parcial.
    • llm: quando dependeu do LLM sem prefill útil.
  • TTL configurável via variável de ambiente.

Segurança e Boas Práticas

  • Segredos via .env (ex.: OPENAI_API_KEY). O arquivo .env está no .gitignore; use .env.example como base.
  • Sem upload de arquivos persistentes: PDFs são gravados em arquivos temporários e removidos após processamento.
  • Logs mínimos (sem conteúdo sensível) e foco em métricas de tempo de LLM em llm_timings.log.

Instalação

Pré-requisitos:

  • Python 3.10+
  • Chave de API da OpenAI

Passos:

  1. (Opcional) Criar e ativar virtualenv
python3 -m venv .venv
source .venv/bin/activate
  1. Instalar dependências
pip install -r pdf_extractor_service/requirements.txt
  1. Configurar ambiente
cp pdf_extractor_service/.env.example pdf_extractor_service/.env
# edite pdf_extractor_service/.env e defina OPENAI_API_KEY e FLASK_SECRET_KEY

Execução

  1. Iniciar o servidor Flask
python pdf_extractor_service/app.py
  1. Acessar a UI
  • Abra http://localhost:5000
  • Forneça um label (livre), um schema JSON (ex.: {"nome": "...", "inscricao": "..."}) e um PDF.

Alternativa: usando o script run.sh

O script de conveniência prepara o ambiente (venv, dependências, .env, permissões do SQLite) e inicia o app automaticamente:

bash pdf_extractor_service/run.sh

Requisitos:

  • Bash e Python 3 disponíveis no PATH
  • Arquivo pdf_extractor_service/.env (pode copiar de .env.example)

Interface

  • Interface web: abas “Único” e “Lote (JSON + pasta)”. Em Lote, é possível:
    • Enviar lote síncrono (botão “Enviar lote”), que retorna JSON completo ao final. As métricas por item são fornecidas via header X-Extraction-Items-Meta e exibidas apenas na UI.
    • Enviar lote em streaming NDJSON (botão “Enviar lote (streaming)”), que vai exibindo resultados e métricas por item conforme ficam prontos.

API

Endpoint: POST /extract (multipart/form-data)

Campos:

  • label: string livre (identificador lógico)
  • extraction_schema: string JSON (objeto com chaves e descrições)
  • pdf: arquivo PDF

Exemplo curl:

curl -X POST http://localhost:5000/extract \
  -F "label=carteira_oab" \
  -F 'extraction_schema={"nome":"Nome","inscricao":"Inscrição"}' \
  -F "pdf=@/caminho/arquivo.pdf"

Resposta: JSON contendo as chaves do schema (sem meta). Meta é enviada apenas por headers para uso na UI:

  • X-Extraction-Source: cache | hybrid | llm
  • X-Extraction-Elapsed: tempo em segundos (string, 3 casas)
  • X-Extraction-Cost: custo em USD (6 casas) quando aplicável

Endpoint: POST /extract-batch

Modos suportados:

  • application/json: body é uma lista de itens [{label, extraction_schema, pdf_path}]; os PDFs são lidos de uma pasta segura definida por PDF_EXTRACTOR_FILES_BASE_DIR.
  • multipart/form-data: enviar dataset (JSON array no formato acima) + múltiplos arquivos no campo pdfs. O match usa o basename de pdf_path.

Resposta (JSON):

{
  "processed": N,
  "items": [ { "index": i, "file": "...", "status": 200, "result": {...} } | { "index": i, "file": "...", "status": code, "error": "..." } ]
}

Meta de cada item é enviada somente por header X-Extraction-Items-Meta (JSON compactado) com entradas {index, file, source, elapsed_s, cost_usd}.

Endpoint: POST /extract-batch-stream

Resposta: Content-Type: application/x-ndjson (uma linha JSON por evento). Eventos por item:

  • Linha meta: { "type": "meta", "index": i, "file": "...", "source": "cache|hybrid|llm", "elapsed_s": 0.123, "cost_usd": 0.000123 | null }
  • Linha resultado: { "type": "result", "index": i, "file": "...", "status": 200, "result": { ... } } ou { "type": "result", ..., "status": 4xx/5xx, "error": "..." }

Dica: para consumir via curl, use -N (no-buffer). Na UI, o stream é lido incrementamente e mostrado conforme chega.

Heurísticas de Otimização

O sistema implementa um motor de heurísticas inteligente que aprende com extrações anteriores e otimiza futuras requisições. As heurísticas reduzem drasticamente custos ao:

  • Diminuir o tamanho do texto enviado ao LLM (menos tokens de entrada)
  • Preencher campos automaticamente quando há alta confiança (evita chamada ao LLM)
  • Eliminar completamente a chamada ao LLM quando todos os campos são preenchidos por heurísticas

Critérios de Ativação

As heurísticas são ativadas apenas quando há dados representativos suficientes:

  • Mínimo de amostras (min_samples): Por padrão, 100 PDFs únicos do mesmo doc_type são necessários
  • Configurável via variável de ambiente: PDF_EXTRACTOR_HEUR_MIN_SAMPLES

Sem dados suficientes, o sistema opera em modo "aprendizado", coletando estatísticas sem aplicar otimizações.

1. Verificação de Presença de Chaves

Objetivo: Evitar perda de dados ao fazer trim do texto.

Como funciona:

  • Verifica se todas as chaves do schema estão presentes no texto extraído
  • Se alguma chave estiver faltando, desabilita as heurísticas de trim (1 e 2)
  • Outras heurísticas (extração por posição, enum, regex) ainda são tentadas

Impacto:

  • Proteção contra perda de dados em documentos com layout variável
  • Garante que o LLM receba contexto completo quando necessário

Exemplo:

Schema: {"nome": "...", "cpf": "...", "rg": "..."}
Texto: "Nome: João Silva CPF: 123.456.789-00"
Resultado: Chave "rg" não encontrada → trim desabilitado

2. Trim Antes da Primeira Chave

Objetivo: Remover texto irrelevante no início do documento (cabeçalhos, logos, etc.).

Como funciona:

  • Calcula quantos PDFs têm valores antes da primeira chave do schema
  • Se < 20% dos casos têm valores antes da primeira chave, remove todo o texto anterior
  • Ativação: Requer min_samples + todas as chaves presentes no texto

Impacto:

  • Redução no tamanho do texto em documentos com cabeçalhos extensos
  • Economia de tokens de entrada no LLM

Exemplo:

Texto original:
"
GOVERNO DO ESTADO
SECRETARIA DE SEGURANÇA
=============================
Nome: João Silva
CPF: 123.456.789-00
"

Após trim:
"
Nome: João Silva
CPF: 123.456.789-00
"

3. Trim de Cauda Baseado em Distância

Objetivo: Remover texto após os valores relevantes (rodapés, observações, etc.).

Como funciona:

  • Para cada chave, calcula a distância média (μ) e desvio padrão (σ) entre a chave e seu valor
  • Se σ < 50 caracteres (alta consistência), corta o texto em posição_da_chave + μ + 3σ
  • Ativação: Requer min_samples + σ < 50 + todas as chaves presentes

Impacto:

  • Redução no tamanho do texto em documentos com rodapés/anexos
  • Maior economia em documentos padronizados (carteiras, certidões, etc.)

Exemplo:

Estatísticas para "nome" em RG:
- Média de distância: 15 caracteres
- Desvio padrão: 8 caracteres
- Corte em: posição("nome") + 15 + 3*8 = posição + 39 chars

Texto original:
"Nome: João Silva
Observações: Válido em todo território nacional.
Emitido conforme Lei 12.345/2020..."

Após trim:
"Nome: João Silva
Observações: Válido"

4. Extração por Posição Fixa

Objetivo: Extrair valores diretamente quando aparecem sempre na mesma posição.

Como funciona:

  • Calcula a consistência de posição: % de PDFs onde o valor está em posição ±10 caracteres
  • Se consistência > 90%, tenta extrair o valor mais comum naquela posição
  • Busca em janela de ±20 caracteres ao redor da posição esperada
  • Ativação: Requer min_samples + consistência > 90%

Impacto:

  • Preenchimento automático de campos altamente previsíveis
  • Evita chamada ao LLM para esses campos específicos
  • Funciona muito bem em documentos com template fixo (formulários, certidões)

Exemplo:

Padrão detectado em 100 CNHs:
- Campo "categoria": sempre na posição 850-860
- Valor mais comum: "AB" (95 ocorrências)

Novo PDF:
- Busca em texto[830:880]
- Encontra "AB" → Preenche campo automaticamente
- LLM não é chamado para este campo

5. Detecção de Valores Enumerados

Objetivo: Preencher campos com valores de um conjunto conhecido (estados, categorias, status, etc.).

Como funciona:

  • Identifica valores que aparecem >= N vezes no histórico (N >= 20)
  • Busca esses valores no texto usando word boundaries (evita matches parciais)
  • Preenche o primeiro valor encontrado
  • Ativação: Requer min_samples + valor aparecer >= enum_min_occurrences vezes
  • Configurável via: PDF_EXTRACTOR_ENUM_MIN_OCCURRENCES

Impacto:

  • Preenchimento instantâneo de campos categóricos
  • Especialmente eficaz para: estados (SP, RJ, MG), categorias (A, B, C, D), sexo (M, F)
  • Evita chamada ao LLM para esses campos

Exemplo:

Histórico de campo "uf" em 50 RGs:
- "SP": 35 ocorrências
- "RJ": 10 ocorrências
- "MG": 5 ocorrências

Novo PDF com texto: "Emitido em São Paulo - SP"
- Busca valores conhecidos no texto
- Encontra "SP" com word boundary
- Preenche automaticamente

6. Extração por Regex (Padrões Únicos)

Objetivo: Extrair valores com formato bem definido (CPF, CNPJ, datas, etc.).

Como funciona:

  • Detecta o tipo de campo pelo nome da chave (ex: "cpf", "data_nascimento", "telefone")
  • Aplica regex específico para o tipo detectado
  • Requer exatamente 1 ocorrência no texto (evita ambiguidade)
  • Preenche automaticamente se encontrar match único

Tipos suportados:

  • CPF: cpf → padrão XXX.XXX.XXX-XX ou XXXXXXXXXXX
  • CNPJ: cnpj → padrão XX.XXX.XXX/XXXX-XX ou XXXXXXXXXXXXXX
  • Data: data, nascimento, emissao, validade → padrões DD/MM/YYYY, DD-MM-YYYY, etc.
  • CEP: cep → padrão XXXXX-XXX ou XXXXXXXX
  • Telefone: telefone, fone, celular → padrões (XX) XXXXX-XXXX, (XX) XXXX-XXXX, etc.

Impacto:

  • Preenchimento instantâneo de campos com formato único
  • Evita chamada ao LLM quando há exatamente 1 ocorrência do padrão
  • Funciona mesmo sem histórico (não depende de min_samples)

Exemplo:

Schema: {"nome": "...", "cpf": "..."}
Texto: "João da Silva - CPF: 123.456.789-00"

Processamento:
1. Detecta campo "cpf" no schema
2. Busca padrão de CPF no texto
3. Encontra exatamente 1 ocorrência: "123.456.789-00"
4. Preenche automaticamente
5. LLM só é chamado para o campo "nome"

Combinação de Heurísticas

As heurísticas são aplicadas em sequência e de forma complementar:

Cenário 1 - Documento padronizado (melhor caso):

Input: RG com 100 amostras históricas
1. Trim antes da primeira chave: Remove cabeçalho (redução de tokens)
2. Trim de cauda: Remove rodapé (redução de tokens)
3. Posição fixa: Preenche "uf" (alta consistência)
4. Enum: Preenche "orgao_emissor" (SSP encontrado)
5. Regex: Preenche "data_emissao" (única data no texto)
6. LLM: Chamado apenas para "nome" e "rg"

Resultado: menos tokens e menos campos para o LLM

Cenário 2 - Documento novo (sem histórico):

Input: Tipo de documento nunca visto
1. Heurísticas 1-5: Desabilitadas (min_samples não atingido)
2. Regex: Tenta preencher campos com padrões únicos
3. LLM: Chamado para todos os campos restantes
4. Sistema aprende: Grava posições/valores para futuras otimizações

Resultado: Apenas regex aplicado, mas sistema começa a aprender

Cenário 3 - Documento variável:

Input: Contrato com layout inconsistente
1. Verificação de chaves: Algumas chaves ausentes no texto
2. Trim: Desabilitado (risco de perda)
3. Posição/Enum: Preenchem campos previsíveis
4. LLM: Chamado com texto completo para campos restantes

Resultado: Proteção contra perda + otimização parcial

Configuração e Ajuste Fino

Variáveis de ambiente para ajustar comportamento:

PDF_EXTRACTOR_HEUR_MIN_SAMPLES=100

PDF_EXTRACTOR_ENUM_MIN_OCCURRENCES=20

Variáveis de Ambiente

  • OPENAI_API_KEY: chave da OpenAI
  • PDF_EXTRACTOR_CACHE_DB : caminho do SQLite (sugestão: ./pdf_extractor_cache.sqlite3)
  • PDF_EXTRACTOR_CACHE_TTL : TTL em segundos (sugestão: 3600)
  • PDF_EXTRACTOR_MODEL: modelo do LLM (sugestão: gpt-5-mini)
  • PDF_EXTRACTOR_FILES_BASE_DIR : pasta base segura para o modo JSON do /extract-batch (sugestão: ./files)
  • PORT: porta do Flask (sugestão: 5000)
  • FLASK_SECRET_KEY: chave de sessão Flask
  • PDF_EXTRACTOR_HEUR_MIN_SAMPLES : número mínimo de PDFs únicos para ativar heurísticas (sugestão: 100)
  • PDF_EXTRACTOR_ENUM_MIN_OCCURRENCES: ocorrências mínimas para valores enumerados (sugestão: 20)

Custos (LLM)

Cálculo estimado de custo por requisição (usado apenas para exibição na UI):

  • Input: US$ 0,075 por 1M tokens
  • Output: US$ 0,30 por 1M tokens O sistema computa cost_usd quando a resposta inclui métricas de uso do modelo.

Estrutura do Projeto (resumo)

.
├── LICENSE
├── README.md                  # Este arquivo
└── pdf_extractor_service
    ├── app.py                 # Flask app
    ├── extractor
    │   ├── cache_layer.py     # AdaptiveCache (SQLite + TTL)
        ├── heuristics_layer.py # heuristicas 
    │   ├── llm_layer.py       # LLMClient (OpenAI)
    │   ├── parsing_layer.py   # PDFTextExtractor (PyPDF2)
        ├── pattern_store.py   # SQLite que armazena dados das inferências da LLM (autoaprendizado)
    │   └── utils.py           # Logger, locks e utilidades
    ├── services
    │   └── extraction_service.py # Orquestrador do pipeline
    ├── templates
    │   └── index.html         # UI
    └── static
        ├── app.js             # lógica da UI
        └── styles.css         # estilos

Logs e Artefatos

  • llm_timings.log: tempos de preparo, chamada e parsing de respostas do LLM
  • pdf_extractor_cache.sqlite3: banco do cache com TTL (ignorado no git)
  • results.txtcl: arquivo NDJSON opcional para dumps/batch (se usado)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages