fix: livros numerados em citações e remoção completa de citação não sustentada
Dois defeitos na validação de citações, ambos visíveis ao usuário:
1. _WORD_RE excluía dígitos, então o número do livro era descartado. Afeta os 18
livros numerados (1/2/3 Samuel, Reis, Crônicas, Coríntios, Tessalonicenses,
Timóteo, Pedro, João):
- extract_reference('1 João 4:8') devolvia ('João',4,8) — perguntar por
1 João 4:8 devolvia João 4:8 em silêncio pela busca determinística
- '2 Reis 5:14' e '1 Coríntios 13:4' devolviam None — caiam na busca por
similaridade em vez da exata
- validate_citations removia citações CORRETAS: '(1 João 4:8)' sustentada
pelas fontes era apagada e sobrava '(1 João )' na resposta
O regex agora casa o número junto ('1 João', e '1Sm' aglutinado via \s*).
2. A remoção de citação não sustentada usava o span só do 'Capítulo:Versículo',
deixando o nome do livro pendurado: '(Gênesis 1:1)' virava '(Gênesis )'.
O span agora cobre do início do nome do livro ao fim dos versículos, e
parênteses que ficam vazios são limpos.
This commit is contained in:
1 parent
816e6be159
commit
0e498e2826
2 files changed
+125
-8
No files matched your search
+18
-8
@@ -42,8 +42,11 @@ _CHAPTER_VERSE_RE = re.compile(
|
||||
r"(?<![\w:])(\d{1,3})\s*:\s*(\d{1,3})(?:\s*[-–]\s*(\d{1,3}))?(?![\w:])"
|
||||
)
|
||||
|
||||
# Palavra que pode compor o nome de um livro.
|
||||
_WORD_RE = re.compile(r"[^\W\d_]+", re.UNICODE)
|
||||
# Palavra que pode compor o nome de um livro, com o número que o precede.
|
||||
# O número é parte do nome: sem ele, "1 João 4:8" seria lido como "João 4:8" e a
|
||||
# busca determinística devolveria outro livro. `\s*` (e não `\s+`) cobre também a
|
||||
# forma aglutinada "1Sm".
|
||||
_WORD_RE = re.compile(r"(?:[123]\s*)?[^\W\d_]+", re.UNICODE)
|
||||
|
||||
# Nomes de livros com até 4 palavras ("Cântico dos Cânticos").
|
||||
_MAX_BOOK_WORDS = 4
|
||||
@@ -122,8 +125,14 @@ def extract_reference(text: str) -> tuple[str, int, int] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _iter_references(text: str) -> Iterable[tuple[re.Match, str, str]]:
|
||||
"""Percorre as referências do texto, devolvendo livro canônico e faixa."""
|
||||
def _iter_references(text: str) -> Iterable[tuple[tuple[int, int], str, str]]:
|
||||
"""Percorre as referências do texto.
|
||||
|
||||
Devolve `(span, livro canônico, referência)`, onde `span` cobre **do início do
|
||||
nome do livro ao fim do capítulo:versículo**. Repassar só o trecho numérico
|
||||
faria a remoção deixar o nome do livro pendurado — "(Gênesis 1:1)" viraria
|
||||
"(Gênesis )" na resposta exibida ao usuário.
|
||||
"""
|
||||
for canonical, start, match in _iter_reference_matches(text):
|
||||
chapter = int(match.group(1))
|
||||
verse = int(match.group(2))
|
||||
@@ -131,7 +140,7 @@ def _iter_references(text: str) -> Iterable[tuple[re.Match, str, str]]:
|
||||
reference = f"{canonical} {chapter}:{verse}"
|
||||
if end_verse:
|
||||
reference = f"{reference}-{end_verse}"
|
||||
yield match, canonical, reference
|
||||
yield (start, match.end()), canonical, reference
|
||||
|
||||
|
||||
def validate_citations(
|
||||
@@ -148,18 +157,19 @@ def validate_citations(
|
||||
removals: list[tuple[int, int]] = []
|
||||
citations: list[str] = []
|
||||
|
||||
for match, canonical, reference in _iter_references(text):
|
||||
for span, canonical, reference in _iter_references(text):
|
||||
base = reference.split("-")[0]
|
||||
if reference in allowed or base in allowed:
|
||||
citations.append(reference)
|
||||
else:
|
||||
removals.append(match.span())
|
||||
removals.append(span)
|
||||
|
||||
cleaned = text
|
||||
for start, end in reversed(removals):
|
||||
cleaned = cleaned[:start] + cleaned[end:]
|
||||
|
||||
# Espaços duplos deixados pela remoção são normalizados.
|
||||
# Resíduos da remoção: parênteses que ficaram vazios e espaços duplos.
|
||||
cleaned = re.sub(r"\(\s*\)", "", cleaned)
|
||||
cleaned = re.sub(r"[ \t]{2,}", " ", cleaned)
|
||||
cleaned = re.sub(r"\s+([.,;:!?])", r"\1", cleaned)
|
||||
|
||||
|
||||
@@ -356,3 +356,110 @@ def test_source_shows_the_cited_verse_not_the_context_window(settings):
|
||||
assert source.reference == "Salmos 23:1"
|
||||
assert source.text == "O Senhor é o meu pastor."
|
||||
assert "Versículo anterior" in result.text, "o modelo recebe a janela completa"
|
||||
|
||||
|
||||
def test_unsupported_citation_is_removed_whole_not_just_the_numbers():
|
||||
"""Remover só o `1:1` deixa `(Gênesis )` pendurado na resposta do usuário.
|
||||
|
||||
Defeito observado na UI: a validação cortava o trecho `Capítulo:Versículo` e
|
||||
preservava o nome do livro, que é justamente o que não tem sustentação.
|
||||
"""
|
||||
from app.chat import validate_citations
|
||||
|
||||
cleaned, citations = validate_citations(
|
||||
"O Senhor é o meu pastor (Salmos 23:1) e no princípio criou Deus (Gênesis 1:1).",
|
||||
{"Salmos 23:1"},
|
||||
)
|
||||
|
||||
assert citations == ["Salmos 23:1"]
|
||||
assert "Gênesis" not in cleaned, "o nome do livro não sustentado tem de sair junto"
|
||||
assert cleaned == "O Senhor é o meu pastor (Salmos 23:1) e no princípio criou Deus."
|
||||
|
||||
|
||||
def test_removal_does_not_leave_empty_parentheses():
|
||||
from app.chat import validate_citations
|
||||
|
||||
cleaned, citations = validate_citations(
|
||||
"Deus é amor (1 João 4:8) e o texto continua (Apocalipse 9:9).",
|
||||
{"1 João 4:8"},
|
||||
)
|
||||
|
||||
assert citations == ["1 João 4:8"]
|
||||
assert "()" not in cleaned
|
||||
assert "( )" not in cleaned
|
||||
assert cleaned == "Deus é amor (1 João 4:8) e o texto continua."
|
||||
|
||||
|
||||
def test_removal_of_a_range_citation_takes_the_whole_range():
|
||||
from app.chat import validate_citations
|
||||
|
||||
cleaned, citations = validate_citations(
|
||||
"Conforme Salmos 23:1-3 e também Jó 1:2-3, há esperança.",
|
||||
{"Salmos 23:1-3"},
|
||||
)
|
||||
|
||||
assert citations == ["Salmos 23:1-3"]
|
||||
assert "Jó" not in cleaned
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Livros numerados (18 dos 66: 1/2/3 Samuel, Reis, Crônicas, Coríntios, ...)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text,expected",
|
||||
[
|
||||
("1 João 4:8", ("1 João", 4, 8)),
|
||||
("2 Reis 5:14", ("2 Reis", 5, 14)),
|
||||
("1 Coríntios 13:4", ("1 Coríntios", 13, 4)),
|
||||
("3 João 1:4", ("3 João", 1, 4)),
|
||||
("I Samuel 17:4", ("1 Samuel", 17, 4)),
|
||||
("II Crônicas 7:14", ("2 Crônicas", 7, 14)),
|
||||
("1Sm 18:6", ("1 Samuel", 18, 6)),
|
||||
("João 3:16", ("João", 3, 16)),
|
||||
("Salmos 23:1", ("Salmos", 23, 1)),
|
||||
],
|
||||
)
|
||||
def test_extract_reference_keeps_the_book_number(text, expected):
|
||||
"""Perder o número devolve outro livro: '1 João 4:8' viraria João 4:8."""
|
||||
from app.chat import extract_reference
|
||||
|
||||
assert extract_reference(text) == expected
|
||||
|
||||
|
||||
def test_extract_reference_inside_a_sentence_keeps_the_book_number():
|
||||
from app.chat import extract_reference
|
||||
|
||||
assert extract_reference("Leia 2 Reis 5:14 com atenção") == ("2 Reis", 5, 14)
|
||||
|
||||
|
||||
def test_reference_after_another_reference_keeps_its_own_book():
|
||||
"""O dígito do livro não pode ser confundido com o versículo anterior."""
|
||||
from app.chat import extract_reference
|
||||
|
||||
assert extract_reference("Salmos 23:1 e também 1 João 4:8") == ("Salmos", 23, 1)
|
||||
|
||||
|
||||
def test_supported_numbered_book_citation_is_kept():
|
||||
"""Uma citação correta de livro numerado não pode ser apagada da resposta."""
|
||||
from app.chat import validate_citations
|
||||
|
||||
cleaned, citations = validate_citations(
|
||||
"Deus é amor (1 João 4:8).", {"1 João 4:8"}
|
||||
)
|
||||
|
||||
assert citations == ["1 João 4:8"]
|
||||
assert cleaned == "Deus é amor (1 João 4:8)."
|
||||
|
||||
|
||||
def test_unsupported_numbered_book_citation_is_removed_whole():
|
||||
from app.chat import validate_citations
|
||||
|
||||
cleaned, citations = validate_citations(
|
||||
"Há esperança (2 Reis 5:14).", {"Salmos 23:1"}
|
||||
)
|
||||
|
||||
assert citations == []
|
||||
assert "Reis" not in cleaned
|
||||
assert cleaned == "Há esperança."
|
||||
Reference in new issue
Block a user