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)
This commit is contained in:
1 parent
621679cee5
commit
2043d5ddcf
34 files changed
+34561
-1842
No files matched your search
@@ -0,0 +1,221 @@
|
||||
# Pastor Online Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Entregar um sistema web RAG "Pastor Online" containerizado, que indexa PDFs da Bíblia (incluindo o PDF fornecido, que exige OCR) e responde em chat pastoral ancorado exclusivamente nos versículos recuperados.
|
||||
|
||||
**Architecture:** Monólito modular FastAPI + ChromaDB persistente + sentence-transformers multilíngue. Adaptadores HTTP próprios (`httpx`) para Ollama, OpenAI, Groq e Anthropic. Frontend React/Vite servido por Nginx, que fala SSE com o backend. Pipeline de ingestão: PyMuPDF → (fallback Tesseract) → parser bíblico → embeddings → Chroma, com cache de OCR por página e manifesto incremental por SHA-256.
|
||||
|
||||
**Tech Stack:** Python 3.11, FastAPI, ChromaDB, sentence-transformers, PyMuPDF, Tesseract, httpx, pytest; React 18, TypeScript, Vite, Vitest, Nginx, Docker Compose.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-11-01-pastor-online-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Python 3.11 (`python:3.11-slim`); React 18 + TypeScript; Docker Compose único.
|
||||
- PDFs em `./PDF`, montados read-only em `/data/pdfs`; `./pdfs` suportado via `PDF_DIR`.
|
||||
- Vetores em `/data/vector_db` (volume nomeado `vector_db`), compartilhado com o cache de OCR.
|
||||
- System prompt pastoral é o da especificação, acrescido de escopo de crise (§10.2 do spec).
|
||||
- Citações no formato único `Livro Capítulo:Versículo`; validação **antes** de exibir (buffer completo).
|
||||
- Resposta sem evidência é fixa e **não** chama o LLM.
|
||||
- Nenhum segredo no bundle do frontend; chaves só no backend via env.
|
||||
- Reindexação concorrente → 409. `REINDEX_TOKEN` opcional protege `POST /api/reindex`.
|
||||
- Texto do corpus indexado é dado, nunca instrução (prompt injection).
|
||||
- OCR: 200 DPI, `tesseract -l por+eng --psm 6`; cache por `(sha256, página)`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- PDF com camada textual corrompida (o arquivo real) ⇒ precisa cair no OCR e ainda assim produzir `Gênesis 1:1` correto.
|
||||
- Pergunta com referência explícita (`João 3:16`) ⇒ resposta ancorada no versículo pedido, não no mais similar.
|
||||
- Resposta do LLM com citação inventada ⇒ citação removida antes de chegar à tela.
|
||||
- Índice do PDF ausente/ilegível ⇒ atribuição de livro por cabeçalho da página, sem perder o arquivo inteiro.
|
||||
- Reindexação com PDF alterado ⇒ só o arquivo alterado é reprocessado; o resto do índice permanece.
|
||||
- OCR de página que falha ⇒ arquivo fica `partial`, páginas com falha são reprocessadas depois, backend não cai.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fundação do backend — config, modelos e extração de PDF
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/requirements.txt`
|
||||
- Create: `backend/requirements-dev.txt`
|
||||
- Create: `backend/pyproject.toml`
|
||||
- Create: `backend/app/__init__.py`
|
||||
- Create: `backend/app/config.py`
|
||||
- Create: `backend/app/models.py`
|
||||
- Create: `backend/app/extraction.py`
|
||||
- Test: `backend/tests/test_extraction.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Settings` (pydantic-settings, `get_settings() -> Settings`); `PageText(page:int, text:str)`;
|
||||
`PdfExtractor(cache_dir: Path, dpi:int=200, ocr_lang:str="por+eng", ocr_timeout:int=120)` com
|
||||
`legible_ratio(text:str)->float`, `extract_page(page, file_hash, page_number)->str`,
|
||||
`extract_document(path)->Iterator[PageText]`, `ocr_page_image(pixmap)->str`.
|
||||
|
||||
- [ ] Step 1: Escrever `backend/tests/test_extraction.py` — `legible_ratio` alto para texto normal,
|
||||
baixo para controles C0; página legível não chama OCR; página corrompida chama OCR; cache
|
||||
reutilizado na segunda chamada; erro de OCR não propaga.
|
||||
- [ ] Step 2: `pytest backend/tests/test_extraction.py -v` → FAIL (módulo inexistente).
|
||||
- [ ] Step 3: Implementar `config.py`, `models.py`, `extraction.py`.
|
||||
- [ ] Step 4: `pytest backend/tests/test_extraction.py -v` → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 2: Parser bíblico
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/bible_parser.py`
|
||||
- Test: `backend/tests/test_bible_parser.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageText` (Task 1).
|
||||
- Produces: `VerseChunk(book, chapter, verse, page, file, text)`; `BOOKS` (66 livros canônicos em
|
||||
português); `parse_book_index(text)->dict[str,int]`; `BibleParser.parse(pages, file_name,
|
||||
book_starts)->list[VerseChunk]`.
|
||||
|
||||
- [ ] Step 1: Escrever testes — atribui livro por faixa de páginas; detecta `[N]`; reconstrói
|
||||
`Gênesis 1:1`; de-hifeniza `esconde-\nme`; número de versículo isolado vira versículo com texto
|
||||
na linha seguinte; página de índice/copyright fora da faixa é descartada.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `bible_parser.py` (lista canônica + aliases; normalização sem acentos).
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 3: Embeddings e ChromaDB
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/embedding.py`
|
||||
- Create: `backend/app/vector_store.py`
|
||||
- Test: `backend/tests/test_vector_store.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Embedder` protocol (`encode(texts:list[str])->list[list[float]]`),
|
||||
`SentenceTransformerEmbedder(model_name)`, `HashEmbedder` (testes, determinístico);
|
||||
`VectorStore(collection_name, persist_dir)` com `upsert(chunks, vectors)`, `delete_file(file)`,
|
||||
`query(vector, n_results, min_similarity)->list[SearchResult]`,
|
||||
`get_by_reference(book, chapter, verse)->list[SearchResult]`, `count()->int`.
|
||||
|
||||
- [ ] Step 1: Escrever testes — upsert + query; `delete_file` remove só do arquivo; filtro por
|
||||
referência; `min_similarity` descarta resultado abaixo do limiar; vetores normalizados.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `embedding.py` e `vector_store.py` (Chroma com `hnsw:space=cosine`).
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 4: Indexador com manifesto e cache de OCR
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/indexer.py`
|
||||
- Test: `backend/tests/test_indexer.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PdfExtractor`, `BibleParser`, `Embedder`, `VectorStore`.
|
||||
- Produces: `IndexStatus(state,pdfs_total,pdfs_indexed,chunks,last_indexed_at,errors)`;
|
||||
`Indexer.reindex(force:bool=False)->IndexStatus`; manifesto em `/data/vector_db/manifest.json`
|
||||
com estados `complete|partial|error`.
|
||||
|
||||
- [ ] Step 1: Escrever testes — só o PDF com SHA alterado é reprocessado; manifestos persistidos;
|
||||
arquivo com página falha fica `partial`; `count()` reflete os chunks.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `indexer.py`.
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 5: Adaptadores de LLM com streaming
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/llm.py`
|
||||
- Test: `backend/tests/test_llm.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `LLMProvider` protocol (`async stream(messages, system)->AsyncIterator[str]`);
|
||||
`OllamaProvider`, `OpenAIProvider` (cobre Groq via `base_url`), `AnthropicProvider`, `StubProvider`;
|
||||
`build_provider(settings)->LLMProvider`; `SYSTEM_PROMPT` com as diretrizes da spec + escopo de crise.
|
||||
|
||||
- [ ] Step 1: Escrever testes — cada adaptador converte o formato do provedor em deltas de texto;
|
||||
`build_provider` escolhe pelo `LLM_PROVIDER`; erro HTTP vira `LLMProviderError` sem expor a chave.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `llm.py`.
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 6: Chat — contexto, validação de citações e resposta pastoral
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/chat.py`
|
||||
- Test: `backend/tests/test_chat.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ChatService(settings, store, embedder, provider)` com
|
||||
`async answer(message, history)->ChatResult`; `ChatResult(text, sources, citations)`;
|
||||
`NO_EVIDENCE_MESSAGE`; `validate_citations(text, allowed)->tuple[str,list[str]]`;
|
||||
`extract_reference(text)->tuple[str,int,int]|None`.
|
||||
|
||||
- [ ] Step 1: Escrever testes — sem evidência ⇒ texto fixo e provider **não** chamado; citação
|
||||
não sustentada é removida; sustentada é preservada; pergunta com `João 3:16` usa o chunk
|
||||
exato; system prompt contém as 4 diretrizes da spec e o escopo de crise.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `chat.py`.
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 7: API FastAPI
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/main.py`
|
||||
- Test: `backend/tests/test_main.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `GET /api/health`, `GET /api/status`, `POST /api/reindex`, `POST /api/chat` (SSE),
|
||||
`GET /api/sources`; `create_app(settings=None, services=None)` para testes.
|
||||
|
||||
- [ ] Step 1: Escrever testes — status; reindex 202/409/token; chat 422 para mensagem longa;
|
||||
formato SSE `sources` → `token` → `done`; CORS restrito à origem configurada.
|
||||
- [ ] Step 2: Rodar → FAIL.
|
||||
- [ ] Step 3: Implementar `main.py`.
|
||||
- [ ] Step 4: Rodar → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 8: Frontend React
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/package.json`, `frontend/tsconfig.json`, `frontend/vite.config.ts`,
|
||||
`frontend/index.html`, `frontend/Dockerfile`, `frontend/nginx.conf`
|
||||
- Create: `frontend/src/main.tsx`, `frontend/src/App.tsx`, `frontend/src/api.ts`,
|
||||
`frontend/src/styles.css`, `frontend/src/lib/sse.ts`
|
||||
- Create: `frontend/src/components/ChatPanel.tsx`, `MessageBubble.tsx`, `SourcesAccordion.tsx`, `StatusPanel.tsx`
|
||||
- Test: `frontend/src/lib/sse.test.ts`, `frontend/src/components/ChatPanel.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: API do backend (§8.2 do spec).
|
||||
- Produces: `parseSSE(stream:ReadableStream<Uint8Array>):AsyncGenerator<SSEEvent>`;
|
||||
`sendMessage(body):AsyncGenerator<SSEEvent>`.
|
||||
|
||||
- [ ] Step 1: Escrever testes Vitest — parser SSE divide eventos; Enter envia, Shift+Enter quebra linha.
|
||||
- [ ] Step 2: `npm test` → FAIL.
|
||||
- [ ] Step 3: Implementar o frontend.
|
||||
- [ ] Step 4: `npm test` → PASS; `npm run build` → PASS.
|
||||
- [ ] Step 5: Commit.
|
||||
|
||||
### Task 9: Docker Compose, env e README
|
||||
|
||||
**Files:**
|
||||
- Create: `docker-compose.yml`, `.env.example`, `.dockerignore`, `README.md`
|
||||
- Modify: `backend/Dockerfile`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: serviços `backend`, `frontend`, `ollama`, `ollama-pull`; volumes `vector_db`, `ollama`;
|
||||
`COMPOSE_PROFILES=local` padrão.
|
||||
|
||||
- [ ] Step 1: Escrever `README.md` com preparación de `.env`, volume de PDFs, comandos de
|
||||
start, provedores, troubleshooting de OCR e de Ollama.
|
||||
- [ ] Step 2: `docker compose config` → exit 0.
|
||||
- [ ] Step 3: `docker compose build` → exit 0.
|
||||
- [ ] Step 4: Commit.
|
||||
|
||||
### Task 10: Verificação de integração
|
||||
|
||||
- [ ] Step 1: `docker compose up -d` e `GET /api/health` → 200.
|
||||
- [ ] Step 2: `POST /api/reindex` → 202; `/api/status` → `indexing` → `ready`.
|
||||
- [ ] Step 3: `POST /api/chat` com `LLM_PROVIDER=stub` → `sources` + `token` + `done`.
|
||||
- [ ] Step 4: Abrir o frontend no navegador e verificar chat, citações, accordion e painel de status.
|
||||
- [ ] Step 5: `pytest backend/tests -v` → 0 falhas; `npm test` → 0 falhas.
|
||||
Reference in new issue
Block a user