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.
27 KiB
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:
create_appcriava oVectorStoreantes do embedder; como o store default paraHashEmbedder, o modelo configurado nunca era carregado em produção e a busca rodava sem semântica nenhuma.transformersnão estava pinado: a versão 5.x exigetorch>=2.6, e o Dockerfile pinava 2.5.1 — osentence-transformersdesativava o PyTorch e o fallback silencioso caía no mesmoHashEmbedder.
/api/statusagora reporta o embedder em uso (não o do.env) e qualquer fallback aparece emerrors.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-smallfoi 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/contendobiblia.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.
pdftotexte 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 6levou ~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
- 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. - Busca semântica multilíngue em CPU, com metadados de livro, capítulo e versículo.
- Chat com streaming, Markdown, citações destacadas e painel retrátil de trechos consultados.
- Painel de status com PDFs processados e vetores indexados.
- Suporte a quatro provedores de LLM por variável de ambiente: Ollama, OpenAI, Groq e Anthropic.
- 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,SentenceSplittere 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 emlocalStorage).frontend/src/components/ChatPanel.tsx: lista de mensagens,textareacom Enter para enviar e Shift+Enter para nova linha.frontend/src/components/MessageBubble.tsx: Markdown viareact-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 viadata-themeem<html>; layout responsivo com painel lateral colapsável abaixo de 900 px.
9. Pipeline de ingestão
- Descoberta:
*.pdfemPDF_DIR, ordenados por nome. - Manifesto:
/data/vector_db/manifest.jsonmapeianome_do_arquivo → {sha256, chunks, indexed_at}. Arquivo ausente do manifesto ou com SHA diferente ⇒ reindexar apenas esse arquivo. - 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.
- Tenta
- 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.
- 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 (
- Embeddings: lotes via
sentence-transformers, vetores normalizados. - Persistência:
collection.upsertcom IDs determinísticossha1(file|book|chapter|verse). Ao reindexar um arquivo, seus IDs antigos são apagados antes do upsert (idempotência). - Estado:
/api/statusreflete 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/statuscomstate="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 enquantostate="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. Umpartialnã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
querycomn_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+verseno 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ículoem prosa, por exemploGênesis 1:1ouSalmos 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 variantes1: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óiGê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 comopartiale é 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 eventossources/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/chatcom um provedor fake registrado emLLM_PROVIDER=fakeapenas 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-v2fica aceito viaEMBEDDING_MODELpara 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:
ollamacomqwen2.5:1.5b; o serviço Ollama no Compose usa profile opcionalollamae um serviçoollama-pullque baixa o modelo na primeira execução. OpenAI, Groq e Anthropic funcionam com apenasLLM_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.