API para processar listas mensais, presença semanal e exports de conversa do WhatsApp do Volei Frederico.
- Python 3.12
- FastAPI para a API HTTP
- Uvicorn como servidor ASGI
- SQLAlchemy 2 com suporte assíncrono
- asyncpg como driver PostgreSQL
- Alembic para migrations
- PostgreSQL 16 como banco de dados
- Docker Swarm para execução local via stack
- Docker Compose para build da imagem e comandos auxiliares
- uv para gerenciamento de dependências e build da imagem
- pytest para testes
- MCP Python SDK para consultas seguras do banco pela LLM
O serviço recebe uma mensagem de texto com a lista mensal de assinantes, interpreta o conteúdo e grava no banco a situação de cada posição da lista.
Hoje o fluxo implementado funciona assim:
- A API recebe um texto em
POST /messages/process. - O parser identifica o mês, o ano e as linhas numeradas da mensagem.
- Cada linha vira um registro de assinante mensal.
- O serviço cria, atualiza ou remove registros em
monthly_subscribers. - A resposta informa o que foi criado, atualizado, removido ou mantido.
O importador de conversa lê o export do WhatsApp em ordem cronológica, classifica cada mensagem e encaminha somente eventos validados para esses mesmos serviços de domínio.
GET /health- Retorna
{"status":"ok"}para health check.
- Retorna
POST /messages/process- Processa a mensagem enviada no corpo da requisição.
POST /messages/monthly-subscribers/process- Processa explicitamente uma lista mensal.
POST /messages/weekly-attendance/process- Processa explicitamente uma lista semanal.
POST /weekly-attendance/promote-due- Executa as promoções de convidados vencidas e retorna o texto atualizado da lista para envio ao WhatsApp.
GET /weekly-attendance/message- Retorna o texto da lista persistida na data atual, após quaisquer promoções.
- Aceita
game_date=YYYY-MM-DDpara consultar uma data específica.
GET /messages/templates/monthly-subscribers- Gera a mensagem inicial de assinantes para o mês atual.
GET /messages/templates/weekly-attendance- Gera a mensagem inicial da lista para a quarta-feira atual ou seguinte.
POST /conversation-imports- Recebe um export UTF-8 do WhatsApp como
text/plain.
- Recebe um export UTF-8 do WhatsApp como
Os endpoints de template aceitam reference_date=YYYY-MM-DD para pré-visualização
determinística. Sem o parâmetro, usam a data atual em America/Sao_Paulo. Eles
somente geram o texto; o agendamento fica no n8n e o envio é feito pelo WAHA.
Exemplo de payload:
{
"text": "LISTA DE ASSINANTES DO MES DE ABRIL\n1. pyu ✅\n2. Daniel ✅\n3. Grid ✅",
"received_at": "2026-04-22T00:00:00"
}Execute primeiro em preview, que simula as alterações e faz rollback:
curl --data-binary @"/caminho/Conversa do WhatsApp.txt" \
-H "Content-Type: text/plain; charset=utf-8" \
"http://localhost:8001/conversation-imports?mode=preview&analysis_mode=hybrid&chat_id=fred"Depois de revisar changed_messages, review_required_messages, warnings e
results, use mode=apply para confirmar uma única transação:
curl --data-binary @"/caminho/Conversa do WhatsApp.txt" \
-H "Content-Type: text/plain; charset=utf-8" \
"http://localhost:8001/conversation-imports?mode=apply&analysis_mode=hybrid&chat_id=fred"Modos de análise:
rules: usa somente formatos locais validados.hybrid: usa os parsers locais primeiro, manda para a IA somente mensagens que as regras ignoraram e usa IA para limpar nomes semanais finais. Sem provedor, executa as regras e inclui um aviso explícito na resposta.ai: exige provedor configurado; caso contrário retorna503.
O provedor deve expor uma API compatível com
POST {CONVERSATION_AI_BASE_URL}/chat/completions. Configure URL, modelo e,
quando necessário, chave em .env. O texto da conversa é tratado como dado não
confiável, e a IA nunca recebe acesso ao banco ou a ferramentas.
No modo hybrid ou ai, a IA tambem e usada para limpar semanticamente nomes
das listas semanais finais antes de salvar. Ela pode remover emojis e
observacoes como depois das 20, mais tarde ou bem provavelmente, mas a API
valida a resposta e rejeita qualquer limpeza que introduza tokens que nao
existiam no texto original.
Para usar o Ollama local compartilhado, a API pode subir o container sob demanda
quando analysis_mode=hybrid ou analysis_mode=ai. Por padrao ela usa o socket
Docker montado em /var/run/docker.sock, cria o container fred-ollama, usa o
volume ollama_ollama-data, garante o modelo configurado e para/remove o
container ao final se foi ela que iniciou. O fallback fora do container usa
docker-compose.ollama.yml. Quando OLLAMA_ENABLE_GPU=true, o container e
criado com reserva NVIDIA equivalente a gpus: all.
Cada evento mensal ou semanal aceito recebe um fingerprint por conversa.
Eventos já aplicados são ignorados em exports posteriores, e eventos mais
antigos que o último estado de um mês ou jogo são marcados como stale para
impedir regressão. Mensagens ignoradas ou pendentes continuam elegíveis para
reanálise quando o modelo ou o prompt mudar.
O importador tambem guarda a posicao da ultima mensagem mensal ou semanal com
status applied ou unchanged. Ao receber novamente o export completo da
conversa, ele inicia o processamento depois dessa posicao, evitando reanalisar
todo o historico. Mensagens em revisao e transacoes que falharam nao avancam
essa posicao.
Listas semanais sem data explicita sao agrupadas na quarta-feira atual/proxima.
Quando varias versoes da mesma lista aparecem no export, somente a ultima versao
daquela quarta-feira atualiza o banco; as anteriores ficam como stale.
A tabela principal hoje é monthly_subscribers, com:
idpositionnamenormalized_name
Apelidos podem ser cadastrados pela API e passam a alimentar o normalized_name
nas próximas mensagens. O cadastro também atualiza os registros materializados
existentes, incluindo invited_by semanal, sem alterar o texto original do
ledger de mensagens processadas:
curl -X POST http://localhost:8001/person-aliases \
-H 'Content-Type: application/json' \
-d '{"alias":"moges","canonical_name":"Gomes"}'O nome recebido continua no campo name; somente sua forma normalizada passa a
ser gomes. Aliases já cadastrados ou conflitos retornam 409.
monthyearhas_paidcreated_atupdated_at
Há uma restrição única por month + year + position, para manter uma posição por mês.
As listas semanais ficam em weekly_attendances e
weekly_attendance_entries. Quando um nome vem com emoji numerico de time,
como Leal(conv)3️⃣, o nome e limpo e o time pre-montado e salvo na coluna
weekly_attendance_entries.prebuilt_team_number.
app/main.py- rotas FastAPIapp/parser.py- leitura e normalização da mensagemapp/services.py- regra de criação/atualização/exclusãoapp/models.py- modelos SQLAlchemyapp/schemas.py- schemas Pydanticapp/database.py- engine e sessão asyncapp/mcp_server.py- servidor MCP somente leitura para consultas do Fredalembic/- migrationstests/- testes do parser
O Fred inclui um servidor MCP separado da API HTTP. Ele expõe somente ferramentas de domínio e nunca aceita SQL arbitrário ou operações de escrita. As ferramentas disponíveis são:
get_weekly_attendance: consulta a lista principal e a fila de espera.list_waiting_guests: consulta os convidados que ainda aguardam vaga.get_monthly_subscribers: consulta os assinantes de um mês e ano.search_person: busca uma pessoa nas listas mensais e semanais.
O servidor roda dentro do Docker e usa a mesma DATABASE_URL do serviço da
API. O deploy principal da stack também sobe um serviço MCP HTTP interno,
disponível para clientes conectados à rede n8n em http://fred-mcp:8000/mcp.
Para iniciar uma instância temporária do endpoint HTTP, use:
docker compose -f docker-compose.yml -f docker-compose.mcp.yml run --rm -T mcpPara clientes que aceitam configuração por URL e estão conectados à rede n8n,
use o endpoint persistente:
{
"mcpServers": {
"fred": {
"url": "http://fred-mcp:8000/mcp"
}
}
}O cliente envia perguntas como “quem está aguardando vaga nesta quarta?”; a LLM escolhe a ferramenta apropriada e recebe dados estruturados do Fred. Credenciais do banco permanecem dentro do ambiente Docker.
Variável de ambiente principal:
DATABASE_URL
Variáveis do importador:
CONVERSATION_IMPORT_MAX_BYTESCONVERSATION_AI_BASE_URLCONVERSATION_AI_API_KEYCONVERSATION_AI_MODELCONVERSATION_AI_CONFIDENCE_THRESHOLDCONVERSATION_AI_BATCH_MESSAGESCONVERSATION_AI_BATCH_CHARSCONVERSATION_AI_ATTEMPTSCONVERSATION_AI_TIMEOUT_SECONDSOLLAMA_MANAGE_SERVICEOLLAMA_SHUTDOWN_WHEN_DONEOLLAMA_PULL_MODEL_WHEN_MISSINGOLLAMA_ENABLE_GPUOLLAMA_STARTUP_TIMEOUT_SECONDSOLLAMA_POLL_INTERVAL_SECONDSOLLAMA_COMPOSE_FILEOLLAMA_DOCKER_SOCKETOLLAMA_CONTAINER_NAMEOLLAMA_IMAGEOLLAMA_VOLUMEOLLAMA_NETWORK
O Ollama usado pelo assistente do WhatsApp permanece sob demanda. O workflow
chama POST /internal/ollama/start antes do agente, que inicia o container
fred-ollama, garante o modelo e o conecta à rede Docker n8n; depois da
resposta chama POST /internal/ollama/stop para parar e remover somente um
container iniciado por aquela execução. A credencial Ollama do n8n deve usar a
URL http://fred-ollama:11434.
Valor padrão para Docker Compose:
DATABASE_URL=postgresql+asyncpg://fred:fred@postgres:5432/fredO projeto está configurado para rodar a API dentro do Docker Compose. Por isso, o host do banco na URL é postgres, que é o nome do serviço PostgreSQL na rede Docker.
docker swarm init
docker network create --driver=overlay --attachable traefik-local
docker compose build api
docker stack deploy --detach=true -c docker-compose.yml fredIsso sobe:
- a API em
http://localhost:8001no host, encaminhando para8000dentro do container - o MCP HTTP interno em
fred-mcp:8000/mcp, na rede Docker do n8n - o PostgreSQL em
localhost:5433
Se o Swarm ja estiver inicializado ou a rede ja existir, os dois primeiros comandos podem retornar erro de recurso existente; nesse caso siga para o build e o deploy da stack.
Para ver o estado dos servicos:
docker stack services fred
docker service ps fred_apiPara acompanhar logs:
docker service logs -f fred_apiQuando a API é consumida pelo Traefik local, ela também entra na rede externa traefik-local e fica acessível pelo alias fred-api dentro do Docker Swarm. Nessa rota, o acesso externo continua em http://fred.localnetwork.
Para subir esse modo, use make deploy-fred no repositório raiz docker/. Esse fluxo publica o Fred como stack no Swarm para que o Traefik local consiga resolver o alias via rede overlay.
docker compose run --rm api pytestAplicar migrations:
docker compose run --rm api alembic upgrade headCriar uma nova migration:
docker compose run --rm api alembic revision --autogenerate -m "descricao"- O parser mensal aceita
LISTA DE ASSINANTES DO MES DE ...,Lista pagamento mensal Frederico,Lista Pagamento FredericoeLista Participantes. - O parser semanal aceita
LISTA VOLEI FREDERICO DD/MMe o formato históricoVolei Frederico 19h30, usando o timestamp do export quando a data não existe no cabeçalho. - Em listas semanais, sufixos com emoji numerico, como
3️⃣, indicam time pre-montado no grupo do WhatsApp. - Linhas numeradas sem nome são interpretadas como remoção da posição.
- Na lista semanal, linha com posicao
0e ignorada porque nao e posicao valida. - O símbolo
✅indica pagamento.