Causa do 'Provedor de LLM indisponível' no ambiente com Ollama local foi
externa: OLLAMA_BASE_URL exportada no shell (http://localhost:11434/v1) vence o
.env na interpolação do docker-compose, e localhost dentro do container é o
próprio container. Nada no app indicava a URL em uso.
Dois defeitos de código que tornavam isso indetectável — e um deles produziria a
mesma mensagem mesmo com a URL correta:
- _BaseProvider criava o httpx.AsyncClient com o atributo de classe (120 s) e as
subclasses atribuíam self.timeout depois. O httpx congela o timeout na
construção, então os 300 s pedidos pelo Ollama nunca valeram: um primeiro
atendimento com modelo frio era cortado em 120 s com exatamente a mensagem
'provedor indisponível'. O timeout agora é passado ao super().
- LLMProviderError de conexão não citava o endereço tentado; o log dizia só 'All
connection attempts failed'. Passa a incluir a endpoint, com credenciais
user:pass@ redigidas. /api/health expõe llm_endpoint para comparação direta
com o .env.
README e .env.example documentam a precedência shell > .env, o fato de /v1 ser a
superfície OpenAI-compatível (o adaptador usa /api/chat nativo) e a latência
medida do primeiro atendimento.
Fusão:
- fuse_rankings() extrai a fusão RRF e aplica MAX_KEYWORD_ONLY (padrão 2):
quantas posições podem ser ocupadas por trechos que só o BM25 achou. O que os
dois lados confirmam nunca conta contra a cota.
- O controle é por cota de posições, não por piso de nota. Medido: a razão da
nota BM25 não separa acerto de ruído — para 'como posso perdoar quem me
feriu?', Deuteronômio 28:27 pontua 1,000 do topo e o correto Lucas 7:47 fica
em 0,716 (rank 12). Um piso cortaria o certo e manteria o errado.
- Ponderar o RRF também não resolve: com peso 0,5 no BM25 os achados só-lexicais
caem abaixo do 6º vetorial (0,5/61 < 1/66) e somem, levando junto Jó 3:13 e
Joel 3:14, os melhores resultados medidos.
Portão off-topic:
- MIN_SIMILARITY não consegue implementar 'sem evidência'. Medido no índice
completo: 0/6 perguntas off-topic bloqueadas em qualquer limiar de 0,30 a
0,50 ('bolo de chocolate' bate 0,544 em Lv 7:14; 'cotação do dólar' 0,494).
A recusa passa a ser instruída no SYSTEM_PROMPT e o limiar fica documentado
como filtro de ruído.
- corpus/: Almeida (João Ferreira de Almeida) em USFX, domínio público,
66 livros / 1189 capítulos / 31098 versículos, versionado com SHA-256
- app/corpus.py: parser USFX (texto no tail de <v/>), nomes canônicos dos
livros, aliases (abreviaturas e numerais romanos), VerseChunk sem página
- indexer: descobre *.xml em CORPUS_DIR, progresso em versículos, lote
configurável, manifesto incremental por SHA-256
- remove extraction.py, parallel.py, bible_parser.py, Tesseract e PyMuPDF
- campo page removido de Source/SearchResult/StatusResponse e do frontend
- embedder: propriedade name; status reporta o embedder em uso, não o do .env
- create_app constrói o embedder ANTES do store (o default HashEmbedder do
VectorStore impedia o modelo configurado de ser carregado em produção)
- fallback do modelo agora aparece em /api/status.errors
- torch 2.6.0 + transformers 5.22.1 pinados (transformers 5.x exige torch>=2.6)