Aplicação em Python apoiar o combate à desinformação no período eleitoral e também atende a cenários mais amplos de verificação de fatos.
O repositório está em nome do Ministério Público do Estado do Acre e segue uma proposta de inovação aberta: desenvolvimento institucional com colaboração da comunidade.
O que existe hoje:
- execução local via terminal;
- workflow em
LangGraph; - roteamento entre agentes por tipo de entrada;
- agente de busca com ferramentas de data atual, descoberta de links e leitura de páginas;
- análise de imagens por modelo multimodal antes da pesquisa online;
- transcrição de áudio por API externa;
- suporte a múltiplos anexos e a links encontrados na consulta;
- síntese final estruturada da resposta;
- persistência opcional das respostas finais no Qdrant;
- geração de embeddings dense, sparse e ColBERT para busca híbrida;
- prompts separados em arquivos
.md.
O que ainda não existe ou está incompleto:
- interface web;
- API
FastAPI; - persistência de casos, protocolos e revisão humana;
O fluxo atual é:
- o usuário digita uma consulta no terminal;
- o router classifica a entrada;
- o workflow decide quais agentes executar;
- imagens, áudios e vídeos são processados em paralelo e convertidos em contexto textual;
- o agente de busca usa ferramentas externas para apuração;
- o router sintetiza a resposta final;
- se o Qdrant estiver habilitado, a persistência da pergunta e do título final é enviada para uma fila dedicada;
- o worker do Qdrant gera os embeddings e armazena um único point na collection.
O point salvo no Qdrant usa o ID do job RQ como identificador e contém:
- os vetores nomeados
dense,sparseecolbert; - o payload
text,meta,query,title,urlseurl_keys.
O uso do ID do job evita a criação de pontos duplicados caso uma mesma execução seja repetida.
Hoje os agentes disponíveis são:
search_agent: faz busca e leitura de fontes;image_agent: interpreta uma imagem pública e encaminha suas alegações ao agente de busca;transcription_agent: envia URLs públicas de áudio ou vídeo para transcrição e encaminha os textos ao agente de busca.
- Python
3.13 uvpara instalar dependências e executar o projeto- acesso aos modelos configurados para o router, a busca e a análise multimodal
- chave da SerpAPI
- serviço HTTP para converter URL em markdown, configurado nas variáveis
FETCH_SITE_* - acesso à API de transcrição, configurado nas variáveis
TRANSCRIPTION_* - acesso a uma instância Qdrant, opcional, para persistência vetorial das respostas finais
- Instale as dependências:
uv sync- Crie o arquivo
.enva partir do.env.example:
cp .env.example .env- Preencha as variáveis necessárias.
Principais grupos de configuração:
ROUTER_*: configuração da LLM do router.SEARCH_*: configuração da LLM do agente de busca.IMAGE_*: configuração opcional da LLM multimodal; semIMAGE_MODEL, reutilizaSEARCH_*.YOUTUBE_*: configuração opcional do Gemini para vídeos públicos; semYOUTUBE_MODEL, reutilizaIMAGE_*.DUPLICATE_JUDGE_*: configuração opcional do avaliador de candidatos semânticos; semDUPLICATE_JUDGE_MODEL, reutilizaROUTER_*.ATTACHMENTS_MAX_ITEMS: quantidade máxima de conteúdos aceitos em uma análise.ANALYZE_REQUESTS_DB_PATH: banco SQLite dos registros de solicitações aceitas.RQ_RETRY_INTERVALS_SECONDS: intervalos, em segundos, entre novas tentativas de uma análise que falhou; uma lista vazia desabilita o retry.SERPAPI_API_KEY: busca de links.FETCH_SITE_*: leitura e conversão de páginas web.TRANSCRIPTION_*: envio do áudio, consulta de status, polling e timeout.TRANSCRIPTION_MEDIA_RELAY_*eGCS_*: cópia temporária opcional de mídias privadas para um bucket do Google Cloud Storage.FINAL_RESULTS_*: fila e API de destino das respostas finais.QDRANT_*: conexão, collection e modelos usados na persistência vetorial opcional.SEMANTIC_RETRIEVER_*: consulta opcional de análises anteriores, usando um token do Qdrant no headerapi-key; sem collection dedicada, reutilizaQDRANT_COLLECTION_NAME.LANGSMITH_*: tracing opcional dos workflows executados pelos workers.*_PROMPT: caminhos dos prompts usados pelo workflow.
Cada valor de RQ_RETRY_INTERVALS_SECONDS representa uma nova tentativa. Por
exemplo, 30,60,120,300,600 permite cinco retries, preservando o mesmo
task_id. Enquanto aguarda o próximo intervalo, o endpoint de status apresenta
o job como queued.
Para ROUTER_*, SEARCH_*, IMAGE_*, YOUTUBE_* e DUPLICATE_JUDGE_*, o
contrato é sempre o mesmo:
*_PROVIDER:google,openaiouvllm*_MODEL: nome do modelo*_API_KEY: credencial do provider*_BASE_URL: endpoint do provider quando ele for OpenAI-compatible
router, search e image podem usar providers diferentes. O modelo configurado
em IMAGE_MODEL precisa aceitar imagens como entrada.
O agente do YouTube requer provider google. Quando YOUTUBE_MODEL não for
informado, ele reutiliza o modelo multimodal configurado em IMAGE_*.
O avaliador de duplicidade compara a consulta com até três candidatos semânticos e produz uma decisão estruturada. Ele é executado pelo worker apenas em modo consultivo e não interrompe a execução dos agentes.
O worker executa a verificação de duplicidade em modo consultivo antes do workflow.
A recuperação diferencia URL exata de candidatos semânticos, ignora
solicitações com anexos explícitos e segue com o fluxo normal quando o endpoint
ou o avaliador ficam indisponíveis. O POST /analyze continua respondendo apenas
com a aceitação do job. Ao término, /status/{task_id} e a entrega ao painel
incluem um resumo seguro em duplicate_check, sem textos recuperados ou o motivo
produzido pelo avaliador.
No LangSmith, o trace duplicate_check registra apenas metadata segura: outcome,
quantidade de candidatos, IDs, tipos, ranks, scores, decisão e confiança. A
consulta, os textos dos candidatos, o motivo do avaliador e as credenciais não
são incluídos nessa metadata. Mantenha LANGSMITH_HIDE_INPUTS=true para ocultar
também as entradas capturadas automaticamente pela chamada ao modelo.
O tracing dos workflows é opcional e permanece desabilitado por padrão. Para ativá-lo, configure uma chave e habilite o envio:
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=sua_chave_langsmith
LANGSMITH_PROJECT=verificaai-development
LANGSMITH_HIDE_INPUTS=true
LANGSMITH_HIDE_OUTPUTS=falseCada execução de análise é registrada como analyze_workflow, com o task_id e
a versão da aplicação em metadata. Isso permite correlacionar a solicitação
aceita com a execução do worker. Com LANGSMITH_HIDE_INPUTS=true, consultas,
anexos, transcrições e prompts ficam ocultos. Com LANGSMITH_HIDE_OUTPUTS=false,
respostas e retornos de ferramentas permanecem disponíveis para observação.
Antes do envio, assinaturas opacas de raciocínio retornadas pelos modelos
(signature e thought_signature) são removidas recursivamente da cópia dos
inputs e outputs registrada no LangSmith. Os objetos usados pelo workflow não
são alterados.
Uma nova tentativa é registrada como analyze_workflow_retry, com a tag retry
e os campos retry_attempt e is_retry na metadata. Todas as tentativas mantêm
o mesmo task_id.
Cada tentativa do worker de entrega ao painel também gera a trace independente
verificaai_painel_delivery, correlacionada pelo mesmo task_id. Ela registra
somente o status HTTP e se o painel respondeu com sucesso (acknowledged), sem
incluir o payload, o token de autenticação ou o corpo da resposta. Uma resposta
de sucesso confirma o aceite HTTP pelo painel, não uma consulta direta ao banco
de dados dele. Com LANGSMITH_HIDE_OUTPUTS=false, esses campos aparecem no
output da trace.
Regra prática:
google: use*_PROVIDER,*_MODELe*_API_KEY; deixe*_BASE_URLvazioopenaievllm: use os quatro campos
Exemplos:
ROUTER_PROVIDER=google
ROUTER_MODEL=gemini-2.5-flash
ROUTER_API_KEY=sua_chave_google
ROUTER_BASE_URL=SEARCH_PROVIDER=vllm
SEARCH_MODEL=Qwen/Qwen3-14B-FP8
SEARCH_API_KEY=sua_chave_vllm
SEARCH_BASE_URL=https://seu-endpoint/v1Para usar a busca nativa do Gemini no lugar do SerpAPI:
SEARCH_PROVIDER=google
SEARCH_MODEL=gemini-2.5-flash
SEARCH_API_KEY=sua_chave_google
SEARCH_BASE_URL=
SEARCH_GOOGLE_SEARCH_ENABLED=trueNesse modo, get_links não é registrado e o SerpAPI não é chamado. A execução
só é aceita quando o Gemini devolve consultas em grounding_metadata e fontes
citadas. As fontes estruturadas do grounding são salvas em final_answer.sources,
e execution.tools inclui google_search somente quando a busca foi executada.
IMAGE_PROVIDER=google
IMAGE_MODEL=gemini-2.5-flash
IMAGE_API_KEY=sua_chave_google
IMAGE_BASE_URL=Configuração opcional dedicada para vídeos públicos do YouTube:
YOUTUBE_PROVIDER=google
YOUTUBE_MODEL=gemini-2.5-flash
YOUTUBE_API_KEY=sua_chave_google
YOUTUBE_BASE_URL=
YOUTUBE_TIMEOUT=300Configuração opcional dedicada ao avaliador de duplicidade:
DUPLICATE_JUDGE_PROVIDER=vllm
DUPLICATE_JUDGE_MODEL=Qwen/Qwen3-8B-FP8
DUPLICATE_JUDGE_API_KEY=sua_chave_vllm
DUPLICATE_JUDGE_BASE_URL=https://seu-endpoint/v1
DUPLICATE_JUDGE_TEMPERATURE=0.0
DUPLICATE_JUDGE_TIMEOUT=120O endpoint /analyze aceita texto, anexos ou ambos. Imagens, áudios e vídeos
enviados por uma integração devem ser informados em attachments:
{
"query": "Verifique os conteúdos enviados",
"attachments": [
{
"type": "image",
"url": "https://example.com/imagem.jpg",
"mime_type": "image/jpeg"
},
{
"type": "audio",
"url": "https://example.com/audio.ogg",
"mime_type": "audio/ogg"
}
]
}Vídeos públicos do YouTube podem ser enviados na própria consulta ou como attachment. A análise aceita inicialmente um vídeo do YouTube por solicitação:
{
"query": "Verifique as alegações apresentadas neste vídeo",
"attachments": [
{
"type": "youtube",
"url": "https://www.youtube.com/watch?v=VIDEO_ID"
}
]
}URLs youtube.com/watch, youtu.be, youtube.com/shorts,
youtube.com/live e youtube-nocookie.com/embed são reconhecidas
automaticamente. O Gemini aceita somente vídeos públicos; vídeos privados,
indisponíveis ou não listados fazem a análise falhar.
Quando o usuário informa uma pergunta ou alegação específica, ela define o foco da análise. Caso contrário, o sistema obtém o título e a thumbnail oficiais pelo endpoint público oEmbed do YouTube, sem chave adicional. O título é a fonte principal da alegação; a thumbnail apenas fornece contexto ou completa um título vago. Manchetes e outros textos exibidos dentro dos frames nunca são tratados como título. Os demais assuntos do vídeo são ignorados na pesquisa e na classificação.
Se o pedido for genérico, o título não trouxer uma alegação clara e o vídeo abordar vários tópicos, a execução termina sem pesquisa e sem classificação. O resultado orienta o analista a pedir que o usuário indique a afirmação, o trecho ou o timestamp que deseja verificar. A resposta final mantém no máximo dez fontes escolhidas pelo router e validadas contra as URLs citadas no grounding.
Quando a solicitação tiver sido originada por uma pessoa em uma aplicação
externa, seus identificadores podem ser informados em requester:
{
"query": "Essa informação é verdadeira?",
"requester": {
"external_id": "+5568999999999",
"conversation_id": "conversation-id",
"message_id": "message-id"
}
}A aplicação de origem não é aceita no payload. Ela é identificada pelo bearer
token e acrescentada pelo VerificaAI antes da entrega ao painel. A criação do
token exige um name e gera um application_id UUID estável e não secreto.
O requester não é enviado aos modelos nem persistido no Qdrant.
Na entrega do resultado ao painel, o objeto é enriquecido desta forma:
{
"requester": {
"application": {
"id": "c824bf11-2a72-43dd-919b-a3f76de5fe04",
"name": "Agente WhatsApp"
},
"external_id": "+5568999999999",
"conversation_id": "conversation-id",
"message_id": "message-id"
}
}Todos os links HTTP ou HTTPS presentes na query também são adicionados
automaticamente aos anexos. Anexos explícitos e links da consulta são
deduplicados, mas a query original permanece inalterada. O tipo é identificado
primeiro como URL do YouTube, depois pelo MIME type informado e por fim pela
extensão da URL.
Áudios e vídeos destinados à transcrição aceitam somente os formatos .mpeg,
.ogg, .mp3, .wav, .mp4, .avi e .webm. Quando a URL não possui
extensão, é obrigatório informar um MIME type equivalente a um desses formatos.
Um formato inválido recebe resposta HTTP 422 antes da criação do job e não é
enviado aos agentes ou modelos.
Quando há várias mídias, os agentes especializados processam cada uma e o agente de busca recebe uma única consulta com todos os contextos extraídos.
Quando TRANSCRIPTION_MEDIA_RELAY_ENABLED=true, áudios e vídeos são baixados
somente de hosts presentes em TRANSCRIPTION_MEDIA_RELAY_ALLOWED_HOSTS, copiados
para um objeto privado no Google Cloud Storage e enviados à API de transcrição
por uma URL assinada temporária. A URL original permanece no attachment e o
objeto temporário é removido ao final da transcrição. O worker de análise deve
ter acesso às credenciais definidas em GCS_SERVICE_ACCOUNT_FILE ou às
credenciais padrão do ambiente Google Cloud. Em Docker, o arquivo deve ser
montado no container do worker e a variável deve apontar para o caminho interno.
O tempo de GCS_SIGNED_URL_TTL_SECONDS deve ser maior que
TRANSCRIPTION_TIMEOUT_SECONDS. Também é recomendado configurar uma regra de
lifecycle no bucket para excluir objetos residuais caso o worker seja encerrado
antes da limpeza.
Depois que uma chamada autenticada e validada é enfileirada, o VerificaAI
registra seu task_id, a aplicação autenticada e o horário de aceitação no banco
definido por ANALYZE_REQUESTS_DB_PATH. O payload da solicitação não é
persistido nesse registro. Uma falha nessa gravação é registrada no log, mas não
altera a resposta 202 nem impede o processamento da análise.
Os registros podem ser consultados com autenticação administrativa:
GET /admin/analyze-requests?application_id={uuid}&limit=50&offset=0
A resposta inclui total, limit, offset e os itens da página. A rota aparece
na documentação somente quando ADMIN_DOCS_ENABLED=true.
O endpoint autenticado POST /reanalyze permite solicitar uma ampliação de um
resultado automático ainda não revisado por uma pessoa:
{
"reanalysis_id": "66c97611-3931-4f96-b963-17f5121b2353",
"final_result_id": "c824bf11-2a72-43dd-919b-a3f76de5fe04",
"prompt": "Verifique também se o fato representado na imagem aconteceu."
}Antes de enfileirar o job, o VerificaAI consulta o resultado original em
FINAL_RESULTS_API_URL/{final_result_id}. Resultados que já tenham classificação
humana recebem HTTP 409 e não são enviados aos agentes.
A reanálise utiliza a consulta, a resposta, as limitações internas, a classificação, as fontes e os
anexos do FinalResult automático. As mídias originais são processadas novamente
antes da pesquisa online. A síntese devolvida é uma nova resposta completa: ela
preserva o conteúdo anterior que continua relevante e incorpora as evidências da
nova consulta. Conteúdo anterior somente deve ser removido quando a instrução
humana solicitar ou quando novas evidências exigirem uma correção.
O enqueue responde com HTTP 202:
{
"task_id": "uuid-do-job",
"status": "queued"
}O andamento pode ser consultado, com autenticação, em
GET /reanalyze/status/{task_id}. Ao terminar, o resultado é enviado para
REANALYSIS_RESULTS_API_URL/{reanalysis_id}/result pela mesma fila e pelo mesmo
worker usados nas entregas ao VerificaAI Painel. Entregas repetidas devem ser
tratadas de forma idempotente pelo painel.
A integração com o Qdrant é opcional. Use QDRANT_ENABLED=false para concluir as
análises sem enfileirar a persistência vetorial. Quando habilitada, a resposta final
é enviada para a fila qdrant, sem bloquear a conclusão do job principal. Um
worker dedicado verifica ou cria a collection, gera os embeddings e persiste o
point. Falhas nessa etapa seguem a política de retry da fila e não alteram o status
público da análise.
Exemplo de configuração:
QDRANT_ENABLED=true
QDRANT_QUEUE_NAME="qdrant"
QDRANT_JOB_TIMEOUT_SECONDS=900
QDRANT_RESULT_TTL_SECONDS=86400
QDRANT_FAILURE_TTL_SECONDS=604800
QDRANT_RETRY_INTERVALS_SECONDS="60,300,900"
QDRANT_DENSE_MODEL="intfloat/multilingual-e5-large"
QDRANT_SPARSE_MODEL="Qdrant/bm25"
QDRANT_COLBERT_MODEL="colbert-ir/colbertv2.0"
QDRANT_MAX_TOKENS=1024
QDRANT_COLLECTION_NAME="verifica-ai"
QDRANT_API_URL="https://seu-qdrant"
QDRANT_API_KEY="sua-chave"
QDRANT_API_PORT=443
QDRANT_TIMEOUT_SECONDS=60Cada análise final é persistida como um único point. A pergunta e o título são
usados para gerar os embeddings dense, sparse e ColBERT. O payload mantém apenas
o texto indexado, a identificação da aplicação, a pergunta e o título; resposta,
fontes e classificação permanecem fora do Qdrant. O prefixo de classificação do
título também é removido para não interferir na similaridade. A formatação exibida
no painel não é alterada. URLs também ficam fora dos embeddings: são preservadas
em urls e normalizadas em url_keys para futuras correspondências exatas. URLs
equivalentes de watch, youtu.be, shorts, live e embed compartilham a chave
youtube:{video_id}. A collection cria um índice keyword para url_keys.
QDRANT_TIMEOUT_SECONDS limita cada operação de rede do cliente, enquanto
QDRANT_JOB_TIMEOUT_SECONDS limita o job completo, incluindo o carregamento dos
modelos e a geração dos embeddings.
O serviço verificaai-qdrant-worker usa a mesma imagem da API e deve ser executado
somente nos deployments em que QDRANT_ENABLED=true.
Ao concluir uma análise, o worker principal enfileira a entrega da resposta para
a API configurada em FINAL_RESULTS_API_URL. A fila final-results é consumida
por um worker dedicado e repete requisições que falharem conforme os intervalos
definidos em FINAL_RESULTS_RETRY_INTERVALS_SECONDS.
FINAL_RESULTS_QUEUE_NAME="final-results"
FINAL_RESULTS_JOB_TIMEOUT_SECONDS=60
FINAL_RESULTS_RESULT_TTL_SECONDS=86400
FINAL_RESULTS_FAILURE_TTL_SECONDS=604800
FINAL_RESULTS_RETRY_INTERVALS_SECONDS="10,30,60,300,900"
FINAL_RESULTS_API_URL="http://laravel:8002/api/v1/final-results"
REANALYSIS_RESULTS_API_URL="http://laravel:8002/api/v1/final-result-reanalyses"
FINAL_RESULTS_API_TOKEN="seu-token"
FINAL_RESULTS_API_TIMEOUT_SECONDS=15O endpoint deve aceitar autenticação Bearer e tratar task_id de forma
idempotente para que novas tentativas não criem resultados duplicados.
O corpo enviado reproduz a resposta final do endpoint /status, acrescentando
o task_id da execução:
{
"task_id": "uuid",
"status": "done",
"result": {
"query": "Consulta analisada",
"attachments": [],
"final_answer": {
"answer": "Resposta final",
"limitations": [],
"sources": [],
"classification": "inconclusivo",
"is_classified": true
}
},
"duplicate_check": {
"outcome": "match",
"candidate_task_id": "uuid-da-analise-anterior",
"match_type": "semantic",
"confidence": "high"
},
"execution": {
"models": [
{
"role": "router",
"provider": "google",
"model": "gemini-3.6-flash",
"usage": {
"input_tokens": 4250,
"output_tokens": 1590,
"thinking_tokens": 910,
"cached_input_tokens": 0,
"total_tokens": 5840
}
}
],
"agents": [],
"tools": [],
"duration_ms": 12345,
"completed_at": "2026-08-04T12:00:00Z",
"app_version": "0.7.1"
},
"error": null
}duplicate_check.outcome pode ser match, exact_match, no_match,
uncertain, skipped ou unavailable. Somente correspondências confirmadas
expõem candidate_task_id e match_type; a confiança é informada para a
correspondência semântica. O workflow dos agentes continua sendo executado em
todos os casos.
duration_ms mede somente a execução do workflow dos agentes. O tempo de
enfileiramento, entrega HTTP, geração de embeddings e persistência no Qdrant não
faz parte dessa duração.
O consumo em usage é acumulado por papel de modelo durante toda a execução.
thinking_tokens representa a parcela de raciocínio já incluída em
output_tokens; portanto, os dois campos não devem ser somados novamente.
Com o ambiente configurado:
uv run python src/main.pyO programa abrirá um prompt no terminal:
O que você quer procurar?
Depois disso, o workflow imprime as fases do processamento e a resposta final estruturada.
Para permitir que o agente acesse URLs encontradas, você pode usar este projeto: https://github.com/mp-ac/link_para_markdown
- o projeto ainda depende de serviços externos para LLM e leitura de páginas;
- a transcrição depende de uma API externa e de uma URL de áudio acessível por ela;
- a execução atual é voltada a teste manual, não a produção;
- os imports e o ponto de entrada ainda estão em transição para uma estrutura mais preparada para múltiplas interfaces;
- a persistência no Qdrant é complementar e não substitui um banco transacional;
- os modelos de embedding podem ser baixados e carregados no primeiro uso, exigindo espaço em disco e memória;
- o README descreve o estado atual da aplicação, não toda a evolução pretendida para a plataforma.
- reduzir acoplamento da interface CLI;
- evoluir o núcleo atual para suportar também uma camada
FastAPI; - adicionar testes automatizados;
- documentar melhor fluxo de contribuição.
Este projeto está licenciado sob a GNU Affero General Public License v3.0 (AGPL-3.0). Veja o arquivo LICENSE para mais detalhes.
Titular institucional do projeto: Ministério Público do Estado do Acre.
Nas análises e reanálises, final_answer.limitations contém uma lista de textos
para o analista humano, separada de final_answer.answer, que contém o texto
público. Quando não houver limitações, a lista será []. Os prompts orientam a
síntese a manter observações internas fora da resposta pública, sem alterar os
critérios de classificação nem omitir a explicação de uma conclusão inconclusiva.
O campo segue no payload de entrega ao painel e nas respostas de status. Na
consulta do resultado original para reanálise, o campo esperado é
data.limitations; valores ausentes ou nulos são tratados como [].
A persistência e a exibição exclusiva ao analista dependem da atualização do
verificaai-painel-api e da interface de análise. O painel deve excluir esse campo
das respostas destinadas ao site e das notificações. Esta alteração no Python
não implementa esse controle de acesso nem modifica resultados antigos.
