Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/everos/service/knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,11 +1138,11 @@ async def _base_retrieve(
async def _enrich_with_content(
candidates: list[Candidate],
) -> list[Candidate]:
"""Batch-fetch SQLite content and attach to candidate metadata.
"""Batch-fetch authoritative content and build safe rerank passages.

Acts as a no-reranker path: returns candidates in their original
order with ``content`` added to metadata for downstream hit
conversion.
``content`` remains the exact SQLite value for downstream API responses.
``rerank_content`` falls back through topic summary/name/id so providers
never receive an empty document.
"""
if not candidates:
return candidates
Expand All @@ -1151,10 +1151,26 @@ async def _enrich_with_content(
topics = await knowledge_topic_sqlite_repo.get_topics_by_ids(topic_ids)
content_map = {t.node_id: t.content for t in topics}

def _first_nonblank(*values: object) -> str:
for value in values:
text = str(value or "").strip()
if text:
return text
return "Knowledge topic"

return [
c.model_copy(
update={
"metadata": {**c.metadata, "content": content_map.get(c.id, "")},
"metadata": {
**c.metadata,
"content": str(content_map.get(c.id, "") or ""),
"rerank_content": _first_nonblank(
content_map.get(c.id, ""),
c.metadata.get("summary"),
c.metadata.get("topic_name"),
c.id,
),
},
}
)
for c in candidates
Expand Down Expand Up @@ -1250,7 +1266,7 @@ async def _retrieve(q: str, k: int) -> list[Candidate]:
recaller, where, method=method, query=q, vector=vector, limit=k
)

raw_rerank = build_rerank_fn(reranker, text_field="content")
raw_rerank = build_rerank_fn(reranker, text_field="rerank_content")

async def _rerank_with_enrich(
q: str, candidates: Sequence[Candidate]
Expand Down
125 changes: 125 additions & 0 deletions tests/unit/test_service/test_knowledge_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pytest
from everalgo.types import Candidate

from everos.component.rerank.protocol import RerankResult
from everos.component.utils.datetime import get_utc_now
from everos.core.errors import ConfigurationError
from everos.infra.persistence.sqlite.tables.knowledge import (
Expand All @@ -23,6 +24,8 @@
from everos.service.knowledge import (
DocumentContext,
SearchKnowledgeResult,
_enrich_with_content,
_run_category_pipeline,
compile_knowledge_where,
search_knowledge,
)
Expand Down Expand Up @@ -169,6 +172,128 @@ def test_accepts_valid_ids_with_at_plus(self) -> None:
assert "proj_1" in result


class TestKnowledgeRerankContent:
async def test_empty_sqlite_content_uses_summary_only_for_reranking(self) -> None:
candidate = _candidate(
node_id="n_empty_content",
summary="Non-empty topic summary for reranking.",
content="",
)
topic = MagicMock(node_id="n_empty_content", content="")

with patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[topic]),
):
enriched = await _enrich_with_content([candidate])

assert enriched[0].metadata["content"] == ""
assert enriched[0].metadata["rerank_content"] == (
"Non-empty topic summary for reranking."
)

async def test_blank_content_and_summary_fall_through_to_topic_name(self) -> None:
candidate = _candidate(
node_id="n_blank_summary",
summary=" \t ",
topic_name="Useful Topic Name",
)
topic = MagicMock(node_id="n_blank_summary", content=" \n ")

with patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[topic]),
):
enriched = await _enrich_with_content([candidate])

assert enriched[0].metadata["content"] == " \n "
assert enriched[0].metadata["rerank_content"] == "Useful Topic Name"

async def test_missing_row_and_blank_metadata_fall_back_to_candidate_id(
self,
) -> None:
candidate = _candidate(
node_id="n_stable_fallback",
summary=" ",
topic_name="\n",
)

with patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[]),
):
enriched = await _enrich_with_content([candidate])

assert enriched[0].metadata["content"] == ""
assert enriched[0].metadata["rerank_content"] == "n_stable_fallback"

async def test_all_blank_fields_use_nonempty_terminal_fallback(self) -> None:
candidate = _candidate(node_id="", summary=" ", topic_name="\n")

with patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[]),
):
enriched = await _enrich_with_content([candidate])

assert enriched[0].metadata["rerank_content"] == "Knowledge topic"

async def test_nonempty_sqlite_content_remains_authoritative(self) -> None:
candidate = _candidate(node_id="n_full_content", summary="Fallback summary")
topic = MagicMock(
node_id="n_full_content",
content="\n Authoritative body. \n",
)

with patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[topic]),
):
enriched = await _enrich_with_content([candidate])

assert enriched[0].metadata["content"] == "\n Authoritative body. \n"
assert enriched[0].metadata["rerank_content"] == "Authoritative body."

async def test_category_pipeline_sends_nonempty_rerank_passage(self) -> None:
candidate = _candidate(
node_id="n_provider_seam",
summary="Provider-safe topic summary.",
content="",
)
topic = MagicMock(node_id="n_provider_seam", content="")
recaller = AsyncMock()
recaller.sparse_recall = AsyncMock(return_value=[candidate])
reranker = AsyncMock()
reranker.rerank = AsyncMock(return_value=[RerankResult(0, 0.9)])
config = _mock_settings().knowledge.search

async def _run_facade(query, *, base_retrieve, rerank_fn, **_kwargs):
recalled = await base_retrieve(query, 10)
return await rerank_fn(query, recalled)

with (
patch(f"{_MOD}._build_recaller", return_value=recaller),
patch(
f"{_MOD}.knowledge_topic_sqlite_repo.get_topics_by_ids",
new=AsyncMock(return_value=[topic]),
),
patch("everalgo.rank.acategory_retrieve", new=_run_facade),
):
await _run_category_pipeline(
"query",
"app_id = 'default' AND project_id = 'default'",
method="keyword",
vector=[],
reranker=reranker,
config=config,
top_k=5,
)

documents = reranker.rerank.await_args.args[1]
assert documents == ["Provider-safe topic summary."]
assert all(document.strip() for document in documents)


# ── search_knowledge ─────────────────────────────────────────────────────────


Expand Down