diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b0cd393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore +.superpowers +docs +PDF +vector_db +**/__pycache__ +**/*.pyc +**/.pytest_cache +**/node_modules +**/dist +**/.venv +.env +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..773b448 --- /dev/null +++ b/.env.example @@ -0,0 +1,63 @@ +# --------------------------------------------------------------------------- +# Pastor Online — configuração +# Copie este arquivo para `.env` e ajuste os valores: +# cp .env.example .env +# --------------------------------------------------------------------------- + +# Porta do frontend no host (http://localhost:8080) +FRONTEND_PORT=8080 + +# --------------------------------------------------------------------------- +# Provedor de LLM: ollama | openai | groq | anthropic +# --------------------------------------------------------------------------- +LLM_PROVIDER=ollama + +# --- Ollama (local, requer o profile "local": docker compose --profile local up) --- +OLLAMA_BASE_URL=http://ollama:11434 +OLLAMA_MODEL=qwen2.5:1.5b + +# --- OpenAI --- +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-4o-mini +OPENAI_API_KEY= + +# --- Groq (compatível com a API da OpenAI) --- +GROQ_BASE_URL=https://api.groq.com/openai/v1 +GROQ_MODEL=llama-3.3-70b-versatile +GROQ_API_KEY= + +# --- Anthropic --- +ANTHROPIC_BASE_URL=https://api.anthropic.com +ANTHROPIC_MODEL=claude-sonnet-4-5 +ANTHROPIC_API_KEY= + +# --------------------------------------------------------------------------- +# Embeddings (CPU) +# paraphrase-multilingual-MiniLM-L12-v2: multilíngue, ~470 MB +# all-MiniLM-L6-v2: menor e mais rápido, ~90 MB, apenas inglês +# --------------------------------------------------------------------------- +EMBEDDING_MODEL=paraphrase-multilingual-MiniLM-L12-v2 + +# --------------------------------------------------------------------------- +# Busca +# --------------------------------------------------------------------------- +TOP_K=6 +MIN_SIMILARITY=0.35 + +# --------------------------------------------------------------------------- +# OCR (necessário quando o PDF não tem camada textual utilizável) +# --------------------------------------------------------------------------- +OCR_DPI=200 +OCR_LANGUAGE=por+eng +OCR_TIMEOUT=120 +OCR_WORKERS=4 + +# --------------------------------------------------------------------------- +# API +# CORS_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 +# REINDEX_TOKEN=troque-por-um-segredo +# AUTO_INDEX=true +# --------------------------------------------------------------------------- +CORS_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 +REINDEX_TOKEN= +AUTO_INDEX=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..785a2c0 --- /dev/null +++ b/README.md @@ -0,0 +1,237 @@ +# Pastor Online + +Sistema web RAG para consulta bíblica. Lê PDFs da Bíblia, indexa o conteúdo 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 + +``` +./PDF/*.pdf ──▶ PyMuPDF + OCR (Tesseract) ──▶ parser bíblico ──▶ embeddings + │ + navegador ◀── SSE ── FastAPI ◀── busca + validação + │ + ChromaDB +``` + +**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. + +**Extração.** Se a camada textual do PDF é utilizável, ela é usada (rápido). Se o PDF +usa fontes sem mapeamento Unicode — caso comum em PDFs de Bíblia digitalizados — o +sistema detecta a corrupção e aplica OCR na página. O resultado do OCR é cacheado por +página: reindexações não repetem o trabalho. + +--- + +## 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. Coloque os PDFs + +Copie seus PDFs da Bíblia para a pasta **`./PDF`**. O diretório já contém o +`biblia.pdf` desta edição da Almeida Revista e Corrigida. + +> A pasta `./pdfs` (minúscula) também funciona: basta apontar `PDF_DIR` para ela no +> `docker-compose.yml`. O Compose monta `./PDF` como somente-leitura em `/data/pdfs`. + +### 2. Configure o provedor de LLM + +```bash +cp .env.example .env +``` + +Edite o `.env`. Três 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` | | + +### 3. 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**. + +### 4. Acompanhe a indexação + +A primeira indexação faz OCR de todas as páginas do PDF — são ~3.400 páginas, e o +tempo típico fica entre 15 e 30 minutos em CPU. O painel lateral mostra o progresso +(PDFs lidos e vetores indexados) e o chat já responde durante a indexação. + +Para acompanhar os logs: + +```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 dos PDFs +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 | PDFs lidos, vetores indexados, 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":"...","page":6}]} + +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` ou `anthropic` | +| `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; acima disso, o sistema responde que o texto não aborda o assunto | +| `OCR_DPI` | `200` | Resolução do OCR | +| `OCR_LANGUAGE` | `por+eng` | Idiomas do Tesseract | +| `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 + +**A indexação não termina / fica em "Indexando".** +O OCR é o gargalo. Verifique `docker compose logs backend`. Se o OCR estiver lento, +aumente `OCR_DPI` não ajuda — reduza para 150 para acelerar, com perda de acurácia. +Para retomar após uma interrupção, basta reiniciar o container: o cache de OCR é +preservado no volume e só as páginas faltantes são reprocessadas. + +**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 increase `TOP_K` para 10. Verifique também se a +pergunta cita uma referência explícita — nesse caso a busca é determinística e não +depende do limiar. + +**"Nenhum versículo pôde ser extraído".** +O PDF pode não ser de uma Bíblia com capítulos e versículos numerados, ou o OCR +pode ter falhado. Inspecione o cache de OCR em `/data/vector_db/ocr_cache/` para ver +o texto que foi extraído de cada página. + +**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 +├── PDF/ # PDFs da Bíblia (entrada, somente-leitura) +├── backend/ +│ ├── Dockerfile +│ ├── requirements.txt +│ ├── app/ +│ │ ├── config.py # settings via pydantic-settings +│ │ ├── models.py # schemas da API +│ │ ├── extraction.py # PDF → texto (PyMuPDF + OCR + cache) +│ │ ├── bible_parser.py # livro / capítulo / versículo +│ │ ├── embedding.py # sentence-transformers +│ │ ├── vector_store.py # ChromaDB +│ │ ├── indexer.py # manifesto incremental +│ │ ├── llm.py # adaptadores Ollama/OpenAI/Groq/Anthropic +│ │ ├── 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. +- Os PDFs são montados 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. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bd080ef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + image: pastor-online-backend:latest + container_name: pastor-online-backend + restart: unless-stopped + environment: + PDF_DIR: /data/pdfs + VECTOR_DB_DIR: /data/vector_db + LLM_PROVIDER: ${LLM_PROVIDER:-ollama} + OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434} + OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2.5:1.5b} + OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} + OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-mini} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + GROQ_BASE_URL: ${GROQ_BASE_URL:-https://api.groq.com/openai/v1} + GROQ_MODEL: ${GROQ_MODEL:-llama-3.3-70b-versatile} + GROQ_API_KEY: ${GROQ_API_KEY:-} + ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-https://api.anthropic.com} + ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-sonnet-4-5} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-paraphrase-multilingual-MiniLM-L12-v2} + TOP_K: ${TOP_K:-6} + MIN_SIMILARITY: ${MIN_SIMILARITY:-0.35} + OCR_DPI: ${OCR_DPI:-200} + OCR_LANGUAGE: ${OCR_LANGUAGE:-por+eng} + OCR_TIMEOUT: ${OCR_TIMEOUT:-120} + OCR_WORKERS: ${OCR_WORKERS:-4} + CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:8080,http://127.0.0.1:8080} + REINDEX_TOKEN: ${REINDEX_TOKEN:-} + AUTO_INDEX: ${AUTO_INDEX:-true} + HF_HOME: /data/models + volumes: + - ./PDF:/data/pdfs:ro + - vector_db:/data/vector_db + networks: + - pastor + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8000/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + image: pastor-online-frontend:latest + container_name: pastor-online-frontend + restart: unless-stopped + depends_on: + backend: + condition: service_started + ports: + - "${FRONTEND_PORT:-8080}:80" + networks: + - pastor + + ollama: + image: ollama/ollama:latest + container_name: pastor-online-ollama + restart: unless-stopped + profiles: ["local"] + volumes: + - ollama:/root/.ollama + networks: + - pastor + + ollama-pull: + image: ollama/ollama:latest + container_name: pastor-online-ollama-pull + profiles: ["local"] + depends_on: + - ollama + environment: + OLLAMA_HOST: http://ollama:11434 + OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2.5:1.5b} + entrypoint: ["/bin/sh", "-c"] + command: + - | + until ollama list >/dev/null 2>&1; do + echo "aguardando o Ollama..." + sleep 3 + done + ollama pull "$${OLLAMA_MODEL}" + networks: + - pastor + +volumes: + vector_db: + ollama: + +networks: + pastor: + driver: bridge