Files
pastor-online/README.md
T
omp 2043d5ddcf refactor: troca PDF+OCR por corpus USFX em domínio público
- 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)
2026-11-02 11:16:07 -03:00

248 lines
8.6 KiB
Markdown

# Pastor Online
Sistema web RAG para consulta bíblica. Indexa um corpus bíblico em um banco vetorial e
responde em chat com um agente pastoral **ancorado exclusivamente nos versículos
recuperados** — com as referências sempre à vista.
Containerizado com Docker Compose: frontend React, backend FastAPI, ChromaDB e
(opcionalmente) Ollama local.
---
## Como funciona
```
./corpus/*.usfx.xml ──▶ parser USFX ──▶ embeddings ──▶ ChromaDB
│
navegador ◀── SSE ── FastAPI ◀── busca + validação de citações
```
**Fidelidade ao texto.** A resposta só é gerada quando há evidência recuperada do
corpus. As citações são conferidas no servidor contra as fontes consultadas: uma
referência que o modelo inventou é removida **antes** de chegar à tela. Sem evidência,
o sistema responde com uma mensagem fixa e não chama o LLM.
**Corpus estruturado.** O texto vem de XML ([USFX](https://ebible.org/usfx/)), não de
PDF: cada versículo já chega com livro, capítulo e número próprios. Não há OCR, nem
reconstrução de capítulos a partir de marcadores de página, nem ruído de
digitalização — a referência exibida é exatamente a coordenada de onde o texto foi
lido.
---
## Corpus
Vem versionado em **`./corpus`**, pronto para uso:
| | |
|---|---|
| Tradução | João Ferreira de Almeida |
| Licença | **Domínio público** — sem atribuição nem restrição |
| Conteúdo | 66 livros · 1.189 capítulos · 31.098 versículos |
| Proveniência | [seven1m/open-bibles](https://github.com/seven1m/open-bibles) |
Detalhes e o SHA-256 do arquivo em [`corpus/README.md`](corpus/README.md).
Para usar outra tradução, coloque um arquivo USFX em `./corpus` e reinicie o backend
(ou chame `POST /api/reindex`). **Verifique a licença antes**: NVI, NTLH, NAA, ACF,
ARA, ARC e KJA pertencem às editoras e não podem ser redistribuídas.
---
## Requisitos
- Docker com Compose v2 (ou Podman com `podman-compose`)
- ~2 GB de RAM livres para o backend
- ~1,5 GB de disco para o modelo de embeddings e o banco vetorial
---
## Início rápido
### 1. Configure o provedor de LLM
```bash
cp .env.example .env
```
Edite o `.env`. Quatro caminhos possíveis:
| Provedor | Configuração | Observação |
|---|---|---|
| **Ollama** (local) | `LLM_PROVIDER=ollama` | Subir com `--profile local`; o modelo é baixado na primeira execução |
| **OpenAI** | `LLM_PROVIDER=openai` + `OPENAI_API_KEY` | |
| **Groq** | `LLM_PROVIDER=groq` + `GROQ_API_KEY` | |
| **Anthropic** | `LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY` | |
Para testar sem nenhum provedor, use `LLM_PROVIDER=stub`: devolve resposta
determinística a partir do contexto recuperado, útil para conferir a busca e as
citações.
### 2. Suba o sistema
```bash
# Com Ollama local:
docker compose --profile local up --build -d
# Sem Ollama (usando OpenAI, Groq ou Anthropic):
docker compose up --build -d
```
Acesse **http://localhost:8080**.
### 3. Acompanhe a indexação
A indexação começa sozinha no arranque. O trabalho é todo embedding de 31.098
versículos em CPU — o painel lateral mostra o progresso por versículo e o chat já
responde enquanto ela roda. Reiniciar o container não repete o trabalho: o manifesto
guarda o SHA-256 de cada arquivo e só reprocessa o que mudou.
```bash
docker compose logs -f backend
```
---
## Comandos úteis
```bash
# Reiniciar só o frontend (após mudanças no React)
docker compose up -d --build frontend
# Forçar a reindexação do corpus
curl -X POST http://localhost:8080/api/reindex
# Ver o estado da indexação
curl http://localhost:8080/api/status
# Rodar os testes do backend
docker compose exec backend pytest tests/ -v
# Rodar os testes do frontend
docker compose exec frontend npm test
```
---
## API
| Rota | Método | Descrição |
|---|---|---|
| `/api/health` | GET | Saúde do serviço |
| `/api/status` | GET | Fontes lidas, versículos indexados, progresso, erros |
| `/api/reindex` | POST | Força a reindexação (202). 409 se já em andamento |
| `/api/chat` | POST | Chat em streaming (SSE) |
| `/api/sources` | GET | Trechos consultados na última resposta |
O chat responde em Server-Sent Events:
```
event: sources
data: {"sources":[{"reference":"Gênesis 1:1","text":"...","book":"Gênesis","chapter":1,"verse":1}]}
event: token
data: {"text":"No princípio criou Deus os céus e a terra..."}
event: done
data: {"citations":["Gênesis 1:1"]}
```
O frontend consome o stream com `fetch` + `ReadableStream`, porque `EventSource` não
aceita `POST`.
---
## Configuração
Todas as variáveis ficam no `.env`. As principais:
| Variável | Padrão | Efeito |
|---|---|---|
| `LLM_PROVIDER` | `ollama` | `ollama`, `openai`, `groq`, `anthropic` ou `stub` |
| `EMBEDDING_MODEL` | `paraphrase-multilingual-MiniLM-L12-v2` | `all-MiniLM-L6-v2` é menor e mais rápido |
| `TOP_K` | `6` | Quantos trechos são recuperados por pergunta |
| `MIN_SIMILARITY` | `0.35` | Corte de relevância; abaixo disso o sistema responde que o texto não aborda o assunto |
| `INDEX_BATCH_SIZE` | `512` | Versículos gravados no banco por lote |
| `CORPUS_DIR` | `/data/corpus` | Diretório dos arquivos USFX (montado de `./corpus`) |
| `REINDEX_TOKEN` | vazio | Se preenchido, `/api/reindex` exige o header `X-Reindex-Token` |
| `CORS_ORIGINS` | `http://localhost:8080` | Origens permitidas |
| `AUTO_INDEX` | `true` | Indexa automaticamente na inicialização |
---
## Solução de problemas
**O chat responde que o texto "não aborda diretamente o assunto".**
O índice pode estar vazio (`/api/status`) ou a pergunta pode estar longe do corpus.
Reduza `MIN_SIMILARITY` para 0.25, ou aumente `TOP_K` para 10. Se a pergunta citar
uma referência explícita, a busca é determinística e não depende do limiar.
**"nenhum versículo pôde ser lido" em `/api/status`.**
O arquivo em `./corpus` não é um USFX válido, ou não contém `<book>`. Confira com:
`docker compose exec backend python -c "from app.corpus import parse_usfx; print(sum(1 for _ in parse_usfx('/data/corpus/NOME.xml')))"`.
**A reindexação não pega um arquivo novo.**
Só `*.xml` é descoberto. Confira o nome e a permissão de leitura em `./corpus`.
**Quero reindexar do zero.**
`docker compose down && docker volume rm biblia-online_vector_db && docker compose up -d`.
**Ollama não sobe.**
`docker compose --profile local up -d`. O serviço `ollama-pull` baixa o modelo
indicado em `OLLAMA_MODEL` na primeira execução. Para outro modelo:
`OLLAMA_MODEL=llama3.2:3b docker compose --profile local up -d`.
**Porta 8080 já em uso.**
`FRONTEND_PORT=8090 docker compose up -d`. Atualize também `CORS_ORIGINS`.
---
## Estrutura do projeto
```
.
├── docker-compose.yml
├── .env.example
├── corpus/ # texto bíblico indexado (USFX, domínio público)
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app/
│ │ ├── config.py # settings via pydantic-settings
│ │ ├── models.py # schemas da API
│ │ ├── corpus.py # USFX → versículos + nomes canônicos dos livros
│ │ ├── embedding.py # sentence-transformers
│ │ ├── vector_store.py # ChromaDB (+ versão em memória para testes)
│ │ ├── indexer.py # descoberta, manifesto incremental, progresso
│ │ ├── llm.py # adaptadores Ollama/OpenAI/Groq/Anthropic/stub
│ │ ├── chat.py # contexto, validação de citações
│ │ └── main.py # rotas FastAPI
│ └── tests/
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf
│ └── src/
│ ├── App.tsx
│ ├── api.ts
│ ├── lib/sse.ts
│ └── components/
└── docs/superpowers/ # design e plano de implementação
```
---
## Segurança
- As chaves de API vivem **apenas** no backend, via variáveis de ambiente. Nunca
chegam ao bundle do navegador.
- O corpus é montado somente-leitura; a escrita fica restrita ao volume `/data`.
- O conteúdo indexado é entregue ao modelo como **dado delimitado**, nunca como
instrução — o que neutraliza tentativas de prompt injection via corpus.
- Mensagens acima de 4.000 caracteres são recusadas; o histórico é limitado às
últimas 10 trocas.
- O endpoint de reindexação rejeita chamadas concorrentes (409) e aceita um token
opcional (`REINDEX_TOKEN`).
A aplicação é local, sem autenticação. Não a exponha à internet sem colocar um proxy
com autenticação à frente.