Você não está olhando para “um TCC com app”. Você está olhando para uma arquitetura que transforma relato bruto em triagem jurídica rastreável.
O repo mba-tcc junta três coisas: corpus da Corte IDH, pipeline factual em Python e uma interface web para intake + análise. A ideia central é simples: não confiar na memória do modelo, e sim em navegação orientada por documentos e citações.
Corpus jurídico
Os casos brasileiros da Corte IDH viram base navegável, não só contexto solto jogado no prompt.
Pipeline factual
O backend transforma a narrativa em cenário, chama tools e grava artefatos de execução.
Interface de triagem
O frontend guia o usuário, mostra o progresso e exibe o raciocínio em formato legível.
Esse projeto troca o padrão “pergunte ao modelo e reze” por “busque, recupere, compare e só depois sintetize”. Em software, isso é sair de palpite para procedimento.
@app.post("/api/analyze")
async def analyze(payload: TriageRequest) -> dict[str, Any]:
item = _build_item(payload)
config = _build_config(payload)
result = await asyncio.to_thread(run_factual_triage, item, config=config)
return {"ok": True, "result": result}
Esta rota existe para receber um pedido completo de análise.
O payload vindo da interface é transformado num item interno que o pipeline entende.
As configurações de modelo e limites viram um pacote separado, para a execução ficar controlada.
O trabalho pesado roda fora da thread principal, para a API continuar responsiva.
Quando termina, a API devolve um resultado estruturado em vez de texto solto.
Se você quisesse copiar a ideia central desse projeto para outro domínio, qual parte é realmente indispensável?
Os personagens principais: quem faz o quê sem pisar no pé dos outros
Pensa nesse repo como uma equipe de investigação. Cada parte tem um papel específico: uma recebe o relato, outra coordena o processo, outra consulta os documentos e outra empacota a resposta final.
Cuida da experiência do usuário e do estado visual da sessão.
É o porteiro: recebe, valida, roteia e transmite eventos.
É o investigador: consulta ferramentas, sintetiza e persiste artefatos.
function submitAnalysis(e?: React.FormEvent) {
if (e) e.preventDefault();
if (!form.narrative.trim() || form.narrative.trim().length < 20) {
setApiError("A narrativa precisa ter pelo menos 20 caracteres.");
return;
}
const socket = new WebSocket(wsUrl);
}
Quando a pessoa envia o formulário, o frontend intercepta o evento em vez de deixar o navegador recarregar a página.
Antes de qualquer coisa, ele verifica se existe narrativa suficiente para a análise fazer sentido.
Se o texto for curto demais, o erro aparece cedo e barato.
Só depois disso a interface abre o canal em tempo real com o backend.
Quando cada camada tem um papel nítido, você consegue trocar UI, modelo ou mecanismo de busca sem reescrever tudo. Esse é um dos superpoderes de arquiteturas boas.
Se você quiser mudar como o progresso da análise aparece na tela, onde deveria mexer primeiro?
A jornada do pedido: do texto da vítima ao resultado no navegador
A melhor metáfora aqui é a de uma central de despacho. A interface recolhe o chamado, a API registra a ocorrência e o runner factual distribui o trabalho entre ferramentas especializadas.
A narrativa entra com contexto, atores, período e objetivo do usuário.
A API transforma os campos em um item interno consistente.
O pipeline faz buscas, abre trechos, consulta violações e reparações.
Enquanto trabalha, o backend vai narrando o progresso via WebSocket.
Requests, predictions e traces ficam salvos para auditoria posterior.
socket.onopen = () => {
setStatus("Triagem em andamento.");
socket.send(
JSON.stringify({
title: form.title,
narrative: form.narrative,
local: form.local,
period: form.period,
actors: form.actors,
user_goal: form.user_goal,
model: form.model,
dry_run: form.dry_run,
}),
);
};
Quando o canal abre, a interface avisa visualmente que a triagem começou.
Em seguida ela envia um pacote JSON com tudo o que o backend precisa para iniciar.
Esse pacote mistura conteúdo do caso, metadados e preferências de execução.
Repara no detalhe importante: o frontend não tenta “pensar juridicamente”; ele só entrega o pedido bem formatado.
@app.websocket("/ws/triage")
async def triage_socket(websocket: WebSocket) -> None:
await websocket.accept()
try:
raw = await websocket.receive_json()
payload = TriageRequest.model_validate(raw)
except Exception as exc:
await websocket.send_json({"type": "error", "error": f"payload inválido: {exc}"})
A API abre oficialmente a sessão em tempo real.
Depois ela espera um JSON chegar pelo socket, em vez de usar uma requisição simples sem streaming.
Esse JSON passa por validação de schema antes de qualquer execução mais cara.
Se vier quebrado, o usuário recebe erro legível imediatamente.
Porque a análise não é instantânea. Em tarefas longas, mostrar eventos intermediários reduz ansiedade do usuário e facilita debug quando algo emperra.
Se amanhã esse app precisar mostrar 15 eventos de progresso durante uma análise de 40 segundos, qual escolha arquitetural do repo já ajuda nisso?
A máquina de evidências: por que esse app deixa migalhas auditáveis em todo lugar
Aqui a metáfora não é tribunal, e sim caixa-preta de voo. Quando algo dá certo ou errado, você quer reconstruir o percurso. É isso que `run_factual_triage` faz ao salvar requests, predictions, traces e retrieval hits.
requests.jsonlO que foi pedido ao sistema naquela execuçãopredictions.jsonlA resposta final estruturada do modelo/pipelinetool_traces.jsonlO histórico das ferramentas chamadas durante a análiseretrieval_hits.jsonlOs trechos e recuperações que sustentaram a sínteseerrors.jsonlFalhas persistidas para pós-morte e repetição do experimento
run_dir = create_ui_run_dir(out_dir)
run_dir.mkdir(parents=True, exist_ok=True)
_write_run_config(run_dir, item, cfg)
requests_path = run_dir / "requests.jsonl"
predictions_path = run_dir / "predictions.jsonl"
errors_path = run_dir / "errors.jsonl"
traces_path = run_dir / "tool_traces.jsonl"
retrieval_path = run_dir / "retrieval_hits.jsonl"
Antes de analisar qualquer caso, o sistema cria uma pasta exclusiva para aquela execução.
Nessa pasta ele grava uma espécie de identidade da corrida: configuração, pedidos, resposta e rastros.
Isso transforma uma sessão efêmera em material observável e repetível.
Em projetos com IA, esse tipo de trilha vale ouro para auditoria e iteração.
Recuperação visível
Você consegue ver não só o resultado, mas quais buscas e trechos ajudaram a chegar nele.
Persistência por execução
Cada rodada ganha seu próprio diretório, facilitando comparação entre versões do sistema.
Menos magia
Quando o output parece estranho, existe caminho para investigar em vez de só culpar “a IA”.
Se um sistema com IA não deixa rastro, você quase sempre vai discutir opinião em vez de evidência quando ele falhar.
Uma análise trouxe um caso análogo claramente ruim. Qual artefato você checaria primeiro para entender por quê?
Onde mexer quando algo quebra: um mapa mental para dirigir IA e depurar rápido
Agora que você já viu os atores e o fluxo, dá para usar esse repo como um manual de intervenção. A pergunta certa quase nunca é “qual arquivo é o culpado?”, mas “em qual camada nasce esse sintoma?”.
O usuário clica em analisar e nada aparece na tela. Não sabe se o problema é validação, socket, pipeline ou renderização.
Sem resposta visual? Comece em apps/web.
Payload inválido? Confira schema em apps/api/main.py.
Rodou mas errou? Siga para scripts/eval e os artefatos em outputs/triage.
if (parsed.type === "event" && parsed.event) {
setEvents((prev) => [...prev, parsed.event]);
} else if (parsed.type === "result" && parsed.result) {
setResult(parsed.result);
setStatus("Sessão concluída.");
setLoading(false);
socket.close();
}
A interface distingue dois tipos nobres de mensagem: evento de progresso e resultado final.
Eventos entram numa lista viva; resultado fecha a sessão e atualiza o estado principal.
Se o usuário diz “travou”, esse trecho já sugere uma boa pergunta: o socket recebeu eventos? recebeu resultado? recebeu erro?
Esse é o tipo de vocabulário que ajuda muito a conversar melhor com um agente de código.
Campos, loading, status, renderização de resultado: olhe primeiro o frontend.
Erro de payload, CORS, rota ou WebSocket: a API costuma ser o gargalo.
Busca ruim, analogia ruim, schema ruim: mergulhe no pipeline factual e nos artefatos persistidos.
Depois de entender esse mapa, você consegue pedir coisas muito melhores para uma IA: “corrige o tratamento do evento no frontend” é infinitamente melhor do que “o app tá meio bugado”.