compass-kb-validation 3.3.0__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/PKG-INFO +1 -1
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/__init__.py +1 -1
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/citation.py +50 -1
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/eval_enrich.py +86 -11
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/PKG-INFO +1 -1
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_citation.py +29 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_eval_enrich.py +26 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/LICENSE +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/README.md +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/__main__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/ask.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/baseline.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/chunk_coherence.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/cli.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/config/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/config_lint.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/content_inspect.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/corpus.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/corpus_health.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/diagnostics.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/embedding_drift.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/eval_interop.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/evidence.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/grounding.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/init_wizard.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/live_dashboard.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/mcp_server.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/models/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/notifications.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/pipeline/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/profiler.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/provenance.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/embedding.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/graph.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/source.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/storage.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/vectorstore.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/remediation.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/_report_css.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/_report_js.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/analysis.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/audit.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/export.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/report/shell.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/retrieval_analysis.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/text_probes.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/trend_tracker.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/utils/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/api_upload.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/base.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/base_sparql.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/cross_stage.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/graphdb_metadata.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/neptune_metadata.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/redis_chunks.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/registry.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/retrieval_quality.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/s3_storage.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/SOURCES.txt +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/dependency_links.txt +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/entry_points.txt +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/requires.txt +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass_kb_validation.egg-info/top_level.txt +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/discover_and_validate.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/ingestion_validation/__init__.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/profile_kb.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/pyproject.toml +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/run_validation.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/setup.cfg +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_alt_adapters.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_analysis_report.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_api_upload.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_ask.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_baseline.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_benchmark.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_check_catalog.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_cli.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_compat_shim.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_config.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_config_lint.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_content_inspect.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_corpus.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_corpus_health.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_coverage.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_cross_stage.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_diagnostics.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_discover_cli.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_discovery_parallel.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_embedding_drift.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_eval_interop.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_evidence.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_gate.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_graph_transport.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_graph_validators.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_grounding.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_init_wizard.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_manifest.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_mcp.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_migration.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_models.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_neptune_bugfixes2.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_notifications.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_pipeline.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_profiler.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_providers.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_redis_chunks.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_registry.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_remediation.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_reporting.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_retrieval_assertions.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_retrieval_quality.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_review_hardening.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_run_cli.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_sources.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_storage_validator.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_trends.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_universal_concerns.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_utils.py +0 -0
- {compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_vectorstore_filters.py +0 -0
|
@@ -377,7 +377,19 @@ def verify_level1(citations: list[Citation], eval_chunks: list[dict], cfg: Any)
|
|
|
377
377
|
v.terms_missing.append(cv.raw)
|
|
378
378
|
v._missing.append(cv)
|
|
379
379
|
if not cit.values:
|
|
380
|
-
|
|
380
|
+
# No numeric claim. If the claim carries distinctive entities (proper nouns / quoted
|
|
381
|
+
# phrases) and they are ALL present in the cited chunk, it is TERM_SUPPORTED - a
|
|
382
|
+
# weaker, lexical tier counted in a SEPARATE term-grounding rate, never the strict
|
|
383
|
+
# numeric one. Missing entities stay UNVERIFIABLE (lexical absence can be paraphrase).
|
|
384
|
+
ents = [] if cit.negated else _extract_entities(cit.claim)
|
|
385
|
+
if ents:
|
|
386
|
+
hay = _squash(_haystack(chunk, text_field))
|
|
387
|
+
v.entity_missing = [e for e in ents if _squash(e) not in hay]
|
|
388
|
+
v.entities_checked = True
|
|
389
|
+
v.l1_verdict, v.severity = (("TERM_SUPPORTED", "INFO") if not v.entity_missing
|
|
390
|
+
else ("UNVERIFIABLE", "INFO"))
|
|
391
|
+
else:
|
|
392
|
+
v.l1_verdict, v.severity = "UNVERIFIABLE", "INFO" # no quantitative claim to check
|
|
381
393
|
if cit.negated:
|
|
382
394
|
v.note = "claim is negated - lexical verification not applicable"
|
|
383
395
|
elif not v.terms_missing:
|
|
@@ -539,6 +551,7 @@ _ATTRIB = {
|
|
|
539
551
|
"citation_error": ("citation_error", "MEDIUM"), # right fact, wrong cited location
|
|
540
552
|
"fabricated_source": ("fabricated_source", "HIGH"),
|
|
541
553
|
"none": ("none", "INFO"),
|
|
554
|
+
"term_supported": ("term_supported", "INFO"), # distinctive entities present in cited chunk
|
|
542
555
|
"unverifiable": ("unverifiable", "INFO"), # no quantitative term; lexical check N/A
|
|
543
556
|
"unknown": ("unknown", "MEDIUM"), # a miss we could not attribute (no live KB)
|
|
544
557
|
}
|
|
@@ -558,6 +571,8 @@ def attribute(verdicts: list[CitationVerdict], l1_ran: bool) -> None:
|
|
|
558
571
|
key = "none"
|
|
559
572
|
elif l2 == "FABRICATED_SOURCE":
|
|
560
573
|
key = "fabricated_source" # cited a document that isn't in the KB
|
|
574
|
+
elif l1 == "TERM_SUPPORTED":
|
|
575
|
+
key = "term_supported" # distinctive entities present; weaker than numeric
|
|
561
576
|
elif l2 in _L2_UNVERIFIABLE:
|
|
562
577
|
key = "unverifiable" # source unresolved / too large / no numeric claim
|
|
563
578
|
elif l1 == "UNVERIFIABLE" and not l2:
|
|
@@ -650,6 +665,7 @@ def audit_citations(response: str, eval_chunks: Optional[list[dict]], adapter: A
|
|
|
650
665
|
summary = summarize(verdicts, response, l1_ran, l2_ran)
|
|
651
666
|
if l1_ran:
|
|
652
667
|
summary.update(context_utilization(citations, eval_chunks or [], cfg))
|
|
668
|
+
summary.update(text_utilization(response, eval_chunks or [], cfg))
|
|
653
669
|
# DEFECT-003 safety net: if the response clearly carries citation-like patterns but none
|
|
654
670
|
# were extracted, say so rather than reporting a silent total_citations=0 that reads as
|
|
655
671
|
# "no citations exist" (which was the DEFECT-001 failure mode).
|
|
@@ -680,6 +696,31 @@ def context_utilization(citations: list[Citation], eval_chunks: list[dict], cfg:
|
|
|
680
696
|
"context_utilization_pct": round(100.0 * used / total, 1)}
|
|
681
697
|
|
|
682
698
|
|
|
699
|
+
def _ngrams(text: str, n: int = 3) -> set:
|
|
700
|
+
toks = re.findall(r"[a-z0-9]+", (text or "").lower())
|
|
701
|
+
return {tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)} if len(toks) >= n else set()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def text_utilization(response: str, eval_chunks: list[dict], cfg: Any,
|
|
705
|
+
n: int = 3, min_overlap: float = 0.1) -> dict:
|
|
706
|
+
"""How much of the retrieved CONTENT actually appears in the response, by n-gram overlap -
|
|
707
|
+
distinct from ``context_utilization_pct`` (which measures whether a chunk's PAGE was cited).
|
|
708
|
+
A chunk counts as used when at least ``min_overlap`` of its n-grams recur in the response."""
|
|
709
|
+
text_field = getattr(cfg, "gt_chunk_text_field", "text")
|
|
710
|
+
total = len(eval_chunks or [])
|
|
711
|
+
if not total:
|
|
712
|
+
return {}
|
|
713
|
+
resp = _ngrams(response, n)
|
|
714
|
+
if not resp:
|
|
715
|
+
return {"text_utilization_pct": 0.0}
|
|
716
|
+
used = 0
|
|
717
|
+
for ch in eval_chunks:
|
|
718
|
+
cg = _ngrams(_haystack(ch, text_field), n)
|
|
719
|
+
if cg and len(cg & resp) / len(cg) >= min_overlap:
|
|
720
|
+
used += 1
|
|
721
|
+
return {"text_utilization_pct": round(100.0 * used / total, 1)}
|
|
722
|
+
|
|
723
|
+
|
|
683
724
|
def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_ran: bool) -> dict:
|
|
684
725
|
"""Aggregate signal for the audit: how many citations are sound, and the fault mix."""
|
|
685
726
|
total = len(verdicts)
|
|
@@ -693,6 +734,11 @@ def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_r
|
|
|
693
734
|
checkable = sound + fault_total
|
|
694
735
|
# Accuracy is over CHECKABLE citations only; unverifiable/unknown are excluded, not scored.
|
|
695
736
|
accuracy = round(100.0 * sound / checkable, 1) if checkable else None
|
|
737
|
+
# Term grounding: a SEPARATE, weaker tier for qualitative citations whose distinctive entities
|
|
738
|
+
# are present in the cited chunk. Never folded into the strict numeric rate above.
|
|
739
|
+
term_supported = sum(1 for v in verdicts if v.attribution == "term_supported")
|
|
740
|
+
term_checkable = sum(1 for v in verdicts if v.entities_checked)
|
|
741
|
+
term_rate = round(100.0 * term_supported / term_checkable, 1) if term_checkable else None
|
|
696
742
|
# Citation coverage: sentences with a checkable quantitative claim that carry NO citation.
|
|
697
743
|
sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", response or "") if s.strip()]
|
|
698
744
|
numeric = [s for s in sentences if extract_terms(s)]
|
|
@@ -704,6 +750,9 @@ def summarize(verdicts: list[CitationVerdict], response: str, l1_ran: bool, l2_r
|
|
|
704
750
|
"checkable": checkable,
|
|
705
751
|
"unverifiable": unverifiable,
|
|
706
752
|
"accuracy_pct": accuracy,
|
|
753
|
+
"term_supported": term_supported,
|
|
754
|
+
"term_checkable": term_checkable,
|
|
755
|
+
"term_grounding_rate_pct": term_rate,
|
|
707
756
|
"faults": faults,
|
|
708
757
|
"coverage_pct": coverage,
|
|
709
758
|
"uncited_numeric_claims": len(uncited),
|
|
@@ -21,10 +21,34 @@ Two capabilities:
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
import re
|
|
24
25
|
from typing import Any, Optional
|
|
25
26
|
|
|
26
27
|
from compass.eval_interop import get_path
|
|
27
28
|
|
|
29
|
+
# Minimal stop-word set for the lexical context-support signal (deterministic, no NLP lib).
|
|
30
|
+
_STOP = frozenset(
|
|
31
|
+
"the a an and or of to in for on with at by from is are was were be been being it its this that "
|
|
32
|
+
"these those as but if then than so we you they he she i our your their his her not no do does did "
|
|
33
|
+
"have has had will would can could may might should must about into over under between out up down".split())
|
|
34
|
+
_WORD_RE = re.compile(r"[a-z0-9][a-z0-9\-]{2,}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_chunks(record: dict, cfg: Any) -> list:
|
|
38
|
+
"""The case's retrieved chunks, via the configured path then common nested fallbacks."""
|
|
39
|
+
chunks = get_path(record, getattr(cfg, "gt_chunks_path", "context.chunks"))
|
|
40
|
+
if isinstance(chunks, list) and chunks:
|
|
41
|
+
return chunks
|
|
42
|
+
for alt in ("context.chunks", "chunks"):
|
|
43
|
+
v = get_path(record, alt)
|
|
44
|
+
if isinstance(v, list) and v:
|
|
45
|
+
return v
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _content_terms(text: str) -> set:
|
|
50
|
+
return {t for t in _WORD_RE.findall((text or "").lower()) if t not in _STOP}
|
|
51
|
+
|
|
28
52
|
# Deterministic signal codes COMPASS owns. KB-derived only - no answer-quality / guardrail / SQL.
|
|
29
53
|
GROUNDED, PARTIAL, UNGROUNDED = "COMPASS_GROUNDED", "COMPASS_PARTIAL_GROUND", "COMPASS_UNGROUNDED"
|
|
30
54
|
_FAULT_CODES = {
|
|
@@ -40,6 +64,7 @@ DEFAULTS = {
|
|
|
40
64
|
"context_waste_below_pct": 20.0,
|
|
41
65
|
"chunk_quality_floor": 0.5,
|
|
42
66
|
"uncited_numeric_max": 3,
|
|
67
|
+
"context_support_floor": 0.4, # lexical (weaker) proxy floor, only used when no citations
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
|
|
@@ -56,14 +81,8 @@ def _case_chunk_quality(record: dict, cfg: Any, min_chars: int) -> Optional[dict
|
|
|
56
81
|
"""Score the chunks THIS case was given, deterministically. None if no chunks are present."""
|
|
57
82
|
from compass.corpus_health import _chunk_quality
|
|
58
83
|
|
|
59
|
-
chunks =
|
|
60
|
-
if not
|
|
61
|
-
for alt in ("context.chunks", "chunks"):
|
|
62
|
-
v = get_path(record, alt)
|
|
63
|
-
if isinstance(v, list) and v:
|
|
64
|
-
chunks = v
|
|
65
|
-
break
|
|
66
|
-
if not isinstance(chunks, list) or not chunks:
|
|
84
|
+
chunks = _resolve_chunks(record, cfg)
|
|
85
|
+
if not chunks:
|
|
67
86
|
return None
|
|
68
87
|
text_field = getattr(cfg, "gt_chunk_text_field", "text") or "text"
|
|
69
88
|
scores, not_self_contained = [], 0
|
|
@@ -83,7 +102,46 @@ def _case_chunk_quality(record: dict, cfg: Any, min_chars: int) -> Optional[dict
|
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
|
|
86
|
-
def
|
|
105
|
+
def _context_support(record: dict, cfg: Any, *, min_overlap: float = 0.25) -> Optional[dict]:
|
|
106
|
+
"""A SEPARATE, weaker lexical signal - never the citation grounding rate. The share of
|
|
107
|
+
response sentences whose content terms are echoed by the retrieved context chunks. It
|
|
108
|
+
estimates support for responses that may carry NO citations, but it is only a lexical proxy,
|
|
109
|
+
and is computed ONLY when the case actually has retrieved chunks (a refusal / non-retrieval
|
|
110
|
+
case has none, so it never fabricates a misleading 'grounding' number)."""
|
|
111
|
+
chunks = _resolve_chunks(record, cfg)
|
|
112
|
+
if not chunks:
|
|
113
|
+
return None
|
|
114
|
+
from compass.grounding import _resolve_field
|
|
115
|
+
|
|
116
|
+
r_val, _, _ = _resolve_field(record, getattr(cfg, "gt_response_path", "response"), "response")
|
|
117
|
+
response = str(r_val or "").strip()
|
|
118
|
+
if not response:
|
|
119
|
+
return None
|
|
120
|
+
text_field = getattr(cfg, "gt_chunk_text_field", "text") or "text"
|
|
121
|
+
ctx_terms: set = set()
|
|
122
|
+
for c in chunks:
|
|
123
|
+
if isinstance(c, dict):
|
|
124
|
+
ctx_terms |= _content_terms(c.get(text_field) or c.get("text") or "")
|
|
125
|
+
if not ctx_terms:
|
|
126
|
+
return None
|
|
127
|
+
scored = supported = 0
|
|
128
|
+
for sent in re.split(r"(?<=[.!?])\s+", response):
|
|
129
|
+
terms = _content_terms(sent)
|
|
130
|
+
if len(terms) < 3: # too short to judge overlap meaningfully
|
|
131
|
+
continue
|
|
132
|
+
scored += 1
|
|
133
|
+
if len(terms & ctx_terms) / len(terms) >= min_overlap:
|
|
134
|
+
supported += 1
|
|
135
|
+
if not scored:
|
|
136
|
+
return None
|
|
137
|
+
return {"context_support_rate": round(supported / scored, 3),
|
|
138
|
+
"sentences_scored": scored, "supported": supported,
|
|
139
|
+
"method": "lexical_term_overlap",
|
|
140
|
+
"note": "Weaker lexical proxy for responses without citations - NOT the citation grounding rate."}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _derive_signals(grounding: dict, chunk_quality: Optional[dict],
|
|
144
|
+
context_support: Optional[dict], thr: dict) -> list[dict]:
|
|
87
145
|
"""Turn the deterministic measurements into COMPASS_* signal codes. Every code is
|
|
88
146
|
check-backed; there is no heuristic guessing here."""
|
|
89
147
|
signals: list[dict] = []
|
|
@@ -113,6 +171,14 @@ def _derive_signals(grounding: dict, chunk_quality: Optional[dict], thr: dict) -
|
|
|
113
171
|
if (grounding.get("uncited_numeric_claims") or 0) > thr["uncited_numeric_max"]:
|
|
114
172
|
signals.append({"code": "COMPASS_UNCITED_NUMERIC", "severity": "MEDIUM",
|
|
115
173
|
"detail": f"{grounding['uncited_numeric_claims']} numeric claim(s) carry no citation"})
|
|
174
|
+
# Separate, weaker lexical signal - only when there is no strong citation grounding to rely on,
|
|
175
|
+
# so it complements (never overrides) the citation grounding rate.
|
|
176
|
+
if context_support and rate is None:
|
|
177
|
+
csr = context_support["context_support_rate"]
|
|
178
|
+
if csr < thr["context_support_floor"]:
|
|
179
|
+
signals.append({"code": "COMPASS_LOW_CONTEXT_SUPPORT", "severity": "LOW",
|
|
180
|
+
"detail": f"only {csr:.0%} of response sentences are lexically echoed "
|
|
181
|
+
f"by the retrieved context (weak proxy; no verifiable citations)"})
|
|
116
182
|
return signals
|
|
117
183
|
|
|
118
184
|
|
|
@@ -128,13 +194,19 @@ def enrich_case(record: dict, cfg: Any, adapter: Any = None, *,
|
|
|
128
194
|
thr = {**DEFAULTS, **(thresholds or {})}
|
|
129
195
|
res = run_audit(record, adapter, cfg)
|
|
130
196
|
summary = res.citations.get("summary", {}) if isinstance(res.citations, dict) else {}
|
|
197
|
+
tr = summary.get("term_grounding_rate_pct")
|
|
131
198
|
grounding = {
|
|
132
199
|
"citation_grounding_rate": _citation_grounding_rate(summary),
|
|
133
200
|
"total_citations": summary.get("total_citations", 0),
|
|
134
201
|
"checkable": summary.get("checkable", 0),
|
|
135
202
|
"sound": summary.get("sound", 0),
|
|
203
|
+
# Separate, weaker tier: distinctive entities present in the cited chunk (not numeric).
|
|
204
|
+
"term_grounding_rate": (tr / 100.0 if tr is not None else None),
|
|
205
|
+
"term_supported": summary.get("term_supported", 0),
|
|
206
|
+
"term_checkable": summary.get("term_checkable", 0),
|
|
136
207
|
"faults": summary.get("faults", {}),
|
|
137
|
-
"context_utilization_pct": summary.get("context_utilization_pct"),
|
|
208
|
+
"context_utilization_pct": summary.get("context_utilization_pct"), # cited-page coverage
|
|
209
|
+
"text_utilization_pct": summary.get("text_utilization_pct"), # actual content overlap
|
|
138
210
|
"uncited_numeric_claims": summary.get("uncited_numeric_claims", 0),
|
|
139
211
|
"context_empty": res.context_empty,
|
|
140
212
|
"levels": summary.get("levels", "none"),
|
|
@@ -142,10 +214,13 @@ def enrich_case(record: dict, cfg: Any, adapter: Any = None, *,
|
|
|
142
214
|
if summary.get("diagnostic"):
|
|
143
215
|
grounding["diagnostic"] = summary["diagnostic"]
|
|
144
216
|
chunk_quality = _case_chunk_quality(record, cfg, min_chars)
|
|
217
|
+
context_support = _context_support(record, cfg)
|
|
145
218
|
block: dict = {"grounding": grounding, "method": "compass_deterministic"}
|
|
146
219
|
if chunk_quality:
|
|
147
220
|
block["chunk_quality"] = chunk_quality
|
|
148
|
-
|
|
221
|
+
if context_support:
|
|
222
|
+
block["context_support"] = context_support
|
|
223
|
+
block["signals"] = _derive_signals(grounding, chunk_quality, context_support, thr)
|
|
149
224
|
return block
|
|
150
225
|
|
|
151
226
|
|
|
@@ -472,3 +472,32 @@ def test_diagnostic_when_pattern_matches_but_nothing_extracted():
|
|
|
472
472
|
res = citation.audit_citations("(SOURCE, 2026, p. 1)", None, None, cfg)
|
|
473
473
|
assert res["summary"]["total_citations"] == 0
|
|
474
474
|
assert "matched by regex but 0 were extracted" in res["summary"].get("diagnostic", "")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_term_supported_is_a_separate_tier_not_the_strict_sound_rate():
|
|
478
|
+
# Problem 1: a qualitative citation whose distinctive entity is present in the cited chunk is
|
|
479
|
+
# TERM_SUPPORTED - counted in a SEPARATE term rate, never inflating the strict numeric rate.
|
|
480
|
+
from types import SimpleNamespace
|
|
481
|
+
cfg = SimpleNamespace(citation_patterns=[], citation_page_tolerance=0, citation_verify_entities=False,
|
|
482
|
+
gt_chunk_doc_field="document_title", gt_chunk_page_field="page_start",
|
|
483
|
+
gt_chunk_text_field="text")
|
|
484
|
+
resp = "The Federal Reserve signaled patience. (Doc, 2026, p. 1) Markets calmed."
|
|
485
|
+
chunks = [{"text": "The Federal Reserve signaled patience on rates.", "document_title": "Doc", "page_start": 1}]
|
|
486
|
+
s = citation.audit_citations(resp, chunks, None, cfg)["summary"]
|
|
487
|
+
assert s["term_supported"] == 1 and s["term_grounding_rate_pct"] == 100.0
|
|
488
|
+
assert s["sound"] == 0 and s["checkable"] == 0 # strict numeric rate untouched
|
|
489
|
+
# entity absent -> stays unverifiable, never a fault (lexical absence can be paraphrase)
|
|
490
|
+
s2 = citation.audit_citations(resp, [{"text": "Unrelated weather note.", "document_title": "Doc",
|
|
491
|
+
"page_start": 1}], None, cfg)["summary"]
|
|
492
|
+
assert s2["term_supported"] == 0 and not s2["faults"]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_text_utilization_measures_content_overlap_distinct_from_page_coverage():
|
|
496
|
+
# Problem 4: text_utilization_pct is n-gram content overlap, separate from context_utilization
|
|
497
|
+
# (which is cited-page coverage).
|
|
498
|
+
from types import SimpleNamespace
|
|
499
|
+
cfg = SimpleNamespace(gt_chunk_text_field="text")
|
|
500
|
+
chunks = [{"text": "quarterly revenue increased sharply on strong product demand"}]
|
|
501
|
+
hi = citation.text_utilization("quarterly revenue increased sharply on strong product demand", chunks, cfg)
|
|
502
|
+
lo = citation.text_utilization("completely unrelated sentence about the weather today", chunks, cfg)
|
|
503
|
+
assert hi["text_utilization_pct"] == 100.0 and lo["text_utilization_pct"] == 0.0
|
|
@@ -107,3 +107,29 @@ def test_extract_judge_scores_handles_varied_shapes():
|
|
|
107
107
|
assert extract_judge_scores(c3, "Answer Relevance") == {"judge_x": 0.9}
|
|
108
108
|
# metric absent -> empty
|
|
109
109
|
assert extract_judge_scores({"scores": {"other": {"j": 0.5}}}, "groundedness") == {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_context_support_is_separate_weak_signal_and_scoped_to_retrieval_cases():
|
|
113
|
+
# Problem 3: a citation-free response gets a SEPARATE lexical context_support (never the
|
|
114
|
+
# citation grounding rate), and only when the case actually carries retrieved chunks.
|
|
115
|
+
cfg = Settings().eval
|
|
116
|
+
echoed = [{"input": "q", "output": "The economy is slowing and inflation is easing gradually.",
|
|
117
|
+
"context": {"chunks": [{"text": "The economy is slowing while inflation eases gradually.",
|
|
118
|
+
"document_title": "D", "page_start": 1}]}}]
|
|
119
|
+
b = enrich_cases(echoed, cfg)[0][0]["compass"]
|
|
120
|
+
assert b["grounding"]["citation_grounding_rate"] is None # no citation -> strict rate stays None
|
|
121
|
+
assert b["context_support"]["context_support_rate"] == 1.0 # separate lexical proxy
|
|
122
|
+
assert "NOT the citation grounding rate" in b["context_support"]["note"]
|
|
123
|
+
# a refusal / non-retrieval case (no chunks) gets NO context_support - no misleading score
|
|
124
|
+
refusal = [{"input": "do X", "output": "I can't help with that.", "context": {}}]
|
|
125
|
+
assert "context_support" not in enrich_cases(refusal, cfg)[0][0]["compass"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_term_grounding_rate_is_separate_from_citation_rate():
|
|
129
|
+
cfg = Settings().eval
|
|
130
|
+
recs = [{"input": "q", "output": "The Federal Reserve held rates steady. (Doc, 2026, p. 1) End.",
|
|
131
|
+
"context": {"chunks": [{"text": "The Federal Reserve held rates steady this month.",
|
|
132
|
+
"document_title": "Doc", "page_start": 1}]}}]
|
|
133
|
+
g = enrich_cases(recs, cfg)[0][0]["compass"]["grounding"]
|
|
134
|
+
assert g["citation_grounding_rate"] is None # no numeric claim -> strict rate None
|
|
135
|
+
assert g["term_grounding_rate"] == 1.0 # but the entity is supported (separate tier)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/providers/vectorstore.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/api_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/base_sparql.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/cross_stage.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/graphdb_metadata.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/neptune_metadata.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/redis_chunks.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/retrieval_quality.py
RENAMED
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/compass/validators/s3_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/ingestion_validation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_discovery_parallel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_retrieval_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_universal_concerns.py
RENAMED
|
File without changes
|
|
File without changes
|
{compass_kb_validation-3.3.0 → compass_kb_validation-3.4.0}/tests/test_vectorstore_filters.py
RENAMED
|
File without changes
|