O campo que não cabe
A Unidade 2 terminou dizendo que havia um campo que ela não tinha tocado.
Este é o capítulo dele.
movimentos[] é a lista de andamentos de um processo: cada despacho, cada conclusão, cada mandado expedido, com o código da Tabela Processual Unificada, o nome do movimento e a data e hora em que aconteceu. É o campo mais útil da API do DataJud para quem julga — é onde está a resposta para "o que andou nesse processo" — e é o único campo do índice capaz de quebrar o seu agente sozinho.
Este capítulo não escreve Tool nenhuma. Ele mede.
Um movimento, de perto
Antes de contar quantos são, vale ver como é um. Este veio do processo 0012105-73.2003.8.19.0042, exatamente como a API o devolve:
{
"complementosTabelados": [
{
"codigo": 2,
"valor": 2,
"nome": "sorteio",
"descricao": "tipo_de_distribuicao_redistribuicao"
}
],
"codigo": 26,
"nome": "Distribuição",
"dataHora": "0995-09-12T00:00:00.000Z"
}
Três coisas aqui merecem nota, e as três voltam depois.
O código 26 é nacional. 26 = Distribuição na TPU, e vale para o TJRJ, para o TJSP e para o STJ. Se você um dia for comparar acervos, é por esse campo — não pelo nome, que é a versão legível e pode variar.
complementosTabelados é um array dentro do array. Um movimento pode carregar complementos — o tipo de distribuição, o motivo da remessa, a classe nova numa mudança de classe. É informação boa, e é a maior parte do peso do campo.
A data diz 0995. Ano novecentos e noventa e cinco. Um processo de 2003, distribuído mil anos antes. Não é erro de leitura: é o que está no índice nacional. Guarde isso — vamos precisar dele no capítulo seguinte, quando a Tool tiver que ordenar por data.
Dado público real vem sujo. Não é defeito do DataJud especificamente; é o que acontece quando 90 tribunais alimentam uma base com décadas de migração de sistema atrás.
O ponto prático: uma Tool que só funciona com dado limpo não é uma Tool, é uma demonstração. A sua vai receber ano 995 uma hora, e o que ela faz nesse dia é decisão sua, tomada hoje.
A pergunta que a API se recusa a responder
A primeira tentativa de medir foi a óbvia: perguntar ao próprio Elasticsearch quantos movimentos os processos têm, em média, usando uma agregação com script.
{
"size": 0,
"query": { "match_all": {} },
"aggs": {
"qtd_movimentos": {
"stats": {
"script": {
"source": "params._source.movimentos == null ? 0 : params._source.movimentos.size()"
}
}
}
}
}
Resposta:
cnj.ErroDataJud: HTTP 504: <html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>NSX LB</center>
</body>
</html>
Faz sentido. A consulta manda o cluster do CNJ abrir o _source de 23 milhões de documentos e contar um array em cada um. O balanceador corta antes de o cluster terminar, e você recebe um HTML de gateway em vez de um erro do Elasticsearch — a resposta nem chegou a ser do índice.
Não há truque para contornar isso, e nem deveria haver: uma API pública que deixasse qualquer um rodar script sobre a base inteira não duraria uma semana.
Então medimos como se mede fora de laboratório: por amostra.
resposta = consultar({
"size": 200,
"query": {"bool": {"filter": [{"range": {"nivelSigilo": {"lte": 0}}}]}},
})
qtds = [len(p["_source"].get("movimentos") or []) for p in resposta["hits"]["hits"]]
Duzentos processos públicos, contados na sua máquina.
Quanto pesa, de fato
| movimentos por processo | |
|---|---|
| mínimo | 3 |
| mediana | 74 |
| média | 99,4 |
| percentil 90 | 222 |
| percentil 95 | 289 |
| percentil 99 | 509 |
| máximo | 613 |
A mediana é 74. Metade dos processos passa disso.
O campeão da amostra é o 0018683-81.2020.8.19.0066 — Procedimento Especial da Lei Antitóxicos, 2ª Vara de Barra do Piraí — com 613 movimentos, 105.123 bytes só no array de andamentos. Cento e cinco mil caracteres, de um processo só.
Uma intuição que este curso teve e a medição desmentiu.
A Unidade 2 supôs que os processos antigos seriam os pesados. É o palpite natural: mais anos, mais andamentos.
Os dez processos mais antigos do índice — incluindo um de 1880 e um de 1901 — têm média de 4,4 movimentos, máximo de 10. Chegaram ao DataJud por migração, com histórico praticamente vazio.
Já os dez processos recentes de Violência Doméstica Contra a Mulher têm média de 23,9. E o campeão, com 613, é de 2020.
O que pesa não é a idade: é a tramitação. Processo criminal em curso, com mandados, intimações e certidões, acumula andamento rápido.
Vale a regra geral: neste curso, quando a intuição e a medição divergirem, quem fica no texto é a medição.
O número que importa não é o do processo
613 movimentos é um número interessante. Não é o número que decide nada.
O que decide é: quanto disso entra numa Observation.
Lembre do ciclo da Unidade 1. A Tool devolve, o retorno vira Observation, a Observation entra no contexto do modelo, e o modelo escreve o passo seguinte lendo aquilo. Tudo o que a sua Tool devolve ocupa janela — e ocupa de novo a cada passo, porque o histórico vai junto.
Agora imagine a pergunta mais natural do mundo para um magistrado:
"Me mostre os últimos andamentos dos processos de violência doméstica dessa vara."
Cinco processos. Se a Tool devolver os cinco inteiros, com movimentos[] completo, isto é o que chega ao modelo:
| bytes | tokens (estimados) | |
|---|---|---|
| 5 processos inteiros | 434.717 | ~108.700 |
| os mesmos 5, com 5 movimentos cada | 1.446 | ~360 |
Trezentas vezes menor.
Os tokens acima são estimativa por regra de bolso — cerca de 4 bytes por token em português. Não é contagem de tokenizador, e o número exato muda com o modelo.
Não muda a conclusão. Mesmo errando por um fator de dois, 108 mil tokens não cabem: o qwen2:7b que você rodou na Unidade 2 trabalha com janela de alguns milhares. A Observation não seria truncada de forma elegante — o passo simplesmente falha, ou o modelo recebe metade de um JSON e raciocina em cima do pedaço.
E repare no detalhe cruel: isso acontece num único passo. Não é degradação gradual que você percebe e corrige. É o primeiro for sobre cinco processos.
O mesmo número, dois documentos
Uma última descoberta da medição, e ela muda o desenho da Tool.
Procurando o campeão pelo número:
consultar({
"size": 5,
"query": {"term": {"numeroProcesso": "00186838120208190066"}},
"_source": ["numeroProcesso", "grau", "classe.nome"],
})
Duas respostas:
{"numeroProcesso": "00186838120208190066", "grau": "G1",
"classe": {"nome": "Procedimento Especial da Lei Antitóxicos"}}
{"numeroProcesso": "00186838120208190066", "grau": "G2",
"classe": {"nome": "Apelação Criminal"}}
O mesmo processo, dois documentos no índice, classes diferentes. Em primeiro grau é ação penal; em segundo, apelação. Cada um com a sua própria lista de movimentos.
Isso é correto e é assim que o DataJud modela: um documento por processo por grau. Mas significa que movimentos[] não é uma coisa só, e que uma Tool que pegue o primeiro hit e ignore o resto vai devolver metade da história sem avisar ninguém.
É o mesmo tipo de erro da Unidade 2 — não dá exceção, dá resultado.
O que este capítulo estabeleceu
Quatro fatos, todos medidos, nenhum suposto:
- A mediana é de 74 movimentos por processo; o máximo encontrado, 613.
- Devolver
movimentos[]inteiro de 5 processos põe ~108 mil tokens numa única Observation. - O peso vem da tramitação, não da idade do processo.
- Um número de processo corresponde a um documento por grau, cada um com a sua lista.
O capítulo seguinte escreve a Tool que lida com isso. E a pergunta que ela responde não é "como trazer movimentos[]" — a API já traz, é fácil.
É: o que não devolver.