Fix: use a root document's effective content for the LLM index and pr… (#14423)

* Fix: use a root document's effective content for the LLM index and prompts

The search index and the document views use the newest version's content as a
root document's content, but everything the LLM side reads used the root's own
content: the text embedded in the LLM index, the content in the classification
prompt, the context blocks from similar documents, and the text that is
searched for similar documents. After a document was replaced by a new
version the LLM kept answering from the old text.

Read get_effective_content() in those four places. The index update annotates
the content in the query like the search index does, and the similar-document
lookup annotates it for the documents it fetches, so no query is made per
document. Entries already in the LLM index keep the old text until the
document next changes or the index is rebuilt.

* Chore: test the LLM query text and bulk annotation use effective content

* Removes the query count testing.  It's flakey from local to CI and workers, doesn't really add much imo
This commit is contained in:
Trenton H authored and GitHub committed 2026-10-12 11:10:50 -07:00
1 parent 9f2f809131
commit b195423f1d
6 files changed
+159 -8

No files matched your search

+40 -1
View File
@@ -12,6 +12,7 @@ from paperless_ai.embedding import _normalize_llm_index_text
from paperless_ai.embedding import build_llm_index_text
from paperless_ai.embedding import get_configured_model_name
from paperless_ai.embedding import get_embedding_model
from paperless_testing.factories import DocumentFactory
@pytest.fixture
@@ -46,6 +47,7 @@ def mock_document():
doc.correspondent.name = "Test Correspondent"
doc.archive_serial_number = "12345"
doc.content = "This is the document content."
doc.get_effective_content.return_value = "This is the document content."
cf1 = MagicMock(__str__=lambda x: "Value1")
cf1.field = MagicMock()
@@ -280,7 +282,7 @@ def test_build_llm_index_text(mock_document):
def test_build_llm_index_text_normalizes_ocr_punctuation_runs(mock_document):
mock_document.content = (
mock_document.get_effective_content.return_value = (
"Introduction ................................................ 7\n"
"Hardware Limitation ________________________________________ 9\n"
"Keep short punctuation like INV-100 and ellipses..."
@@ -294,6 +296,43 @@ def test_build_llm_index_text_normalizes_ocr_punctuation_runs(mock_document):
assert "ellipses..." in result
@pytest.mark.django_db
class TestBuildLlmIndexTextVersions:
"""A root document is indexed with its effective content, like in the search index."""
def test_root_uses_the_newest_versions_content(self) -> None:
"""
GIVEN:
- A root document with two versions
WHEN:
- The LLM index text is built for the root
THEN:
- It contains the newest version's content and not the others'
"""
root = DocumentFactory(content="stale text")
DocumentFactory(root_document=root, version_index=1, content="older text")
DocumentFactory(root_document=root, version_index=2, content="latest text")
text = build_llm_index_text(root)
assert "latest text" in text
assert "stale text" not in text
assert "older text" not in text
def test_root_without_versions_uses_its_own_content(self) -> None:
"""
GIVEN:
- A root document without versions
WHEN:
- The LLM index text is built for it
THEN:
- It contains the document's own content
"""
root = DocumentFactory(content="own text")
assert "own text" in build_llm_index_text(root)
def test_normalize_llm_index_text_collapses_ocr_leaders_without_joining_lines():
assert _normalize_llm_index_text("A........B\nC____D----E") == "A B\nC D E"