Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

370 lines
14 KiB
Python

"""Deterministic, LLM-free helpers for building reference (Evidence) blocks.
Evidence is grounded in the generated answer, not in whatever happened to be
retrieved before the LLM was called:
- ``format_chunk_references`` builds an Evidence block from retrieved vector
payloads, keeping only chunks that share significant terms with the answer
(the ``RAG_COMPLETION`` / chunk path, where candidates are the chunks the
LLM actually read).
- ``build_answer_grounded_chunk_references`` runs the answer text as a vector
query against the chunk index and formats the results (the graph completion
path, where the LLM context is not chunk-shaped).
- ``append_chunk_evidence`` / ``append_answer_grounded_evidence`` apply the
above to a list of completions, one Evidence block per string completion.
All helpers are pure with respect to the LLM (no model calls) so they can be
unit tested in isolation. All return ``""`` (or the completions unchanged)
when there is nothing usable, and never raise on backend failures.
"""
import re
from typing import Any, List, Optional, Set, Tuple
from cognee.shared.logging_utils import get_logger
logger = get_logger("references")
# Header emitted on its own line above the bullets. Kept here so both helpers
# and the wiring code agree on the exact literal.
EVIDENCE_HEADER = "Evidence:"
# Maximum length of a rendered text snippet (characters) before truncation.
_SNIPPET_MAX_CHARS = 160
# Hard upper bound on bullets regardless of the requested limit (3-5 range).
_MAX_BULLETS = 5
_MIN_LIMIT = 3
# Vector collection holding document chunks (same one ChunksRetriever queries).
_CHUNK_COLLECTION = "DocumentChunk_text"
# How many vector candidates to fetch before answer-overlap filtering.
_CANDIDATE_POOL = 10
# Common English words excluded from answer/chunk term overlap scoring.
_STOPWORDS = frozenset(
"""
a about above after again all also an and any are as at be because been
before being below between both but by can did do does doing down during
each few for from further had has have having he her here hers him his how
i if in into is it its just me more most my no nor not of off on once only
or other our ours out over own same she should so some such than that the
their theirs them then there these they this those through to too under
until up very was we were what when where which while who whom why will
with you your yours
""".split()
)
def _clamp_limit(limit: int) -> int:
"""Clamp the requested bullet limit into the contracted 3-5 range."""
if limit < _MIN_LIMIT:
return _MIN_LIMIT
if limit > _MAX_BULLETS:
return _MAX_BULLETS
return limit
def _clean_str(value: Any) -> Optional[str]:
"""Return a stripped string, or None if the value is unusable.
Missing, null, non-string, or empty/whitespace-only values are treated as
unusable (the common state for data indexed before reference fields
existed).
"""
if value is None:
return None
if not isinstance(value, str):
# Numbers etc. are not valid document names / text; reject defensively.
return None
stripped = value.strip()
return stripped or None
def _snippet(text: str) -> str:
"""Collapse whitespace and truncate text into a short snippet."""
collapsed = " ".join(text.split())
if len(collapsed) <= _SNIPPET_MAX_CHARS:
return collapsed
return collapsed[: _SNIPPET_MAX_CHARS - 1].rstrip() + "…"
def _chunk_number(payload: dict) -> Optional[int]:
"""Resolve the 1-based display number from payload.
Prefers an explicit ``chunk_number`` if present; otherwise derives it from
the 0-based ``chunk_index`` as ``chunk_index + 1``. Returns None when no
usable index information is present.
"""
chunk_number = payload.get("chunk_number")
if isinstance(chunk_number, bool): # guard: bool is an int subclass
chunk_number = None
if isinstance(chunk_number, int) and chunk_number > 0:
return chunk_number
chunk_index = payload.get("chunk_index")
if isinstance(chunk_index, bool):
chunk_index = None
if isinstance(chunk_index, int) and chunk_index >= 0:
return chunk_index + 1
return None
def _get_payload(obj: Any) -> Optional[dict]:
"""Extract a payload dict from a retrieved object.
Retrieved objects are ``ScoredResult`` instances exposing ``.payload`` as a
dict, but we also tolerate a raw dict or any object carrying a ``payload``
attribute so the helper stays unit-testable without constructing a full
``ScoredResult``.
"""
if isinstance(obj, dict):
# Either the object IS the payload, or it wraps one under "payload".
inner = obj.get("payload")
if isinstance(inner, dict):
return inner
return obj
payload = getattr(obj, "payload", None)
if isinstance(payload, dict):
return payload
return None
def _provenance_suffix(data_id: Optional[str], chunk_id: Optional[str]) -> str:
"""Render a '(data_id: …, chunk_id: …)' annotation for whichever ids exist.
Lets a reader map the citation back to the ingested data item and the exact
cited chunk, instead of only a (possibly auto-generated) document name and a
positional chunk number.
"""
parts = []
if data_id:
parts.append(f"data_id: {data_id}")
if chunk_id:
parts.append(f"chunk_id: {chunk_id}")
return f" ({', '.join(parts)})" if parts else ""
def _chunk_id(obj: Any, payload: dict) -> Optional[str]:
"""Resolve a stable chunk id for dedup, preferring the object id."""
obj_id = getattr(obj, "id", None)
if obj_id is not None:
return str(obj_id)
payload_id = payload.get("id")
if payload_id is not None:
return str(payload_id)
# No stable id: fall back to (document_name, chunk_number) signature so we
# still avoid duplicate bullets, computed by the caller from the payload.
return None
def _significant_terms(text: str) -> Set[str]:
"""Lowercased alphanumeric terms of an answer, minus stopwords and stubs."""
tokens = re.findall(r"[a-z0-9]+", text.lower())
return {token for token in tokens if len(token) >= 3 and token not in _STOPWORDS}
def format_chunk_references(
retrieved_objects: Any, answer: Optional[str] = None, limit: int = 5
) -> str:
"""Build an Evidence block from retrieved vector payloads, grounded in the answer.
Reads ``payload["document_name"]``, ``payload["chunk_number"]`` (falling back
to ``payload["chunk_index"] + 1``), and ``payload["text"]`` from each
retrieved object. Entries missing usable document name or chunk-number
metadata are skipped. Results are deduplicated by chunk id and capped at
3-5 bullets.
When ``answer`` is provided, candidates that share no significant terms
with the answer are dropped and the remainder is ranked by term overlap,
so bullets reflect answer provenance rather than retrieval order. An
answer with no significant terms cannot be grounded and yields ``""``.
Parameters
----------
retrieved_objects:
An iterable of retrieved vector results (``ScoredResult``-like objects
exposing a ``.payload`` dict), or raw payload dicts.
answer:
The generated answer text used to filter and rank candidates. When
None, candidates keep their retrieval order unfiltered.
limit:
Desired maximum number of bullets, clamped into the 3-5 range.
Returns
-------
str
A multi-line Evidence block prefixed by an ``Evidence:`` header, or an
empty string when nothing usable was found.
"""
if not retrieved_objects:
return ""
try:
iterator = list(retrieved_objects)
except TypeError:
return ""
answer_terms: Optional[Set[str]] = None
if answer is not None:
answer_terms = _significant_terms(answer)
if not answer_terms:
# Nothing to ground the citation in (e.g. "Yes."): omit Evidence
# rather than presenting unverifiable retrieval order as provenance.
return ""
# (overlap_score, document_name, number, text, data_id, chunk_id) per candidate.
candidates: List[Tuple[int, str, int, str, Optional[str], Optional[str]]] = []
seen: set = set()
for obj in iterator:
payload = _get_payload(obj)
if payload is None:
continue
document_name = _clean_str(payload.get("document_name"))
number = _chunk_number(payload)
text = _clean_str(payload.get("text"))
# Document name and a chunk number are both required to ground the
# citation; text is required for a meaningful snippet.
if document_name is None or number is None or text is None:
continue
chunk_id = _chunk_id(obj, payload)
# document_id == the ingested Data item's id (cognify sets
# Document.id = data.id), i.e. the dataId a caller needs to map a
# citation back to the document they ingested.
data_id = _clean_str(payload.get("document_id"))
dedup_key = chunk_id or f"{document_name}#{number}"
if dedup_key in seen:
continue
seen.add(dedup_key)
score = 0
if answer_terms is not None:
chunk_terms = set(re.findall(r"[a-z0-9]+", text.lower()))
score = len(answer_terms & chunk_terms)
if score == 0:
# No term from the answer appears in this chunk: it is almost
# certainly not a source of the answer.
continue
candidates.append((score, document_name, number, text, data_id, chunk_id))
if not candidates:
return ""
if answer_terms is not None:
# Stable sort: highest answer overlap first, retrieval order as tiebreak.
candidates.sort(key=lambda candidate: -candidate[0])
max_bullets = _clamp_limit(limit)
bullets = [
f"- chunk {number} of document {document_name}"
f'{_provenance_suffix(data_id, chunk_id)}: "{_snippet(text)}"'
for _, document_name, number, text, data_id, chunk_id in candidates[:max_bullets]
]
return EVIDENCE_HEADER + "\n" + "\n".join(bullets)
async def build_answer_grounded_chunk_references(
answer: str, vector_engine: Any, limit: int = 5
) -> str:
"""Build an Evidence block by running the answer as a vector query over chunks.
This grounds Evidence in the answer text itself, independent of how the
original retrieval was done (graph traversal, triplets, ...): the answer is
embedded once and matched against the existing chunk index, then candidates
are additionally filtered by term overlap with the answer.
Never raises: a missing collection or backend failure degrades to ``""``.
Parameters
----------
answer:
The generated answer text to ground.
vector_engine:
A vector engine exposing ``search(collection, query, limit, include_payload)``.
limit:
Desired maximum number of bullets, clamped into the 3-5 range.
Returns
-------
str
A multi-line Evidence block prefixed by an ``Evidence:`` header, or an
empty string when nothing usable was found or the search failed.
"""
cleaned_answer = _clean_str(answer)
if cleaned_answer is None or vector_engine is None:
return ""
try:
found_chunks = await vector_engine.search(
_CHUNK_COLLECTION,
cleaned_answer,
limit=_CANDIDATE_POOL,
include_payload=True,
)
except Exception as error:
logger.debug(f"Answer-grounded chunk search failed: {error}")
return ""
return format_chunk_references(found_chunks, answer=cleaned_answer, limit=limit)
def append_chunk_evidence(
completions: List[Any], retrieved_objects: Any, enabled: bool
) -> List[Any]:
"""Append an answer-grounded chunk Evidence block to string completions.
Each string completion gets its own Evidence block, filtered and ranked by
that completion's text against the retrieved candidates. Non-string
completions (structured response models) are never touched, and an empty
Evidence block leaves the completion unchanged.
"""
if not enabled:
return completions
appended: List[Any] = []
for completion in completions:
if not isinstance(completion, str):
appended.append(completion)
continue
evidence = format_chunk_references(retrieved_objects, answer=completion)
appended.append(f"{completion}\n\n{evidence}" if evidence else completion)
return appended
async def append_answer_grounded_evidence(completions: List[Any], enabled: bool) -> List[Any]:
"""Append an answer-grounded Evidence block to string completions.
Each string completion is run as a vector query against the chunk index
(see :func:`build_answer_grounded_chunk_references`). Non-string completions
are never touched; any backend failure degrades to no Evidence.
"""
if not enabled:
return completions
try:
from cognee.infrastructure.databases.vector import get_vector_engine_async
vector_engine = await get_vector_engine_async()
except Exception as error:
logger.debug(f"Unable to obtain vector engine for references: {error}")
return completions
appended: List[Any] = []
for completion in completions:
if not isinstance(completion, str):
appended.append(completion)
continue
evidence = await build_answer_grounded_chunk_references(completion, vector_engine)
appended.append(f"{completion}\n\n{evidence}" if evidence else completion)
return appended