Files
pastor-online/docs/superpowers/specs/2026-11-01-pastor-online-design.md
T
omp 8f5e098d8f feat: stemming português no BM25 e índice lexical sobre o versículo
Dois defeitos medidos no índice real (31.098 versículos), ambos na metade
lexical da busca híbrida:

1. Sem normalização morfológica, 'amar' da pergunta nunca casava com 'amai' ou
   'amarás' do versículo. Para 'o que Jesus disse sobre amar os inimigos?',
   Mateus 5:44 ficava em 434º no BM25 enquanto Marcos 12:34 liderava por casar
   apenas 'jesus'+'disse'. keyword.py ganha um stemmer de sufixos em etapas
   (estilo RSLP, sem dependência externa) com guarda de comprimento mínimo do
   radical — sem ela 'deus' viraria 'd' e 'mar' viraria 'm'. Rank de Mt 5:44: 12º.

2. O BM25 indexava a janela de contexto. Os dois lados querem granularidades
   opostas: a janela dá tema ao embedding, mas multiplica os termos incidentais
   que o BM25 pontua. Indexando o versículo isolado, Mt 5:44 vai a 1º e os
   achados só-lexicais deixam de ser aleatórios ('ansioso e não consigo dormir'
   -> Jó 3:13 'teria dormido'; 'não sei que decisão tomar' -> Joel 3:14 'vale da
   decisão').

Colisão conhecida do stemming: 'luto' e 'lutavam' reduzem ao mesmo radical.
2026-11-02 12:39:54 -03:00

27 KiB
Raw Blame History

Pastor Online — Design do Sistema RAG

Data: 2026-11-01 Status: Proposed

Atualização (2026-11-02): ingestão por corpus estruturado

As seções que descrevem PDF → OCR → parser de índice → reconstrução de capítulo/versículo foram substituídas. O resto do desenho (FastAPI, ChromaDB, validação de citações, SSE, Docker Compose, provedores de LLM) segue válido.

O que mudou: a entrada passou a ser XML USFX em domínio público (corpus/por-almeida.usfx.xml, Almeida, 66 livros, 31.098 versículos), versionado no repositório.

Por quê: o PDF exigia OCR das 3.364 páginas e a camada textual é corrompida, então cada referência era reconstruída a partir de texto ruidoso. Com o corpus estruturado a referência é a coordenada de onde o texto foi lido. Medido: indexação de 31.098 versículos em ~30 min contra 17–67 min de OCR, sem Tesseract, sem PyMuPDF e sem cache de páginas.

Duas falhas encontradas no caminho, ambas silenciadas por projeto anterior:

  1. create_app criava o VectorStore antes do embedder; como o store default para HashEmbedder, o modelo configurado nunca era carregado em produção e a busca rodava sem semântica nenhuma.
  2. transformers não estava pinado: a versão 5.x exige torch>=2.6, e o Dockerfile pinava 2.5.1 — o sentence-transformers desativava o PyTorch e o fallback silencioso caía no mesmo HashEmbedder.

/api/status agora reporta o embedder em uso (não o do .env) e qualquer fallback aparece em errors.

Busca: passou a híbrida (vetorial + BM25, fusão por RRF) e cada chunk indexa o versículo com uma janela de vizinhos. Medido em 5.703 versículos reais: a busca puramente vetorial não liga "estou com medo do futuro" a "não temas, porque eu sou contigo" (similaridade 0,19, no nível de um par sem relação), enquanto acerta "amar os inimigos" → Mateus 5:44 (0,683). Os dois lados erram de modos diferentes. intfloat/multilingual-e5-small foi descartado apesar de treinado para recuperação: a faixa de similaridade dele tem 0,02 de largura (0,853–0,873), o que inutilizaria o piso de evidência do qual a garantia de fidelidade depende.

1. Intenção

O Pastor Online é uma aplicação web local, containerizada, que lê PDFs da Bíblia, indexa seu conteúdo em um banco vetorial e responde perguntas em linguagem natural com tom pastoral, fundamentado exclusivamente nos trechos recuperados do corpus bíblico indexado.

Motivo: oferecer acompanhamento pastoral consistente, auditável e ancorado no texto — sem depender da memória ou da opinião do modelo de linguagem.

2. Estado atual do repositório

  • Repositório vazio, exceto pelo diretório PDF/ contendo biblia.pdf (8,1 MB, 3.364 páginas).
  • Não há código, testes, Dockerfiles ou configuração Git. O diretório não é um repositório Git.
  • Fato técnico decisivo: a camada textual do PDF não possui mapeamento Unicode. pdftotext e PyMuPDF devolvem sequências de controle (\x01–\x1f) em vez de letras. A inspeção de páginas amostradas (6, 7, 101, 501, 1501, 3001) confirmou razão de caracteres alfanuméricos legíveis < 2% em todas elas. O PDF é visualmente correto e precisa de OCR para ser utilizável.
  • Edição identificada: Almeida Revista e Corrigida (publicação L.C.C./eBooksBrasil, 2006), 66 livros canônicos, sem deuterocanônicos. Termina em Apocalipse 22:21, seguido de uma página de copyright a ser descartada da indexação.
  • Estrutura validada por OCR: página 3–4 traz o índice com a página inicial de cada livro (Génesis: 6, Êxodo: 168, … Apocalipse: 3314); cada página de texto traz o nome do livro no cabeçalho, o capítulo como [N] e os versículos numerados no início da linha.
  • Medição de desempenho: OCR a 200 DPI com tesseract -l por+eng --psm 6 levou ~1,2 s por página e reproduziu o texto com acentos corretos. Com 4 workers, a indexação inicial de 3.364 páginas fica em torno de 17 minutos; com cache de OCR, reindexações são quase instantâneas.

3. Objetivos

  1. Ingestão automática de PDFs em ./PDF (montados como /data/pdfs) na inicialização do backend, com reindexação incremental por hash de arquivo.
  2. Busca semântica multilíngue em CPU, com metadados de livro, capítulo e versículo.
  3. Chat com streaming, Markdown, citações destacadas e painel retrátil de trechos consultados.
  4. Painel de status com PDFs processados e vetores indexados.
  5. Suporte a quatro provedores de LLM por variável de ambiente: Ollama, OpenAI, Groq e Anthropic.
  6. Execução por docker compose up --build, com volumes persistentes.

4. Não objetivos

  • Autenticação, multiusuário, histórico persistente de conversas ou papéis administrativos.
  • Bancos vetoriais distribuídos, observabilidade centralizada ou telemetria de usuários.
  • Índice de busca full-text paralelo ao vetorial.
  • Suporte a documentos além de PDFs de Bíblia, nem a múltiplos corpus configurados.
  • Aplicação móvel nativa.
  • Avaliação teológica automática das respostas.

5. Antiobjetivos

  • Nunca permitir que o LLM responda a partir de conhecimento próprio quando o corpus não sustentar a resposta.
  • Nunca exibir uma citação cujo livro/capítulo/versículo não exista nos metadados recuperados.
  • Nunca expor chaves de API em respostas, logs ou no bundle do frontend.
  • Nunca aceitar PDFs arbitrários pela rede: a ingestão lê apenas do diretório montado.

6. Restrições e decisões de ambiente

Item Decisão
Runtime backend Python 3.11+ (imagem python:3.11-slim)
API FastAPI + Uvicorn
Banco vetorial ChromaDB persistente (/data/vector_db)
Embeddings sentence-transformers, paraphrase-multilingual-MiniLM-L12-v2 (CPU)
Extração PyMuPDF (camada textual) + Tesseract OCR (por+eng) como fallback
Frontend React 18 + Vite + TypeScript, CSS com tokens de tema, Nginx servindo o build
Orquestração LLM Adaptadores HTTP próprios sobre httpx (sem LlamaIndex/LangChain)
PDFs ./PDF montado em /data/pdfs; ./pdfs suportado via PDF_DIR
Volumes vector_db (Chroma + cache de OCR), ollama (modelos)
Orquestração Docker Compose; serviço ollama sob profile opcional ollama

6.1 Requisito de CPU e tempo de indexação

OCR de 3.364 páginas é o gargalo real. Medidas de projeto:

  • Renderização da página a 200 DPI + Tesseract em português.
  • Paralelismo por processo com OCR_WORKERS (padrão: min(4, cpu_count)).
  • Cache de OCR por página em /data/vector_db/ocr_cache/<sha256-do-arquivo>/<página>.txt, invalidado apenas quando o hash do PDF muda. Sem esse cache, cada reindexação repetiria horas de OCR.
  • Embeddings calculados em lotes (EMBED_BATCH_SIZE, padrão 32).

7. Abordagens consideradas

A. Monólito modular com adaptadores próprios — escolhida

FastAPI + ChromaDB + sentence-transformers + adaptadores HTTP diretos para os quatro provedores de LLM.

  • Prós: menor árvore de dependências; controle explícito do chunking, do prompt, do streaming SSE e das citações; comportamento previsível em CPU; menos superfície de abstração indireta.
  • Contras: o roteamento entre provedores e o parser de respostas são mantidos no projeto.

B. LlamaIndex sobre FastAPI

  • Prós: VectorStoreIndex, SentenceSplitter e readers reducesem código próprio.
  • Contras: ciclo de vida de serialização do LlamaIndex; streaming e citações ainda exigiriam código específico; a ingestão precisaria de reader customizado para o fallback de OCR de qualquer forma.

C. LangChain sobre FastAPI

  • Prós: ecossistema largo de provedores e retrievers.
  • Contras: maior peso, cadeia de dependências mais difícil de auditar e risco de fallback silencioso em respostas de LLM — incompatível com a exigência de fidelidade.

Justificativa: A opção B não elimina o trabalho específico (OCR, parser de versículos, citações), enquanto a C introduz risco no requisito mais crítico. A opção A entrega o menor caminho verificável entre as fontes e a resposta.

8. Arquitetura

flowchart LR
  subgraph Host
    PDF["./PDF/*.pdf"]
  end

  subgraph Compose
    BE["Backend FastAPI<br/>/data/pdfs · /data/vector_db"]
    OCR["PyMuPDF + Tesseract"]
    EMB["sentence-transformers"]
    CHROMA[("ChromaDB")]
    LLM["Adaptadores LLM<br/>httpx"]
    FE["Frontend React/Nginx"]
  end

  subgraph Optional
    OL["Ollama<br/>profile: ollama"]
  end

  PDF -->|bind mount ro| OCR
  OCR -->|versículos| EMB
  EMB -->|vetores + metadata| CHROMA
  CHROMA -->|top-k trechos| BE
  BE -->|stream SSE| FE
  BE -->|HTTP| LLM
  LLM -.-> OL
  LLM -.-> OpenAI
  LLM -.-> Groq
  LLM -.-> Anthropic
  FE -->|usuário| BE

8.1 Componentes do backend

Arquivo Responsabilidade
backend/app/config.py Settings via pydantic-settings; leitura de .env
backend/app/models.py Schemas Pydantic de request/response e de(chunk) fonte
backend/app/extraction.py Extração de texto por página: camada textual + fallback OCR + cache
backend/app/bible_parser.py Reconstrução de livro/capítulo/versículo e segmentação em versículos
backend/app/embedding.py Wrapper do sentence-transformers; normalização e lotes
backend/app/vector_store.py Cliente Chroma persistente; upsert, query e contagem
backend/app/indexer.py Manifesto incremental por SHA-256; orquestra extração → parser → embeddings → Chroma
backend/app/llm.py Interface LLMProvider + adaptadores Ollama/OpenAI/Groq/Anthropic com streaming
backend/app/chat.py Montagem do contexto, prompt pastoral, validação de citações e fallback sem evidência
backend/app/main.py Rotas FastAPI, lifespan, CORS, tratamento de erros
backend/tests/ Testes pytest do backend

8.2 Contratos de API

Rota Método Contrato
/api/health GET {"status":"ok","llm_provider":"ollama","indexing":"ready"}
/api/status GET `{"state":"ready
/api/reindex POST Inicia reindexação; retorna 202 {"state":"indexing"}. Sem REINDEX_TOKEN configurado, aberto. Com token, exige header X-Reindex-Token.
/api/chat POST Body {"message":str,"history":[{"role","content"}]}. Responde text/event-stream.
/api/sources GET {"sources":[{...}]} — últimos trechos recuperados (depuração/inspeção)

Fluxo SSE de /api/chat:

event: sources
data: {"sources":[{"reference":"Gênesis 1:1","text":"...","page":6,"file":"biblia.pdf"}]}

event: token
data: {"text":"No início"}

event: done
data: {"citations":["Gênesis 1:1"]}

event: error
data: {"message":"..."}

O frontend usa fetch + ReadableStream porque EventSource não aceita POST.

8.3 Frontend

  • frontend/src/App.tsx: layout, tema (claro/escuro persistido em localStorage).
  • frontend/src/components/ChatPanel.tsx: lista de mensagens, textarea com Enter para enviar e Shift+Enter para nova linha.
  • frontend/src/components/MessageBubble.tsx: Markdown via react-markdown + remark-gfm.
  • frontend/src/components/SourcesAccordion.tsx: <details> retrátil com os trechos consultados.
  • frontend/src/components/StatusPanel.tsx: PDFs lidos, vetores indexados, botão de reindexação.
  • frontend/src/lib/sse.ts: parser de stream SSE (testável isoladamente).
  • frontend/src/api.ts: cliente HTTP do backend.
  • Estilos: CSS único com variáveis (--fg, --muted, --surface, --accent, --border), tema via data-theme em <html>; layout responsivo com painel lateral colapsável abaixo de 900 px.

9. Pipeline de ingestão

  1. Descoberta: *.pdf em PDF_DIR, ordenados por nome.
  2. Manifesto: /data/vector_db/manifest.json mapeia nome_do_arquivo → {sha256, chunks, indexed_at}. Arquivo ausente do manifesto ou com SHA diferente ⇒ reindexar apenas esse arquivo.
  3. Extração por página (extraction.py):
    • Tenta page.get_text("text") do PyMuPDF.
    • Calcula legible_ratio = caracteres alfanuméricos imprimíveis / caracteres não-branco.
    • Se legible_ratio < 0.35 ⇒ considera a camada textual corrompida, rasteriza a página (200 DPI) e aplica Tesseract (por+eng) via subprocess com timeout de 120 s.
    • Resultado é cacheado por (sha256, página); páginas em cache evitam OCR e extração.
  4. Parser bíblico (bible_parser.py):
    • Atribuição de livro por faixa de páginas: o índice das páginas 3–4 do PDF fornece a página inicial de cada um dos 66 livros (Génesis: 6 … Apocalipse: 3314). Esse índice é OCR'd uma única vez e memoizado em /data/vector_db/book_index.json. Um chunk na página p pertence ao último livro cujo início é ≤ p. Isso é determinístico e independe de detecção de título por estilo — muito mais robusto que casar caixa-alta.
    • Fallback: se o índice não puder ser lido, detectar o livro pela linha de cabeçalho da página (nome do livro repetido no topo de cada página) ou por título isolado em caixa-alta.
    • Capítulos: linha isolada [N].
    • Versículos: linha iniciada por número seguido de texto; a numeração reinicia a cada capítulo. OCR pode unir o número ao texto ou separá-los em linhas distintas — ambos os formatos são aceitos.
    • De-hifenização: palavras hifenizadas no fim da linha (esconde-\nme) são religadas, preservando hífens léxicos (mas o Espírito).
    • Cada versículo vira um chunk com metadados {book, chapter, verse, page, file, text}.
    • Páginas sem numeração reconhecível (ex.: continuação de versículo longo) são anexadas ao capítulo corrente como continuação do último versículo.
    • Páginas fora da faixa bíblica (índice, copyright) são descartadas por não pertencerem a nenhum livro segundo o índice.
  5. Embeddings: lotes via sentence-transformers, vetores normalizados.
  6. Persistência: collection.upsert com IDs determinísticos sha1(file|book|chapter|verse). Ao reindexar um arquivo, seus IDs antigos são apagados antes do upsert (idempotência).
  7. Estado: /api/status reflete contagens durante e após o processo.

9.2 Ciclo de vida da indexação e falhas parciais

  • O backend sobe imediatamente e serve /api/status com state="indexing". Não bloqueia o startup: uma indexação inicial de ~17 min não pode impedir a aplicação de abrir.
  • A indexação roda como BackgroundTask; o chat responde durante ela, mas enquanto state="indexing" o backend informa no stream que o índice ainda está em construção (fonte indisponível → caminho “sem evidência”), em vez de fingir que não há evidência.
  • O manifesto distingue estados por arquivo: indexing, complete, partial, error.
    • complete: todas as páginas do arquivo foram extraídas e parseadas.
    • partial: algumas páginas falharam (OCR/parse). O arquivo é indexado com o que deu certo, mas o estado e a lista de páginas com falha ficam registrados e visíveis em /api/status. Um partial não é considerado “indexado com sucesso” no manifesto: a próxima reindexação reprocessa apenas as páginas que faltam.
    • error: nenhuma página pôde ser processada; o arquivo é ignorado e o erro aparece no status.
  • Reindexação interrompida (restart do container) retoma do cache de OCR: as páginas já extraídas são reaproveitadas, e só as faltantes são reprocessadas.
  • Reindexação concorrente é rejeitada com 409 (lock não-bloqueante).

9.3 Detecção de camada textual corrompida

Critério: legible_ratio < 0.35 ou presença de caracteres de controle C0 além de \t, \n, \r acima de 1% do texto. Ambos foram validados contra o PDF fornecido. Texto extraído com alta densidade de controles é ilegível para o usuário e para o modelo; usá-lo silenciosamente produziria respostas sem sentido — o fallback de OCR é obrigatório.

10. Busca e geração

  • Consulta semântica: embedding da pergunta; Chroma query com n_results=6; MIN_SIMILARITY=0.35 (configurável). Trechos abaixo do limiar são descartados.
  • Consulta por referência explícita: perguntas como “o que diz João 3:16” são atendidas por um caminho determinístico: o backend detecta a referência na mensagem, busca o chunk por metadados (book + chapter + verse no Chroma) e usa esse trecho como evidência principal, independentemente da similaridade semântica. Sem isso, uma pergunta por referência dependeria de o embedding posicioná-la acima do limiar — frágil.
  • Sem evidência: o backend responde com texto fixo em português, sem chamar o LLM:

    "O texto bíblico indexado não aborda diretamente esse assunto. Posso meditar com você os princípios de amor e sabedoria presentes na Palavra, se quiser explorar um deles."

  • Contexto: os trechos recuperados entram delimitados e numerados ([FONTE 1] Gênesis 1:1 ...), instruindo o modelo a tratá-los como dados, nunca como instruções.
  • System prompt: o prompt fornecido na especificação, acrescido de: responder apenas com base no contexto; citar livro, capítulo e versículo conforme [FONTE n]; não inventar referências; não criar doutrina; tom pastoral empático.
  • Formato de citação (único): Livro Capítulo:Versículo em prosa, por exemplo Gênesis 1:1 ou Salmos 23:1-3. O marcador [FONTE n] existe apenas no contexto enviado ao LLM e nunca aparece na resposta exibida. O backend extrai as referências do texto final com o mesmo padrão, de modo que resposta, validação e UI usam uma única forma.
  • Validação de citações (servidor): extrai referências no padrão Livro Capítulo:Versículo (incluindo variantes 1:1-3), confere se cada uma existe na lista de referências recuperadas e remove do texto final apenas as referências não sustentadas. Se a resposta não citar nenhuma fonte válida, o backend emite um aviso padronizado de que a resposta não foi ancorada e as fontes são exibidas separadamente.
  • Streaming e validação: ver §10.1 — a política de ordem entre tokens e validação é uma decisão de produto registrada abaixo.

10.1 Ordem entre streaming e validação de citações

Decisão do usuário: validar antes de exibir (buffer completo).

A resposta é gerada integralmente pelo LLM, as citações são validadas contra as fontes recuperadas, e só então o texto é transmitido ao navegador. Nenhuma citação inválida alcança a tela. O streaming interno do LLM permanece ativo (tokens são consumidos conforme chegam), mas o frontend exibe a resposta concluída, com indicador de “preparando resposta” enquanto aguarda.

O contrato SSE permanece o mesmo (sources → token → done), porém o evento token transporta a resposta já validada — em vez de um token por vez, pode ser um único evento com o texto completo. Isso preserva o formato do frontend e a possibilidade de exibir progresso, sem sacrificar a fidelidade.

O custo é perceptível: o usuário espera o texto completo antes de vê-lo. É o preço aceito pela garantia de fidelidade, e é explícito na interface.

Semântica de remoção: uma referência não sustentada é removida do texto; o texto resultante é enviado como está. Se a remoção deixar a resposta sem qualquer citação sustentada, o backend acrescenta ao final uma nota padrão:

"Observação: não foi possível ancorar esta resposta em um versículo específico do texto indexado. Os trechos consultados estão listados abaixo."

10.2 Escopo pastoral

Decisão do usuário: reflexão bíblica com encaminhamento em situações de crise.

O system prompt delimita o escopo:

  • Oferece consolo, oração, reflexão e orientação ancorada nos versículos recuperados.
  • Não emite diagnósticos, prescrições médicas, aconselhamento jurídico ou orientações financeiras.
  • Em indícios de crise (desespero, autodano, dependência, crise de saúde mental), a resposta Bárbara exorta o cuidado com segurança imediata, sugere procurar ajuda profissional e de confiança, e oferece versículos de conforto — sem substituir o cuidado humano.
  • Sinais explícitos de risco iminente geram orientação para os serviços de emergência locais, sem inventar números de telefone: o texto remete aos serviços públicos de emergência do país do usuário, quando conhecido, ou a “emergência local”.

10.3 Continuidade de conversa

O histórico entra no prompt (últimas 10 trocas) para dar contexto. A consulta usa a mensagem atual acrescida do histórico apenas quando a pergunta atual é claramente contextual (começa com pronome como “e no Antigo Testamento?”, “o que mais?”), caso em que a pergunta anterior é reescrita junto. A reescrita é feita pelo próprio LLM em uma chamada curta e não-streaming antes da busca; se a chamada falhar, usa-se a mensagem atual isolada. O chat não persiste conversas entre recargas da página (não objetivo).

11. Threat model

Escopo: ingestão de PDFs não confiáveis, mensagens de usuário e chamada a provedores externos.

Ativo / fronteira Ameaça Mitigação
PDF de terceiros → parser PDF malformado causando crash/exaustão Tratar cada PDF/página em try/except; validar magic bytes %PDF; limite de tamanho por arquivo; timeout por página no OCR; erros por arquivo registrados, sem derrubar o backend
Conteúdo do PDF → prompt do LLM Prompt injection em texto bíblico/indexado Contexto delimitado e rotulado como dado; system prompt declara que conteúdo recuperado é evidência, nunca instrução; validação de citações no servidor
Mensagem do usuário → LLM Injection do usuário, bypass de fidelidade Mesma delimitação; histórico limitado às últimas 10 trocas; mensagens acima de 4.000 caracteres rejeitadas com 422
Backend → API externa Vazamento de chave Chaves só em variáveis de ambiente do backend; logging com filtro que mascara Authorization; erros externos retornam mensagem genérica; chave nunca vai ao bundle do navegador
Usuário → endpoint de reindexação Reindexação repetida = DoS Endpoint POST; REINDEX_TOKEN opcional; reindexação concorrente é rejeitada (lock); idempotência por hash evita retrabalho
Backend → navegador CORS amplo demais CORS_ORIGINS explícito; default http://localhost:8080
Container → host Escrita fora do volume PDF montado read-only; escrita restrita a /data
LLM indisponível Resposta genérica/inventada Erro 503 explícito; nunca fallback silencioso para outro provedor ou conhecimento próprio do modelo

Categorias de risco avaliadas segundo OWASP Top 10 e MITRE ATT&CK. Riscos aceitos: aplicação local sem autenticação (o Compose não expõe portas fora da rede do projeto, exceto o frontend), e ausência de criptografia em repouso no volume local.

12. Testes

Backend (pytest):

  • extraction: página com texto legível não aciona OCR; página corrompida aciona OCR; resultado é cacheado e reaproveitado; erro de OCR não aborta o arquivo.
  • bible_parser: atribui livro por faixa de páginas do índice; detecta capítulo [N]; reconstrói Gênesis 1:1; de-hifeniza; texto sem numeração é anexado ao capítulo corrente; páginas de índice/copyright são descartadas.
  • indexer: reindexa apenas arquivo cujo SHA mudou; ids antigos são removidos; manifesto persistido; arquivo parcialmente falho fica como partial e é reprocessado depois.
  • chat: sem evidência ⇒ resposta fixa sem chamar LLM; citação não sustentada é removida; citação sustentada é preservada; system prompt contém as diretrizes e o escopo pastoral; pergunta com referência explícita (João 3:16) busca pelo metadado.
  • llm: cada adaptador converte streaming do provedor em delta de texto (transporte falso).
  • main: status, reindexação (409 concorrente, token), validação de entrada, formato do SSE.

Frontend (Vitest):

  • sse.ts: parser de eventos sources/token/done/error.
  • ChatPanel: Enter envia, Shift+Enter quebra linha.
  • SourcesAccordion: renderiza e abre os trechos consultados.

Verificação de integração:

  • docker compose up --build; GET /api/health; POST /api/reindex; POST /api/chat com um provedor fake registrado em LLM_PROVIDER=fake apenas para o smoke test; conferir no navegador a renderização da resposta, das citações e do painel de status.

Verificação do corpus (calibração, não gate de build):

  • Amostra de 30 páginas OCR'd é conferida contra a renderização visual do PDF.
  • O número de versículos por livro extraído é comparado com a contagem canônica (31.102 versículos na Almeida). Divergências acima de 2% por livro disparam revisão do parser.
  • 5 perguntas de referência com resposta esperada conhecida (ex.: “Quem disse ‘no princípio criou Deus os céus e a terra’?” ⇒ Gênesis 1:1) validam o caminho determinístico de citação.

13. Decisões tomadas

13.1 Decididas pelo usuário

  • Extração: camada textual + fallback OCR.
  • Local dos PDFs: manter ./PDF, montado em /data/pdfs.
  • Arquitetura: monólito modular com adaptadores próprios.
  • Citações e streaming: validar antes de exibir (buffer completo); nenhuma citação não sustentada chega à tela.
  • Escopo pastoral: reflexão bíblica com encaminhamento a profissionais e serviços de emergência em situações de crise; sem diagnóstico, prescrição, aconselhamento jurídico ou financeiro.

13.2 Decididas pelo projetor

  • Modelo de embeddings: paraphrase-multilingual-MiniLM-L12-v2, por operação multilíngue em CPU; all-MiniLM-L6-v2 fica aceito via EMBEDDING_MODEL para quem preferir menor consumo.
  • Idioma do OCR: por+eng.
  • Atribuição de livros: pelo índice de páginas do próprio PDF, com fallback por cabeçalho.
  • Canon: 66 livros, sem deuterocanônicos, conforme a edição Almeida do PDF fornecido.
  • Ciclo de vida: startup não bloqueante; indexação em background; manifesto com estados complete / partial / error.
  • Provedor LLM padrão: ollama com qwen2.5:1.5b; o serviço Ollama no Compose usa profile opcional ollama e um serviço ollama-pull que baixa o modelo na primeira execução. OpenAI, Groq e Anthropic funcionam com apenas LLM_PROVIDER + chave.
  • Sem git init: o diretório não é um repositório Git e o usuário não solicitou versionamento; o documento de design é gravado, não commitado.