Pular para o conteúdo

Funcionalidade: Enriquecimento de Chunks

pipeline/enrich_chunks.py — Classifica cada chunk usando o LLM MiniMax M2.5 para adicionar metadados juridicos estruturados. E a etapa que transforma texto bruto em conhecimento pesquisavel e filtravel, associando a cada chunk seu conceito juridico (instituto), tipo de conteudo, ramo do direito, fase processual e fontes normativas.

PropriedadeValor
Scriptpipeline/enrich_chunks.py (403 linhas)
EntradaArquivos markdown com status_enriquecimento: "pendente"
SaidaMesmos arquivos com frontmatter enriquecido (13+ campos de metadados)
LLMMiniMax M2.5 via Anthropic SDK com base_url customizada
Concorrencia5 threads, 0.5s de delay entre requests
IdempotenteSim — pula chunks com status_enriquecimento: "completo" ou "lixo"
flowchart TD
LOAD["Carrega template enrich_prompt.md"]
SCAN["Varre chunks nao enriquecidos"]
NOISE["Verifica: is_noise_chunk?"]
SKIP["Marca como lixo, pula"]
SEND["Envia ao MiniMax M2.5"]
PARSE["extract_json() da resposta"]
MERGE["merge_classification() no frontmatter"]
WRITE["Grava arquivo enriquecido"]
LOAD --> SCAN
SCAN --> NOISE
NOISE -->|"Sim (prefacio, dedicatoria, <200 chars)"| SKIP
NOISE -->|"Nao"| SEND
SEND --> PARSE
PARSE -->|"JSON valido"| MERGE
PARSE -->|"Invalido/vazio"| ERR["Log de erro, contador++"]
MERGE --> WRITE

O prompt de enriquecimento e carregado de pipeline/enrich_prompt.md. Variaveis placeholder sao substituidas por chunk:

  • {livro_titulo} — titulo do livro
  • {autor} — nome do autor
  • {capitulo} — titulo do capitulo
  • {chunk_numero} / {chunk_total} — posicao do chunk
  • {chunk_content} — primeiros 8.000 caracteres do corpo do chunk

Antes de enviar ao LLM, cada chunk e verificado contra padroes de ruido. Chunks que correspondem a qualquer um destes sao marcados como "lixo" sem chamadas a API:

  • Titulo contem: “prefacio”, “agradecimento”, “dedicatoria”, “nota do editor”, “sobre o autor”, etc.
  • Texto do corpo tem menos de 200 caracteres

O texto do chunk + prompt e enviado ao MiniMax M2.5 via Anthropic SDK com base URL customizada:

client = anthropic.Anthropic(
api_key=api_key,
base_url="https://api.minimax.io/anthropic"
)
message = client.messages.create(
model="MiniMax-M2.5",
max_tokens=2000,
system="Voce e um classificador juridico especializado. "
"Responda APENAS com JSON valido, sem markdown, sem backticks, sem explicacoes.",
messages=[{"role": "user", "content": prompt}]
)

A resposta do LLM e parseada usando extract_json(), que possui um fallback de contagem de chaves para casos em que o LLM envolve o JSON em code fences markdown ou adiciona texto ao redor:

def extract_json(text: str) -> dict:
text = text.strip()
text = re.sub(r'^```json\s*', '', text)
text = re.sub(r'\s*```$', '', text)
text = text.strip()
# Try direct parse first
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# Extract first complete JSON object by brace matching
start = text.find('{')
if start == -1:
raise json.JSONDecodeError("No JSON object found", text, 0)
depth = 0
in_string = False
escape = False
for i in range(start, len(text)):
c = text[i]
if escape:
escape = False
continue
if c == '\\' and in_string:
escape = True
continue
if c == '"' and not escape:
in_string = not in_string
continue
if in_string:
continue
if c == '{':
depth += 1
elif c == '}':
depth -= 1
if depth == 0:
return json.loads(text[start:i+1])
raise json.JSONDecodeError("Incomplete JSON object", text, len(text))

Os campos JSON parseados sao mesclados no frontmatter existente via merge_classification(), e metadados de status sao adicionados:

enriched["status_enriquecimento"] = "completo"
enriched["data_enriquecimento"] = datetime.now().isoformat()
enriched["modelo_enriquecimento"] = "MiniMax-M2.5"

A funcao merge_classification() mapeia 13 campos da resposta do LLM para o frontmatter do chunk. Esses campos sao a base para toda busca e filtragem posterior.

#CampoTipoDescricaoExemplos
1categoriastrCategoria de alto nivel"doutrina", "legislacao_comentada"
2tipo_contratualstrTipo de contrato (se aplicavel)"compra_e_venda", "locacao"
3objeto_especificostrAssunto especifico"clausula_penal", "exceptio"
4institutolist[str]Conceitos/institutos juridicos["exceptio_non_adimpleti_contractus", "contrato_bilateral"]
5sub_institutolist[str]Sub-conceitos["inadimplemento_relativo"]
6faselist[str]Fase contratual/processual["formacao", "execucao", "extincao"]
7ramostrRamo do direito"direito_civil", "processo_civil"
8fontes_normativaslist[str]Referencias legislativas["CC art. 476", "CC art. 477"]
9tipo_conteudolist[str]Classificacao de tipo de conteudo["definicao", "requisitos", "jurisprudencia_comentada"]
10utilidadestrUtilidade pratica"alta", "media", "baixa"
11confiabilidadestrConfiabilidade da fonte"alta", "media"
12jurisdicao_estrangeirabool ou strReferencia a jurisdicao estrangeirafalse, "common_law"
13justificativastrRaciocinio do LLM para a classificacaoTexto livre explicando a logica de classificacao

Alem desses 13 campos fornecidos pelo LLM, merge_classification() adiciona 3 campos de sistema:

CampoTipoValor
status_enriquecimentostr"completo"
data_enriquecimentostrTimestamp ISO 8601
modelo_enriquecimentostr"MiniMax-M2.5"
VariavelObrigatoriaDescricao
MINIMAX_API_KEYSim (exceto --dry-run)Autenticacao da API MiniMax
VAULT_PATHNaoDiretorio base (padrao: /mnt/c/Users/sensd/vault)
Terminal window
# Enriquecer todos os chunks nao enriquecidos de todos os livros
python3 pipeline/enrich_chunks.py all
# Enriquecer um livro especifico
python3 pipeline/enrich_chunks.py contratos-orlando-gomes
# Re-enriquecer chunks ja completos
python3 pipeline/enrich_chunks.py all --force
# Limitar aos primeiros N chunks (para testes)
python3 pipeline/enrich_chunks.py all --limit 10
# Simular sem chamadas a API
python3 pipeline/enrich_chunks.py all --dry-run
# Usar uma API key especifica
python3 pipeline/enrich_chunks.py all --api-key "sk-..."
# Ajustar numero de threads (padrao: 5)
python3 pipeline/enrich_chunks.py all --workers 3
ConfiguracaoValorDescricao
WORKERS5Numero de threads concorrentes
DELAY_BETWEEN_REQUESTS0.5sDelay apos cada chamada de API (por thread)

O tempo estimado de processamento e exibido na inicializacao:

Estimativa: ~{(total * 0.5) / workers / 60:.0f} min

Para 1.000 chunks com 5 workers: aproximadamente 1,7 minutos.

Os resultados sao adicionados a Logs/enrichment_log.jsonl:

{
"timestamp": "2026-02-28T14:30:00",
"file": "/path/to/chunk.md",
"success": true,
"model": "MiniMax-M2.5",
"tags_count": 3,
"tipo_conteudo": ["definicao", "requisitos"]
}
  • Sem validacao de schema na saida do LLM. Se o LLM retornar valores inesperados nos campos (ex.: ramo: "unknown_branch"), o dado e aceito sem validacao. Metadados invalidos se propagam para embeddings e busca. Rastreado como mitigacao M10 no ROADMAP.
  • except Exception amplo em classify_chunk() e process_one() engole todos os erros, incluindo falhas de API, rate limits e problemas de rede. O contador de erros incrementa mas a causa especifica nao e registrada em classify_chunk().
  • Sem medicao de acuracia. A qualidade da classificacao do LLM nunca foi validada contra julgamento humano. A mitigacao M06 propoe amostrar 200 chunks para revisao manual. Se a acuracia estiver abaixo de 85%, todo o enriquecimento deve ser refeito.
  • MiniMax via Anthropic SDK usa uma camada de compatibilidade nao documentada. Se a MiniMax alterar sua API, a integracao pode quebrar silenciosamente. A decisao D06 no ROADMAP considera migracao para Claude ou modelos locais.
  • Thread safety depende de threading.Lock() para contadores e gravacao de logs. O client anthropic.Anthropic e compartilhado entre threads sem documentacao explicita de que e thread-safe.
  • Conteudo do chunk e truncado em 8.000 caracteres antes do envio ao LLM. Chunks mais longos podem ter conteudo importante no final que o classificador nunca ve.